Files
kis_bot/scripts/momentum_ratchet_ab_715.py
Your Name fc27e726f9 feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선
변경 사항 (Changes):

구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함.

스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함.

코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함.

시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함.

기대 효과 (Impact):

이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
2026-07-17 01:09:09 +09:00

192 lines
6.7 KiB
Python
Raw 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
"""
이전 모멘텀 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"Δ bestworst = {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"Δ OFFLIVE = {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())