#!/usr/bin/env python3 """ scripts/adhoc_cancelable_reconcile_smoke.py =========================================== Worker SELL C+D — 주말 구조 스모크 (REST 주문·cancel 호출 없음). 검증: · orders broker debug 컬럼 migrate · update_order_broker_snapshot / get_latest_sell_order_for_code 헬퍼 · OrderManager._reconcile_cancelable_open disabled 분기 (noop) · poll 만료 cancel 실패 시 CANCELLED 금지 소스 앵커 실행: python3 -u scripts/adhoc_cancelable_reconcile_smoke.py """ from __future__ import annotations import inspect import sys from pathlib import Path from unittest.mock import MagicMock, patch ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) REQUIRED_ORDER_COLS = ( "broker_open_qty", "broker_open_odno", "broker_reconcile_at", "broker_reconcile_note", ) def main() -> int: ok = True print("=== adhoc_cancelable_reconcile_smoke ===") from kis_trader.database.db_manager import get_db from kis_trader.execution.order_manager import OrderManager, OrderRequest db = get_db() db._ensure_orders_table() cols = [r["Field"] for r in db.conn.execute( "SHOW COLUMNS FROM orders", ).fetchall()] missing = [c for c in REQUIRED_ORDER_COLS if c not in cols] if missing: print(f"FAIL orders 컬럼 누락: {missing}") ok = False else: print(f"OK orders broker debug 컬럼 {len(REQUIRED_ORDER_COLS)}개 확인") snap_ok = db.update_order_broker_snapshot( ord_no="__smoke__", strategy_id="SMOKE", code="000000", open_qty=0, open_odno=None, note="adhoc_smoke", ) if snap_ok: print("OK update_order_broker_snapshot (no-row UPDATE 허용)") else: print("WARN update_order_broker_snapshot False (no matching row — migrate OK)") mock_client = MagicMock() mock_db = MagicMock() mock_db.get_latest_sell_order_for_code.return_value = None om = OrderManager(client=mock_client, db=mock_db) req = OrderRequest( strategy_id="SMOKE", code="232680", name="232680", side="SELL", qty=1, price_ref=0.0, reason="smoke", ) def _env_bool(key, default=True): if key == "CANCELABLE_RECONCILE_ENABLED": return False if key == "CANCELABLE_RECONCILE_CANCEL_RETRY": return False return default with patch( "kis_trader.execution.order_manager.get_env_bool", side_effect=_env_bool, ): action = om._reconcile_cancelable_open( req, {"232680": {"psbl_qty": 10, "odno": "X"}}, ) if action != "disabled": print(f"FAIL reconcile disabled 기대=disabled 실제={action}") ok = False else: print("OK _reconcile_cancelable_open disabled → noop 경로") from kis_trader.execution import order_manager as om_mod src = inspect.getsource(om_mod.OrderManager.poll_pending_fills) if "cancel_failed_at_poll" in src and "CANCELLED 금지" in src: print("OK poll_pending_fills cancel 실패 → CANCELLED 금지 앵커") else: print("FAIL poll_pending_fills cancel-fail 가드 소스 누락") ok = False from kis_trader.strategies import base as base_mod base_src = inspect.getsource(base_mod.BaseStrategy._enqueue_sell) if "SELL_LOCKED_ENQUEUE_COOLDOWN_SEC" in base_src: print("OK base._enqueue_sell sell_locked cooldown 앵커") else: print("FAIL base enqueue cooldown 소스 누락") ok = False ow_src = Path(ROOT / "kis_trader/execution/order_worker.py").read_text(encoding="utf-8") if "prefetch_broker_holdings(force=False)" in ow_src: print("OK order_worker prefetch force=False") else: print("FAIL order_worker prefetch force=False 누락") ok = False print("=== 최종:", "통과" if ok else "실패", "===") return 0 if ok else 1 if __name__ == "__main__": raise SystemExit(main())