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
90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
"""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
|