feat(tests): 신규 키움 웹소켓 조건검색 및 실시간 조건검색 테스트 추가
변경 사항 ---- - _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>
This commit is contained in:
12
scripts/mom_apply_rank1_at_6am.sh
Executable file
12
scripts/mom_apply_rank1_at_6am.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
# 모멘텀 param_search 1위 → DB 저장 (아침 6시 실행용)
|
||||
# ※ total_pnl > 0 일 때만 apply (param_search_momentum 내장 규칙)
|
||||
set -euo pipefail
|
||||
cd /home/hoon/kis_bot
|
||||
LOG="/tmp/mom_apply_6am.log"
|
||||
{
|
||||
echo "========================================"
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] mom_apply_rank1 시작"
|
||||
python3 -u -m kis_trader.backtest.param_search_momentum --from-file --apply 1
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] mom_apply_rank1 종료 (exit=$?)"
|
||||
} >> "$LOG" 2>&1
|
||||
16
scripts/schedule_mom_apply_6am.sh
Executable file
16
scripts/schedule_mom_apply_6am.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# 오늘(또는 내일) 06:00 KST 까지 sleep 후 mom_apply_rank1_at_6am.sh 실행
|
||||
set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
LOG="/tmp/mom_apply_6am_scheduler.log"
|
||||
NOW=$(date +%s)
|
||||
TARGET=$(date -d 'today 06:00' +%s 2>/dev/null || date +%s)
|
||||
if [ "$TARGET" -le "$NOW" ]; then
|
||||
TARGET=$(date -d 'tomorrow 06:00' +%s)
|
||||
fi
|
||||
WAIT=$((TARGET - NOW))
|
||||
{
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S %Z')] 06:00 적용 예약 — ${WAIT}초 후 실행 (~$(date -d "@$TARGET" '+%H:%M'))"
|
||||
} >> "$LOG"
|
||||
sleep "$WAIT"
|
||||
exec bash "$SCRIPT_DIR/mom_apply_rank1_at_6am.sh"
|
||||
119
scripts/verify_live_portfolio_align.py
Normal file
119
scripts/verify_live_portfolio_align.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user