Continue with Step 4

This commit is contained in:
dam
2026-04-05 21:42:12 +02:00
parent 81416630a7
commit 4c54391574
7 changed files with 821 additions and 1619 deletions

View File

@@ -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

View File

@@ -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"})

View File

@@ -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]

File diff suppressed because it is too large Load Diff

View File

@@ -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() {

View File

@@ -2,20 +2,10 @@
{% block content %}
<div class="container-xl">
<!-- ========================= -->
<!-- Wizard header -->
<!-- ========================= -->
<div class="d-flex align-items-center mb-4">
<!-- Back arrow -->
<div class="me-3">
<a href="/calibration/risk" class="btn btn-outline-secondary btn-icon">
<svg xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-left"
width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor"
fill="none" stroke-linecap="round" stroke-linejoin="round">
<a href="/calibration/strategies" class="btn btn-outline-secondary btn-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-left" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M15 6l-6 6l6 6"/>
</svg>
@@ -23,342 +13,112 @@
</div>
<div class="flex-grow-1 text-center">
<h2 class="mb-0">Calibración · Paso 3 · Strategies</h2>
<div class="text-secondary">Optimización + Walk Forward (OOS)</div>
<h2 class="mb-0">Calibración · Paso 4 · Optimization</h2>
<div class="text-secondary">Baseline vs optimized walk-forward validation</div>
</div>
<!-- Forward arrow (disabled until OK) -->
<div class="ms-3">
<a
id="next-step-btn"
href="#"
class="btn btn-outline-secondary btn-icon"
aria-disabled="true"
title="Next step not implemented yet"
>
<svg xmlns="http://www.w3.org/2000/svg"
class="icon icon-tabler icon-tabler-arrow-right"
width="24" height="24" viewBox="0 0 24 24"
stroke-width="2" stroke="currentColor"
fill="none" stroke-linecap="round" stroke-linejoin="round">
<a id="next-step-btn" href="#" class="btn btn-outline-secondary btn-icon" aria-disabled="true" title="Step 5 not implemented yet">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-arrow-right" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M9 6l6 6l-6 6"/>
</svg>
</a>
</div>
</div>
<!-- ========================= -->
<!-- Context -->
<!-- ========================= -->
<div class="card mb-4">
<div class="card-header">
<h3 class="card-title">Context</h3>
<div class="card-header"><h3 class="card-title">Step 4 input artifact</h3></div>
<div class="card-body">
<div id="artifactSummary" class="text-secondary">Loading artifact…</div>
<pre id="artifactPreview" class="mt-3 p-3 bg-light border rounded" style="max-height: 260px; overflow:auto;"></pre>
</div>
</div>
<div class="card mb-4">
<div class="card-header"><h3 class="card-title">Inherited context</h3></div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Symbol</label>
<input id="symbol" class="form-control" placeholder="BTC/USDT">
</div>
<div class="col-md-4">
<label class="form-label">Timeframe</label>
<input id="timeframe" class="form-control" placeholder="1h">
</div>
<div class="col-md-4">
<label class="form-label">Account equity</label>
<input id="account_equity" class="form-control" type="number" step="0.01" value="10000">
</div>
<div class="col-md-3"><label class="form-label">Symbol</label><input id="symbol" class="form-control" readonly></div>
<div class="col-md-2"><label class="form-label">Timeframe</label><input id="timeframe" class="form-control" readonly></div>
<div class="col-md-2"><label class="form-label">Account equity</label><input id="account_equity" type="number" step="0.01" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Commission</label><input id="commission" type="number" step="0.0001" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Slippage</label><input id="slippage" type="number" step="0.0001" class="form-control"></div>
</div>
<div class="mt-3 text-secondary">
Tip: Symbol y timeframe se cargan desde Step 1 (localStorage). Si no aparecen, rellénalos manualmente.
<div class="row g-3 mt-1">
<div class="col-md-2"><label class="form-label">Train days</label><input id="wf_train_days" type="number" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Test days</label><input id="wf_test_days" type="number" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Step days</label><input id="wf_step_days" type="number" class="form-control"></div>
<div class="col-md-3"><label class="form-label">Optimizer metric</label><select id="opt_metric" class="form-select"><option value="sharpe_ratio">sharpe_ratio</option><option value="total_return_pct">total_return_pct</option><option value="max_drawdown_pct">max_drawdown_pct</option></select></div>
<div class="col-md-2"><label class="form-label">Max combinations</label><input id="opt_max_combinations" type="number" class="form-control"></div>
</div>
<div class="row g-3 mt-1">
<div class="col-md-2"><label class="form-label">Min trades train</label><input id="opt_min_trades_train" type="number" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Min trades test</label><input id="opt_min_trades_test" type="number" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Risk %</label><input id="risk_fraction" type="number" step="0.01" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Max position %</label><input id="max_position_fraction" type="number" step="0.1" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Stop type</label><select id="stop_type" class="form-select"><option value="fixed">fixed</option><option value="trailing">trailing</option><option value="atr">atr</option></select></div>
<div class="col-md-2"><label class="form-label">Stop fraction %</label><input id="stop_fraction" type="number" step="0.01" class="form-control"></div>
</div>
<div class="row g-3 mt-1">
<div class="col-md-2"><label class="form-label">ATR period</label><input id="atr_period" type="number" class="form-control"></div>
<div class="col-md-2"><label class="form-label">ATR multiplier</label><input id="atr_multiplier" type="number" step="0.1" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Max DD %</label><input id="max_drawdown_pct" type="number" step="0.1" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Daily loss %</label><input id="daily_loss_limit_pct" type="number" step="0.1" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Max consec. losses</label><input id="max_consecutive_losses" type="number" class="form-control"></div>
<div class="col-md-2"><label class="form-label">Cooldown bars</label><input id="cooldown_bars" type="number" class="form-control"></div>
</div>
</div>
</div>
<!-- ========================= -->
<!-- Risk & Stops -->
<!-- ========================= -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title mb-0">Risk & Stops(Step 2)</h3>
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" id="lock_inherited" checked>
<label class="form-check-label" for="lock_inherited">
Bloquear parámetros heredados
</label>
</div>
</div>
<div class="card-body">
<!-- ================= -->
<!-- Risk Configuration -->
<!-- ================= -->
<h4 class="mb-3">Risk Configuration</h4>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label">Risk per Trade (%)</label>
<input id="risk_fraction" class="form-control inherited-field" type="number" step="0.01">
</div>
<div class="col-md-4">
<label class="form-label">Max Position Size (%)</label>
<input id="max_position_fraction" class="form-control inherited-field" type="number" step="0.1">
</div>
</div>
<!-- ================= -->
<!-- Stop Configuration -->
<!-- ================= -->
<h4 class="mb-3">Stop Configuration</h4>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label">Stop Type</label>
<select id="stop_type" class="form-select inherited-field">
<option value="fixed">fixed</option>
<option value="trailing">trailing</option>
<option value="atr">atr</option>
</select>
</div>
<div id="stop_fraction_group" class="col-md-4">
<label class="form-label">Stop fraction (%)</label>
<input id="stop_fraction" class="form-control inherited-field" type="number" step="0.01">
</div>
<div id="atr_group" class="col-md-4 d-none">
<label class="form-label">ATR period</label>
<input id="atr_period" class="form-control inherited-field" type="number">
</div>
<div id="atr_multiplier_group" class="col-md-4 d-none">
<label class="form-label">ATR multiplier</label>
<input id="atr_multiplier" class="form-control inherited-field" type="number" step="0.1">
</div>
</div>
<!-- ================= -->
<!-- Global Rules -->
<!-- ================= -->
<h4 class="mb-3">Global Rules</h4>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label">Max Drawdown (%)</label>
<input id="max_drawdown_pct" class="form-control inherited-field" type="number" step="0.1">
</div>
</div>
<!-- ================= -->
<!-- Optional Parameters -->
<!-- ================= -->
<h4 class="mb-3">Optional Parameters</h4>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Daily loss limit (%)</label>
<input id="daily_loss_limit_pct" class="form-control optional-field" type="number" step="0.1">
</div>
<div class="col-md-4">
<label class="form-label">Max consecutive losses</label>
<input id="max_consecutive_losses" class="form-control optional-field" type="number">
</div>
<div class="col-md-4">
<label class="form-label">Cooldown bars</label>
<input id="cooldown_bars" class="form-control optional-field" type="number">
</div>
</div>
</div>
</div>
<!-- ========================= -->
<!-- WF + Optimizer config -->
<!-- ========================= -->
<div class="card mb-4">
<div class="card-header">
<h3 class="card-title">Walk-Forward & Optimization</h3>
<h3 class="card-title mb-0">Optimization ranges</h3>
<div id="strategyCount" class="text-secondary small"></div>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<label class="form-label">Train days</label>
<input id="wf_train_days" class="form-control" type="number" step="1" value="120">
</div>
<div class="col-md-3">
<label class="form-label">Test days</label>
<input id="wf_test_days" class="form-control" type="number" step="1" value="30">
</div>
<div class="col-md-3">
<label class="form-label">Step days (optional)</label>
<input id="wf_step_days" class="form-control" type="number" step="1" value="">
</div>
<div class="col-md-3">
<label class="form-label">Metric</label>
<select id="opt_metric" class="form-select">
<option value="sharpe_ratio">sharpe_ratio</option>
<option value="total_return">total_return</option>
<option value="max_drawdown">max_drawdown</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Max combinations</label>
<input id="opt_max_combinations" class="form-control" type="number" step="1" value="300">
</div>
<div class="col-md-3">
<label class="form-label">Min trades (train)</label>
<input id="opt_min_trades_train" class="form-control" type="number" step="1" value="30">
</div>
<div class="col-md-3">
<label class="form-label">Min trades (test)</label>
<input id="opt_min_trades_test" class="form-control" type="number" step="1" value="10">
</div>
<div class="col-md-3">
<label class="form-label">Commission</label>
<input id="commission" class="form-control" type="number" step="0.0001" value="0.001">
</div>
<div class="col-md-3">
<label class="form-label">Slippage</label>
<input id="slippage" class="form-control" type="number" step="0.0001" value="0.0005">
</div>
</div>
<div id="optimizationStrategies" class="d-flex flex-column gap-3"></div>
</div>
</div>
<!-- ========================= -->
<!-- Strategy selection -->
<!-- ========================= -->
<div class="card mb-4">
<div class="card-header">
<h3 class="card-title">Strategies</h3>
<div class="card-actions">
<button id="refresh_strategies_btn" class="btn btn-sm btn-outline-secondary">Refresh</button>
</div>
</div>
<div class="card-body">
<div id="strategies_container" class="d-flex flex-column gap-4"></div>
<div class="card p-3">
<div class="d-flex justify-content-between">
<strong>Total combinations</strong>
<span id="combination_counter">0</span>
</div>
</div>
<div class="mt-2 text-end">
<small class="text-muted">
Estimated WF time:
<span id="wf_time_estimate">~ 0 sec</span>
</small>
</div>
<div class="mt-3 text-secondary">
Cada estrategia incluye un <b>param_grid</b> en JSON.
</div>
</div>
</div>
<!-- ========================= -->
<!-- Actions -->
<!-- ========================= -->
<div class="d-flex gap-2 mb-4">
<button id="validate_strategies_btn" class="btn btn-primary">
Validate (WF)
</button>
<button id="report_strategies_btn" class="btn btn-outline-primary">
Generate PDF report
</button>
<button id="runOptimizationBtn" class="btn btn-primary">Run optimization</button>
<button id="generateReportBtn" class="btn btn-outline-primary">Generate PDF report</button>
</div>
<!-- ========================= -->
<!-- Prograss Bar -->
<!-- ========================= -->
<div id="wf_progress_card" class="card mb-4">
<div class="card-header">
<h3 class="card-title">Walk-Forward Progress</h3>
</div>
<div id="progressCard" class="card mb-4 d-none">
<div class="card-header"><h3 class="card-title">Optimization progress</h3></div>
<div class="card-body">
<div class="progress mb-2">
<div
id="wfProgressBar"
class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
style="width: 0%"
>
0%
</div>
</div>
<div id="wf_progress_text" class="text-secondary small">
Waiting to start...
</div>
<div class="progress mb-2"><div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated" style="width:0%">0%</div></div>
<div id="progressText" class="text-secondary small">Waiting to start…</div>
</div>
</div>
<!-- ========================= -->
<!-- Results -->
<!-- ========================= -->
<div class="card mb-4">
<div class="card-header">
<h3 class="card-title">Results</h3>
<div class="card-actions">
<span id="strategies_status_badge" class="badge bg-secondary"></span>
</div>
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title mb-0">Results</h3>
<span id="resultsBadge" class="badge bg-secondary"></span>
</div>
<div class="card-body">
<div id="strategies_message" class="mb-3 text-secondary">Run validation to see results.</div>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Strategy plot</label>
<select id="plot_strategy_select" class="form-select"></select>
</div>
<div id="resultsMessage" class="text-secondary mb-3">Run Step 4 to compare baseline vs optimized.</div>
<div class="row g-3 mb-3">
<div class="col-md-4"><label class="form-label">Strategy plot</label><select id="plotStrategySelect" class="form-select"></select></div>
</div>
<div class="mt-3">
<div id="plot_equity" style="height: 320px;"></div>
</div>
<div class="mt-3">
<div id="plot_returns" style="height: 320px;"></div>
</div>
<div id="plot_equity" style="height: 320px;"></div>
<div class="mt-3" id="plot_returns" style="height: 320px;"></div>
<hr class="my-4">
<div id="strategies_table_wrap"></div>
<details class="mt-3">
<summary class="text-secondary">Debug JSON</summary>
<pre id="strategies_debug" class="mt-2" style="max-height: 300px; overflow:auto;"></pre>
</details>
<div id="resultsTableWrap"></div>
<details class="mt-3"><summary class="text-secondary">Debug JSON</summary><pre id="debugJson" class="mt-2" style="max-height: 320px; overflow:auto;"></pre></details>
</div>
</div>
<!-- ========================= -->
<!-- PDF Viewer -->
<!-- ========================= -->
<div id="pdf_viewer_section" class="card mb-4 d-none">
<div class="card-header">
<h3 class="card-title">Strategies Report (PDF)</h3>
<div class="card-actions">
<button id="close_pdf_btn" class="btn btn-sm btn-outline-secondary">Close</button>
</div>
</div>
<div class="card-body">
<iframe id="pdf_frame" style="width: 100%; height: 800px; border: none;"></iframe>
<div id="pdfViewerSection" class="card mb-4 d-none">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title mb-0">Optimization report</h3>
<button id="closePdfBtn" class="btn btn-sm btn-outline-secondary">Close</button>
</div>
<div class="card-body"><iframe id="pdfFrame" style="width:100%; height:800px; border:none;"></iframe></div>
</div>
</div>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>

View File

@@ -312,7 +312,7 @@
<div id="promotionSummary" class="mt-3"></div>
<div class="mt-2 d-flex gap-2 flex-wrap">
<button id="btnBuildStep4Selection" class="btn btn-success btn-sm">
<button id="btnBuildStep4Selection" class="btn btn-success btn-sm text-white fw-semibold">
Build Step 4 Selection
</button>
</div>