preparando step 3 para dejarlo fino

This commit is contained in:
dam
2026-03-03 11:49:08 +01:00
parent 9a59879988
commit 35efec8dd6
44 changed files with 267296 additions and 1479 deletions

View File

@@ -87,6 +87,103 @@ def _accumulate_equity(initial: float, returns_pct: List[float]) -> List[float]:
return eq return eq
def _compute_wf_diagnostics(
*,
window_returns_pct: List[float],
window_trades: List[int],
window_equity: List[float],
test_days: int,
rolling_window: int = 3,
hist_bins: int = 10,
) -> Dict[str, Any]:
"""Compute backend-ready diagnostics for Step 3 visualization.
Notes:
- Operates at *WF window* granularity (not daily).
- All series are aligned to windows (len == n_windows) except equity/drawdown
which include initial point (len == n_windows + 1).
"""
returns = np.asarray(window_returns_pct, dtype=float)
n = int(len(returns))
mean_r = float(np.mean(returns)) if n else 0.0
std_r = float(np.std(returns, ddof=0)) if n else 0.0
pos_rate = float(np.mean(returns > 0.0)) if n else 0.0
# Linear trend of window returns over time (per window index)
if n >= 2:
x = np.arange(n, dtype=float)
slope = float(np.polyfit(x, returns, 1)[0])
else:
slope = 0.0
# Rolling Sharpe-like over windows
k = int(max(1, rolling_window))
roll = [None] * n
if n and k > 1:
scale = float(np.sqrt(k))
for i in range(k - 1, n):
seg = returns[i - k + 1 : i + 1]
m = float(np.mean(seg))
s = float(np.std(seg, ddof=0))
roll[i] = float((m / s) * scale) if s > 0 else 0.0
elif n and k == 1:
roll = [float(r) for r in returns.tolist()]
# Histogram of window returns
if n >= 2:
bins = int(max(3, min(hist_bins, n)))
counts, edges = np.histogram(returns, bins=bins)
hist_counts = [int(c) for c in counts.tolist()]
hist_edges = [float(e) for e in edges.tolist()]
elif n == 1:
hist_counts = [1]
hist_edges = [float(returns[0] - 1.0), float(returns[0] + 1.0)]
else:
hist_counts = []
hist_edges = []
# Drawdown series from equity
eq = [float(x) for x in (window_equity or [])]
dd = []
peak = None
for v in eq:
if peak is None or v > peak:
peak = v
dd.append(float((v / peak - 1.0) * 100.0) if peak and peak > 0 else 0.0)
# Trades density
td = int(test_days) if int(test_days) > 0 else 1
t_int = [int(t) for t in (window_trades or [])]
trades_per_day = [float(t) / float(td) for t in t_int]
return {
"stability": {
"n_windows": n,
"mean_return_pct": mean_r,
"std_return_pct": std_r,
"positive_window_rate": pos_rate,
"return_slope_per_window": slope,
},
"rolling": {
"rolling_window": k,
"rolling_sharpe_like": roll,
},
"distribution": {
"hist_bin_edges": hist_edges,
"hist_counts": hist_counts,
},
"drawdown": {
"equity": eq,
"drawdown_pct": dd,
},
"trades": {
"trades_per_window": t_int,
"trades_per_day": trades_per_day,
},
}
# -------------------------------------------------- # --------------------------------------------------
# Main # Main
# -------------------------------------------------- # --------------------------------------------------
@@ -289,6 +386,7 @@ def inspect_strategies_config(
"oos_total_return_pct": 0.0, "oos_total_return_pct": 0.0,
"oos_max_dd_worst_pct": 0.0, "oos_max_dd_worst_pct": 0.0,
"degradation_sharpe": None, "degradation_sharpe": None,
"diagnostics": _compute_wf_diagnostics(window_returns_pct=[], window_trades=[], window_equity=[float(payload.account_equity)], test_days=int(payload.wf.test_days)),
"windows": [], "windows": [],
}) })
@@ -298,6 +396,7 @@ def inspect_strategies_config(
"window_returns_pct": [], "window_returns_pct": [],
"window_equity": [float(payload.account_equity)], "window_equity": [float(payload.account_equity)],
"window_trades": [], "window_trades": [],
"diagnostics": _compute_wf_diagnostics(window_returns_pct=[], window_trades=[], window_equity=[float(payload.account_equity)], test_days=int(payload.wf.test_days)),
} }
if overall_status == "ok": if overall_status == "ok":
@@ -349,12 +448,23 @@ def inspect_strategies_config(
}) })
oos_returns = win_df["return_pct"].astype(float).tolist() oos_returns = win_df["return_pct"].astype(float).tolist()
eq_curve = _accumulate_equity(float(payload.account_equity), oos_returns) 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_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_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 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
diagnostics = _compute_wf_diagnostics(
window_returns_pct=oos_returns,
window_trades=win_df["trades"].astype(int).tolist(),
window_equity=eq_curve,
test_days=int(payload.wf.test_days),
rolling_window=3,
hist_bins=10,
)
# keep worst-window DD also at top-level for backwards compatibility
diagnostics["drawdown"]["worst_window_dd_pct"] = float(oos_max_dd)
results.append({ results.append({
"strategy_id": sid, "strategy_id": sid,
"status": status, "status": status,
@@ -367,6 +477,7 @@ def inspect_strategies_config(
"oos_total_return_pct": float(oos_total_return), "oos_total_return_pct": float(oos_total_return),
"oos_max_dd_worst_pct": float(oos_max_dd), "oos_max_dd_worst_pct": float(oos_max_dd),
"degradation_sharpe": None, "degradation_sharpe": None,
"diagnostics": diagnostics,
"windows": windows_out, "windows": windows_out,
}) })
@@ -375,6 +486,7 @@ def inspect_strategies_config(
"window_returns_pct": oos_returns, "window_returns_pct": oos_returns,
"window_equity": eq_curve, "window_equity": eq_curve,
"window_trades": win_df["trades"].tolist(), "window_trades": win_df["trades"].tolist(),
"diagnostics": diagnostics,
} }
except Exception as e: except Exception as e:

View File

@@ -1,38 +0,0 @@
# src/web/api/deps.py
from functools import lru_cache
from src.paper.state_store import StateStore
from .settings import settings
from .providers.base import StateProvider
from .providers.sqlite import SQLiteStateProvider
from .providers.mock import MockStateProvider
# --------------------------------------------------
# StateStore singleton (solo para SQLite provider)
# --------------------------------------------------
@lru_cache(maxsize=1)
def get_store() -> StateStore:
"""
Singleton del StateStore.
La API es read-only, así que es seguro compartir conexión.
"""
return StateStore(settings.state_db_path)
# --------------------------------------------------
# Provider selector
# --------------------------------------------------
def get_provider() -> StateProvider:
"""
Devuelve el provider adecuado según configuración.
- mock_mode = True → MockStateProvider
- mock_mode = False → SQLiteStateProvider (real)
"""
if settings.mock_mode:
return MockStateProvider()
store = get_store()
return SQLiteStateProvider(store)

View File

@@ -1,80 +0,0 @@
# src/web/api/main.py
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from .settings import settings
# Routers
from .routers.health import router as health_router
from .routers.bot import router as bot_router
from .routers.equity import router as equity_router
from .routers.trades import router as trades_router
from .routers.metrics import router as metrics_router
from .routers.events import router as events_router
from .routers.positions import router as positions_router
def create_app() -> FastAPI:
app = FastAPI(
title=settings.api_title,
version=settings.api_version,
)
# --------------------------------------------------
# CORS (solo lectura)
# --------------------------------------------------
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost",
"http://127.0.0.1",
"http://localhost:3000",
"http://127.0.0.1:3000",
],
allow_credentials=True,
allow_methods=["GET"],
allow_headers=["*"],
)
# --------------------------------------------------
# Static files (UI)
# --------------------------------------------------
app.mount(
"/static",
StaticFiles(directory="src/web/ui/static"),
name="static",
)
templates = Jinja2Templates(directory="src/web/ui/templates")
# --------------------------------------------------
# UI routes
# --------------------------------------------------
@app.get("/")
def dashboard(request: Request):
return templates.TemplateResponse(
"dashboard.html",
{"request": request},
)
# --------------------------------------------------
# API routers (versionados)
# --------------------------------------------------
api_prefix = settings.api_prefix
app.include_router(health_router, prefix=api_prefix)
app.include_router(bot_router, prefix=api_prefix)
app.include_router(equity_router, prefix=api_prefix)
app.include_router(trades_router, prefix=api_prefix)
app.include_router(metrics_router, prefix=api_prefix)
app.include_router(events_router, prefix=api_prefix)
# app.include_router(positions_router, prefix=api_prefix)
return app
# Instancia ASGI
app = create_app()

View File

@@ -1,36 +0,0 @@
# src/web/api/providers/base.py
from abc import ABC, abstractmethod
from typing import Optional, Dict, Any, List
class StateProvider(ABC):
"""
Contrato que define qué datos expone el bot a la API.
La UI y los routers dependen SOLO de esto.
"""
# -----------------------------
# Core state
# -----------------------------
@abstractmethod
def get_broker(self) -> Optional[Dict[str, Any]]:
...
@abstractmethod
def get_loop(self) -> Optional[Dict[str, Any]]:
...
@abstractmethod
def get_metrics(self) -> Optional[Dict[str, Any]]:
...
# -----------------------------
# Trades
# -----------------------------
@abstractmethod
def list_trades(
self,
limit: int = 200,
symbol: Optional[str] = None,
) -> List[Dict[str, Any]]:
...

View File

@@ -1,61 +0,0 @@
# src/web/api/providers/mock.py
import random
from datetime import datetime, timezone
from typing import Optional, Dict, Any, List
from .base import StateProvider
class MockStateProvider(StateProvider):
"""
Provider mock para desarrollo de UI sin bot corriendo.
"""
def _now(self) -> str:
return datetime.now(timezone.utc).isoformat()
# -----------------------------
# Core state
# -----------------------------
def get_broker(self) -> Optional[Dict[str, Any]]:
equity = 10_000 + random.uniform(-300, 600)
cash = 4_000 + random.uniform(-100, 100)
return {
"initial_cash": 10_000,
"cash": cash,
"equity": equity,
"realized_pnl": equity - 10_000,
"positions": {},
"last_price": {"BTC/USDT": 42_000 + random.uniform(-200, 200)},
"trades_count": random.randint(5, 25),
"updated_at": self._now(),
}
def get_loop(self) -> Optional[Dict[str, Any]]:
curve = [10_000 + i * 3 for i in range(120)]
return {
"last_ts": self._now(),
"equity_curve": curve,
"equity_timestamps": [],
}
def get_metrics(self) -> Optional[Dict[str, Any]]:
return {
"cagr": 0.21,
"max_drawdown": -0.14,
"calmar_ratio": 1.5,
"volatility": 0.28,
"time_in_drawdown": 0.39,
"ulcer_index": 7.4,
}
# -----------------------------
# Trades
# -----------------------------
def list_trades(
self,
limit: int = 200,
symbol: Optional[str] = None,
) -> List[Dict[str, Any]]:
return []

View File

@@ -1,72 +0,0 @@
# src/web/api/providers/sqlite.py
from typing import Optional, Dict, Any, List
from datetime import datetime, timedelta
from src.paper.state_store import StateStore
from .base import StateProvider
class SQLiteStateProvider(StateProvider):
"""
Provider real que lee del StateStore (SQLite).
Read-only.
"""
def __init__(self, store: StateStore):
self.store = store
# -----------------------------
# Core state
# -----------------------------
def get_broker(self) -> Optional[Dict[str, Any]]:
return self.store.load_broker_snapshot()
def get_loop(self) -> Optional[Dict[str, Any]]:
return self.store.load_loop_state()
def get_metrics(self) -> Optional[Dict[str, Any]]:
# opcional, por ahora puede devolver None
return None
# -----------------------------
# Equity
# -----------------------------
def get_equity_state(self) -> Dict[str, Any]:
"""
Estado actual de equity (último snapshot).
"""
broker = self.store.load_broker_snapshot() or {}
return {
"equity": broker.get("equity"),
"cash": broker.get("cash"),
"updated_at": broker.get("updated_at"),
}
def get_equity_curve(self, range: str) -> Dict[str, List]:
"""
Serie temporal de equity filtrada por rango.
"""
now = datetime.utcnow()
from_ts = None
if range == "1h":
from_ts = now - timedelta(hours=1)
elif range == "6h":
from_ts = now - timedelta(hours=6)
elif range == "24h":
from_ts = now - timedelta(hours=24)
elif range == "all":
from_ts = None
return self.store.load_equity_curve(from_ts=from_ts)
# -----------------------------
# Trades
# -----------------------------
def list_trades(
self,
limit: int = 200,
symbol: Optional[str] = None,
) -> List[Dict[str, Any]]:
return self.store.load_trades(limit=limit)

View File

@@ -1,39 +0,0 @@
# src/web/api/routers/bot.py
from fastapi import APIRouter, Depends
from datetime import datetime, timezone
from src.web.api.deps import get_provider
from src.web.api.providers.base import StateProvider
from src.web.api.settings import settings
router = APIRouter(prefix="/bot", tags=["bot"])
def _parse_iso(ts: str) -> datetime:
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
@router.get("/status")
def bot_status(provider: StateProvider = Depends(get_provider)):
broker = provider.get_broker()
loop = provider.get_loop()
if not broker:
return {
"state": "INIT",
"heartbeat_ts": None,
"last_ts": loop.get("last_ts") if loop else None,
}
heartbeat_ts = broker.get("updated_at")
age = (
datetime.now(timezone.utc) - _parse_iso(heartbeat_ts)
).total_seconds()
state = "RUNNING" if age <= settings.heartbeat_stale_seconds else "ERROR"
return {
"state": state,
"heartbeat_ts": heartbeat_ts,
"last_ts": loop.get("last_ts") if loop else None,
}

View File

@@ -1,41 +0,0 @@
# src/web/api/routers/equity.py
from fastapi import APIRouter, Depends, Query
from src.web.api.v1.deps import get_provider
from src.web.api.v1.providers.base import StateProvider
router = APIRouter(prefix="/equity", tags=["equity"])
# --------------------------------------------------
# Equity state (KPIs)
# --------------------------------------------------
@router.get("/state")
def equity_state(provider: StateProvider = Depends(get_provider)):
broker = provider.get_broker() or {}
equity = broker.get("equity")
cash = broker.get("cash")
realized_pnl = broker.get("realized_pnl")
updated_at = broker.get("updated_at")
return {
"cash": cash,
"equity": equity,
"realized_pnl": realized_pnl,
"updated_at": updated_at,
}
# --------------------------------------------------
# Equity curve (TIME SERIES)
# --------------------------------------------------
@router.get("/curve")
def equity_curve(
range: str = Query("all", pattern="^(1h|6h|24h|all)$"),
provider: StateProvider = Depends(get_provider),
):
"""
Devuelve la equity curve filtrada por rango temporal.
"""
return provider.get_equity_curve(range=range)

View File

@@ -1,40 +0,0 @@
# src/web/api/routers/events.py
from fastapi import APIRouter, Query
from pathlib import Path
from collections import deque
router = APIRouter(prefix="/events", tags=["events"])
def _tail_file(path: Path, n: int) -> list[str]:
if not path.exists():
return []
dq = deque(maxlen=n)
with path.open("r", encoding="utf-8", errors="ignore") as f:
for line in f:
dq.append(line.rstrip())
return list(dq)
@router.get("")
def events(
limit: int = Query(200, ge=1, le=2000),
kind: str = Query("trading", pattern="^(trading|errors)$"),
):
logs_dir = Path("logs")
pattern = "trading_bot_*.log" if kind == "trading" else "errors_*.log"
files = sorted(
logs_dir.glob(pattern),
key=lambda p: p.stat().st_mtime,
reverse=True,
)
if not files:
return {"items": []}
lines = _tail_file(files[0], limit)
return {
"items": lines,
"file": files[0].name,
}

View File

@@ -1,20 +0,0 @@
# src/web/api/routers/heallth.py
from fastapi import APIRouter, Depends
from src.web.api.deps import get_provider
from src.web.api.providers.base import StateProvider
router = APIRouter(tags=["health"])
@router.get("/health")
def health(provider: StateProvider = Depends(get_provider)):
broker = provider.get_broker()
loop = provider.get_loop()
metrics = provider.get_metrics()
return {
"ok": True,
"has_broker": broker is not None,
"has_loop": loop is not None,
"has_metrics": metrics is not None,
}

View File

@@ -1,11 +0,0 @@
# src/web/api/routers/metrics.py
from fastapi import APIRouter, Depends
from src.web.api.deps import get_provider
from src.web.api.providers.base import StateProvider
router = APIRouter(prefix="/metrics", tags=["metrics"])
@router.get("")
def metrics(provider: StateProvider = Depends(get_provider)):
return provider.get_metrics() or {}

View File

@@ -1,25 +0,0 @@
# src/web/api/routers/positions.py
# src/web/api/routers/positions.py
from fastapi import APIRouter, Depends
from src.web.api.deps import get_provider
from src.web.api.providers.base import StateProvider
router = APIRouter(prefix="/positions", tags=["positions"])
@router.get("")
def positions(provider: StateProvider = Depends(get_provider)):
broker = provider.get_broker() or {}
positions = broker.get("positions") or {}
out = []
for _, p in positions.items():
qty = float(p.get("qty", 0.0))
if qty > 0:
out.append(p)
return {
"items": out,
"updated_at": broker.get("updated_at"),
}

View File

@@ -1,21 +0,0 @@
# src/web/api/routers/trades.py
from fastapi import APIRouter, Depends, Query
from typing import Optional
from src.web.api.deps import get_provider
from src.web.api.providers.base import StateProvider
router = APIRouter(prefix="/trades", tags=["trades"])
@router.get("")
def list_trades(
limit: int = Query(200, ge=1, le=5000),
symbol: Optional[str] = None,
provider: StateProvider = Depends(get_provider),
):
trades = provider.list_trades(limit=limit, symbol=symbol)
return {
"items": trades,
"count": len(trades),
}

View File

@@ -1,47 +0,0 @@
# src/web/api/settings.py
from pydantic import BaseModel
from pathlib import Path
import os
PROJECT_ROOT = Path(__file__).resolve().parents[3]
# src/web/api/settings.py → api → web → src → trading-bot
class Settings(BaseModel):
# --------------------------------------------------
# API
# --------------------------------------------------
api_prefix: str = "/api/v1"
api_title: str = "Trading Bot API"
api_version: str = "1.0.0"
# --------------------------------------------------
# Data source
# --------------------------------------------------
state_db_path: Path = PROJECT_ROOT / "data/paper_trading/state.db"
# --------------------------------------------------
# Runtime behaviour
# --------------------------------------------------
heartbeat_stale_seconds: int = 180 # si no hay heartbeat → ERROR
mock_mode: bool = False
def load_settings() -> Settings:
"""
Carga settings desde ENV sin romper defaults.
"""
return Settings(
state_db_path=Path(
os.getenv("STATE_DB_PATH",
PROJECT_ROOT / "data/paper_trading/state.db"
)
),
heartbeat_stale_seconds=int(
os.getenv("HEARTBEAT_STALE_SECONDS", 180)
),
mock_mode=os.getenv("MOCK_MODE", "false").lower() == "true",
)
settings = load_settings()

View File

View File

@@ -68,6 +68,8 @@ class StrategyRunResultSchema(BaseModel):
oos_max_dd_worst_pct: float oos_max_dd_worst_pct: float
degradation_sharpe: Optional[float] = None degradation_sharpe: Optional[float] = None
diagnostics: Optional[Dict[str, Any]] = None
windows: List[WindowRowSchema] windows: List[WindowRowSchema]

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
const API_BASE = "/api/v1";
async function apiGet(path) {
const res = await fetch(`${API_BASE}${path}`);
if (!res.ok) throw new Error(res.statusText);
return res.json();
}

View File

@@ -1,306 +0,0 @@
let equityChart = null;
let showDrawdown = true;
let currentRange = "all";
// --------------------------------------------------
// STATUS
// --------------------------------------------------
async function updateStatus() {
const data = await apiGet("/bot/status");
const el = document.getElementById("bot-status");
el.textContent = data.state;
el.className =
"ms-auto badge " +
(data.state === "RUNNING"
? "bg-green"
: data.state === "ERROR"
? "bg-red"
: "bg-secondary");
}
// --------------------------------------------------
// KPI
// --------------------------------------------------
async function updateEquity() {
const data = await apiGet("/equity/state");
document.getElementById("kpi-equity").textContent =
data.equity?.toFixed(2) ?? "—";
document.getElementById("kpi-pnl").textContent =
data.realized_pnl?.toFixed(2) ?? "—";
}
async function fetchTrades() {
const data = await apiGet("/trades?limit=500");
return data.items || [];
}
// --------------------------------------------------
// MAIN CHART
// --------------------------------------------------
async function updateCurve() {
const data = await apiGet(`/equity/curve?range=${currentRange}`);
const labels = data.timestamps;
const equity = data.equity;
const cash = data.cash;
// -----------------------------
// Max Equity (for drawdown)
// -----------------------------
const maxEquityCurve = [];
let runningMax = -Infinity;
for (let i = 0; i < equity.length; i++) {
runningMax = Math.max(runningMax, equity[i]);
maxEquityCurve.push(runningMax);
}
// -----------------------------
// Max Drawdown KPI
// -----------------------------
let maxDD = 0;
for (let i = 0; i < equity.length; i++) {
const dd = (equity[i] / maxEquityCurve[i] - 1) * 100;
maxDD = Math.min(maxDD, dd);
}
const elDD = document.getElementById("kpi-max-dd");
if (elDD) elDD.textContent = `${maxDD.toFixed(2)} %`;
// -----------------------------
// Trades → markers
// -----------------------------
const trades = await fetchTrades();
const buyPoints = [];
const sellPoints = [];
const minEquity = Math.min(...equity);
const maxEquity = Math.max(...equity);
const offset = Math.max((maxEquity - minEquity) * 0.05, 350);
trades.forEach(t => {
if (!t.timestamp || !t.side) return;
const tradeTs = new Date(t.timestamp).getTime();
let idx = labels.length - 1;
for (let i = labels.length - 1; i >= 0; i--) {
if (new Date(labels[i]).getTime() <= tradeTs) {
idx = i;
break;
}
}
const y = equity[idx];
if (y == null) return;
if (t.side === "BUY") {
buyPoints.push({ x: labels[idx], y: y - offset, trade: t });
}
if (t.side === "SELL" || t.side === "CLOSE") {
sellPoints.push({ x: labels[idx], y: y + offset, trade: t });
}
});
// --------------------------------------------------
// INIT CHART
// --------------------------------------------------
if (!equityChart) {
const ctx = document.getElementById("equityChart").getContext("2d");
equityChart = new Chart(ctx, {
type: "line",
data: {
labels,
datasets: [
{
label: "Max Equity",
data: maxEquityCurve,
borderColor: "rgba(0,0,0,0)",
pointRadius: 0
},
{
label: "Equity",
data: equity,
borderColor: "#206bc4",
backgroundColor: "rgba(214,57,57,0.15)",
pointRadius: 0,
fill: {
target: 0,
above: "rgba(0,0,0,0)",
below: "rgba(214,57,57,0.15)"
}
},
{
label: "Cash",
data: cash,
borderColor: "#2fb344",
borderDash: [5, 5],
pointRadius: 0
},
{
type: "scatter",
label: "BUY",
data: buyPoints,
pointStyle: "triangle",
pointRotation: 0,
pointRadius: 6,
backgroundColor: "#2fb344"
},
{
type: "scatter",
label: "SELL",
data: sellPoints,
pointStyle: "triangle",
pointRotation: 180,
pointRadius: 6,
backgroundColor: "#d63939"
}
]
},
options: {
responsive: true,
animation: false,
// -----------------------------
// SOLUCIÓN 1: TIME SCALE
// -----------------------------
scales: {
x: {
type: "time",
time: {
tooltipFormat: "yyyy-MM-dd HH:mm",
displayFormats: {
minute: "HH:mm",
hour: "HH:mm",
day: "MMM dd"
}
}
}
},
// -----------------------------
// SOLUCIÓN 2: ZOOM + PAN
// -----------------------------
plugins: {
zoom: {
limits: {
x: { min: "original", max: "original" }
},
pan: {
enabled: true,
mode: "x",
modifierKey: "ctrl"
},
zoom: {
wheel: { enabled: true },
pinch: { enabled: true },
mode: "x"
}
},
// -----------------------------
// SOLUCIÓN 3: DECIMATION
// -----------------------------
decimation: {
enabled: true,
algorithm: "lttb",
samples: 500
},
// -----------------------------
// LEYENDA
// -----------------------------
legend: {
display: true,
labels: {
usePointStyle: true,
generateLabels(chart) {
return chart.data.datasets
// ❌ fuera Max Equity
.map((ds, i) => ({ ds, i }))
.filter(({ ds }) => ds.label !== "Max Equity")
.map(({ ds, i }) => {
const isScatter = ds.type === "scatter";
return {
text: ds.label,
datasetIndex: i,
hidden: !chart.isDatasetVisible(i),
// 🎨 colores reales del dataset
fillStyle: isScatter ? ds.backgroundColor : ds.borderColor,
strokeStyle: ds.borderColor,
// 📏 línea vs punto
lineWidth: isScatter ? 0 : 3,
borderDash: ds.borderDash || [],
// 🔺 BUY / SELL = triángulos reales
pointStyle: isScatter ? ds.pointStyle : "line",
rotation: isScatter ? ds.pointRotation || 0 : 0,
};
});
}
}
}
}
}
});
} else {
equityChart.data.labels = labels;
equityChart.data.datasets[0].data = maxEquityCurve;
equityChart.data.datasets[1].data = equity;
equityChart.data.datasets[2].data = cash;
equityChart.data.datasets[3].data = buyPoints;
equityChart.data.datasets[4].data = sellPoints;
equityChart.update("none");
}
}
// --------------------------------------------------
// SOLUCIÓN 4: TIME WINDOWS
// --------------------------------------------------
function setTimeWindow(hours) {
if (!equityChart) return;
if (hours === "ALL") {
equityChart.options.scales.x.min = undefined;
equityChart.options.scales.x.max = undefined;
} else {
const now = Date.now();
equityChart.options.scales.x.min = now - hours * 3600_000;
equityChart.options.scales.x.max = now;
}
equityChart.update();
}
async function updateEvents() {
const data = await apiGet("/events?limit=20");
document.getElementById("events-log").textContent =
data.items.join("\n");
}
// --------------------------------------------------
// UI
// --------------------------------------------------
document.getElementById("reset-zoom")?.addEventListener("click", () => {
equityChart?.resetZoom();
});
document.getElementById("toggle-dd")?.addEventListener("change", e => {
showDrawdown = e.target.checked;
equityChart.data.datasets[1].fill = showDrawdown
? { target: 0, above: "rgba(0,0,0,0)", below: "rgba(214,57,57,0.15)" }
: false;
equityChart.update("none");
});
// --------------------------------------------------
poll(updateStatus, 2000);
poll(updateEquity, 5000);
poll(updateCurve, 10000);
poll(updateEvents, 10000);

View File

@@ -1,4 +0,0 @@
function poll(fn, interval) {
fn();
return setInterval(fn, interval);
}

View File

@@ -1,37 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}Trading Bot{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="/static/css/tabler.min.css" rel="stylesheet">
</head>
<body>
<div class="page">
<header class="navbar navbar-expand-md navbar-light d-print-none">
<div class="container-xl">
<span class="navbar-brand">
🤖 Trading Bot
</span>
<div id="bot-status" class="ms-auto badge bg-secondary">
INIT
</div>
</div>
</header>
<div class="page-wrapper">
<div class="container-xl">
{% block content %}{% endblock %}
</div>
</div>
</div>
<script src="/static/js/api.js"></script>
<script src="/static/js/polling.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -1,76 +0,0 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<div class="row row-deck row-cards">
<!-- KPIs -->
<div class="col-sm-6 col-lg-3">
<div class="card"><div class="card-body">
<div class="subheader">Equity</div>
<div id="kpi-equity" class="h1"></div>
</div></div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card"><div class="card-body">
<div class="subheader">PnL</div>
<div id="kpi-pnl" class="h1"></div>
</div></div>
</div>
<div class="col-sm-6 col-lg-3">
<div class="card"><div class="card-body">
<div class="subheader">Max DD</div>
<div id="kpi-max-dd" class="h1"></div>
</div></div>
</div>
<!-- CHART -->
<div class="col-12">
<div class="card position-relative">
<div class="card-header">
<h3 class="card-title">Equity Curve</h3>
<div class="btn-group position-absolute top-0 end-0 m-2">
<button class="btn btn-sm btn-outline-secondary" onclick="setTimeWindow(1)">1h</button>
<button class="btn btn-sm btn-outline-secondary" onclick="setTimeWindow(6)">6h</button>
<button class="btn btn-sm btn-outline-secondary" onclick="setTimeWindow(24)">24h</button>
<button class="btn btn-sm btn-outline-secondary" onclick="setTimeWindow('ALL')">ALL</button>
<button id="reset-zoom" class="btn btn-sm btn-outline-secondary">Reset</button>
</div>
<div class="form-check form-switch position-absolute top-20 start-50 m-2">
<input class="form-check-input" type="checkbox" id="toggle-dd" checked>
<label class="form-check-label">Drawdown</label>
</div>
</div>
<div class="card-body">
<canvas id="equityChart" height="120"></canvas>
</div>
</div>
</div>
<!-- EVENTS -->
<div class="col-12">
<div class="card">
<div class="card-header"><h3 class="card-title">Events</h3></div>
<div class="card-body">
<pre id="events-log" style="max-height:200px; overflow:auto;"></pre>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- ✅ Time adapter necesario para scales.x.type="time" -->
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3"></script>
<!-- ✅ Zoom plugin -->
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1"></script>
<!-- Tu dashboard -->
<script src="/static/js/dashboard.js"></script>
{% endblock %}

View File

@@ -728,6 +728,179 @@ async function pollStatus(jobId) {
}, 1000); }, 1000);
} }
// =================================================
// DIFFERENT RENDER PLOTS
// =================================================
function renderEquityAndReturns(strategyId, s, data) {
console.log("Plotly object:", Plotly); // Esto debería mostrarte el objeto Plotly completo
if (Plotly && Plotly.subplots) {
console.log("make_subplots is available");
} else {
console.error("make_subplots is NOT available");
}
// Crea el subplot con dos filas y una columna (1x2)
var fig = Plotly.subplots.make_subplots({
rows: 2, // 2 filas
cols: 1, // 1 columna
shared_xaxes: true, // Compartir el eje X entre ambos gráficos
vertical_spacing: 0.1, // Espacio entre los gráficos
subplot_titles: [`Equity — ${strategyId}`, `Returns & Trades — ${strategyId}`], // Títulos para cada subgráfico
column_widths: [0.7] // Ajustar el ancho de las columnas si es necesario
});
// Datos de Equity
const equityTrace = {
y: s.window_equity, // Datos de la equity
type: "scatter", // Tipo de gráfico: línea
mode: "lines", // Modo: línea
name: "Equity"
};
// Datos de Return %
const returnsTrace = {
y: s.window_returns_pct || [], // Datos de returns
type: "bar", // Tipo de gráfico: barra
name: "Return %",
marker: { color: "#3b82f6" }, // Color de la barra
yaxis: "y1", // Asociar al primer eje Y
};
// Datos de Trades
const tradesTrace = {
y: s.window_trades || [], // Datos de trades
type: "bar", // Tipo de gráfico: barra
name: "Trades",
marker: { color: "#f59e0b" }, // Color de la barra
yaxis: "y2", // Asociar al segundo eje Y
};
// Agregar la traza de "Equity" al subplot (fila 1, columna 1)
fig.addTrace(equityTrace, 1, 1);
// Agregar las trazas de "Returns" y "Trades" al subplot (fila 2, columna 1)
fig.addTrace(returnsTrace, 2, 1);
fig.addTrace(tradesTrace, 2, 1);
// Configurar los ejes y los márgenes
fig.update_layout({
title: `Strategy Overview — ${strategyId}`,
yaxis: {
title: "Equity",
showgrid: true
},
yaxis2: {
title: "Return % / Trades",
overlaying: "y",
side: "right",
showgrid: true
},
xaxis: {
title: "Windows",
showgrid: true
},
showlegend: true
});
// Renderizar en el contenedor del gráfico
Plotly.newPlot("plot_strategy", fig);
}
function renderRollingSharpe(strategyId, s, data) {
const roll = s.diagnostics?.rolling?.rolling_sharpe_like || [];
const x = roll.map((_, i) => i + 1);
Plotly.newPlot("plot_strategy", [{
x,
y: roll,
type: "scatter",
mode: "lines+markers",
name: `Rolling Sharpe-like (k=${s.diagnostics?.rolling?.rolling_window ?? "?"})`
}], {
margin: { t: 40 },
title: `Rolling Sharpe-like — ${strategyId}`,
xaxis: { title: "Window" },
yaxis: { title: "Sharpe-like" }
});
}
function renderOOSReturnsDistribution(strategyId, s, data) {
const edges = s.diagnostics?.distribution?.hist_bin_edges || [];
const counts = s.diagnostics?.distribution?.hist_counts || [];
const centers = edges.map((_, i) => (edges[i] + edges[i + 1]) / 2);
Plotly.newPlot("plot_strategy", [{
x: centers,
y: counts,
type: "bar",
name: "OOS Return% (bins)"
}], {
margin: { t: 40 },
title: `OOS Returns Distribution — ${strategyId}`,
xaxis: { title: "Return % (window)" },
yaxis: { title: "Count" }
});
}
function renderDrawdownEvolution(strategyId, s, data) {
const dd = s.diagnostics?.drawdown?.drawdown_pct || [];
const x = dd.map((_, i) => i);
Plotly.newPlot("plot_strategy", [{
x,
y: dd,
type: "scatter",
mode: "lines",
name: "Drawdown %"
}], {
margin: { t: 40 },
title: `Drawdown Evolution — ${strategyId}`,
xaxis: { title: "Point (initial + windows)" },
yaxis: { title: "Drawdown %", zeroline: true }
});
}
function renderTradeDensity(strategyId, s, data) {
const tpw = s.diagnostics?.trades?.trades_per_window || [];
const tpd = s.diagnostics?.trades?.trades_per_day || [];
const x = tpw.map((_, i) => i + 1);
Plotly.newPlot("plot_strategy", [
{
x,
y: tpw,
type: "bar",
name: "Trades / window",
yaxis: "y1"
},
{
x,
y: tpd,
type: "scatter",
mode: "lines+markers",
name: "Trades / day",
yaxis: "y2"
}
], {
margin: { t: 40 },
title: `Trade Density — ${strategyId}`,
barmode: "group",
xaxis: { title: "Window" },
yaxis: { title: "Trades / window" },
yaxis2: {
title: "Trades / day",
overlaying: "y",
side: "right",
zeroline: false
}
});
}
// ================================================= // =================================================
// RENDER RESULTS // RENDER RESULTS
// ================================================= // =================================================
@@ -854,6 +1027,7 @@ function populatePlotSelector(data) {
function selectStrategy(strategyId, data) { function selectStrategy(strategyId, data) {
if (!strategyId || !data) return; if (!strategyId || !data) return;
// Actualiza selectedStrategyId
selectedStrategyId = strategyId; selectedStrategyId = strategyId;
const row = (data.results || []).find(r => r.strategy_id === strategyId); const row = (data.results || []).find(r => r.strategy_id === strategyId);
@@ -880,35 +1054,26 @@ function selectStrategy(strategyId, data) {
return; return;
} }
// 3) Renderizar serie si existe // 3) Verificar si los datos de la estrategia están disponibles
const s = data?.series?.strategies?.[strategyId]; const strategyData = data?.series?.strategies?.[selectedStrategyId];
if (!s) {
// fallback explícito (por si backend antiguo no manda series_available) if (!strategyData) {
showPlotAlert( showPlotAlert(
row?.status === "fail" ? "danger" : "warning", "warning",
`${(row?.status || "warning").toUpperCase()}${strategyId}`, `No data available${strategyId}`,
row?.message || "No chart series available for this strategy.", "Strategy data not available for rendering.",
row?.warnings []
); );
clearPlots(); clearPlots();
highlightSelectedRow(strategyId); highlightSelectedRow(strategyId);
return; return;
} }
renderStrategyCharts(strategyId, s, data); // 4) Mantén el gráfico previamente seleccionado en el dropdown
highlightSelectedRow(strategyId); const chartType = document.getElementById("plot_strategy_select").value;
renderChart(chartType, selectedStrategyId, strategyData, data); // Renderiza el gráfico correctamente
// 4) Caso “serie vacía” (opción B) -> warning explícito (aunque ya lo tengas) highlightSelectedRow(strategyId);
const trd = s.window_trades || [];
const hasTrades = Array.isArray(trd) && trd.some(v => (v ?? 0) > 0);
if (!hasTrades && row?.status !== "fail") {
showPlotAlert(
"warning",
`NO TRADES — ${strategyId}`,
"Walk-forward produced no closed trades in OOS. Charts may be flat/empty.",
row?.warnings
);
}
} }
function renderValidateResponse(data) { function renderValidateResponse(data) {
@@ -1011,115 +1176,27 @@ function renderValidateResponse(data) {
} }
} }
function renderStrategyCharts(strategyId, s, data) { function renderChart(chartType, strategyId, s, data) {
switch (chartType) {
if (!s) return case "equity":
renderEquityAndReturns(strategyId, s, data);
// ============================ break;
// 1⃣ EQUITY case "rolling_sharpe":
// ============================ renderRollingSharpe(strategyId, s, data);
break;
Plotly.newPlot("plot_equity", [{ case "hist_oos_returns":
y: s.window_equity, renderOOSReturnsDistribution(strategyId, s, data);
type: "scatter", break;
mode: "lines", case "drawdown":
name: "Equity" renderDrawdownEvolution(strategyId, s, data);
}], { break;
margin: { t: 20 }, case "trade_density":
title: `Equity — ${strategyId}` renderTradeDensity(strategyId, s, data);
}); break;
default:
// ============================ renderEquityAndReturns(strategyId, s, data);
// 2⃣ RETURNS + TRADES break;
// ============================
const ret = s.window_returns_pct || [];
const trd = s.window_trades || [];
const retMax = Math.max(0, ...ret);
const retMin = Math.min(0, ...ret);
const minTrades = data.config?.wf?.min_trades_test ?? 10;
const trdMaxRaw = Math.max(0, ...trd);
const trdMax = Math.max(trdMaxRaw, minTrades);
// Evitar divisiones raras
const retPosSpan = Math.max(1e-9, retMax);
const retNegSpan = Math.abs(retMin);
// Alinear el 0 visualmente
const trdNegSpan = (retNegSpan / retPosSpan) * trdMax;
const y1Range = [retMin, retMax];
const y2Range = [-trdNegSpan, trdMax];
Plotly.newPlot("plot_returns", [
{
y: ret,
type: "bar",
name: "Return %",
marker: { color: "#3b82f6" },
yaxis: "y1",
offsetgroup: "returns",
alignmentgroup: "group1"
},
{
y: trd,
type: "bar",
name: "Trades",
marker: { color: "#f59e0b" },
yaxis: "y2",
offsetgroup: "trades",
alignmentgroup: "group1"
} }
], {
margin: { t: 20 },
barmode: "group",
yaxis: {
title: "Return %",
range: y1Range,
zeroline: true,
zerolinewidth: 2
},
yaxis2: {
title: "Trades",
overlaying: "y",
side: "right",
range: y2Range,
zeroline: true,
zerolinewidth: 2
},
shapes: [
{
type: "line",
x0: -0.5,
x1: trd.length - 0.5,
y0: minTrades,
y1: minTrades,
yref: "y2",
line: {
color: "red",
width: 2,
dash: "dash"
}
}
],
legend: {
orientation: "h"
},
title: `Returns & Trades — ${strategyId}`
});
} }
function highlightSelectedRow(strategyId) { function highlightSelectedRow(strategyId) {
@@ -1310,6 +1387,17 @@ async function init() {
wireButtons(); wireButtons();
document.getElementById("plot_strategy_select").addEventListener("change", function() {
const chartType = this.value;
const strategyData = lastValidationResult.series.strategies[selectedStrategyId];
// Verifica que selectedStrategyId tenga el valor correcto
console.log("selectedStrategyId:", selectedStrategyId);
console.log("Strategy Data:", strategyData);
renderChart(chartType, selectedStrategyId, strategyData, lastValidationResult);
});
const strategies = await fetchAvailableStrategies(); const strategies = await fetchAvailableStrategies();
renderStrategiesList(strategies); renderStrategiesList(strategies);
@@ -1329,7 +1417,7 @@ function ensurePlotAlertContainer() {
let el = document.getElementById("plot_alert"); let el = document.getElementById("plot_alert");
if (el) return el; if (el) return el;
const anchor = document.getElementById("plot_equity"); const anchor = document.getElementById("plot_strategy");
if (!anchor || !anchor.parentElement) return null; if (!anchor || !anchor.parentElement) return null;
el = document.createElement("div"); el = document.createElement("div");
@@ -1373,10 +1461,8 @@ function clearPlotAlert() {
} }
function clearPlots() { function clearPlots() {
const eq = document.getElementById("plot_equity"); const eq = document.getElementById("plot_strategy");
const ret = document.getElementById("plot_returns");
if (eq) eq.innerHTML = ""; if (eq) eq.innerHTML = "";
if (ret) ret.innerHTML = "";
} }
document.getElementById("lock_inherited") document.getElementById("lock_inherited")

File diff suppressed because one or more lines are too long

View File

@@ -306,15 +306,18 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Strategy plot</label> <label class="form-label">Strategy plot</label>
<select id="plot_strategy_select" class="form-select"></select> <select id="plot_strategy_select" class="form-select">
<option value="equity">Equity + Returns & Trades</option>
<option value="rolling_sharpe">Rolling Sharpe-like</option>
<option value="hist_oos_returns">OOS Returns Distribution</option>
<option value="drawdown">Drawdown Evolution</option>
<option value="trade_density">Trade Density</option>
</select>
</div> </div>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<div id="plot_equity" style="height: 320px;"></div> <div id="plot_strategy" style="height: 320px;"></div>
</div>
<div class="mt-3">
<div id="plot_returns" style="height: 320px;"></div>
</div> </div>
<hr class="my-4"> <hr class="my-4">
@@ -345,6 +348,6 @@
</div> </div>
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script> <script src="/static/js/plotly.js"></script>
<script src="/static/js/pages/calibration_strategies.js"></script> <script src="/static/js/pages/calibration_strategies.js"></script>
{% endblock %} {% endblock %}

View File

@@ -1,366 +0,0 @@
{% 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 -->
<!-- ========================= -->
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h3 class="card-title mb-0">Risk & Stops(Step 2)</h3>
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" id="lock_inherited" checked>
<label class="form-check-label" for="lock_inherited">
Bloquear parámetros heredados
</label>
</div>
</div>
<div class="card-body">
<!-- ================= -->
<!-- Risk Configuration -->
<!-- ================= -->
<h4 class="mb-3">Risk Configuration</h4>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label">Risk per Trade (%)</label>
<input id="risk_fraction" class="form-control inherited-field" type="number" step="0.01">
</div>
<div class="col-md-4">
<label class="form-label">Max Position Size (%)</label>
<input id="max_position_fraction" class="form-control inherited-field" type="number" step="0.1">
</div>
</div>
<!-- ================= -->
<!-- Stop Configuration -->
<!-- ================= -->
<h4 class="mb-3">Stop Configuration</h4>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label">Stop Type</label>
<select id="stop_type" class="form-select inherited-field">
<option value="fixed">fixed</option>
<option value="trailing">trailing</option>
<option value="atr">atr</option>
</select>
</div>
<div id="stop_fraction_group" class="col-md-4">
<label class="form-label">Stop fraction (%)</label>
<input id="stop_fraction" class="form-control inherited-field" type="number" step="0.01">
</div>
<div id="atr_group" class="col-md-4 d-none">
<label class="form-label">ATR period</label>
<input id="atr_period" class="form-control inherited-field" type="number">
</div>
<div id="atr_multiplier_group" class="col-md-4 d-none">
<label class="form-label">ATR multiplier</label>
<input id="atr_multiplier" class="form-control inherited-field" type="number" step="0.1">
</div>
</div>
<!-- ================= -->
<!-- Global Rules -->
<!-- ================= -->
<h4 class="mb-3">Global Rules</h4>
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label">Max Drawdown (%)</label>
<input id="max_drawdown_pct" class="form-control inherited-field" type="number" step="0.1">
</div>
</div>
<!-- ================= -->
<!-- Optional Parameters -->
<!-- ================= -->
<h4 class="mb-3">Optional Parameters</h4>
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Daily loss limit (%)</label>
<input id="daily_loss_limit_pct" class="form-control optional-field" type="number" step="0.1">
</div>
<div class="col-md-4">
<label class="form-label">Max consecutive losses</label>
<input id="max_consecutive_losses" class="form-control optional-field" type="number">
</div>
<div class="col-md-4">
<label class="form-label">Cooldown bars</label>
<input id="cooldown_bars" class="form-control optional-field" type="number">
</div>
</div>
</div>
</div>
<!-- ========================= -->
<!-- WF + Optimizer config -->
<!-- ========================= -->
<div class="card mb-4">
<div class="card-header">
<h3 class="card-title">Walk-Forward & Optimization</h3>
</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_container" class="d-flex flex-column gap-4"></div>
<div class="card p-3">
<div class="d-flex justify-content-between">
<strong>Total combinations</strong>
<span id="combination_counter">0</span>
</div>
</div>
<div class="mt-2 text-end">
<small class="text-muted">
Estimated WF time:
<span id="wf_time_estimate">~ 0 sec</span>
</small>
</div>
<div class="mt-3 text-secondary">
Cada estrategia incluye un <b>param_grid</b> en JSON.
</div>
</div>
</div>
<!-- ========================= -->
<!-- Actions -->
<!-- ========================= -->
<div class="d-flex gap-2 mb-4">
<button id="validate_strategies_btn" class="btn btn-primary">
Validate (WF)
</button>
<button id="report_strategies_btn" class="btn btn-outline-primary">
Generate PDF report
</button>
</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 %}