diff --git a/src/calibration/optimization_inspector.py b/src/calibration/optimization_inspector.py
index a877d1d..af712a2 100644
--- a/src/calibration/optimization_inspector.py
+++ b/src/calibration/optimization_inspector.py
@@ -1,60 +1,41 @@
# src/calibration/strategies_inspector.py
from __future__ import annotations
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Tuple
import numpy as np
import pandas as pd
-from src.data.storage import StorageManager
-from src.utils.logger import log
-
from src.core.walk_forward import WalkForwardValidator
-
+from src.data.storage import StorageManager
+from src.risk.sizing.percent_risk import PercentRiskSizer
from src.risk.stops.fixed_stop import FixedStop
from src.risk.stops.trailing_stop import TrailingStop
from src.risk.stops.atr_stop import ATRStop
+from src.strategies.registry import STRATEGY_REGISTRY
+from src.utils.logger import log
-from src.risk.sizing.percent_risk import PercentRiskSizer
-
-
-# --------------------------------------------------
-# Strategy registry (con metadata de parámetros)
-# --------------------------------------------------
-from src.strategies.ma_crossover import MovingAverageCrossover
-from src.strategies.rsi_reversion import RSIStrategy
-
-
-STRATEGY_REGISTRY = {
- "moving_average": {
- "class": MovingAverageCrossover,
- "params": ["fast_period", "slow_period"],
- },
- "rsi": {
- "class": RSIStrategy,
- "params": ["rsi_period", "overbought", "oversold"],
- },
-}
-
-
-# --------------------------------------------------
-# Helpers
-# --------------------------------------------------
def list_available_strategies() -> List[Dict[str, Any]]:
- """
- Devuelve metadata completa para UI.
- """
out = []
-
- for sid, entry in STRATEGY_REGISTRY.items():
+ for sid, strategy_class in STRATEGY_REGISTRY.items():
+ schema = strategy_class.parameters_schema()
out.append({
"strategy_id": sid,
- "name": entry["class"].__name__,
- "params": entry["params"],
- "tags": [], # puedes rellenar más adelante
+ "name": strategy_class.__name__,
+ "params": list(schema.keys()),
+ "parameters_meta": [
+ {
+ "name": name,
+ "type": meta.get("type"),
+ "default_value": meta.get("default"),
+ "choices": meta.get("choices"),
+ "min": meta.get("min"),
+ "max": meta.get("max"),
+ }
+ for name, meta in schema.items()
+ ],
})
-
return out
@@ -89,32 +70,163 @@ def _accumulate_equity(initial: float, returns_pct: List[float]) -> List[float]:
return eq
-def _build_param_values(min_v: float, max_v: float, step: float) -> List[float]:
- min_v = float(min_v)
- max_v = float(max_v)
- step = float(step)
+def _coerce_like(value: Any, meta: Dict[str, Any]) -> Any:
+ typ = (meta or {}).get("type")
+ if typ == "int":
+ return int(round(float(value)))
+ if typ == "float":
+ return float(value)
+ if typ == "bool":
+ if isinstance(value, bool):
+ return value
+ if isinstance(value, str):
+ return value.strip().lower() in {"1", "true", "yes", "on"}
+ return bool(value)
+ return value
- # Valor único si min == max
- if min_v == max_v:
- return [min_v]
- # Valor único si step <= 1
- if step <= 1:
- return [min_v]
+def _build_numeric_grid(meta: Dict[str, Any], baseline_value: Any, spec: Dict[str, Any]) -> List[Any]:
+ min_v = float(spec.get("min", baseline_value))
+ max_v = float(spec.get("max", baseline_value))
+ step = float(spec.get("step", 1))
- values = []
- v = min_v
- while v <= max_v:
- values.append(v)
- v += step
+ if max_v < min_v:
+ min_v, max_v = max_v, min_v
- return values
+ if step <= 0 or min_v == max_v:
+ values = [min_v]
+ else:
+ n_steps = int(np.floor((max_v - min_v) / step))
+ values = [min_v + i * step for i in range(n_steps + 1)]
+ if not np.isclose(values[-1], max_v):
+ values.append(max_v)
-# --------------------------------------------------
-# Main
-# --------------------------------------------------
+ coerced = []
+ for value in values:
+ coerced_value = _coerce_like(value, meta)
+ if coerced_value not in coerced:
+ coerced.append(coerced_value)
-def inspect_strategies_config(
+ baseline_coerced = _coerce_like(baseline_value, meta)
+ if baseline_coerced not in coerced:
+ coerced.append(baseline_coerced)
+
+ return coerced
+
+
+def _build_strategy_param_grid(strategy_class, baseline_parameters: Dict[str, Any], optimization_parameters: Dict[str, Any]) -> Dict[str, List[Any]]:
+ schema = strategy_class.parameters_schema()
+ grid: Dict[str, List[Any]] = {}
+
+ for param_name, meta in schema.items():
+ baseline_value = baseline_parameters.get(param_name, meta.get("default"))
+ spec = optimization_parameters.get(param_name, baseline_value)
+ ptype = meta.get("type")
+
+ if isinstance(spec, dict) and {"min", "max", "step"}.issubset(spec.keys()) and ptype in {"int", "float"}:
+ grid[param_name] = _build_numeric_grid(meta, baseline_value, spec)
+ else:
+ grid[param_name] = [_coerce_like(spec, meta)]
+
+ return grid
+
+
+def _summarize_wf_result(wf_res: Dict[str, Any], initial_capital: float) -> Tuple[Dict[str, Any], List[float], List[float]]:
+ win_df: pd.DataFrame = wf_res["windows"]
+ if win_df is None or win_df.empty:
+ summary = {
+ "n_windows": 0,
+ "oos_final_equity": float(initial_capital),
+ "oos_total_return_pct": 0.0,
+ "oos_max_dd_worst_pct": 0.0,
+ "oos_avg_sharpe": None,
+ "total_trades": 0,
+ "windows": [],
+ }
+ return summary, [], [float(initial_capital)]
+
+ windows_out = []
+ oos_returns = win_df["return_pct"].astype(float).tolist()
+ equity_curve = _accumulate_equity(float(initial_capital), oos_returns)
+
+ for _, r in win_df.iterrows():
+ windows_out.append({
+ "window": int(r["window"]),
+ "train_start": str(r["train_start"]),
+ "train_end": str(r["train_end"]),
+ "test_start": str(r["test_start"]),
+ "test_end": str(r["test_end"]),
+ "return_pct": float(r["return_pct"]),
+ "sharpe": float(r["sharpe"]),
+ "max_dd_pct": float(r["max_dd_pct"]),
+ "trades": int(r["trades"]),
+ "params": dict(r["params"]) if isinstance(r["params"], dict) else r["params"],
+ })
+
+ oos_final = float(equity_curve[-1]) if equity_curve else float(initial_capital)
+ summary = {
+ "n_windows": int(len(windows_out)),
+ "oos_final_equity": oos_final,
+ "oos_total_return_pct": float((oos_final / float(initial_capital) - 1.0) * 100.0),
+ "oos_max_dd_worst_pct": float(np.min(win_df["max_dd_pct"])) if not win_df.empty else 0.0,
+ "oos_avg_sharpe": float(win_df["sharpe"].mean()) if not win_df.empty else None,
+ "total_trades": int(win_df["trades"].sum()) if not win_df.empty else 0,
+ "windows": windows_out,
+ }
+ return summary, oos_returns, equity_curve
+
+
+def _decide_acceptance(baseline: Dict[str, Any], optimized: Dict[str, Any]) -> Dict[str, Any]:
+ return_delta = float(optimized["oos_total_return_pct"] - baseline["oos_total_return_pct"])
+ sharpe_delta = float((optimized.get("oos_avg_sharpe") or 0.0) - (baseline.get("oos_avg_sharpe") or 0.0))
+ dd_delta = float(optimized["oos_max_dd_worst_pct"] - baseline["oos_max_dd_worst_pct"])
+ trades_delta = int(optimized.get("total_trades", 0) - baseline.get("total_trades", 0))
+
+ reasons: List[str] = []
+ overfit_flag = False
+ accepted = True
+
+ if return_delta <= 0:
+ accepted = False
+ reasons.append("optimized_return_not_better")
+
+ if sharpe_delta < -0.15:
+ accepted = False
+ overfit_flag = True
+ reasons.append("sharpe_deterioration")
+
+ if dd_delta < -5.0:
+ accepted = False
+ overfit_flag = True
+ reasons.append("drawdown_worse_than_baseline")
+
+ baseline_trades = max(int(baseline.get("total_trades", 0)), 1)
+ optimized_trades = int(optimized.get("total_trades", 0))
+ if optimized_trades < max(3, int(round(baseline_trades * 0.6))):
+ accepted = False
+ overfit_flag = True
+ reasons.append("trade_count_collapse")
+
+ if return_delta > 0 and sharpe_delta >= -0.05 and dd_delta >= -2.5 and optimized_trades >= max(3, int(round(baseline_trades * 0.75))):
+ accepted = True
+ if reasons == ["optimized_return_not_better"]:
+ reasons = []
+
+ if accepted and not reasons:
+ reasons.append("improvement_accepted")
+
+ return {
+ "accepted": accepted,
+ "overfit_flag": overfit_flag,
+ "reasons": reasons,
+ "return_delta_pct": return_delta,
+ "sharpe_delta": sharpe_delta,
+ "max_dd_delta_pct": dd_delta,
+ "trades_delta": trades_delta,
+ }
+
+
+def inspect_optimization_config(
*,
storage: StorageManager,
payload,
@@ -122,21 +234,15 @@ def inspect_strategies_config(
include_series: bool,
progress_callback=None,
) -> Dict[str, Any]:
-
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
if df is None or df.empty:
- return {
- "valid": False,
- "status": "fail",
- "checks": {},
- "message": "No OHLCV data",
- "results": [],
- }
+ return {"valid": False, "status": "fail", "checks": {}, "message": "No OHLCV data", "results": []}
- checks: Dict[str, Any] = {}
- checks["data_quality"] = {
- "status": data_quality.get("status", "unknown"),
- "message": data_quality.get("message", ""),
+ checks: Dict[str, Any] = {
+ "data_quality": {
+ "status": data_quality.get("status", "unknown"),
+ "message": data_quality.get("message", ""),
+ }
}
if data_quality.get("status") == "fail":
@@ -144,105 +250,114 @@ def inspect_strategies_config(
"valid": False,
"status": "fail",
"checks": checks,
- "message": "Step 1 data quality is FAIL. Strategies cannot be validated.",
+ "message": "Step 1 data quality is FAIL. Optimization cannot be executed.",
"results": [],
- "series": {} if include_series else None,
+ "series": {"strategies": {}} if include_series else None,
}
stop_loss = _build_stop_loss(payload.stop)
base_sizer = _build_position_sizer(payload.risk)
+ class _CappedSizer(type(base_sizer)):
+ def __init__(self, inner):
+ self.inner = inner
+
+ def calculate_size(self, *, capital, entry_price, stop_price=None, max_capital=None, volatility=None):
+ u = self.inner.calculate_size(
+ capital=capital,
+ entry_price=entry_price,
+ stop_price=stop_price,
+ max_capital=max_capital,
+ volatility=volatility,
+ )
+ return _cap_units_by_max_position_fraction(
+ units=float(u),
+ capital=float(capital),
+ entry_price=float(entry_price),
+ max_position_fraction=float(payload.risk.max_position_fraction),
+ )
+
+ capped_sizer = _CappedSizer(base_sizer)
+
train_td = pd.Timedelta(days=int(payload.wf.train_days))
test_td = pd.Timedelta(days=int(payload.wf.test_days))
step_td = pd.Timedelta(days=int(payload.wf.step_days or payload.wf.test_days))
- overall_status = "ok"
results: List[Dict[str, Any]] = []
series: Dict[str, Any] = {"strategies": {}} if include_series else {}
+ overall_status = "ok"
+
+ progress_total = max(len(payload.strategies) * 2, 1)
+ progress_done = 0
+
+ def report_progress(strategy_id: str, phase: str, local_window: int, local_total: int):
+ nonlocal progress_done
+ if progress_callback is None:
+ return
+ effective_done = progress_done + (local_window / max(local_total, 1))
+ progress_callback(
+ current_strategy=strategy_id,
+ current_phase=phase,
+ completed_runs=effective_done,
+ total_runs=progress_total,
+ )
for sel in payload.strategies:
-
sid = sel.strategy_id
- entry = STRATEGY_REGISTRY.get(sid)
-
- if entry is None:
+ strategy_class = STRATEGY_REGISTRY.get(sid)
+ if strategy_class is None:
results.append({
"strategy_id": sid,
"status": "fail",
"message": f"Unknown strategy_id: {sid}",
- "n_windows": 0,
- "oos_final_equity": payload.account_equity,
- "oos_total_return_pct": 0.0,
- "oos_max_dd_worst_pct": 0.0,
- "degradation_sharpe": None,
- "windows": [],
+ "promotion_status": sel.promotion_status,
+ "promotion_score": sel.promotion_score,
+ "selection_source": sel.selection_source,
+ "diversity_blocked_by": sel.diversity_blocked_by,
+ "diversity_correlation": sel.diversity_correlation,
+ "baseline_parameters": sel.baseline_parameters,
+ "optimized_parameters": sel.baseline_parameters,
+ "optimization_parameters": sel.optimization_parameters,
+ "baseline": _summarize_wf_result({"windows": pd.DataFrame()}, payload.account_equity)[0],
+ "optimized": _summarize_wf_result({"windows": pd.DataFrame()}, payload.account_equity)[0],
+ "decision": {
+ "accepted": False,
+ "overfit_flag": False,
+ "reasons": ["unknown_strategy"],
+ "return_delta_pct": 0.0,
+ "sharpe_delta": 0.0,
+ "max_dd_delta_pct": 0.0,
+ "trades_delta": 0,
+ },
})
overall_status = "fail"
continue
- strategy_class = entry["class"]
- valid_params = set(entry["params"])
- range_params = set(sel.parameters.keys())
-
-
- # 🔒 Validación estricta de parámetros
- if range_params != valid_params:
- msg = f"Parameter keys {range_params} do not match expected {valid_params}"
-
- results.append({
- "strategy_id": sid,
- "status": "fail",
- "message": msg,
- "n_windows": 0,
- "oos_final_equity": payload.account_equity,
- "oos_total_return_pct": 0.0,
- "oos_max_dd_worst_pct": 0.0,
- "degradation_sharpe": None,
- "windows": [],
- })
- overall_status = "fail"
- continue
-
- # --------------------------------------------------
- # Convert ranges -> param_grid real
- # --------------------------------------------------
- param_grid = {}
-
- for pname, prange in sel.parameters.items():
- values = _build_param_values(
- min_v=prange.min,
- max_v=prange.max,
- step=prange.step,
- )
- param_grid[pname] = values
-
- # Wrapper sizer
- class _CappedSizer(type(base_sizer)):
- def __init__(self, inner):
- self.inner = inner
-
- def calculate_size(self, *, capital, entry_price, stop_price=None, max_capital=None, volatility=None):
- u = self.inner.calculate_size(
- capital=capital,
- entry_price=entry_price,
- stop_price=stop_price,
- max_capital=max_capital,
- volatility=volatility,
- )
- return _cap_units_by_max_position_fraction(
- units=float(u),
- capital=float(capital),
- entry_price=float(entry_price),
- max_position_fraction=float(payload.risk.max_position_fraction),
- )
-
- capped_sizer = _CappedSizer(base_sizer)
-
- log.info(f"🧠 Step3 | WF run | strategy={sid}")
-
try:
- wf = WalkForwardValidator(
+ baseline_wf = WalkForwardValidator(
strategy_class=strategy_class,
+ fixed_params=sel.baseline_parameters,
+ param_grid=None,
+ data=df,
+ train_window=train_td,
+ test_window=test_td,
+ step_size=step_td,
+ initial_capital=float(payload.account_equity),
+ commission=float(payload.commission),
+ slippage=float(payload.slippage),
+ optimizer_metric=str(payload.optimization.optimizer_metric),
+ position_sizer=capped_sizer,
+ stop_loss=stop_loss,
+ max_combinations=int(payload.optimization.max_combinations),
+ progress_callback=lambda window_id, total_windows, sid=sid: report_progress(sid, "baseline", window_id, total_windows),
+ )
+ baseline_res = baseline_wf.run()
+ progress_done += 1
+
+ param_grid = _build_strategy_param_grid(strategy_class, sel.baseline_parameters, sel.optimization_parameters)
+ optimized_wf = WalkForwardValidator(
+ strategy_class=strategy_class,
+ fixed_params=None,
param_grid=param_grid,
data=df,
train_window=train_td,
@@ -255,103 +370,97 @@ def inspect_strategies_config(
position_sizer=capped_sizer,
stop_loss=stop_loss,
max_combinations=int(payload.optimization.max_combinations),
- progress_callback=progress_callback,
+ progress_callback=lambda window_id, total_windows, sid=sid: report_progress(sid, "optimized", window_id, total_windows),
)
+ optimized_res = optimized_wf.run()
+ progress_done += 1
- wf_res = wf.run()
- win_df: pd.DataFrame = wf_res["windows"]
+ baseline_summary, baseline_returns, baseline_equity = _summarize_wf_result(baseline_res, payload.account_equity)
+ optimized_summary, optimized_returns, optimized_equity = _summarize_wf_result(optimized_res, payload.account_equity)
+ decision = _decide_acceptance(baseline_summary, optimized_summary)
- if win_df is None or win_df.empty:
+ optimized_parameters = sel.baseline_parameters
+ raw_results = optimized_res.get("raw_results", [])
+ if raw_results:
+ optimized_parameters = raw_results[-1].get("best_params", optimized_parameters)
+
+ status = "ok" if decision["accepted"] else "warning"
+ message = "Optimization accepted" if decision["accepted"] else "Optimization rejected by anti-overfitting rules"
+ if optimized_summary["n_windows"] == 0 or baseline_summary["n_windows"] == 0:
status = "fail"
- msg = "WF produced no valid windows"
+ message = "Walk-forward produced no valid windows"
overall_status = "fail"
- windows_out = []
- oos_returns = []
- else:
- trades = win_df["trades"].astype(int).tolist()
- too_few = sum(t < int(payload.optimization.min_trades_test) for t in trades)
-
- if too_few > 0:
- status = "warning"
- msg = f"{too_few} windows below min_trades_test"
- if overall_status == "ok":
- overall_status = "warning"
- else:
- status = "ok"
- msg = "WF OK"
-
- windows_out = []
- for _, r in win_df.iterrows():
- windows_out.append({
- "window": int(r["window"]),
- "train_start": str(r["train_start"]),
- "train_end": str(r["train_end"]),
- "test_start": str(r["test_start"]),
- "test_end": str(r["test_end"]),
- "return_pct": float(r["return_pct"]),
- "sharpe": float(r["sharpe"]),
- "max_dd_pct": float(r["max_dd_pct"]),
- "trades": int(r["trades"]),
- "params": dict(r["params"]) if isinstance(r["params"], dict) else r["params"],
- })
-
- oos_returns = win_df["return_pct"].astype(float).tolist()
-
- eq_curve = _accumulate_equity(float(payload.account_equity), oos_returns)
- oos_final = float(eq_curve[-1]) if eq_curve else float(payload.account_equity)
- oos_total_return = (oos_final / float(payload.account_equity) - 1.0) * 100.0
- oos_max_dd = float(np.min(win_df["max_dd_pct"])) if (win_df is not None and not win_df.empty) else 0.0
+ elif status == "warning" and overall_status == "ok":
+ overall_status = "warning"
results.append({
"strategy_id": sid,
"status": status,
- "message": msg,
- "n_windows": int(len(windows_out)),
- "oos_final_equity": oos_final,
- "oos_total_return_pct": float(oos_total_return),
- "oos_max_dd_worst_pct": float(oos_max_dd),
- "degradation_sharpe": None,
- "windows": windows_out,
+ "message": message,
+ "promotion_status": sel.promotion_status,
+ "promotion_score": sel.promotion_score,
+ "selection_source": sel.selection_source,
+ "diversity_blocked_by": sel.diversity_blocked_by,
+ "diversity_correlation": sel.diversity_correlation,
+ "baseline_parameters": sel.baseline_parameters,
+ "optimized_parameters": optimized_parameters,
+ "optimization_parameters": sel.optimization_parameters,
+ "baseline": baseline_summary,
+ "optimized": optimized_summary,
+ "decision": decision,
})
if include_series:
series["strategies"][sid] = {
- "window_returns_pct": oos_returns,
- "window_equity": eq_curve,
+ "baseline": {
+ "window_returns_pct": baseline_returns,
+ "window_equity": baseline_equity,
+ },
+ "optimized": {
+ "window_returns_pct": optimized_returns,
+ "window_equity": optimized_equity,
+ },
}
-
- except Exception as e:
- log.error(f"❌ Step3 WF error | strategy={sid} | {e}")
+ except Exception as exc:
+ log.error(f"❌ Step4 optimization error | strategy={sid} | {exc}")
results.append({
"strategy_id": sid,
"status": "fail",
- "message": f"Exception: {e}",
- "n_windows": 0,
- "oos_final_equity": float(payload.account_equity),
- "oos_total_return_pct": 0.0,
- "oos_max_dd_worst_pct": 0.0,
- "degradation_sharpe": None,
- "windows": [],
+ "message": f"Exception: {exc}",
+ "promotion_status": sel.promotion_status,
+ "promotion_score": sel.promotion_score,
+ "selection_source": sel.selection_source,
+ "diversity_blocked_by": sel.diversity_blocked_by,
+ "diversity_correlation": sel.diversity_correlation,
+ "baseline_parameters": sel.baseline_parameters,
+ "optimized_parameters": sel.baseline_parameters,
+ "optimization_parameters": sel.optimization_parameters,
+ "baseline": _summarize_wf_result({"windows": pd.DataFrame()}, payload.account_equity)[0],
+ "optimized": _summarize_wf_result({"windows": pd.DataFrame()}, payload.account_equity)[0],
+ "decision": {
+ "accepted": False,
+ "overfit_flag": False,
+ "reasons": ["exception"],
+ "return_delta_pct": 0.0,
+ "sharpe_delta": 0.0,
+ "max_dd_delta_pct": 0.0,
+ "trades_delta": 0,
+ },
})
overall_status = "fail"
valid = overall_status != "fail"
-
- human_msg = {
- "ok": "Strategies validation OK",
- "warning": "Strategies validation has warnings",
- "fail": "Strategies validation FAILED",
- }[overall_status]
-
out = {
"valid": valid,
"status": overall_status,
"checks": checks,
- "message": human_msg,
+ "message": {
+ "ok": "Optimization completed successfully",
+ "warning": "Optimization completed with warnings",
+ "fail": "Optimization failed",
+ }[overall_status],
"results": results,
}
-
if include_series:
out["series"] = series
-
return out
diff --git a/src/web/api/v2/routers/calibration_optimization.py b/src/web/api/v2/routers/calibration_optimization.py
index 44a86e9..254831d 100644
--- a/src/web/api/v2/routers/calibration_optimization.py
+++ b/src/web/api/v2/routers/calibration_optimization.py
@@ -1,105 +1,120 @@
# src/web/api/v2/routers/calibration_strategies.py
-
import logging
import re
+import threading
import uuid
from pathlib import Path
from typing import Dict
-from fastapi import APIRouter, Depends, HTTPException, Request
-from fastapi.responses import JSONResponse, HTMLResponse
+from fastapi import APIRouter, Depends, HTTPException
+from fastapi.responses import JSONResponse
-from src.data.storage import StorageManager
-from src.calibration.optimization_inspector import (
- inspect_strategies_config,
- list_available_strategies,
-)
+from src.calibration.optimization_inspector import inspect_optimization_config, list_available_strategies
from src.calibration.reports.optimization_report import generate_strategies_report_pdf
+from src.data.storage import StorageManager
from ..schemas.calibration_optimization import (
- CalibrationStrategiesInspectRequest,
- CalibrationStrategiesInspectResponse,
- CalibrationStrategiesValidateResponse,
+ CalibrationOptimizationInspectRequest,
+ CalibrationOptimizationInspectResponse,
+ CalibrationOptimizationValidateResponse,
)
logger = logging.getLogger("tradingbot.api.v2")
-router = APIRouter(
- prefix="/calibration/optimization",
- tags=["calibration"],
-)
-
+router = APIRouter(prefix="/calibration/optimization", tags=["calibration"])
WF_JOBS: Dict[str, Dict] = {}
+
def get_storage() -> StorageManager:
return StorageManager.from_env()
@router.get("/catalog")
def strategy_catalog():
- strategies = list_available_strategies()
+ return {"strategies": list_available_strategies()}
- # Añadimos defaults sugeridos
- for s in strategies:
- s["parameters_meta"] = [
- {
- "name": p,
- "type": "int",
- "default_min": 10,
- "default_max": 50,
- "default_step": 10,
- }
- for p in s["params"]
- ]
- return {"strategies": strategies}
-
-@router.post("/inspect", response_model=CalibrationStrategiesInspectResponse)
-def inspect_strategies(
- payload: CalibrationStrategiesInspectRequest,
- storage: StorageManager = Depends(get_storage),
-):
+@router.post("/inspect", response_model=CalibrationOptimizationInspectResponse)
+def inspect_optimization(payload: CalibrationOptimizationInspectRequest, storage: StorageManager = Depends(get_storage)):
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
if df is None or df.empty:
raise HTTPException(status_code=400, detail="No OHLCV data found. Run Step 1 first.")
from .calibration_data import analyze_data_quality
data_quality = analyze_data_quality(df, payload.timeframe)
-
- result = inspect_strategies_config(
+ result = inspect_optimization_config(
storage=storage,
payload=payload,
data_quality=data_quality,
include_series=False,
)
- return CalibrationStrategiesInspectResponse(**result)
+ return CalibrationOptimizationInspectResponse(**result)
-@router.post("/validate", response_model=CalibrationStrategiesValidateResponse)
-def validate_strategies(
- payload: CalibrationStrategiesInspectRequest,
- storage: StorageManager = Depends(get_storage),
-):
+
+@router.post("/validate", response_model=CalibrationOptimizationValidateResponse)
+def validate_optimization(payload: CalibrationOptimizationInspectRequest, storage: StorageManager = Depends(get_storage)):
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
if df is None or df.empty:
raise HTTPException(status_code=400, detail="No OHLCV data found. Run Step 1 first.")
from .calibration_data import analyze_data_quality
data_quality = analyze_data_quality(df, payload.timeframe)
-
- result = inspect_strategies_config(
+ result = inspect_optimization_config(
storage=storage,
payload=payload,
data_quality=data_quality,
include_series=True,
)
- return CalibrationStrategiesValidateResponse(**result)
+ return CalibrationOptimizationValidateResponse(**result)
+
+
+@router.post("/run")
+def run_optimization_async(payload: CalibrationOptimizationInspectRequest, storage: StorageManager = Depends(get_storage)):
+ job_id = uuid.uuid4().hex
+ WF_JOBS[job_id] = {
+ "status": "running",
+ "progress": 0,
+ "current_strategy": None,
+ "current_phase": "queued",
+ "result": None,
+ }
+
+ def background_job():
+ def progress_cb(current_strategy, current_phase, completed_runs, total_runs):
+ WF_JOBS[job_id]["current_strategy"] = current_strategy
+ WF_JOBS[job_id]["current_phase"] = current_phase
+ WF_JOBS[job_id]["progress"] = int(max(0, min(100, round((completed_runs / max(total_runs, 1)) * 100))))
+
+ try:
+ result = inspect_optimization_config(
+ storage=storage,
+ payload=payload,
+ data_quality={"status": "ok"},
+ include_series=True,
+ progress_callback=progress_cb,
+ )
+ WF_JOBS[job_id]["status"] = "done"
+ WF_JOBS[job_id]["progress"] = 100
+ WF_JOBS[job_id]["current_phase"] = "done"
+ WF_JOBS[job_id]["result"] = result
+ except Exception as exc:
+ WF_JOBS[job_id]["status"] = "fail"
+ WF_JOBS[job_id]["current_phase"] = "error"
+ WF_JOBS[job_id]["result"] = {"detail": str(exc)}
+
+ thread = threading.Thread(target=background_job)
+ thread.start()
+ return {"job_id": job_id}
+
+
+@router.get("/status/{job_id}")
+def get_status(job_id: str):
+ return WF_JOBS.get(job_id, {"status": "unknown"})
+
@router.post("/report")
-def report_strategies(
- payload: CalibrationStrategiesInspectRequest,
- storage: StorageManager = Depends(get_storage),
-):
- logger.info(f"🧾 Generating strategies report | {payload.symbol} {payload.timeframe}")
+def report_optimization(payload: CalibrationOptimizationInspectRequest, storage: StorageManager = Depends(get_storage)):
+ logger.info("🧾 Generating optimization report | %s %s", payload.symbol, payload.timeframe)
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
if df is None or df.empty:
@@ -107,30 +122,21 @@ def report_strategies(
from .calibration_data import analyze_data_quality
data_quality = analyze_data_quality(df, payload.timeframe)
-
- result = inspect_strategies_config(
+ result = inspect_optimization_config(
storage=storage,
payload=payload,
data_quality=data_quality,
include_series=True,
)
- # ---------------------------------------------
- # Prepare PDF output path (outside src)
- # ---------------------------------------------
- project_root = Path(__file__).resolve().parents[4] # .../src
- # project_root currently points to src/web/api/v2/routers -> parents[4] == src
- project_root = project_root.parent # repo root
-
- reports_dir = project_root / "reports" / "strategies"
+ project_root = Path(__file__).resolve().parents[4].parent
+ reports_dir = project_root / "reports" / "optimization"
reports_dir.mkdir(parents=True, exist_ok=True)
safe_symbol = re.sub(r"[^a-zA-Z0-9_-]", "_", payload.symbol)
- filename = f"strategies_report_{safe_symbol}_{payload.timeframe}_{uuid.uuid4().hex}.pdf"
-
+ filename = f"optimization_report_{safe_symbol}_{payload.timeframe}_{uuid.uuid4().hex}.pdf"
symbol_dir = reports_dir / safe_symbol
symbol_dir.mkdir(exist_ok=True)
-
output_path = symbol_dir / filename
generate_strategies_report_pdf(
@@ -141,9 +147,6 @@ def report_strategies(
"Account equity": payload.account_equity,
},
config={
- "Stop type": payload.stop.type,
- "Risk per trade (%)": payload.risk.risk_fraction * 100,
- "Max position fraction (%)": payload.risk.max_position_fraction * 100,
"WF train_days": payload.wf.train_days,
"WF test_days": payload.wf.test_days,
"WF step_days": payload.wf.step_days or payload.wf.test_days,
@@ -153,54 +156,5 @@ def report_strategies(
results=result,
)
- public_url = f"/reports/strategies/{safe_symbol}/{filename}"
+ public_url = f"/reports/optimization/{safe_symbol}/{filename}"
return JSONResponse(content={"status": result.get("status", "ok"), "url": public_url})
-
-@router.post("/run")
-def run_strategies_async(
- payload: CalibrationStrategiesInspectRequest,
- storage: StorageManager = Depends(get_storage),
-):
- import threading
- import uuid
-
- job_id = uuid.uuid4().hex
-
- WF_JOBS[job_id] = {
- "status": "running",
- "progress": 0,
- "current_window": 0,
- "total_windows": 0,
- "current_strategy": None,
- "result": None,
- }
-
- def background_job():
-
- def progress_cb(window_id, total_windows):
- WF_JOBS[job_id]["current_window"] = window_id
- WF_JOBS[job_id]["total_windows"] = total_windows
- WF_JOBS[job_id]["progress"] = int(
- window_id / total_windows * 100
- )
-
- result = inspect_strategies_config(
- storage=storage,
- payload=payload,
- data_quality={"status": "ok"},
- include_series=True,
- progress_callback=progress_cb, # ← lo pasamos
- )
-
- WF_JOBS[job_id]["status"] = "done"
- WF_JOBS[job_id]["progress"] = 100
- WF_JOBS[job_id]["result"] = result
-
- thread = threading.Thread(target=background_job)
- thread.start()
-
- return {"job_id": job_id}
-
-@router.get("/status/{job_id}")
-def get_status(job_id: str):
- return WF_JOBS.get(job_id, {"status": "unknown"})
diff --git a/src/web/api/v2/schemas/calibration_optimization.py b/src/web/api/v2/schemas/calibration_optimization.py
index d1c9ad3..e1a486c 100644
--- a/src/web/api/v2/schemas/calibration_optimization.py
+++ b/src/web/api/v2/schemas/calibration_optimization.py
@@ -1,5 +1,4 @@
# src/web/api/v2/schemas/calibration_strategies.py
-
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, Field
@@ -9,38 +8,43 @@ from .calibration_risk import StopConfigSchema, RiskConfigSchema, GlobalRiskRule
class WalkForwardConfigSchema(BaseModel):
train_days: int = Field(..., gt=0)
test_days: int = Field(..., gt=0)
- step_days: Optional[int] = Field(None, gt=0) # if None => step = test_days
+ step_days: Optional[int] = Field(None, gt=0)
class OptimizationConfigSchema(BaseModel):
optimizer_metric: str = Field("sharpe_ratio")
- max_combinations: int = Field(500, gt=0)
+ max_combinations: int = Field(300, gt=0)
min_trades_train: int = Field(30, ge=0)
min_trades_test: int = Field(10, ge=0)
-class ParameterRangeSchema(BaseModel):
+class NumericParameterRangeSchema(BaseModel):
min: float
max: float
step: float
-class StrategySelectionSchema(BaseModel):
+class OptimizationStrategySchema(BaseModel):
strategy_id: str
- parameters: Dict[str, ParameterRangeSchema]
+ baseline_parameters: Dict[str, Any]
+ optimization_parameters: Dict[str, Any]
+ promotion_status: Optional[str] = None
+ promotion_score: Optional[float] = None
+ selection_source: Optional[str] = None
+ diversity_blocked_by: Optional[str] = None
+ diversity_correlation: Optional[float] = None
-class CalibrationStrategiesInspectRequest(BaseModel):
+class CalibrationOptimizationInspectRequest(BaseModel):
symbol: str
timeframe: str
- # snapshot from Step 2 (closed)
stop: StopConfigSchema
risk: RiskConfigSchema
global_rules: GlobalRiskRulesSchema
account_equity: float = Field(..., gt=0)
- strategies: List[StrategySelectionSchema]
+ strategies: List[OptimizationStrategySchema]
wf: WalkForwardConfigSchema
optimization: OptimizationConfigSchema
@@ -54,7 +58,6 @@ class WindowRowSchema(BaseModel):
train_end: str
test_start: str
test_end: str
-
return_pct: float
sharpe: float
max_dd_pct: float
@@ -62,28 +65,53 @@ class WindowRowSchema(BaseModel):
params: Dict[str, Any]
-class StrategyRunResultSchema(BaseModel):
- strategy_id: str
- status: Literal["ok", "warning", "fail"]
- message: str
-
+class RunSummarySchema(BaseModel):
n_windows: int
oos_final_equity: float
oos_total_return_pct: float
oos_max_dd_worst_pct: float
- degradation_sharpe: Optional[float] = None
-
+ oos_avg_sharpe: Optional[float] = None
+ total_trades: int = 0
windows: List[WindowRowSchema]
-class CalibrationStrategiesInspectResponse(BaseModel):
+class OptimizationDecisionSchema(BaseModel):
+ accepted: bool
+ overfit_flag: bool = False
+ reasons: List[str] = []
+ return_delta_pct: float = 0.0
+ sharpe_delta: float = 0.0
+ max_dd_delta_pct: float = 0.0
+ trades_delta: int = 0
+
+
+class StrategyOptimizationResultSchema(BaseModel):
+ strategy_id: str
+ status: Literal["ok", "warning", "fail"]
+ message: str
+
+ promotion_status: Optional[str] = None
+ promotion_score: Optional[float] = None
+ selection_source: Optional[str] = None
+ diversity_blocked_by: Optional[str] = None
+ diversity_correlation: Optional[float] = None
+
+ baseline_parameters: Dict[str, Any]
+ optimized_parameters: Dict[str, Any]
+ optimization_parameters: Dict[str, Any]
+
+ baseline: RunSummarySchema
+ optimized: RunSummarySchema
+ decision: OptimizationDecisionSchema
+
+
+class CalibrationOptimizationInspectResponse(BaseModel):
valid: bool
status: Literal["ok", "warning", "fail"]
checks: Dict[str, Any]
message: str
-
- results: List[StrategyRunResultSchema]
+ results: List[StrategyOptimizationResultSchema]
-class CalibrationStrategiesValidateResponse(CalibrationStrategiesInspectResponse):
+class CalibrationOptimizationValidateResponse(CalibrationOptimizationInspectResponse):
series: Dict[str, Any]
diff --git a/src/web/ui/v2/static/js/pages/calibration_optimization.js b/src/web/ui/v2/static/js/pages/calibration_optimization.js
index 2f5d5b0..dc7f6dd 100644
--- a/src/web/ui/v2/static/js/pages/calibration_optimization.js
+++ b/src/web/ui/v2/static/js/pages/calibration_optimization.js
@@ -1,1040 +1,355 @@
// src/web/ui/v2/static/js/pages/calibration_strategies.js
+console.log("[calibration_optimization] loaded", new Date().toISOString());
-console.log("[calibration_strategies] script loaded ✅", new Date().toISOString());
-
+let STEP4_ARTIFACT = null;
+let STEP4_RESULT = null;
let STRATEGY_CATALOG = [];
-let strategySlots = [];
-const MAX_STRATEGIES = 10;
-// =================================================
-// WIZARD NAVIGATION
-// =================================================
-
-function enableNextStep() {
- const btn = document.getElementById("next-step-btn");
- if (!btn) return;
- btn.classList.remove("btn-outline-secondary");
- btn.classList.add("btn-outline-primary");
- btn.setAttribute("aria-disabled", "false");
+function byId(id) {
+ return document.getElementById(id);
}
-function disableNextStep() {
- const btn = document.getElementById("next-step-btn");
- if (!btn) return;
- btn.classList.remove("btn-outline-primary");
- btn.classList.add("btn-outline-secondary");
- btn.setAttribute("aria-disabled", "true");
+function setVal(id, v) {
+ const el = byId(id); if (el) el.value = v ?? "";
}
-// =================================================
-// UTILS
-// =================================================
+function str(id) {
+ return String(byId(id)?.value ?? "").trim();
+}
-function loadContextFromLocalStorage() {
- const symbol = localStorage.getItem("calibration.symbol");
- const timeframe = localStorage.getItem("calibration.timeframe");
- if (symbol) setVal("symbol", symbol);
- if (timeframe) setVal("timeframe", timeframe);
+function num(id) {
+ const v = parseFloat(byId(id)?.value ?? "");
+ return Number.isFinite(v) ? v : null;
+}
- // Step 2 snapshot (if stored)
- const stop_type = localStorage.getItem("calibration.stop.type");
- const stop_fraction = localStorage.getItem("calibration.stop.stop_fraction");
- const atr_period = localStorage.getItem("calibration.stop.atr_period");
- const atr_multiplier = localStorage.getItem("calibration.stop.atr_multiplier");
- const risk_fraction = localStorage.getItem("calibration.risk.risk_fraction");
- const max_position_fraction = localStorage.getItem("calibration.risk.max_position_fraction");
- const max_drawdown_pct = localStorage.getItem("calibration.rules.max_drawdown_pct");
- const daily_loss_limit_pct = localStorage.getItem("calibration.rules.daily_loss_limit_pct");
- const max_consecutive_losses = localStorage.getItem("calibration.rules.max_consecutive_losses");
- const cooldown_bars = localStorage.getItem("calibration.rules.cooldown_bars");
- const account_equity = localStorage.getItem("calibration.account_equity");
+function setStatusBadge(status) {
+ const badge = byId("resultsBadge");
+ if (!badge) return;
+ badge.className = "badge ";
+ if (status === "ok") badge.className += "bg-success";
+ else if (status === "warning") badge.className += "bg-warning";
+ else if (status === "fail") badge.className += "bg-danger";
+ else badge.className += "bg-secondary";
+ badge.textContent = status || "—";
+}
- if (account_equity) setVal("account_equity", account_equity);
+function loadInheritedContext() {
+ const raw = localStorage.getItem("calibration.step4.selection");
+ if (!raw) throw new Error("Step 4 Selection Artifact not found in localStorage.");
+ STEP4_ARTIFACT = JSON.parse(raw);
- if (stop_type) setVal("stop_type", stop_type);
- if (stop_fraction) setVal("stop_fraction", stop_fraction);
- if (atr_period) setVal("atr_period", atr_period);
- if (atr_multiplier) setVal("atr_multiplier", atr_multiplier);
- if (risk_fraction) setVal("risk_fraction", risk_fraction);
- if (max_position_fraction) setVal("max_position_fraction", max_position_fraction);
- if (max_drawdown_pct) setVal("max_drawdown_pct", max_drawdown_pct);
- if (daily_loss_limit_pct) setVal("daily_loss_limit_pct", daily_loss_limit_pct);
- if (max_consecutive_losses) setVal("max_consecutive_losses", max_consecutive_losses);
- if (cooldown_bars) setVal("cooldown_bars", cooldown_bars);
+ byId("artifactPreview").textContent = JSON.stringify(STEP4_ARTIFACT, null, 2);
+ byId("artifactSummary").textContent = `Selected strategies: ${(STEP4_ARTIFACT.selected_strategies || []).length}`;
+
+ setVal("symbol", STEP4_ARTIFACT.symbol);
+ setVal("timeframe", STEP4_ARTIFACT.timeframe);
+ setVal("account_equity", localStorage.getItem("calibration.account_equity") || 10000);
+ setVal("commission", localStorage.getItem("calibration.commission") || 0.001);
+ setVal("slippage", localStorage.getItem("calibration.slippage") || 0.0005);
+ setVal("wf_train_days", localStorage.getItem("calibration.wf.train_days") || 120);
+ setVal("wf_test_days", localStorage.getItem("calibration.wf.test_days") || 30);
+ setVal("wf_step_days", localStorage.getItem("calibration.wf.step_days") || 30);
+ setVal("opt_metric", localStorage.getItem("calibration.optimization.metric") || "sharpe_ratio");
+ setVal("opt_max_combinations", localStorage.getItem("calibration.optimization.max_combinations") || 300);
+ setVal("opt_min_trades_train", localStorage.getItem("calibration.optimization.min_trades_train") || 30);
+ setVal("opt_min_trades_test", localStorage.getItem("calibration.wf.min_trades_test") || 10);
+
+ setVal("risk_fraction", ((parseFloat(localStorage.getItem("calibration.risk.risk_fraction") || "0.01")) * 100).toFixed(2));
+ setVal("max_position_fraction", ((parseFloat(localStorage.getItem("calibration.risk.max_position_fraction") || "0.95")) * 100).toFixed(2));
+ setVal("stop_type", localStorage.getItem("calibration.stop.type") || "fixed");
+ setVal("stop_fraction", ((parseFloat(localStorage.getItem("calibration.stop.stop_fraction") || "0.01")) * 100).toFixed(2));
+ setVal("atr_period", localStorage.getItem("calibration.stop.atr_period") || 14);
+ setVal("atr_multiplier", localStorage.getItem("calibration.stop.atr_multiplier") || 3);
+ setVal("max_drawdown_pct", ((parseFloat(localStorage.getItem("calibration.rules.max_drawdown_pct") || "0.2")) * 100).toFixed(2));
+ setVal("daily_loss_limit_pct", localStorage.getItem("calibration.rules.daily_loss_limit_pct") ? (parseFloat(localStorage.getItem("calibration.rules.daily_loss_limit_pct")) * 100).toFixed(2) : "");
+ setVal("max_consecutive_losses", localStorage.getItem("calibration.rules.max_consecutive_losses") || "");
+ setVal("cooldown_bars", localStorage.getItem("calibration.rules.cooldown_bars") || "");
+}
+
+async function loadCatalog() {
+ const res = await fetch("/api/v2/calibration/strategies/catalog");
+ if (!res.ok) throw new Error(`Catalog failed: ${res.status}`);
+ const data = await res.json();
+ STRATEGY_CATALOG = data.strategies || [];
+}
+
+function findStrategyMeta(strategyId) {
+ return (STRATEGY_CATALOG || []).find((s) => s.strategy_id === strategyId);
+}
+
+function defaultRange(meta, baseline) {
+ const type = meta?.type || typeof baseline;
+ if (type === "int") {
+ const step = Math.max(1, Math.round(Math.abs(Number(baseline || meta?.default_value || 1)) * 0.2));
+ return { min: Math.max(meta?.min ?? 1, Number(baseline) - step), max: Math.min(meta?.max ?? Number(baseline) + step, Number(baseline) + step), step };
+ }
+ if (type === "float") {
+ const span = Math.max(0.1, Math.abs(Number(baseline || meta?.default_value || 1)) * 0.2);
+ const step = Math.max(0.1, +(span / 2).toFixed(4));
+ return { min: +(Math.max(meta?.min ?? -1e9, Number(baseline) - span)).toFixed(4), max: +(Math.min(meta?.max ?? 1e9, Number(baseline) + span)).toFixed(4), step };
+ }
+ return null;
+}
+
+function renderStrategyCards() {
+ const wrap = byId("optimizationStrategies");
+ const selected = STEP4_ARTIFACT.selected_strategies || [];
+ byId("strategyCount").textContent = `${selected.length} strategy(ies)`;
+ wrap.innerHTML = "";
+
+ selected.forEach((s, idx) => {
+ const meta = findStrategyMeta(s.strategy_id);
+ const paramsMeta = meta?.parameters_meta || [];
+ const rows = paramsMeta.map((p) => {
+ const baseline = s.parameters?.[p.name] ?? p.default_value ?? "";
+ const range = defaultRange(p, baseline);
+ if (range) {
+ return `
+
+ ${p.name} |
+ ${baseline} |
+ |
+ |
+ |
+
`;
+ }
+ return `
+
+ ${p.name} |
+ ${String(baseline)} |
+ Fixed in optimization grid |
+
`;
+ }).join("");
+
+ const div = document.createElement("div");
+ div.className = "card";
+ div.innerHTML = `
+
+
+
+
+
+ | Param | Baseline | Min | Max | Step |
+
+ ${rows}
+
+
+
`;
+ wrap.appendChild(div);
+ });
}
function buildPayload() {
- const symbol = str("symbol");
- const timeframe = str("timeframe");
- const stopType = str("stop_type");
-
- if (!symbol || !timeframe) {
- throw new Error("symbol/timeframe missing");
- }
-
- const stop = { type: stopType };
-
- if (stopType === "fixed" || stopType === "trailing") {
- stop.stop_fraction = (num("stop_fraction") ?? 1.0) / 100;
- }
- if (stopType === "atr") {
- stop.atr_period = num("atr_period") ?? 14;
- stop.atr_multiplier = num("atr_multiplier") ?? 3.0;
- }
-
- const risk_fraction = (num("risk_fraction") ?? 1.0) / 100;
- const max_position_fraction = (num("max_position_fraction") ?? 95) / 100;
-
- const global_rules = {
- max_drawdown_pct: (num("max_drawdown_pct") ?? 20) / 100,
- daily_loss_limit_pct: num("daily_loss_limit_pct") ? num("daily_loss_limit_pct") / 100 : null,
- max_consecutive_losses: num("max_consecutive_losses"),
- cooldown_bars: num("cooldown_bars"),
- };
-
- const wf_train_days = num("wf_train_days") ?? 120;
- const wf_test_days = num("wf_test_days") ?? 30;
- const wf_step_days = num("wf_step_days");
-
- const strategies = collectSelectedStrategies();
+ const strategies = (STEP4_ARTIFACT.selected_strategies || []).map((s, idx) => {
+ const meta = findStrategyMeta(s.strategy_id);
+ const paramsMeta = meta?.parameters_meta || [];
+ const optimization_parameters = {};
+ paramsMeta.forEach((p) => {
+ const baseline = s.parameters?.[p.name] ?? p.default_value;
+ const minEl = byId(`${idx}_${p.name}_min`);
+ const maxEl = byId(`${idx}_${p.name}_max`);
+ const stepEl = byId(`${idx}_${p.name}_step`);
+ if (minEl && maxEl && stepEl) {
+ optimization_parameters[p.name] = {
+ min: parseFloat(minEl.value),
+ max: parseFloat(maxEl.value),
+ step: parseFloat(stepEl.value),
+ };
+ } else {
+ optimization_parameters[p.name] = baseline;
+ }
+ });
+ return {
+ strategy_id: s.strategy_id,
+ baseline_parameters: { ...(s.parameters || {}) },
+ optimization_parameters,
+ promotion_status: s.promotion_status || null,
+ promotion_score: s.promotion_score ?? null,
+ selection_source: s.selection_source || null,
+ diversity_blocked_by: s.diversity_blocked_by ?? null,
+ diversity_correlation: s.diversity_correlation ?? null,
+ };
+ });
return {
- symbol,
- timeframe,
+ symbol: str("symbol"),
+ timeframe: str("timeframe"),
account_equity: num("account_equity") ?? 10000,
-
- stop,
- risk: {
- risk_fraction,
- max_position_fraction,
+ stop: {
+ type: str("stop_type"),
+ stop_fraction: ((num("stop_fraction") ?? 1) / 100),
+ atr_period: num("atr_period") ?? 14,
+ atr_multiplier: num("atr_multiplier") ?? 3,
+ },
+ risk: {
+ risk_fraction: ((num("risk_fraction") ?? 1) / 100),
+ max_position_fraction: ((num("max_position_fraction") ?? 95) / 100),
+ },
+ global_rules: {
+ max_drawdown_pct: ((num("max_drawdown_pct") ?? 20) / 100),
+ daily_loss_limit_pct: num("daily_loss_limit_pct") != null ? ((num("daily_loss_limit_pct") ?? 0) / 100) : null,
+ max_consecutive_losses: num("max_consecutive_losses"),
+ cooldown_bars: num("cooldown_bars"),
},
- global_rules,
-
strategies,
wf: {
- train_days: wf_train_days,
- test_days: wf_test_days,
- step_days: wf_step_days,
+ train_days: num("wf_train_days") ?? 120,
+ test_days: num("wf_test_days") ?? 30,
+ step_days: num("wf_step_days") ?? 30,
},
optimization: {
- optimizer_metric: str("opt_metric") ?? "sharpe_ratio",
+ optimizer_metric: str("opt_metric") || "sharpe_ratio",
max_combinations: num("opt_max_combinations") ?? 300,
min_trades_train: num("opt_min_trades_train") ?? 30,
min_trades_test: num("opt_min_trades_test") ?? 10,
},
-
commission: num("commission") ?? 0.001,
slippage: num("slippage") ?? 0.0005,
};
}
-function collectSelectedStrategies() {
-
- const strategies = [];
-
- strategySlots.forEach((slot, index) => {
-
- if (!slot.strategy_id) return;
-
- const strategyMeta = STRATEGY_CATALOG.find(
- s => s.strategy_id === slot.strategy_id
- );
-
- const parameters = {};
-
- strategyMeta.params.forEach(paramName => {
-
- const min = parseFloat(
- document.getElementById(`${paramName}_min_${index}`)?.value
- );
-
- const max = parseFloat(
- document.getElementById(`${paramName}_max_${index}`)?.value
- );
-
- const step = parseFloat(
- document.getElementById(`${paramName}_step_${index}`)?.value
- );
-
- parameters[paramName] = {
- min: min,
- max: max,
- step: step
- };
- });
-
- strategies.push({
- strategy_id: slot.strategy_id,
- parameters: parameters
- });
- });
-
- return strategies;
-}
-
-async function fetchAvailableStrategies() {
- const res = await fetch("/api/v2/calibration/strategies/catalog");
- const data = await res.json();
- return data.strategies || [];
-}
-
-function num(id) {
- const el = document.getElementById(id);
- if (!el) return null;
- const val = el.value;
- if (val === "" || val === null || val === undefined) return null;
- const n = Number(val);
- return Number.isFinite(n) ? n : null;
-}
-
-function str(id) {
- const el = document.getElementById(id);
- if (!el) return null;
- const v = el.value;
- return v === null || v === undefined ? null : String(v);
-}
-
-function setVal(id, value) {
- const el = document.getElementById(id);
- if (!el) return;
- el.value = value ?? "";
-}
-
-function loadFromStep2() {
-
- document.getElementById("risk_fraction").value =
- localStorage.getItem("calibration.risk_fraction") ?? "";
-
- document.getElementById("max_position_fraction").value =
- localStorage.getItem("calibration.max_position_fraction") ?? "";
-
- document.getElementById("stop_type").value =
- localStorage.getItem("calibration.stop_type") ?? "fixed";
-
- document.getElementById("stop_fraction").value =
- localStorage.getItem("calibration.stop_fraction") ?? "";
-
- document.getElementById("atr_period").value =
- localStorage.getItem("calibration.atr_period") ?? "";
-
- document.getElementById("atr_multiplier").value =
- localStorage.getItem("calibration.atr_multiplier") ?? "";
-
- document.getElementById("max_drawdown_pct").value =
- localStorage.getItem("calibration.max_drawdown_pct") ?? "";
-
- document.getElementById("daily_loss_limit_pct").value =
- localStorage.getItem("calibration.daily_loss_limit_pct") ?? "";
-
- document.getElementById("max_consecutive_losses").value =
- localStorage.getItem("calibration.max_consecutive_losses") ?? "";
-
- document.getElementById("cooldown_bars").value =
- localStorage.getItem("calibration.cooldown_bars") ?? "";
-
- // Forzar actualización de UI según stop heredado
- setTimeout(() => {
- updateStopUI();
- }, 0);
-
- console.log("[calibration_strategies] Parameters loaded from Step 2 ✅");
-}
-
-function updateStopUI() {
-
- const type = document.getElementById("stop_type").value;
-
- const stopFraction = document.getElementById("stop_fraction_group");
- const atrPeriod = document.getElementById("atr_group");
- const atrMultiplier = document.getElementById("atr_multiplier_group");
-
- if (type === "fixed" || type === "trailing") {
- stopFraction.classList.remove("d-none");
- atrPeriod.classList.add("d-none");
- atrMultiplier.classList.add("d-none");
- }
-
- if (type === "atr") {
- stopFraction.classList.add("d-none");
- atrPeriod.classList.remove("d-none");
- atrMultiplier.classList.remove("d-none");
- }
-}
-
-async function loadStrategyCatalog() {
-
- const res = await fetch("/api/v2/calibration/strategies/catalog");
- const data = await res.json();
-
- STRATEGY_CATALOG = data.strategies;
-}
-
-function addStrategySlot() {
-
- // Si ya hay un slot vacío al final, no crear otro
- if (strategySlots.length > 0 &&
- strategySlots[strategySlots.length - 1].strategy_id === null) {
- return;
- }
-
- if (strategySlots.length >= MAX_STRATEGIES) return;
-
- const index = strategySlots.length;
-
- strategySlots.push({
- strategy_id: null,
- parameters: {}
- });
-
- renderStrategySlot(index);
-}
-
-function renderStrategySlot(index) {
-
- const container = document.getElementById("strategies_container");
-
- const slot = document.createElement("div");
- slot.className = "card p-3";
- slot.id = `strategy_slot_${index}`;
-
- slot.innerHTML = `
-
-
-
-
-
-
-
-
- Combinations:
- 0
-
-
- `;
-
- container.appendChild(slot);
-
- document
- .getElementById(`strategy_select_${index}`)
- .addEventListener("change", (e) => {
- onStrategySelected(index, e.target.value);
- });
-}
-
-function onStrategySelected(index, strategyId) {
-
- if (!strategyId) {
- removeStrategySlot(index);
- return;
- }
-
- strategySlots[index].strategy_id = strategyId;
-
- renderParametersOnly(index, strategyId);
-
- // Si es el último slot activo, añadir nuevo vacío
- if (index === strategySlots.length - 1 &&
- strategySlots.length < MAX_STRATEGIES) {
-
- strategySlots.push({ strategy_id: null, parameters: {} });
- renderStrategySlot(strategySlots.length - 1);
- }
-
- updateCombinationCounter();
-}
-
-function validateParameterInputs() {
-
- let valid = true;
-
- document.querySelectorAll(".param-input").forEach(input => {
- input.classList.remove("is-invalid");
- });
-
- strategySlots.forEach((slot, index) => {
-
- if (!slot.strategy_id) return;
-
- const strategyMeta = STRATEGY_CATALOG.find(
- s => s.strategy_id === slot.strategy_id
- );
-
- strategyMeta.params.forEach(paramName => {
-
- const minEl = document.getElementById(`${paramName}_min_${index}`);
- const maxEl = document.getElementById(`${paramName}_max_${index}`);
- const stepEl = document.getElementById(`${paramName}_step_${index}`);
-
- const min = parseFloat(minEl?.value);
- const max = parseFloat(maxEl?.value);
- const step = parseFloat(stepEl?.value);
-
- if (max < min) {
- maxEl.classList.add("is-invalid");
- valid = false;
- }
-
- if (step <= 0) {
- stepEl.classList.add("is-invalid");
- valid = false;
- }
-
- });
- });
-
- updateCombinationCounter();
-
- return valid;
-}
-
-function updateCombinationCounter() {
-
- let globalTotal = 1;
- let hasAnyStrategy = false;
-
- strategySlots.forEach((slot, index) => {
-
- if (!slot.strategy_id) return;
-
- hasAnyStrategy = true;
-
- const strategyMeta = STRATEGY_CATALOG.find(
- s => s.strategy_id === slot.strategy_id
- );
-
- let strategyTotal = 1;
-
- strategyMeta.params.forEach(paramName => {
-
- const min = parseFloat(
- document.getElementById(`${paramName}_min_${index}`)?.value
- );
-
- const max = parseFloat(
- document.getElementById(`${paramName}_max_${index}`)?.value
- );
-
- const step = parseFloat(
- document.getElementById(`${paramName}_step_${index}`)?.value
- );
-
- if (isNaN(min) || isNaN(max) || isNaN(step)) return;
-
- if (min === max || step == 0) {
- strategyTotal *= 1;
- } else {
- const count = Math.floor((max - min) / step) + 1;
- strategyTotal *= Math.max(count, 1);
- }
-
- });
-
- const perStrategyEl = document.getElementById(`strategy_combo_${index}`);
- if (perStrategyEl) {
- perStrategyEl.textContent = strategyTotal;
- }
-
- globalTotal *= strategyTotal;
- });
-
- if (!hasAnyStrategy) globalTotal = 0;
-
- const globalEl = document.getElementById("combination_counter");
- if (globalEl) globalEl.textContent = globalTotal;
-
- applyCombinationWarnings(globalTotal);
- updateTimeEstimate(globalTotal);
-
- return globalTotal;
-}
-
-function applyCombinationWarnings(total) {
-
- const maxComb = parseInt(
- document.getElementById("opt_max_combinations")?.value || 0
- );
-
- const counter = document.getElementById("combination_counter");
- if (!counter) return;
-
- counter.classList.remove("text-warning", "text-danger");
-
- if (total > 10000) {
- counter.classList.add("text-danger");
- } else if (maxComb && total > maxComb) {
- counter.classList.add("text-warning");
- }
-}
-
-function updateTimeEstimate(totalComb) {
-
- const trainDays = parseInt(
- document.getElementById("wf_train_days")?.value || 0
- );
-
- const testDays = parseInt(
- document.getElementById("wf_test_days")?.value || 0
- );
-
- const approxWindows = Math.max(
- Math.floor(365 / testDays),
- 1
- );
-
- const operations = totalComb * approxWindows;
-
- // 0.003s por combinación (estimación conservadora)
- const seconds = operations * 0.003;
-
- let label;
-
- if (seconds < 60) {
- label = `~ ${seconds.toFixed(1)} sec`;
- } else if (seconds < 3600) {
- label = `~ ${(seconds / 60).toFixed(1)} min`;
- } else {
- label = `~ ${(seconds / 3600).toFixed(1)} h`;
- }
-
- const el = document.getElementById("wf_time_estimate");
- if (el) el.textContent = label;
-}
-
-function removeStrategySlot(index) {
-
- strategySlots.splice(index, 1);
-
- rerenderStrategySlots();
-}
-
-function rerenderStrategySlots() {
-
- const container = document.getElementById("strategies_container");
- container.innerHTML = "";
-
- const currentStrategies = strategySlots
- .filter(s => s.strategy_id !== null);
-
- strategySlots = [];
-
- currentStrategies.forEach((slotData, index) => {
-
- strategySlots.push({
- strategy_id: slotData.strategy_id,
- parameters: {}
- });
-
- renderStrategySlot(index);
-
- const select = document.getElementById(`strategy_select_${index}`);
- select.value = slotData.strategy_id;
-
- renderParametersOnly(index, slotData.strategy_id);
- });
-
- // Siempre añadir un slot vacío al final
- if (strategySlots.length < MAX_STRATEGIES) {
- strategySlots.push({ strategy_id: null, parameters: {} });
- renderStrategySlot(strategySlots.length - 1);
- }
-
- updateCombinationCounter();
-}
-
-function renderParametersOnly(index, strategyId) {
-
- const paramsContainer = document.getElementById(`strategy_params_${index}`);
- paramsContainer.innerHTML = "";
-
- if (!strategyId) return;
-
- const strategyMeta = STRATEGY_CATALOG.find(
- s => s.strategy_id === strategyId
- );
-
- if (!strategyMeta) return;
-
- strategyMeta.params.forEach(paramName => {
-
- const col = document.createElement("div");
- col.className = "col-md-4";
-
- col.innerHTML = `
-
-
- `;
-
- paramsContainer.appendChild(col);
- });
-}
-
-// =================================================
-// PROGRESS BAR
-// =================================================
-
-function startWF() {
- document
- .getElementById("wf_progress_card")
- .classList.remove("d-none");
-
- document.getElementById("wfProgressBar").style.width = "0%";
- document.getElementById("wfProgressBar").innerText = "0%";
-}
-
-async function pollStatus(jobId) {
- const interval = setInterval(async () => {
- const res = await fetch(`/api/v2/calibration/strategies/status/${jobId}`);
+async function pollJob(jobId) {
+ byId("progressCard").classList.remove("d-none");
+ const timer = setInterval(async () => {
+ const res = await fetch(`/api/v2/calibration/optimization/status/${jobId}`);
const data = await res.json();
-
- const bar = document.getElementById("wfProgressBar");
- bar.style.width = data.progress + "%";
- bar.innerText = data.progress + "%";
+ const pct = data.progress || 0;
+ byId("progressBar").style.width = `${pct}%`;
+ byId("progressBar").textContent = `${pct}%`;
+ byId("progressText").textContent = `${data.current_strategy || "—"} · ${data.current_phase || "running"}`;
if (data.status === "done") {
- clearInterval(interval);
- bar.classList.remove("progress-bar-animated");
- console.log("WF finished");
+ clearInterval(timer);
+ byId("progressBar").classList.remove("progress-bar-animated");
+ STEP4_RESULT = data.result;
+ renderResults(STEP4_RESULT);
+ }
+ if (data.status === "fail") {
+ clearInterval(timer);
+ byId("progressBar").classList.remove("progress-bar-animated");
+ byId("resultsMessage").textContent = data.result?.detail || "Optimization failed.";
+ setStatusBadge("fail");
}
}, 1000);
}
-// =================================================
-// RENDER RESULTS
-// =================================================
-
-function renderStrategiesList(strategies) {
- const list = document.getElementById("strategies_list");
- if (!list) return;
-
- list.innerHTML = "";
-
- strategies.forEach((s) => {
- const col = document.createElement("div");
- col.className = "col-12 col-lg-6";
- col.setAttribute("data-strategy-item", "1");
-
- const defaultGrid = s.default_grid || {};
- const defaultGridText = JSON.stringify(defaultGrid, null, 2);
-
- col.innerHTML = `
-
-
-
-
-
-
-
-
Tip: usa listas. Ej: {"fast":[10,20],"slow":[50,100]}
-
-
-
- `;
-
- list.appendChild(col);
+async function runOptimization() {
+ const payload = buildPayload();
+ const res = await fetch("/api/v2/calibration/optimization/run", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
});
-}
-
-function setBadge(status) {
- const badge = document.getElementById("strategies_status_badge");
- if (!badge) return;
-
- badge.classList.remove("bg-secondary", "bg-success", "bg-warning", "bg-danger");
- badge.classList.add(
- status === "ok" ? "bg-success" : status === "warning" ? "bg-warning" : status === "fail" ? "bg-danger" : "bg-secondary"
- );
- badge.textContent = status ? status.toUpperCase() : "—";
+ if (!res.ok) throw new Error(await res.text());
+ const data = await res.json();
+ await pollJob(data.job_id);
}
function renderResultsTable(data) {
- const wrap = document.getElementById("strategies_table_wrap");
- if (!wrap) return;
-
- const rows = [];
- (data.results || []).forEach((r) => {
- rows.push(`
-
- | ${r.strategy_id} |
- ${r.status} |
- ${r.n_windows} |
- ${Number(r.oos_total_return_pct).toFixed(2)}% |
- ${Number(r.oos_max_dd_worst_pct).toFixed(2)}% |
- ${Number(r.oos_final_equity).toFixed(2)} |
- ${r.message || ""} |
-
- `);
- });
-
- wrap.innerHTML = `
+ const rows = (data.results || []).map((r) => `
+
+ | ${r.strategy_id} |
+ ${r.decision.accepted ? 'accepted' : 'rejected'} |
+ ${Number(r.baseline.oos_total_return_pct).toFixed(2)}% |
+ ${Number(r.optimized.oos_total_return_pct).toFixed(2)}% |
+ ${Number(r.decision.return_delta_pct).toFixed(2)}% |
+ ${Number(r.baseline.oos_avg_sharpe ?? 0).toFixed(2)} |
+ ${Number(r.optimized.oos_avg_sharpe ?? 0).toFixed(2)} |
+ ${Number(r.baseline.oos_max_dd_worst_pct).toFixed(2)}% |
+ ${Number(r.optimized.oos_max_dd_worst_pct).toFixed(2)}% |
+ ${(r.decision.reasons || []).join(", ")} |
+
`).join("");
+ byId("resultsTableWrap").innerHTML = `
-
-
- | Strategy |
- Status |
- Windows |
- OOS return |
- Worst DD |
- Final equity |
- Message |
-
-
-
- ${rows.join("")}
-
+ | Strategy | Decision | Baseline ret | Opt ret | Δ ret | Base sharpe | Opt sharpe | Base DD | Opt DD | Reasons |
+ ${rows}
-
- `;
+ `;
}
function populatePlotSelector(data) {
- const sel = document.getElementById("plot_strategy_select");
- if (!sel) return;
-
+ const sel = byId("plotStrategySelect");
sel.innerHTML = "";
- const ids = Object.keys((data.series && data.series.strategies) ? data.series.strategies : {});
+ const ids = Object.keys(data.series?.strategies || {});
ids.forEach((sid) => {
const opt = document.createElement("option");
opt.value = sid;
opt.textContent = sid;
sel.appendChild(opt);
});
-
- sel.onchange = () => renderPlotsForSelected(data);
-
- if (ids.length > 0) {
+ sel.onchange = () => renderPlots(data);
+ if (ids.length) {
sel.value = ids[0];
+ renderPlots(data);
}
}
-function renderPlotsForSelected(data) {
- const sel = document.getElementById("plot_strategy_select");
- const sid = sel ? sel.value : null;
- if (!sid) return;
-
+function renderPlots(data) {
+ const sid = byId("plotStrategySelect").value;
const s = data.series?.strategies?.[sid];
if (!s) return;
- const equity = s.window_equity || [];
- const returns = s.window_returns_pct || [];
- const xEq = [...Array(equity.length).keys()];
- const xRet = [...Array(returns.length).keys()].map((i) => i + 1);
-
+ const beq = s.baseline?.window_equity || [];
+ const oeq = s.optimized?.window_equity || [];
Plotly.newPlot("plot_equity", [
- { x: xEq, y: equity, type: "scatter", mode: "lines", name: "Equity (OOS)" },
- ], {
- title: `WF OOS equity · ${sid}`,
- margin: { t: 40, l: 50, r: 20, b: 40 },
- xaxis: { title: "Window index" },
- yaxis: { title: "Equity" },
- }, { displayModeBar: false });
+ { x: [...Array(beq.length).keys()], y: beq, type: "scatter", mode: "lines", name: "Baseline" },
+ { x: [...Array(oeq.length).keys()], y: oeq, type: "scatter", mode: "lines", name: "Optimized" },
+ ], { title: `Equity · ${sid}`, margin: { t: 40, l: 50, r: 20, b: 40 } });
+ const br = s.baseline?.window_returns_pct || [];
+ const orr = s.optimized?.window_returns_pct || [];
Plotly.newPlot("plot_returns", [
- { x: xRet, y: returns, type: "bar", name: "Return % (per window)" },
- ], {
- title: `WF returns per window · ${sid}`,
- margin: { t: 40, l: 50, r: 20, b: 40 },
- xaxis: { title: "Window" },
- yaxis: { title: "Return (%)" },
- }, { displayModeBar: false });
+ { x: br.map((_, i) => i + 1), y: br, type: "bar", name: "Baseline" },
+ { x: orr.map((_, i) => i + 1), y: orr, type: "bar", name: "Optimized" },
+ ], { title: `Window returns · ${sid}`, barmode: "group", margin: { t: 40, l: 50, r: 20, b: 40 } });
}
-function renderValidateResponse(data) {
-
- // -------------------------------
- // 1️⃣ Badge + message
- // -------------------------------
- const badge = document.getElementById("strategies_status_badge");
- const msg = document.getElementById("strategies_message");
-
- badge.textContent = data.status ?? "—";
-
- badge.className = "badge";
- if (data.status === "ok") badge.classList.add("bg-success");
- else if (data.status === "warning") badge.classList.add("bg-warning");
- else badge.classList.add("bg-danger");
-
- msg.textContent = data.message ?? "";
-
- // -------------------------------
- // 2️⃣ Debug JSON
- // -------------------------------
- document.getElementById("strategies_debug").textContent =
- JSON.stringify(data, null, 2);
-
- // -------------------------------
- // 3️⃣ Plots (primera estrategia por ahora)
- // -------------------------------
- if (data.series && data.series.strategies) {
- const keys = Object.keys(data.series.strategies);
- if (keys.length > 0) {
- const s = data.series.strategies[keys[0]];
-
- Plotly.newPlot("plot_equity", [{
- y: s.window_equity,
- type: "scatter",
- mode: "lines",
- name: "Equity"
- }], { margin: { t: 20 } });
-
- Plotly.newPlot("plot_returns", [{
- y: s.window_returns_pct,
- type: "bar",
- name: "Window returns %"
- }], { margin: { t: 20 } });
- }
- }
-
- // -------------------------------
- // 4️⃣ Table
- // -------------------------------
- const wrap = document.getElementById("strategies_table_wrap");
- wrap.innerHTML = "";
-
- if (data.results) {
- let html = `
-
-
- | Strategy |
- Status |
- OOS Return % |
- OOS Max DD % |
- Windows |
-
-
- `;
-
- for (const r of data.results) {
- html += `
-
- | ${r.strategy_id} |
- ${r.status} |
- ${r.oos_total_return_pct?.toFixed(2)} |
- ${r.oos_max_dd_worst_pct?.toFixed(2)} |
- ${r.n_windows} |
-
- `;
- }
-
- html += "
";
- wrap.innerHTML = html;
- }
-}
-
-async function validateStrategies() {
- console.log("[calibration_strategies] validateStrategies() START");
-
- const bar = document.getElementById("wfProgressBar");
- const txt = document.getElementById("wf_progress_text");
-
- const setProgress = (pct, text) => {
- const p = Math.max(0, Math.min(100, Number(pct || 0)));
- bar.style.width = `${p}%`;
- bar.textContent = `${p}%`;
- if (text) txt.textContent = text;
- };
-
- if (!validateParameterInputs()) {
- alert("Please fix parameter errors before running WF.");
- return;
- }
-
-
- try {
- // 0) Reset UI
- setProgress(0, "Starting...");
-
- // 1) Construye payload igual que antes (usa tu función existente)
- const payload = buildPayload(); // <-- NO CAMBIES tu builder, reutilízalo
-
- // 2) Arranca job async
- const runResp = await fetch("/api/v2/calibration/strategies/run", {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(payload),
- });
-
- if (!runResp.ok) {
- const errText = await runResp.text();
- throw new Error(`Run failed: ${runResp.status} ${errText}`);
- }
-
- const { job_id } = await runResp.json();
- if (!job_id) throw new Error("No job_id returned from /run");
-
- // 3) Poll status
- const pollEveryMs = 500;
- const maxMinutes = 30;
- const maxPolls = Math.ceil((maxMinutes * 60 * 1000) / pollEveryMs);
-
- for (let i = 0; i < maxPolls; i++) {
- await new Promise((r) => setTimeout(r, pollEveryMs));
-
- const stResp = await fetch(`/api/v2/calibration/strategies/status/${job_id}`);
- if (!stResp.ok) continue;
-
- const st = await stResp.json();
-
- const pct = st.progress ?? 0;
- const cw = st.current_window ?? 0;
- const tw = st.total_windows ?? 0;
-
- const label =
- tw > 0
- ? `WF running... window ${cw}/${tw}`
- : "WF running...";
-
- setProgress(pct, label);
-
- if (st.status === "done") {
- setProgress(100, "WF completed ✅");
-
- // 4) Renderiza resultados usando el MISMO renderer que usabas con /validate
- // (ojo: el resultado viene dentro de st.result)
- if (!st.result) throw new Error("Job done but no result in status payload");
-
- renderValidateResponse(st.result); // <-- usa tu función existente de render (plots, tablas, etc.)
-
- console.log("[calibration_strategies] validateStrategies() DONE ok");
- return;
- }
-
- if (st.status === "unknown") {
- setProgress(0, "Unknown job (server lost state?)");
- break;
- }
- }
-
- throw new Error("Timeout waiting for WF job to finish");
-
- } catch (err) {
- console.error(err);
- // deja un estado visible
- const txt = document.getElementById("wf_progress_text");
- if (txt) txt.textContent = `Error: ${err.message}`;
- console.log("[calibration_strategies] validateStrategies() DONE fail");
- }
+function renderResults(data) {
+ setStatusBadge(data.status);
+ byId("resultsMessage").textContent = data.message || "Optimization complete.";
+ byId("debugJson").textContent = JSON.stringify(data, null, 2);
+ renderResultsTable(data);
+ populatePlotSelector(data);
}
async function generateReport() {
- console.log("[calibration_strategies] generateReport() START");
-
- let payload;
- try {
- payload = buildPayload();
- } catch (e) {
- alert(e.message);
- return;
- }
-
- const res = await fetch("/api/v2/calibration/strategies/report", {
+ const payload = buildPayload();
+ const res = await fetch("/api/v2/calibration/optimization/report", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
-
+ if (!res.ok) throw new Error(await res.text());
const data = await res.json();
-
- if (data.url) {
- const viewer = document.getElementById("pdf_viewer_section");
- const frame = document.getElementById("pdf_frame");
- frame.src = data.url;
- viewer.classList.remove("d-none");
- viewer.scrollIntoView({ behavior: "smooth" });
- } else {
- alert("Failed to generate report");
- }
-}
-
-function wireButtons() {
- document.getElementById("validate_strategies_btn")?.addEventListener("click", validateStrategies);
- document.getElementById("report_strategies_btn")?.addEventListener("click", generateReport);
-
- document.getElementById("refresh_strategies_btn")?.addEventListener("click", async () => {
- const strategies = await fetchAvailableStrategies();
- renderStrategiesList(strategies);
- });
-
- document.getElementById("load_step2_btn")?.addEventListener("click", () => {
- loadContextFromLocalStorage();
- });
-
- document.getElementById("close_pdf_btn")?.addEventListener("click", () => {
- const viewer = document.getElementById("pdf_viewer_section");
- const frame = document.getElementById("pdf_frame");
- frame.src = "";
- viewer.classList.add("d-none");
- });
-}
-
-function applyInheritedLock() {
- const locked = document.getElementById("lock_inherited").checked;
- const fields = document.querySelectorAll(".inherited-field");
-
- fields.forEach(f => {
- f.disabled = locked;
- if (locked) {
- f.classList.add("bg-light");
- } else {
- f.classList.remove("bg-light");
- }
- });
+ byId("pdfViewerSection").classList.remove("d-none");
+ byId("pdfFrame").src = data.url;
}
async function init() {
- await loadStrategyCatalog();
- addStrategySlot();
-
- loadContextFromLocalStorage();
- loadFromStep2();
- applyInheritedLock();
-
- document.getElementById("stop_type")
- .addEventListener("change", updateStopUI);
-
- wireButtons();
-
- const strategies = await fetchAvailableStrategies();
- renderStrategiesList(strategies);
-
- // Pre-select 1 strategy by default (moving_average) if exists
- setTimeout(() => {
- const first = document.querySelector('input[type=checkbox][data-strategy-id="moving_average"]');
- if (first) first.checked = true;
- }, 0);
+ try {
+ loadInheritedContext();
+ await loadCatalog();
+ renderStrategyCards();
+ byId("runOptimizationBtn").addEventListener("click", async () => {
+ try { await runOptimization(); } catch (err) { setStatusBadge("fail"); byId("resultsMessage").textContent = err.message; }
+ });
+ byId("generateReportBtn").addEventListener("click", async () => {
+ try { await generateReport(); } catch (err) { byId("resultsMessage").textContent = err.message; }
+ });
+ byId("closePdfBtn").addEventListener("click", () => byId("pdfViewerSection").classList.add("d-none"));
+ } catch (err) {
+ setStatusBadge("fail");
+ byId("resultsMessage").textContent = err.message;
+ byId("artifactSummary").textContent = err.message;
+ }
}
-document.getElementById("lock_inherited")
- .addEventListener("change", applyInheritedLock);
-
-
-init();
+init();
\ No newline at end of file
diff --git a/src/web/ui/v2/static/js/pages/calibration_strategies.js b/src/web/ui/v2/static/js/pages/calibration_strategies.js
index 533e8f7..375b514 100644
--- a/src/web/ui/v2/static/js/pages/calibration_strategies.js
+++ b/src/web/ui/v2/static/js/pages/calibration_strategies.js
@@ -22,6 +22,8 @@ function enableNextStep() {
btn.classList.remove("btn-outline-secondary");
btn.classList.add("btn-outline-primary");
btn.setAttribute("aria-disabled", "false");
+ btn.setAttribute("href", "/calibration/optimization");
+ btn.setAttribute("title", "Go to Step 4 · Optimization");
}
@@ -31,6 +33,8 @@ function disableNextStep() {
btn.classList.remove("btn-outline-primary");
btn.classList.add("btn-outline-secondary");
btn.setAttribute("aria-disabled", "true");
+ btn.setAttribute("href", "#");
+ btn.setAttribute("title", "Build a Step 4 Selection Artifact first");
}
@@ -1959,6 +1963,7 @@ async function init() {
});
wirePromotionUI();
+ restoreStep4SelectionArtifact();
document.getElementById("plot_strategy_select").addEventListener("change", function() {
if (!lastValidationResult || !selectedStrategyId) return;
@@ -2283,9 +2288,40 @@ function buildStep4SelectionArtifact() {
previewEl.textContent = JSON.stringify(artifact, null, 2);
previewEl.classList.remove("d-none");
+ localStorage.setItem("calibration.step4.selection", JSON.stringify(artifact));
+
+ if ((artifact.selected_strategies || []).length > 0) enableNextStep();
+ else disableNextStep();
+
updateStep4SelectionSummary();
}
+function restoreStep4SelectionArtifact() {
+ const raw = localStorage.getItem("calibration.step4.selection");
+ if (!raw) {
+ disableNextStep();
+ return;
+ }
+
+ try {
+ const artifact = JSON.parse(raw);
+ const previewEl = document.getElementById("step4SelectionPreview");
+ if (previewEl && artifact) {
+ previewEl.textContent = JSON.stringify(artifact, null, 2);
+ previewEl.classList.remove("d-none");
+ }
+
+ STEP4_SELECTION = Array.isArray(artifact?.selected_strategies) ? artifact.selected_strategies : [];
+ if (STEP4_SELECTION.length > 0) enableNextStep();
+ else disableNextStep();
+
+ updateStep4SelectionSummary();
+ } catch (err) {
+ console.error("restoreStep4SelectionArtifact failed", err);
+ disableNextStep();
+ }
+}
+
async function runPromotion() {
diff --git a/src/web/ui/v2/templates/pages/calibration/calibration_optimization.html b/src/web/ui/v2/templates/pages/calibration/calibration_optimization.html
index 71c912c..07b0069 100644
--- a/src/web/ui/v2/templates/pages/calibration/calibration_optimization.html
+++ b/src/web/ui/v2/templates/pages/calibration/calibration_optimization.html
@@ -2,20 +2,10 @@
{% block content %}
-
-
-
-
-
-
-
Calibración · Paso 3 · Strategies
-
Optimización + Walk Forward (OOS)
+
Calibración · Paso 4 · Optimization
+
Baseline vs optimized walk-forward validation
-
-
-
-
-
-
+
+
+
-
- Tip: Symbol y timeframe se cargan desde Step 1 (localStorage). Si no aparecen, rellénalos manualmente.
+
+
+
-
-
-
-
-
-
-
-
-
-
Risk Configuration
-
-
-
-
-
-
-
Stop Configuration
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Global Rules
-
-
-
-
-
-
-
-
-
-
-
-
Optional Parameters
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Total combinations
- 0
-
-
-
-
- Estimated WF time:
- ~ 0 sec
-
-
-
- Cada estrategia incluye un param_grid en JSON.
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
+
+
-
-
-
-
- Waiting to start...
-
-
+
+
Waiting to start…
-
-
-
-