feat: 새로운 안전 규칙 및 최적화 적용을 통한 트레이딩 시스템 개선

변경 사항 (Changes):

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

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

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

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

기대 효과 (Impact):

이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
This commit is contained in:
Your Name
2026-07-17 01:09:09 +09:00
parent a4626e0351
commit fc27e726f9
151 changed files with 20718 additions and 6450 deletions

View File

@@ -0,0 +1,191 @@
#!/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())