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
+91
View File
@@ -0,0 +1,91 @@
There are 2 projects combined in this program.
First peogram : localhosr:5000/
Data Analysis & Signal Generation layer that happens before the simulation even begins.
Here is a summary of those specific capabilities:
1. Automated Pricing Retrieval
The program acts as a data bridge. Instead of you manually searching for prices, it can:
Fetch real-time or historical data for specific tickers (like SPY).
Maintain a local CSV database, ensuring you have a persistent record of price action to analyze without re-downloading everything every time.
2. EMA (Exponential Moving Average) Calculation
The program adds a layer of technical analysis to the raw prices:
It calculates the EMA (typically a 200-day or 50-day trend line).
Unlike a simple average, the EMA gives more weight to recent prices, making it more responsive to new market information.
3. Price/EMA Ratio Analysis
This is one of the program's most powerful analytical features. It generates a "Value Ratio" to help you understand market positioning:
The Math: It divides the current price by the EMA.
The Signal: * Ratio > 1.0: The asset is trading above its average (potentially overextended or in a strong uptrend).
Ratio < 1.0: The asset is trading below its average (potentially undervalued or in a downtrend).
Strategic Use: This ratio can be used to "tune" your DVA strategy—for example, investing more aggressively when the ratio is low and being more cautious when the ratio is high.
4. Tabular Data Export
All of this technical data—the raw price, the EMA, and the resulting Ratio—is organized into a clean, structured list. This allows you to:
See a "Heat Map" of when the asset was historically cheap or expensive relative to its trend.
Use these technical signals to justify the "Buy" or "Sell" recommendations the dashboard provides.
Summary of the "Technical Stack"CapabilityBenefitPrice FetchingSaves time on manual data entry.EMA CalculationIdentifies the long-term "fair value" trend.Price/EMA RatioQuantifies exactly how "expensive" or "cheap" the market is today.
Second Project ; localhost:5000/backtest
a Dynamic Value Averaging (DVA) Backtester. Its primary purpose is to help investors simulate a sophisticated "Value Averaging" strategy to determine exactly how much they should buy or sell of an asset (like SPY) on a specific day of the month to stay on track with a financial goal.
Here is a summary of what the programs actually do:
1. Automated Strategy Simulation
The program takes a historical CSV file and "plays back" time.
It identifies a Monthly Anchor Day (e.g., the 27th of every month) and simulates making a trade on that exact day.
It calculates your Target Value Goal, which grows every month by a set amount (e.g., $500/month).
2. Intelligent "Buy/Sell" Recommendations
Instead of just buying a fixed amount (DCA), the program calculates the "Value Gap":
If the market is down: It tells you to Buy More to reach your target.
If the market is up: It tells you to Sell (or buy less) to lock in gains.
It calculates the exact number of units/shares you need to trade based on the most recent price in your data.
3. Real-Time Data Synchronization
The program is designed to handle "incomplete" months:
If today is the 28th but your anchor day is the 15th, the program doesn't just stop at the 15th.
It forces the latest available data point from your CSV to be processed, ensuring your "Next Recommended Move" is based on the most recent price (e.g., yesterdays close).
4. Interactive Dashboard Reporting
Once the simulation runs, the program visualizes the health of your investment:
Target Value Goal: Where you should be according to your plan.
Total Invested: The actual cash you've put in out of pocket.
Current Portfolio Value: What your holdings are worth today.
Data Freshness: A badge that turns red if your CSV data is more than 5 days old.
Summary of the "User Flow"
Input: You provide a CSV of stock prices and a monthly investment goal.
Execution: The Python engine calculates the historical performance and the current gap.
Output: The Web Dashboard gives you a clear instruction: "Buy/Sell X units for $Y.YY."
1. Core Architecture OverviewThe project is split into three distinct layers that communicate in a linear pipeline.
A. The Data Engine (data_engine.py / CSV)This is the "Source of Truth."Role: Manages the ingestion of historical market data (e.g., SPY).Logic: It ensures the CSV is sorted chronologically and provides a path for the strategy engine to read price points.
B. The Strategy Engine (engine.py)This is the "Brain" of the program where the financial math happens.Data Slicing: Uses the Anchor Day logic to filter the CSV for specific monthly dates (e.g., the 27th of every month).Safety Net: Forces the inclusion of the absolute latest CSV row (e.g., 2026-01-27) to ensure the dashboard is never out of date.Simulation Loop: Iterates through dates, calculating:Target Value: (Monthly Increment $\times$ Month Number).Market Value: (Shares Owned $\times$ Current Price).The Move (va_diff): The gap between Target and Market value.JSON Serialization: Rounds all values and packages them into a history list of dictionaries to be sent to the frontend.
C. The Web Dashboard (backtest.html / JS)This is the "Interface" for the user.Controller (runSimulation): Triggers the Python logic via an API/Socket call and receives the history array.KPI Processor (updateKPIs): * Maps Python keys (like va_invested) to UI elements.Determines the Action Verb (Buy/Sell) based on the sign of va_diff.Updates the DOM with formatted currency strings.Integrity Checker (updateSyncBadge): Compares the data date to the current system date to warn the user if data is stale.
2. Key Data MappingsTo prevent the "Value = 0" issues we encountered, the architecture relies on strict key consistency:Python Key (Source)JavaScript VariableUI Element IDva_diffnextMoveAmountnextInvAmtva_investedtotalInvestedValuetotalSavedva_valuecurrentPortfolioValtotalValpricelatestPricelatestPriceDisplay
3. Data Flow SequenceTrigger: User clicks "Run Simulation" in the browser.Process: Python reads CSV $\rightarrow$ Filters by Anchor Day $\rightarrow$ Appends Latest Row $\rightarrow$ Calculates DVA Steps.Transmit: Python returns a JSON array of the simulation history.Render: JavaScript grabs the last element of that array to fill the KPI cards and uses the full array to draw the charts.
Binary file not shown.
+15 -10
View File
@@ -65,35 +65,39 @@ def api_backtest():
# 1. Extract and Sanitize Symbol
symbol = data.get('symbol', '').strip().upper()
print(f"DEBUG: Processing {symbol}")
print(f"DEBUG: Processing {symbol} with payload: {data}")
if not symbol:
return jsonify({"error": "Symbol is required"}), 400
try:
# 2. Extract Numerical and Logic Inputs (Matching updated JS keys)
# 2. Extract and Cast Inputs (Ensuring types match engine requirements)
# Note: JS uses 'startDate' (camelCase), Python often uses 'start_date'
initial = float(data.get('initial_inv', 0))
monthly = float(data.get('monthly_target', 0))
start_date = data.get('start_date', '2024-01-01')
start_date = data.get('startDate') or data.get('start_date', '2024-01-01')
frequency = data.get('frequency', 'Monthly')
# Robust Boolean Check for toggles
# Robust Boolean Check
allow_sell = data.get('allow_sell') is True
allow_frac = data.get('allow_fractional') is True
# 3. Initialize Engines
# DataEngine __init__ now calls ensure_data() to download if missing
data_eng = DataEngine(symbol=symbol)
# Verify file exists after DataEngine logic
if not os.path.exists(data_eng.file_path):
return jsonify({"error": f"Data for {symbol} could not be retrieved."}), 404
strat_eng = StrategyEngine(data_eng)
# 4. Calculation
history = strat_eng.calculate_va_vs_dca(
initial_inv=initial,
monthly_target=monthly,
# 4. Calculation - Calling the correctly named method 'run_simulation'
# Ensure arguments match the signature in your engine.py
history = strat_eng.run_simulation(
start_date=start_date,
monthly_goal=monthly,
initial_inv=initial,
frequency=frequency,
allow_sell=allow_sell,
allow_fractional=allow_frac
)
@@ -104,7 +108,8 @@ def api_backtest():
import traceback
print("CRITICAL ERROR in /api/backtest:")
print(traceback.format_exc())
return jsonify({"error": "Internal Server Error. Check terminal logs."}), 500
# Returning the actual error message helps debugging the '500' faster
return jsonify({"error": str(e)}), 500
@app.route('/backtest') # This is the URL you will actually visit
def backtest_ui():
+170 -39
View File
@@ -478,32 +478,52 @@ class StrategyEngine:
anchor_day = start_dt_obj.day
latest_csv_date = df['date'].max() # This captures 2026-01-27
# 3. Filter data starting from your start_date
df_filtered = df[df['date'] >= start_dt_obj].copy()
# 4. Select recurring monthly days (The first trading day on/after the anchor day)
monthly_df = df_filtered[df_filtered['date'].dt.day >= anchor_day].groupby([
df_filtered['date'].dt.year,
df_filtered['date'].dt.month
], as_index=False).first()
# 5. FORCE LAST ROW: If the latest date from CSV isn't in our list, append it
if monthly_df.empty or monthly_df.iloc[-1]['date'] != latest_csv_date:
last_row = df_filtered[df_filtered['date'] == latest_csv_date]
monthly_df = pd.concat([monthly_df, last_row]).drop_duplicates(subset=['date'])
# 6. Finalize index for the strategy loop
monthly_df.index = pd.to_datetime(monthly_df['date'])
if monthly_df.empty:
# --- 3. Filter data starting from your start_date ---
df_from_start = df[df['date'] >= start_dt_obj].copy()
if df_from_start.empty:
return []
# Helper for share calculation based on user toggle
# --- 4. Choose rows based on Frequency ---
if frequency == "Weekly":
# Take every 5th trading day
final_df = df_from_start.iloc[::5].copy()
step_increment = float((monthly_goal * 12) / 52)
elif frequency == "Bi-Weekly":
# Take every 10th trading day
final_df = df_from_start.iloc[::10].copy()
step_increment = float((monthly_goal * 12) / 26)
else: # Default to Monthly
# Group by year/month and take the first available day >= anchor_day
final_df = df_from_start[df_from_start['date'].dt.day >= anchor_day].groupby([
df_from_start['date'].dt.year,
df_from_start['date'].dt.month
], as_index=False).first()
step_increment = float(monthly_goal)
# --- 5. FORCE LAST ROW SAFETY ---
# Reset index to make concatenation clean
final_df = final_df.reset_index(drop=True)
if not df_from_start.empty:
latest_row = df_from_start.iloc[[-1]] # Gets the absolute last available day
if final_df.empty or latest_row.iloc[0]['date'] != final_df.iloc[-1]['date']:
final_df = pd.concat([final_df, latest_row]).drop_duplicates(subset=['date'])
# Sort and final index reset before the loop
final_df = final_df.sort_values('date').reset_index(drop=True)
if final_df.empty:
return []
# --- 6. Helper for share calculation ---
def get_shares(cash, prc):
if prc <= 0: return 0
# Note: allow_fractional should be defined in your class/scope
return cash / prc if allow_fractional else math.floor(cash / prc)
# 2. Initial Setup
# --- 7. Initial Setup ---
va_shares = 0
dca_shares = 0
va_invested = 0
@@ -511,41 +531,43 @@ class StrategyEngine:
va_target_value = 0
history = []
# 3. Strategy Loop
for i, row in monthly_df.iterrows():
actual_date_str = i.strftime('%Y-%m-%d')
# --- 8. Strategy Loop ---
for step, (idx, row) in enumerate(final_df.iterrows()):
actual_date_str = row['date'].strftime('%Y-%m-%d')
price = float(row['close'])
if i == monthly_df.index[0]:
# --- MONTH 0: INITIAL DEPOSIT ---
actual_inv = initial_inv # This is the 'va_diff'
if step == 0:
# --- STEP 0: INITIAL DEPOSIT ---
actual_inv = initial_inv
dca_actual_inv = initial_inv
va_target_value = initial_inv
diff = 0
va_new_shares = get_shares(actual_inv, price)
dca_new_shares = va_new_shares
else:
# --- MONTH 1+: DVA vs DCA ---
# --- STEP 1+: DVA vs DCA ---
# Use step_increment (calculated in your frequency logic)
# to ensure growth matches frequency (Weekly/Monthly)
current_increment = step_increment
# DCA Logic
dca_actual_inv = monthly_target
dca_actual_inv = current_increment
dca_new_shares = get_shares(dca_actual_inv, price)
# DVA Logic (Fixed Value Path)
va_target_value += monthly_target
va_target_value += current_increment
# Gap calculation: Target vs. current value BEFORE this month's investment
# Gap calculation: Target vs. current value BEFORE this step's investment
current_va_val_pre = va_shares * price
diff = va_target_value - current_va_val_pre
# Apply Buy/Sell constraints
# note: allow_sell should be defined in your class/scope
actual_inv = diff if (diff >= 0 or allow_sell) else 0
va_new_shares = get_shares(actual_inv, price)
# --- STATE UPDATES (Must happen for both Month 0 and Month 1+) ---
# --- STATE UPDATES ---
va_shares += va_new_shares
dca_shares += dca_new_shares
@@ -553,7 +575,6 @@ class StrategyEngine:
dca_invested += dca_actual_inv
# --- Unified History Append ---
# We calculate these here so they are ALWAYS defined for every row
history.append({
"date": actual_date_str,
"price": round(price, 2),
@@ -561,14 +582,124 @@ class StrategyEngine:
"dca_invested": round(dca_invested, 2),
"dca_shares_trans": round(dca_new_shares, 4),
"dca_shares_total": round(dca_shares, 4),
"va_value": round(va_shares * price, 2), # Becomes 'Current Portfolio Value'
"va_invested": round(va_invested, 2), # Becomes 'Total Invested'
"va_value": round(va_shares * price, 2),
"va_invested": round(va_invested, 2),
"va_diff": round(actual_inv, 2),
"va_shares_trans": round(va_new_shares, 4),
"va_shares_total": round(va_shares, 4),
"va_target_value": round(va_target_value, 2) # Used for next goal
"va_target_value": round(va_target_value, 2)
})
return history
def run_simulation(self, start_date, monthly_goal, initial_inv, frequency="Monthly", allow_sell=True, allow_fractional=True):
# 1. DATA LOADING
df = pd.read_csv(self.data_engine.file_path)
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values('date')
# 2. DATE PREP
start_dt_obj = pd.to_datetime(start_date)
latest_csv_date = df['date'].max()
# Filter data starting from user's choice
df_from_start = df[df['date'] >= start_dt_obj].copy()
if df_from_start.empty:
return []
# 3. FREQUENCY LOGIC (Hardened)
# Standardize string for comparison
freq_check = str(frequency).strip().title()
if freq_check == "Weekly":
final_df = df_from_start.iloc[::5].copy()
step_increment = float((monthly_goal * 12) / 52)
elif freq_check == "Bi-Weekly":
final_df = df_from_start.iloc[::10].copy()
step_increment = float((monthly_goal * 12) / 26)
else:
# For Monthly, we need the anchor day
anchor_day = start_dt_obj.day
final_df = df_from_start[df_from_start['date'].dt.day >= anchor_day].groupby([
df_from_start['date'].dt.year,
df_from_start['date'].dt.month
], as_index=False).first()
step_increment = float(monthly_goal)
# 4. SAFETY: Ensure most recent price is included
final_df = final_df.reset_index(drop=True)
last_actual_row = df_from_start.iloc[[-1]]
if final_df.empty or last_actual_row.iloc[0]['date'] != final_df.iloc[-1]['date']:
final_df = pd.concat([final_df, last_actual_row]).drop_duplicates(subset=['date'])
final_df = final_df.sort_values('date').reset_index(drop=True)
# 5. STRATEGY INITIALIZATION
def get_shares(cash, prc):
if prc <= 0: return 0
if allow_fractional:
return float(cash / prc)
else:
# Handles both buying (+) and selling (-) for whole shares
return float(math.floor(cash / prc)) if cash >= 0 else float(math.ceil(cash / prc))
# Ensure these are initialized as floats
va_shares, dca_shares = 0.0, 0.0
va_invested, dca_invested = 0.0, 0.0
history = []
for step, (idx, row) in enumerate(final_df.iterrows()):
actual_date_str = row['date'].strftime('%Y-%m-%d')
price = float(row['close'])
if step == 0:
# First row: Both strategies start with the Initial Investment
va_actual_inv = float(initial_inv)
dca_actual_inv = float(initial_inv)
va_target_value = float(initial_inv)
else:
# Subsequent rows: Use the frequency-adjusted step_increment
va_target_value += step_increment
# DCA logic: Always invests the same amount every period
dca_actual_inv = float(step_increment)
# VA logic: Invests enough to hit the target value
current_va_market_val = va_shares * price
diff = va_target_value - current_va_market_val
# Apply "Allow Sell" constraint
va_actual_inv = diff if (diff >= 0 or allow_sell) else 0.0
# Update Shares based on fractional setting
va_new_shares = get_shares(va_actual_inv, price)
dca_new_shares = get_shares(dca_actual_inv, price)
# Running totals for shares
va_shares += va_new_shares
dca_shares += dca_new_shares
# Running totals for principal invested
va_invested += va_actual_inv
dca_invested += dca_actual_inv
history.append({
"date": actual_date_str,
"price": round(price, 2),
"va_diff": round(va_actual_inv, 2), # Invested this step (VA)
"va_shares_trans": round(va_new_shares, 4),
"va_value": round(va_shares * price, 2), # Current Portfolio Value (VA)
"va_invested": round(va_invested, 2), # Total Out-of-Pocket (VA)
"va_shares_total": round(va_shares, 4),
"va_target_value": round(float(va_target_value), 2),
"dca_diff": round(dca_actual_inv, 2), # Invested this step (DCA)
"dca_shares_trans": round(dca_new_shares, 4),
"dca_value": round(dca_shares * price, 2), # Current Portfolio Value (DCA)
"dca_invested": round(dca_invested, 2), # Total Out-of-Pocket (DCA)
"dca_shares_total": round(dca_shares, 4)
})
# Debugging print
print(f"Date: {i.strftime('%Y-%m')}, Target: {va_target_value:.2f}, Portfolio: {va_invested:.2f}, Diff: {diff:.2f}")
return history
+155 -157
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}`);
if (!payload.symbol) {
alert("Please enter a ticker symbol.");
return;
}
const data = await res.json();
// Loading State
el.btn.disabled = true;
el.btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Loading...';
// Update UI
document.getElementById('kpiArea')?.classList.remove('d-none');
document.getElementById('resultsArea')?.classList.remove('d-none');
const res = await fetch('/api/backtest', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
updateKPIs(data, payload.monthly_target);
renderDetailedChart(data);
renderTable(data);
// Parse response to check for specific Python errors
const data = await res.json();
} 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;
}
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.
@@ -236,54 +250,33 @@ function updateKPIs(data, monthlyTarget) {
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>