feat: use 60min volume, add KRW P&L log, relax re-entry after win

- strategy: replace daily volume check with 60-min candle volume
  (daily volume at 5am is tiny -> BTC/ETH never matched; now uses
  last 1h candle vs previous 23h avg × 2)
- trader: log actual KRW net profit and fee on every sell
- trader: skip re-entry +1% block when last trade was a win
  (allow re-entry on new trend signal even below last sell price)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-01 05:38:37 +09:00
parent d2a5c3ae9e
commit 5df56a933e
2 changed files with 43 additions and 20 deletions

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import logging import logging
import os import os
import pyupbit
from .market import get_current_price, get_ohlcv from .market import get_current_price, get_ohlcv
from .price_db import get_price_n_hours_ago from .price_db import get_price_n_hours_ago
@@ -46,27 +47,41 @@ def check_trend(ticker: str) -> bool:
def check_momentum(ticker: str) -> bool: def check_momentum(ticker: str) -> bool:
"""모멘텀 조건: 현재가 > MA20 AND 오늘 거래량 > 20일 평균 × 2.""" """모멘텀 조건: 현재가 > MA20(일봉) AND 최근 1h 거래량 > 24h 평균 × 2 (60분봉 기준).
df = get_ohlcv(ticker, count=MA_PERIOD + 1)
if df is None or len(df) < MA_PERIOD + 1: 일봉 거래량은 오전에 항상 미달하므로 60분봉으로 교체.
"""
# MA20: 일봉 기준
df_daily = get_ohlcv(ticker, count=MA_PERIOD + 1)
if df_daily is None or len(df_daily) < MA_PERIOD + 1:
return False return False
ma = df["close"].iloc[-MA_PERIOD:].mean() ma = df_daily["close"].iloc[-MA_PERIOD:].mean()
avg_vol = df["volume"].iloc[:-1].mean()
today_vol = df["volume"].iloc[-1]
current = get_current_price(ticker) current = get_current_price(ticker)
if current is None: if current is None:
return False return False
price_ok = current > ma price_ok = current > ma
vol_ok = today_vol > avg_vol * VOLUME_MULTIPLIER if not price_ok:
result = price_ok and vol_ok return False
# 거래량: 60분봉 기준 (최근 1h vs 이전 24h 평균)
try:
df_hour = pyupbit.get_ohlcv(ticker, interval="minute60", count=26)
except Exception:
return False
if df_hour is None or len(df_hour) < 5:
return False
recent_vol = df_hour["volume"].iloc[-2] # 직전 완성된 1h 봉
avg_vol_1h = df_hour["volume"].iloc[-25:-2].mean() # 이전 23h 평균
vol_ok = avg_vol_1h > 0 and recent_vol > avg_vol_1h * VOLUME_MULTIPLIER
result = price_ok and vol_ok
if result: if result:
logger.debug( logger.debug(
f"[모멘텀] {ticker} 현재={current:,.0f} MA20={ma:,.0f} " f"[모멘텀] {ticker} 현재={current:,.0f} MA20={ma:,.0f} "
f"거래량={today_vol:.0f} 평균={avg_vol:.0f}" f"1h거래량={recent_vol:.0f} 평균={avg_vol_1h:.0f}"
) )
return result return result

View File

@@ -198,7 +198,11 @@ def buy(ticker: str) -> bool:
return False return False
# 직전 매도가 +1% 이상일 때만 재진입 (손절 직후 역방향 재매수 방지) # 직전 매도가 +1% 이상일 때만 재진입 (손절 직후 역방향 재매수 방지)
# 단, 직전 거래가 수익(승)이었으면 이 필터 스킵 — 다시 상승 시 재진입 허용
if ticker in _last_sell_prices: if ticker in _last_sell_prices:
hist = _get_history(ticker)
last_was_win = bool(hist[-1]) if hist else False
if not last_was_win:
current_check = pyupbit.get_current_price(ticker) current_check = pyupbit.get_current_price(ticker)
last_sell = _last_sell_prices[ticker] last_sell = _last_sell_prices[ticker]
threshold = last_sell * 1.01 threshold = last_sell * 1.01
@@ -299,9 +303,13 @@ def sell(ticker: str, reason: str = "") -> bool:
current = pyupbit.get_current_price(ticker) current = pyupbit.get_current_price(ticker)
pnl = (current - pos["buy_price"]) / pos["buy_price"] * 100 if current else 0.0 pnl = (current - pos["buy_price"]) / pos["buy_price"] * 100 if current else 0.0
# 실제 KRW 손익 및 수수료 계산 (업비트 수수료 0.05% 양방향)
sell_value = (current or pos["buy_price"]) * actual_amount
fee = pos["invested_krw"] * 0.0005 + sell_value * 0.0005
krw_profit = sell_value - pos["invested_krw"] - fee
logger.info( logger.info(
f"[매도] {ticker} @ {current:,.0f}원 | " f"[매도] {ticker} @ {current:,.0f}원 | "
f"수익률={pnl:+.1f}% | 사유={reason}" f"수익률={pnl:+.1f}% | 순익={krw_profit:+,.0f}원 (수수료 {fee:,.0f}원) | 사유={reason}"
) )
notify_sell(ticker, current, pnl, reason) notify_sell(ticker, current, pnl, reason)
if current: if current: