Files
ai-trade/services/trade-monitor/tests/test_conditions_sell.py
gahusb 5dbb11ac83 fix(trade-monitor): sell_climax holdings_intel 정합
BE 회신(holdings_intel.py:109-118)에 맞춰 반전 기준을
price<day_open → price<day_high×climax_close_pct(윗꼬리)로 변경.
- kis_client.get_quote에 day_high(stck_hgpr) 추가
- monitor._build_ctx가 day_high를 ctx로 전달
- climax_vol_x·climax_close_pct를 monitor-set exit_params에서 읽기
  (fallback: TM_CLIMAX_VOL_MULT/0.97)
- 테스트 36/36 (climax exit_params 2건 추가)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N83vbXEA8h83GMXQcg8fxD
2026-07-03 11:15:27 +09:00

90 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""evaluate_sell — 5개 매도 조건 경계."""
from conditions import evaluate_sell
EXIT = {"stop_pct": 0.08, "take_pct": 0.25, "trailing_pct": 0.10,
"climax_vol_x": 3.0, "climax_close_pct": 0.97}
def _ctx(**over):
base = dict(
ticker="000660", name="SK하이닉스", price=100.0, day_open=100.0,
day_high=100.0, today_volume=100.0, closes=[100.0] * 60,
highs=[100.0] * 60, lows=[100.0] * 60, volumes=[100.0] * 60,
avg_price=100.0, qty=10, holding_high=100.0, climax_vol_mult=3.0,
)
base.update(over)
return base
def _c(firing):
return {f["condition"] for f in firing}
def test_stop_loss_fires():
ctx = _ctx(price=90.0, avg_price=100.0) # -10% <= -8%
assert "sell_stop_loss" in _c(evaluate_sell(ctx, EXIT))
def test_stop_loss_skips_above_threshold():
ctx = _ctx(price=95.0, avg_price=100.0) # -5% > -8%
assert "sell_stop_loss" not in _c(evaluate_sell(ctx, EXIT))
def test_take_profit_fires():
ctx = _ctx(price=130.0, avg_price=100.0) # +30% >= 25%
assert "sell_take_profit" in _c(evaluate_sell(ctx, EXIT))
def test_trailing_stop_fires():
ctx = _ctx(price=89.0, holding_high=100.0) # 89 <= 100*0.9=90
assert "sell_trailing_stop" in _c(evaluate_sell(ctx, EXIT))
def test_ma_break_severity_high():
# price가 ma50/ma200 아래 → severity high (ma200 계산 위해 200봉 필요)
closes = [200.0] * 200
ctx = _ctx(price=100.0, closes=closes, avg_price=100.0, holding_high=100.0)
firing = evaluate_sell(ctx, EXIT)
mb = [f for f in firing if f["condition"] == "sell_ma_break"]
assert mb and mb[0]["detail"]["severity"] == "high"
def test_climax_fires():
# holdings_intel 정합: 거래량 3배 이상 + 종가 < 당일고가×0.97 (윗꼬리)
ctx = _ctx(price=96.0, day_high=100.0, today_volume=400.0,
volumes=[100.0] * 60) # 400>=3*100, 96 < 100*0.97=97
assert "sell_climax" in _c(evaluate_sell(ctx, EXIT))
def test_climax_skips_when_not_reversal():
# 종가가 당일고가의 97% 이상 → 윗꼬리 아님
ctx = _ctx(price=99.0, day_high=100.0, today_volume=400.0,
volumes=[100.0] * 60) # 99 >= 100*0.97=97 → 반전 아님
assert "sell_climax" not in _c(evaluate_sell(ctx, EXIT))
def test_climax_uses_exit_params_vol_x():
# exit_params.climax_vol_x=5.0 → 400 < 5*100=500 → 미발화
exit5 = {**EXIT, "climax_vol_x": 5.0}
ctx = _ctx(price=96.0, day_high=100.0, today_volume=400.0,
volumes=[100.0] * 60)
assert "sell_climax" not in _c(evaluate_sell(ctx, exit5))
def test_climax_uses_exit_params_close_pct():
# climax_close_pct=0.90 → 임계 90, price=95 → 95<90? No → 미발화
exit90 = {**EXIT, "climax_close_pct": 0.90}
ctx = _ctx(price=95.0, day_high=100.0, today_volume=400.0,
volumes=[100.0] * 60)
assert "sell_climax" not in _c(evaluate_sell(ctx, exit90))
# 기본 0.97이면 95 < 97 → 발화
assert "sell_climax" in _c(evaluate_sell(ctx, EXIT))
def test_no_avg_no_pnl_conditions():
# avg_price None(보유정보 없음) → stop/take 미발화
ctx = _ctx(price=50.0, avg_price=None, holding_high=None,
closes=[100.0] * 60)
conds = _c(evaluate_sell(ctx, EXIT))
assert "sell_stop_loss" not in conds and "sell_take_profit" not in conds