Continue with Step 4
This commit is contained in:
@@ -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,21 +234,15 @@ 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":
|
||||||
@@ -144,105 +250,114 @@ def inspect_strategies_config(
|
|||||||
"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)
|
||||||
|
|
||||||
|
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))
|
train_td = pd.Timedelta(days=int(payload.wf.train_days))
|
||||||
test_td = pd.Timedelta(days=int(payload.wf.test_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))
|
step_td = pd.Timedelta(days=int(payload.wf.step_days or payload.wf.test_days))
|
||||||
|
|
||||||
overall_status = "ok"
|
|
||||||
results: List[Dict[str, Any]] = []
|
results: List[Dict[str, Any]] = []
|
||||||
series: Dict[str, Any] = {"strategies": {}} if include_series else {}
|
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:
|
for sel in payload.strategies:
|
||||||
|
|
||||||
sid = sel.strategy_id
|
sid = sel.strategy_id
|
||||||
entry = STRATEGY_REGISTRY.get(sid)
|
strategy_class = STRATEGY_REGISTRY.get(sid)
|
||||||
|
if strategy_class is None:
|
||||||
if entry is None:
|
|
||||||
results.append({
|
results.append({
|
||||||
"strategy_id": sid,
|
"strategy_id": sid,
|
||||||
"status": "fail",
|
"status": "fail",
|
||||||
"message": f"Unknown strategy_id: {sid}",
|
"message": f"Unknown strategy_id: {sid}",
|
||||||
"n_windows": 0,
|
"promotion_status": sel.promotion_status,
|
||||||
"oos_final_equity": 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": ["unknown_strategy"],
|
||||||
|
"return_delta_pct": 0.0,
|
||||||
|
"sharpe_delta": 0.0,
|
||||||
|
"max_dd_delta_pct": 0.0,
|
||||||
|
"trades_delta": 0,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
overall_status = "fail"
|
overall_status = "fail"
|
||||||
continue
|
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:
|
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 = []
|
overall_status = "warning"
|
||||||
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
|
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -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"})
|
|
||||||
|
|||||||
@@ -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
@@ -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() {
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
<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-4">
|
<div class="col-md-2"><label class="form-label">Slippage</label><input id="slippage" type="number" step="0.0001" class="form-control"></div>
|
||||||
<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>
|
</div>
|
||||||
<div class="mt-3 text-secondary">
|
<div class="row g-3 mt-1">
|
||||||
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">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>
|
</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 id="wf_progress_text" class="text-secondary small">
|
|
||||||
Waiting to start...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</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 id="plot_equity" style="height: 320px;"></div>
|
||||||
<div class="mt-3">
|
<div class="mt-3" id="plot_returns" style="height: 320px;"></div>
|
||||||
<div id="plot_equity" style="height: 320px;"></div>
|
|
||||||
</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="pdf_frame" style="width: 100%; height: 800px; border: none;"></iframe>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="card-body"><iframe id="pdfFrame" style="width:100%; height:800px; border:none;"></iframe></div>
|
||||||
</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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user