Step 2: Risk and Stops - Generating reports with pdf. It is missing to reorder the PDF.
This commit is contained in:
@@ -10,6 +10,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
|
||||
|
||||
# --------------------------------------------------
|
||||
# Logging
|
||||
@@ -91,12 +92,25 @@ def create_app() -> FastAPI:
|
||||
"step": 1,
|
||||
},
|
||||
)
|
||||
|
||||
@app.get("/calibration/risk", response_class=HTMLResponse)
|
||||
def calibration_risk_page(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
"pages/calibration/calibration_risk.html",
|
||||
{
|
||||
"request": request,
|
||||
"page": "calibration",
|
||||
"step": 2,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# API routers (versionados)
|
||||
# --------------------------------------------------
|
||||
api_prefix = settings.api_prefix
|
||||
app.include_router(calibration_data_router, prefix=api_prefix)
|
||||
app.include_router(calibration_risk_router, prefix=api_prefix)
|
||||
|
||||
return app
|
||||
|
||||
|
||||
285
src/web/api/v2/routers/calibration_risk.py
Normal file
285
src/web/api/v2/routers/calibration_risk.py
Normal file
@@ -0,0 +1,285 @@
|
||||
# src/web/api/v2/routers/calibration_risk.py
|
||||
|
||||
import logging
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import FileResponse
|
||||
from pathlib import Path
|
||||
import uuid
|
||||
|
||||
from src.data.storage import StorageManager
|
||||
from src.calibration.risk_inspector import (
|
||||
inspect_risk_config,
|
||||
StopConfig,
|
||||
RiskConfig,
|
||||
GlobalRiskRules,
|
||||
)
|
||||
|
||||
from ..schemas.calibration_risk import (
|
||||
CalibrationRiskInspectRequest,
|
||||
CalibrationRiskInspectResponse,
|
||||
CalibrationRiskValidateResponse,
|
||||
)
|
||||
|
||||
from src.calibration.reports.risk_report import generate_risk_report_pdf
|
||||
|
||||
logger = logging.getLogger("tradingbot.api.v2")
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/calibration/risk",
|
||||
tags=["calibration"],
|
||||
)
|
||||
|
||||
# =================================================
|
||||
# Dependencies
|
||||
# =================================================
|
||||
|
||||
def get_storage() -> StorageManager:
|
||||
return StorageManager.from_env()
|
||||
|
||||
|
||||
# =================================================
|
||||
# INSPECT (RISK & STOPS)
|
||||
# =================================================
|
||||
|
||||
@router.post(
|
||||
"/inspect",
|
||||
response_model=CalibrationRiskInspectResponse,
|
||||
)
|
||||
def inspect_calibration_risk(
|
||||
payload: CalibrationRiskInspectRequest,
|
||||
storage: StorageManager = Depends(get_storage),
|
||||
):
|
||||
logger.info(
|
||||
f"🛡️ Inspecting risk | {payload.symbol} {payload.timeframe}"
|
||||
)
|
||||
|
||||
# --------------------------------------------------
|
||||
# Step 1 dependency check
|
||||
# --------------------------------------------------
|
||||
# Por ahora: recalculamos data_quality igual que Step 1
|
||||
# (más adelante se puede cachear si quieres)
|
||||
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.",
|
||||
)
|
||||
|
||||
# reutilizamos la misma función que en calibration_data
|
||||
from .calibration_data import analyze_data_quality
|
||||
|
||||
data_quality = analyze_data_quality(df, payload.timeframe)
|
||||
|
||||
# --------------------------------------------------
|
||||
# Map schemas -> domain models
|
||||
# --------------------------------------------------
|
||||
stop = StopConfig(
|
||||
type=payload.stop.type,
|
||||
stop_fraction=payload.stop.stop_fraction,
|
||||
atr_period=payload.stop.atr_period,
|
||||
atr_multiplier=payload.stop.atr_multiplier,
|
||||
)
|
||||
|
||||
risk = RiskConfig(
|
||||
risk_fraction=payload.risk.risk_fraction,
|
||||
max_position_fraction=payload.risk.max_position_fraction,
|
||||
)
|
||||
|
||||
rules = GlobalRiskRules(
|
||||
max_drawdown_pct=payload.global_rules.max_drawdown_pct,
|
||||
daily_loss_limit_pct=payload.global_rules.daily_loss_limit_pct,
|
||||
max_consecutive_losses=payload.global_rules.max_consecutive_losses,
|
||||
cooldown_bars=payload.global_rules.cooldown_bars,
|
||||
)
|
||||
|
||||
# --------------------------------------------------
|
||||
# Domain inspection
|
||||
# --------------------------------------------------
|
||||
result = inspect_risk_config(
|
||||
storage=storage,
|
||||
symbol=payload.symbol,
|
||||
timeframe=payload.timeframe,
|
||||
stop=stop,
|
||||
risk=risk,
|
||||
rules=rules,
|
||||
account_equity=payload.account_equity,
|
||||
data_quality=data_quality,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"🧪 Risk inspect result | status={result['status']}"
|
||||
)
|
||||
|
||||
return CalibrationRiskInspectResponse(**result)
|
||||
|
||||
# =================================================
|
||||
# VALIDATE (RISK & STOPS — with series for plots)
|
||||
# =================================================
|
||||
|
||||
@router.post(
|
||||
"/validate",
|
||||
response_model=CalibrationRiskValidateResponse,
|
||||
)
|
||||
def validate_calibration_risk(
|
||||
payload: CalibrationRiskInspectRequest,
|
||||
storage: StorageManager = Depends(get_storage),
|
||||
):
|
||||
logger.info(
|
||||
f"📊 Validating risk (with series) | {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)
|
||||
|
||||
stop = StopConfig(
|
||||
type=payload.stop.type,
|
||||
stop_fraction=payload.stop.stop_fraction,
|
||||
atr_period=payload.stop.atr_period,
|
||||
atr_multiplier=payload.stop.atr_multiplier,
|
||||
)
|
||||
|
||||
risk = RiskConfig(
|
||||
risk_fraction=payload.risk.risk_fraction,
|
||||
max_position_fraction=payload.risk.max_position_fraction,
|
||||
)
|
||||
|
||||
rules = GlobalRiskRules(
|
||||
max_drawdown_pct=payload.global_rules.max_drawdown_pct,
|
||||
daily_loss_limit_pct=payload.global_rules.daily_loss_limit_pct,
|
||||
max_consecutive_losses=payload.global_rules.max_consecutive_losses,
|
||||
cooldown_bars=payload.global_rules.cooldown_bars,
|
||||
)
|
||||
|
||||
result = inspect_risk_config(
|
||||
storage=storage,
|
||||
symbol=payload.symbol,
|
||||
timeframe=payload.timeframe,
|
||||
stop=stop,
|
||||
risk=risk,
|
||||
rules=rules,
|
||||
account_equity=payload.account_equity,
|
||||
data_quality=data_quality,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"📈 Risk validate result | status={result['status']}"
|
||||
)
|
||||
|
||||
return CalibrationRiskValidateResponse(**result)
|
||||
|
||||
# =================================================
|
||||
# REPORT (PDF)
|
||||
# =================================================
|
||||
|
||||
@router.post("/report")
|
||||
def generate_risk_report(
|
||||
payload: CalibrationRiskInspectRequest,
|
||||
storage: StorageManager = Depends(get_storage),
|
||||
):
|
||||
logger.info(
|
||||
f"🧾 Generating risk 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)
|
||||
|
||||
stop = StopConfig(
|
||||
type=payload.stop.type,
|
||||
stop_fraction=payload.stop.stop_fraction,
|
||||
atr_period=payload.stop.atr_period,
|
||||
atr_multiplier=payload.stop.atr_multiplier,
|
||||
)
|
||||
|
||||
risk = RiskConfig(
|
||||
risk_fraction=payload.risk.risk_fraction,
|
||||
max_position_fraction=payload.risk.max_position_fraction,
|
||||
)
|
||||
|
||||
rules = GlobalRiskRules(
|
||||
max_drawdown_pct=payload.global_rules.max_drawdown_pct,
|
||||
daily_loss_limit_pct=payload.global_rules.daily_loss_limit_pct,
|
||||
max_consecutive_losses=payload.global_rules.max_consecutive_losses,
|
||||
cooldown_bars=payload.global_rules.cooldown_bars,
|
||||
)
|
||||
|
||||
result = inspect_risk_config(
|
||||
storage=storage,
|
||||
symbol=payload.symbol,
|
||||
timeframe=payload.timeframe,
|
||||
stop=stop,
|
||||
risk=risk,
|
||||
rules=rules,
|
||||
account_equity=payload.account_equity,
|
||||
data_quality=data_quality,
|
||||
)
|
||||
|
||||
# ---------------------------------------------
|
||||
# Prepare PDF data
|
||||
# ---------------------------------------------
|
||||
|
||||
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,
|
||||
"Max drawdown (%)": payload.global_rules.max_drawdown_pct * 100,
|
||||
}
|
||||
|
||||
results = result
|
||||
|
||||
# ---------------------------------------------
|
||||
# Generate file
|
||||
# ---------------------------------------------
|
||||
|
||||
reports_dir = Path("reports")
|
||||
reports_dir.mkdir(exist_ok=True)
|
||||
|
||||
filename = f"risk_report_{uuid.uuid4().hex}.pdf"
|
||||
output_path = reports_dir / filename
|
||||
|
||||
generate_risk_report_pdf(
|
||||
output_path=output_path,
|
||||
context=context,
|
||||
config=config,
|
||||
results=results,
|
||||
)
|
||||
|
||||
return FileResponse(
|
||||
path=str(output_path),
|
||||
media_type="application/pdf",
|
||||
filename=filename,
|
||||
)
|
||||
84
src/web/api/v2/schemas/calibration_risk.py
Normal file
84
src/web/api/v2/schemas/calibration_risk.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# src/web/api/v2/schemas/calibration_risk.py
|
||||
|
||||
from typing import Literal, Optional, Dict, Any
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
|
||||
|
||||
class StopConfigSchema(BaseModel):
|
||||
type: Literal["fixed", "trailing", "atr"]
|
||||
|
||||
stop_fraction: Optional[float] = Field(None, gt=0)
|
||||
atr_period: Optional[int] = Field(None, gt=1)
|
||||
atr_multiplier: Optional[float] = Field(None, gt=0)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_by_type(self):
|
||||
if self.type in ("fixed", "trailing"):
|
||||
if self.stop_fraction is None:
|
||||
raise ValueError(
|
||||
"stop_fraction required for fixed/trailing stop"
|
||||
)
|
||||
|
||||
if self.type == "atr":
|
||||
if self.atr_period is None:
|
||||
raise ValueError(
|
||||
"atr_period required for ATR stop"
|
||||
)
|
||||
if self.atr_multiplier is None:
|
||||
raise ValueError(
|
||||
"atr_multiplier required for ATR stop"
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
# class StopConfigSchema(BaseModel):
|
||||
# type: Literal["fixed", "trailing", "atr"]
|
||||
|
||||
# stop_fraction: Optional[float] = Field(None, gt=0)
|
||||
|
||||
# atr_period: Optional[int] = Field(None, gt=1)
|
||||
# atr_multiplier: Optional[float] = Field(None, gt=0)
|
||||
|
||||
|
||||
class RiskConfigSchema(BaseModel):
|
||||
risk_fraction: float = Field(..., gt=0, lt=1)
|
||||
max_position_fraction: float = Field(..., gt=0, lt=1)
|
||||
|
||||
|
||||
class GlobalRiskRulesSchema(BaseModel):
|
||||
max_drawdown_pct: float = Field(..., gt=0, lt=1)
|
||||
|
||||
daily_loss_limit_pct: Optional[float] = Field(None, gt=0, lt=1)
|
||||
max_consecutive_losses: Optional[int] = Field(None, gt=0)
|
||||
cooldown_bars: Optional[int] = Field(None, ge=0)
|
||||
|
||||
|
||||
class CalibrationRiskInspectRequest(BaseModel):
|
||||
symbol: str
|
||||
timeframe: str
|
||||
|
||||
stop: StopConfigSchema
|
||||
risk: RiskConfigSchema
|
||||
global_rules: GlobalRiskRulesSchema
|
||||
|
||||
account_equity: float = Field(..., gt=0)
|
||||
|
||||
|
||||
class CalibrationRiskInspectResponse(BaseModel):
|
||||
valid: bool
|
||||
status: Literal["ok", "warning", "fail"]
|
||||
|
||||
checks: Dict[str, Any]
|
||||
message: str
|
||||
|
||||
class CalibrationRiskValidateResponse(BaseModel):
|
||||
valid: bool
|
||||
status: Literal["ok", "warning", "fail"]
|
||||
|
||||
checks: Dict[str, Any]
|
||||
message: str
|
||||
|
||||
# ⬇️ NUEVO
|
||||
series: Dict[str, Any]
|
||||
@@ -0,0 +1,9 @@
|
||||
/* =================================================
|
||||
Wizard navigation (Calibration steps)
|
||||
================================================= */
|
||||
|
||||
a[aria-disabled="true"] {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ async function inspectCalibrationData() {
|
||||
resultEl.textContent = "⏳ Inspeccionando datos en DB...";
|
||||
}
|
||||
|
||||
// por si vienes de un estado previo OK y ahora inspeccionas otra cosa
|
||||
disableNextStep();
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/v2/calibration/data/inspect", {
|
||||
method: "POST",
|
||||
@@ -48,11 +51,22 @@ async function inspectCalibrationData() {
|
||||
|
||||
renderDataSummary(data);
|
||||
|
||||
// ✅ habilita el paso 2 si:
|
||||
// - valid = true
|
||||
// - data_quality no es FAIL (warnings dejan pasar)
|
||||
const dqStatus = data.data_quality?.status ?? "ok";
|
||||
if (data.valid && dqStatus !== "fail") {
|
||||
enableNextStep();
|
||||
localStorage.setItem("calibration.symbol", data.symbol);
|
||||
localStorage.setItem("calibration.timeframe", data.timeframe);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("[calibration_data] inspect FAILED", err);
|
||||
if (resultEl) {
|
||||
resultEl.textContent = "❌ Error inspeccionando datos";
|
||||
}
|
||||
disableNextStep();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,8 +188,28 @@ function renderDownloadProgress(job) {
|
||||
text.textContent = job.message || job.status;
|
||||
}
|
||||
|
||||
function enableNextStep() {
|
||||
const btn = document.getElementById("next-step-btn");
|
||||
if (!btn) return;
|
||||
|
||||
btn.style.display = "inline-flex";
|
||||
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.style.display = "inline-flex";
|
||||
btn.classList.remove("btn-outline-primary");
|
||||
btn.classList.add("btn-outline-secondary");
|
||||
btn.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// DATA SUMMARY (YA EXISTENTE)
|
||||
// DATA SUMMARY
|
||||
// =================================================
|
||||
|
||||
function renderDataSummary(data) {
|
||||
@@ -183,7 +217,7 @@ function renderDataSummary(data) {
|
||||
if (!card) return;
|
||||
|
||||
card.classList.remove("d-none");
|
||||
|
||||
|
||||
document.getElementById("first-ts").textContent =
|
||||
data.first_available ?? "–";
|
||||
document.getElementById("last-ts").textContent =
|
||||
@@ -209,6 +243,9 @@ function renderDataSummary(data) {
|
||||
logEl.textContent =
|
||||
`❌ No hay datos para ${data.symbol} @ ${data.timeframe}`;
|
||||
}
|
||||
|
||||
// si no es válido, ocultamos data quality
|
||||
document.getElementById("data-quality-card")?.classList.add("d-none");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -241,8 +278,9 @@ function renderDataSummary(data) {
|
||||
`${(dq.checks.coverage.ratio * 100).toFixed(2)}%`;
|
||||
document.getElementById("dq-volume").textContent =
|
||||
dq.checks.volume;
|
||||
} else {
|
||||
dqCard?.classList.add("d-none");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// =================================================
|
||||
|
||||
809
src/web/ui/v2/static/js/pages/calibration_risk.js
Normal file
809
src/web/ui/v2/static/js/pages/calibration_risk.js
Normal file
@@ -0,0 +1,809 @@
|
||||
// src/web/ui/v2/static/js/pages/calibration_risk.js
|
||||
|
||||
console.log(
|
||||
"[calibration_risk] script loaded ✅",
|
||||
new Date().toISOString()
|
||||
);
|
||||
|
||||
// =================================================
|
||||
// INSPECT RISK & STOPS
|
||||
// =================================================
|
||||
|
||||
async function inspectCalibrationRisk() {
|
||||
console.log("[calibration_risk] inspectCalibrationRisk() START ✅");
|
||||
|
||||
// --------------------------------------------------
|
||||
// Load calibration context from Step 1
|
||||
// --------------------------------------------------
|
||||
const symbol = localStorage.getItem("calibration.symbol");
|
||||
const timeframe = localStorage.getItem("calibration.timeframe");
|
||||
|
||||
if (!symbol || !timeframe) {
|
||||
alert("Calibration context not found. Please complete Step 1 (Data) first.");
|
||||
return;
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Read form inputs
|
||||
// --------------------------------------------------
|
||||
const accountEquityEl = document.getElementById("account_equity");
|
||||
const riskFractionEl = document.getElementById("risk_fraction");
|
||||
const maxPositionEl = document.getElementById("max_position_fraction");
|
||||
const stopTypeEl = document.getElementById("stop_type");
|
||||
const stopFractionEl = document.getElementById("stop_fraction");
|
||||
const atrPeriodEl = document.getElementById("atr_period");
|
||||
const atrMultiplierEl = document.getElementById("atr_multiplier");
|
||||
const maxDrawdownEl = document.getElementById("max_drawdown_pct");
|
||||
|
||||
if (!accountEquityEl || !riskFractionEl || !maxPositionEl || !stopTypeEl || !maxDrawdownEl) {
|
||||
console.error("[calibration_risk] Missing required form elements");
|
||||
alert("Internal error: risk form is incomplete.");
|
||||
return;
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Build stop config
|
||||
// --------------------------------------------------
|
||||
const stopType = stopTypeEl.value;
|
||||
let stopConfig = { type: stopType };
|
||||
|
||||
if (stopType === "fixed" || stopType === "trailing") {
|
||||
if (!stopFractionEl) {
|
||||
alert("Stop fraction input missing");
|
||||
return;
|
||||
}
|
||||
stopConfig.stop_fraction = parseFloat(stopFractionEl.value) / 100;
|
||||
}
|
||||
|
||||
if (stopType === "atr") {
|
||||
if (!atrPeriodEl || !atrMultiplierEl) {
|
||||
alert("ATR parameters missing");
|
||||
return;
|
||||
}
|
||||
stopConfig.atr_period = parseInt(atrPeriodEl.value);
|
||||
stopConfig.atr_multiplier = parseFloat(atrMultiplierEl.value);
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Build payload
|
||||
// --------------------------------------------------
|
||||
const payload = buildRiskPayload();
|
||||
|
||||
console.log("[calibration_risk] inspect payload:", payload);
|
||||
|
||||
// --------------------------------------------------
|
||||
// Disable next step while inspecting
|
||||
// --------------------------------------------------
|
||||
disableNextStep();
|
||||
|
||||
// --------------------------------------------------
|
||||
// Call API
|
||||
// --------------------------------------------------
|
||||
try {
|
||||
const res = await fetch("/api/v2/calibration/risk/inspect", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
console.log("[calibration_risk] inspect response:", data);
|
||||
|
||||
renderRiskResult(payload, data);
|
||||
|
||||
// --------------------------------------------------
|
||||
// Enable next step if OK or WARNING
|
||||
// --------------------------------------------------
|
||||
if (data.valid && data.status !== "fail") {
|
||||
enableNextStep();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("[calibration_risk] inspect FAILED", err);
|
||||
alert("Error inspecting risk configuration");
|
||||
disableNextStep();
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// VALIDATE RISK & STOPS
|
||||
// =================================================
|
||||
|
||||
async function validateCalibrationRisk() {
|
||||
console.log("[calibration_risk] inspectCalibrationRisk() START ✅");
|
||||
|
||||
// --------------------------------------------------
|
||||
// Load calibration context from Step 1
|
||||
// --------------------------------------------------
|
||||
const symbol = localStorage.getItem("calibration.symbol");
|
||||
const timeframe = localStorage.getItem("calibration.timeframe");
|
||||
|
||||
if (!symbol || !timeframe) {
|
||||
alert("Calibration context not found. Please complete Step 1 (Data) first.");
|
||||
return;
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Read form inputs
|
||||
// --------------------------------------------------
|
||||
const accountEquityEl = document.getElementById("account_equity");
|
||||
const riskFractionEl = document.getElementById("risk_fraction");
|
||||
const maxPositionEl = document.getElementById("max_position_fraction");
|
||||
const stopTypeEl = document.getElementById("stop_type");
|
||||
const stopFractionEl = document.getElementById("stop_fraction");
|
||||
const atrPeriodEl = document.getElementById("atr_period");
|
||||
const atrMultiplierEl = document.getElementById("atr_multiplier");
|
||||
const maxDrawdownEl = document.getElementById("max_drawdown_pct");
|
||||
|
||||
if (!accountEquityEl || !riskFractionEl || !maxPositionEl || !stopTypeEl || !maxDrawdownEl) {
|
||||
console.error("[calibration_risk] Missing required form elements");
|
||||
alert("Internal error: risk form is incomplete.");
|
||||
return;
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Build stop config
|
||||
// --------------------------------------------------
|
||||
const stopType = stopTypeEl.value;
|
||||
let stopConfig = { type: stopType };
|
||||
|
||||
if (stopType === "fixed" || stopType === "trailing") {
|
||||
if (!stopFractionEl) {
|
||||
alert("Stop fraction input missing");
|
||||
return;
|
||||
}
|
||||
stopConfig.stop_fraction = parseFloat(stopFractionEl.value) / 100;
|
||||
}
|
||||
|
||||
if (stopType === "atr") {
|
||||
if (!atrPeriodEl || !atrMultiplierEl) {
|
||||
alert("ATR parameters missing");
|
||||
return;
|
||||
}
|
||||
stopConfig.atr_period = parseInt(atrPeriodEl.value);
|
||||
stopConfig.atr_multiplier = parseFloat(atrMultiplierEl.value);
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// Build payload
|
||||
// --------------------------------------------------
|
||||
const payload = buildRiskPayload();
|
||||
|
||||
console.log("[calibration_risk] inspect payload:", payload);
|
||||
|
||||
// --------------------------------------------------
|
||||
// Disable next step while inspecting
|
||||
// --------------------------------------------------
|
||||
disableNextStep();
|
||||
|
||||
// --------------------------------------------------
|
||||
// Call API
|
||||
// --------------------------------------------------
|
||||
try {
|
||||
const res = await fetch("/api/v2/calibration/risk/validate", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
console.log("[calibration_risk] inspect response:", data);
|
||||
|
||||
renderRiskResult(payload, data);
|
||||
|
||||
// --------------------------------------------------
|
||||
// Enable next step if OK or WARNING
|
||||
// --------------------------------------------------
|
||||
if (data.valid && data.status !== "fail") {
|
||||
enableNextStep();
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("[calibration_risk] inspect FAILED", err);
|
||||
alert("Error inspecting risk configuration");
|
||||
disableNextStep();
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// PDF REPORT RISK & STOPS
|
||||
// =================================================
|
||||
|
||||
async function generateRiskReport() {
|
||||
console.log("[calibration_risk] generateRiskReport() START");
|
||||
|
||||
const payload = buildRiskPayload(); // reutiliza la misma función que validate
|
||||
|
||||
const res = await fetch("/api/v2/calibration/risk/report", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error("Failed to generate report");
|
||||
return;
|
||||
}
|
||||
|
||||
const blob = await res.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = "risk_report.pdf";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// 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");
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// RENDER RESULT
|
||||
// =================================================
|
||||
|
||||
function renderRiskResult(payload, data) {
|
||||
const container = document.getElementById("risk_result");
|
||||
const badge = document.getElementById("risk_status_badge");
|
||||
const message = document.getElementById("risk_message");
|
||||
const debug = document.getElementById("risk_debug");
|
||||
|
||||
if (!container || !badge || !message || !debug) {
|
||||
console.warn("[calibration_risk] Result elements missing");
|
||||
return;
|
||||
}
|
||||
|
||||
container.classList.remove("d-none");
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
badge.textContent = data.status.toUpperCase();
|
||||
message.textContent = data.message;
|
||||
debug.textContent = JSON.stringify(data.checks, null, 2);
|
||||
|
||||
renderRiskChecks(data.checks);
|
||||
if (data.checks?.stop_sanity?.metrics) {
|
||||
renderStopQuantiles(data.checks.stop_sanity.metrics);
|
||||
}
|
||||
|
||||
renderRiskFormulas(payload, data)
|
||||
|
||||
if (!data.series) {
|
||||
console.warn("[calibration_risk] No series returned, skipping plots");
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.series?.timestamps && data.series?.position_size_pct) {
|
||||
renderPositionSizePlot({
|
||||
timestamps: data.series.timestamps,
|
||||
positionSizePct: data.series.position_size_pct,
|
||||
maxPositionFraction: payload.risk.max_position_fraction
|
||||
});
|
||||
renderEffectiveRiskPlot({
|
||||
timestamps: data.series.timestamps,
|
||||
effectiveRiskPct: data.series.effective_risk_pct,
|
||||
targetRiskFraction: payload.risk.risk_fraction
|
||||
});
|
||||
renderStopDistanceDistribution({
|
||||
stopDistances: data.series.stop_distances,
|
||||
quantiles: data.checks?.stop_sanity?.metrics
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// RENDER RISK CHECKS
|
||||
// =================================================
|
||||
|
||||
function renderRiskChecks(checks) {
|
||||
const list = document.getElementById("risk_checks_list");
|
||||
if (!list || !checks) return;
|
||||
|
||||
list.innerHTML = "";
|
||||
|
||||
Object.entries(checks).forEach(([key, check]) => {
|
||||
const li = document.createElement("li");
|
||||
li.classList.add("mb-1");
|
||||
|
||||
let icon = "❌";
|
||||
let color = "text-danger";
|
||||
|
||||
if (check.status === "ok") {
|
||||
icon = "✅";
|
||||
color = "text-success";
|
||||
} else if (check.status === "warning") {
|
||||
icon = "⚠️";
|
||||
color = "text-warning";
|
||||
}
|
||||
|
||||
const title = key.replace(/_/g, " ");
|
||||
|
||||
li.innerHTML = `
|
||||
<span class="${color}">
|
||||
${icon} <strong>${title}</strong>
|
||||
</span>
|
||||
${check.message ? `<span class="text-muted"> — ${check.message}</span>` : ""}
|
||||
`;
|
||||
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function renderStopQuantiles(metrics) {
|
||||
const el = document.getElementById("stop_quantiles");
|
||||
if (!el || !metrics) return;
|
||||
|
||||
el.innerHTML = `
|
||||
<li>P50 (typical): ${(metrics.p50 * 100).toFixed(2)}%</li>
|
||||
<li>P90 (wide): ${(metrics.p90 * 100).toFixed(2)}%</li>
|
||||
<li>P99 (extreme): ${(metrics.p99 * 100).toFixed(2)}%</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// RENDER RISK FORMULAS
|
||||
// =================================================
|
||||
|
||||
function renderRiskFormulas(payload, result) {
|
||||
const el = document.getElementById("risk_formulas");
|
||||
if (!el) return;
|
||||
|
||||
const equity = payload.account_equity;
|
||||
const riskFraction = payload.risk.risk_fraction;
|
||||
const maxPositionFraction = payload.risk.max_position_fraction;
|
||||
|
||||
const stopMetrics = result.checks?.stop_sanity?.metrics;
|
||||
if (!stopMetrics || stopMetrics.p50 == null) {
|
||||
el.textContent = "Unable to compute risk formulas (missing stop metrics)";
|
||||
return;
|
||||
}
|
||||
|
||||
const stopP50 = stopMetrics.p50;
|
||||
|
||||
const idealPosition = (equity * riskFraction) / stopP50;
|
||||
const maxPosition = equity * maxPositionFraction;
|
||||
const effectivePosition = Math.min(idealPosition, maxPosition);
|
||||
const effectiveRisk = (effectivePosition * stopP50) / equity;
|
||||
|
||||
el.innerHTML = `
|
||||
<li>Position size (ideal)
|
||||
= ${equity.toLocaleString()} × ${(riskFraction * 100).toFixed(2)}% ÷ ${(stopP50 * 100).toFixed(2)}%
|
||||
≈ ${idealPosition.toFixed(0)}</li>
|
||||
|
||||
<li>Max position size
|
||||
= ${equity.toLocaleString()} × ${(maxPositionFraction * 100).toFixed(2)}%
|
||||
= ${maxPosition.toFixed(0)}</li>
|
||||
|
||||
<li>Effective position
|
||||
= min(${idealPosition.toFixed(0)}, ${maxPosition.toFixed(0)})
|
||||
= ${effectivePosition.toFixed(0)}</li>
|
||||
|
||||
<li>Effective risk
|
||||
= ${effectivePosition.toFixed(0)} × ${(stopP50 * 100).toFixed(2)}% ÷ ${equity.toLocaleString()}
|
||||
≈ ${(effectiveRisk * 100).toFixed(2)}%</li>
|
||||
`;
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// STOP TYPE UI LOGIC
|
||||
// =================================================
|
||||
|
||||
function updateStopParamsUI() {
|
||||
const stopType = document.getElementById("stop_type")?.value;
|
||||
if (!stopType) return;
|
||||
|
||||
// Hide all stop param blocks
|
||||
document.querySelectorAll(".stop-param").forEach(el => {
|
||||
el.classList.add("d-none");
|
||||
});
|
||||
|
||||
// Show relevant blocks
|
||||
if (stopType === "fixed" || stopType === "trailing") {
|
||||
document.querySelectorAll(".stop-fixed, .stop-trailing").forEach(el => {
|
||||
el.classList.remove("d-none");
|
||||
});
|
||||
}
|
||||
|
||||
if (stopType === "atr") {
|
||||
document.querySelectorAll(".stop-atr").forEach(el => {
|
||||
el.classList.remove("d-none");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// RENDER PLOTS
|
||||
// =================================================
|
||||
|
||||
function renderPositionSizePlot({
|
||||
timestamps,
|
||||
positionSizePct,
|
||||
maxPositionFraction
|
||||
}) {
|
||||
const el = document.getElementById("position_size_plot");
|
||||
if (!el) {
|
||||
console.warn("[calibration_risk] position_size_plot not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!timestamps || !positionSizePct || timestamps.length === 0) {
|
||||
el.innerHTML = "<em>No position size data available</em>";
|
||||
return;
|
||||
}
|
||||
|
||||
const trace = {
|
||||
x: timestamps,
|
||||
y: positionSizePct.map(v => v * 100),
|
||||
type: "scatter",
|
||||
mode: "lines",
|
||||
name: "Position size",
|
||||
line: {
|
||||
color: "#59a14f",
|
||||
width: 2
|
||||
}
|
||||
};
|
||||
|
||||
const layout = {
|
||||
yaxis: {
|
||||
title: "Position size (% equity)",
|
||||
rangemode: "tozero"
|
||||
},
|
||||
xaxis: {
|
||||
title: "Time"
|
||||
},
|
||||
shapes: [
|
||||
{
|
||||
type: "line",
|
||||
xref: "paper",
|
||||
x0: 0,
|
||||
x1: 1,
|
||||
y0: maxPositionFraction * 100,
|
||||
y1: maxPositionFraction * 100,
|
||||
line: {
|
||||
dash: "dot",
|
||||
color: "#e15759",
|
||||
width: 2
|
||||
}
|
||||
}
|
||||
],
|
||||
annotations: [
|
||||
{
|
||||
xref: "paper",
|
||||
x: 1,
|
||||
y: maxPositionFraction * 100,
|
||||
xanchor: "left",
|
||||
text: "Max position cap",
|
||||
showarrow: false,
|
||||
font: {
|
||||
size: 11,
|
||||
color: "#e15759"
|
||||
}
|
||||
}
|
||||
],
|
||||
margin: { t: 20 }
|
||||
};
|
||||
|
||||
Plotly.newPlot(el, [trace], layout, {
|
||||
responsive: true,
|
||||
displayModeBar: true
|
||||
});
|
||||
}
|
||||
|
||||
function renderEffectiveRiskPlot({
|
||||
timestamps,
|
||||
effectiveRiskPct,
|
||||
targetRiskFraction
|
||||
}) {
|
||||
const el = document.getElementById("effective_risk_plot");
|
||||
if (!el) {
|
||||
console.warn("[calibration_risk] effective_risk_plot not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!timestamps || !effectiveRiskPct || timestamps.length === 0) {
|
||||
el.innerHTML = "<em>No effective risk data available</em>";
|
||||
return;
|
||||
}
|
||||
|
||||
const trace = {
|
||||
x: timestamps,
|
||||
y: effectiveRiskPct.map(v => v * 100),
|
||||
type: "scatter",
|
||||
mode: "lines",
|
||||
name: "Effective risk",
|
||||
line: {
|
||||
color: "#f28e2b",
|
||||
width: 2
|
||||
}
|
||||
};
|
||||
|
||||
const layout = {
|
||||
yaxis: {
|
||||
title: "Effective risk (% equity)",
|
||||
rangemode: "tozero"
|
||||
},
|
||||
xaxis: {
|
||||
title: "Time"
|
||||
},
|
||||
shapes: [
|
||||
{
|
||||
type: "line",
|
||||
xref: "paper",
|
||||
x0: 0,
|
||||
x1: 1,
|
||||
y0: targetRiskFraction * 100,
|
||||
y1: targetRiskFraction * 100,
|
||||
line: {
|
||||
dash: "dot",
|
||||
color: "#4c78a8",
|
||||
width: 2
|
||||
}
|
||||
}
|
||||
],
|
||||
annotations: [
|
||||
{
|
||||
xref: "paper",
|
||||
x: 1,
|
||||
y: targetRiskFraction * 100,
|
||||
xanchor: "left",
|
||||
text: "Target risk",
|
||||
showarrow: false,
|
||||
font: {
|
||||
size: 11,
|
||||
color: "#4c78a8"
|
||||
}
|
||||
}
|
||||
],
|
||||
margin: { t: 20 }
|
||||
};
|
||||
|
||||
Plotly.newPlot(el, [trace], layout, {
|
||||
responsive: true,
|
||||
displayModeBar: true
|
||||
});
|
||||
}
|
||||
|
||||
function renderStopDistanceDistribution({
|
||||
stopDistances,
|
||||
quantiles
|
||||
}) {
|
||||
const el = document.getElementById("stop_distance_plot");
|
||||
if (!el) {
|
||||
console.warn("[calibration_risk] stop_distance_plot not found");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!stopDistances || stopDistances.length === 0) {
|
||||
el.innerHTML = "<em>No stop distance data available</em>";
|
||||
return;
|
||||
}
|
||||
|
||||
const valuesPct = stopDistances
|
||||
.filter(v => v != null && v > 0)
|
||||
.map(v => v * 100);
|
||||
|
||||
const trace = {
|
||||
x: valuesPct,
|
||||
type: "histogram",
|
||||
nbinsx: 40,
|
||||
name: "Stop distance",
|
||||
marker: {
|
||||
color: "#bab0ac"
|
||||
}
|
||||
};
|
||||
|
||||
const shapes = [];
|
||||
const annotations = [];
|
||||
|
||||
if (quantiles) {
|
||||
const qMap = [
|
||||
{ key: "p50", label: "P50 (typical)", color: "#4c78a8" },
|
||||
{ key: "p90", label: "P90 (wide)", color: "#f28e2b" },
|
||||
{ key: "p99", label: "P99 (extreme)", color: "#e15759" }
|
||||
];
|
||||
|
||||
qMap.forEach(q => {
|
||||
const val = quantiles[q.key];
|
||||
if (val != null) {
|
||||
shapes.push({
|
||||
type: "line",
|
||||
xref: "x",
|
||||
yref: "paper",
|
||||
x0: val * 100,
|
||||
x1: val * 100,
|
||||
y0: 0,
|
||||
y1: 1,
|
||||
line: {
|
||||
dash: "dot",
|
||||
width: 2,
|
||||
color: q.color
|
||||
}
|
||||
});
|
||||
|
||||
annotations.push({
|
||||
x: val * 100,
|
||||
y: 1,
|
||||
yref: "paper",
|
||||
xanchor: "left",
|
||||
text: q.label,
|
||||
showarrow: false,
|
||||
font: {
|
||||
size: 11,
|
||||
color: q.color
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const layout = {
|
||||
xaxis: {
|
||||
title: "Stop distance (% price)"
|
||||
},
|
||||
yaxis: {
|
||||
title: "Frequency"
|
||||
},
|
||||
shapes: shapes,
|
||||
annotations: annotations,
|
||||
margin: { t: 20 }
|
||||
};
|
||||
|
||||
Plotly.newPlot(el, [trace], layout, {
|
||||
responsive: true,
|
||||
displayModeBar: true
|
||||
});
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// UTILS
|
||||
// =================================================
|
||||
|
||||
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 buildRiskPayload() {
|
||||
const symbol = localStorage.getItem("calibration.symbol");
|
||||
const timeframe = localStorage.getItem("calibration.timeframe");
|
||||
|
||||
const stopType = document.getElementById("stop_type")?.value;
|
||||
|
||||
const stop = { type: stopType };
|
||||
|
||||
if (stopType === "fixed" || stopType === "trailing") {
|
||||
stop.stop_fraction = num("stop_fraction");
|
||||
}
|
||||
|
||||
if (stopType === "atr") {
|
||||
stop.atr_period = num("atr_period");
|
||||
stop.atr_multiplier = num("atr_multiplier");
|
||||
}
|
||||
|
||||
const payload = {
|
||||
symbol,
|
||||
timeframe,
|
||||
account_equity: num("account_equity"),
|
||||
|
||||
risk: {
|
||||
risk_fraction: num("risk_fraction") / 100,
|
||||
max_position_fraction: num("max_position_fraction") / 100,
|
||||
},
|
||||
|
||||
stop,
|
||||
|
||||
global_rules: {
|
||||
max_drawdown_pct: num("max_drawdown_pct") / 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"),
|
||||
},
|
||||
};
|
||||
|
||||
console.log("[calibration_risk] FINAL PAYLOAD:", payload);
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
// =================================================
|
||||
// INIT
|
||||
// =================================================
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("[calibration_risk] DOMContentLoaded ✅");
|
||||
|
||||
document
|
||||
.getElementById("inspect_risk_btn")
|
||||
?.addEventListener("click", inspectCalibrationRisk);
|
||||
|
||||
const stopTypeSelect = document.getElementById("stop_type");
|
||||
stopTypeSelect?.addEventListener("change", updateStopParamsUI);
|
||||
|
||||
const symbol = localStorage.getItem("calibration.symbol");
|
||||
const timeframe = localStorage.getItem("calibration.timeframe");
|
||||
|
||||
if (symbol && timeframe) {
|
||||
const symbolEl = document.getElementById("ctx_symbol");
|
||||
const timeframeEl = document.getElementById("ctx_timeframe");
|
||||
|
||||
if (symbolEl) symbolEl.textContent = symbol;
|
||||
if (timeframeEl) timeframeEl.textContent = timeframe;
|
||||
}
|
||||
|
||||
// Initial state
|
||||
updateStopParamsUI();
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("[calibration_risk] DOMContentLoaded ✅");
|
||||
|
||||
document
|
||||
.getElementById("validate_risk_btn")
|
||||
?.addEventListener("click", validateCalibrationRisk);
|
||||
|
||||
const stopTypeSelect = document.getElementById("stop_type");
|
||||
stopTypeSelect?.addEventListener("change", updateStopParamsUI);
|
||||
|
||||
const symbol = localStorage.getItem("calibration.symbol");
|
||||
const timeframe = localStorage.getItem("calibration.timeframe");
|
||||
|
||||
if (symbol && timeframe) {
|
||||
const symbolEl = document.getElementById("ctx_symbol");
|
||||
const timeframeEl = document.getElementById("ctx_timeframe");
|
||||
|
||||
if (symbolEl) symbolEl.textContent = symbol;
|
||||
if (timeframeEl) timeframeEl.textContent = timeframe;
|
||||
}
|
||||
|
||||
// Initial state
|
||||
updateStopParamsUI();
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
console.log("[calibration_risk] DOMContentLoaded ✅");
|
||||
|
||||
document
|
||||
.getElementById("generate_report_btn")
|
||||
?.addEventListener("click", generateRiskReport);
|
||||
});
|
||||
|
||||
|
||||
@@ -3,7 +3,55 @@
|
||||
{% block content %}
|
||||
<div class="container-xl">
|
||||
|
||||
<h2 class="mb-4">Calibración · Paso 1 · Datos</h2>
|
||||
<!-- ========================= -->
|
||||
<!-- Wizard header -->
|
||||
<!-- ========================= -->
|
||||
<div class="d-flex align-items-center mb-4">
|
||||
|
||||
<!-- Back arrow (disabled on step 1) -->
|
||||
<div class="me-3">
|
||||
<button class="btn btn-outline-secondary btn-icon" disabled>
|
||||
<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"/>
|
||||
<line x1="19" y1="12" x2="5" y2="12" />
|
||||
<line x1="11" y1="18" x2="5" y2="12" />
|
||||
<line x1="11" y1="6" x2="5" y2="12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Center title -->
|
||||
<div class="flex-grow-1 text-center">
|
||||
<h2 class="mb-0">Calibración · Paso 1 · Datos</h2>
|
||||
</div>
|
||||
|
||||
<!-- Forward arrow (JS will enable when Step1 is OK) -->
|
||||
<div class="ms-3">
|
||||
<a
|
||||
id="next-step-btn"
|
||||
href="/calibration/risk"
|
||||
class="btn btn-outline-secondary btn-icon"
|
||||
aria-disabled="true"
|
||||
>
|
||||
<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"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<line x1="13" y1="6" x2="19" y2="12" />
|
||||
<line x1="13" y1="18" x2="19" y2="12" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- FORMULARIO -->
|
||||
<div class="card mb-4">
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-xl">
|
||||
|
||||
<!-- Page header -->
|
||||
<div class="page-header d-print-none mb-4">
|
||||
<div class="row align-items-center">
|
||||
<div class="col">
|
||||
<h2 class="page-title">
|
||||
Calibration · Step 1 — Data
|
||||
</h2>
|
||||
<div class="text-muted mt-1">
|
||||
Select market data used for calibration
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data selection card -->
|
||||
<div class="row">
|
||||
<div class="col-12 col-lg-8">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Market data</h3>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- Symbol -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Symbol</label>
|
||||
<select class="form-select">
|
||||
<option selected>BTC/USDT</option>
|
||||
<option>ETH/USDT</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Timeframe -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Timeframe</label>
|
||||
<select class="form-select">
|
||||
<option>1m</option>
|
||||
<option>5m</option>
|
||||
<option>15m</option>
|
||||
<option selected>1h</option>
|
||||
<option>4h</option>
|
||||
<option>1d</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Start date -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Start date</label>
|
||||
<input type="date" class="form-control">
|
||||
</div>
|
||||
|
||||
<!-- End date -->
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">End date</label>
|
||||
<input type="date" class="form-control">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="card-footer text-end">
|
||||
<a href="/calibration/step2" class="btn btn-primary">
|
||||
Continue
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Help / info -->
|
||||
<div class="col-12 col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4>What happens in this step?</h4>
|
||||
<p class="text-muted">
|
||||
You define the historical market data that will be used to:
|
||||
</p>
|
||||
<ul class="text-muted">
|
||||
<li>download candles</li>
|
||||
<li>compute indicators</li>
|
||||
<li>calibrate strategies</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
246
src/web/ui/v2/templates/pages/calibration/calibration_risk.html
Normal file
246
src/web/ui/v2/templates/pages/calibration/calibration_risk.html
Normal file
@@ -0,0 +1,246 @@
|
||||
{% 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/data" 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"/>
|
||||
<line x1="19" y1="12" x2="5" y2="12" />
|
||||
<line x1="11" y1="18" x2="5" y2="12" />
|
||||
<line x1="11" y1="6" x2="5" y2="12" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Center title -->
|
||||
<div class="flex-grow-1 text-center">
|
||||
<h2 class="mb-0">Calibración · Paso 2 · Risk & Stops</h2>
|
||||
</div>
|
||||
|
||||
<!-- Forward arrow (disabled until Step2 OK) -->
|
||||
<div class="ms-3">
|
||||
<a
|
||||
id="next-step-btn"
|
||||
href="/calibration/strategies"
|
||||
class="btn btn-outline-secondary btn-icon"
|
||||
aria-disabled="true"
|
||||
>
|
||||
<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"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12" />
|
||||
<line x1="13" y1="6" x2="19" y2="12" />
|
||||
<line x1="13" y1="18" x2="19" y2="12" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- Risk configuration form -->
|
||||
<!-- ========================= -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h3 class="card-title">Data Context</h3>
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- Symbol -->
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Símbolo</label>
|
||||
<div
|
||||
class="form-control bg-light text-muted"
|
||||
style="cursor: not-allowed;"
|
||||
>
|
||||
<span id="ctx_symbol">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeframe -->
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Timeframe</label>
|
||||
<div
|
||||
class="form-control bg-light text-muted"
|
||||
style="cursor: not-allowed;"
|
||||
>
|
||||
<span id="ctx_timeframe">-</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<h3 class="card-title">Risk configuration</h3>
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- Account equity -->
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Account equity</label>
|
||||
<input id="account_equity" class="form-control" type="number" value="10000" min="0">
|
||||
</div>
|
||||
|
||||
<!-- Risk per trade -->
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Risk per trade (%)</label>
|
||||
<input id="risk_fraction" class="form-control" type="number" step="0.01" value="0.5">
|
||||
</div>
|
||||
|
||||
<!-- Max position -->
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Max position size (%)</label>
|
||||
<input id="max_position_fraction" class="form-control" type="number" step="1" value="25">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- Stop configuration -->
|
||||
<h3 class="card-title">Stop configuration</h3>
|
||||
|
||||
<div class="row g-3">
|
||||
|
||||
<!-- Stop type -->
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Stop type</label>
|
||||
<select id="stop_type" class="form-select">
|
||||
<option value="fixed">Fixed stop</option>
|
||||
<option value="trailing">Trailing stop</option>
|
||||
<option value="atr">ATR stop</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Fixed / trailing -->
|
||||
<div class="col-md-4 stop-param stop-fixed stop-trailing">
|
||||
<label class="form-label">Stop distance (%)</label>
|
||||
<input id="stop_fraction" class="form-control" type="number" step="0.1" value="2">
|
||||
</div>
|
||||
|
||||
<!-- ATR params -->
|
||||
<div class="col-md-4 stop-param stop-atr d-none">
|
||||
<label class="form-label">ATR period</label>
|
||||
<input id="atr_period" class="form-control" type="number" value="14">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 stop-param stop-atr d-none">
|
||||
<label class="form-label">ATR multiplier</label>
|
||||
<input id="atr_multiplier" class="form-control" type="number" step="0.1" value="3">
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<!-- Global rules -->
|
||||
<h3 class="card-title">Global rules</h3>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label">Max drawdown (%)</label>
|
||||
<input id="max_drawdown_pct" class="form-control" type="number" step="1" value="20">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<button id="inspect_risk_btn" class="btn btn-primary">
|
||||
Inspect Risk & Stops
|
||||
</button>
|
||||
<button id="validate_risk_btn" class="btn btn-primary">
|
||||
Validate Risk & Stops
|
||||
</button>
|
||||
<button id="generate_report_btn" class="btn btn-outline-dark">
|
||||
Generate PDF Report
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- Risk quality result -->
|
||||
<!-- ========================= -->
|
||||
<div id="risk_result" class="d-none">
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
|
||||
<h3 class="card-title">Risk quality</h3>
|
||||
|
||||
<div id="risk_status_badge" class="mb-3"></div>
|
||||
|
||||
<p id="risk_message" class="mb-3"></p>
|
||||
|
||||
<pre id="risk_debug" class="bg-light p-3 rounded small d-none"></pre>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RISK CHECKS -->
|
||||
<div class="mt-3">
|
||||
<ul id="risk_checks_list" class="list-unstyled mb-0"></ul>
|
||||
</div>
|
||||
|
||||
<!-- STOP QUANTILES -->
|
||||
<div class="mt-3">
|
||||
<h5 class="mb-2">Stop distance statistics</h5>
|
||||
<ul class="list-unstyled text-muted" id="stop_quantiles">
|
||||
<li>P50: –</li>
|
||||
<li>P90: –</li>
|
||||
<li>P99: –</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- RISK FORMULAS -->
|
||||
<div class="mt-3">
|
||||
<h5 class="mb-2">Risk sizing breakdown</h5>
|
||||
<ul id="risk_formulas" class="list-unstyled text-muted">
|
||||
Calculating…
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ========================= -->
|
||||
<!-- Gráficas -->
|
||||
<!-- ========================= -->
|
||||
<!-- POSITION SIZE OVER TIME -->
|
||||
<div class="mt-4">
|
||||
<h5 class="mb-2">Position size over time</h5>
|
||||
<div id="position_size_plot" style="height: 360px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- EFFECTIVE RISK OVER TIME -->
|
||||
<div class="mt-4">
|
||||
<h5 class="mb-2">Effective risk over time</h5>
|
||||
<div id="effective_risk_plot" style="height: 360px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- STOP DISTANCE DISTRIBUTION -->
|
||||
<div class="mt-4">
|
||||
<h5 class="mb-2">Stop distance distribution</h5>
|
||||
<div id="stop_distance_plot" style="height: 360px;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- Plotly -->
|
||||
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
|
||||
<!-- Page logic -->
|
||||
<script src="/static/js/pages/calibration_risk.js"></script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user