diff --git a/core/strategy.py b/core/strategy.py index 7f57b0f..a1d0ff4 100644 --- a/core/strategy.py +++ b/core/strategy.py @@ -5,6 +5,7 @@ from __future__ import annotations import logging import os +import pyupbit from .market import get_current_price, get_ohlcv 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: - """모멘텀 조건: 현재가 > MA20 AND 오늘 거래량 > 20일 평균 × 2.""" - df = get_ohlcv(ticker, count=MA_PERIOD + 1) - if df is None or len(df) < MA_PERIOD + 1: + """모멘텀 조건: 현재가 > MA20(일봉) AND 최근 1h 거래량 > 24h 평균 × 2 (60분봉 기준). + + 일봉 거래량은 오전에 항상 미달하므로 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 - ma = df["close"].iloc[-MA_PERIOD:].mean() - avg_vol = df["volume"].iloc[:-1].mean() - today_vol = df["volume"].iloc[-1] + ma = df_daily["close"].iloc[-MA_PERIOD:].mean() current = get_current_price(ticker) - if current is None: return False price_ok = current > ma - vol_ok = today_vol > avg_vol * VOLUME_MULTIPLIER - result = price_ok and vol_ok + if not price_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: logger.debug( 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 diff --git a/core/trader.py b/core/trader.py index 73a866a..fa81a24 100644 --- a/core/trader.py +++ b/core/trader.py @@ -198,16 +198,20 @@ def buy(ticker: str) -> bool: return False # 직전 매도가 +1% 이상일 때만 재진입 (손절 직후 역방향 재매수 방지) + # 단, 직전 거래가 수익(승)이었으면 이 필터 스킵 — 다시 상승 시 재진입 허용 if ticker in _last_sell_prices: - current_check = pyupbit.get_current_price(ticker) - last_sell = _last_sell_prices[ticker] - threshold = last_sell * 1.01 - if current_check and current_check < threshold: - logger.info( - f"[재매수 차단] {ticker} 현재={current_check:,.2f} < " - f"직전매도+1%={threshold:,.2f} → 상승 흐름 미확인" - ) - return False + 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) + last_sell = _last_sell_prices[ticker] + threshold = last_sell * 1.01 + if current_check and current_check < threshold: + logger.info( + f"[재매수 차단] {ticker} 현재={current_check:,.2f} < " + f"직전매도+1%={threshold:,.2f} → 상승 흐름 미확인" + ) + return False # Walk-forward 필터: 직전 WF_WINDOW건 승률이 낮으면 진입 차단 if WF_MIN_WIN_RATE > 0: @@ -299,9 +303,13 @@ def sell(ticker: str, reason: str = "") -> bool: current = pyupbit.get_current_price(ticker) 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( f"[매도] {ticker} @ {current:,.0f}원 | " - f"수익률={pnl:+.1f}% | 사유={reason}" + f"수익률={pnl:+.1f}% | 순익={krw_profit:+,.0f}원 (수수료 {fee:,.0f}원) | 사유={reason}" ) notify_sell(ticker, current, pnl, reason) if current: