#!/usr/bin/env python3 """손익비 후보 소수 조합 — 단일 프로세스·학습+OOS 동시 표 (멀티프로세싱 없음).""" from __future__ import annotations import os import sys from itertools import product 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.backtest.backtest_portfolio_common import load_portfolio_env_row from kis_trader.backtest.param_search_momentum import ( _evaluate_momentum_chunk, _load_candles_for_search, _mom_fixed_defaults, ) from kis_trader.backtest.momentum_rr_crossval import _base_entry_ui, _load_universe def main() -> None: train = ("2026-05-11", "2026-05-30") oos = ("2026-06-01", "2026-06-01") fixed = _mom_fixed_defaults() base = _base_entry_ui(fixed) # 핵심 손익비 후보만 (실행 시간 — 전체 Cartesian 은 param_search --mode rr 사용) candidates = [ (2.0, 2.0, 1.8, 0.5, 0.02), # baseline_live (2.0, 2.5, 2.5, 50.0, 0.25), # shoulder_off (1.5, 2.5, 2.5, 1.0, 0.25), # wide_shoulder (1.2, 3.0, 3.0, 1.2, 0.30), # delayed_shoulder (1.5, 2.0, 2.0, 50.0, 0.20), # tp_only (1.8, 2.2, 2.2, 0.8, 0.20), # loose_shoulder (1.5, 2.5, 2.5, 0.5, 0.15), (1.5, 2.5, 2.5, 1.0, 0.15), (1.8, 2.0, 2.0, 1.0, 0.20), (2.0, 2.5, 2.5, 1.0, 0.15), (1.2, 2.5, 2.5, 1.0, 0.25), (1.5, 3.0, 3.0, 50.0, 0.25), ] combos = [] for sl, tp, tpm, sa, sc in candidates: ui = dict(base) ui.update({ "sl_pct": sl, "tp_pct": tp, "tp_max_pct": tpm, "shoulder_min_high": sa, "shoulder_cut_pct": sc, }) combos.append(ui) print(f"평가 조합: {len(combos)}개 (단일 프로세스)") periods = [("train", train), ("oos", oos)] data_cache = {} uni_cache = {} for label, (s, e) in periods: data_cache[label] = _load_candles_for_search(s, e, 3) uni_cache[label], src = _load_universe(s, e) print(f" {label} {s}~{e}: candles={len(data_cache[label])} uni={src}") env_row = load_portfolio_env_row() from kis_trader.backtest import scalping_backtest_common as sbc fee, tax, _ = sbc.fee_and_slot_from_env(env_row, strategy="MOMENTUM") rows = [] keys = list(combos[0].keys()) if combos else [] for i, ui in enumerate(combos, 1): row_train = row_oos = None for label, _ in periods: heap = _evaluate_momentum_chunk( [ui], fixed, keys, data_cache[label], 1, 0.0, 0.0, 1, uni_cache[label], 200_000, 20, 2_000_000, fee, tax, 20 if label == "train" else 1, ) if heap: _, _, _, pkg = heap[0] if label == "train": row_train = pkg else: row_oos = pkg if not row_train or not row_oos: continue rows.append({ "sl": ui["sl_pct"], "tp": ui["tp_pct"], "tpmax": ui["tp_max_pct"], "sh": f"{ui['shoulder_min_high']}/{ui['shoulder_cut_pct']}", "train_pnl": row_train["total_pnl"], "oos_pnl": row_oos["total_pnl"], "train_pf": row_train["pf"], "oos_pf": row_oos["pf"], "oos_trades": row_oos["total_trades"], }) if i % 10 == 0: print(f" ... {i}/{len(combos)}") rows.sort(key=lambda r: (r["oos_pnl"], r["train_pnl"]), reverse=True) print("\n=== OOS 손익 우선 TOP 10 (train 5/11~30 + oos 6/1) ===") print(f"{'sl':>4} {'tp':>4} {'max':>4} {'shoulder':>12} {'train':>10} {'oos':>10} {'oos_pf':>6}") for r in rows[:10]: print( f"{r['sl']:4.1f} {r['tp']:4.1f} {r['tpmax']:4.1f} {r['sh']:>12} " f"{r['train_pnl']:>10,} {r['oos_pnl']:>10,} {r['oos_pf']:>6.2f}" ) pos_both = [r for r in rows if r["train_pnl"] > 0 and r["oos_pnl"] > 0] print(f"\n학습·OOS 둘 다 플러스: {len(pos_both)}건") if pos_both: b = pos_both[0] print(f" 추천: sl={b['sl']} tp={b['tp']} tpmax={b['tpmax']} shoulder={b['sh']}") if __name__ == "__main__": main()