refactor: enhance Optuna backtesting framework, optimize orderbook filtering, and update database management utilities.

This commit is contained in:
Your Name
2026-08-12 10:19:19 +09:00
parent cb7e5037a0
commit c6bd62a25f
218 changed files with 31613 additions and 759 deletions

View File

@@ -226,30 +226,76 @@ def backfill_day(db, day: str, *, slot_min: int = 1) -> Tuple[int, int]:
return len(slot_list), total_rows
def quality_report(db, day: str, *, top: int = 20) -> None:
def quality_report(db, day: str, *, top: int = 20, since: str = "00:00:00") -> None:
"""LS 틱/봉/히스토리/키움후보 대비 퀄리티 출력."""
_ensure_table(db)
d = _day_compact(day)
start_ts = f"{day} {since}"
end_ts = f"{day} 23:59:59.999"
s_compact = since.replace(":", "")
while len(s_compact) < 6:
s_compact += "0"
start_dt_str = f"{d}{s_compact[:6]}"
start_hm_str = f"{d}{s_compact[:4]}"
print("=" * 72)
print(f"LS 종목 히스토리 퀄리티 — day={day}")
print(f"LS 종목 히스토리 퀄리티 — day={day} (분석 기준 시각: {since} ~ 23:59:59)")
print("=" * 72)
tick = db.conn.execute(
print(f"\n[1. 실매매 틱 / 캔들 / 호가 적재 현황 — {day} {since} 이후]")
tick_ls = db.conn.execute(
"SELECT COUNT(*) n, COUNT(DISTINCT code) codes, MIN(ts) mn, MAX(ts) mx "
"FROM ls_ws_ticks WHERE ts >= %s AND ts <= %s",
(f"{day} 00:00:00", f"{day} 23:59:59.999"),
(start_ts, end_ts),
).fetchone()
print(f"[ls_ws_ticks] n={tick['n']:,} codes={tick['codes']} range={tick['mn']} ~ {tick['mx']}")
print(f" 🔸 [LS 틱 (ls_ws_ticks)] n={tick_ls['n']:>9,d} | codes={tick_ls['codes']:>4} | range={tick_ls['mn']} ~ {tick_ls['mx']}")
cndl = db.conn.execute(
try:
tick_kis = db.conn.execute(
"SELECT COUNT(*) n, COUNT(DISTINCT code) codes, MIN(tick_time) mn, MAX(tick_time) mx "
"FROM ws_ticks WHERE tick_time >= %s AND tick_time <= %s",
(start_dt_str, f"{d}235959"),
).fetchone()
print(f" 🔸 [KIS 틱 (ws_ticks)] n={tick_kis['n']:>9,d} | codes={tick_kis['codes']:>4} | range={tick_kis['mn']} ~ {tick_kis['mx']}")
except Exception as e:
print(f" 🔸 [KIS 틱 (ws_ticks)] 조회스킵({e})")
cndl_ls = db.conn.execute(
"SELECT COUNT(*) n, COUNT(DISTINCT code) codes, MIN(datetime) mn, MAX(datetime) mx "
"FROM ls_ws_candles WHERE tf_min=1 AND datetime LIKE %s",
(f"{day}%",),
"FROM ls_ws_candles WHERE tf_min=1 AND datetime >= %s AND datetime <= %s",
(start_ts, end_ts),
).fetchone()
print(
f"[ls_ws_candles 1m] n={cndl['n']:,} codes={cndl['codes']} "
f"range={cndl['mn']} ~ {cndl['mx']}"
)
print(f" 🔸 [LS 분봉 (ls_ws_candles)] n={cndl_ls['n']:>9,d} | codes={cndl_ls['codes']:>4} | range={cndl_ls['mn']} ~ {cndl_ls['mx']}")
try:
cndl_kis = db.conn.execute(
"SELECT COUNT(*) n, COUNT(DISTINCT code) codes, MIN(candle_time) mn, MAX(candle_time) mx "
"FROM ws_candles WHERE timeframe=1 AND candle_time >= %s AND candle_time <= %s",
(start_hm_str, f"{d}2359"),
).fetchone()
print(f" 🔸 [KIS 분봉 (ws_candles)] n={cndl_kis['n']:>9,d} | codes={cndl_kis['codes']:>4} | range={cndl_kis['mn']} ~ {cndl_kis['mx']}")
except Exception as e:
print(f" 🔸 [KIS 분봉 (ws_candles)] 조회스킵({e})")
try:
ob_ls = db.conn.execute(
"SELECT COUNT(*) n, COUNT(DISTINCT code) codes, MIN(snap_time) mn, MAX(snap_time) mx "
"FROM ls_ws_orderbook WHERE snap_time >= %s AND snap_time <= %s",
(start_dt_str, f"{d}235959"),
).fetchone()
print(f" 🔸 [LS 호가 (ls_ws_orderbook)] n={ob_ls['n']:>9,d} | codes={ob_ls['codes']:>4} | range={ob_ls['mn']} ~ {ob_ls['mx']}")
except Exception as e:
print(f" 🔸 [LS 호가 (ls_ws_orderbook)] 조회스킵({e})")
try:
ob_kis = db.conn.execute(
"SELECT COUNT(*) n, COUNT(DISTINCT code) codes, MIN(snap_time) mn, MAX(snap_time) mx "
"FROM ws_orderbook WHERE snap_time >= %s AND snap_time <= %s",
(start_dt_str, f"{d}235959"),
).fetchone()
print(f" 🔸 [KIS 호가 (ws_orderbook)] n={ob_kis['n']:>9,d} | codes={ob_kis['codes']:>4} | range={ob_kis['mn']} ~ {ob_kis['mx']}")
except Exception as e:
print(f" 🔸 [KIS 호가 (ws_orderbook)] 조회스킵({e})")
hist = db.conn.execute(
"SELECT COUNT(*) n, COUNT(DISTINCT code) codes, COUNT(DISTINCT slot_key) slots "
@@ -312,22 +358,45 @@ def quality_report(db, day: str, *, top: int = 20) -> None:
except Exception as e:
print(f"(키움 교차 비교 스킵: {e})")
# KIS↔LS 갭 (validation)
print("\n[2. 증권사 간 시세 및 레이텐시(수신 지연) 3자 비교 실측]")
def _fmt(val):
return f"{val:.1f}ms" if val is not None else "N/A"
try:
v = db.conn.execute(
v1 = db.conn.execute(
"SELECT COUNT(*) n, AVG(ABS(diff_pct)) avg_abs, MAX(ABS(diff_pct)) max_abs, "
"AVG(kis_age_ms) kis_ms, AVG(kiwoom_age_ms) kw_ms "
"FROM ws_price_validation WHERE ts >= %s AND ts <= %s AND diff_pct IS NOT NULL",
(start_ts, end_ts),
).fetchone()
if v1 and v1["n"] > 0:
print(f" 1) KIS ↔ 키움 시세 갭 실측 (ws_price_validation, 표본: {v1['n']:,}건)")
print(f" 👉 가격 괴리율: 평균 {v1['avg_abs']:.4f}% (최대 {v1['max_abs']:.4f}%)")
print(f" 👉 수신 지연(Age): KIS 평균 {_fmt(v1['kis_ms'])} vs 키움 평균 {_fmt(v1['kw_ms'])}")
else:
print(" 1) KIS ↔ 키움 (ws_price_validation): 당일 실측 표본 없음")
except Exception as e:
print(f" (ws_price_validation 스킵: {e})")
try:
v2 = db.conn.execute(
"SELECT COUNT(*) n, "
"AVG(ABS(diff_kis_ls_pct)) avg_abs, "
"MAX(ABS(diff_kis_ls_pct)) max_abs "
"AVG(ABS(diff_kis_ls_pct)) kis_ls_avg, MAX(ABS(diff_kis_ls_pct)) kis_ls_max, "
"AVG(ABS(diff_kw_ls_pct)) kw_ls_avg, MAX(ABS(diff_kw_ls_pct)) kw_ls_max, "
"AVG(kis_age_ms) kis_ms, AVG(kiwoom_age_ms) kw_ms, AVG(ls_age_ms) ls_ms "
"FROM ws_price_validation_ls "
"WHERE ts >= %s AND ts <= %s AND diff_kis_ls_pct IS NOT NULL",
(f"{day} 00:00:00", f"{day} 23:59:59.999"),
(start_ts, end_ts),
).fetchone()
print(
f"\n[ws_price_validation_ls] samples={v['n']:,} "
f"avg|kis-ls|%={v['avg_abs']} max|kis-ls|%={v['max_abs']}"
)
if v2 and v2["n"] > 0:
print(f" 2) KIS ↔ 키움 ↔ LS 삼각 시세 갭 실측 (ws_price_validation_ls, 표본: {v2['n']:,}건)")
print(f" 👉 KIS vs LS 가격 괴리율: 평균 {v2['kis_ls_avg']:.4f}% (최대 {v2['kis_ls_max']:.4f}%)")
print(f" 👉 키움 vs LS 가격 괴리율: 평균 {v2['kw_ls_avg']:.4f}% (최대 {v2['kw_ls_max']:.4f}%)")
print(f" 👉 수신 지연(Age): KIS 평균 {_fmt(v2['kis_ms'])} vs 키움 평균 {_fmt(v2['kw_ms'])} vs LS 평균 {_fmt(v2['ls_ms'])}")
else:
print(" 2) KIS ↔ 키움 ↔ LS (ws_price_validation_ls): 당일 실측 표본 없음")
except Exception as e:
print(f"(validation_ls 스킵: {e})")
print(f" (ws_price_validation_ls 스킵: {e})")
# 최근 슬롯 커버
recent = db.conn.execute(
@@ -351,6 +420,7 @@ def main() -> int:
ap.add_argument("--loop", type=int, default=0, help="N초마다 --collect 반복 (0=OFF)")
ap.add_argument("--quality-every", type=int, default=0, help="루프 N회마다 quality")
ap.add_argument("--top", type=int, default=20, help="퀄리티 상위 종목 수")
ap.add_argument("--since", default="00:00:00", help="이 시각(HH:MM:SS) 이후부터 분석 (기본 00:00:00)")
args = ap.parse_args()
day = _parse_day(args.day)
@@ -370,7 +440,7 @@ def main() -> int:
print(f"[collect] upsert codes={n} slot={_slot_key_from_dt(datetime.now(), args.slot_min)}")
if args.quality and not args.loop:
quality_report(db, day, top=max(5, args.top))
quality_report(db, day, top=max(5, args.top), since=args.since)
if args.loop and args.loop > 0:
i = 0
@@ -381,14 +451,14 @@ def main() -> int:
print(f"[{datetime.now():%H:%M:%S}] collect slot={sk} codes={n}")
i += 1
if args.quality_every > 0 and i % args.quality_every == 0:
quality_report(db, day, top=max(5, args.top))
quality_report(db, day, top=max(5, args.top), since=args.since)
time.sleep(max(5, args.loop))
# 아무 플래그 없으면 backfill+quality 기본
if not any([args.collect, args.backfill, args.quality, args.loop]):
ns, nr = backfill_day(db, day, slot_min=max(1, args.slot_min))
print(f"[backfill] slots={ns} upsert_rows≈{nr}")
quality_report(db, day, top=max(5, args.top))
quality_report(db, day, top=max(5, args.top), since=args.since)
finally:
try:
db.close()