feat(calibration-step3): complete Strategy Promotion workflow and prepare Step 4 entry
Step 3 is now functionally complete and ready to hand off selected strategies to the optimization stage (Step 4). Main additions and improvements: Strategy Promotion - Added promotion engine results visualization. - Promotion statuses supported: promote, review, review_diversity, reject. - Diversity filter included in promotion results. Manual Step 4 selection - Added "Step 4" checkbox column in Promotion table. - Default selection rules: - promote → selected automatically - review / review_diversity / reject → unselected by default - User can manually override any selection. - Selection source tracked (auto, manual_on, manual_off). Step 4 selection artifact - Implemented Step 4 selection builder. - Generates JSON artifact with selected strategies and parameters. - Includes promotion metadata (status, score, diversity info). - Preview shown directly in UI. Dataset metadata integration - Added backend endpoint: GET /api/v2/calibration/strategies/data-info - Returns dataset metadata: start_date, end_date, n_rows, n_days. - UI now displays dataset information: Dataset: start → end | rows | days | WF windows. Walk-forward estimation improvements - WF windows calculated using real dataset size. - Time estimation updated with calibrated formula: ~1.5 seconds per window per strategy. - Time estimate now robust even without date inputs. UI improvements - Dataset info line added to calibration page. - Promotion table extended with Step 4 controls. - Step 4 selection summary and JSON preview added. Architecture - Step 3 now outputs a clean Step 4 selection artifact. - Promotion acts as recommendation, not final decision. - Human override supported. This commit closes the functional implementation of Step 3 and prepares the pipeline for Step 4 (strategy parameter optimization).
This commit is contained in:
@@ -5,8 +5,9 @@ import re
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
import pandas as pd
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Query
|
||||
from fastapi.responses import JSONResponse, HTMLResponse
|
||||
|
||||
from src.data.storage import StorageManager
|
||||
@@ -76,6 +77,64 @@ def strategy_catalog():
|
||||
return {"strategies": enriched}
|
||||
|
||||
|
||||
@router.get("/status/{job_id}")
|
||||
def get_status(job_id: str):
|
||||
return WF_JOBS.get(job_id, {"status": "unknown"})
|
||||
|
||||
|
||||
@router.get("/data-info")
|
||||
def get_dataset_info(
|
||||
symbol: str = Query(...),
|
||||
timeframe: str = Query(...),
|
||||
storage: StorageManager = Depends(get_storage),
|
||||
):
|
||||
df = storage.load_ohlcv(symbol=symbol, timeframe=timeframe)
|
||||
|
||||
if df is None or df.empty:
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"timeframe": timeframe,
|
||||
"start_date": None,
|
||||
"end_date": None,
|
||||
"n_rows": 0,
|
||||
"n_days": 0,
|
||||
}
|
||||
|
||||
# soporta tanto índice datetime como columna timestamp/datetime
|
||||
if hasattr(df.index, "min") and str(getattr(df.index, "dtype", "")).startswith("datetime"):
|
||||
start_ts = df.index.min()
|
||||
end_ts = df.index.max()
|
||||
elif "timestamp" in df.columns:
|
||||
ts = pd.to_datetime(df["timestamp"], errors="coerce")
|
||||
start_ts = ts.min()
|
||||
end_ts = ts.max()
|
||||
elif "datetime" in df.columns:
|
||||
ts = pd.to_datetime(df["datetime"], errors="coerce")
|
||||
start_ts = ts.min()
|
||||
end_ts = ts.max()
|
||||
else:
|
||||
start_ts = None
|
||||
end_ts = None
|
||||
|
||||
if start_ts is not None and end_ts is not None and pd.notna(start_ts) and pd.notna(end_ts):
|
||||
n_days = max(1, int((end_ts - start_ts).total_seconds() / 86400))
|
||||
start_date = pd.Timestamp(start_ts).isoformat()
|
||||
end_date = pd.Timestamp(end_ts).isoformat()
|
||||
else:
|
||||
n_days = 0
|
||||
start_date = None
|
||||
end_date = None
|
||||
|
||||
return {
|
||||
"symbol": symbol,
|
||||
"timeframe": timeframe,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
"n_rows": int(len(df)),
|
||||
"n_days": int(n_days),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/inspect", response_model=CalibrationStrategiesInspectResponse)
|
||||
def inspect_strategies(
|
||||
payload: CalibrationStrategiesInspectRequest,
|
||||
@@ -258,8 +317,3 @@ def run_strategies_async(
|
||||
thread.start()
|
||||
|
||||
return {"job_id": job_id}
|
||||
|
||||
|
||||
@router.get("/status/{job_id}")
|
||||
def get_status(job_id: str):
|
||||
return WF_JOBS.get(job_id, {"status": "unknown"})
|
||||
|
||||
@@ -6,6 +6,9 @@ let STRATEGY_CATALOG = [];
|
||||
let strategySlots = [];
|
||||
let selectedStrategyId = null;
|
||||
let lastValidationResult = null;
|
||||
let DATASET_INFO = null;
|
||||
let PROMOTION_RESULTS = [];
|
||||
let STEP4_SELECTION = [];
|
||||
const MAX_STRATEGIES = 10;
|
||||
|
||||
// =================================================
|
||||
@@ -111,6 +114,52 @@ function sleep(ms) {
|
||||
}
|
||||
|
||||
|
||||
function resolveDateRange() {
|
||||
// 1) Prioridad: configuración heredada de steps anteriores
|
||||
const cfg =
|
||||
window.currentConfig ||
|
||||
window.calibrationConfig ||
|
||||
lastValidationResult?.config ||
|
||||
null;
|
||||
|
||||
const candidates = [
|
||||
cfg?.date_from,
|
||||
cfg?.from_date,
|
||||
cfg?.start_date,
|
||||
cfg?.data?.date_from,
|
||||
cfg?.data?.from_date,
|
||||
cfg?.data?.start_date,
|
||||
];
|
||||
|
||||
const candidatesTo = [
|
||||
cfg?.date_to,
|
||||
cfg?.to_date,
|
||||
cfg?.end_date,
|
||||
cfg?.data?.date_to,
|
||||
cfg?.data?.to_date,
|
||||
cfg?.data?.end_date,
|
||||
];
|
||||
|
||||
const inheritedFrom = candidates.find(v => !!v) || null;
|
||||
const inheritedTo = candidatesTo.find(v => !!v) || null;
|
||||
|
||||
if (inheritedFrom && inheritedTo) {
|
||||
return { dateFrom: inheritedFrom, dateTo: inheritedTo };
|
||||
}
|
||||
|
||||
// 2) Fallback: DOM, por si existen en algún layout
|
||||
const domFrom = str("date_from");
|
||||
const domTo = str("date_to");
|
||||
|
||||
if (domFrom && domTo) {
|
||||
return { dateFrom: domFrom, dateTo: domTo };
|
||||
}
|
||||
|
||||
// 3) Sin rango explícito
|
||||
return { dateFrom: null, dateTo: null };
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// FETCH CATALOG
|
||||
// =================================================
|
||||
@@ -454,6 +503,7 @@ function updateTimeEstimate(globalTotal) {
|
||||
const el =
|
||||
document.getElementById("wf_time_estimate") ||
|
||||
document.getElementById("time_estimate");
|
||||
|
||||
if (!el) return;
|
||||
|
||||
if (!globalTotal || globalTotal <= 0) {
|
||||
@@ -463,31 +513,28 @@ function updateTimeEstimate(globalTotal) {
|
||||
|
||||
const trainDays = num("wf_train_days") || 180;
|
||||
const testDays = num("wf_test_days") || 30;
|
||||
const stepDays = num("wf_step_days") || 30;
|
||||
const stepDays = num("wf_step_days") || testDays;
|
||||
|
||||
const dateFrom = str("date_from");
|
||||
const dateTo = str("date_to");
|
||||
// usar metadata real del dataset si ya se ha cargado
|
||||
let totalDays = Number(DATASET_INFO?.n_days || 0);
|
||||
|
||||
let totalDays = 0;
|
||||
if (dateFrom && dateTo) {
|
||||
const fromTs = new Date(dateFrom).getTime();
|
||||
const toTs = new Date(dateTo).getTime();
|
||||
|
||||
if (Number.isFinite(fromTs) && Number.isFinite(toTs) && toTs > fromTs) {
|
||||
totalDays = Math.max(1, Math.ceil((toTs - fromTs) / 86400000));
|
||||
}
|
||||
if (!Number.isFinite(totalDays) || totalDays <= 0) {
|
||||
// fallback razonable, pero ya no es el camino principal
|
||||
totalDays = 3650;
|
||||
}
|
||||
|
||||
let estimatedWindows = 1;
|
||||
const minSpan = trainDays + testDays;
|
||||
let estimatedWindows = 1;
|
||||
|
||||
if (totalDays > minSpan && stepDays > 0) {
|
||||
if (totalDays > minSpan) {
|
||||
estimatedWindows = Math.max(
|
||||
1,
|
||||
Math.floor((totalDays - minSpan) / stepDays) + 1
|
||||
);
|
||||
}
|
||||
|
||||
const timeframe = str("timeframe") || "1h";
|
||||
|
||||
const barsPerDayMap = {
|
||||
"1m": 1440,
|
||||
"3m": 480,
|
||||
@@ -503,24 +550,20 @@ function updateTimeEstimate(globalTotal) {
|
||||
"1d": 1
|
||||
};
|
||||
|
||||
const timeframe = str("timeframe") || "1h";
|
||||
const barsPerDay = barsPerDayMap[timeframe] || 24;
|
||||
|
||||
const estimatedBars = Math.max(1, totalDays * barsPerDay);
|
||||
|
||||
// Heurística V1:
|
||||
// - overhead fijo de arranque
|
||||
// - coste por estrategia
|
||||
// - coste por ventana
|
||||
// - pequeño coste por volumen de datos
|
||||
const baseSeconds = 1.5;
|
||||
const perStrategySeconds = 0.9 * globalTotal;
|
||||
const perWindowSeconds = 0.35 * estimatedWindows * globalTotal;
|
||||
const dataSeconds = estimatedBars * globalTotal * 0.00003;
|
||||
const overheadSeconds = 3;
|
||||
const perWindowPerStrategySeconds = 0.25;
|
||||
const dataSeconds = estimatedBars * globalTotal * 0.00005;
|
||||
|
||||
const totalSeconds = Math.max(
|
||||
1,
|
||||
Math.round(baseSeconds + perStrategySeconds + perWindowSeconds + dataSeconds)
|
||||
Math.round(
|
||||
overheadSeconds +
|
||||
(estimatedWindows * globalTotal * perWindowPerStrategySeconds) +
|
||||
dataSeconds
|
||||
)
|
||||
);
|
||||
|
||||
if (totalSeconds < 60) {
|
||||
@@ -542,6 +585,27 @@ function updateTimeEstimate(globalTotal) {
|
||||
}
|
||||
|
||||
|
||||
function estimateWFWindows() {
|
||||
|
||||
if (!DATASET_INFO) return 0;
|
||||
|
||||
const trainDays = num("wf_train_days") || 180;
|
||||
const testDays = num("wf_test_days") || 30;
|
||||
const stepDays = num("wf_step_days") || testDays;
|
||||
|
||||
const totalDays = Number(DATASET_INFO.n_days || 0);
|
||||
|
||||
const minSpan = trainDays + testDays;
|
||||
|
||||
if (totalDays <= minSpan) return 1;
|
||||
|
||||
const windows =
|
||||
Math.floor((totalDays - minSpan) / stepDays) + 1;
|
||||
|
||||
return Math.max(1, windows);
|
||||
}
|
||||
|
||||
|
||||
function removeStrategySlot(index) {
|
||||
|
||||
strategySlots.splice(index, 1);
|
||||
@@ -550,6 +614,32 @@ function removeStrategySlot(index) {
|
||||
}
|
||||
|
||||
|
||||
function renderDatasetInfo() {
|
||||
|
||||
const el = document.getElementById("dataset_info");
|
||||
if (!el) return;
|
||||
|
||||
if (!DATASET_INFO) {
|
||||
el.textContent = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const start = DATASET_INFO.start_date
|
||||
? new Date(DATASET_INFO.start_date).toISOString().slice(0, 10)
|
||||
: "?";
|
||||
|
||||
const end = DATASET_INFO.end_date
|
||||
? new Date(DATASET_INFO.end_date).toISOString().slice(0, 10)
|
||||
: "?";
|
||||
|
||||
const rows = Number(DATASET_INFO.n_rows || 0).toLocaleString();
|
||||
const days = Number(DATASET_INFO.n_days || 0).toLocaleString();
|
||||
const windows = estimateWFWindows().toLocaleString();
|
||||
|
||||
el.textContent = `Dataset: ${start} → ${end} | ${rows} rows | ${days} days | ${windows} WF windows`;
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// PAYLOAD
|
||||
// =================================================
|
||||
@@ -675,6 +765,30 @@ async function fetchAvailableStrategies() {
|
||||
}
|
||||
|
||||
|
||||
async function fetchDatasetInfo() {
|
||||
const symbol = str("symbol");
|
||||
const timeframe = str("timeframe");
|
||||
|
||||
if (!symbol || !timeframe) {
|
||||
DATASET_INFO = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams({ symbol, timeframe });
|
||||
|
||||
const res = await fetch(`/api/v2/calibration/strategies/data-info?${qs.toString()}`);
|
||||
if (!res.ok) {
|
||||
DATASET_INFO = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
DATASET_INFO = data;
|
||||
renderDatasetInfo();
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
function loadFromStep2() {
|
||||
|
||||
document.getElementById("risk_fraction").value =
|
||||
@@ -1768,8 +1882,10 @@ function wireButtons() {
|
||||
renderStrategiesList(strategies);
|
||||
});
|
||||
|
||||
document.getElementById("load_step2_btn")?.addEventListener("click", () => {
|
||||
document.getElementById("load_step2_btn")?.addEventListener("click", async () => {
|
||||
loadContextFromLocalStorage();
|
||||
await fetchDatasetInfo();
|
||||
updateCombinationCounter();
|
||||
});
|
||||
|
||||
document.getElementById("close_pdf_btn")?.addEventListener("click", () => {
|
||||
@@ -1804,24 +1920,44 @@ async function init() {
|
||||
loadFromStep2();
|
||||
applyInheritedLock();
|
||||
|
||||
await fetchDatasetInfo();
|
||||
|
||||
document.getElementById("stop_type")
|
||||
.addEventListener("change", updateStopUI);
|
||||
|
||||
wireButtons();
|
||||
|
||||
[
|
||||
"date_from",
|
||||
"date_to",
|
||||
"timeframe",
|
||||
"wf_train_days",
|
||||
"wf_test_days",
|
||||
"wf_step_days"
|
||||
].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) {
|
||||
el.addEventListener("change", updateCombinationCounter);
|
||||
el.addEventListener("input", updateCombinationCounter);
|
||||
if (!el) return;
|
||||
|
||||
if (id === "timeframe") {
|
||||
el.addEventListener("change", async () => {
|
||||
await fetchDatasetInfo();
|
||||
updateCombinationCounter();
|
||||
renderDatasetInfo();
|
||||
});
|
||||
el.addEventListener("input", async () => {
|
||||
await fetchDatasetInfo();
|
||||
updateCombinationCounter();
|
||||
renderDatasetInfo();
|
||||
});
|
||||
} else {
|
||||
el.addEventListener("change", () => {
|
||||
updateCombinationCounter();
|
||||
renderDatasetInfo();
|
||||
});
|
||||
el.addEventListener("input", () => {
|
||||
updateCombinationCounter();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wirePromotionUI();
|
||||
|
||||
document.getElementById("plot_strategy_select").addEventListener("change", function() {
|
||||
@@ -2076,6 +2212,81 @@ function renderRegimeSummary(strategyId, s, data) {
|
||||
// =================================================
|
||||
|
||||
|
||||
function defaultStep4SelectionForStatus(status) {
|
||||
return status === "promote";
|
||||
}
|
||||
|
||||
|
||||
function normalizePromotionResults(results) {
|
||||
return (results || []).map(r => ({
|
||||
...r,
|
||||
selected_for_step4: defaultStep4SelectionForStatus(r.status),
|
||||
selection_source: defaultStep4SelectionForStatus(r.status) ? "auto" : "manual_off"
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function getStrategyConfigFromSlots(strategyId) {
|
||||
const slot = (strategySlots || []).find(s => s.strategy_id === strategyId);
|
||||
if (!slot) return null;
|
||||
|
||||
return {
|
||||
strategy_id: slot.strategy_id,
|
||||
parameters: { ...(slot.parameters || {}) }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function collectStep4Selection() {
|
||||
return (PROMOTION_RESULTS || [])
|
||||
.filter(r => r.selected_for_step4)
|
||||
.map(r => {
|
||||
const cfg = getStrategyConfigFromSlots(r.strategy_id);
|
||||
|
||||
return {
|
||||
strategy_id: r.strategy_id,
|
||||
promotion_status: r.status,
|
||||
promotion_score: r.score,
|
||||
selection_source: r.selection_source,
|
||||
diversity_blocked_by: r.diversity_blocked_by ?? null,
|
||||
diversity_correlation: r.diversity_correlation ?? null,
|
||||
parameters: cfg?.parameters || {}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function updateStep4SelectionSummary() {
|
||||
const el = document.getElementById("step4SelectionSummary");
|
||||
if (!el) return;
|
||||
|
||||
const selected = (PROMOTION_RESULTS || []).filter(r => r.selected_for_step4).length;
|
||||
const total = (PROMOTION_RESULTS || []).length;
|
||||
|
||||
el.textContent = `Selected for Step 4: ${selected} / ${total}`;
|
||||
}
|
||||
|
||||
|
||||
function buildStep4SelectionArtifact() {
|
||||
STEP4_SELECTION = collectStep4Selection();
|
||||
|
||||
const previewEl = document.getElementById("step4SelectionPreview");
|
||||
if (!previewEl) return;
|
||||
|
||||
const artifact = {
|
||||
created_at: new Date().toISOString(),
|
||||
symbol: str("symbol"),
|
||||
timeframe: str("timeframe"),
|
||||
selected_strategies: STEP4_SELECTION
|
||||
};
|
||||
|
||||
previewEl.textContent = JSON.stringify(artifact, null, 2);
|
||||
previewEl.classList.remove("d-none");
|
||||
|
||||
updateStep4SelectionSummary();
|
||||
}
|
||||
|
||||
|
||||
async function runPromotion() {
|
||||
|
||||
if (!Array.isArray(window.lastStrategiesResult) || window.lastStrategiesResult.length === 0) {
|
||||
@@ -2105,7 +2316,10 @@ async function runPromotion() {
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
renderPromotionResults(data.results || []);
|
||||
|
||||
PROMOTION_RESULTS = normalizePromotionResults(data.results || []);
|
||||
renderPromotionResults(PROMOTION_RESULTS);
|
||||
updateStep4SelectionSummary();
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -2119,7 +2333,7 @@ function renderPromotionResults(results) {
|
||||
const table = document.querySelector("#promotionTable tbody");
|
||||
table.innerHTML = "";
|
||||
|
||||
results.forEach(r => {
|
||||
results.forEach((r, idx) => {
|
||||
|
||||
const tr = document.createElement("tr");
|
||||
|
||||
@@ -2128,13 +2342,39 @@ function renderPromotionResults(results) {
|
||||
? "—"
|
||||
: Number(r.diversity_correlation).toFixed(4);
|
||||
|
||||
const checked = r.selected_for_step4 ? "checked" : "";
|
||||
|
||||
let statusBadge = "bg-secondary-lt text-secondary";
|
||||
if (r.status === "promote") statusBadge = "bg-success-lt text-success";
|
||||
else if (r.status === "review") statusBadge = "bg-warning-lt text-warning";
|
||||
else if (r.status === "review_diversity") statusBadge = "bg-orange-lt text-orange";
|
||||
else if (r.status === "reject") statusBadge = "bg-danger-lt text-danger";
|
||||
|
||||
let sourceBadge = "bg-secondary-lt text-secondary";
|
||||
let sourceLabel = r.selection_source || "manual_off";
|
||||
|
||||
if (sourceLabel === "auto") sourceBadge = "bg-success-lt text-success";
|
||||
else if (sourceLabel === "manual_on") sourceBadge = "bg-blue-lt text-blue";
|
||||
else if (sourceLabel === "manual_off") sourceBadge = "bg-secondary-lt text-secondary";
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${r.rank}</td>
|
||||
<td>${r.strategy_id}</td>
|
||||
<td>${r.score}</td>
|
||||
<td>${r.status}</td>
|
||||
<td><span class="badge ${statusBadge}">${r.status}</span></td>
|
||||
<td>${blockedBy}</td>
|
||||
<td>${corr}</td>
|
||||
<td>
|
||||
<label class="form-check m-0">
|
||||
<input
|
||||
class="form-check-input promotion-step4-checkbox"
|
||||
type="checkbox"
|
||||
data-index="${idx}"
|
||||
${checked}
|
||||
>
|
||||
</label>
|
||||
</td>
|
||||
<td><span class="badge ${sourceBadge}">${sourceLabel}</span></td>
|
||||
`;
|
||||
|
||||
table.appendChild(tr);
|
||||
@@ -2144,16 +2384,49 @@ function renderPromotionResults(results) {
|
||||
const review = results.filter(r => r.status === "review").length;
|
||||
const reviewDiversity = results.filter(r => r.status === "review_diversity").length;
|
||||
const reject = results.filter(r => r.status === "reject").length;
|
||||
const selected = results.filter(r => r.selected_for_step4).length;
|
||||
|
||||
document.getElementById("promotionSummary").innerHTML =
|
||||
`Promoted: ${promoted} | Review: ${review} | Review diversity: ${reviewDiversity} | Rejected: ${reject}`;
|
||||
`Promoted: ${promoted} | Review: ${review} | Review diversity: ${reviewDiversity} | Rejected: ${reject} | Step 4 selected: ${selected}`;
|
||||
|
||||
updateStep4SelectionSummary();
|
||||
}
|
||||
|
||||
|
||||
function handlePromotionStep4Toggle(event) {
|
||||
const target = event.target;
|
||||
if (!target || !target.classList.contains("promotion-step4-checkbox")) return;
|
||||
|
||||
const index = Number(target.dataset.index);
|
||||
if (!Number.isInteger(index) || !PROMOTION_RESULTS[index]) return;
|
||||
|
||||
const row = PROMOTION_RESULTS[index];
|
||||
row.selected_for_step4 = !!target.checked;
|
||||
|
||||
if (row.selected_for_step4) {
|
||||
row.selection_source = row.status === "promote" ? "auto" : "manual_on";
|
||||
} else {
|
||||
row.selection_source = "manual_off";
|
||||
}
|
||||
|
||||
renderPromotionResults(PROMOTION_RESULTS);
|
||||
}
|
||||
|
||||
|
||||
function wirePromotionUI() {
|
||||
const btn = document.getElementById("btnRunPromotion");
|
||||
if (btn) {
|
||||
btn.addEventListener("click", runPromotion);
|
||||
const btnRun = document.getElementById("btnRunPromotion");
|
||||
if (btnRun) {
|
||||
btnRun.addEventListener("click", runPromotion);
|
||||
}
|
||||
|
||||
const table = document.querySelector("#promotionTable");
|
||||
if (table) {
|
||||
table.addEventListener("change", handlePromotionStep4Toggle);
|
||||
}
|
||||
|
||||
const btnBuild = document.getElementById("btnBuildStep4Selection");
|
||||
if (btnBuild) {
|
||||
btnBuild.addEventListener("click", buildStep4SelectionArtifact);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<div class="flex-grow-1 text-center">
|
||||
<h2 class="mb-0">Calibración · Paso 3 · Strategies</h2>
|
||||
<div class="text-secondary">Optimización + Walk Forward (OOS)</div>
|
||||
<div class="text-secondary">Strategy Selector</div>
|
||||
</div>
|
||||
|
||||
<!-- Forward arrow (disabled until OK) -->
|
||||
@@ -245,6 +245,11 @@
|
||||
<span id="wf_time_estimate">~ 0 sec</span>
|
||||
</small>
|
||||
</div>
|
||||
<div class="mt-2 text-end">
|
||||
<small class="text-muted">
|
||||
<span id="dataset_info"></span>
|
||||
</small>
|
||||
</div>
|
||||
<div class="mt-3 text-secondary">
|
||||
Cada estrategia utiliza parámetros fijos (validación OOS sin grid).
|
||||
</div>
|
||||
@@ -306,6 +311,17 @@
|
||||
|
||||
<div id="promotionSummary" class="mt-3"></div>
|
||||
|
||||
<div class="mt-2 d-flex gap-2 flex-wrap">
|
||||
<button id="btnBuildStep4Selection" class="btn btn-success btn-sm">
|
||||
Build Step 4 Selection
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="step4SelectionSummary" class="mt-2 text-secondary small"></div>
|
||||
|
||||
<pre id="step4SelectionPreview" class="mt-2 p-2 bg-light border rounded d-none"
|
||||
style="max-height: 280px; overflow:auto;"></pre>
|
||||
|
||||
<table class="table mt-3" id="promotionTable">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -315,6 +331,8 @@
|
||||
<th>Status</th>
|
||||
<th>Blocked by</th>
|
||||
<th>Corr</th>
|
||||
<th>Step 4</th>
|
||||
<th>Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
|
||||
Reference in New Issue
Block a user