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:
@@ -16,6 +16,8 @@ from optuna.samplers import RandomSampler, TPESampler
|
||||
from database import TradeDB
|
||||
from kis_trader.backtest import breakout_backtest_common as bbc
|
||||
from kis_trader.backtest.optuna_search_space import breakout_grid_axis_keys, suggest_breakout_params
|
||||
from kis_trader.backtest.optuna_mode_combo import enrich_out_data_with_mode_combo
|
||||
from kis_trader.backtest.optuna_common import announce_optuna_json_path, release_shared_tick_store
|
||||
from kis_trader.backtest.param_search_breakout import (
|
||||
_bo_fixed_defaults,
|
||||
_load_candles_for_search,
|
||||
@@ -102,8 +104,8 @@ def prepare_breakout_search_context(
|
||||
|
||||
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 {}
|
||||
from kis_trader.backtest.backtest_portfolio_common import load_portfolio_env_row
|
||||
env_row = load_portfolio_env_row(db)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@@ -333,79 +335,119 @@ def run_breakout_optuna(
|
||||
t0 = time.time()
|
||||
try:
|
||||
study.optimize(objective, n_trials=n_trials, n_jobs=n_jobs, show_progress_bar=show_progress)
|
||||
finally:
|
||||
# 탐색 종료(또는 예외) 시 공유메모리 즉시 해제 (atexit 는 크래시 대비 이중 안전장치).
|
||||
_store = getattr(ctx, "shared_tick_store", None)
|
||||
if _store is not None:
|
||||
elapsed = time.time() - t0
|
||||
|
||||
passing: List[Dict[str, Any]] = []
|
||||
for trial in study.trials:
|
||||
if trial.state != optuna.trial.TrialState.COMPLETE:
|
||||
continue
|
||||
if not trial.user_attrs.get("gates_ok"):
|
||||
continue
|
||||
merged_raw = trial.user_attrs.get("merged_json") or "{}"
|
||||
try:
|
||||
_store.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
ctx.shared_tick_store = None
|
||||
elapsed = time.time() - t0
|
||||
merged = json.loads(merged_raw)
|
||||
except json.JSONDecodeError:
|
||||
merged = dict(trial.params)
|
||||
passing.append({
|
||||
"params": dict(trial.params),
|
||||
"merged_params": merged,
|
||||
"total_trades": int(trial.user_attrs.get("total_trades") or 0),
|
||||
"win_rate": float(trial.user_attrs.get("win_rate") or 0),
|
||||
"total_pnl": float(trial.user_attrs.get("total_pnl") or 0),
|
||||
"pf": float(trial.user_attrs.get("pf") or 0),
|
||||
"optuna_trial_number": trial.number,
|
||||
})
|
||||
|
||||
passing: List[Dict[str, Any]] = []
|
||||
for trial in study.trials:
|
||||
if trial.state != optuna.trial.TrialState.COMPLETE:
|
||||
continue
|
||||
if not trial.user_attrs.get("gates_ok"):
|
||||
continue
|
||||
merged_raw = trial.user_attrs.get("merged_json") or "{}"
|
||||
try:
|
||||
merged = json.loads(merged_raw)
|
||||
except json.JSONDecodeError:
|
||||
merged = dict(trial.params)
|
||||
passing.append({
|
||||
"params": dict(trial.params),
|
||||
"merged_params": merged,
|
||||
"total_trades": int(trial.user_attrs.get("total_trades") or 0),
|
||||
"win_rate": float(trial.user_attrs.get("win_rate") or 0),
|
||||
"total_pnl": float(trial.user_attrs.get("total_pnl") or 0),
|
||||
"pf": float(trial.user_attrs.get("pf") or 0),
|
||||
"optuna_trial_number": trial.number,
|
||||
})
|
||||
if sort_by == "win_rate":
|
||||
passing.sort(key=lambda r: (-r["win_rate"], -r["total_pnl"]))
|
||||
else:
|
||||
passing.sort(key=lambda r: (-r["total_pnl"], -r["win_rate"]))
|
||||
profitable = [r for r in passing if r["total_pnl"] > 0]
|
||||
if profitable:
|
||||
passing = profitable
|
||||
|
||||
if sort_by == "win_rate":
|
||||
passing.sort(key=lambda r: (-r["win_rate"], -r["total_pnl"]))
|
||||
else:
|
||||
passing.sort(key=lambda r: (-r["total_pnl"], -r["win_rate"]))
|
||||
profitable = [r for r in passing if r["total_pnl"] > 0]
|
||||
if profitable:
|
||||
passing = profitable
|
||||
hints: Dict[str, str] = {}
|
||||
out_data = {
|
||||
"engine": "optuna",
|
||||
"strategy": "breakout",
|
||||
"mode": ctx.mode,
|
||||
"start": ctx.start,
|
||||
"end": ctx.end,
|
||||
"slot_money": int(ctx.slot_money),
|
||||
"max_stocks": ctx.max_stocks,
|
||||
"total_budget_krw": int(ctx.total_budget_krw),
|
||||
"backtest_days": ctx.period_days,
|
||||
"min_trades": min_trades,
|
||||
"min_win_rate": min_win_rate,
|
||||
"min_pf": min_pf,
|
||||
"sort_by": sort_by,
|
||||
"grid_keys": ctx.grid_keys,
|
||||
"grid_axis_hints": {k: hints[k] for k in ctx.grid_keys if k in hints},
|
||||
"optuna_study_name": study_name,
|
||||
"optuna_storage": storage_url,
|
||||
"optuna_n_trials_requested": n_trials,
|
||||
"optuna_trials_completed": len(study.trials),
|
||||
"optuna_best_value": study.best_value if study.best_trial else None,
|
||||
"optuna_best_trial_number": study.best_trial.number if study.best_trial else None,
|
||||
"elapsed_sec": round(elapsed, 1),
|
||||
"results": passing[:5000],
|
||||
}
|
||||
|
||||
hints: Dict[str, str] = {}
|
||||
out_data = {
|
||||
"engine": "optuna",
|
||||
"strategy": "breakout",
|
||||
"mode": ctx.mode,
|
||||
"start": ctx.start,
|
||||
"end": ctx.end,
|
||||
"slot_money": int(ctx.slot_money),
|
||||
"max_stocks": ctx.max_stocks,
|
||||
"total_budget_krw": int(ctx.total_budget_krw),
|
||||
"backtest_days": ctx.period_days,
|
||||
"min_trades": min_trades,
|
||||
"min_win_rate": min_win_rate,
|
||||
"min_pf": min_pf,
|
||||
"sort_by": sort_by,
|
||||
"grid_keys": ctx.grid_keys,
|
||||
"grid_axis_hints": {k: hints[k] for k in ctx.grid_keys if k in hints},
|
||||
"optuna_study_name": study_name,
|
||||
"optuna_storage": storage_url,
|
||||
"optuna_n_trials_requested": n_trials,
|
||||
"optuna_trials_completed": len(study.trials),
|
||||
"optuna_best_value": study.best_value if study.best_trial else None,
|
||||
"optuna_best_trial_number": study.best_trial.number if study.best_trial else None,
|
||||
"elapsed_sec": round(elapsed, 1),
|
||||
"results": passing[:5000],
|
||||
}
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
out_path = os.path.join(_results_dir_for_write(), f"optuna_breakout_{ctx.mode}_{ts}.json")
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(out_data, f, indent=2, ensure_ascii=False)
|
||||
logger.info("💾 Optuna 결과 저장: %s", out_path)
|
||||
study._kis_export_path = out_path # type: ignore[attr-defined]
|
||||
return study
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
out_path = os.path.join(_results_dir_for_write(), f"optuna_breakout_{ctx.mode}_{ts}.json")
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(out_data, f, indent=2, ensure_ascii=False)
|
||||
announce_optuna_json_path(
|
||||
out_path, strategy="breakout", mode=ctx.mode, note="중간저장(mode 전)", log=logger,
|
||||
)
|
||||
|
||||
def _eval_mode(combo: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
return evaluate_breakout_param_combo(
|
||||
combo,
|
||||
base_fixed=ctx.base_fixed,
|
||||
grid_keys=ctx.grid_keys,
|
||||
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,
|
||||
share_denom_by_code=ctx.share_denom_by_code,
|
||||
)
|
||||
|
||||
def _save_partial(_data: Dict[str, Any]) -> None:
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(_data, f, indent=2, ensure_ascii=False)
|
||||
announce_optuna_json_path(
|
||||
out_path, strategy="breakout", mode=ctx.mode, note="mode_combo params 저장(실측 전)", log=logger,
|
||||
)
|
||||
|
||||
enrich_out_data_with_mode_combo(
|
||||
out_data,
|
||||
evaluate_fn=_eval_mode,
|
||||
grid_keys=ctx.grid_keys,
|
||||
log=logger,
|
||||
on_partial_save=_save_partial,
|
||||
)
|
||||
with open(out_path, "w", encoding="utf-8") as f:
|
||||
json.dump(out_data, f, indent=2, ensure_ascii=False)
|
||||
announce_optuna_json_path(
|
||||
out_path, strategy="breakout", mode=ctx.mode, note="최종 JSON", log=logger,
|
||||
)
|
||||
study._kis_export_path = out_path # type: ignore[attr-defined]
|
||||
return study
|
||||
finally:
|
||||
release_shared_tick_store(ctx, log=logger)
|
||||
|
||||
|
||||
def apply_best_breakout_trial(study: optuna.Study) -> bool:
|
||||
|
||||
Reference in New Issue
Block a user