변경 사항 (Changes): 구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함. 스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함. 코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함. 시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함. 기대 효과 (Impact): 이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
192 lines
6.7 KiB
Python
192 lines
6.7 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
이전 모멘텀 fine #1 파라미터 고정 × 래칫만 A/B (apply 없음).
|
||
|
||
기준 JSON: optuna_momentum_fine_20260716_014654.json (best ~+83k, 래칫 축 없음)
|
||
기간: 2026-07-15 / 포트 120만 / orderbook off
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import sys
|
||
import time
|
||
from datetime import datetime
|
||
|
||
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
||
if ROOT not in sys.path:
|
||
sys.path.insert(0, ROOT)
|
||
|
||
os.environ.setdefault("MOMENTUM_BACKTEST_REST_WARMUP", "1")
|
||
os.environ.setdefault("MOMENTUM_BACKTEST_REST_WARMUP_BARS", "700")
|
||
os.environ.setdefault("MOMENTUM_BACKTEST_REST_SLEEP_SEC", "0.25")
|
||
|
||
from kis_trader.backtest.optuna_common import announce_optuna_json_path
|
||
from kis_trader.backtest.optuna_momentum import prepare_momentum_search_context
|
||
from kis_trader.backtest.param_search_momentum import evaluate_momentum_param_combo
|
||
from kis_trader.utils.env import get_env_from_db
|
||
|
||
# 래칫 후보: OFF + 실매 + fine 격자 대표 + 최근 Optuna 선호
|
||
RATCHET_CASES = [
|
||
("OFF", ""),
|
||
("LIVE", "5:2,10:1.5"),
|
||
("OPTUNA_TOP", "2:1.5,5:1"),
|
||
("MID", "2:1,5:0.8,8:0.6"),
|
||
]
|
||
|
||
BASE_JSON = os.path.join(
|
||
ROOT,
|
||
"kis_trader/backtest/results/optuna_momentum_fine_20260716_014654.json",
|
||
)
|
||
|
||
|
||
def main() -> int:
|
||
with open(BASE_JSON, encoding="utf-8") as f:
|
||
src = json.load(f)
|
||
base_combo = dict(src["results"][0]["params"])
|
||
start = str(src.get("start") or "2026-07-15")
|
||
end = str(src.get("end") or start)
|
||
|
||
print("=" * 72, flush=True)
|
||
print("모멘텀 래칫 A/B | 이전 fine#1 고정 | apply 없음", flush=True)
|
||
print(f"기준 JSON: {BASE_JSON}", flush=True)
|
||
print(f"기간: {start} ~ {end}", flush=True)
|
||
print(f"고정 params: {json.dumps(base_combo, ensure_ascii=False)}", flush=True)
|
||
print(
|
||
f"DB MOMENTUM_RATCHET_TIERS(참고): {get_env_from_db('MOMENTUM_RATCHET_TIERS', '')!r}",
|
||
flush=True,
|
||
)
|
||
print("=" * 72, flush=True)
|
||
|
||
t0 = time.time()
|
||
ctx = prepare_momentum_search_context(
|
||
start,
|
||
end,
|
||
"fine",
|
||
orderbook_filter="off",
|
||
)
|
||
if ctx is None:
|
||
print("❌ context 준비 실패", flush=True)
|
||
return 1
|
||
print(
|
||
f"✅ context OK | {time.time() - t0:.1f}s | "
|
||
f"slot={ctx.slot_money:,.0f} max={ctx.max_stocks} budget={ctx.total_budget_krw:,.0f}",
|
||
flush=True,
|
||
)
|
||
|
||
rows = []
|
||
for label, ratchet in RATCHET_CASES:
|
||
combo = dict(base_combo)
|
||
combo["ratchet_tiers"] = ratchet
|
||
print("-" * 72, flush=True)
|
||
print(f"▶ 케이스 {label} | ratchet_tiers={ratchet!r}", flush=True)
|
||
t1 = time.time()
|
||
result = evaluate_momentum_param_combo(
|
||
combo,
|
||
base_fixed=ctx.base_fixed,
|
||
grid_keys=list(ctx.grid_keys) + ["ratchet_tiers"],
|
||
codes_candles=ctx.codes_candles,
|
||
min_trades=1,
|
||
min_win_rate=0.0,
|
||
min_pf=0.0,
|
||
universe_by_slot=ctx.universe_by_slot,
|
||
slot_money=ctx.slot_money,
|
||
max_stocks=ctx.max_stocks,
|
||
total_budget_krw=ctx.total_budget_krw,
|
||
fee_rate=ctx.fee_rate,
|
||
sell_tax=ctx.sell_tax,
|
||
period_days=ctx.period_days,
|
||
cache_holder=ctx.cache_holder,
|
||
ticks_by_code=ctx.ticks_by_code,
|
||
orderbook_by_code=ctx.orderbook_by_code,
|
||
program_by_code=ctx.program_by_code,
|
||
log_verdict_by_code=ctx.log_verdict_by_code,
|
||
start_key=ctx.start_key,
|
||
end_key=ctx.end_key,
|
||
)
|
||
elapsed = time.time() - t1
|
||
if result is None:
|
||
row = {
|
||
"label": label,
|
||
"ratchet_tiers": ratchet,
|
||
"ok": False,
|
||
"elapsed_sec": round(elapsed, 2),
|
||
"note": "evaluate None",
|
||
}
|
||
print(f" ❌ None ({elapsed:.1f}s)", flush=True)
|
||
else:
|
||
row = {
|
||
"label": label,
|
||
"ratchet_tiers": ratchet,
|
||
"ok": True,
|
||
"elapsed_sec": round(elapsed, 2),
|
||
"total_pnl": float(result.get("total_pnl") or 0),
|
||
"total_trades": int(result.get("total_trades") or 0),
|
||
"win_rate": float(result.get("win_rate") or 0),
|
||
"pf": float(result.get("pf") or 0) if result.get("pf") is not None else None,
|
||
}
|
||
print(
|
||
f" ✅ pnl={row['total_pnl']:,.0f} | trades={row['total_trades']} | "
|
||
f"wr={row['win_rate']:.1f}% | pf={row['pf']} | {elapsed:.1f}s",
|
||
flush=True,
|
||
)
|
||
rows.append(row)
|
||
|
||
ok_rows = [r for r in rows if r.get("ok")]
|
||
ok_rows.sort(key=lambda r: (-float(r["total_pnl"]), -int(r["total_trades"])))
|
||
|
||
print("=" * 72, flush=True)
|
||
print("📊 A/B 결과 (PnL 내림차순)", flush=True)
|
||
for i, r in enumerate(ok_rows, 1):
|
||
print(
|
||
f" {i}. [{r['label']}] ratchet={r['ratchet_tiers']!r} | "
|
||
f"pnl={r['total_pnl']:,.0f} | trades={r['total_trades']} | wr={r['win_rate']:.1f}%",
|
||
flush=True,
|
||
)
|
||
if len(ok_rows) >= 2:
|
||
best, worst = ok_rows[0], ok_rows[-1]
|
||
print(
|
||
f"Δ best−worst = {best['total_pnl'] - worst['total_pnl']:+,.0f}원 "
|
||
f"({best['label']} vs {worst['label']})",
|
||
flush=True,
|
||
)
|
||
off = next((r for r in ok_rows if r["label"] == "OFF"), None)
|
||
live = next((r for r in ok_rows if r["label"] == "LIVE"), None)
|
||
if off and live:
|
||
print(
|
||
f"Δ OFF−LIVE = {off['total_pnl'] - live['total_pnl']:+,.0f}원 "
|
||
f"(OFF {off['total_pnl']:,.0f} / LIVE {live['total_pnl']:,.0f})",
|
||
flush=True,
|
||
)
|
||
print("=" * 72, flush=True)
|
||
|
||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
out_dir = os.path.join(ROOT, "kis_trader/backtest/results")
|
||
os.makedirs(out_dir, exist_ok=True)
|
||
out_path = os.path.join(out_dir, f"momentum_ratchet_ab_{ts}.json")
|
||
out = {
|
||
"kind": "momentum_ratchet_ab",
|
||
"apply": False,
|
||
"base_json": BASE_JSON,
|
||
"base_pnl_reported": src["results"][0].get("total_pnl"),
|
||
"base_params": base_combo,
|
||
"start": start,
|
||
"end": end,
|
||
"slot_money": int(ctx.slot_money),
|
||
"max_stocks": int(ctx.max_stocks),
|
||
"total_budget_krw": int(ctx.total_budget_krw),
|
||
"cases": rows,
|
||
"ranked": ok_rows,
|
||
"elapsed_sec": round(time.time() - t0, 1),
|
||
}
|
||
with open(out_path, "w", encoding="utf-8") as f:
|
||
json.dump(out, f, indent=2, ensure_ascii=False)
|
||
announce_optuna_json_path(
|
||
out_path, strategy="momentum", mode="ratchet_ab", note="래칫 A/B 최종 JSON",
|
||
)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|