fix: 한투 호가 2키 전용, LS RAM 합집합, 분봉 쓰레기면 다음 소스 통째

메인 시세 41에 H0STASP0를 붙이는 폴백을 없앤다. MINIMAL과 무관하게
후보∪보유∪영구를 LS RAM에 붙인다. 그 분 틱이 없거나 봉끝 대비 늦으면
그 WS 봉을 버리고 2차·LS·REST 봉을 통째로 쓴다.

같은 파일에 직전 커밋 이후 쌓인 시세 폴백/ENV 키 정리도 포함된다.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Your Name
2026-08-19 22:09:03 +09:00
parent 6d2a706a48
commit d1dc274f0e
10 changed files with 2146 additions and 459 deletions

View File

@@ -2,12 +2,12 @@
ws_candles 시리즈 — source=증권사, channel=ws|rest|rollup.
저장은 섞지 않음: 키움 REST 갭보정은 항상 source=kiwoom, channel=rest.
읽기(실매=Optuna): 메인 WS 우선, 구멍만 kiwoom+rest.
읽기(실매=Optuna): 메인 WS → 2차 WS → LS WS → kiwoom+rest → rollup.
"""
from __future__ import annotations
import os
from typing import Any, Dict, List, Optional, Sequence, Tuple
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
from kis_trader.utils.env import get_env_from_db
@@ -67,20 +67,16 @@ def _provider_and_override() -> Tuple[str, str]:
def live_read_pairs() -> Tuple[ReadPair, ...]:
"""실매·Optuna 기본 읽기 순서.
메인 WS 한 칸, 없으면 키움 REST 구멍.
메인 WS → 2차 WS → LS WS(있으면) → 키움 REST 구멍 → 롤업.
CANDLE_SOURCE=kis|kiwoom 이면 그 메인을 씀. 빈값이면 LIVE_TICK_PROVIDER.
"""
provider, override = _provider_and_override()
main = override or provider
# 메인 WS → 구멍 키움 REST → 1M 롤업(상위 TF 웜업). 증권사 행은 섞지 않음.
if main == "kis":
return (
("kis", CHANNEL_WS),
("kiwoom", CHANNEL_REST),
("kiwoom", CHANNEL_ROLLUP),
)
aux = "kis" if main == "kiwoom" else "kiwoom"
return (
("kiwoom", CHANNEL_WS),
(main, CHANNEL_WS),
(aux, CHANNEL_WS),
("ls", CHANNEL_WS),
("kiwoom", CHANNEL_REST),
("kiwoom", CHANNEL_ROLLUP),
)
@@ -89,19 +85,30 @@ def live_read_pairs() -> Tuple[ReadPair, ...]:
def live_read_label() -> str:
provider, override = _provider_and_override()
main = override or provider
aux = "kis" if main == "kiwoom" else "kiwoom"
if override:
return f"{override.upper()}+kiwoom_rest"
return f"LIVE({main})+kiwoom_rest"
return f"{override.upper()}+{aux}+ls+kiwoom_rest"
return f"LIVE({main})+{aux}+ls+kiwoom_rest"
def dedupe_by_read_pairs(
rows: Sequence[Dict[str, Any]],
pairs: Optional[Sequence[ReadPair]] = None,
*,
ticks: Optional[Sequence[Dict[str, Any]]] = None,
tf_min: int = 1,
missing_policy: str = "hole",
live_cover: bool = False,
) -> List[Dict[str, Any]]:
"""candle_time 1행 — pairs 앞쪽이 이김. source/channel 은 결과에 남기지 않음(레거시 호환)."""
"""candle_time 1행 — pairs 앞쪽이 이김. source/channel 은 결과에 남기지 않음.
ticks 가 있으면 WS 봉만 쓰레기(0건·봉끝 나이초과)일 때 다음 쌍. 한 봉 혼합 없음.
live_cover=True 이면 링이 그 분을 커버 못할 때 그 WS 봉을 유지.
"""
from kis_trader.engine.feed_fallback import bar_is_garbage, live_bar_is_garbage
order = tuple(pairs or live_read_pairs())
rank = {p: i for i, p in enumerate(order)}
seen: Dict[str, Tuple[int, Dict[str, Any]]] = {}
by_ct: Dict[str, Dict[ReadPair, Dict[str, Any]]] = {}
for row in rows:
ct = str(row.get("candle_time") or "")[:12]
if not ct:
@@ -110,14 +117,54 @@ def dedupe_by_read_pairs(
str(row.get("source") or ""),
str(row.get("channel") or ""),
)
pri = rank.get((src, ch), 999)
prev = seen.get(ct)
if prev is None or pri < prev[0]:
seen[ct] = (pri, dict(row))
by_ct.setdefault(ct, {})
pair = (src, ch)
if pair not in by_ct[ct]:
by_ct[ct][pair] = dict(row)
tf = max(1, int(tf_min or 1))
out: List[Dict[str, Any]] = []
for ct in sorted(seen.keys()):
d = seen[ct][1]
d.pop("source", None)
d.pop("channel", None)
out.append(d)
for ct in sorted(by_ct.keys()):
picked: Optional[Dict[str, Any]] = None
for pair in order:
bar = by_ct[ct].get(pair)
if bar is None:
continue
src, ch = pair
if ticks is not None and ch == CHANNEL_WS:
if live_cover:
bad = live_bar_is_garbage(
ticks, candle_time=ct, tf_min=tf, source=src,
)
else:
bad = bar_is_garbage(
ticks, candle_time=ct, tf_min=tf, source=src,
missing_policy=missing_policy,
)
if bad:
continue
picked = dict(bar)
break
if picked is None:
continue
picked.pop("source", None)
picked.pop("channel", None)
out.append(picked)
return out
def pick_bar_by_read_pairs(
rows: Sequence[Dict[str, Any]],
pairs: Optional[Sequence[ReadPair]] = None,
*,
ticks: Optional[Sequence[Dict[str, Any]]] = None,
tf_min: int = 1,
missing_policy: str = "hole",
live_cover: bool = False,
) -> Optional[Dict[str, Any]]:
"""한 candle_time 묶음에서 승자 봉 1개. 없으면 None."""
out = dedupe_by_read_pairs(
rows, pairs, ticks=ticks, tf_min=tf_min,
missing_policy=missing_policy, live_cover=live_cover,
)
return out[0] if out else None