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 = ` +
+
+ ${s.strategy_id} + ${s.promotion_status || "n/a"} + ${s.selection_source || "manual"} +
+
Score: ${s.promotion_score ?? "—"}
+
+
+
+ + + + + ${rows} +
ParamBaselineMinMaxStep
+
+
`; + 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 = ` - -
-
- Min - -
-
- Max - -
-
- Step - -
-
- `; - - 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 = `
- - - - - - - - - - - - - ${rows.join("")} - + + ${rows}
StrategyStatusWindowsOOS returnWorst DDFinal equityMessage
StrategyDecisionBaseline retOpt retΔ retBase sharpeOpt sharpeBase DDOpt DDReasons
-
- `; + `; } 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 = ` - - - - - - - - - - `; - - for (const r of data.results) { - html += ` - - - - - - - - `; - } - - html += "
StrategyStatusOOS Return %OOS Max DD %Windows
${r.strategy_id}${r.status}${r.oos_total_return_pct?.toFixed(2)}${r.oos_max_dd_worst_pct?.toFixed(2)}${r.n_windows}
"; - 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
-
-
- - -
-
-

Context

+

Step 4 input artifact

+
+
Loading artifact…
+

     
+
+ +
+

Inherited context

-
- - -
-
- - -
-
- - -
+
+
+
+
+
-
- Tip: Symbol y timeframe se cargan desde Step 1 (localStorage). Si no aparecen, rellénalos manualmente. +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
- - -
-

Risk & Stops(Step 2)

- -
- - -
-
- -
- - - - -

Risk Configuration

- -
-
- - -
- -
- - -
-
- - - - -

Stop Configuration

- -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - - - - -

Global Rules

- -
-
- - -
-
- - - - -

Optional Parameters

- -
-
- - -
- -
- - -
- -
- - -
-
- -
-
- - - - -
-
-

Walk-Forward & Optimization

+

Optimization ranges

+
-
-
- - -
-
- - -
-
- - -
-
- - -
- -
- - -
-
- - -
-
- - -
- -
- - -
-
- - -
-
+
- - - -
-
-

Strategies

-
- -
-
-
-
-
-
- Total combinations - 0 -
-
-
- - Estimated WF time: - ~ 0 sec - -
-
- Cada estrategia incluye un param_grid en JSON. -
-
-
- - - -
- - + +
- - - -
-
-

Walk-Forward Progress

-
+
+

Optimization progress

- -
-
- 0% -
-
- -
- Waiting to start... -
- +
0%
+
Waiting to start…
- - -
-
-

Results

-
- -
+
+

Results

+
-
Run validation to see results.
- -
-
- - -
+
Run Step 4 to compare baseline vs optimized.
+
+
- -
-
-
-
-
-
- +
+

- -
- -
- Debug JSON -

-      
+
+
Debug JSON
- - - -
-
-

Strategies Report (PDF)

-
- -
-
-
- +
+
+

Optimization report

+
+
-
diff --git a/src/web/ui/v2/templates/pages/calibration/calibration_strategies.html b/src/web/ui/v2/templates/pages/calibration/calibration_strategies.html index afce7e9..a3ba8dd 100644 --- a/src/web/ui/v2/templates/pages/calibration/calibration_strategies.html +++ b/src/web/ui/v2/templates/pages/calibration/calibration_strategies.html @@ -312,7 +312,7 @@
-