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,
|
||||
)
|
||||
from src.calibration.reports.strategies_report import generate_strategies_report_pdf
|
||||
from src.calibration.strategy_promotion import rank_strategies
|
||||
|
||||
from ..schemas.calibration_strategies import (
|
||||
CalibrationStrategiesInspectRequest,
|
||||
CalibrationStrategiesInspectResponse,
|
||||
CalibrationStrategiesValidateResponse,
|
||||
CalibrationStrategiesPromoteRequest,
|
||||
CalibrationStrategiesPromoteResponse,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("tradingbot.api.v2")
|
||||
@@ -94,6 +97,7 @@ def inspect_strategies(
|
||||
)
|
||||
return CalibrationStrategiesInspectResponse(**result)
|
||||
|
||||
|
||||
@router.post("/validate", response_model=CalibrationStrategiesValidateResponse)
|
||||
def validate_strategies(
|
||||
payload: CalibrationStrategiesInspectRequest,
|
||||
@@ -115,6 +119,13 @@ def validate_strategies(
|
||||
)
|
||||
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")
|
||||
def report_strategies(
|
||||
payload: CalibrationStrategiesInspectRequest,
|
||||
@@ -178,6 +189,7 @@ def report_strategies(
|
||||
public_url = f"/reports/strategies/{safe_symbol}/{filename}"
|
||||
return JSONResponse(content={"status": result.get("status", "ok"), "url": public_url})
|
||||
|
||||
|
||||
@router.post("/run")
|
||||
def run_strategies_async(
|
||||
payload: CalibrationStrategiesInspectRequest,
|
||||
@@ -247,6 +259,7 @@ def run_strategies_async(
|
||||
|
||||
return {"job_id": job_id}
|
||||
|
||||
|
||||
@router.get("/status/{job_id}")
|
||||
def get_status(job_id: str):
|
||||
return WF_JOBS.get(job_id, {"status": "unknown"})
|
||||
|
||||
@@ -85,3 +85,26 @@ class CalibrationStrategiesInspectResponse(BaseModel):
|
||||
|
||||
class CalibrationStrategiesValidateResponse(CalibrationStrategiesInspectResponse):
|
||||
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
|
||||
// =================================================
|
||||
|
||||
|
||||
function enableNextStep() {
|
||||
const btn = document.getElementById("next-step-btn");
|
||||
if (!btn) return;
|
||||
@@ -20,6 +21,7 @@ function enableNextStep() {
|
||||
btn.setAttribute("aria-disabled", "false");
|
||||
}
|
||||
|
||||
|
||||
function disableNextStep() {
|
||||
const btn = document.getElementById("next-step-btn");
|
||||
if (!btn) return;
|
||||
@@ -28,10 +30,12 @@ function disableNextStep() {
|
||||
btn.setAttribute("aria-disabled", "true");
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// UTILS
|
||||
// =================================================
|
||||
|
||||
|
||||
function loadContextFromLocalStorage() {
|
||||
const symbol = localStorage.getItem("calibration.symbol");
|
||||
const timeframe = localStorage.getItem("calibration.timeframe");
|
||||
@@ -81,16 +85,19 @@ function loadContextFromLocalStorage() {
|
||||
if (slippage) setVal("slippage", slippage);
|
||||
}
|
||||
|
||||
|
||||
function setVal(id, v) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.value = v;
|
||||
}
|
||||
|
||||
|
||||
function str(id) {
|
||||
const el = document.getElementById(id);
|
||||
return el ? String(el.value || "").trim() : "";
|
||||
}
|
||||
|
||||
|
||||
function num(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return null;
|
||||
@@ -98,14 +105,17 @@ function num(id) {
|
||||
return Number.isFinite(v) ? v : null;
|
||||
}
|
||||
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(res => setTimeout(res, ms));
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// FETCH CATALOG
|
||||
// =================================================
|
||||
|
||||
|
||||
async function fetchCatalog() {
|
||||
const symbol = str("symbol");
|
||||
const timeframe = str("timeframe");
|
||||
@@ -118,16 +128,19 @@ async function fetchCatalog() {
|
||||
console.log("[catalog] strategies:", STRATEGY_CATALOG.map(s => s.strategy_id));
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// UI RENDERING
|
||||
// =================================================
|
||||
|
||||
|
||||
function initSlots() {
|
||||
strategySlots = [
|
||||
{ strategy_id: null, parameters: {} }
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
function rerenderStrategySlots() {
|
||||
|
||||
const container = document.getElementById("strategies_container");
|
||||
@@ -157,6 +170,7 @@ function rerenderStrategySlots() {
|
||||
updateCombinationCounter();
|
||||
}
|
||||
|
||||
|
||||
function renderStrategySlot(index) {
|
||||
|
||||
const container = document.getElementById("strategies_container");
|
||||
@@ -206,6 +220,7 @@ function renderStrategySlot(index) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onStrategySelected(index, strategyId) {
|
||||
|
||||
if (!strategyId) {
|
||||
@@ -228,6 +243,7 @@ function onStrategySelected(index, strategyId) {
|
||||
updateCombinationCounter();
|
||||
}
|
||||
|
||||
|
||||
function renderParametersOnly(index, strategyId) {
|
||||
|
||||
const paramsContainer = document.getElementById(`strategy_params_${index}`);
|
||||
@@ -304,6 +320,7 @@ function renderParametersOnly(index, strategyId) {
|
||||
validateParameterInputs();
|
||||
}
|
||||
|
||||
|
||||
function validateParameterInputs() {
|
||||
|
||||
let valid = true;
|
||||
@@ -380,6 +397,7 @@ function validateParameterInputs() {
|
||||
return valid;
|
||||
}
|
||||
|
||||
|
||||
function updateCombinationCounter() {
|
||||
|
||||
let hasAnyStrategy = false;
|
||||
@@ -407,6 +425,7 @@ function updateCombinationCounter() {
|
||||
return globalTotal;
|
||||
}
|
||||
|
||||
|
||||
function applyCombinationWarnings(total) {
|
||||
|
||||
const maxComb = parseInt(
|
||||
@@ -425,6 +444,7 @@ function applyCombinationWarnings(total) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateTimeEstimate(totalComb) {
|
||||
|
||||
const trainDays = parseInt(
|
||||
@@ -459,6 +479,7 @@ function updateTimeEstimate(totalComb) {
|
||||
if (el) el.textContent = label;
|
||||
}
|
||||
|
||||
|
||||
function removeStrategySlot(index) {
|
||||
|
||||
strategySlots.splice(index, 1);
|
||||
@@ -466,10 +487,12 @@ function removeStrategySlot(index) {
|
||||
rerenderStrategySlots();
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// PAYLOAD
|
||||
// =================================================
|
||||
|
||||
|
||||
function buildPayload() {
|
||||
const symbol = str("symbol");
|
||||
const timeframe = str("timeframe");
|
||||
@@ -530,6 +553,7 @@ function buildPayload() {
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function collectSelectedStrategies() {
|
||||
|
||||
const strategies = [];
|
||||
@@ -581,33 +605,13 @@ function collectSelectedStrategies() {
|
||||
return strategies;
|
||||
}
|
||||
|
||||
|
||||
async function fetchAvailableStrategies() {
|
||||
const res = await fetch("/api/v2/calibration/strategies/catalog");
|
||||
const data = await res.json();
|
||||
return data.strategies || [];
|
||||
}
|
||||
|
||||
function num(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return null;
|
||||
const val = el.value;
|
||||
if (val === "" || val === null || val === undefined) return null;
|
||||
const n = Number(val);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
}
|
||||
|
||||
function str(id) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return null;
|
||||
const v = el.value;
|
||||
return v === null || v === undefined ? null : String(v);
|
||||
}
|
||||
|
||||
function setVal(id, value) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) return;
|
||||
el.value = value ?? "";
|
||||
}
|
||||
|
||||
function loadFromStep2() {
|
||||
|
||||
@@ -649,6 +653,7 @@ function loadFromStep2() {
|
||||
console.log("[calibration_strategies] Parameters loaded from Step 2 ✅");
|
||||
}
|
||||
|
||||
|
||||
function updateStopUI() {
|
||||
|
||||
const type = document.getElementById("stop_type").value;
|
||||
@@ -670,6 +675,7 @@ function updateStopUI() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function loadStrategyCatalog() {
|
||||
|
||||
const res = await fetch("/api/v2/calibration/strategies/catalog");
|
||||
@@ -678,6 +684,7 @@ async function loadStrategyCatalog() {
|
||||
STRATEGY_CATALOG = data.strategies;
|
||||
}
|
||||
|
||||
|
||||
function addStrategySlot() {
|
||||
|
||||
// Si ya hay un slot vacío al final, no crear otro
|
||||
@@ -698,10 +705,12 @@ function addStrategySlot() {
|
||||
renderStrategySlot(index);
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// PROGRESS BAR
|
||||
// =================================================
|
||||
|
||||
|
||||
function startWF() {
|
||||
document
|
||||
.getElementById("wf_progress_card")
|
||||
@@ -711,6 +720,7 @@ function startWF() {
|
||||
document.getElementById("wfProgressBar").innerText = "0%";
|
||||
}
|
||||
|
||||
|
||||
async function pollStatus(jobId) {
|
||||
const interval = setInterval(async () => {
|
||||
const res = await fetch(`/api/v2/calibration/strategies/status/${jobId}`);
|
||||
@@ -733,6 +743,7 @@ async function pollStatus(jobId) {
|
||||
// DIFFERENT RENDER PLOTS
|
||||
// =================================================
|
||||
|
||||
|
||||
function renderEquityAndReturns(strategyId, s, data) {
|
||||
const equity = s.window_equity || [];
|
||||
const ret = s.window_returns_pct || [];
|
||||
@@ -1090,6 +1101,7 @@ function renderEquityAndReturns(strategyId, s, data) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function renderRollingSharpe(strategyId, s, data) {
|
||||
const roll = s.diagnostics?.rolling?.rolling_sharpe_like || [];
|
||||
const x = roll.map((_, i) => i + 1);
|
||||
@@ -1118,6 +1130,7 @@ function renderRollingSharpe(strategyId, s, data) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function renderOOSReturnsDistribution(strategyId, s, data) {
|
||||
const edges = s.diagnostics?.distribution?.hist_bin_edges || [];
|
||||
const counts = s.diagnostics?.distribution?.hist_counts || [];
|
||||
@@ -1137,6 +1150,7 @@ function renderOOSReturnsDistribution(strategyId, s, data) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function renderDrawdownEvolution(strategyId, s, data) {
|
||||
const dd = s.diagnostics?.drawdown?.drawdown_pct || [];
|
||||
const x = dd.map((_, i) => i);
|
||||
@@ -1155,6 +1169,7 @@ function renderDrawdownEvolution(strategyId, s, data) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function renderTradeDensity(strategyId, s, data) {
|
||||
const tpw = s.diagnostics?.trades?.trades_per_window || [];
|
||||
const tpd = s.diagnostics?.trades?.trades_per_day || [];
|
||||
@@ -1203,10 +1218,12 @@ function renderTradeDensity(strategyId, s, data) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// RENDER RESULTS
|
||||
// =================================================
|
||||
|
||||
|
||||
function renderStrategiesList(strategies) {
|
||||
const list = document.getElementById("strategies_list");
|
||||
if (!list) return;
|
||||
@@ -1245,6 +1262,7 @@ function renderStrategiesList(strategies) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function setBadge(status) {
|
||||
const badge = document.getElementById("strategies_status_badge");
|
||||
if (!badge) return;
|
||||
@@ -1256,6 +1274,7 @@ function setBadge(status) {
|
||||
badge.textContent = status ? status.toUpperCase() : "—";
|
||||
}
|
||||
|
||||
|
||||
function renderResultsTable(data) {
|
||||
const wrap = document.getElementById("strategies_table_wrap");
|
||||
if (!wrap) return;
|
||||
@@ -1300,6 +1319,7 @@ function renderResultsTable(data) {
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
function populatePlotSelector(data) {
|
||||
const sel = document.getElementById("plot_strategy_select");
|
||||
if (!sel) return;
|
||||
@@ -1326,6 +1346,7 @@ function populatePlotSelector(data) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function selectStrategy(strategyId, data) {
|
||||
if (!strategyId || !data) return;
|
||||
|
||||
@@ -1385,9 +1406,11 @@ function selectStrategy(strategyId, data) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function renderValidateResponse(data) {
|
||||
|
||||
lastValidationResult = data;
|
||||
window.lastStrategiesResult = Array.isArray(data.results) ? data.results : [];
|
||||
|
||||
// -------------------------------
|
||||
// 1️⃣ Badge + message
|
||||
@@ -1506,6 +1529,7 @@ function renderValidateResponse(data) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderChart(chartType, strategyId, s, data) {
|
||||
switch (chartType) {
|
||||
case "equity":
|
||||
@@ -1529,6 +1553,7 @@ function renderChart(chartType, strategyId, s, data) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function highlightSelectedRow(strategyId) {
|
||||
|
||||
document.querySelectorAll(".strategy-row").forEach(el => {
|
||||
@@ -1544,6 +1569,7 @@ function highlightSelectedRow(strategyId) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function validateStrategies() {
|
||||
console.log("[calibration_strategies] validateStrategies() START");
|
||||
|
||||
@@ -1639,6 +1665,7 @@ async function validateStrategies() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function generateReport() {
|
||||
console.log("[calibration_strategies] generateReport() START");
|
||||
|
||||
@@ -1669,6 +1696,7 @@ async function generateReport() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function wireButtons() {
|
||||
document.getElementById("validate_strategies_btn")?.addEventListener("click", validateStrategies);
|
||||
document.getElementById("report_strategies_btn")?.addEventListener("click", generateReport);
|
||||
@@ -1690,6 +1718,7 @@ function wireButtons() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function applyInheritedLock() {
|
||||
const locked = document.getElementById("lock_inherited").checked;
|
||||
const fields = document.querySelectorAll(".inherited-field");
|
||||
@@ -1704,6 +1733,7 @@ function applyInheritedLock() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async function init() {
|
||||
await loadStrategyCatalog();
|
||||
addStrategySlot();
|
||||
@@ -1716,12 +1746,16 @@ async function init() {
|
||||
.addEventListener("change", updateStopUI);
|
||||
|
||||
wireButtons();
|
||||
wirePromotionUI();
|
||||
|
||||
document.getElementById("plot_strategy_select").addEventListener("change", function() {
|
||||
const chartType = this.value;
|
||||
const strategyData = lastValidationResult.series.strategies[selectedStrategyId];
|
||||
if (!lastValidationResult || !selectedStrategyId) return;
|
||||
|
||||
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("Strategy Data:", strategyData);
|
||||
|
||||
@@ -1738,20 +1772,24 @@ async function init() {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// MARKET REGIME
|
||||
// =================================================
|
||||
|
||||
|
||||
function fmtPct(v, digits = 2) {
|
||||
const n = Number(v ?? 0);
|
||||
return `${n.toFixed(digits)}%`;
|
||||
}
|
||||
|
||||
|
||||
function fmtNum(v, digits = 2) {
|
||||
const n = Number(v ?? 0);
|
||||
return n.toFixed(digits);
|
||||
}
|
||||
|
||||
|
||||
function regimeBadgeClass(regime) {
|
||||
switch (regime) {
|
||||
case "bull":
|
||||
@@ -1767,6 +1805,7 @@ function regimeBadgeClass(regime) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function regimeLabel(regime) {
|
||||
switch (regime) {
|
||||
case "bull_strong": return "Bull strong";
|
||||
@@ -1779,6 +1818,7 @@ function regimeLabel(regime) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function ensureRegimeContainer() {
|
||||
let el = document.getElementById("regime_analysis_wrap");
|
||||
if (el) return el;
|
||||
@@ -1794,11 +1834,13 @@ function ensureRegimeContainer() {
|
||||
return el;
|
||||
}
|
||||
|
||||
|
||||
function clearRegimeSummary() {
|
||||
const el = document.getElementById("regime_analysis_wrap");
|
||||
if (el) el.innerHTML = "";
|
||||
}
|
||||
|
||||
|
||||
function getBestRegime(perf) {
|
||||
const candidates = ["bull", "sideways", "bear"]
|
||||
.map((k) => ({
|
||||
@@ -1814,6 +1856,7 @@ function getBestRegime(perf) {
|
||||
return candidates[0].regime;
|
||||
}
|
||||
|
||||
|
||||
function renderRegimeSummary(strategyId, s, data) {
|
||||
const wrap = ensureRegimeContainer();
|
||||
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
|
||||
// =================================================
|
||||
|
||||
|
||||
function ensurePlotAlertContainer() {
|
||||
// Lo colocamos antes del primer plot si existe
|
||||
let el = document.getElementById("plot_alert");
|
||||
@@ -1970,6 +2093,7 @@ function ensurePlotAlertContainer() {
|
||||
return el;
|
||||
}
|
||||
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str ?? "")
|
||||
.replaceAll("&", "&")
|
||||
@@ -1979,6 +2103,7 @@ function escapeHtml(str) {
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
|
||||
function showPlotAlert(type, title, message, warnings) {
|
||||
const el = ensurePlotAlertContainer();
|
||||
if (!el) return;
|
||||
@@ -1998,11 +2123,13 @@ function showPlotAlert(type, title, message, warnings) {
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
function clearPlotAlert() {
|
||||
const el = document.getElementById("plot_alert");
|
||||
if (el) el.innerHTML = "";
|
||||
}
|
||||
|
||||
|
||||
function clearPlots() {
|
||||
const eq = document.getElementById("plot_strategy");
|
||||
if (eq) eq.innerHTML = "";
|
||||
@@ -2010,6 +2137,7 @@ function clearPlots() {
|
||||
clearRegimeSummary();
|
||||
}
|
||||
|
||||
|
||||
document.getElementById("lock_inherited")
|
||||
.addEventListener("change", applyInheritedLock);
|
||||
|
||||
|
||||
@@ -290,6 +290,36 @@
|
||||
</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 -->
|
||||
<!-- ========================= -->
|
||||
|
||||
Reference in New Issue
Block a user