fetch data bug fix for both index and DVA/DCA calculation

This commit is contained in:
2026-02-02 06:48:49 +08:00
parent d33b521b22
commit 6506932042
15 changed files with 35346 additions and 16918 deletions
+104 -72
View File
@@ -98,7 +98,7 @@
<div class="col-md-4">
<div class="card p-3 text-center border-primary h-100">
<h6 class="text-muted text-uppercase small">Next Recommended Move</h6>
<h6 class="text-muted text-uppercase small">Next Payment Estimates</h6>
<h2 id="nextInvAmt" class="fw-bold mb-1">$0.00</h2>
<div class="small text-muted mb-2">
Price: <span id="latestPriceDisplay" class="fw-bold text-dark">$0.00</span>
@@ -188,53 +188,53 @@
const originalText = el.btn.innerHTML;
try {
const payload = {
symbol: el.symbol.value.trim().toUpperCase(),
initial_inv: parseFloat(el.initial.value) || 0,
monthly_target: parseFloat(el.monthly.value) || 0,
startDate: el.date.value, // Match backend's preferred key
frequency: el.freq.value, // <--- CRITICAL: MUST BE SENT
allow_sell: el.sell.checked,
allow_fractional: el.frac.checked
};
const payload = {
symbol: el.symbol.value.trim().toUpperCase(),
initial_inv: parseFloat(el.initial.value) || 0,
monthly_target: parseFloat(el.monthly.value) || 0,
startDate: el.date.value,
frequency: el.freq.value,
allow_sell: el.sell.checked,
allow_fractional: el.frac.checked
};
if (!payload.symbol) {
alert("Please enter a ticker symbol.");
return;
}
// Loading State
el.btn.disabled = true;
el.btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Loading...';
const res = await fetch('/api/backtest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
// Parse response to check for specific Python errors
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || `Server returned ${res.status}`);
}
// Update UI
document.getElementById('kpiArea')?.classList.remove('d-none');
document.getElementById('resultsArea')?.classList.remove('d-none');
updateKPIs(data, payload.monthly_target);
renderDetailedChart(data);
renderTable(data);
} catch (err) {
console.error("Simulation Error:", err);
alert(`Analysis Failed: ${err.message}`);
} finally {
el.btn.disabled = false;
el.btn.innerHTML = originalText;
if (!payload.symbol) {
alert("Please enter a ticker symbol.");
return;
}
// Loading State
el.btn.disabled = true;
el.btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Loading...';
// We add ?t= + timestamp to the URL to bypass browser caching
const res = await fetch(`/api/backtest?t=${new Date().getTime()}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || `Server returned ${res.status}`);
}
// Update UI
document.getElementById('kpiArea')?.classList.remove('d-none');
document.getElementById('resultsArea')?.classList.remove('d-none');
updateKPIs(data, payload.monthly_target);
renderDetailedChart(data);
renderTable(data);
} catch (err) {
console.error("Simulation Error:", err);
alert(`Analysis Failed: ${err.message}`);
} finally {
el.btn.disabled = false;
el.btn.innerHTML = originalText;
}
}
/**
* Updates the summary cards with the DVA "Next Move" recommendation.
@@ -247,40 +247,72 @@
*/
function updateKPIs(data, monthlyTarget) {
if (!data || data.length === 0) return;
const last = data[data.length - 1];
// Helper function to update text ONLY if the element exists
const safeUpdate = (id, value) => {
// 1. Data Extraction (defined once to avoid VS Code red errors)
const latestPrice = last.price || 0;
const latestDate = last.date || "N/A";
const nextMoveAmt = last.va_diff || 0;
const sharesToMove = last.va_shares_trans || 0;
const isNeg = nextMoveAmt < 0;
const isPos = nextMoveAmt > 0;
// 2. Helper for standard KPI cards
const setUI = (id, val) => {
const el = document.getElementById(id);
if (el) {
el.innerText = value;
} else {
console.warn(`KPI Error: Element with ID '${id}' not found in HTML.`);
}
if (el) el.innerText = val;
};
// 1. Format the values from the Python response
const targetVal = (last.va_target_value || 0).toLocaleString(undefined, {minimumFractionDigits: 2});
const investedVal = (last.va_invested || 0).toLocaleString(undefined, {minimumFractionDigits: 2});
const marketVal = (last.va_value || 0).toLocaleString(undefined, {minimumFractionDigits: 2});
// Update the main 3 cards
setUI('targetValueCard', `$${(last.va_target_value || 0).toLocaleString(undefined, {minimumFractionDigits: 2})}`);
setUI('totalSaved', `$${(last.va_invested || 0).toLocaleString(undefined, {minimumFractionDigits: 2})}`);
setUI('totalVal', `$${(last.va_value || 0).toLocaleString(undefined, {minimumFractionDigits: 2})}`);
// 2. Push to the UI using the IDs defined in the HTML above
safeUpdate('targetValueCard', `$${targetVal}`);
safeUpdate('totalSaved', `$${investedVal}`);
safeUpdate('totalVal', `$${marketVal}`);
// 3. Update the Recommendation Card (Next Move)
const nextMove = last.va_diff || 0;
const nextAmtEl = document.getElementById('nextInvAmt');
if (nextAmtEl) {
nextAmtEl.innerText = (nextMove >= 0 ? "+" : "") + "$" + Math.abs(nextMove).toLocaleString();
nextAmtEl.className = nextMove >= 0 ? "fw-bold text-success" : "fw-bold text-danger";
// 3. Recommendation Card Logic (Background & Border)
const recommendationCard = document.getElementById('nextMoveCard');
if (recommendationCard) {
if (isNeg) {
recommendationCard.style.backgroundColor = "#fff5f5"; // Light Red
recommendationCard.style.border = "2px solid #feb2b2";
} else if (isPos) {
recommendationCard.style.backgroundColor = "#f0fff4"; // Light Green
recommendationCard.style.border = "2px solid #9ae6b4";
} else {
recommendationCard.style.backgroundColor = "#ffffff";
recommendationCard.style.border = "1px solid #dee2e6";
}
}
// 4. Update Move Amount & Color
const amtEl = document.getElementById('nextInvAmt');
if (amtEl) {
amtEl.innerText = `${isNeg ? '-' : '+'}$${Math.abs(nextMoveAmt).toLocaleString(undefined, {minimumFractionDigits: 2})}`;
amtEl.className = isNeg ? "fw-bold text-danger display-6" : "fw-bold text-success display-6";
}
// 5. Update Action Message (Verb + Units)
const msgEl = document.getElementById('nextInvMsg');
if (msgEl) {
const verb = isNeg ? "Sell" : "Buy";
const colorClass = isNeg ? "text-danger" : "text-success";
msgEl.innerHTML = `<span class="${colorClass} fw-bold">${verb}</span> <strong>${Math.abs(sharesToMove).toFixed(4)}</strong> units`;
}
// 6. Update Footer Labels (Price & Date)
const priceDisplay = document.getElementById('latestPriceDisplay');
if (priceDisplay) {
priceDisplay.innerText = `$${latestPrice.toLocaleString(undefined, {minimumFractionDigits: 2})}`;
}
const dateDisplay = document.getElementById('priceDateLabel');
if (dateDisplay) {
dateDisplay.innerText = latestDate;
}
// Optional: Sync Badge
if (typeof updateSyncBadge === "function") updateSyncBadge(latestDate);
}
/**
* Updates a visual badge showing if data is fresh or stale
*/
function updateSyncBadge(dateString) {
const syncDateEl = document.getElementById('lastSyncDate');
const syncBadge = document.getElementById('syncBadge');