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:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user