add weekly and bi-weekly frequently

This commit is contained in:
2026-01-28 12:09:19 +08:00
parent cf708d2466
commit d33b521b22
5 changed files with 431 additions and 206 deletions
+156 -158
View File
@@ -29,47 +29,57 @@
</style>
</head>
<body>
<div class="card shadow-sm p-4 mb-4">
<h4 class="fw-bold mb-4">🛠️ Value Averging Strategy Analysis</h4>
<form id="calcForm">
<div class="row g-3 mb-4">
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase">Ticker</label>
<input type="text" id="symbol" class="form-control" value="SPY">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase">Initial ($)</label>
<input type="number" id="initial_inv" class="form-control" value="20000">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase">Monthly Target ($)</label>
<input type="number" id="monthly_target" class="form-control" value="500">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase">Start Date</label>
<input type="date" id="startDate" class="form-control" value="2024-01-01">
</div>
</div>
<div class="container-fluid py-3" style="max-width: 1200px; margin-left: 0;">
<div class="d-flex justify-content-between align-items-center mb-3">
<h2 class="mb-0 fw-bold" style="font-size: 1.5rem;">🛠️ Value Averaging Strategy Analysis</h2>
<button onclick="resetAll()" class="btn btn-warning btn-sm px-3">Reset</button>
</div>
<div class="row g-3 align-items-center">
<div class="col-md-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="allow_sell" checked>
<label class="form-check-label fw-bold small text-uppercase" for="allow_sell">Allow Share Sales</label>
</div>
</div>
<div class="col-md-3">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="allow_fractional" checked>
<label class="form-check-label fw-bold small text-uppercase" for="allow_fractional">Fractional Shares</label>
</div>
</div>
<div class="col-md-6 d-flex justify-content-end gap-2">
<button type="button" onclick="runSimulation()" class="btn btn-primary px-5 fw-bold">Run Analysis</button>
<button type="button" onclick="resetAll()" class="btn btn-outline-secondary px-4">Reset</button>
<div class="row g-2 mb-3">
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase mb-1">Ticker</label>
<input type="text" id="symbol" class="form-control form-control-sm" value="SPY">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase mb-1">Initial ($)</label>
<input type="number" id="initial_inv" class="form-control form-control-sm" value="20000">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase mb-1">Monthly Target ($)</label>
<input type="number" id="monthly_target" class="form-control form-control-sm" value="500">
</div>
<div class="col-md-3">
<label class="form-label small fw-bold text-uppercase mb-1">Start Date</label>
<input type="date" id="startDate" class="form-control form-control-sm" value="2024-01-01">
</div>
</div>
<div class="row align-items-center g-3">
<div class="col-auto">
<div class="d-flex align-items-center">
<label class="small fw-bold text-uppercase me-2 mb-0">Frequency</label>
<select id="frequency" class="form-select form-select-sm" style="width: auto;">
<option value="Weekly">Weekly</option>
<option value="Bi-Weekly">Bi-Weekly</option>
<option value="Monthly" selected>Monthly</option>
</select>
</div>
</div>
</form>
<div class="col-auto">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="allow_sell" checked>
<label class="form-check-label small fw-bold text-uppercase" for="allow_sell">Allow Share Sales</label>
</div>
</div>
<div class="col-auto">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="allow_fractional" checked>
<label class="form-check-label small fw-bold text-uppercase" for="allow_fractional">Fractional Shares</label>
</div>
</div>
<div class="col-auto">
<button onclick="runSimulation()" class="btn btn-primary fw-bold px-4">Run Analysis</button>
</div>
</div>
<div id="kpiArea" class="d-none mb-4">
<div class="d-flex justify-content-end mb-2">
@@ -154,73 +164,77 @@
<script>
let myChart = null;
async function runSimulation() {
// 1. Get references to all elements first
const el = {
symbol: document.getElementById('symbol'),
initial: document.getElementById('initial_inv'),
monthly: document.getElementById('monthly_target'),
date: document.getElementById('startDate'),
sell: document.getElementById('allow_sell'),
frac: document.getElementById('allow_fractional'),
btn: document.querySelector("button[onclick='runSimulation()']")
};
// 2. Safety Check: Ensure all elements exist before proceeding
for (const [key, element] of Object.entries(el)) {
if (!element) {
console.error(`Error: Element with ID or selector for '${key}' not found.`);
alert(`UI Error: Component '${key}' is missing. Please refresh.`);
return;
}
}
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,
start_date: el.date.value,
allow_sell: el.sell.checked,
allow_fractional: el.frac.checked
async function runSimulation() {
// 1. Get references (Added frequency)
const el = {
symbol: document.getElementById('symbol'),
initial: document.getElementById('initial_inv'),
monthly: document.getElementById('monthly_target'),
date: document.getElementById('startDate'),
freq: document.getElementById('frequency'), // <--- ADDED
sell: document.getElementById('allow_sell'),
frac: document.getElementById('allow_fractional'),
btn: document.querySelector("button[onclick='runSimulation()']")
};
if (!payload.symbol) {
alert("Please enter a ticker symbol.");
return;
// 2. Safety Check
for (const [key, element] of Object.entries(el)) {
if (!element) {
console.error(`Error: Component '${key}' not found.`);
return;
}
}
// Loading State
el.btn.disabled = true;
el.btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Loading...';
const originalText = el.btn.innerHTML;
const res = await fetch('/api/backtest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
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
};
if (!res.ok) throw new Error(`Server returned ${res.status}`);
const data = await res.json();
if (!payload.symbol) {
alert("Please enter a ticker symbol.");
return;
}
// Update UI
document.getElementById('kpiArea')?.classList.remove('d-none');
document.getElementById('resultsArea')?.classList.remove('d-none');
// Loading State
el.btn.disabled = true;
el.btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Loading...';
updateKPIs(data, payload.monthly_target);
renderDetailedChart(data);
renderTable(data);
const res = await fetch('/api/backtest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
} catch (err) {
console.error("Simulation Error:", err);
alert("Failed to run analysis. See console for details.");
} finally {
el.btn.disabled = false;
el.btn.innerHTML = originalText;
}
// 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;
}
}
/**
* Updates the summary cards with the DVA "Next Move" recommendation.
@@ -235,55 +249,34 @@ function updateKPIs(data, monthlyTarget) {
if (!data || data.length === 0) return;
const last = data[data.length - 1];
// 1. Extract Data with multiple fallback options for key names
const latestPrice = last.price || last.close || 0;
const latestDate = last.date || "N/A";
const nextMoveAmount = last.va_diff || 0;
const sharesToMove = last.va_shares_trans || (latestPrice > 0 ? Math.abs(nextMoveAmount / latestPrice) : 0);
// Check these against your history.append keys
const totalInvestedValue = last.va_invested || 0;
const currentPortfolioVal = last.va_value || 0;
const currentTarget = last.va_target_value || 0;
// 2. Helper to set text safely
const updateEl = (id, val) => {
// Helper function to update text ONLY if the element exists
const safeUpdate = (id, value) => {
const el = document.getElementById(id);
if (el) el.innerText = val;
if (el) {
el.innerText = value;
} else {
console.warn(`KPI Error: Element with ID '${id}' not found in HTML.`);
}
};
// 3. Update the UI
updateEl('targetValueCard', `$${currentTarget.toLocaleString(undefined, {minimumFractionDigits: 2})}`);
updateEl('totalSaved', `$${totalInvestedValue.toLocaleString(undefined, {minimumFractionDigits: 2})}`);
updateEl('totalVal', `$${currentPortfolioVal.toLocaleString(undefined, {minimumFractionDigits: 2})}`);
updateEl('priceDateLabel', latestDate);
updateEl('latestPriceDisplay', `$${latestPrice.toFixed(2)}`);
// 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});
// 4. Handle the Recommendation Card
const amtEl = document.getElementById('nextInvAmt');
if (amtEl) {
// This ensures the minus sign shows up naturally from the number
const sign = nextMoveAmount < 0 ? "-" : (nextMoveAmount > 0 ? "+" : "");
amtEl.innerText = `${sign}$${Math.abs(nextMoveAmount).toLocaleString(undefined, {minimumFractionDigits: 2})}`;
amtEl.className = nextMoveAmount >= 0 ? "fw-bold text-primary" : "fw-bold text-danger";
// 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";
}
// 5. Handle the Action Message
const msgEl = document.getElementById('nextInvMsg');
if (msgEl) {
const actionVerb = nextMoveAmount >= 0 ? "Buy" : "Sell";
const actionColor = nextMoveAmount >= 0 ? "text-primary" : "text-danger";
msgEl.innerHTML = `<span class="${actionColor} fw-bold">${actionVerb}</span> <strong>${Math.abs(sharesToMove).toFixed(4)}</strong> units @ $${latestPrice.toFixed(2)}`;
}
if (typeof updateSyncBadge === "function") updateSyncBadge(latestDate);
}
// Helper function to prevent errors if an ID is missing from HTML
function safeSetText(id, text) {
const el = document.getElementById(id);
if (el) el.innerText = text;
}
/**
* Updates a visual badge showing if data is fresh or stale
@@ -350,48 +343,52 @@ function updateSyncBadge(dateString) {
const initialInput = parseFloat(document.getElementById('initial_inv').value) || 0;
const monthlyInput = parseFloat(document.getElementById('monthly_target').value) || 0;
if (!tableBody) return;
if (!tableBody || !data) return;
tableBody.innerHTML = data.map((d, index) => {
// Use initial investment for row 0, monthly target for others
const dcaInvestedThisMonth = index === 0 ? initialInput : monthlyInput;
// Calculate Returns for badges
const vaAnnRet = d.va_invested > 0 ? (((d.va_value - d.va_invested) / d.va_invested) * 100).toFixed(2) : 0;
const dcaAnnRet = d.dca_invested > 0 ? (((d.dca_value - d.dca_invested) / d.dca_invested) * 100).toFixed(2) : 0;
// Safety Fallbacks: Ensure numbers exist before calculation
const vaVal = d.va_value || 0;
const vaInv = d.va_invested || 0;
const dcaVal = d.dca_value || 0;
const dcaInv = d.dca_invested || 0;
const vaAnnRet = vaInv > 0 ? (((vaVal - vaInv) / vaInv) * 100).toFixed(2) : "0.00";
const dcaAnnRet = dcaInv > 0 ? (((dcaVal - dcaInv) / dcaInv) * 100).toFixed(2) : "0.00";
return `
<tr>
<td class="small fw-bold">${d.date}</td>
<td>$${d.price.toLocaleString(undefined, {minimumFractionDigits: 2})}</td>
<td>$${(d.price || 0).toLocaleString(undefined, {minimumFractionDigits: 2})}</td>
<td class="text-success fw-bold">
$${d.va_diff.toLocaleString()}<br>
<small class="text-muted">${d.va_shares_trans >= 0 ? '+' : ''}${d.va_shares_trans.toFixed(2)}</small>
<td class="${(d.va_diff || 0) >= 0 ? 'text-success' : 'text-danger'} fw-bold">
$${(d.va_diff || 0).toLocaleString()}<br>
<small class="text-muted">${(d.va_shares_trans || 0) >= 0 ? '+' : ''}${(d.va_shares_trans || 0).toFixed(2)}</small>
</td>
<td>
$${dcaInvestedThisMonth.toLocaleString()}<br>
<small class="text-muted">+${d.dca_shares_trans.toFixed(2)}</small>
<small class="text-muted">+${(d.dca_shares_trans || 0).toFixed(2)}</small>
</td>
<td class="fw-bold">
$${d.va_value.toLocaleString()}<br>
<small class="text-primary">${d.va_shares_total.toFixed(2)}</small>
$${vaVal.toLocaleString()}<br>
<small class="text-primary">${(d.va_shares_total || 0).toFixed(2)}</small>
</td>
<td>
$${d.dca_value.toLocaleString()}<br>
<small class="text-primary">${d.dca_shares_total.toFixed(2)}</small>
$${dcaVal.toLocaleString()}<br>
<small class="text-primary">${(d.dca_shares_total || 0).toFixed(2)}</small>
</td>
<td>
<span class="badge ${vaAnnRet >= 0 ? 'bg-success' : 'bg-danger'}">
<span class="badge ${parseFloat(vaAnnRet) >= 0 ? 'bg-success' : 'bg-danger'}">
${vaAnnRet}%
</span>
</td>
<td>
<span class="badge ${dcaAnnRet >= 0 ? 'bg-info' : 'bg-danger'}">
<span class="badge ${parseFloat(dcaAnnRet) >= 0 ? 'bg-info' : 'bg-danger'}">
${dcaAnnRet}%
</span>
</td>
@@ -406,12 +403,13 @@ function updateSyncBadge(dateString) {
}
function resetAll() {
// 1. Reset Inputs to Defaults
// 1. Reset Inputs to Defaults (Including Frequency)
const defaults = {
'symbol': 'SPY',
'initial_inv': 20000,
'monthly_target': 500,
'startDate': '2024-01-01'
'startDate': '2024-01-01',
'frequency': 'Monthly' // <--- Added this
};
Object.keys(defaults).forEach(id => {
@@ -439,7 +437,7 @@ function updateSyncBadge(dateString) {
window.detailedChart = null;
}
console.log("Dashboard reset successfully.");
console.log("Dashboard reset to defaults.");
}
</script>
</body>