feat(tests): 신규 키움 웹소켓 조건검색 및 실시간 조건검색 테스트 추가
변경 사항 ---- - _test_kiwoom_condition_list.py: 키움 웹소켓 조건검색 '목록조회' 기능을 단독으로 테스트하는 스크립트 추가 - _test_kiwoom_condition_realtime.py: 'momentum' 조건식을 실시간으로 등록하고 초기 매칭 종목 리스트 및 실시간 편입/이탈을 수신하는 테스트 스크립트 추가 - _verify_columnar_bitid.py, _verify_shared_e2e_breakout.py, _verify_shared_e2e.py: 공유 메모리 및 dict 간의 데이터 일관성을 검증하는 테스트 추가 영향 ---- - 신규 테스트 스크립트 추가로 키움 웹소켓 API의 기능 검증 및 안정성을 높임 - 기존 기능에 대한 영향 없음 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
567
kis_trader/backtest/momentum_random_benchmark.py
Normal file
567
kis_trader/backtest/momentum_random_benchmark.py
Normal file
@@ -0,0 +1,567 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
모멘텀 vs 무작위 진입 벤치마크 — param_search 와 동일 데이터·포트폴리오·청산.
|
||||
|
||||
청산: ``check_sell_signal_momentum_backtest_bar`` (실매 ``check_sell_signal_momentum_live`` 동일).
|
||||
무작위: 유니버스(MOMENTUM history) + 매매시간 + 쿨다운·일일한도만 맞추고,
|
||||
TRIGGER(RSI·vol·EMA) 없이 슬롯마다 후보 1종목 무작위 선택.
|
||||
|
||||
실행 (기본: 백그라운드 — nohup 불필요):
|
||||
cd /home/hoon/kis_bot
|
||||
python3 -m kis_trader.backtest.momentum_random_benchmark \\
|
||||
--start 2026-06-01 --end 2026-06-14 --seeds 100
|
||||
|
||||
# 포그라운드(터미널 붙잡기)가 필요할 때만:
|
||||
python3 -m kis_trader.backtest.momentum_random_benchmark --foreground --seeds 10
|
||||
|
||||
로그: /tmp/mom_random_bench.log (기본) · PID: /tmp/mom_random_bench.pid
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
_BG_WORKER_ENV = "MOM_RANDOM_BENCH_WORKER"
|
||||
DEFAULT_LOG_PATH = "/tmp/mom_random_bench.log"
|
||||
DEFAULT_PID_PATH = "/tmp/mom_random_bench.pid"
|
||||
|
||||
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 database import TradeDB # noqa: E402
|
||||
from kis_trader.backtest import momentum_backtest_common as mbc # noqa: E402
|
||||
from kis_trader.backtest import scalping_backtest_common as sbc # noqa: E402
|
||||
from kis_trader.backtest.backtest_portfolio_common import ( # noqa: E402
|
||||
attach_scalp_trade_pnl,
|
||||
backtest_slip_pct,
|
||||
min_invest_ratio_of_slot,
|
||||
portfolio_exposure_krw,
|
||||
target_qty_and_cost,
|
||||
)
|
||||
from kis_trader.backtest.momentum_portfolio_backtest import ( # noqa: E402
|
||||
_buy_priority_key,
|
||||
_max_stocks_from_params,
|
||||
_resolve_invest_cap_krw,
|
||||
_total_budget_from_params,
|
||||
)
|
||||
from kis_trader.backtest.param_search_momentum import ( # noqa: E402
|
||||
_load_candles_for_search,
|
||||
_mom_fixed_defaults,
|
||||
_ui_to_engine_params,
|
||||
)
|
||||
from kis_trader.engine.momentum_engine import ( # noqa: E402
|
||||
MOMENTUM_STRATEGY_ID,
|
||||
_slot_key,
|
||||
_t2dt,
|
||||
_to_bool,
|
||||
check_sell_signal_momentum_backtest_bar,
|
||||
effective_tp_pct_from_params,
|
||||
eval_momentum_buy_at_index,
|
||||
)
|
||||
|
||||
|
||||
def _hm_from_candle_time(t: str) -> int:
|
||||
s = str(t)[8:12]
|
||||
return int(s) if len(s) >= 4 else 0
|
||||
|
||||
|
||||
def _session_ok(t: str, params: Dict[str, Any]) -> bool:
|
||||
hm = _hm_from_candle_time(t)
|
||||
ts = int(params.get("time_start_hm", 900))
|
||||
te = int(params.get("time_end_hm", 1530))
|
||||
return ts <= hm < te
|
||||
|
||||
|
||||
def _cooldown_ok(t: str, day: str, last_exit_dt, cooldown_min: float) -> bool:
|
||||
if not last_exit_dt:
|
||||
return True
|
||||
try:
|
||||
from datetime import datetime as _dt
|
||||
cur = _dt.strptime(t, "%Y%m%d%H%M%S")
|
||||
last = last_exit_dt if hasattr(last_exit_dt, "year") else _t2dt(str(last_exit_dt))
|
||||
elapsed = (cur - last).total_seconds() / 60.0
|
||||
return elapsed >= float(cooldown_min)
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
def run_portfolio(
|
||||
codes_candles: Dict[str, List[Dict]],
|
||||
params: Dict[str, Any],
|
||||
universe_by_slot: Optional[Dict[str, List[str]]],
|
||||
*,
|
||||
random_seed: Optional[int] = None,
|
||||
) -> List[Dict]:
|
||||
"""시각순 포트폴리오 — random_seed 있으면 무작위 진입."""
|
||||
rng = random.Random(random_seed) if random_seed is not None else None
|
||||
rsi_period = int(params.get("rsi_period", 3))
|
||||
min_bars = max(rsi_period + 5, 6)
|
||||
force_eod_exit = _to_bool(params.get("force_eod_exit"), False)
|
||||
sl_pct = abs(float(params.get("sl_pct", 0.015)))
|
||||
tp_pct = effective_tp_pct_from_params(params)
|
||||
max_stocks = _max_stocks_from_params(params)
|
||||
slot_money = float(params.get("slot_money", 300_000))
|
||||
total_budget = _total_budget_from_params(params)
|
||||
if total_budget <= 0:
|
||||
total_budget = float(max_stocks * slot_money)
|
||||
min_invest_ratio = min_invest_ratio_of_slot(params, strategy=MOMENTUM_STRATEGY_ID)
|
||||
invest_cap = _resolve_invest_cap_krw(params, slot_money)
|
||||
cooldown_min = float(params.get("cooldown_min", 10))
|
||||
max_daily = int(params.get("max_daily", 5))
|
||||
|
||||
ctx_by_code: Dict[str, Dict[str, Any]] = {}
|
||||
all_times_set = set()
|
||||
for code, raw_rows in codes_candles.items():
|
||||
if len(raw_rows) < min_bars:
|
||||
continue
|
||||
candles = [dict(r) for r in raw_rows]
|
||||
ctx_by_code[code] = {
|
||||
"code": code,
|
||||
"candles": candles,
|
||||
"time_index": {c["candle_time"]: idx for idx, c in enumerate(candles)},
|
||||
"last_exit_dt": {},
|
||||
"daily_cnt": {},
|
||||
"pending_entry": None,
|
||||
}
|
||||
for c in candles:
|
||||
all_times_set.add(c["candle_time"])
|
||||
|
||||
all_times = sorted(all_times_set)
|
||||
portfolio: Dict[str, Dict[str, Any]] = {}
|
||||
all_trades: List[Dict] = []
|
||||
|
||||
for t in all_times:
|
||||
if not _session_ok(t, params):
|
||||
continue
|
||||
slot_key = _slot_key(t, int(params.get("scan_interval_min", 1)))
|
||||
|
||||
pending_codes = [
|
||||
code for code, ctx in ctx_by_code.items()
|
||||
if ctx.get("pending_entry") and ctx["pending_entry"].get("entry_time") == t
|
||||
]
|
||||
pending_codes.sort(key=lambda c: _buy_priority_key(c, slot_key, universe_by_slot))
|
||||
for code in pending_codes:
|
||||
ctx = ctx_by_code[code]
|
||||
pe = ctx.pop("pending_entry", None)
|
||||
if not pe or code in portfolio:
|
||||
continue
|
||||
if len(portfolio) >= max_stocks:
|
||||
break
|
||||
entry_price = float(pe["entry_price"])
|
||||
if entry_price <= 0:
|
||||
continue
|
||||
exposure = portfolio_exposure_krw(portfolio)
|
||||
remaining = max(0.0, total_budget - exposure)
|
||||
target_qty, target_cost = target_qty_and_cost(entry_price, invest_cap)
|
||||
min_required = target_cost * min_invest_ratio
|
||||
if target_qty < 1 or remaining < min_required:
|
||||
continue
|
||||
invest = min(invest_cap, remaining, target_cost)
|
||||
qty = int(invest / entry_price)
|
||||
if qty < 1:
|
||||
continue
|
||||
cost = qty * entry_price
|
||||
if cost < min_required or exposure + cost > total_budget + 1e-6:
|
||||
continue
|
||||
portfolio[code] = {
|
||||
"entry_price": entry_price,
|
||||
"entry_time": t,
|
||||
"qty": qty,
|
||||
"stop": pe["stop"],
|
||||
"target": pe["target"],
|
||||
"max_price": entry_price,
|
||||
"rsi": pe.get("rsi"),
|
||||
}
|
||||
break
|
||||
|
||||
for code in list(portfolio.keys()):
|
||||
ctx = ctx_by_code.get(code)
|
||||
if ctx is None:
|
||||
continue
|
||||
idx = ctx["time_index"].get(t)
|
||||
if idx is None:
|
||||
continue
|
||||
candles = ctx["candles"]
|
||||
c = candles[idx]
|
||||
day = t[:8]
|
||||
if t == portfolio[code]["entry_time"]:
|
||||
continue
|
||||
is_eod_raw = (idx == len(candles) - 1) or (candles[idx + 1]["candle_time"][:8] != day)
|
||||
is_eod = is_eod_raw and force_eod_exit
|
||||
cur_c_info = {
|
||||
"open": float(c["open"]),
|
||||
"high": float(c["high"]),
|
||||
"low": float(c["low"]),
|
||||
"close": float(c["close"]),
|
||||
"candle_time": t,
|
||||
}
|
||||
pos = portfolio[code]
|
||||
res = check_sell_signal_momentum_backtest_bar(pos, cur_c_info, params, is_eod=is_eod)
|
||||
if not res:
|
||||
continue
|
||||
reason, exit_price = res
|
||||
trade: Dict[str, Any] = {
|
||||
"code": code,
|
||||
"buy_time": pos["entry_time"],
|
||||
"sell_time": t,
|
||||
"buy_price": pos["entry_price"],
|
||||
"sell_price": round(exit_price, 2),
|
||||
"qty": pos.get("qty", 1),
|
||||
"pnl": 0,
|
||||
"sell_reason": reason,
|
||||
"hold_min": 0,
|
||||
"strategy": MOMENTUM_STRATEGY_ID,
|
||||
}
|
||||
all_trades.append(trade)
|
||||
ctx["last_exit_dt"][day] = _t2dt(t)
|
||||
del portfolio[code]
|
||||
|
||||
if len(portfolio) >= max_stocks:
|
||||
continue
|
||||
if portfolio_exposure_krw(portfolio) >= total_budget - 1e-6:
|
||||
continue
|
||||
|
||||
candidates: List[Tuple[Tuple[int, str], str, Dict[str, Any]]] = []
|
||||
for code, ctx in ctx_by_code.items():
|
||||
if code in portfolio or ctx.get("pending_entry"):
|
||||
continue
|
||||
idx = ctx["time_index"].get(t)
|
||||
if idx is None:
|
||||
continue
|
||||
candles = ctx["candles"]
|
||||
c = candles[idx]
|
||||
day = t[:8]
|
||||
cl = float(c["close"])
|
||||
if universe_by_slot is not None and code not in universe_by_slot.get(slot_key, []):
|
||||
continue
|
||||
if cl <= 0 or idx < 5:
|
||||
continue
|
||||
if ctx["daily_cnt"].get(day, 0) >= max_daily:
|
||||
continue
|
||||
if not _cooldown_ok(t, day, ctx["last_exit_dt"].get(day), cooldown_min):
|
||||
continue
|
||||
if idx + 1 >= len(candles):
|
||||
continue
|
||||
next_c = candles[idx + 1]
|
||||
if next_c["candle_time"][:8] != day:
|
||||
continue
|
||||
entry_price = float(next_c["open"])
|
||||
if entry_price <= 0:
|
||||
continue
|
||||
|
||||
if rng is None:
|
||||
eval_params = dict(params)
|
||||
eval_params.setdefault("skip_hts_scan_dupes", universe_by_slot is not None)
|
||||
state = {
|
||||
"daily_cnt": ctx["daily_cnt"].get(day, 0),
|
||||
"last_exit_dt": ctx["last_exit_dt"].get(day),
|
||||
}
|
||||
reject, _msg, sig = eval_momentum_buy_at_index(candles, idx, eval_params, state)
|
||||
if reject or not sig:
|
||||
continue
|
||||
pe_data: Dict[str, Any] = {
|
||||
"entry_time": next_c["candle_time"],
|
||||
"entry_price": entry_price,
|
||||
"stop": entry_price * (1 - sl_pct),
|
||||
"target": entry_price * (1 + tp_pct),
|
||||
"rsi": sig.get("rsi"),
|
||||
}
|
||||
else:
|
||||
pe_data = {
|
||||
"entry_time": next_c["candle_time"],
|
||||
"entry_price": entry_price,
|
||||
"stop": entry_price * (1 - sl_pct),
|
||||
"target": entry_price * (1 + tp_pct),
|
||||
"rsi": None,
|
||||
}
|
||||
candidates.append((_buy_priority_key(code, slot_key, universe_by_slot), code, pe_data))
|
||||
|
||||
if not candidates:
|
||||
continue
|
||||
if rng is not None:
|
||||
_pri, pick_code, pe = rng.choice(candidates)
|
||||
else:
|
||||
candidates.sort(key=lambda x: x[0])
|
||||
_pri, pick_code, pe = candidates[0]
|
||||
ctx_by_code[pick_code]["pending_entry"] = pe
|
||||
ctx_by_code[pick_code]["daily_cnt"][t[:8]] = (
|
||||
ctx_by_code[pick_code]["daily_cnt"].get(t[:8], 0) + 1
|
||||
)
|
||||
|
||||
fee_rate = float(params.get("fee_rate", 0.00015))
|
||||
sell_tax = float(params.get("sell_tax", 0.0018))
|
||||
attach_scalp_trade_pnl(
|
||||
all_trades, fee_rate=fee_rate, sell_tax=sell_tax,
|
||||
slip_pct=backtest_slip_pct(params),
|
||||
)
|
||||
return all_trades
|
||||
|
||||
|
||||
def _build_engine_params(ui: Dict[str, Any], fixed: Dict[str, Any]) -> Dict[str, Any]:
|
||||
merged = dict(fixed)
|
||||
merged.update(ui)
|
||||
return _ui_to_engine_params(merged)
|
||||
|
||||
|
||||
def _load_universe(start: str, end: str) -> Optional[Dict[str, List[str]]]:
|
||||
start_ymd = start.replace("-", "")
|
||||
end_ymd = end.replace("-", "")
|
||||
try:
|
||||
universe, _, _, _, _ = mbc.resolve_momentum_universe(
|
||||
start_ymd, end_ymd, use_saved_history=True, strategy_id="MOMENTUM",
|
||||
)
|
||||
return universe
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _stats(trades: List[Dict], total_budget: float, period_days: int) -> Dict[str, Any]:
|
||||
return mbc.summarize_momentum_trades(
|
||||
trades, total_budget_krw=total_budget, period_days=period_days,
|
||||
)
|
||||
|
||||
|
||||
def _print_row(label: str, st: Dict[str, Any]) -> None:
|
||||
print(
|
||||
f" {label:<22} | 손익 {st['total_pnl']:>10,.0f}원 | "
|
||||
f"거래 {st['total_trades']:>3} | 승률 {st['win_rate']:>5.1f}% | "
|
||||
f"PF {st['pf']:>5.2f} | MDD {st.get('mdd_krw', st.get('mdd', 0)):,.0f}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
|
||||
def _read_running_pid(pid_path: str) -> Optional[int]:
|
||||
try:
|
||||
with open(pid_path, "r", encoding="utf-8") as f:
|
||||
pid = int(f.read().strip())
|
||||
os.kill(pid, 0)
|
||||
return pid
|
||||
except (OSError, ValueError, ProcessLookupError):
|
||||
return None
|
||||
|
||||
|
||||
def _write_pid(pid_path: str) -> None:
|
||||
with open(pid_path, "w", encoding="utf-8") as f:
|
||||
f.write(str(os.getpid()))
|
||||
|
||||
|
||||
def _clear_pid(pid_path: str) -> None:
|
||||
try:
|
||||
if _read_running_pid(pid_path) == os.getpid():
|
||||
os.remove(pid_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _spawn_background(log_path: str, pid_path: str) -> int:
|
||||
"""부모는 즉시 반환 — 워커는 detached 세션에서 로그 파일로 출력."""
|
||||
running = _read_running_pid(pid_path)
|
||||
if running:
|
||||
print(f"⛔ 이미 실행 중 (pid={running})", flush=True)
|
||||
print(f" tail -f {log_path}", flush=True)
|
||||
return 2
|
||||
|
||||
os.makedirs(os.path.dirname(log_path) or ".", exist_ok=True)
|
||||
log_f = open(log_path, "a", encoding="utf-8")
|
||||
stamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
log_f.write(f"\n[{stamp}] 백그라운드 워커 시작\n")
|
||||
log_f.flush()
|
||||
|
||||
child_argv = [sys.executable, "-u"] + sys.argv[1:]
|
||||
if "--foreground" not in child_argv:
|
||||
child_argv.append("--foreground")
|
||||
|
||||
env = os.environ.copy()
|
||||
env[_BG_WORKER_ENV] = "1"
|
||||
|
||||
proc = subprocess.Popen(
|
||||
child_argv,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=log_f,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=ROOT,
|
||||
env=env,
|
||||
start_new_session=True,
|
||||
)
|
||||
log_f.close()
|
||||
try:
|
||||
with open(pid_path, "w", encoding="utf-8") as pf:
|
||||
pf.write(str(proc.pid))
|
||||
except OSError:
|
||||
pass
|
||||
print(f"✅ 백그라운드 시작 pid={proc.pid}", flush=True)
|
||||
print(f" 로그: {log_path}", flush=True)
|
||||
print(f" 확인: tail -f {log_path}", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="모멘텀 vs 무작위 진입 벤치마크")
|
||||
parser.add_argument("--start", default="2026-06-01")
|
||||
parser.add_argument("--end", default="2026-06-14")
|
||||
parser.add_argument("--seeds", type=int, default=100, help="무작위 시드 반복 횟수")
|
||||
parser.add_argument("--json-rank1", default="", help="search_momentum JSON (1위 params)")
|
||||
parser.add_argument(
|
||||
"--foreground", action="store_true",
|
||||
help="포그라운드 실행 (기본: 백그라운드)",
|
||||
)
|
||||
parser.add_argument("--log", default=DEFAULT_LOG_PATH, help="백그라운드 로그 경로")
|
||||
parser.add_argument("--pid-file", default=DEFAULT_PID_PATH, help="실행 중 PID 파일")
|
||||
args = parser.parse_args()
|
||||
|
||||
is_worker = os.environ.get(_BG_WORKER_ENV) == "1" or args.foreground
|
||||
if not is_worker:
|
||||
return _spawn_background(args.log, args.pid_file)
|
||||
|
||||
_write_pid(args.pid_file)
|
||||
try:
|
||||
return _run_benchmark(args)
|
||||
finally:
|
||||
_clear_pid(args.pid_file)
|
||||
|
||||
|
||||
def _run_benchmark(args: argparse.Namespace) -> int:
|
||||
t0 = time.time()
|
||||
fixed = _mom_fixed_defaults()
|
||||
rsi_period = int(fixed.get("rsi_period", 3))
|
||||
|
||||
db = TradeDB()
|
||||
try:
|
||||
row = db.conn.execute("SELECT * FROM env_config ORDER BY id DESC LIMIT 1").fetchone()
|
||||
env_row = dict(row) if row else {}
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
fee_rate, sell_tax, slot_from_env = sbc.fee_and_slot_from_env(env_row, strategy="MOMENTUM")
|
||||
portfolio = sbc.resolve_scalp_portfolio_params(
|
||||
env_row, None, strategy="MOMENTUM", slot_money=slot_from_env,
|
||||
)
|
||||
slot_money = float(portfolio["slot_money"])
|
||||
max_stocks = int(portfolio["max_stocks"])
|
||||
total_budget = float(portfolio["total_budget_krw"])
|
||||
period_days = max(
|
||||
1,
|
||||
(datetime.strptime(args.end, "%Y-%m-%d") - datetime.strptime(args.start, "%Y-%m-%d")).days + 1,
|
||||
)
|
||||
|
||||
json_path = args.json_rank1
|
||||
if not json_path:
|
||||
json_path = os.path.join(
|
||||
HERE, "results", "search_momentum_fast_20260615_012529.json",
|
||||
)
|
||||
with open(json_path, "r", encoding="utf-8") as f:
|
||||
search_data = json.load(f)
|
||||
rank1_ui = dict((search_data.get("top") or [{}])[0].get("params") or {})
|
||||
|
||||
rank1_engine = _build_engine_params(rank1_ui, fixed)
|
||||
rank1_engine["slot_money"] = slot_money
|
||||
rank1_engine["max_stocks"] = max_stocks
|
||||
rank1_engine["total_budget_krw"] = total_budget
|
||||
rank1_engine["fee_rate"] = fee_rate
|
||||
rank1_engine["sell_tax"] = sell_tax
|
||||
|
||||
db_engine = _build_engine_params({}, fixed)
|
||||
db_engine["slot_money"] = slot_money
|
||||
db_engine["max_stocks"] = max_stocks
|
||||
db_engine["total_budget_krw"] = total_budget
|
||||
db_engine["fee_rate"] = fee_rate
|
||||
db_engine["sell_tax"] = sell_tax
|
||||
|
||||
print("=" * 72, flush=True)
|
||||
print(f"모멘텀 벤치마크 {args.start} ~ {args.end} ({period_days}일)", flush=True)
|
||||
print(
|
||||
f"포트폴리오: 슬롯 {slot_money:,.0f} | 동시 {max_stocks} | 한도 {total_budget:,.0f} | "
|
||||
f"수수료 {fee_rate*100:.4f}% + 세 {sell_tax*100:.2f}%",
|
||||
flush=True,
|
||||
)
|
||||
print("=" * 72, flush=True)
|
||||
|
||||
print("⏳ 캔들 로드...", flush=True)
|
||||
candles = _load_candles_for_search(args.start, args.end, rsi_period)
|
||||
print(f"✅ {len(candles):,}종목", flush=True)
|
||||
|
||||
universe = _load_universe(args.start, args.end)
|
||||
if universe:
|
||||
avg = sum(len(v) for v in universe.values()) / max(1, len(universe))
|
||||
print(f"✅ 유니버스: MOMENTUM history | {len(universe):,}슬롯 · 평균 {avg:.1f}종", flush=True)
|
||||
else:
|
||||
print("⚠️ 유니버스 이력 없음 — 전종목", flush=True)
|
||||
|
||||
print("\n[1] 탐색 1위 로직 (fast grid rank1)", flush=True)
|
||||
print(f" params: vol×{rank1_ui.get('mom_vol_mult')} RSI {rank1_ui.get('mom_rsi_min')}~{rank1_ui.get('mom_rsi_max')} "
|
||||
f"EMA {'ON' if rank1_ui.get('use_ema_filter') else 'OFF'} "
|
||||
f"{rank1_ui.get('ema_fast_period')}/{rank1_ui.get('ema_slow_period')}", flush=True)
|
||||
t1 = run_portfolio(candles, rank1_engine, universe, random_seed=None)
|
||||
st1 = _stats(t1, total_budget, period_days)
|
||||
_print_row("탐색1위 로직", st1)
|
||||
|
||||
print("\n[2] 현재 DB 실매 설정", flush=True)
|
||||
t2 = run_portfolio(candles, db_engine, universe, random_seed=None)
|
||||
st2 = _stats(t2, total_budget, period_days)
|
||||
_print_row("DB 실매", st2)
|
||||
|
||||
print(f"\n[3] 무작위 진입 × {args.seeds}회 (동일 유니버스·청산·포트폴리오)", flush=True)
|
||||
pnls: List[float] = []
|
||||
trades_n: List[int] = []
|
||||
win_rates: List[float] = []
|
||||
pfs: List[float] = []
|
||||
for seed in range(1, args.seeds + 1):
|
||||
tr = run_portfolio(candles, rank1_engine, universe, random_seed=seed)
|
||||
st = _stats(tr, total_budget, period_days)
|
||||
pnls.append(float(st["total_pnl"]))
|
||||
trades_n.append(int(st["total_trades"]))
|
||||
win_rates.append(float(st["win_rate"]))
|
||||
pfs.append(float(st["pf"]))
|
||||
if seed % 25 == 0:
|
||||
print(f" ... seed {seed}/{args.seeds}", flush=True)
|
||||
|
||||
import statistics
|
||||
avg_pnl = statistics.mean(pnls)
|
||||
med_pnl = statistics.median(pnls)
|
||||
avg_pf = statistics.mean(pfs)
|
||||
avg_wr = statistics.mean(win_rates)
|
||||
avg_tr = statistics.mean(trades_n)
|
||||
beat = sum(1 for p in pnls if p > st1["total_pnl"])
|
||||
beat_db = sum(1 for p in pnls if p > st2["total_pnl"])
|
||||
|
||||
print(f"\n{'=' * 72}", flush=True)
|
||||
print("📊 요약", flush=True)
|
||||
_print_row("탐색1위 로직", st1)
|
||||
_print_row("DB 실매", st2)
|
||||
print(
|
||||
f" {'무작위(평균)':<22} | 손익 {avg_pnl:>10,.0f}원 | "
|
||||
f"거래 {avg_tr:>5.0f} | 승률 {avg_wr:>5.1f}% | PF {avg_pf:>5.2f}",
|
||||
flush=True,
|
||||
)
|
||||
print(
|
||||
f" {'무작위(중앙값)':<22} | 손익 {med_pnl:>10,.0f}원 | "
|
||||
f"min {min(pnls):,.0f} max {max(pnls):,.0f}",
|
||||
flush=True,
|
||||
)
|
||||
print(
|
||||
f"\n 무작위 {args.seeds}회 중 탐색1위보다 나은 비율: {beat}/{args.seeds} ({100*beat/args.seeds:.0f}%)",
|
||||
flush=True,
|
||||
)
|
||||
print(
|
||||
f" 무작위 {args.seeds}회 중 DB실매보다 나은 비율: {beat_db}/{args.seeds} ({100*beat_db/args.seeds:.0f}%)",
|
||||
flush=True,
|
||||
)
|
||||
if st1["total_pnl"] <= avg_pnl:
|
||||
print("\n ⚠️ 탐색1위 로직 ≤ 무작위 평균 → TRIGGER 엣지 없음 (운/노이즈 수준)", flush=True)
|
||||
else:
|
||||
print(f"\n ✅ 탐색1위가 무작위 평균 대비 {st1['total_pnl']-avg_pnl:+,.0f}원", flush=True)
|
||||
print(f"\n⏱ 총 {time.time()-t0:.0f}초", flush=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user