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
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
"""evaluate_sell — 5개 매도 조건 경계."""
|
||||
from conditions import evaluate_sell
|
||||
|
||||
EXIT = {"stop_pct": 0.08, "take_pct": 0.25, "trailing_pct": 0.10}
|
||||
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,
|
||||
today_volume=100.0, closes=[100.0] * 60, highs=[100.0] * 60,
|
||||
lows=[100.0] * 60, volumes=[100.0] * 60,
|
||||
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)
|
||||
@@ -49,18 +50,37 @@ def test_ma_break_severity_high():
|
||||
|
||||
|
||||
def test_climax_fires():
|
||||
# 거래량 3배 이상 + 종가(현재가)<시가 반전
|
||||
ctx = _ctx(price=98.0, day_open=100.0, today_volume=400.0,
|
||||
volumes=[100.0] * 60) # 400 >= 3*100, 98<100
|
||||
# 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():
|
||||
ctx = _ctx(price=101.0, day_open=100.0, today_volume=400.0,
|
||||
volumes=[100.0] * 60) # 상승 마감 → 반전 아님
|
||||
# 종가가 당일고가의 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,
|
||||
|
||||
@@ -29,10 +29,12 @@ async def test_get_quote_parses():
|
||||
return_value=httpx.Response(200, json={"access_token": "TKN", "expires_in": 86400}))
|
||||
respx.get(f"{BASE}/uapi/domestic-stock/v1/quotations/inquire-price").mock(
|
||||
return_value=httpx.Response(200, json={"output": {
|
||||
"stck_prpr": "71500", "stck_oprc": "71000", "acml_vol": "1234567"}}))
|
||||
"stck_prpr": "71500", "stck_oprc": "71000", "stck_hgpr": "72000",
|
||||
"acml_vol": "1234567"}}))
|
||||
c = _client()
|
||||
q = await c.get_quote("005930")
|
||||
assert q["price"] == 71500 and q["day_open"] == 71000 and q["today_volume"] == 1234567
|
||||
assert q["day_high"] == 72000
|
||||
await c.close()
|
||||
|
||||
|
||||
|
||||
@@ -32,8 +32,8 @@ class _FakeKIS:
|
||||
async def get_quote(self, ticker):
|
||||
if ticker in self._fail_on:
|
||||
raise RuntimeError("KIS down")
|
||||
return {"price": self._price, "day_open": 99, "today_volume": 1000,
|
||||
"as_of": "x"}
|
||||
return {"price": self._price, "day_open": 99, "day_high": 100,
|
||||
"today_volume": 1000, "as_of": "x"}
|
||||
|
||||
async def get_daily_ohlcv(self, ticker, days=250):
|
||||
# 정배열 + 저가 근접 → ma20_pullback 발화 유도
|
||||
|
||||
Reference in New Issue
Block a user