이번에 들어간 내용

한투 호가 = 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

@@ -8,7 +8,7 @@ from collections import defaultdict
from datetime import datetime, timedelta
from typing import Any, Dict, Iterator, List, Optional, Set, Tuple
from ..utils.env import get_env_bool, get_env_from_db, get_env_int
from ..utils.env import get_env_from_db, get_env_int
from ..utils.logger import get_logger
logger = get_logger("kis_trader.breakout_tick_loader")
@@ -121,7 +121,7 @@ def _fetch_ws_ticks_day_rows(
params.append(tick_source)
sql = f"""
SELECT code, tick_time, price, volume, source
SELECT code, tick_time, price, volume, source, recv_ts, tick_time_raw
FROM {table}
WHERE market = %s
AND tick_time >= %s
@@ -155,13 +155,34 @@ def _fetch_ws_ticks_day_rows(
return []
def _tick_row_lag_seconds(row: Dict[str, Any]) -> Optional[int]:
"""ws_ticks 1행 recv_ts vs 체결시각(raw 우선) 지연(초). 계산 불가 시 None."""
from kis_trader.engine.feed_fallback import packet_lag_seconds
recv_ts = str(row.get("recv_ts") or "").strip()
raw = str(row.get("tick_time_raw") or "").strip()
tt = str(row.get("tick_time") or "").strip()
try:
recv_dt = datetime.strptime(recv_ts[:19], "%Y-%m-%d %H:%M:%S")
except Exception:
return None
lag = packet_lag_seconds(raw or tt, now_dt=recv_dt)
if lag is None:
return None
return int(lag)
def _ingest_tick_rows(
rows: List[Dict[str, Any]],
out: Dict[str, Dict[str, List[Dict[str, Any]]]],
) -> int:
"""SELECT 행 → 종목·분봉 버킷. 반환=적재 건수."""
"""SELECT 행 → 종목·분봉 버킷. 반환=적재 건수.
공책은 버리지 않음. lag 는 읽기 폴백(2초)용 메타만 붙인다.
"""
n = 0
for r in rows:
kw_lag = _tick_row_lag_seconds(r)
code = str(r["code"]).strip()
tt = str(r["tick_time"])[:14]
if len(tt) < 12:
@@ -173,6 +194,7 @@ def _ingest_tick_rows(
"price": float(r["price"] or 0),
"volume": int(r.get("volume") or 0),
"source": r.get("source") or "",
"_lag_sec": kw_lag,
}
bucket = out[code].setdefault(minute_key, [])
bucket.append(tick)
@@ -199,8 +221,7 @@ def load_breakout_ticks_by_code(
"""
mkt = (market or get_env_from_db("WS_TICK_DEFAULT_MARKET", "KR") or "KR").strip().upper()
tt_start, tt_end = _candle_keys_to_tick_range(start_key, end_key)
# 해외는 키움 체결 없음(kis_us) — 키움 우선 필터 끄기
prefer_kw = bool(get_env_bool("WS_TICK_PREFER_KIWOOM", True)) and mkt != "US"
# 분봉에 메인 1건이면 보조 전부 버리기는 하지 않음. 같은 초만 메인 우선.
table = _ws_ticks_table(mkt)
try:
@@ -258,24 +279,25 @@ def load_breakout_ticks_by_code(
)
# 일별 SELECT 에 ORDER BY 없음 → 분봉 버킷 시간순 정렬 (틱 재생 정합)
for _code, minutes in out.items():
for _mk, ticks in minutes.items():
ticks.sort(key=lambda t: str(t.get("tick_time") or ""))
from kis_trader.engine.feed_fallback import live_tick_primary, merge_ticks_time_axis_fallback
# 실매(키움 구독) 정합: 분봉 단위 키움 우선 (모멘텀/꼬리 로더가 이 함수 재사용)
if prefer_kw and out:
kept = 0
for code, minutes in out.items():
for mk, ticks in list(minutes.items()):
filtered = _prefer_kiwoom_minute_ticks(ticks)
minutes[mk] = filtered
main_src = live_tick_primary() if mkt != "US" else "kis"
kept = 0
for _code, minutes in out.items():
for _mk, ticks in list(minutes.items()):
ticks.sort(key=lambda t: str(t.get("tick_time") or ""))
if mkt != "US":
filtered = merge_ticks_time_axis_fallback(ticks, main_src=main_src)
minutes[_mk] = filtered
kept += len(filtered)
if kept != total:
logger.info(
"📌 WS_TICK_PREFER_KIWOOM: raw=%d → kept=%d (분봉내 키움 우선)",
total, kept,
)
total = kept
else:
kept += len(ticks)
if kept != total:
logger.info(
"📌 틱 시간축 폴백: raw=%d → kept=%d (같은 초 메인 우선, 메인 lag>2s면 보조)",
total, kept,
)
total = kept
return dict(out), total