feat(calibration): Step 3 - Stategies preparado conceptualmente

This commit is contained in:
DaM
2026-02-14 13:47:08 +01:00
parent f4f4e8e5be
commit 4365366e7d
9 changed files with 1664 additions and 3 deletions

View File

@@ -11,6 +11,7 @@ import time
from .settings import settings
from src.web.api.v2.routers.calibration_data import router as calibration_data_router
from src.web.api.v2.routers.calibration_risk import router as calibration_risk_router
from src.web.api.v2.routers.calibration_strategies import router as calibration_strategies_router
# --------------------------------------------------
# Logging
@@ -115,6 +116,17 @@ def create_app() -> FastAPI:
"step": 2,
},
)
@app.get("/calibration/strategies", response_class=HTMLResponse)
def calibration_risk_page(request: Request):
return templates.TemplateResponse(
"pages/calibration/calibration_strategies.html",
{
"request": request,
"page": "calibration",
"step": 3,
},
)
# --------------------------------------------------
@@ -123,6 +135,7 @@ def create_app() -> FastAPI:
api_prefix = settings.api_prefix
app.include_router(calibration_data_router, prefix=api_prefix)
app.include_router(calibration_risk_router, prefix=api_prefix)
app.include_router(calibration_strategies_router, prefix=api_prefix)
return app

View File

@@ -0,0 +1,191 @@
# src/web/api/v2/routers/calibration_strategies.py
import logging
import re
import uuid
from pathlib import Path
from typing import Dict
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse, HTMLResponse
from src.data.storage import StorageManager
from src.calibration.strategies_inspector import (
inspect_strategies_config,
list_available_strategies,
)
from src.calibration.reports.strategies_report import generate_strategies_report_pdf
from ..schemas.calibration_strategies import (
CalibrationStrategiesInspectRequest,
CalibrationStrategiesInspectResponse,
CalibrationStrategiesValidateResponse,
)
logger = logging.getLogger("tradingbot.api.v2")
router = APIRouter(
prefix="/calibration/strategies",
tags=["calibration"],
)
WF_JOBS: Dict[str, Dict] = {}
def get_storage() -> StorageManager:
return StorageManager.from_env()
@router.get("/available")
def available_strategies():
return {"strategies": list_available_strategies()}
@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)
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(
storage=storage,
payload=payload,
data_quality=data_quality,
include_series=False,
)
return CalibrationStrategiesInspectResponse(**result)
@router.post("/validate", response_model=CalibrationStrategiesValidateResponse)
def validate_strategies(
payload: CalibrationStrategiesInspectRequest,
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(
storage=storage,
payload=payload,
data_quality=data_quality,
include_series=True,
)
return CalibrationStrategiesValidateResponse(**result)
@router.post("/report")
def report_strategies(
payload: CalibrationStrategiesInspectRequest,
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)
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(
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"
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"
symbol_dir = reports_dir / safe_symbol
symbol_dir.mkdir(exist_ok=True)
output_path = symbol_dir / filename
generate_strategies_report_pdf(
output_path=output_path,
context={
"Symbol": payload.symbol,
"Timeframe": payload.timeframe,
"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,
"Optimizer metric": payload.optimization.optimizer_metric,
"Max combinations": payload.optimization.max_combinations,
},
results=result,
)
public_url = f"/reports/strategies/{safe_symbol}/{filename}"
return JSONResponse(content={"status": result.get("status", "ok"), "url": public_url})
@router.post("/run")
def run_strategies_async(
payload: CalibrationStrategiesInspectRequest,
storage: StorageManager = Depends(get_storage),
):
import threading
import uuid
job_id = uuid.uuid4().hex
WF_JOBS[job_id] = {
"status": "running",
"progress": 0,
"current_window": 0,
"total_windows": 0,
"current_strategy": None,
"result": None,
}
def background_job():
def progress_cb(window_id, total_windows):
WF_JOBS[job_id]["current_window"] = window_id
WF_JOBS[job_id]["total_windows"] = total_windows
WF_JOBS[job_id]["progress"] = int(
window_id / total_windows * 100
)
result = inspect_strategies_config(
storage=storage,
payload=payload,
data_quality={"status": "ok"},
include_series=True,
progress_callback=progress_cb, # ← lo pasamos
)
WF_JOBS[job_id]["status"] = "done"
WF_JOBS[job_id]["progress"] = 100
WF_JOBS[job_id]["result"] = result
thread = threading.Thread(target=background_job)
thread.start()
return {"job_id": job_id}
@router.get("/status/{job_id}")
def get_status(job_id: str):
return WF_JOBS.get(job_id, {"status": "unknown"})

View File

@@ -0,0 +1,83 @@
# src/web/api/v2/schemas/calibration_strategies.py
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, Field
from .calibration_risk import StopConfigSchema, RiskConfigSchema, GlobalRiskRulesSchema
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
class OptimizationConfigSchema(BaseModel):
optimizer_metric: str = Field("sharpe_ratio")
max_combinations: int = Field(500, gt=0)
min_trades_train: int = Field(30, ge=0)
min_trades_test: int = Field(10, ge=0)
class StrategySelectionSchema(BaseModel):
strategy_id: str
param_grid: Dict[str, List[Any]]
class CalibrationStrategiesInspectRequest(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]
wf: WalkForwardConfigSchema
optimization: OptimizationConfigSchema
commission: float = Field(0.001, ge=0)
slippage: float = Field(0.0005, ge=0)
class WindowRowSchema(BaseModel):
window: int
train_start: str
train_end: str
test_start: str
test_end: str
return_pct: float
sharpe: float
max_dd_pct: float
trades: int
params: Dict[str, Any]
class StrategyRunResultSchema(BaseModel):
strategy_id: str
status: Literal["ok", "warning", "fail"]
message: str
n_windows: int
oos_final_equity: float
oos_total_return_pct: float
oos_max_dd_worst_pct: float
degradation_sharpe: Optional[float] = None
windows: List[WindowRowSchema]
class CalibrationStrategiesInspectResponse(BaseModel):
valid: bool
status: Literal["ok", "warning", "fail"]
checks: Dict[str, Any]
message: str
results: List[StrategyRunResultSchema]
class CalibrationStrategiesValidateResponse(CalibrationStrategiesInspectResponse):
series: Dict[str, Any]

View File

@@ -0,0 +1,595 @@
// src/web/ui/v2/static/js/pages/calibration_strategies.js
console.log("[calibration_strategies] script loaded ✅", new Date().toISOString());
// =================================================
// WIZARD NAVIGATION
// =================================================
function enableNextStep() {
const btn = document.getElementById("next-step-btn");
if (!btn) return;
btn.classList.remove("btn-outline-secondary");
btn.classList.add("btn-outline-primary");
btn.setAttribute("aria-disabled", "false");
}
function disableNextStep() {
const btn = document.getElementById("next-step-btn");
if (!btn) return;
btn.classList.remove("btn-outline-primary");
btn.classList.add("btn-outline-secondary");
btn.setAttribute("aria-disabled", "true");
}
// =================================================
// UTILS
// =================================================
function loadContextFromLocalStorage() {
const symbol = localStorage.getItem("calibration.symbol");
const timeframe = localStorage.getItem("calibration.timeframe");
if (symbol) setVal("symbol", symbol);
if (timeframe) setVal("timeframe", timeframe);
// Step 2 snapshot (if stored)
const stop_type = localStorage.getItem("calibration.stop.type");
const stop_fraction = localStorage.getItem("calibration.stop.stop_fraction");
const atr_period = localStorage.getItem("calibration.stop.atr_period");
const atr_multiplier = localStorage.getItem("calibration.stop.atr_multiplier");
const risk_fraction = localStorage.getItem("calibration.risk.risk_fraction");
const max_position_fraction = localStorage.getItem("calibration.risk.max_position_fraction");
const max_drawdown_pct = localStorage.getItem("calibration.rules.max_drawdown_pct");
const daily_loss_limit_pct = localStorage.getItem("calibration.rules.daily_loss_limit_pct");
const max_consecutive_losses = localStorage.getItem("calibration.rules.max_consecutive_losses");
const cooldown_bars = localStorage.getItem("calibration.rules.cooldown_bars");
const account_equity = localStorage.getItem("calibration.account_equity");
if (account_equity) setVal("account_equity", account_equity);
if (stop_type) setVal("stop_type", stop_type);
if (stop_fraction) setVal("stop_fraction", stop_fraction);
if (atr_period) setVal("atr_period", atr_period);
if (atr_multiplier) setVal("atr_multiplier", atr_multiplier);
if (risk_fraction) setVal("risk_fraction", risk_fraction);
if (max_position_fraction) setVal("max_position_fraction", max_position_fraction);
if (max_drawdown_pct) setVal("max_drawdown_pct", max_drawdown_pct);
if (daily_loss_limit_pct) setVal("daily_loss_limit_pct", daily_loss_limit_pct);
if (max_consecutive_losses) setVal("max_consecutive_losses", max_consecutive_losses);
if (cooldown_bars) setVal("cooldown_bars", cooldown_bars);
}
function buildPayload() {
const symbol = str("symbol");
const timeframe = str("timeframe");
const stopType = str("stop_type");
if (!symbol || !timeframe) {
throw new Error("symbol/timeframe missing");
}
const stop = { type: stopType };
if (stopType === "fixed" || stopType === "trailing") {
stop.stop_fraction = (num("stop_fraction") ?? 1.0) / 100;
}
if (stopType === "atr") {
stop.atr_period = num("atr_period") ?? 14;
stop.atr_multiplier = num("atr_multiplier") ?? 3.0;
}
const risk_fraction = (num("risk_fraction") ?? 1.0) / 100;
const max_position_fraction = (num("max_position_fraction") ?? 95) / 100;
const global_rules = {
max_drawdown_pct: (num("max_drawdown_pct") ?? 20) / 100,
daily_loss_limit_pct: num("daily_loss_limit_pct") ? num("daily_loss_limit_pct") / 100 : null,
max_consecutive_losses: num("max_consecutive_losses"),
cooldown_bars: num("cooldown_bars"),
};
const wf_train_days = num("wf_train_days") ?? 120;
const wf_test_days = num("wf_test_days") ?? 30;
const wf_step_days = num("wf_step_days");
const strategies = collectSelectedStrategies();
return {
symbol,
timeframe,
account_equity: num("account_equity") ?? 10000,
stop,
risk: {
risk_fraction,
max_position_fraction,
},
global_rules,
strategies,
wf: {
train_days: wf_train_days,
test_days: wf_test_days,
step_days: wf_step_days,
},
optimization: {
optimizer_metric: str("opt_metric") ?? "sharpe_ratio",
max_combinations: num("opt_max_combinations") ?? 300,
min_trades_train: num("opt_min_trades_train") ?? 30,
min_trades_test: num("opt_min_trades_test") ?? 10,
},
commission: num("commission") ?? 0.001,
slippage: num("slippage") ?? 0.0005,
};
}
function collectSelectedStrategies() {
const items = document.querySelectorAll("[data-strategy-item]");
const out = [];
items.forEach((node) => {
const checkbox = node.querySelector("input[type=checkbox]");
if (!checkbox || !checkbox.checked) return;
const sid = checkbox.getAttribute("data-strategy-id");
const textarea = node.querySelector("textarea");
let grid = {};
if (textarea && textarea.value.trim()) {
try {
grid = JSON.parse(textarea.value);
} catch (e) {
throw new Error(`Invalid JSON param_grid for ${sid}: ${e.message}`);
}
}
out.push({ strategy_id: sid, param_grid: grid });
});
if (out.length === 0) {
throw new Error("Select at least 1 strategy");
}
return out;
}
async function fetchAvailableStrategies() {
const res = await fetch("/api/v2/calibration/strategies/available");
const data = await res.json();
return data.strategies || [];
}
function num(id) {
const el = document.getElementById(id);
if (!el) return null;
const val = el.value;
if (val === "" || val === null || val === undefined) return null;
const n = Number(val);
return Number.isFinite(n) ? n : null;
}
function str(id) {
const el = document.getElementById(id);
if (!el) return null;
const v = el.value;
return v === null || v === undefined ? null : String(v);
}
function setVal(id, value) {
const el = document.getElementById(id);
if (!el) return;
el.value = value ?? "";
}
// =================================================
// PROGRESS BAR
// =================================================
function startWF() {
document
.getElementById("wf_progress_card")
.classList.remove("d-none");
document.getElementById("wfProgressBar").style.width = "0%";
document.getElementById("wfProgressBar").innerText = "0%";
}
async function pollStatus(jobId) {
const interval = setInterval(async () => {
const res = await fetch(`/api/v2/calibration/strategies/status/${jobId}`);
const data = await res.json();
const bar = document.getElementById("wfProgressBar");
bar.style.width = data.progress + "%";
bar.innerText = data.progress + "%";
if (data.status === "done") {
clearInterval(interval);
bar.classList.remove("progress-bar-animated");
console.log("WF finished");
}
}, 1000);
}
// =================================================
// RENDER RESULTS
// =================================================
function renderStrategiesList(strategies) {
const list = document.getElementById("strategies_list");
if (!list) return;
list.innerHTML = "";
strategies.forEach((s) => {
const col = document.createElement("div");
col.className = "col-12 col-lg-6";
col.setAttribute("data-strategy-item", "1");
const defaultGrid = s.default_grid || {};
const defaultGridText = JSON.stringify(defaultGrid, null, 2);
col.innerHTML = `
<div class="card">
<div class="card-body">
<label class="form-check form-switch">
<input class="form-check-input" type="checkbox" data-strategy-id="${s.strategy_id}">
<span class="form-check-label">
<b>${s.name}</b>
<span class="text-secondary ms-2">${(s.tags || []).join(" · ")}</span>
</span>
</label>
<div class="mt-3">
<label class="form-label">param_grid (JSON)</label>
<textarea class="form-control" rows="7" spellcheck="false">${defaultGridText}</textarea>
<div class="form-hint">Tip: usa listas. Ej: {"fast":[10,20],"slow":[50,100]}</div>
</div>
</div>
</div>
`;
list.appendChild(col);
});
}
function setBadge(status) {
const badge = document.getElementById("strategies_status_badge");
if (!badge) return;
badge.classList.remove("bg-secondary", "bg-success", "bg-warning", "bg-danger");
badge.classList.add(
status === "ok" ? "bg-success" : status === "warning" ? "bg-warning" : status === "fail" ? "bg-danger" : "bg-secondary"
);
badge.textContent = status ? status.toUpperCase() : "—";
}
function renderResultsTable(data) {
const wrap = document.getElementById("strategies_table_wrap");
if (!wrap) return;
const rows = [];
(data.results || []).forEach((r) => {
rows.push(`
<tr>
<td><b>${r.strategy_id}</b></td>
<td>${r.status}</td>
<td>${r.n_windows}</td>
<td>${Number(r.oos_total_return_pct).toFixed(2)}%</td>
<td>${Number(r.oos_max_dd_worst_pct).toFixed(2)}%</td>
<td>${Number(r.oos_final_equity).toFixed(2)}</td>
<td class="text-secondary">${r.message || ""}</td>
</tr>
`);
});
wrap.innerHTML = `
<div class="table-responsive">
<table class="table table-vcenter">
<thead>
<tr>
<th>Strategy</th>
<th>Status</th>
<th>Windows</th>
<th>OOS return</th>
<th>Worst DD</th>
<th>Final equity</th>
<th>Message</th>
</tr>
</thead>
<tbody>
${rows.join("")}
</tbody>
</table>
</div>
`;
}
function populatePlotSelector(data) {
const sel = document.getElementById("plot_strategy_select");
if (!sel) return;
sel.innerHTML = "";
const ids = Object.keys((data.series && data.series.strategies) ? data.series.strategies : {});
ids.forEach((sid) => {
const opt = document.createElement("option");
opt.value = sid;
opt.textContent = sid;
sel.appendChild(opt);
});
sel.onchange = () => renderPlotsForSelected(data);
if (ids.length > 0) {
sel.value = ids[0];
}
}
function renderPlotsForSelected(data) {
const sel = document.getElementById("plot_strategy_select");
const sid = sel ? sel.value : null;
if (!sid) return;
const s = data.series?.strategies?.[sid];
if (!s) return;
const equity = s.window_equity || [];
const returns = s.window_returns_pct || [];
const xEq = [...Array(equity.length).keys()];
const xRet = [...Array(returns.length).keys()].map((i) => i + 1);
Plotly.newPlot("plot_equity", [
{ x: xEq, y: equity, type: "scatter", mode: "lines", name: "Equity (OOS)" },
], {
title: `WF OOS equity · ${sid}`,
margin: { t: 40, l: 50, r: 20, b: 40 },
xaxis: { title: "Window index" },
yaxis: { title: "Equity" },
}, { displayModeBar: false });
Plotly.newPlot("plot_returns", [
{ x: xRet, y: returns, type: "bar", name: "Return % (per window)" },
], {
title: `WF returns per window · ${sid}`,
margin: { t: 40, l: 50, r: 20, b: 40 },
xaxis: { title: "Window" },
yaxis: { title: "Return (%)" },
}, { displayModeBar: false });
}
function renderValidateResponse(data) {
// -------------------------------
// 1⃣ Badge + message
// -------------------------------
const badge = document.getElementById("strategies_status_badge");
const msg = document.getElementById("strategies_message");
badge.textContent = data.status ?? "—";
badge.className = "badge";
if (data.status === "ok") badge.classList.add("bg-success");
else if (data.status === "warning") badge.classList.add("bg-warning");
else badge.classList.add("bg-danger");
msg.textContent = data.message ?? "";
// -------------------------------
// 2⃣ Debug JSON
// -------------------------------
document.getElementById("strategies_debug").textContent =
JSON.stringify(data, null, 2);
// -------------------------------
// 3⃣ Plots (primera estrategia por ahora)
// -------------------------------
if (data.series && data.series.strategies) {
const keys = Object.keys(data.series.strategies);
if (keys.length > 0) {
const s = data.series.strategies[keys[0]];
Plotly.newPlot("plot_equity", [{
y: s.window_equity,
type: "scatter",
mode: "lines",
name: "Equity"
}], { margin: { t: 20 } });
Plotly.newPlot("plot_returns", [{
y: s.window_returns_pct,
type: "bar",
name: "Window returns %"
}], { margin: { t: 20 } });
}
}
// -------------------------------
// 4⃣ Table
// -------------------------------
const wrap = document.getElementById("strategies_table_wrap");
wrap.innerHTML = "";
if (data.results) {
let html = `<table class="table table-sm table-bordered">
<thead>
<tr>
<th>Strategy</th>
<th>Status</th>
<th>OOS Return %</th>
<th>OOS Max DD %</th>
<th>Windows</th>
</tr>
</thead>
<tbody>`;
for (const r of data.results) {
html += `
<tr>
<td>${r.strategy_id}</td>
<td>${r.status}</td>
<td>${r.oos_total_return_pct?.toFixed(2)}</td>
<td>${r.oos_max_dd_worst_pct?.toFixed(2)}</td>
<td>${r.n_windows}</td>
</tr>
`;
}
html += "</tbody></table>";
wrap.innerHTML = html;
}
}
async function validateStrategies() {
console.log("[calibration_strategies] validateStrategies() START");
const bar = document.getElementById("wfProgressBar");
const txt = document.getElementById("wf_progress_text");
const setProgress = (pct, text) => {
const p = Math.max(0, Math.min(100, Number(pct || 0)));
bar.style.width = `${p}%`;
bar.textContent = `${p}%`;
if (text) txt.textContent = text;
};
try {
// 0) Reset UI
setProgress(0, "Starting...");
// 1) Construye payload igual que antes (usa tu función existente)
const payload = buildPayload(); // <-- NO CAMBIES tu builder, reutilízalo
// 2) Arranca job async
const runResp = await fetch("/api/v2/calibration/strategies/run", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!runResp.ok) {
const errText = await runResp.text();
throw new Error(`Run failed: ${runResp.status} ${errText}`);
}
const { job_id } = await runResp.json();
if (!job_id) throw new Error("No job_id returned from /run");
// 3) Poll status
const pollEveryMs = 500;
const maxMinutes = 30;
const maxPolls = Math.ceil((maxMinutes * 60 * 1000) / pollEveryMs);
for (let i = 0; i < maxPolls; i++) {
await new Promise((r) => setTimeout(r, pollEveryMs));
const stResp = await fetch(`/api/v2/calibration/strategies/status/${job_id}`);
if (!stResp.ok) continue;
const st = await stResp.json();
const pct = st.progress ?? 0;
const cw = st.current_window ?? 0;
const tw = st.total_windows ?? 0;
const label =
tw > 0
? `WF running... window ${cw}/${tw}`
: "WF running...";
setProgress(pct, label);
if (st.status === "done") {
setProgress(100, "WF completed ✅");
// 4) Renderiza resultados usando el MISMO renderer que usabas con /validate
// (ojo: el resultado viene dentro de st.result)
if (!st.result) throw new Error("Job done but no result in status payload");
renderValidateResponse(st.result); // <-- usa tu función existente de render (plots, tablas, etc.)
console.log("[calibration_strategies] validateStrategies() DONE ok");
return;
}
if (st.status === "unknown") {
setProgress(0, "Unknown job (server lost state?)");
break;
}
}
throw new Error("Timeout waiting for WF job to finish");
} catch (err) {
console.error(err);
// deja un estado visible
const txt = document.getElementById("wf_progress_text");
if (txt) txt.textContent = `Error: ${err.message}`;
console.log("[calibration_strategies] validateStrategies() DONE fail");
}
}
async function generateReport() {
console.log("[calibration_strategies] generateReport() START");
let payload;
try {
payload = buildPayload();
} catch (e) {
alert(e.message);
return;
}
const res = await fetch("/api/v2/calibration/strategies/report", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
const data = await res.json();
if (data.url) {
const viewer = document.getElementById("pdf_viewer_section");
const frame = document.getElementById("pdf_frame");
frame.src = data.url;
viewer.classList.remove("d-none");
viewer.scrollIntoView({ behavior: "smooth" });
} else {
alert("Failed to generate report");
}
}
function wireButtons() {
document.getElementById("validate_strategies_btn")?.addEventListener("click", validateStrategies);
document.getElementById("report_strategies_btn")?.addEventListener("click", generateReport);
document.getElementById("refresh_strategies_btn")?.addEventListener("click", async () => {
const strategies = await fetchAvailableStrategies();
renderStrategiesList(strategies);
});
document.getElementById("load_step2_btn")?.addEventListener("click", () => {
loadContextFromLocalStorage();
});
document.getElementById("close_pdf_btn")?.addEventListener("click", () => {
const viewer = document.getElementById("pdf_viewer_section");
const frame = document.getElementById("pdf_frame");
frame.src = "";
viewer.classList.add("d-none");
});
}
async function init() {
loadContextFromLocalStorage();
wireButtons();
const strategies = await fetchAvailableStrategies();
renderStrategiesList(strategies);
// Pre-select 1 strategy by default (moving_average) if exists
setTimeout(() => {
const first = document.querySelector('input[type=checkbox][data-strategy-id="moving_average"]');
if (first) first.checked = true;
}, 0);
}
init();

View File

@@ -0,0 +1,322 @@
{% extends "layout.html" %}
{% 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">
<path stroke="none" d="M0 0h24v24H0z"/>
<path d="M15 6l-6 6l6 6"/>
</svg>
</a>
</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>
</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">
<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>
<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>
<div class="mt-3 text-secondary">
Tip: Symbol y timeframe se cargan desde Step 1 (localStorage). Si no aparecen, rellénalos manualmente.
</div>
</div>
</div>
<!-- ========================= -->
<!-- Risk & Stops snapshot -->
<!-- ========================= -->
<div class="card mb-4">
<div class="card-header">
<h3 class="card-title">Risk & Stops snapshot (Step 2)</h3>
<div class="card-actions">
<button id="load_step2_btn" class="btn btn-sm btn-outline-primary">Load from Step 2</button>
</div>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-3">
<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-3">
<label class="form-label">Stop fraction (%)</label>
<input id="stop_fraction" class="form-control" type="number" step="0.01" value="1.0">
</div>
<div class="col-md-3">
<label class="form-label">ATR period</label>
<input id="atr_period" class="form-control" type="number" step="1" value="14">
</div>
<div class="col-md-3">
<label class="form-label">ATR multiplier</label>
<input id="atr_multiplier" class="form-control" type="number" step="0.1" value="3.0">
</div>
<div class="col-md-3">
<label class="form-label">Risk per trade (%)</label>
<input id="risk_fraction" class="form-control" type="number" step="0.01" value="1.0">
</div>
<div class="col-md-3">
<label class="form-label">Max position fraction (%)</label>
<input id="max_position_fraction" class="form-control" type="number" step="0.1" value="95">
</div>
<div class="col-md-3">
<label class="form-label">Max DD (%)</label>
<input id="max_drawdown_pct" class="form-control" type="number" step="0.1" value="20">
</div>
<div class="col-md-3">
<label class="form-label">Daily loss limit (%) (optional)</label>
<input id="daily_loss_limit_pct" class="form-control" type="number" step="0.1" value="">
</div>
<div class="col-md-3">
<label class="form-label">Max consecutive losses (optional)</label>
<input id="max_consecutive_losses" class="form-control" type="number" step="1" value="">
</div>
<div class="col-md-3">
<label class="form-label">Cooldown bars (optional)</label>
<input id="cooldown_bars" class="form-control" type="number" step="1" value="">
</div>
</div>
<div class="mt-3 text-secondary">
Este snapshot se envía al backend para reproducibilidad y para que WF/optimizer use el mismo sizing/stop.
</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 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>
</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_list" class="row g-3"></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>
</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 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>
</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>
<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>
<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>
<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>
</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>
</div>
</div>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
<script src="/static/js/pages/calibration_strategies.js"></script>
{% endblock %}