Files
kis_bot/kis_trader/ws/candle_series.py
Your Name 36a3e2b4a1 feat: Enhance trading system with new permanent subscription features and order book management
Changes:
- Added a new API endpoint for managing permanent subscriptions, allowing users to enable or disable subscriptions dynamically.
- Implemented a function to fill candle data from Kiwoom, ensuring that only relevant data is inserted into the database.
- Introduced a mechanism to handle master subscription states, improving the management of subscription statuses.
- Updated the database schema to include new fields for managing subscription states and order book filtering.

Impact:
- These enhancements improve the flexibility and reliability of the trading system, allowing for better management of subscriptions and order book data, while reducing the risk of data inconsistencies.

히스토리 align 제거 븅신같은 초기설계 아예 제거
진입모드에 구멍메움
호가진입을 켜도 호가가 안들어올때 호가 안보고 그냥 사버림
2026-08-15 23:01:14 +09:00

124 lines
3.8 KiB
Python

"""
ws_candles 시리즈 — source=증권사, channel=ws|rest|rollup.
저장은 섞지 않음: 키움 REST 갭보정은 항상 source=kiwoom, channel=rest.
읽기(실매=Optuna): 틱 메인 WS 우선, 구멍만 kiwoom+rest.
"""
from __future__ import annotations
import os
from typing import Any, Dict, List, Optional, Sequence, Tuple
from kis_trader.utils.env import get_env_from_db
CHANNEL_WS = "ws"
CHANNEL_REST = "rest"
CHANNEL_ROLLUP = "rollup"
# 읽기 우선순위 쌍 (source, channel)
ReadPair = Tuple[str, str]
def normalize_source_channel(
source: str = "",
channel: str = "",
) -> Tuple[str, str]:
"""레거시 source=rest|kw_rest|rollup_1m 을 (증권사, channel)로 정규화."""
s = str(source or "").strip().lower()
ch = str(channel or "").strip().lower()
if s in ("rest", "kw_rest"):
return "kiwoom", CHANNEL_REST
if s in ("rollup_1m", "rollup"):
src = "kiwoom"
if ch in ("kis",):
src = "kis"
return src, CHANNEL_ROLLUP
if s in ("kis", "kis_us", "ws", ""):
return "kis", (ch or CHANNEL_WS)
if s == "kiwoom":
return "kiwoom", (ch or CHANNEL_WS)
if s == "ls":
return "ls", (ch or CHANNEL_WS)
return (s or "kis")[:16], (ch or CHANNEL_WS)[:10]
def _provider_and_override() -> Tuple[str, str]:
override = ""
raw = os.environ.get("CANDLE_SOURCE")
if raw is None:
try:
raw = get_env_from_db("CANDLE_SOURCE", "")
except Exception:
raw = ""
override = str(raw or "").strip().lower()
if override not in ("kis", "kiwoom"):
override = ""
try:
provider = (
get_env_from_db("LIVE_TICK_PROVIDER", "kiwoom") or "kiwoom"
).strip().lower()
except Exception:
provider = "kiwoom"
if provider not in ("kis", "kiwoom"):
provider = "kiwoom"
return provider, override
def live_read_pairs() -> Tuple[ReadPair, ...]:
"""실매·Optuna 기본 읽기 순서.
틱 메인 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),
)
return (
("kiwoom", CHANNEL_WS),
("kiwoom", CHANNEL_REST),
("kiwoom", CHANNEL_ROLLUP),
)
def live_read_label() -> str:
provider, override = _provider_and_override()
main = override or provider
if override:
return f"{override.upper()}+kiwoom_rest"
return f"LIVE({main})+kiwoom_rest"
def dedupe_by_read_pairs(
rows: Sequence[Dict[str, Any]],
pairs: Optional[Sequence[ReadPair]] = None,
) -> List[Dict[str, Any]]:
"""candle_time 1행 — pairs 앞쪽이 이김. source/channel 은 결과에 남기지 않음(레거시 호환)."""
order = tuple(pairs or live_read_pairs())
rank = {p: i for i, p in enumerate(order)}
seen: Dict[str, Tuple[int, Dict[str, Any]]] = {}
for row in rows:
ct = str(row.get("candle_time") or "")[:12]
if not ct:
continue
src, ch = normalize_source_channel(
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))
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)
return out