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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user