fetch data bug fix for both index and DVA/DCA calculation
This commit is contained in:
+104
-72
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user