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

@@ -451,7 +451,7 @@ class TradeDBExt:
"""
target_candidates_history 에 필요한 확장 컬럼을 자동 추가:
* strategy_id — 전략 구분 (기존 키움 스캐너 행은 NULL)
* event_time — 스냅샷 시각 (초 단위 'YYYY-MM-DD HH:MM:SS')
* event_time — 스냅샷 시각 ('YYYY-MM-DD HH:MM:SS.ffffff', varchar(26))
"""
if TradeDBExt._HISTORY_MIGRATED:
return
@@ -466,9 +466,32 @@ class TradeDBExt:
if "event_time" not in cols:
self.conn.execute(
"ALTER TABLE target_candidates_history "
"ADD COLUMN event_time VARCHAR(19) DEFAULT NULL"
"ADD COLUMN event_time VARCHAR(26) DEFAULT NULL"
)
logger.info("📌 target_candidates_history.event_time 컬럼 추가")
else:
try:
meta = self.conn.execute(
"SHOW COLUMNS FROM target_candidates_history LIKE %s",
("event_time",),
).fetchone()
typ = str((meta or {}).get("Type") or "").lower()
n = 0
if "varchar" in typ:
import re
m = re.search(r"varchar\((\d+)\)", typ)
n = int(m.group(1)) if m else 0
if n and n < 26:
self.conn.execute(
"ALTER TABLE target_candidates_history "
"MODIFY COLUMN event_time VARCHAR(26) DEFAULT NULL"
)
logger.info(
"📌 target_candidates_history.event_time 길이 %s→26",
n,
)
except Exception as e:
logger.debug("event_time 길이 ALTER: %s", e)
# 인덱스 — 백테스트 조회 속도 확보
for idx_name, idx_def in (
("idx_strategy_event",
@@ -498,10 +521,11 @@ class TradeDBExt:
"""
조건검색 변동(ENTER/EXIT) 발생 tick 마다 호출되는 **풀 스냅샷** 저장.
같은 ``event_time`` 으로 들어온 N 행 = 그 시점의 유니버스 전체.
INSERT만 (같은 초 DELETE 바꿔치기 없음). 한 스냅샷 N행은 한 트랜잭션.
Args:
strategy_id : 'SCALP' | 'SHORT' | 'BREAKOUT' ...
event_time : 'YYYY-MM-DD HH:MM:SS' (초 단위). 백테스트 기준 시각.
event_time : 'YYYY-MM-DD HH:MM:SS.ffffff'. 백테스트 기준 시각.
items : [{"code": "...", "name": "..."}, ...] — 현재 유니버스 전체
slot_key : (선택) 기존 5분 슬롯 키. 과거 대시보드/쿼리 호환용.
@@ -513,55 +537,45 @@ class TradeDBExt:
# slot_key 기본값 — 기존 스키마 NOT NULL 이므로 최소한 값 채움
if not slot_key:
# event_time 'YYYY-MM-DD HH:MM:SS' → 'YYYYMMDDHHMM' (5분 단위 반올림 X, 그대로)
try:
dp = event_time.replace("-", "").replace(":", "").replace(" ", "")
slot_key = dp[:12] # YYYYMMDDHHMM
except Exception:
slot_key = event_time[:12]
inserted = 0
rows = []
for it in items:
code = (it.get("code") or "").strip()
if not code:
continue
raw_name = (it.get("name") or code)[:100]
name = raw_name
if not name or name == code:
try:
from kis_trader.utils.stock_name import resolve_stock_display_name
name = resolve_stock_display_name(
self, code, fallback=code, cache_to_meta=True,
)[:100]
except Exception:
name = code
rows.append(
(slot_key, event_time, code, name, 0.0, 0.0,
"Q", "", "", strategy_id, event_time),
)
if not rows:
return 0
sql = """
INSERT INTO target_candidates_history
(slot_key, scan_time, code, name, score, price,
market, sector, theme, strategy_id, event_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
try:
with self.conn:
# 동일 (전략, event_time) 키로 재호출되면 이전 것 지우고 재기록
# (사실상 중복 호출 방지용 — ConditionSearchManager 가 tick 단위 유니크)
self.conn.execute(
"DELETE FROM target_candidates_history "
"WHERE strategy_id=%s AND event_time=%s",
(strategy_id, event_time),
)
for it in items:
code = (it.get("code") or "").strip()
if not code:
continue
raw_name = (it.get("name") or code)[:100]
name = raw_name
if not name or name == code:
try:
from kis_trader.utils.stock_name import resolve_stock_display_name
name = resolve_stock_display_name(
self, code, fallback=code, cache_to_meta=True,
)[:100]
except Exception:
name = code
try:
self.conn.execute(
"""
INSERT INTO target_candidates_history
(slot_key, scan_time, code, name, score, price,
market, sector, theme, strategy_id, event_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""",
(slot_key, event_time, code, name, 0.0, 0.0,
"Q", "", "", strategy_id, event_time),
)
inserted += 1
except Exception as e:
logger.debug("history insert 실패(%s/%s): %s",
strategy_id, code, e)
n = self.conn.executemany_tx(sql, rows)
return int(n if n and n > 0 else len(rows))
except Exception as e:
logger.warning("history 스냅샷 저장 실패: %s", e)
return inserted
return 0
# 범용 별칭 — 유니버스 소스가 condition 이든 ranking 이든 같은 테이블을 공유
# (함수명 혼동 줄이기 위한 alias. 기존 호출부는 그대로 동작)
@@ -590,13 +604,16 @@ class TradeDBExt:
self, *, strategy_id: str, at_time: str, history_source: str = "kiwoom",
) -> List[Dict]:
"""
``at_time`` ('YYYY-MM-DD HH:MM:SS') 시점에 봇이 보던 유니버스를 복원.
``at_time`` ('YYYY-MM-DD HH:MM:SS' 또는 마이크로초) 시점에 봇이 보던 유니버스를 복원.
= 그 시각 이전의 가장 최근 event_time 스냅샷.
초 단위 조회는 그 초의 **마지막** 마이크로 스냅샷.
``history_source``: ``kiwoom``(target_candidates_history) | ``ls``(ls_candidates_history)
"""
self._ensure_history_columns()
table = self._universe_history_table(history_source)
from kis_trader.backtest.universe_timeline import event_time_query_upper
at_q = event_time_query_upper(at_time)
try:
row = self.conn.execute(
f"""
@@ -604,7 +621,7 @@ class TradeDBExt:
FROM {table}
WHERE strategy_id=%s AND event_time <= %s
""",
(strategy_id, at_time),
(strategy_id, at_q),
).fetchone()
et = (row or {}).get("et") if row else None
if not et:
@@ -642,6 +659,8 @@ class TradeDBExt:
"""
self._ensure_history_columns()
table = self._universe_history_table(history_source)
from kis_trader.backtest.universe_timeline import event_time_query_upper
end_q = event_time_query_upper(end_time)
order_clause = "event_time, id" if preserve_insert_order else "event_time, code"
try:
rows = self.conn.execute(
@@ -652,7 +671,7 @@ class TradeDBExt:
AND event_time BETWEEN %s AND %s
ORDER BY {order_clause}
""",
(strategy_id, start_time, end_time),
(strategy_id, start_time, end_q),
).fetchall()
grouped: Dict[str, List[Dict]] = {}
for r in rows:
@@ -689,10 +708,10 @@ class TradeDBExt:
"""
백테스트 편의용 — 1분봉 캔들 시각(YYYYMMDDHHMM)을 키로 하는 유니버스 dict.
실매매는 10초 주기로 REST 를 돌려 변동 tick 마다 초단위 ``event_time``
(YYYY-MM-DD HH:MM:SS) 으로 ``target_candidates_history`` 에 적재한다.
실매매는 조건검색 변동 tick 마다 마이크로초 ``event_time``
(YYYY-MM-DD HH:MM:SS.ffffff) 으로 ``target_candidates_history`` 에 적재한다.
반면 백테스트 엔진은 1분봉 단위로 돌아가므로, 각 캔들에 대해
**그 캔들의 종가 형성 시각 (HH:MM:59) 이전의 가장 최근 스냅샷** 을
**그 캔들의 종가 형성 시각 (HH:MM:59 초의 마지막 스냅샷) 이전의 가장 최근 스냅샷** 을
선택해 "그 시점에 봇이 보던 유니버스" 를 재현한다.
``strict=True`` (모멘텀 백테 실매 정합):
@@ -734,7 +753,7 @@ class TradeDBExt:
start_time, source=history_source,
)
end_time = (
f"{end_ymd[:4]}-{end_ymd[4:6]}-{end_ymd[6:8]} 23:59:59"
f"{end_ymd[:4]}-{end_ymd[4:6]}-{end_ymd[6:8]} 23:59:59.999999"
)
events = self.iter_universe_events(
@@ -747,17 +766,15 @@ class TradeDBExt:
if not events:
return {}
# 각 스냅샷의 event_time 을 'YYYYMMDDHHMMSS' (14자리 정수비교용) 으로 정규화.
from kis_trader.backtest.universe_timeline import _event_time_to_key
# 각 스냅샷의 event_time 을 'YYYYMMDDHHMMSSffffff' 로 정규화.
# event_time 문자열은 이미 시간순 정렬됨 (iter_universe_events).
normalized: List[tuple] = []
for ev in events:
et = str(ev.get("event_time") or "")
if len(et) < 19:
et_key = _event_time_to_key(str(ev.get("event_time") or ""))
if not et_key:
continue
et_key = (
et[0:4] + et[5:7] + et[8:10]
+ et[11:13] + et[14:16] + et[17:19]
) # YYYYMMDDHHMMSS
codes = [it["code"] for it in ev.get("items", []) if it.get("code")]
normalized.append((et_key, codes))
@@ -816,7 +833,7 @@ class TradeDBExt:
t_end = datetime.combine(day, datetime.min.time()).replace(hour=16)
while t <= t_end:
candle_time = t.strftime("%Y%m%d%H%M")
candle_end_key = candle_time + "59" # YYYYMMDDHHMMSS
candle_end_key = candle_time + "59999999" # YYYYMMDDHHMMSS + 999999
# 그 이전 또는 같은 시각의 가장 최근 event_time 까지 포인터 전진
while ev_idx < n_ev and normalized[ev_idx][0] <= candle_end_key:
et_key, codes = normalized[ev_idx]