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:
@@ -9,12 +9,18 @@ kis_trader/engine/orderbook_filter.py — TRIGGER 진입 호가 필터
|
||||
- ``WS_ORDERBOOK_COLLECT_ENABLED`` — TRIGGER 판정 스냅 저장 (필터 OFF여도 가능)
|
||||
|
||||
모든 임계값 env/DB — 하드코딩 금지 (기본값은 orderbook_env 상수).
|
||||
필터 호가 나이(``WS_ORDERBOOK_FILTER_MAX_AGE_SEC``, 기본 0=마지막 RAM)와
|
||||
저장 TTL(``WS_ORDERBOOK_TICK_MAX_AGE_SEC``)은 분리한다.
|
||||
필터 ON 인데 호가 사진이 한 장도 없으면
|
||||
``WS_ORDERBOOK_FILTER_REJECT_IF_EMPTY``(기본 true) 일 때 거절한다. REST 없음.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from kis_trader.utils.env import get_env_float, get_env_int
|
||||
from kis_trader.utils.env import get_env_bool, get_env_float, get_env_int
|
||||
from kis_trader.ws.orderbook_cache import OrderbookSnapshot
|
||||
from kis_trader.ws.trigger_eval_recorder import get_trigger_eval_recorder
|
||||
|
||||
@@ -25,6 +31,8 @@ from .orderbook_env import (
|
||||
)
|
||||
from .trigger_eval_collect import orderbook_collect_enabled
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
LOG_BACKFILL_SOURCE = "log_backfill"
|
||||
|
||||
|
||||
@@ -66,30 +74,41 @@ def _fetch_snapshot(params: Dict[str, Any]) -> Optional[OrderbookSnapshot]:
|
||||
if not ws or not code:
|
||||
return None
|
||||
|
||||
# 소형주는 0D 업데이트 주기가 10~60초 이상 → 3초 기본값이면 항상 None
|
||||
# WS_ORDERBOOK_TICK_MAX_AGE_SEC 공유해서 일관된 허용 나이 적용
|
||||
max_age = float(get_env_float("WS_ORDERBOOK_TICK_MAX_AGE_SEC", 30.0) or 30.0)
|
||||
# 필터: 기본 0 → 캐시 get 이 나이를 무시하고 마지막 호가로 검사 (REST 없음).
|
||||
# 저장(0D 덤프) TTL 은 WS_ORDERBOOK_TICK_MAX_AGE_SEC 를 그대로 씀.
|
||||
max_age = float(get_env_float("WS_ORDERBOOK_FILTER_MAX_AGE_SEC", 0.0) or 0.0)
|
||||
|
||||
getter = getattr(ws, "get_orderbook_snapshot", None)
|
||||
snap = None
|
||||
if callable(getter):
|
||||
try:
|
||||
snap = getter(code, max_age_sec=max_age)
|
||||
if snap is not None:
|
||||
return snap
|
||||
except Exception:
|
||||
pass
|
||||
getter2 = getattr(ws, "get_orderbook", None)
|
||||
if not callable(getter2):
|
||||
return None
|
||||
snap = None
|
||||
if snap is None:
|
||||
getter2 = getattr(ws, "get_orderbook", None)
|
||||
if not callable(getter2):
|
||||
return None
|
||||
try:
|
||||
raw = getter2(code, max_age_sec=max_age)
|
||||
except Exception:
|
||||
return None
|
||||
if raw is None:
|
||||
return None
|
||||
if not isinstance(raw, OrderbookSnapshot):
|
||||
return None
|
||||
snap = raw
|
||||
save_ttl = float(get_env_float("WS_ORDERBOOK_TICK_MAX_AGE_SEC", 30.0) or 30.0)
|
||||
try:
|
||||
raw = getter2(code, max_age_sec=max_age)
|
||||
except Exception:
|
||||
return None
|
||||
if raw is None:
|
||||
return None
|
||||
if isinstance(raw, OrderbookSnapshot):
|
||||
return raw
|
||||
return None
|
||||
age = time.time() - float(snap.ts or 0.0)
|
||||
except (TypeError, ValueError):
|
||||
age = 0.0
|
||||
if save_ttl > 0 and age > save_ttl:
|
||||
logger.debug(
|
||||
"호가필터: 저장TTL(%.0fs) 초과 age=%.1fs 이지만 마지막 스냅으로 평가 code=%s",
|
||||
save_ttl, age, code,
|
||||
)
|
||||
return snap
|
||||
|
||||
|
||||
def _estimate_entry_qty(params: Dict[str, Any], price: float) -> int:
|
||||
@@ -237,7 +256,13 @@ def orderbook_reject_for_entry(
|
||||
|
||||
snap = _fetch_snapshot(params)
|
||||
if snap is None:
|
||||
# 실매 WS 미구독·만료 — 필터·수집 모두 스킵 (REST 부하 없음)
|
||||
# 한 번도 호가 없음(미구독). 만료만으로는 여기 오지 않음(FILTER_MAX_AGE 기본 0).
|
||||
# 필터 ON + REJECT_IF_EMPTY(기본 true) → 이번 루프 안 삼. REST 없음.
|
||||
if filter_on and get_env_bool("WS_ORDERBOOK_FILTER_REJECT_IF_EMPTY", True):
|
||||
return (
|
||||
"탈락-호가없음",
|
||||
"호가창 사진이 없어 이번엔 안 삼 (다음 검사에서 사진 있으면 다시 봄)",
|
||||
)
|
||||
return (None, None)
|
||||
|
||||
verdict = _evaluate_orderbook_verdict(
|
||||
|
||||
@@ -27,24 +27,25 @@ logger = get_logger("kis_trader.post_sell_candle_backfill")
|
||||
_INSERT_SQL_OVERWRITE = """
|
||||
INSERT INTO ws_candles
|
||||
(code, timeframe, candle_time, `open`, high, low, close,
|
||||
volume, rsi_2, rsi_3, rsi_5, is_confirmed, source, updated_at)
|
||||
volume, rsi_2, rsi_3, rsi_5, is_confirmed, source, channel, updated_at)
|
||||
VALUES
|
||||
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
`open`=VALUES(`open`), high=VALUES(high), low=VALUES(low),
|
||||
close=VALUES(close),
|
||||
volume=IF(VALUES(volume) > volume, VALUES(volume), volume),
|
||||
is_confirmed=1, updated_at=VALUES(updated_at),
|
||||
source=IF(VALUES(volume) > volume, VALUES(source), source)
|
||||
source=IF(VALUES(volume) > volume, VALUES(source), source),
|
||||
channel=IF(VALUES(volume) > volume, VALUES(channel), channel)
|
||||
"""
|
||||
|
||||
# freeze ON: 없는 분만 INSERT. 확정·미확정 행이 있으면 OHLCV 유지 (구멍 메우기 전용)
|
||||
_INSERT_SQL_FREEZE = """
|
||||
INSERT INTO ws_candles
|
||||
(code, timeframe, candle_time, `open`, high, low, close,
|
||||
volume, rsi_2, rsi_3, rsi_5, is_confirmed, source, updated_at)
|
||||
volume, rsi_2, rsi_3, rsi_5, is_confirmed, source, channel, updated_at)
|
||||
VALUES
|
||||
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
(%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
candle_time=candle_time
|
||||
"""
|
||||
@@ -150,6 +151,7 @@ def load_kiwoom_credentials(db: Any = None) -> Tuple[str, str, bool]:
|
||||
def _upsert_df_rows(db: Any, code: str, tf_min: int, rows: List[Dict[str, Any]]) -> int:
|
||||
if not rows:
|
||||
return 0
|
||||
from kis_trader.ws.candle_series import normalize_source_channel
|
||||
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
payload = []
|
||||
for rec in rows:
|
||||
@@ -157,6 +159,10 @@ def _upsert_df_rows(db: Any, code: str, tf_min: int, rows: List[Dict[str, Any]])
|
||||
ct = str(rec.get("candle_time") or rec.get("time") or "")[:12]
|
||||
if len(ct) < 12:
|
||||
continue
|
||||
src, ch = normalize_source_channel(
|
||||
str(rec.get("source") or "kiwoom"),
|
||||
str(rec.get("channel") or "rest"),
|
||||
)
|
||||
payload.append((
|
||||
code,
|
||||
int(tf_min),
|
||||
@@ -168,7 +174,8 @@ def _upsert_df_rows(db: Any, code: str, tf_min: int, rows: List[Dict[str, Any]])
|
||||
int(float(rec.get("volume") or 0)),
|
||||
None, None, None,
|
||||
1,
|
||||
str(rec.get("source") or "kw_rest")[:10],
|
||||
src,
|
||||
ch,
|
||||
now_str,
|
||||
))
|
||||
except Exception:
|
||||
@@ -275,7 +282,8 @@ def backfill_hold_window(
|
||||
"low": float(rec.get("low") or cl),
|
||||
"close": cl,
|
||||
"volume": int(float(rec.get("volume") or 0)),
|
||||
"source": "kw_rest",
|
||||
"source": "kiwoom",
|
||||
"channel": "rest",
|
||||
})
|
||||
|
||||
try:
|
||||
@@ -293,7 +301,8 @@ def backfill_hold_window(
|
||||
|
||||
bars3 = rollup_1m_bars_to_tf(rows_1m, 3)
|
||||
for b in bars3:
|
||||
b["source"] = "rollup_1m"
|
||||
b["source"] = "kiwoom"
|
||||
b["channel"] = "rollup"
|
||||
out["rollup3m"] = _upsert_df_rows(db, code, 3, bars3)
|
||||
except Exception as e:
|
||||
logger.debug("3M 롤업 스킵 %s: %s", code, e)
|
||||
|
||||
Reference in New Issue
Block a user