변경 사항 ---- - _test_kiwoom_condition_list.py: 키움 웹소켓 조건검색 '목록조회' 기능을 단독으로 테스트하는 스크립트 추가 - _test_kiwoom_condition_realtime.py: 'momentum' 조건식을 실시간으로 등록하고 초기 매칭 종목 리스트 및 실시간 편입/이탈을 수신하는 테스트 스크립트 추가 - _verify_columnar_bitid.py, _verify_shared_e2e_breakout.py, _verify_shared_e2e.py: 공유 메모리 및 dict 간의 데이터 일관성을 검증하는 테스트 추가 영향 ---- - 신규 테스트 스크립트 추가로 키움 웹소켓 API의 기능 검증 및 안정성을 높임 - 기존 기능에 대한 영향 없음 Co-authored-by: Cursor <cursoragent@cursor.com>
120 lines
3.5 KiB
Python
120 lines
3.5 KiB
Python
#!/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)
|