이번에 들어간 내용

한투 호가 = 2번째 앱키 전용
키 없거나 start 실패 시 메인에 H0STASP0 안 붙임. 운영설정 WS_ORDERBOOK_SAVE_KIS 빨간 danger.

LS RAM 합집합
후보∪보유∪영구∪grace. sync_targets와 split reconcile 둘 다. 틱 DB 영구 게이트는 그대로.

분봉 쓰레기 → 다음 소스 봉 통째
그 분 틱 0건이거나 전부 봉끝 대비 LIVE_FEED_FALLBACK_MAX_AGE_SEC 초과면 구멍. 메인 WS → 2차 → LS → REST → rollup. CANDLE_GARBAGE_FALLBACK 기본 true.

파일: feed_fallback.py(신규), ws_manager.py, kis_ws.py, candle_series.py, bt_candle_source.py, live_config_schema.py, database.py, 스모크, MD 2개.

같은 ws_manager/database/kis_ws/live_config에는 직전 커밋 이후 쌓여 있던 시세 폴백·ENV 키 정리도 같이 들어갔습니다. 파일 단위로 나눌 수 없어서입니다.
This commit is contained in:
Your Name
2026-08-19 22:11:31 +09:00
parent d1dc274f0e
commit 0ecac7cb95
229 changed files with 8487 additions and 1647 deletions

View File

@@ -9,7 +9,10 @@ import logging
from collections import defaultdict
from typing import Any, Dict, List, Optional, Set, Tuple
from kis_trader.utils.env import get_env_bool, get_env_from_db
from kis_trader.engine.feed_fallback import (
live_feed_fallback_max_age_sec,
orderbook_row_lag_seconds,
)
from kis_trader.ws.orderbook_cache import OrderbookSnapshot, orderbook_snapshot_from_storage
from kis_trader.ws.program_cache import ProgramSnapshot, program_snapshot_from_storage
from kis_trader.ws.trigger_eval_recorder import FILTER_EVAL_SOURCE
@@ -332,6 +335,63 @@ def _index_log_verdict_row(
})
def _merge_ls_orderbook_fallback(
out: Dict[str, Dict[str, List[OrderbookSnapshot]]],
db,
start_key: str,
end_key: str,
codes: Optional[Set[str]],
market: str,
kiwoom_valid_minutes: Dict[str, Dict[str, str]],
) -> int:
"""메인 호가 없거나 2초 초과인 분에 ls_ws_orderbook 을 넣음. 빈 스냅=필터 통과 금지."""
added = 0
try:
rows = _load_orderbook_rows(
db, start_key, end_key, codes, market=market,
sources=LS_BODY_SOURCES, strategy="",
table="ls_ws_orderbook", use_strategy_col=False,
)
if not rows:
rows = _load_orderbook_rows(
db, start_key, end_key, codes, market=market,
sources=tuple(), strategy="",
table="ls_ws_orderbook", use_strategy_col=False,
)
except Exception as e:
logger.debug("ls_ws_orderbook 폴백 로드 스킵: %s", e)
return 0
age = live_feed_fallback_max_age_sec()
ls_latest: Dict[str, Dict[str, Any]] = defaultdict(dict)
for r in rows:
code = str(r.get("code") or "").strip()
st = str(r.get("snap_time") or "").strip()
minute_key = _minute_key(st)
if not code or not minute_key:
continue
lag = orderbook_row_lag_seconds(r)
if age > 0 and lag is not None and lag > age:
continue
prev = ls_latest[code].get(minute_key)
if prev is None or str(st) >= str(prev.get("snap_time") or ""):
ls_latest[code][minute_key] = r
for code, minutes in ls_latest.items():
valid = kiwoom_valid_minutes.get(code) or {}
for mk, r in minutes.items():
if mk in valid:
continue
snap = orderbook_snapshot_from_storage(r)
bucket = out.setdefault(code, {}).setdefault(mk, [])
if not bucket:
bucket.append(snap)
added += 1
else:
bucket[-1] = snap
if added:
logger.info("📌 호가 3차 LS 폴백: +%s분 (메인 없거나 2초 초과)", added)
return added
def load_orderbook_snapshots_by_code(
db,
start_key: str,
@@ -446,6 +506,10 @@ def load_orderbook_snapshots_by_code(
seen_eval.add((code, st[:14]))
total += 1
elif src == KIWOOM_BODY_SOURCE:
lag = orderbook_row_lag_seconds(r)
age = live_feed_fallback_max_age_sec()
if age > 0 and lag is not None and lag > age:
continue
# 분당 최신 1건만 (rows 는 snap_time ASC 정렬 → 뒤가 최신)
prev = kiwoom_latest[code].get(minute_key)
snap = orderbook_snapshot_from_storage(r)
@@ -469,6 +533,9 @@ def load_orderbook_snapshots_by_code(
bucket.append(snap)
total += 1
total += _merge_ls_orderbook_fallback(
out, db, start_key, end_key, codes, mkt, kiwoom_latest,
)
return dict(out), total, dict(log_verdict)