Continue with Step 4
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"})
|
||||
|
||||
@@ -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
@@ -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() {
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user