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

@@ -17,6 +17,8 @@ from database import TradeDB
from kis_trader.backtest import momentum_backtest_common as mbc
from kis_trader.backtest import scalping_backtest_common as sbc
from kis_trader.backtest.optuna_search_space import momentum_grid_axis_keys, suggest_momentum_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_cli_common import (
apply_session_to_fixed,
combo_passes_search_filters,
@@ -81,7 +83,7 @@ def prepare_momentum_search_context(
) -> Optional[MomentumSearchContext]:
grids = _momentum_grids()
if mode not in grids:
logger.error("❌ 모멘텀 mode: %s (fast/rr/coarse/fine/full)", mode)
logger.error("❌ 모멘텀 mode: %s (fast/exit/rr/coarse/fine/wide/full)", mode)
return None
base_fixed = _mom_fixed_defaults()
@@ -108,8 +110,8 @@ def prepare_momentum_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()
@@ -174,6 +176,24 @@ def prepare_momentum_search_context(
else:
base_fixed["scan_interval_min"] = 1
# DB 전일봉 없으면 키움 REST 1회/종목 → 메모리 prepend (실매 갭보정 정합, DB 미기록)
try:
from kis_trader.backtest.momentum_backtest_common import (
inject_momentum_rest_warmup_memory,
)
_rw = inject_momentum_rest_warmup_memory(
codes_candles,
start_key,
universe_by_slot=universe_by_slot,
)
if int(_rw.get("ok") or 0) > 0 or int(_rw.get("need") or 0) > 0:
logger.info(
"📡 REST 웜업: need=%s ok=%s fail=%s bars=%s",
_rw.get("need"), _rw.get("ok"), _rw.get("fail"), _rw.get("bars"),
)
except Exception as exc:
logger.warning("⚠️ REST 웜업 스킵: %s", exc)
grid = grids[mode]
_ob_axes = ("max_spread_pct", "min_bid_ask_ratio", "ask_max_mult")
_ob_sweeping = any(len(set(grid.get(k) or [])) > 1 for k in _ob_axes)
@@ -345,84 +365,129 @@ def run_momentum_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)
row = {
"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),
"mdd": float(trial.user_attrs.get("mdd") or 0),
"score": float(trial.user_attrs.get("score") or 0),
"optuna_trial_number": trial.number,
}
passing.append(row)
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)
row = {
"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),
"mdd": float(trial.user_attrs.get("mdd") or 0),
"score": float(trial.user_attrs.get("score") or 0),
"optuna_trial_number": trial.number,
if sort_by == "score":
passing.sort(key=lambda r: (-r["score"], -r["total_pnl"], -r["win_rate"]))
elif 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
out_data = {
"engine": "optuna",
"strategy": "momentum",
"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: MOMENTUM_GRID_AXIS_HINTS_KO[k] for k in ctx.grid_keys if k in MOMENTUM_GRID_AXIS_HINTS_KO},
"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],
}
passing.append(row)
if sort_by == "score":
passing.sort(key=lambda r: (-r["score"], -r["total_pnl"], -r["win_rate"]))
elif 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"]))
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
out_path = os.path.join(_results_dir_for_write(), f"optuna_momentum_{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="momentum", mode=ctx.mode, note="중간저장(mode 전)", log=logger,
)
profitable = [r for r in passing if r["total_pnl"] > 0]
if profitable:
passing = profitable
def _eval_mode(combo: Dict[str, Any]) -> Optional[Dict[str, Any]]:
# 최빈 Frankenstein 실측 — 게이트는 느슨하게(리포트용)
# ※ shared_tick_store 가 아직 살아 있어야 함 (optimize 직후 unlink 금지)
return evaluate_momentum_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,
start_key=ctx.start_key,
end_key=ctx.end_key,
)
out_data = {
"engine": "optuna",
"strategy": "momentum",
"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: MOMENTUM_GRID_AXIS_HINTS_KO[k] for k in ctx.grid_keys if k in MOMENTUM_GRID_AXIS_HINTS_KO},
"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_momentum_{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
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="momentum", 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="momentum", mode=ctx.mode, note="최종 JSON", log=logger,
)
study._kis_export_path = out_path # type: ignore[attr-defined]
return study
finally:
# mode_combo 실측이 ticks 공유메모리 뷰를 쓰므로, 여기서 해제 (optimize 직후 X)
release_shared_tick_store(ctx, log=logger)
def apply_best_momentum_trial(study: optuna.Study) -> bool: