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 # src/calibration/strategies_inspector.py
from __future__ import annotations from __future__ import annotations
from typing import Any, Dict, List from typing import Any, Dict, List, Tuple
import numpy as np import numpy as np
import pandas as pd 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.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.fixed_stop import FixedStop
from src.risk.stops.trailing_stop import TrailingStop from src.risk.stops.trailing_stop import TrailingStop
from src.risk.stops.atr_stop import ATRStop 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]]: def list_available_strategies() -> List[Dict[str, Any]]:
"""
Devuelve metadata completa para UI.
"""
out = [] out = []
for sid, strategy_class in STRATEGY_REGISTRY.items():
for sid, entry in STRATEGY_REGISTRY.items(): schema = strategy_class.parameters_schema()
out.append({ out.append({
"strategy_id": sid, "strategy_id": sid,
"name": entry["class"].__name__, "name": strategy_class.__name__,
"params": entry["params"], "params": list(schema.keys()),
"tags": [], # puedes rellenar más adelante "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 return out
@@ -89,32 +70,163 @@ def _accumulate_equity(initial: float, returns_pct: List[float]) -> List[float]:
return eq return eq
def _build_param_values(min_v: float, max_v: float, step: float) -> List[float]: def _coerce_like(value: Any, meta: Dict[str, Any]) -> Any:
min_v = float(min_v) typ = (meta or {}).get("type")
max_v = float(max_v) if typ == "int":
step = float(step) 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 def _build_numeric_grid(meta: Dict[str, Any], baseline_value: Any, spec: Dict[str, Any]) -> List[Any]:
if step <= 1: min_v = float(spec.get("min", baseline_value))
return [min_v] max_v = float(spec.get("max", baseline_value))
step = float(spec.get("step", 1))
values = [] if max_v < min_v:
v = min_v min_v, max_v = max_v, min_v
while v <= max_v:
values.append(v)
v += step
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)
# -------------------------------------------------- coerced = []
# Main 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, storage: StorageManager,
payload, payload,
@@ -122,101 +234,30 @@ def inspect_strategies_config(
include_series: bool, include_series: bool,
progress_callback=None, progress_callback=None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe) df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
if df is None or df.empty: if df is None or df.empty:
return { return {"valid": False, "status": "fail", "checks": {}, "message": "No OHLCV data", "results": []}
"valid": False,
"status": "fail",
"checks": {},
"message": "No OHLCV data",
"results": [],
}
checks: Dict[str, Any] = {} checks: Dict[str, Any] = {
checks["data_quality"] = { "data_quality": {
"status": data_quality.get("status", "unknown"), "status": data_quality.get("status", "unknown"),
"message": data_quality.get("message", ""), "message": data_quality.get("message", ""),
} }
}
if data_quality.get("status") == "fail": if data_quality.get("status") == "fail":
return { return {
"valid": False, "valid": False,
"status": "fail", "status": "fail",
"checks": checks, "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": [], "results": [],
"series": {} if include_series else None, "series": {"strategies": {}} if include_series else None,
} }
stop_loss = _build_stop_loss(payload.stop) stop_loss = _build_stop_loss(payload.stop)
base_sizer = _build_position_sizer(payload.risk) base_sizer = _build_position_sizer(payload.risk)
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 {}
for sel in payload.strategies:
sid = sel.strategy_id
entry = STRATEGY_REGISTRY.get(sid)
if entry 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": [],
})
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)): class _CappedSizer(type(base_sizer)):
def __init__(self, inner): def __init__(self, inner):
self.inner = inner self.inner = inner
@@ -238,11 +279,85 @@ def inspect_strategies_config(
capped_sizer = _CappedSizer(base_sizer) capped_sizer = _CappedSizer(base_sizer)
log.info(f"🧠 Step3 | WF run | strategy={sid}") 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))
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
strategy_class = STRATEGY_REGISTRY.get(sid)
if strategy_class is None:
results.append({
"strategy_id": sid,
"status": "fail",
"message": f"Unknown strategy_id: {sid}",
"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
try: try:
wf = WalkForwardValidator( baseline_wf = WalkForwardValidator(
strategy_class=strategy_class, 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, param_grid=param_grid,
data=df, data=df,
train_window=train_td, train_window=train_td,
@@ -255,103 +370,97 @@ def inspect_strategies_config(
position_sizer=capped_sizer, position_sizer=capped_sizer,
stop_loss=stop_loss, stop_loss=stop_loss,
max_combinations=int(payload.optimization.max_combinations), 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() baseline_summary, baseline_returns, baseline_equity = _summarize_wf_result(baseline_res, payload.account_equity)
win_df: pd.DataFrame = wf_res["windows"] 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" status = "fail"
msg = "WF produced no valid windows" message = "Walk-forward produced no valid windows"
overall_status = "fail" overall_status = "fail"
windows_out = [] elif status == "warning" and overall_status == "ok":
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" 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
results.append({ results.append({
"strategy_id": sid, "strategy_id": sid,
"status": status, "status": status,
"message": msg, "message": message,
"n_windows": int(len(windows_out)), "promotion_status": sel.promotion_status,
"oos_final_equity": oos_final, "promotion_score": sel.promotion_score,
"oos_total_return_pct": float(oos_total_return), "selection_source": sel.selection_source,
"oos_max_dd_worst_pct": float(oos_max_dd), "diversity_blocked_by": sel.diversity_blocked_by,
"degradation_sharpe": None, "diversity_correlation": sel.diversity_correlation,
"windows": windows_out, "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: if include_series:
series["strategies"][sid] = { series["strategies"][sid] = {
"window_returns_pct": oos_returns, "baseline": {
"window_equity": eq_curve, "window_returns_pct": baseline_returns,
"window_equity": baseline_equity,
},
"optimized": {
"window_returns_pct": optimized_returns,
"window_equity": optimized_equity,
},
} }
except Exception as exc:
except Exception as e: log.error(f"❌ Step4 optimization error | strategy={sid} | {exc}")
log.error(f"❌ Step3 WF error | strategy={sid} | {e}")
results.append({ results.append({
"strategy_id": sid, "strategy_id": sid,
"status": "fail", "status": "fail",
"message": f"Exception: {e}", "message": f"Exception: {exc}",
"n_windows": 0, "promotion_status": sel.promotion_status,
"oos_final_equity": float(payload.account_equity), "promotion_score": sel.promotion_score,
"oos_total_return_pct": 0.0, "selection_source": sel.selection_source,
"oos_max_dd_worst_pct": 0.0, "diversity_blocked_by": sel.diversity_blocked_by,
"degradation_sharpe": None, "diversity_correlation": sel.diversity_correlation,
"windows": [], "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" overall_status = "fail"
valid = 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 = { out = {
"valid": valid, "valid": valid,
"status": overall_status, "status": overall_status,
"checks": checks, "checks": checks,
"message": human_msg, "message": {
"ok": "Optimization completed successfully",
"warning": "Optimization completed with warnings",
"fail": "Optimization failed",
}[overall_status],
"results": results, "results": results,
} }
if include_series: if include_series:
out["series"] = series out["series"] = series
return out return out

View File

@@ -1,105 +1,120 @@
# src/web/api/v2/routers/calibration_strategies.py # src/web/api/v2/routers/calibration_strategies.py
import logging import logging
import re import re
import threading
import uuid import uuid
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict
from fastapi import APIRouter, Depends, HTTPException, Request from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse, HTMLResponse from fastapi.responses import JSONResponse
from src.data.storage import StorageManager from src.calibration.optimization_inspector import inspect_optimization_config, list_available_strategies
from src.calibration.optimization_inspector import (
inspect_strategies_config,
list_available_strategies,
)
from src.calibration.reports.optimization_report import generate_strategies_report_pdf from src.calibration.reports.optimization_report import generate_strategies_report_pdf
from src.data.storage import StorageManager
from ..schemas.calibration_optimization import ( from ..schemas.calibration_optimization import (
CalibrationStrategiesInspectRequest, CalibrationOptimizationInspectRequest,
CalibrationStrategiesInspectResponse, CalibrationOptimizationInspectResponse,
CalibrationStrategiesValidateResponse, CalibrationOptimizationValidateResponse,
) )
logger = logging.getLogger("tradingbot.api.v2") logger = logging.getLogger("tradingbot.api.v2")
router = APIRouter( router = APIRouter(prefix="/calibration/optimization", tags=["calibration"])
prefix="/calibration/optimization",
tags=["calibration"],
)
WF_JOBS: Dict[str, Dict] = {} WF_JOBS: Dict[str, Dict] = {}
def get_storage() -> StorageManager: def get_storage() -> StorageManager:
return StorageManager.from_env() return StorageManager.from_env()
@router.get("/catalog") @router.get("/catalog")
def strategy_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=CalibrationOptimizationInspectResponse)
def inspect_optimization(payload: CalibrationOptimizationInspectRequest, storage: StorageManager = Depends(get_storage)):
@router.post("/inspect", response_model=CalibrationStrategiesInspectResponse)
def inspect_strategies(
payload: CalibrationStrategiesInspectRequest,
storage: StorageManager = Depends(get_storage),
):
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe) df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
if df is None or df.empty: if df is None or df.empty:
raise HTTPException(status_code=400, detail="No OHLCV data found. Run Step 1 first.") raise HTTPException(status_code=400, detail="No OHLCV data found. Run Step 1 first.")
from .calibration_data import analyze_data_quality from .calibration_data import analyze_data_quality
data_quality = analyze_data_quality(df, payload.timeframe) data_quality = analyze_data_quality(df, payload.timeframe)
result = inspect_optimization_config(
result = inspect_strategies_config(
storage=storage, storage=storage,
payload=payload, payload=payload,
data_quality=data_quality, data_quality=data_quality,
include_series=False, include_series=False,
) )
return CalibrationStrategiesInspectResponse(**result) return CalibrationOptimizationInspectResponse(**result)
@router.post("/validate", response_model=CalibrationStrategiesValidateResponse)
def validate_strategies( @router.post("/validate", response_model=CalibrationOptimizationValidateResponse)
payload: CalibrationStrategiesInspectRequest, def validate_optimization(payload: CalibrationOptimizationInspectRequest, storage: StorageManager = Depends(get_storage)):
storage: StorageManager = Depends(get_storage),
):
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe) df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
if df is None or df.empty: if df is None or df.empty:
raise HTTPException(status_code=400, detail="No OHLCV data found. Run Step 1 first.") raise HTTPException(status_code=400, detail="No OHLCV data found. Run Step 1 first.")
from .calibration_data import analyze_data_quality from .calibration_data import analyze_data_quality
data_quality = analyze_data_quality(df, payload.timeframe) data_quality = analyze_data_quality(df, payload.timeframe)
result = inspect_optimization_config(
result = inspect_strategies_config(
storage=storage, storage=storage,
payload=payload, payload=payload,
data_quality=data_quality, data_quality=data_quality,
include_series=True, 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") @router.post("/report")
def report_strategies( def report_optimization(payload: CalibrationOptimizationInspectRequest, storage: StorageManager = Depends(get_storage)):
payload: CalibrationStrategiesInspectRequest, logger.info("🧾 Generating optimization report | %s %s", payload.symbol, payload.timeframe)
storage: StorageManager = Depends(get_storage),
):
logger.info(f"🧾 Generating strategies report | {payload.symbol} {payload.timeframe}")
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe) df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
if df is None or df.empty: if df is None or df.empty:
@@ -107,30 +122,21 @@ def report_strategies(
from .calibration_data import analyze_data_quality from .calibration_data import analyze_data_quality
data_quality = analyze_data_quality(df, payload.timeframe) data_quality = analyze_data_quality(df, payload.timeframe)
result = inspect_optimization_config(
result = inspect_strategies_config(
storage=storage, storage=storage,
payload=payload, payload=payload,
data_quality=data_quality, data_quality=data_quality,
include_series=True, include_series=True,
) )
# --------------------------------------------- project_root = Path(__file__).resolve().parents[4].parent
# Prepare PDF output path (outside src) reports_dir = project_root / "reports" / "optimization"
# ---------------------------------------------
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"
reports_dir.mkdir(parents=True, exist_ok=True) reports_dir.mkdir(parents=True, exist_ok=True)
safe_symbol = re.sub(r"[^a-zA-Z0-9_-]", "_", payload.symbol) 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 = reports_dir / safe_symbol
symbol_dir.mkdir(exist_ok=True) symbol_dir.mkdir(exist_ok=True)
output_path = symbol_dir / filename output_path = symbol_dir / filename
generate_strategies_report_pdf( generate_strategies_report_pdf(
@@ -141,9 +147,6 @@ def report_strategies(
"Account equity": payload.account_equity, "Account equity": payload.account_equity,
}, },
config={ 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 train_days": payload.wf.train_days,
"WF test_days": payload.wf.test_days, "WF test_days": payload.wf.test_days,
"WF step_days": payload.wf.step_days or 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, 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}) 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 # src/web/api/v2/schemas/calibration_strategies.py
from typing import Any, Dict, List, Literal, Optional from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -9,38 +8,43 @@ from .calibration_risk import StopConfigSchema, RiskConfigSchema, GlobalRiskRule
class WalkForwardConfigSchema(BaseModel): class WalkForwardConfigSchema(BaseModel):
train_days: int = Field(..., gt=0) train_days: int = Field(..., gt=0)
test_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): class OptimizationConfigSchema(BaseModel):
optimizer_metric: str = Field("sharpe_ratio") 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_train: int = Field(30, ge=0)
min_trades_test: int = Field(10, ge=0) min_trades_test: int = Field(10, ge=0)
class ParameterRangeSchema(BaseModel): class NumericParameterRangeSchema(BaseModel):
min: float min: float
max: float max: float
step: float step: float
class StrategySelectionSchema(BaseModel): class OptimizationStrategySchema(BaseModel):
strategy_id: str 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 symbol: str
timeframe: str timeframe: str
# snapshot from Step 2 (closed)
stop: StopConfigSchema stop: StopConfigSchema
risk: RiskConfigSchema risk: RiskConfigSchema
global_rules: GlobalRiskRulesSchema global_rules: GlobalRiskRulesSchema
account_equity: float = Field(..., gt=0) account_equity: float = Field(..., gt=0)
strategies: List[StrategySelectionSchema] strategies: List[OptimizationStrategySchema]
wf: WalkForwardConfigSchema wf: WalkForwardConfigSchema
optimization: OptimizationConfigSchema optimization: OptimizationConfigSchema
@@ -54,7 +58,6 @@ class WindowRowSchema(BaseModel):
train_end: str train_end: str
test_start: str test_start: str
test_end: str test_end: str
return_pct: float return_pct: float
sharpe: float sharpe: float
max_dd_pct: float max_dd_pct: float
@@ -62,28 +65,53 @@ class WindowRowSchema(BaseModel):
params: Dict[str, Any] params: Dict[str, Any]
class StrategyRunResultSchema(BaseModel): class RunSummarySchema(BaseModel):
strategy_id: str
status: Literal["ok", "warning", "fail"]
message: str
n_windows: int n_windows: int
oos_final_equity: float oos_final_equity: float
oos_total_return_pct: float oos_total_return_pct: float
oos_max_dd_worst_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] 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 valid: bool
status: Literal["ok", "warning", "fail"] status: Literal["ok", "warning", "fail"]
checks: Dict[str, Any] checks: Dict[str, Any]
message: str message: str
results: List[StrategyOptimizationResultSchema]
results: List[StrategyRunResultSchema]
class CalibrationStrategiesValidateResponse(CalibrationStrategiesInspectResponse): class CalibrationOptimizationValidateResponse(CalibrationOptimizationInspectResponse):
series: Dict[str, Any] 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.remove("btn-outline-secondary");
btn.classList.add("btn-outline-primary"); btn.classList.add("btn-outline-primary");
btn.setAttribute("aria-disabled", "false"); 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.remove("btn-outline-primary");
btn.classList.add("btn-outline-secondary"); btn.classList.add("btn-outline-secondary");
btn.setAttribute("aria-disabled", "true"); 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(); wirePromotionUI();
restoreStep4SelectionArtifact();
document.getElementById("plot_strategy_select").addEventListener("change", function() { document.getElementById("plot_strategy_select").addEventListener("change", function() {
if (!lastValidationResult || !selectedStrategyId) return; if (!lastValidationResult || !selectedStrategyId) return;
@@ -2283,9 +2288,40 @@ function buildStep4SelectionArtifact() {
previewEl.textContent = JSON.stringify(artifact, null, 2); previewEl.textContent = JSON.stringify(artifact, null, 2);
previewEl.classList.remove("d-none"); previewEl.classList.remove("d-none");
localStorage.setItem("calibration.step4.selection", JSON.stringify(artifact));
if ((artifact.selected_strategies || []).length > 0) enableNextStep();
else disableNextStep();
updateStep4SelectionSummary(); 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() { async function runPromotion() {

View File

@@ -2,20 +2,10 @@
{% block content %} {% block content %}
<div class="container-xl"> <div class="container-xl">
<!-- ========================= -->
<!-- Wizard header -->
<!-- ========================= -->
<div class="d-flex align-items-center mb-4"> <div class="d-flex align-items-center mb-4">
<!-- Back arrow -->
<div class="me-3"> <div class="me-3">
<a href="/calibration/risk" class="btn btn-outline-secondary btn-icon"> <a href="/calibration/strategies" class="btn btn-outline-secondary btn-icon">
<svg xmlns="http://www.w3.org/2000/svg" <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">
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 stroke="none" d="M0 0h24v24H0z"/>
<path d="M15 6l-6 6l6 6"/> <path d="M15 6l-6 6l6 6"/>
</svg> </svg>
@@ -23,342 +13,112 @@
</div> </div>
<div class="flex-grow-1 text-center"> <div class="flex-grow-1 text-center">
<h2 class="mb-0">Calibración · Paso 3 · Strategies</h2> <h2 class="mb-0">Calibración · Paso 4 · Optimization</h2>
<div class="text-secondary">Optimización + Walk Forward (OOS)</div> <div class="text-secondary">Baseline vs optimized walk-forward validation</div>
</div> </div>
<!-- Forward arrow (disabled until OK) -->
<div class="ms-3"> <div class="ms-3">
<a <a id="next-step-btn" href="#" class="btn btn-outline-secondary btn-icon" aria-disabled="true" title="Step 5 not implemented yet">
id="next-step-btn" <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">
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">
<path stroke="none" d="M0 0h24v24H0z"/> <path stroke="none" d="M0 0h24v24H0z"/>
<path d="M9 6l6 6l-6 6"/> <path d="M9 6l6 6l-6 6"/>
</svg> </svg>
</a> </a>
</div> </div>
</div> </div>
<!-- ========================= -->
<!-- Context -->
<!-- ========================= -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header"><h3 class="card-title">Step 4 input artifact</h3></div>
<h3 class="card-title">Context</h3> <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>
<div class="card mb-4">
<div class="card-header"><h3 class="card-title">Inherited context</h3></div>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-3"><label class="form-label">Symbol</label><input id="symbol" class="form-control" readonly></div>
<label class="form-label">Symbol</label> <div class="col-md-2"><label class="form-label">Timeframe</label><input id="timeframe" class="form-control" readonly></div>
<input id="symbol" class="form-control" placeholder="BTC/USDT"> <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>
<div class="col-md-4"> <div class="row g-3 mt-1">
<label class="form-label">Timeframe</label> <div class="col-md-2"><label class="form-label">Train days</label><input id="wf_train_days" type="number" class="form-control"></div>
<input id="timeframe" class="form-control" placeholder="1h"> <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>
<div class="col-md-4"> <div class="row g-3 mt-1">
<label class="form-label">Account equity</label> <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>
<input id="account_equity" class="form-control" type="number" step="0.01" value="10000"> <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>
</div> <div class="row g-3 mt-1">
<div class="mt-3 text-secondary"> <div class="col-md-2"><label class="form-label">ATR period</label><input id="atr_period" type="number" class="form-control"></div>
Tip: Symbol y timeframe se cargan desde Step 1 (localStorage). Si no aparecen, rellénalos manualmente. <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> </div>
</div> </div>
<!-- ========================= -->
<!-- Risk & Stops -->
<!-- ========================= -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title mb-0">Risk & Stops(Step 2)</h3> <h3 class="card-title mb-0">Optimization ranges</h3>
<div id="strategyCount" class="text-secondary small"></div>
<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>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <div id="optimizationStrategies" class="d-flex flex-column gap-3"></div>
<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> </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"> <div class="d-flex gap-2 mb-4">
<button id="validate_strategies_btn" class="btn btn-primary"> <button id="runOptimizationBtn" class="btn btn-primary">Run optimization</button>
Validate (WF) <button id="generateReportBtn" class="btn btn-outline-primary">Generate PDF report</button>
</button>
<button id="report_strategies_btn" class="btn btn-outline-primary">
Generate PDF report
</button>
</div> </div>
<!-- ========================= --> <div id="progressCard" class="card mb-4 d-none">
<!-- Prograss Bar --> <div class="card-header"><h3 class="card-title">Optimization progress</h3></div>
<!-- ========================= -->
<div id="wf_progress_card" class="card mb-4">
<div class="card-header">
<h3 class="card-title">Walk-Forward Progress</h3>
</div>
<div class="card-body"> <div class="card-body">
<div class="progress mb-2"><div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated" style="width:0%">0%</div></div>
<div class="progress mb-2"> <div id="progressText" class="text-secondary small">Waiting to start…</div>
<div
id="wfProgressBar"
class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
style="width: 0%"
>
0%
</div> </div>
</div> </div>
<div id="wf_progress_text" class="text-secondary small">
Waiting to start...
</div>
</div>
</div>
<!-- ========================= -->
<!-- Results -->
<!-- ========================= -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title">Results</h3> <h3 class="card-title mb-0">Results</h3>
<div class="card-actions"> <span id="resultsBadge" class="badge bg-secondary"></span>
<span id="strategies_status_badge" class="badge bg-secondary"></span>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="strategies_message" class="mb-3 text-secondary">Run validation to see results.</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="row g-3"> <div class="col-md-4"><label class="form-label">Strategy plot</label><select id="plotStrategySelect" class="form-select"></select></div>
<div class="col-md-4">
<label class="form-label">Strategy plot</label>
<select id="plot_strategy_select" class="form-select"></select>
</div> </div>
</div>
<div class="mt-3">
<div id="plot_equity" style="height: 320px;"></div> <div id="plot_equity" style="height: 320px;"></div>
</div> <div class="mt-3" id="plot_returns" style="height: 320px;"></div>
<div class="mt-3">
<div id="plot_returns" style="height: 320px;"></div>
</div>
<hr class="my-4"> <hr class="my-4">
<div id="resultsTableWrap"></div>
<div id="strategies_table_wrap"></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>
<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> </div>
</div> </div>
<!-- ========================= --> <div id="pdfViewerSection" class="card mb-4 d-none">
<!-- PDF Viewer --> <div class="card-header d-flex justify-content-between align-items-center">
<!-- ========================= --> <h3 class="card-title mb-0">Optimization report</h3>
<div id="pdf_viewer_section" class="card mb-4 d-none"> <button id="closePdfBtn" class="btn btn-sm btn-outline-secondary">Close</button>
<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="pdfFrame" style="width:100%; height:800px; border:none;"></iframe></div>
</div> </div>
<div class="card-body">
<iframe id="pdf_frame" style="width: 100%; height: 800px; border: none;"></iframe>
</div>
</div>
</div> </div>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script> <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 id="promotionSummary" class="mt-3"></div>
<div class="mt-2 d-flex gap-2 flex-wrap"> <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 Build Step 4 Selection
</button> </button>
</div> </div>