Strategy promotion added
This commit is contained in:
68
src/calibration/strategy_promotion.py
Normal file
68
src/calibration/strategy_promotion.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
#src/calibration/strategy_promotion.py
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
def _clamp(v, lo, hi):
|
||||||
|
return max(lo, min(hi, v))
|
||||||
|
|
||||||
|
def _score_return(oos_return):
|
||||||
|
return _clamp((oos_return / 50.0) * 30.0, 0, 30)
|
||||||
|
|
||||||
|
def _score_stability(positive_rate, std_return):
|
||||||
|
score = positive_rate * 15
|
||||||
|
score += _clamp(10 - std_return, 0, 10)
|
||||||
|
return _clamp(score, 0, 25)
|
||||||
|
|
||||||
|
def _score_risk(worst_dd):
|
||||||
|
return _clamp((1 + worst_dd / 20.0) * 20.0, 0, 20)
|
||||||
|
|
||||||
|
def _score_trades(avg_trades):
|
||||||
|
return _clamp(avg_trades / 10.0 * 10.0, 0, 10)
|
||||||
|
|
||||||
|
def _score_regime(regime_detail: Dict[str, Any]):
|
||||||
|
if not regime_detail:
|
||||||
|
return 0
|
||||||
|
positives = sum(1 for r in regime_detail.values() if r.get("avg_return", 0) >= 0)
|
||||||
|
total = len(regime_detail)
|
||||||
|
return (positives / total) * 15 if total else 0
|
||||||
|
|
||||||
|
def evaluate_strategy(strategy: Dict[str, Any], config: Dict[str, Any]):
|
||||||
|
metrics = strategy.get("diagnostics", {})
|
||||||
|
stability = metrics.get("stability", {})
|
||||||
|
trades = metrics.get("trades", {})
|
||||||
|
regimes = metrics.get("regimes", {}).get("performance", {}).get("detail", {})
|
||||||
|
|
||||||
|
oos_return = strategy.get("oos_total_return_pct", 0)
|
||||||
|
worst_dd = strategy.get("oos_max_dd_worst_pct", -100)
|
||||||
|
positive_rate = stability.get("positive_window_rate", 0)
|
||||||
|
std_return = stability.get("std_return_pct", 0)
|
||||||
|
avg_trades = trades.get("avg_trades_per_window", 0)
|
||||||
|
|
||||||
|
score = 0
|
||||||
|
score += _score_return(oos_return)
|
||||||
|
score += _score_stability(positive_rate, std_return)
|
||||||
|
score += _score_risk(worst_dd)
|
||||||
|
score += _score_trades(avg_trades)
|
||||||
|
score += _score_regime(regimes)
|
||||||
|
|
||||||
|
promote_thr = config.get("promote_score_threshold", 70)
|
||||||
|
review_thr = config.get("review_score_threshold", 55)
|
||||||
|
|
||||||
|
if score >= promote_thr:
|
||||||
|
status = "promote"
|
||||||
|
elif score >= review_thr:
|
||||||
|
status = "review"
|
||||||
|
else:
|
||||||
|
status = "reject"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"strategy_id": strategy.get("strategy_id"),
|
||||||
|
"score": round(score, 2),
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
|
||||||
|
def rank_strategies(strategies: List[Dict[str, Any]], config: Dict[str, Any]):
|
||||||
|
evaluated = [evaluate_strategy(s, config) for s in strategies]
|
||||||
|
evaluated.sort(key=lambda x: x["score"], reverse=True)
|
||||||
|
for i, e in enumerate(evaluated):
|
||||||
|
e["rank"] = i + 1
|
||||||
|
return evaluated
|
||||||
@@ -16,11 +16,14 @@ from src.calibration.strategies_inspector import (
|
|||||||
list_available_strategies,
|
list_available_strategies,
|
||||||
)
|
)
|
||||||
from src.calibration.reports.strategies_report import generate_strategies_report_pdf
|
from src.calibration.reports.strategies_report import generate_strategies_report_pdf
|
||||||
|
from src.calibration.strategy_promotion import rank_strategies
|
||||||
|
|
||||||
from ..schemas.calibration_strategies import (
|
from ..schemas.calibration_strategies import (
|
||||||
CalibrationStrategiesInspectRequest,
|
CalibrationStrategiesInspectRequest,
|
||||||
CalibrationStrategiesInspectResponse,
|
CalibrationStrategiesInspectResponse,
|
||||||
CalibrationStrategiesValidateResponse,
|
CalibrationStrategiesValidateResponse,
|
||||||
|
CalibrationStrategiesPromoteRequest,
|
||||||
|
CalibrationStrategiesPromoteResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger("tradingbot.api.v2")
|
logger = logging.getLogger("tradingbot.api.v2")
|
||||||
@@ -94,6 +97,7 @@ def inspect_strategies(
|
|||||||
)
|
)
|
||||||
return CalibrationStrategiesInspectResponse(**result)
|
return CalibrationStrategiesInspectResponse(**result)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/validate", response_model=CalibrationStrategiesValidateResponse)
|
@router.post("/validate", response_model=CalibrationStrategiesValidateResponse)
|
||||||
def validate_strategies(
|
def validate_strategies(
|
||||||
payload: CalibrationStrategiesInspectRequest,
|
payload: CalibrationStrategiesInspectRequest,
|
||||||
@@ -115,6 +119,13 @@ def validate_strategies(
|
|||||||
)
|
)
|
||||||
return CalibrationStrategiesValidateResponse(**result)
|
return CalibrationStrategiesValidateResponse(**result)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/promote", response_model=CalibrationStrategiesPromoteResponse)
|
||||||
|
def promote_strategies(payload: CalibrationStrategiesPromoteRequest):
|
||||||
|
results = rank_strategies(payload.strategies, payload.promotion.dict())
|
||||||
|
return {"results": results}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/report")
|
@router.post("/report")
|
||||||
def report_strategies(
|
def report_strategies(
|
||||||
payload: CalibrationStrategiesInspectRequest,
|
payload: CalibrationStrategiesInspectRequest,
|
||||||
@@ -178,6 +189,7 @@ def report_strategies(
|
|||||||
public_url = f"/reports/strategies/{safe_symbol}/{filename}"
|
public_url = f"/reports/strategies/{safe_symbol}/{filename}"
|
||||||
return JSONResponse(content={"status": result.get("status", "ok"), "url": public_url})
|
return JSONResponse(content={"status": result.get("status", "ok"), "url": public_url})
|
||||||
|
|
||||||
|
|
||||||
@router.post("/run")
|
@router.post("/run")
|
||||||
def run_strategies_async(
|
def run_strategies_async(
|
||||||
payload: CalibrationStrategiesInspectRequest,
|
payload: CalibrationStrategiesInspectRequest,
|
||||||
@@ -247,6 +259,7 @@ def run_strategies_async(
|
|||||||
|
|
||||||
return {"job_id": job_id}
|
return {"job_id": job_id}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/status/{job_id}")
|
@router.get("/status/{job_id}")
|
||||||
def get_status(job_id: str):
|
def get_status(job_id: str):
|
||||||
return WF_JOBS.get(job_id, {"status": "unknown"})
|
return WF_JOBS.get(job_id, {"status": "unknown"})
|
||||||
|
|||||||
@@ -85,3 +85,26 @@ class CalibrationStrategiesInspectResponse(BaseModel):
|
|||||||
|
|
||||||
class CalibrationStrategiesValidateResponse(CalibrationStrategiesInspectResponse):
|
class CalibrationStrategiesValidateResponse(CalibrationStrategiesInspectResponse):
|
||||||
series: Dict[str, Any]
|
series: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class PromotionConfigSchema(BaseModel):
|
||||||
|
min_windows_required: int = 6
|
||||||
|
min_positive_window_rate: float = 0.45
|
||||||
|
max_worst_window_dd_pct: float = -15
|
||||||
|
max_low_trade_window_ratio: float = 0.40
|
||||||
|
min_avg_trades_per_window: float = 3
|
||||||
|
promote_score_threshold: float = 70
|
||||||
|
review_score_threshold: float = 55
|
||||||
|
|
||||||
|
class PromotionResultSchema(BaseModel):
|
||||||
|
strategy_id: str
|
||||||
|
score: float
|
||||||
|
status: Literal["promote", "review", "reject"]
|
||||||
|
rank: int
|
||||||
|
|
||||||
|
class CalibrationStrategiesPromoteRequest(BaseModel):
|
||||||
|
strategies: List[Dict[str, Any]]
|
||||||
|
promotion: PromotionConfigSchema
|
||||||
|
|
||||||
|
class CalibrationStrategiesPromoteResponse(BaseModel):
|
||||||
|
results: List[PromotionResultSchema]
|
||||||
@@ -12,6 +12,7 @@ const MAX_STRATEGIES = 10;
|
|||||||
// WIZARD NAVIGATION
|
// WIZARD NAVIGATION
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|
||||||
|
|
||||||
function enableNextStep() {
|
function enableNextStep() {
|
||||||
const btn = document.getElementById("next-step-btn");
|
const btn = document.getElementById("next-step-btn");
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
@@ -20,6 +21,7 @@ function enableNextStep() {
|
|||||||
btn.setAttribute("aria-disabled", "false");
|
btn.setAttribute("aria-disabled", "false");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function disableNextStep() {
|
function disableNextStep() {
|
||||||
const btn = document.getElementById("next-step-btn");
|
const btn = document.getElementById("next-step-btn");
|
||||||
if (!btn) return;
|
if (!btn) return;
|
||||||
@@ -28,10 +30,12 @@ function disableNextStep() {
|
|||||||
btn.setAttribute("aria-disabled", "true");
|
btn.setAttribute("aria-disabled", "true");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// UTILS
|
// UTILS
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|
||||||
|
|
||||||
function loadContextFromLocalStorage() {
|
function loadContextFromLocalStorage() {
|
||||||
const symbol = localStorage.getItem("calibration.symbol");
|
const symbol = localStorage.getItem("calibration.symbol");
|
||||||
const timeframe = localStorage.getItem("calibration.timeframe");
|
const timeframe = localStorage.getItem("calibration.timeframe");
|
||||||
@@ -81,16 +85,19 @@ function loadContextFromLocalStorage() {
|
|||||||
if (slippage) setVal("slippage", slippage);
|
if (slippage) setVal("slippage", slippage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function setVal(id, v) {
|
function setVal(id, v) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (el) el.value = v;
|
if (el) el.value = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function str(id) {
|
function str(id) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
return el ? String(el.value || "").trim() : "";
|
return el ? String(el.value || "").trim() : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function num(id) {
|
function num(id) {
|
||||||
const el = document.getElementById(id);
|
const el = document.getElementById(id);
|
||||||
if (!el) return null;
|
if (!el) return null;
|
||||||
@@ -98,14 +105,17 @@ function num(id) {
|
|||||||
return Number.isFinite(v) ? v : null;
|
return Number.isFinite(v) ? v : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function sleep(ms) {
|
function sleep(ms) {
|
||||||
return new Promise(res => setTimeout(res, ms));
|
return new Promise(res => setTimeout(res, ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// FETCH CATALOG
|
// FETCH CATALOG
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|
||||||
|
|
||||||
async function fetchCatalog() {
|
async function fetchCatalog() {
|
||||||
const symbol = str("symbol");
|
const symbol = str("symbol");
|
||||||
const timeframe = str("timeframe");
|
const timeframe = str("timeframe");
|
||||||
@@ -118,16 +128,19 @@ async function fetchCatalog() {
|
|||||||
console.log("[catalog] strategies:", STRATEGY_CATALOG.map(s => s.strategy_id));
|
console.log("[catalog] strategies:", STRATEGY_CATALOG.map(s => s.strategy_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// UI RENDERING
|
// UI RENDERING
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|
||||||
|
|
||||||
function initSlots() {
|
function initSlots() {
|
||||||
strategySlots = [
|
strategySlots = [
|
||||||
{ strategy_id: null, parameters: {} }
|
{ strategy_id: null, parameters: {} }
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function rerenderStrategySlots() {
|
function rerenderStrategySlots() {
|
||||||
|
|
||||||
const container = document.getElementById("strategies_container");
|
const container = document.getElementById("strategies_container");
|
||||||
@@ -157,6 +170,7 @@ function rerenderStrategySlots() {
|
|||||||
updateCombinationCounter();
|
updateCombinationCounter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderStrategySlot(index) {
|
function renderStrategySlot(index) {
|
||||||
|
|
||||||
const container = document.getElementById("strategies_container");
|
const container = document.getElementById("strategies_container");
|
||||||
@@ -206,6 +220,7 @@ function renderStrategySlot(index) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function onStrategySelected(index, strategyId) {
|
function onStrategySelected(index, strategyId) {
|
||||||
|
|
||||||
if (!strategyId) {
|
if (!strategyId) {
|
||||||
@@ -228,6 +243,7 @@ function onStrategySelected(index, strategyId) {
|
|||||||
updateCombinationCounter();
|
updateCombinationCounter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderParametersOnly(index, strategyId) {
|
function renderParametersOnly(index, strategyId) {
|
||||||
|
|
||||||
const paramsContainer = document.getElementById(`strategy_params_${index}`);
|
const paramsContainer = document.getElementById(`strategy_params_${index}`);
|
||||||
@@ -304,6 +320,7 @@ function renderParametersOnly(index, strategyId) {
|
|||||||
validateParameterInputs();
|
validateParameterInputs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function validateParameterInputs() {
|
function validateParameterInputs() {
|
||||||
|
|
||||||
let valid = true;
|
let valid = true;
|
||||||
@@ -380,6 +397,7 @@ function validateParameterInputs() {
|
|||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateCombinationCounter() {
|
function updateCombinationCounter() {
|
||||||
|
|
||||||
let hasAnyStrategy = false;
|
let hasAnyStrategy = false;
|
||||||
@@ -407,6 +425,7 @@ function updateCombinationCounter() {
|
|||||||
return globalTotal;
|
return globalTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function applyCombinationWarnings(total) {
|
function applyCombinationWarnings(total) {
|
||||||
|
|
||||||
const maxComb = parseInt(
|
const maxComb = parseInt(
|
||||||
@@ -425,6 +444,7 @@ function applyCombinationWarnings(total) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateTimeEstimate(totalComb) {
|
function updateTimeEstimate(totalComb) {
|
||||||
|
|
||||||
const trainDays = parseInt(
|
const trainDays = parseInt(
|
||||||
@@ -459,6 +479,7 @@ function updateTimeEstimate(totalComb) {
|
|||||||
if (el) el.textContent = label;
|
if (el) el.textContent = label;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function removeStrategySlot(index) {
|
function removeStrategySlot(index) {
|
||||||
|
|
||||||
strategySlots.splice(index, 1);
|
strategySlots.splice(index, 1);
|
||||||
@@ -466,10 +487,12 @@ function removeStrategySlot(index) {
|
|||||||
rerenderStrategySlots();
|
rerenderStrategySlots();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// PAYLOAD
|
// PAYLOAD
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|
||||||
|
|
||||||
function buildPayload() {
|
function buildPayload() {
|
||||||
const symbol = str("symbol");
|
const symbol = str("symbol");
|
||||||
const timeframe = str("timeframe");
|
const timeframe = str("timeframe");
|
||||||
@@ -530,6 +553,7 @@ function buildPayload() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function collectSelectedStrategies() {
|
function collectSelectedStrategies() {
|
||||||
|
|
||||||
const strategies = [];
|
const strategies = [];
|
||||||
@@ -581,33 +605,13 @@ function collectSelectedStrategies() {
|
|||||||
return strategies;
|
return strategies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function fetchAvailableStrategies() {
|
async function fetchAvailableStrategies() {
|
||||||
const res = await fetch("/api/v2/calibration/strategies/catalog");
|
const res = await fetch("/api/v2/calibration/strategies/catalog");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data.strategies || [];
|
return data.strategies || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function num(id) {
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (!el) return null;
|
|
||||||
const val = el.value;
|
|
||||||
if (val === "" || val === null || val === undefined) return null;
|
|
||||||
const n = Number(val);
|
|
||||||
return Number.isFinite(n) ? n : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function str(id) {
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (!el) return null;
|
|
||||||
const v = el.value;
|
|
||||||
return v === null || v === undefined ? null : String(v);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setVal(id, value) {
|
|
||||||
const el = document.getElementById(id);
|
|
||||||
if (!el) return;
|
|
||||||
el.value = value ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadFromStep2() {
|
function loadFromStep2() {
|
||||||
|
|
||||||
@@ -649,6 +653,7 @@ function loadFromStep2() {
|
|||||||
console.log("[calibration_strategies] Parameters loaded from Step 2 ✅");
|
console.log("[calibration_strategies] Parameters loaded from Step 2 ✅");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function updateStopUI() {
|
function updateStopUI() {
|
||||||
|
|
||||||
const type = document.getElementById("stop_type").value;
|
const type = document.getElementById("stop_type").value;
|
||||||
@@ -670,6 +675,7 @@ function updateStopUI() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function loadStrategyCatalog() {
|
async function loadStrategyCatalog() {
|
||||||
|
|
||||||
const res = await fetch("/api/v2/calibration/strategies/catalog");
|
const res = await fetch("/api/v2/calibration/strategies/catalog");
|
||||||
@@ -678,6 +684,7 @@ async function loadStrategyCatalog() {
|
|||||||
STRATEGY_CATALOG = data.strategies;
|
STRATEGY_CATALOG = data.strategies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function addStrategySlot() {
|
function addStrategySlot() {
|
||||||
|
|
||||||
// Si ya hay un slot vacío al final, no crear otro
|
// Si ya hay un slot vacío al final, no crear otro
|
||||||
@@ -698,10 +705,12 @@ function addStrategySlot() {
|
|||||||
renderStrategySlot(index);
|
renderStrategySlot(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// PROGRESS BAR
|
// PROGRESS BAR
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|
||||||
|
|
||||||
function startWF() {
|
function startWF() {
|
||||||
document
|
document
|
||||||
.getElementById("wf_progress_card")
|
.getElementById("wf_progress_card")
|
||||||
@@ -711,6 +720,7 @@ function startWF() {
|
|||||||
document.getElementById("wfProgressBar").innerText = "0%";
|
document.getElementById("wfProgressBar").innerText = "0%";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function pollStatus(jobId) {
|
async function pollStatus(jobId) {
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
const res = await fetch(`/api/v2/calibration/strategies/status/${jobId}`);
|
const res = await fetch(`/api/v2/calibration/strategies/status/${jobId}`);
|
||||||
@@ -733,6 +743,7 @@ async function pollStatus(jobId) {
|
|||||||
// DIFFERENT RENDER PLOTS
|
// DIFFERENT RENDER PLOTS
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|
||||||
|
|
||||||
function renderEquityAndReturns(strategyId, s, data) {
|
function renderEquityAndReturns(strategyId, s, data) {
|
||||||
const equity = s.window_equity || [];
|
const equity = s.window_equity || [];
|
||||||
const ret = s.window_returns_pct || [];
|
const ret = s.window_returns_pct || [];
|
||||||
@@ -1090,6 +1101,7 @@ function renderEquityAndReturns(strategyId, s, data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderRollingSharpe(strategyId, s, data) {
|
function renderRollingSharpe(strategyId, s, data) {
|
||||||
const roll = s.diagnostics?.rolling?.rolling_sharpe_like || [];
|
const roll = s.diagnostics?.rolling?.rolling_sharpe_like || [];
|
||||||
const x = roll.map((_, i) => i + 1);
|
const x = roll.map((_, i) => i + 1);
|
||||||
@@ -1118,6 +1130,7 @@ function renderRollingSharpe(strategyId, s, data) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderOOSReturnsDistribution(strategyId, s, data) {
|
function renderOOSReturnsDistribution(strategyId, s, data) {
|
||||||
const edges = s.diagnostics?.distribution?.hist_bin_edges || [];
|
const edges = s.diagnostics?.distribution?.hist_bin_edges || [];
|
||||||
const counts = s.diagnostics?.distribution?.hist_counts || [];
|
const counts = s.diagnostics?.distribution?.hist_counts || [];
|
||||||
@@ -1137,6 +1150,7 @@ function renderOOSReturnsDistribution(strategyId, s, data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderDrawdownEvolution(strategyId, s, data) {
|
function renderDrawdownEvolution(strategyId, s, data) {
|
||||||
const dd = s.diagnostics?.drawdown?.drawdown_pct || [];
|
const dd = s.diagnostics?.drawdown?.drawdown_pct || [];
|
||||||
const x = dd.map((_, i) => i);
|
const x = dd.map((_, i) => i);
|
||||||
@@ -1155,6 +1169,7 @@ function renderDrawdownEvolution(strategyId, s, data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderTradeDensity(strategyId, s, data) {
|
function renderTradeDensity(strategyId, s, data) {
|
||||||
const tpw = s.diagnostics?.trades?.trades_per_window || [];
|
const tpw = s.diagnostics?.trades?.trades_per_window || [];
|
||||||
const tpd = s.diagnostics?.trades?.trades_per_day || [];
|
const tpd = s.diagnostics?.trades?.trades_per_day || [];
|
||||||
@@ -1203,10 +1218,12 @@ function renderTradeDensity(strategyId, s, data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// RENDER RESULTS
|
// RENDER RESULTS
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|
||||||
|
|
||||||
function renderStrategiesList(strategies) {
|
function renderStrategiesList(strategies) {
|
||||||
const list = document.getElementById("strategies_list");
|
const list = document.getElementById("strategies_list");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
@@ -1245,6 +1262,7 @@ function renderStrategiesList(strategies) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function setBadge(status) {
|
function setBadge(status) {
|
||||||
const badge = document.getElementById("strategies_status_badge");
|
const badge = document.getElementById("strategies_status_badge");
|
||||||
if (!badge) return;
|
if (!badge) return;
|
||||||
@@ -1256,6 +1274,7 @@ function setBadge(status) {
|
|||||||
badge.textContent = status ? status.toUpperCase() : "—";
|
badge.textContent = status ? status.toUpperCase() : "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderResultsTable(data) {
|
function renderResultsTable(data) {
|
||||||
const wrap = document.getElementById("strategies_table_wrap");
|
const wrap = document.getElementById("strategies_table_wrap");
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
@@ -1300,6 +1319,7 @@ function renderResultsTable(data) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function populatePlotSelector(data) {
|
function populatePlotSelector(data) {
|
||||||
const sel = document.getElementById("plot_strategy_select");
|
const sel = document.getElementById("plot_strategy_select");
|
||||||
if (!sel) return;
|
if (!sel) return;
|
||||||
@@ -1326,6 +1346,7 @@ function populatePlotSelector(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function selectStrategy(strategyId, data) {
|
function selectStrategy(strategyId, data) {
|
||||||
if (!strategyId || !data) return;
|
if (!strategyId || !data) return;
|
||||||
|
|
||||||
@@ -1385,9 +1406,11 @@ function selectStrategy(strategyId, data) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderValidateResponse(data) {
|
function renderValidateResponse(data) {
|
||||||
|
|
||||||
lastValidationResult = data;
|
lastValidationResult = data;
|
||||||
|
window.lastStrategiesResult = Array.isArray(data.results) ? data.results : [];
|
||||||
|
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
// 1️⃣ Badge + message
|
// 1️⃣ Badge + message
|
||||||
@@ -1506,6 +1529,7 @@ function renderValidateResponse(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderChart(chartType, strategyId, s, data) {
|
function renderChart(chartType, strategyId, s, data) {
|
||||||
switch (chartType) {
|
switch (chartType) {
|
||||||
case "equity":
|
case "equity":
|
||||||
@@ -1529,6 +1553,7 @@ function renderChart(chartType, strategyId, s, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function highlightSelectedRow(strategyId) {
|
function highlightSelectedRow(strategyId) {
|
||||||
|
|
||||||
document.querySelectorAll(".strategy-row").forEach(el => {
|
document.querySelectorAll(".strategy-row").forEach(el => {
|
||||||
@@ -1544,6 +1569,7 @@ function highlightSelectedRow(strategyId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function validateStrategies() {
|
async function validateStrategies() {
|
||||||
console.log("[calibration_strategies] validateStrategies() START");
|
console.log("[calibration_strategies] validateStrategies() START");
|
||||||
|
|
||||||
@@ -1639,6 +1665,7 @@ async function validateStrategies() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function generateReport() {
|
async function generateReport() {
|
||||||
console.log("[calibration_strategies] generateReport() START");
|
console.log("[calibration_strategies] generateReport() START");
|
||||||
|
|
||||||
@@ -1669,6 +1696,7 @@ async function generateReport() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function wireButtons() {
|
function wireButtons() {
|
||||||
document.getElementById("validate_strategies_btn")?.addEventListener("click", validateStrategies);
|
document.getElementById("validate_strategies_btn")?.addEventListener("click", validateStrategies);
|
||||||
document.getElementById("report_strategies_btn")?.addEventListener("click", generateReport);
|
document.getElementById("report_strategies_btn")?.addEventListener("click", generateReport);
|
||||||
@@ -1690,6 +1718,7 @@ function wireButtons() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function applyInheritedLock() {
|
function applyInheritedLock() {
|
||||||
const locked = document.getElementById("lock_inherited").checked;
|
const locked = document.getElementById("lock_inherited").checked;
|
||||||
const fields = document.querySelectorAll(".inherited-field");
|
const fields = document.querySelectorAll(".inherited-field");
|
||||||
@@ -1704,6 +1733,7 @@ function applyInheritedLock() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
await loadStrategyCatalog();
|
await loadStrategyCatalog();
|
||||||
addStrategySlot();
|
addStrategySlot();
|
||||||
@@ -1716,12 +1746,16 @@ async function init() {
|
|||||||
.addEventListener("change", updateStopUI);
|
.addEventListener("change", updateStopUI);
|
||||||
|
|
||||||
wireButtons();
|
wireButtons();
|
||||||
|
wirePromotionUI();
|
||||||
|
|
||||||
document.getElementById("plot_strategy_select").addEventListener("change", function() {
|
document.getElementById("plot_strategy_select").addEventListener("change", function() {
|
||||||
const chartType = this.value;
|
if (!lastValidationResult || !selectedStrategyId) return;
|
||||||
const strategyData = lastValidationResult.series.strategies[selectedStrategyId];
|
|
||||||
|
const strategyData = lastValidationResult?.series?.strategies?.[selectedStrategyId];
|
||||||
|
if (!strategyData) return;
|
||||||
|
|
||||||
|
const chartType = this.value;
|
||||||
|
|
||||||
// Verifica que selectedStrategyId tenga el valor correcto
|
|
||||||
console.log("selectedStrategyId:", selectedStrategyId);
|
console.log("selectedStrategyId:", selectedStrategyId);
|
||||||
console.log("Strategy Data:", strategyData);
|
console.log("Strategy Data:", strategyData);
|
||||||
|
|
||||||
@@ -1738,20 +1772,24 @@ async function init() {
|
|||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// MARKET REGIME
|
// MARKET REGIME
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|
||||||
|
|
||||||
function fmtPct(v, digits = 2) {
|
function fmtPct(v, digits = 2) {
|
||||||
const n = Number(v ?? 0);
|
const n = Number(v ?? 0);
|
||||||
return `${n.toFixed(digits)}%`;
|
return `${n.toFixed(digits)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function fmtNum(v, digits = 2) {
|
function fmtNum(v, digits = 2) {
|
||||||
const n = Number(v ?? 0);
|
const n = Number(v ?? 0);
|
||||||
return n.toFixed(digits);
|
return n.toFixed(digits);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function regimeBadgeClass(regime) {
|
function regimeBadgeClass(regime) {
|
||||||
switch (regime) {
|
switch (regime) {
|
||||||
case "bull":
|
case "bull":
|
||||||
@@ -1767,6 +1805,7 @@ function regimeBadgeClass(regime) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function regimeLabel(regime) {
|
function regimeLabel(regime) {
|
||||||
switch (regime) {
|
switch (regime) {
|
||||||
case "bull_strong": return "Bull strong";
|
case "bull_strong": return "Bull strong";
|
||||||
@@ -1779,6 +1818,7 @@ function regimeLabel(regime) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function ensureRegimeContainer() {
|
function ensureRegimeContainer() {
|
||||||
let el = document.getElementById("regime_analysis_wrap");
|
let el = document.getElementById("regime_analysis_wrap");
|
||||||
if (el) return el;
|
if (el) return el;
|
||||||
@@ -1794,11 +1834,13 @@ function ensureRegimeContainer() {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function clearRegimeSummary() {
|
function clearRegimeSummary() {
|
||||||
const el = document.getElementById("regime_analysis_wrap");
|
const el = document.getElementById("regime_analysis_wrap");
|
||||||
if (el) el.innerHTML = "";
|
if (el) el.innerHTML = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function getBestRegime(perf) {
|
function getBestRegime(perf) {
|
||||||
const candidates = ["bull", "sideways", "bear"]
|
const candidates = ["bull", "sideways", "bear"]
|
||||||
.map((k) => ({
|
.map((k) => ({
|
||||||
@@ -1814,6 +1856,7 @@ function getBestRegime(perf) {
|
|||||||
return candidates[0].regime;
|
return candidates[0].regime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderRegimeSummary(strategyId, s, data) {
|
function renderRegimeSummary(strategyId, s, data) {
|
||||||
const wrap = ensureRegimeContainer();
|
const wrap = ensureRegimeContainer();
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
@@ -1951,10 +1994,90 @@ function renderRegimeSummary(strategyId, s, data) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =================================================
|
||||||
|
// STRATEGY PROMOTION
|
||||||
|
// =================================================
|
||||||
|
|
||||||
|
|
||||||
|
async function runPromotion() {
|
||||||
|
|
||||||
|
if (!Array.isArray(window.lastStrategiesResult) || window.lastStrategiesResult.length === 0) {
|
||||||
|
alert("Primero ejecuta Validate Strategies para generar resultados.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
strategies: window.lastStrategiesResult,
|
||||||
|
promotion: {
|
||||||
|
promote_score_threshold: 70,
|
||||||
|
review_score_threshold: 55
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch('/api/v2/calibration/strategies/promote', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errText = await res.text();
|
||||||
|
throw new Error(`Promotion failed: ${res.status} ${errText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
renderPromotionResults(data.results || []);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
alert(`Promotion error: ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function renderPromotionResults(results) {
|
||||||
|
|
||||||
|
const table = document.querySelector("#promotionTable tbody");
|
||||||
|
table.innerHTML = "";
|
||||||
|
|
||||||
|
results.forEach(r => {
|
||||||
|
|
||||||
|
const tr = document.createElement("tr");
|
||||||
|
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${r.rank}</td>
|
||||||
|
<td>${r.strategy_id}</td>
|
||||||
|
<td>${r.score}</td>
|
||||||
|
<td>${r.status}</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
table.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
const promoted = results.filter(r => r.status === "promote").length;
|
||||||
|
const review = results.filter(r => r.status === "review").length;
|
||||||
|
const reject = results.filter(r => r.status === "reject").length;
|
||||||
|
|
||||||
|
document.getElementById("promotionSummary").innerHTML =
|
||||||
|
`Promoted: ${promoted} | Review: ${review} | Rejected: ${reject}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function wirePromotionUI() {
|
||||||
|
const btn = document.getElementById("btnRunPromotion");
|
||||||
|
if (btn) {
|
||||||
|
btn.addEventListener("click", runPromotion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// PLOT ALERTS (Tabler) + SAFE PLOT CLEAR
|
// PLOT ALERTS (Tabler) + SAFE PLOT CLEAR
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|
||||||
|
|
||||||
function ensurePlotAlertContainer() {
|
function ensurePlotAlertContainer() {
|
||||||
// Lo colocamos antes del primer plot si existe
|
// Lo colocamos antes del primer plot si existe
|
||||||
let el = document.getElementById("plot_alert");
|
let el = document.getElementById("plot_alert");
|
||||||
@@ -1970,6 +2093,7 @@ function ensurePlotAlertContainer() {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function escapeHtml(str) {
|
function escapeHtml(str) {
|
||||||
return String(str ?? "")
|
return String(str ?? "")
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
@@ -1979,6 +2103,7 @@ function escapeHtml(str) {
|
|||||||
.replaceAll("'", "'");
|
.replaceAll("'", "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function showPlotAlert(type, title, message, warnings) {
|
function showPlotAlert(type, title, message, warnings) {
|
||||||
const el = ensurePlotAlertContainer();
|
const el = ensurePlotAlertContainer();
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
@@ -1998,11 +2123,13 @@ function showPlotAlert(type, title, message, warnings) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function clearPlotAlert() {
|
function clearPlotAlert() {
|
||||||
const el = document.getElementById("plot_alert");
|
const el = document.getElementById("plot_alert");
|
||||||
if (el) el.innerHTML = "";
|
if (el) el.innerHTML = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function clearPlots() {
|
function clearPlots() {
|
||||||
const eq = document.getElementById("plot_strategy");
|
const eq = document.getElementById("plot_strategy");
|
||||||
if (eq) eq.innerHTML = "";
|
if (eq) eq.innerHTML = "";
|
||||||
@@ -2010,6 +2137,7 @@ function clearPlots() {
|
|||||||
clearRegimeSummary();
|
clearRegimeSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
document.getElementById("lock_inherited")
|
document.getElementById("lock_inherited")
|
||||||
.addEventListener("change", applyInheritedLock);
|
.addEventListener("change", applyInheritedLock);
|
||||||
|
|
||||||
|
|||||||
@@ -290,6 +290,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- Strategy Promotion / Selection -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Strategy Promotion / Selection</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<button id="btnRunPromotion" class="btn btn-primary">
|
||||||
|
Run Promotion
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="promotionSummary" class="mt-3"></div>
|
||||||
|
|
||||||
|
<table class="table mt-3" id="promotionTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Rank</th>
|
||||||
|
<th>Strategy</th>
|
||||||
|
<th>Score</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ========================= -->
|
<!-- ========================= -->
|
||||||
<!-- Results -->
|
<!-- Results -->
|
||||||
<!-- ========================= -->
|
<!-- ========================= -->
|
||||||
|
|||||||
Reference in New Issue
Block a user