- 스윙 체결가 확인 전 executed_price NULL + fill_sweep, 진입가 resolve(ActiveTrade↔Portfolio), 0체결/부분체결 매수 장부 원복 - DT 고아 감지(근거 한정)·0체결 매도 복원·stale 점검 확대, 시간손절 -0.3% 원복 - 폴백 손절 ATR 캐시, 거래량부족-차단 신호 매수 차단, 현금 5% 유보 재적용, MDD peak/reset 수정 - KIS 서킷브레이커 청산 우회·경보, db_worker 재큐잉 순서, heartbeat·health-watch 사이드카 - 실시간 WS 재구독·41건 우선순위·다건 프레임 분할, 차트 샘플 제거·당일봉 합성, WS 서브프로토콜 인증 - 프론트: 미관리 그룹·확인 모달·차트 실시간 반영·예수금 표시 정정 - 전문가 에이전트 6종 추가(.claude/agents) - 유닛 스위트 2667 passed (소켓 차단 하), tsc OK Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y83Za98XsNw9zuA4FTUytN
72 lines
2.5 KiB
Python
72 lines
2.5 KiB
Python
"""R5: MDD 축소는 DT 인메모리가 0 이어도 전략명 없는 close/upsert 를 내지 않는다."""
|
|
import pytest
|
|
|
|
from app.services import mdd_manager as m
|
|
from app.services.db_worker import DT_STRATEGY_NAMES
|
|
|
|
|
|
@pytest.fixture
|
|
def cap(monkeypatch):
|
|
out = {"up": [], "wr": []}
|
|
|
|
async def _up(p):
|
|
out["up"].append(p)
|
|
|
|
async def _wr(p):
|
|
out["wr"].append(p)
|
|
|
|
async def _boom(*a, **k):
|
|
raise AssertionError("전략명 없는 close 금지")
|
|
|
|
monkeypatch.setattr("app.services.db_worker.enqueue_active_trade_upsert", _up)
|
|
monkeypatch.setattr("app.services.db_worker.enqueue_db_write", _wr)
|
|
monkeypatch.setattr("app.services.db_worker.enqueue_close_active_trade", _boom)
|
|
monkeypatch.setattr("app.services.db_worker.enqueue_close_active_trade_v2", _boom)
|
|
monkeypatch.setattr(m, "_dt_position_qty", lambda _c: 0)
|
|
return out
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_full_close_without_dt_uses_non_dt_filter(cap):
|
|
await m._sync_active_trade_after_reduction("005930", 10, 0)
|
|
assert len(cap["wr"]) == 1
|
|
d = cap["wr"][0]["data"]
|
|
assert cap["wr"][0]["type"] == "close_active_trade"
|
|
assert tuple(d["strategy_name_not"]) == tuple(DT_STRATEGY_NAMES)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_partial_without_dt_uses_non_dt_filter(cap):
|
|
await m._sync_active_trade_after_reduction("005930", 10, 7)
|
|
assert len(cap["up"]) == 1
|
|
p = cap["up"][0]
|
|
assert p["quantity"] == 7
|
|
assert tuple(p["strategy_name_not"]) == tuple(DT_STRATEGY_NAMES)
|
|
|
|
|
|
def test_dt_qty_failure_is_logged_not_silent(monkeypatch, caplog):
|
|
import app.services.daytrading.position_manager as pm
|
|
|
|
class _Boom:
|
|
@property
|
|
def positions(self):
|
|
raise RuntimeError("boom")
|
|
|
|
monkeypatch.setattr(pm, "day_trading_state", _Boom())
|
|
with caplog.at_level("WARNING"):
|
|
assert m._dt_position_qty("005930") == 0
|
|
assert any("DT 인메모리 수량 조회 실패" in r.message for r in caplog.records)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("remaining", [0, 3, 7])
|
|
async def test_dt_present_never_emits_strategyless_payload(cap, monkeypatch, remaining):
|
|
"""DT 병존 시에도 모든 close/upsert payload 는 전략 필터를 가진다."""
|
|
monkeypatch.setattr(m, "_dt_position_qty", lambda _c: 4)
|
|
await m._sync_active_trade_after_reduction("005930", 10, remaining)
|
|
for p in cap["up"]:
|
|
assert p.get("strategy_name") or p.get("strategy_name_not")
|
|
for w in cap["wr"]:
|
|
d = w["data"]
|
|
assert d.get("strategy_name") or d.get("strategy_names") or d.get("strategy_name_not")
|