fix(시세): LS spill-only·토큰 파일캐시·호가 snap_time 3초컷

- _feed_fallback 미러 OFF, LS cap/grace/hold RAM을 KIS·키움 spill과 정합
- LS 접근토큰 .ls_token_cache_*.json (재시작 재사용, revoke 루프 없음)
- 호가 RAM을 틱과 동일 LIVE_FEED_FALLBACK(snap_time)로 컷, 필터 max_age=0은 유지
- 익절 지정가 로그에 실제 호가 벤더(kis/kiwoom/ls 1·2·3차) 표기

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-08-27 15:21:21 +09:00
parent ba2f9f0953
commit 5e44b86f8b
24 changed files with 1923 additions and 203 deletions

View File

@@ -51,6 +51,21 @@ def is_feed_read_stale(
return False
def is_orderbook_snap_time_stale(
snap_time: Any,
*,
max_age_sec: Optional[float] = None,
) -> bool:
"""호가 거래소 시각(snap_time) vs 지금 — 틱 ``packet_lag`` + ``is_feed_read_stale`` 와 동일.
snap_time 없거나 파싱 실패 → False(버리지 않음). LIVE_FEED_FALLBACK 0 이하면 OFF.
"""
return is_feed_read_stale(
packet_lag_seconds(str(snap_time or "").strip()),
max_age_sec=max_age_sec,
)
def live_feed_fallback_max_age_sec() -> float:
"""읽기 유효 나이(초). 기본 3. 0 이하면 나이로 벤더를 버리지 않음(레거시)."""
try:
@@ -361,6 +376,59 @@ def merge_ticks_time_axis_fallback(
return out
def merge_ls_ticks_third_fallback(
ticks_by_code: Dict[str, Dict[str, List[Dict[str, Any]]]],
ls_ticks_by_code: Dict[str, Dict[str, List[Dict[str, Any]]]],
*,
max_lag_sec: Optional[float] = None,
) -> int:
"""실매 틱 3차: 같은 초에 kis/kiwoom 이 이미 있으면 LS 안 넣음.
메인·2차가 비었거나(또는 나이컷으로 비움) 그 초만 ``ls_ws_ticks`` 로 채움.
``LIVE_FEED_FALLBACK_MAX_AGE_SEC``(기본 3) — lag 모르면 버리지 않음(실매와 동일).
반환=추가한 틱 수.
"""
if not ls_ticks_by_code:
return 0
age = live_feed_fallback_max_age_sec() if max_lag_sec is None else float(max_lag_sec)
added = 0
for code, ls_minutes in ls_ticks_by_code.items():
code_s = str(code or "").strip()
if not code_s or not isinstance(ls_minutes, dict):
continue
main_minutes = ticks_by_code.setdefault(code_s, {})
occupied: set = set()
for _mk, ticks in main_minutes.items():
for t in ticks or []:
if not isinstance(t, dict):
continue
sk = _tick_second_key(t)
if sk:
occupied.add(sk)
for mk, ls_ticks in ls_minutes.items():
mk_s = str(mk or "").strip()
if not mk_s:
continue
bucket = main_minutes.setdefault(mk_s, [])
for t in ls_ticks or []:
if not isinstance(t, dict):
continue
sk = _tick_second_key(t)
if not sk or sk in occupied:
continue
lag = t.get("_lag_sec")
if is_feed_read_stale(lag, age):
continue
row = dict(t)
row["source"] = "ls"
bucket.append(row)
occupied.add(sk)
added += 1
if bucket:
bucket.sort(key=lambda x: str(x.get("tick_time") or ""))
return added
def orderbook_row_lag_seconds(row: Dict[str, Any]) -> Optional[int]:
"""recv_ts vs snap_time 초 차이. 파싱 실패면 None(유효로 봄)."""
recv_ts = str(row.get("recv_ts") or "").strip()