#!/usr/bin/env python3 """실매 ↔ 백테 포트폴리오·유니버스 슬롯 정합 검증.""" from __future__ import annotations import os import sys sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from datetime import datetime as dt from typing import Dict, List from kis_trader.utils.live_portfolio_common import ( filter_candidates_by_history_universe, live_portfolio_budget_full, live_universe_slot_align_enabled, portfolio_strategy_key, resolve_live_buy_qty, resolve_live_total_budget_krw, slot_key_from_dt, ) class _FakeDB: def __init__(self, rows: List[Dict]): self._rows = rows def get_universe_at(self, *, strategy_id: str, at_time: str) -> List[Dict]: eligible = [r for r in self._rows if r["event_time"] <= at_time] if not eligible: return [] et = max(r["event_time"] for r in eligible) return [ {"code": r["code"], "name": r["name"]} for r in eligible if r["event_time"] == et ] def _ok(msg: str) -> None: print(f" OK {msg}") def _fail(msg: str) -> None: print(f" FAIL {msg}") raise SystemExit(1) def main() -> None: print("=== live_portfolio_common 단위 ===") when = dt(2026, 7, 2, 9, 31, 25) sk = slot_key_from_dt(when, 1) if sk != "202607020931": _fail(f"slot_key {sk}") _ok(f"slot_key={sk}") if portfolio_strategy_key("SHORT") != "TAIL": _fail("SHORT→TAIL mapping") _ok("SHORT→TAIL") tb = resolve_live_total_budget_krw("BREAKOUT", max_stocks=20, slot_money=300_000) if tb <= 0: _fail(f"total_budget={tb}") _ok(f"BREAKOUT total_budget={tb:,.0f}") holdings = {"005930": {"buy_price": 70000, "qty": 5}} full = live_portfolio_budget_full(holdings, "BREAKOUT", 300_000, 20) _ok(f"budget_full={full}") qty, invest, rej = resolve_live_buy_qty( 70000, {}, "BREAKOUT", 300_000, max_stocks=20, invest_cap=300_000, ) if qty < 1 or rej: _fail(f"buy_qty qty={qty} rej={rej}") _ok(f"buy_qty qty={qty} invest={invest:,.0f}") db = _FakeDB([ {"event_time": "2026-07-02 09:30:00", "code": "005930", "name": "삼성"}, {"event_time": "2026-07-02 09:30:00", "code": "000660", "name": "SK"}, {"event_time": "2026-07-02 09:31:10", "code": "005930", "name": "삼성"}, ]) cands = [ {"code": "005930", "name": "삼성"}, {"code": "035720", "name": "카카오"}, ] out, dropped = filter_candidates_by_history_universe( cands, db, "BREAKOUT", when=when, ) if len(out) != 1 or out[0]["code"] != "005930" or dropped != 1: _fail(f"universe filter out={out} dropped={dropped}") _ok("universe history ∩ live candidates") print("=== 전략 import ===") from kis_trader.strategies import ( # noqa: WPS433 BreakoutStrategy, DbBandStrategy, MomentumStrategy, RangeBreakStrategy, ScalpingStrategy, TailCatchStrategy, ) for cls in ( BreakoutStrategy, ScalpingStrategy, MomentumStrategy, RangeBreakStrategy, TailCatchStrategy, DbBandStrategy, ): _ok(cls.__name__) print("=== 슬롯 정합 플래그 ===") for sid in ("BREAKOUT", "SCALP", "MOMENTUM", "RANGE_BREAK", "SHORT", "UPDOW", "DBBAND"): en = live_universe_slot_align_enabled(sid) print(f" {sid}: universe_slot={en}") print("\n✅ verify_live_portfolio_align 전부 통과") if __name__ == "__main__": try: main() except SystemExit: sys.exit(1)