변경 사항 (Changes): 구문 오류(Syntax error) 및 토큰 낭비를 방지하기 위해 에이전트 쉘(Agent shell)과 파이썬 코드 스니펫에 다수의 신규 안전 규칙(Safety rules)을 추가함. 스키마 검증 및 적절한 SQL 포맷팅을 보장하기 위해 임시(Ad-hoc) 데이터베이스 쿼리 작성 가이드라인을 도입함. 코드 수정 후 UI 기능이 정상 작동하는지 확인하기 위해, 백테스트 웹 서비스 재시작 및 브라우저 검증에 대한 새로운 규칙을 구현함. 시스템 전반의 무결성(Integrity)을 유지하기 위해 실전 매매(Live trading), 웹 백테스팅, 파라미터 탐색(Parameter searches) 간의 일관성 검사(Consistency checks) 체계를 확립함. 기대 효과 (Impact): 이러한 개선 사항들은 트레이딩 시스템의 견고성(Robustness)과 신뢰성을 향상시키며, 에러 발생을 최소화하고 다양한 시스템 컴포넌트 간의 원활한 상호작용을 보장함.
184 lines
5.7 KiB
Python
184 lines
5.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Optuna 결과 JSON vs 현재 DB — 백테 비교표를 로그 파일 끝에 append (DB 미저장)."""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from database import TradeDB
|
|
from kis_trader.backtest import tail_backtest_common as tbc
|
|
from kis_trader.engine import tail_engine as te
|
|
|
|
|
|
COMPARE_KEYS = [
|
|
"entry_mode",
|
|
"min_drop_rate",
|
|
"min_recovery_ratio",
|
|
"tail_ratio_min",
|
|
"tail_pct_min",
|
|
"max_rec_3m",
|
|
"shoulder_min_high",
|
|
"shoulder_cut_pct",
|
|
"stop_atr_mult",
|
|
"target_atr_mult",
|
|
"atr_sl_min_pct",
|
|
"atr_sl_max_pct",
|
|
"atr_tp_min_pct",
|
|
"atr_tp_max_pct",
|
|
"max_daily_change",
|
|
"max_loss_krw",
|
|
"limit_atr_mult",
|
|
"tail_vol_mult",
|
|
"tail_vol_win",
|
|
"ratchet_tiers",
|
|
"rsi_threshold",
|
|
"cooldown_min",
|
|
"bar_chg_min_pct",
|
|
"bar_chg_max_pct",
|
|
"symbol_daily_loss_limit_krw",
|
|
"symbol_daily_loss_limit_pct",
|
|
"reentry_min_edge_krw",
|
|
"reentry_require_nonneg",
|
|
"trail_tiers",
|
|
"trail_drop_pct",
|
|
"trail_arm_krw",
|
|
"pattern_pin",
|
|
"pattern_engulfing",
|
|
"pattern_piercing",
|
|
"max_daily",
|
|
"max_spread_pct",
|
|
"min_bid_ask_ratio",
|
|
]
|
|
|
|
|
|
def _same(a, b) -> bool:
|
|
if a == b:
|
|
return True
|
|
try:
|
|
return abs(float(a) - float(b)) < 1e-9
|
|
except Exception:
|
|
return str(a) == str(b)
|
|
|
|
|
|
def _run(candles_by_code, universe, base, port, fee, tax, slot, budget, meta, overrides):
|
|
params = dict(base)
|
|
tbc.merge_tail_portfolio_into_params(params, port)
|
|
params.update(overrides or {})
|
|
trades = tbc.run_tail_backtest_web_aligned(
|
|
candles_by_code,
|
|
params,
|
|
universe,
|
|
slot_money=slot,
|
|
fee_rate=fee,
|
|
sell_tax=tax,
|
|
total_budget_krw=budget,
|
|
meta_out=dict(meta),
|
|
)
|
|
wins = [t for t in trades if float(t.get("pnl") or 0) > 0]
|
|
pnl = sum(float(t.get("pnl") or 0) for t in trades)
|
|
wr = (len(wins) / len(trades) * 100.0) if trades else 0.0
|
|
return {
|
|
"trades": len(trades),
|
|
"wins": len(wins),
|
|
"wr": wr,
|
|
"pnl": pnl,
|
|
"params": {k: params.get(k) for k in COMPARE_KEYS},
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--json", required=True, help="optuna_tail_*.json 경로")
|
|
ap.add_argument("--log", required=True, help="append 대상 로그 경로")
|
|
ap.add_argument("--date", default="2026-07-10", help="백테 일자 YYYY-MM-DD")
|
|
args = ap.parse_args()
|
|
|
|
json_path = Path(args.json)
|
|
log_path = Path(args.log)
|
|
data = json.loads(json_path.read_text(encoding="utf-8"))
|
|
best = (data.get("results") or [None])[0]
|
|
if not best:
|
|
msg = "❌ Optuna results 비어 있음 — 비교 스킵\n"
|
|
with log_path.open("a", encoding="utf-8") as f:
|
|
f.write(msg)
|
|
print(msg, end="")
|
|
return 1
|
|
|
|
best_params = best.get("params") or {}
|
|
day = args.date
|
|
start_key = day.replace("-", "") + "0000"
|
|
end_key = day.replace("-", "") + "2359"
|
|
start_ymd, end_ymd = start_key[:8], end_key[:8]
|
|
|
|
db = TradeDB()
|
|
base = te.get_tail_defaults_from_db(db)
|
|
universe, src, n_slots, _ = tbc.resolve_tail_universe(
|
|
start_ymd, end_ymd, use_saved_history=True, strategy_id="SHORT",
|
|
)
|
|
tf = int(base.get("timeframe") or 3)
|
|
rsi = int(base.get("rsi_period") or 14)
|
|
candles_by_code, _, _ = tbc.load_tail_candles_by_code(
|
|
db, start_key, end_key, tf, rsi_period=rsi,
|
|
)
|
|
row = db.conn.execute("SELECT * FROM env_config ORDER BY id DESC LIMIT 1").fetchone()
|
|
r = dict(row) if row else {}
|
|
fee, tax, _ = tbc.fee_and_slot_from_env_row(r)
|
|
port = tbc.resolve_tail_portfolio_params(r, base)
|
|
slot = float(port["slot_money"])
|
|
budget = float(port["total_budget_krw"])
|
|
meta = {"db": db, "start_key": start_key, "end_key": end_key}
|
|
|
|
cur = _run(candles_by_code, universe, base, port, fee, tax, slot, budget, meta, {})
|
|
bst = _run(
|
|
candles_by_code, universe, base, port, fee, tax, slot, budget, meta, best_params,
|
|
)
|
|
delta = bst["pnl"] - cur["pnl"]
|
|
trial_no = best.get("optuna_trial_number") or data.get("optuna_best_trial_number")
|
|
mode = data.get("mode") or "?"
|
|
elapsed = data.get("elapsed_sec")
|
|
|
|
lines = []
|
|
lines.append("")
|
|
lines.append("=" * 72)
|
|
lines.append(f"[COMPARE] CURRENT_DB vs OPTUNA_{mode.upper()}_BEST | {day} | DB미저장")
|
|
lines.append("=" * 72)
|
|
lines.append(f"json={json_path}")
|
|
lines.append(f"universe={src} slots={n_slots} | trial=#{trial_no} elapsed={elapsed}s")
|
|
lines.append("")
|
|
lines.append("| 구분 | 거래 | 승 | 승률 | 손익 |")
|
|
lines.append("|------|------|----|------|------|")
|
|
lines.append(
|
|
f"| CURRENT_DB | {cur['trades']} | {cur['wins']} | {cur['wr']:.1f}% | {cur['pnl']:+,.0f} |"
|
|
)
|
|
lines.append(
|
|
f"| OPTUNA_BEST | {bst['trades']} | {bst['wins']} | {bst['wr']:.1f}% | {bst['pnl']:+,.0f} |"
|
|
)
|
|
lines.append(f"| Δ(best-current) | | | | {delta:+,.0f} |")
|
|
lines.append("")
|
|
lines.append("| 파라미터 | CURRENT_DB | OPTUNA_BEST | diff |")
|
|
lines.append("|----------|------------|--------------|------|")
|
|
for k in COMPARE_KEYS:
|
|
a = cur["params"].get(k)
|
|
b = bst["params"].get(k)
|
|
mark = "" if _same(a, b) else "<<"
|
|
lines.append(f"| {k} | {a} | {b} | {mark} |")
|
|
lines.append("")
|
|
lines.append("DB 저장 없음 (--apply-best 미사용)")
|
|
lines.append("=" * 72)
|
|
lines.append("")
|
|
text = "\n".join(lines)
|
|
with log_path.open("a", encoding="utf-8") as f:
|
|
f.write(text)
|
|
print(text, end="")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|