feat(trade-monitor): 매수/매도 조건 로직 (§6 8개 조건)
This commit is contained in:
66
services/trade-monitor/tests/test_conditions_buy.py
Normal file
66
services/trade-monitor/tests/test_conditions_buy.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""evaluate_buy — 3개 매수 조건 경계."""
|
||||
from conditions import evaluate_buy
|
||||
|
||||
BUY_PARAMS = {"rsi_oversold": 30, "breakout_vol_mult": 1.5, "pullback_pct": 0.02}
|
||||
|
||||
|
||||
def _ctx(**over):
|
||||
base = dict(
|
||||
ticker="005930", name="삼성전자", price=100.0, day_open=99.0,
|
||||
today_volume=1000.0, closes=[], highs=[], lows=[], volumes=[],
|
||||
avg_price=None, qty=None, holding_high=None, climax_vol_mult=3.0,
|
||||
)
|
||||
base.update(over)
|
||||
return base
|
||||
|
||||
|
||||
def _conditions(firing):
|
||||
return {f["condition"] for f in firing}
|
||||
|
||||
|
||||
def test_ma20_pullback_fires():
|
||||
# 정배열(ma20>ma50>ma200), 최근 저가가 ma20 근처, price가 ma20 위로 반등
|
||||
closes = [90.0] * 200 + [100.0] * 20 # ma20=100, ma50/ma200 낮음 → 정배열
|
||||
lows = [90.0] * 217 + [100.5, 100.4, 100.3] # 최근 3봉 저가 ~ma20*(1.02)=102 이하
|
||||
ctx = _ctx(price=101.0, closes=closes, highs=closes, lows=lows,
|
||||
volumes=[1.0] * len(closes))
|
||||
assert "buy_ma20_pullback" in _conditions(evaluate_buy(ctx, BUY_PARAMS))
|
||||
|
||||
|
||||
def test_ma20_pullback_skips_when_not_aligned():
|
||||
closes = [100.0] * 200 + [90.0] * 20 # 역배열
|
||||
ctx = _ctx(price=91.0, closes=closes, highs=closes, lows=closes,
|
||||
volumes=[1.0] * len(closes))
|
||||
assert "buy_ma20_pullback" not in _conditions(evaluate_buy(ctx, BUY_PARAMS))
|
||||
|
||||
|
||||
def test_breakout_fires():
|
||||
closes = [50.0] * 25
|
||||
highs = [60.0] * 25 # 직전 20봉 최고 60
|
||||
vols = [100.0] * 25 # avg20=100
|
||||
ctx = _ctx(price=61.0, today_volume=200.0, closes=closes, highs=highs,
|
||||
lows=closes, volumes=vols) # 61>60, 200>1.5*100
|
||||
assert "buy_breakout" in _conditions(evaluate_buy(ctx, BUY_PARAMS))
|
||||
|
||||
|
||||
def test_breakout_skips_on_low_volume():
|
||||
highs = [60.0] * 25
|
||||
ctx = _ctx(price=61.0, today_volume=120.0, closes=[50.0] * 25, highs=highs,
|
||||
lows=[50.0] * 25, volumes=[100.0] * 25) # 120 < 1.5*100=150
|
||||
assert "buy_breakout" not in _conditions(evaluate_buy(ctx, BUY_PARAMS))
|
||||
|
||||
|
||||
def test_rsi_bounce_fires():
|
||||
# 14봉 급락으로 RSI<30 찍고 5봉 반등하여 30 위로 복귀
|
||||
closes = [100.0]
|
||||
for _ in range(14):
|
||||
closes.append(closes[-1] * 0.97) # 하락 → RSI 저하
|
||||
for _ in range(5):
|
||||
closes.append(closes[-1] * 1.05) # 반등 → RSI 30 위로
|
||||
ctx = _ctx(price=closes[-1], closes=closes, highs=closes, lows=closes,
|
||||
volumes=[1.0] * len(closes))
|
||||
assert "buy_rsi_bounce" in _conditions(evaluate_buy(ctx, BUY_PARAMS))
|
||||
|
||||
|
||||
def test_empty_series_no_fire():
|
||||
assert evaluate_buy(_ctx(), BUY_PARAMS) == []
|
||||
69
services/trade-monitor/tests/test_conditions_sell.py
Normal file
69
services/trade-monitor/tests/test_conditions_sell.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""evaluate_sell — 5개 매도 조건 경계."""
|
||||
from conditions import evaluate_sell
|
||||
|
||||
EXIT = {"stop_pct": 0.08, "take_pct": 0.25, "trailing_pct": 0.10}
|
||||
|
||||
|
||||
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,
|
||||
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():
|
||||
# 거래량 3배 이상 + 종가(현재가)<시가 반전
|
||||
ctx = _ctx(price=98.0, day_open=100.0, today_volume=400.0,
|
||||
volumes=[100.0] * 60) # 400 >= 3*100, 98<100
|
||||
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) # 상승 마감 → 반전 아님
|
||||
assert "sell_climax" not 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
|
||||
Reference in New Issue
Block a user