Files
kis_bot/kis_trader/backtest/param_search_updow_us.py
Your Name 0ecac7cb95 이번에 들어간 내용
한투 호가 = 2번째 앱키 전용
키 없거나 start 실패 시 메인에 H0STASP0 안 붙임. 운영설정 WS_ORDERBOOK_SAVE_KIS 빨간 danger.

LS RAM 합집합
후보∪보유∪영구∪grace. sync_targets와 split reconcile 둘 다. 틱 DB 영구 게이트는 그대로.

분봉 쓰레기 → 다음 소스 봉 통째
그 분 틱 0건이거나 전부 봉끝 대비 LIVE_FEED_FALLBACK_MAX_AGE_SEC 초과면 구멍. 메인 WS → 2차 → LS → REST → rollup. CANDLE_GARBAGE_FALLBACK 기본 true.

파일: feed_fallback.py(신규), ws_manager.py, kis_ws.py, candle_series.py, bt_candle_source.py, live_config_schema.py, database.py, 스모크, MD 2개.

같은 ws_manager/database/kis_ws/live_config에는 직전 커밋 이후 쌓여 있던 시세 폴백·ENV 키 정리도 같이 들어갔습니다. 파일 단위로 나눌 수 없어서입니다.
2026-08-19 22:11:31 +09:00

287 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
kis_trader/backtest/param_search_updow_us.py — UPDOW 해외(US) 종목 파라미터 Grid Search
======================================================================================
- 국내용 ``param_search_updow.py`` 와 동일 엔진(``run_param_search_updow``)
- **차이**: 영문 티커(QQQM 등), ``holding_min_candles`` 해외 분봉(KIS), **코스피 레짐(069500) 미사용**, 어깨 그리드는 국내와 동일
실행:
cd /home/hoon/kis_bot
python3 kis_trader/backtest/param_search_updow_us.py --symbol QQQM --start 2026-05-01 --end 2026-05-29 --tf 15
python3 -m kis_trader.backtest.param_search_updow_us --symbol QQQM --exchange NASD --mode fast --save-top
옵션:
--symbol 해외 티커 (필수, 예: QQQM). --code 와 동일
--exchange 거래소 (기본 NASD → KIS 시세 API는 NAS 로 변환)
--start / --end / --tf / --mode / --max-combos / --min_trades / --top / --out / --save-top
→ 의미는 국내 CLI 와 동일
탐색 규모(기본):
fast : US 전용 경량 그리드 = 2,880조합 (limit_atr_mult 0.0~1.5 포함, 레짐 OFF).
1회 백테 ≈ 0.25초(60분 400봉) → **약 10~15분**. 캡은 env UPDOW_US_FAST_MAX_COMBOS.
※ 저변동 지수 ETF는 깊은 지정가(ATR×1.5)가 거의 체결 안 되므로 낮은 limit_atr_mult 가 핵심.
full : 54,000조합 (≈ 수 시간) — 정밀 탐색용.
사전 준비:
웹 UPDOW 탭 또는 API 로 해당 심볼 분봉을 ``holding_min_candles`` 에 수집해 두세요.
(market_type=US, exchange=NASD, symbol=QQQM)
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import sys
from datetime import datetime, timedelta
from typing import Any, Dict, List, Tuple
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(HERE))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)
from kis_trader.utils.legacy_root import ensure_legacy_root # noqa: E402
ensure_legacy_root()
import holding_bot as hb # noqa: E402
from database import TradeDB # noqa: E402
from kis_trader.strategies import updow_buy as udb # noqa: E402
from kis_trader.strategies import updow_holding_cfg as uhc # noqa: E402
from kis_trader.strategies.updow_holding_cfg import ( # noqa: E402
ensure_updow_backtest_tables,
load_updow_engine_cfg,
set_updow_stock_config,
)
logging.basicConfig(level=logging.INFO, format="%(message)s")
logger = logging.getLogger("param_search_updow_us")
def _db() -> TradeDB:
db = TradeDB()
ensure_updow_backtest_tables(db)
return db
def _results_dir() -> str:
d = os.path.join(HERE, "results")
os.makedirs(d, exist_ok=True)
return d
def _parse_symbol_exchange(args: argparse.Namespace) -> Tuple[str, str, str, str]:
"""(code, symbol, market_type, exchange) — DB·holding_min_candles 키는 code=symbol."""
raw = str(args.symbol or args.code or "").strip().upper()
if not raw:
raise ValueError("--symbol (또는 --code) 필수 (예: QQQM)")
ex_in = str(args.exchange or "NASD").strip().upper()
mt_in = str(args.market_type or "US").strip().upper()
mt, ex, sym = uhc.resolve_market_meta(raw, mt_in, ex_in, raw)
if mt != "US":
raise ValueError(
f"해외 전용 CLI 입니다. symbol={raw} → market_type={mt} (US 필요). "
"국내 6자리는 param_search_updow.py 를 사용하세요."
)
code = sym
return code, sym, mt, ex
def main(argv: List[str] | None = None) -> int:
from datetime import date, timedelta
from kis_trader.utils.kr_trading_day import clamp_to_prev_weekday
today = clamp_to_prev_weekday(date.today())
default_start = clamp_to_prev_weekday(
date.fromisoformat(today) - timedelta(days=365)
)
ap = argparse.ArgumentParser(
description="UPDOW 해외(US) 파라미터 그리드 탐색 (레짐 OFF, holding_min_candles)"
)
ap.add_argument("--symbol", default="", help="해외 티커 (예: QQQM)")
ap.add_argument("--code", default="", help="--symbol 과 동일 (호환)")
ap.add_argument("--exchange", default="NASD", help="거래소 코드 (NASD/NYSE/AMEX)")
ap.add_argument("--market-type", default="US", dest="market_type", help="시장 (기본 US)")
ap.add_argument("--start", default=default_start, help="시작일 YYYY-MM-DD (주말→이전 평일)")
ap.add_argument("--end", default=today, help="종료일 YYYY-MM-DD (주말→이전 평일)")
ap.add_argument("--tf", type=int, default=-1, help="분봉 (기본 -1 = env UPDOW_TF_MIN)")
ap.add_argument("--mode", choices=["fast", "full"], default="fast", help="fast|full")
ap.add_argument("--max-combos", type=int, default=-1, help="조합 상한 (0=무제한)")
ap.add_argument("--min_trades", type=int, default=1, help="최소 거래 건수")
ap.add_argument("--top", type=int, default=30, help="상위 N개 출력")
ap.add_argument("--out", default="", help="JSON 경로")
ap.add_argument("--save-top", action="store_true", help="1위 → updow_stock_config INSERT")
args = ap.parse_args(argv)
try:
code, symbol, market_type, exchange = _parse_symbol_exchange(args)
except ValueError as e:
logger.error("%s", e)
return 1
db = _db()
try:
latest = db.get_latest_env()
snap = dict(latest["snapshot"]) if latest else {}
tf_default = udb.read_updow_tf_min(snap)
tf = int(args.tf) if int(args.tf) >= 0 else tf_default
if tf not in hb.KIWOOM_MINUTE_TICS:
logger.error("tf=%s 는 허용 목록에 없습니다: %s", tf, hb.KIWOOM_MINUTE_TICS)
return 1
candles = hb.get_stored_min_candles(db, code, args.start, args.end, tf_min=tf)
if not candles:
logger.error(
"%s(%s) %s분봉 없음 (%s ~ %s).\n"
" → 웹 UPDOW 탭에서 [US:%s] %s 분봉 수집 후 재시도.",
symbol,
exchange,
tf,
args.start,
args.end,
exchange,
symbol,
)
return 2
if len(candles) < 5:
logger.error("봉 부족: %s개 (최소 5)", len(candles))
return 3
base_cfg = udb.cfg_from_env_snapshot(snap)
base_cfg = load_updow_engine_cfg(db, code, base_cfg)
# 해외: 코스피 레짐(069500) 미적용 — 종목 설정에 60이 있어도 탐색 베이스는 OFF
base_cfg["regime_ma_bars"] = 0.0
base_cfg["regime_ma_ease_pct"] = 0.0
mode = str(args.mode or "fast").strip().lower()
if mode == "full":
grid = udb.default_param_grid_us(snap)
max_combos = 0 if args.max_combos < 0 else max(0, int(args.max_combos))
else:
grid = udb.default_param_grid_us_fast(snap)
# fast 기본 캡 = env UPDOW_US_FAST_MAX_COMBOS (기본 2880 ≈ 10~15분)
max_combos = (
udb.read_us_fast_max_combos(snap)
if args.max_combos < 0
else max(0, int(args.max_combos))
)
results, meta = udb.run_param_search_updow(
candles,
grid=grid,
min_trades=max(0, int(args.min_trades)),
base_cfg=base_cfg,
env_snapshot=snap,
regime_candles=None,
stock_tf_min=tf,
max_combos=max_combos,
)
meta["market_type"] = market_type
meta["exchange"] = exchange
meta["symbol"] = symbol
meta["regime_proxy_code"] = None
meta["regime_1min_bars_loaded"] = 0
meta["regime_note"] = "US: KOSPI proxy regime disabled"
meta["search_mode"] = mode
meta["combo_cap"] = max_combos
meta["tf"] = tf
meta["code"] = code
meta["start"] = args.start
meta["end"] = args.end
meta["candle_count"] = len(candles)
top_n = max(1, int(args.top))
top = results[:top_n]
logger.info(
"=== UPDOW US param search %s [%s:%s] tf=%s 봉=%s ===\n"
"그리드 조합=%s 백테=%s 통과=%s (min_trades=%s, 레짐=OFF)",
symbol,
market_type,
exchange,
tf,
len(candles),
meta.get("cartesian_product"),
meta.get("backtests_run"),
meta.get("passed"),
args.min_trades,
)
hints = meta.get("grid_axis_hints") or {}
if hints:
logger.info("--- 그리드 축 ---")
for k, txt in hints.items():
logger.info(" [%s] %s", k, txt)
for i, row in enumerate(top, 1):
p = row.get("params") or {}
logger.info(
"#%s pnl=%s win=%s%% trades=%s pf=%s hold=%s params=%s",
i,
row.get("total_pnl"),
row.get("win_rate"),
row.get("total_trades"),
row.get("pf"),
row.get("avg_hold"),
p,
)
if args.save_top:
if not results:
logger.error("--save-top 지정했으나 통과한 결과가 없습니다.")
return 4
ac = results[0].get("apply_cfg") or {}
ac["regime_ma_bars"] = 0.0
ac["regime_ma_ease_pct"] = 0.0
nm = symbol
m = uhc.get_updow_stock_meta(db, code)
if m and m.get("name"):
nm = str(m["name"]).strip() or symbol
set_updow_stock_config(
db,
code,
nm,
ac,
tf_min=tf,
market_type=market_type,
exchange=exchange,
symbol=symbol,
)
logger.info(
"updow_stock_config 저장 (%s US:%s tf=%s max_hold=%s)",
code,
exchange,
tf,
int(float(ac.get("max_hold_bars", 16))),
)
payload: Dict[str, Any] = {
"code": code,
"symbol": symbol,
"market_type": market_type,
"exchange": exchange,
"tf": tf,
"start": args.start,
"end": args.end,
"meta": meta,
"top": top,
}
out_path = args.out.strip()
if not out_path:
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out_path = os.path.join(
_results_dir(),
f"updow_param_us_{symbol}_{tf}m_{ts}.json",
)
with open(out_path, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
logger.info("저장: %s", out_path)
return 0
finally:
db.close()
if __name__ == "__main__":
raise SystemExit(main())