Strategy promotion added

This commit is contained in:
dam
2026-03-08 12:40:35 +01:00
parent a42255d58c
commit ca36383bb3
5 changed files with 286 additions and 24 deletions

View 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

View File

@@ -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"})

View File

@@ -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]

View File

@@ -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("&", "&amp;") .replaceAll("&", "&amp;")
@@ -1979,6 +2103,7 @@ function escapeHtml(str) {
.replaceAll("'", "&#039;"); .replaceAll("'", "&#039;");
} }
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);

View File

@@ -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 -->
<!-- ========================= --> <!-- ========================= -->