add weekly and bi-weekly frequently
This commit is contained in:
+156
-158
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user