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 제거 븅신같은 초기설계 아예 제거
진입모드에 구멍메움
호가진입을 켜도 호가가 안들어올때 호가 안보고 그냥 사버림
This commit is contained in:
Your Name
2026-08-15 23:01:14 +09:00
parent 4a18ce2697
commit 36a3e2b4a1
94 changed files with 6368 additions and 1639 deletions

View File

@@ -193,6 +193,42 @@ def list_permanent_subs(db: Any, enabled_only: bool = False) -> List[Dict[str, A
return out
def subscribe_master_enabled() -> bool:
"""마스터 스위치 — OFF면 행은 두고 구독만 안 함."""
from kis_trader.utils.env import get_env_bool
return get_env_bool("PERMANENT_SUBSCRIBE_ENABLED", True)
def enabled_code_set(db: Any, market_type: Optional[str] = None) -> set:
"""enabled=1 영구구독 코드. 마스터와 무관 (저장 가드는 이 집합)."""
mt = str(market_type or "").strip().upper()
out = set()
for r in list_permanent_subs(db, enabled_only=True):
if mt and str(r.get("market_type") or "KR").strip().upper() != mt:
continue
c = str(r.get("code") or "").strip()
if c:
out.add(c)
return out
def subscribe_codes(db: Any, market_type: str) -> List[Dict[str, Any]]:
"""실제 WS 구독에 쓸 목록 = 마스터 ON ∧ enabled."""
if not subscribe_master_enabled():
return []
return codes_by_market(db, market_type, enabled_only=True)
def should_persist_ls(code: str, perm_codes: Optional[set] = None) -> bool:
"""LS DB 적재: 영구구독 enabled 코드만 (후보 spill 제외)."""
c = str(code or "").strip()
if not c:
return False
if perm_codes is None:
return False
return c in perm_codes
def codes_by_market(db: Any, market_type: str, enabled_only: bool = True) -> List[Dict[str, Any]]:
"""특정 시장(KR/US) 영구구독 목록 (WS 배선용)."""
mt = str(market_type or "").strip().upper()
@@ -267,20 +303,50 @@ def last_quotes_for_codes(
now_slot = "999999999999"
raw = _core(db)
for code in uniq:
try:
rows = raw.conn.execute(
"""
SELECT candle_time, open, close, volume, updated_at
FROM ws_candles
WHERE code=%s AND timeframe=%s AND candle_time<=%s
ORDER BY candle_time DESC
LIMIT 2
""",
[code, tf, now_slot],
).fetchall() or []
except Exception as e:
logger.debug("last_quotes candle %s: %s", code, e)
rows = []
rows = []
# KR 영구구독 시세는 ls_ws_candles 우선 (키움/KIS 슬롯 이관)
if code.isdigit() and len(code) == 6:
try:
now_ls = ""
if len(now_slot) >= 12:
now_ls = (
f"{now_slot[0:4]}-{now_slot[4:6]}-{now_slot[6:8]} "
f"{now_slot[8:10]}:{now_slot[10:12]}:00"
)
ls_rows = raw.conn.execute(
"""
SELECT datetime, open, close, volume, updated_at
FROM ls_ws_candles
WHERE code=%s AND tf_min=%s AND datetime<=%s
ORDER BY datetime DESC
LIMIT 2
""",
[code, tf, now_ls or "9999-12-31 23:59:00"],
).fetchall() or []
for r in ls_rows:
d = dict(r) if not isinstance(r, dict) else dict(r)
dt = str(d.get("datetime") or "")
digits = "".join(ch for ch in dt if ch.isdigit())[:12]
d["candle_time"] = digits
rows.append(d)
except Exception as e:
logger.debug("last_quotes ls_ws_candles %s: %s", code, e)
rows = []
if not rows:
try:
rows = raw.conn.execute(
"""
SELECT candle_time, open, close, volume, updated_at
FROM ws_candles
WHERE code=%s AND timeframe=%s AND candle_time<=%s
ORDER BY candle_time DESC
LIMIT 2
""",
[code, tf, now_slot],
).fetchall() or []
except Exception as e:
logger.debug("last_quotes candle %s: %s", code, e)
rows = []
if not rows:
continue
parsed: List[Dict[str, Any]] = []
@@ -317,3 +383,86 @@ def last_quotes_for_codes(
"updated_at": str(last.get("updated_at") or ""),
})
return out
def fill_ls_candles_from_kiwoom(
db: Any,
codes: List[str],
*,
n_bars: Optional[int] = None,
tf_min: int = 1,
) -> Dict[str, Any]:
"""키움 ka10080 → ls_ws_candles INSERT IGNORE (구멍만). ws_candles 에 넣지 않음."""
import random
import time as _time
from kis_trader.utils.env import get_env_bool, get_env_float, get_env_from_db, get_env_int
from kis_trader.ws.kis_ws import get_kiwoom_candles_df
from database import TradeDB
tf = max(1, int(tf_min or 1))
n_req = int(n_bars) if n_bars else int(get_env_int("PERM_LS_FILL_BARS", 500))
sleep_lo = float(get_env_float("PERM_LS_FILL_SLEEP_MIN", 1.0))
sleep_hi = float(get_env_float("PERM_LS_FILL_SLEEP_MAX", 3.0))
if sleep_hi < sleep_lo:
sleep_hi = sleep_lo
force_real = get_env_bool("KIWOOM_WS_FORCE_REAL", True)
is_mock = False if force_real else get_env_bool("KIS_MOCK", False)
if is_mock:
key = str(get_env_from_db("KIWOOM_APP_KEY_MOCK", "") or "").strip()
sec = str(get_env_from_db("KIWOOM_APP_SECRET_MOCK", "") or "").strip()
else:
key = str(get_env_from_db("KIWOOM_APP_KEY_REAL", "") or "").strip()
sec = str(get_env_from_db("KIWOOM_APP_SECRET_REAL", "") or "").strip()
if not key or not sec:
key = str(get_env_from_db("KIWOOM_APP_KEY", "") or "").strip()
sec = str(get_env_from_db("KIWOOM_APP_SECRET", "") or "").strip()
if not key or not sec:
return {"ok": False, "error": "키움 API 키 없음", "codes": []}
raw = _core(db)
out_codes: List[Dict[str, Any]] = []
for i, code in enumerate(codes or []):
c = str(code or "").strip()
if not (c.isdigit() and len(c) == 6):
out_codes.append({"code": c, "ok": False, "error": "KR 6자리만", "inserted": 0})
continue
inserted = 0
err = ""
try:
df = get_kiwoom_candles_df(c, tf, key, sec, is_mock=is_mock, n=n_req)
if df is None or getattr(df, "empty", True):
err = "빈응답"
else:
for _, rec in df.iterrows():
ct = str(rec.get("time") or "")[:12]
if len(ct) < 12:
continue
close = float(rec.get("close") or 0)
if close <= 0:
continue
dt = TradeDB._candle_time_to_ls_datetime(ct)
if not dt:
continue
candle = {
"datetime": dt,
"tf_min": tf,
"open": float(rec.get("open") or close),
"high": float(rec.get("high") or close),
"low": float(rec.get("low") or close),
"close": close,
"volume": float(rec.get("volume") or 0),
"tick_count": 0,
}
if raw.insert_ls_ws_candle_if_absent(code=c, candle=candle):
inserted += 1
except Exception as e:
err = str(e)
out_codes.append({
"code": c, "ok": not err, "error": err, "inserted": inserted,
})
if i + 1 < len(codes or []):
_time.sleep(random.uniform(sleep_lo, sleep_hi))
ok_n = sum(1 for x in out_codes if x.get("ok"))
return {"ok": True, "codes": out_codes, "ok_n": ok_n}