add weekly and bi-weekly frequently
This commit is contained in:
@@ -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., yesterday’s 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.
@@ -65,36 +65,40 @@ 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,
|
||||
start_date=start_date,
|
||||
allow_sell=allow_sell,
|
||||
# 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():
|
||||
|
||||
@@ -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()
|
||||
# --- 3. Filter data starting from your start_date ---
|
||||
df_from_start = df[df['date'] >= start_dt_obj].copy()
|
||||
if df_from_start.empty:
|
||||
return []
|
||||
|
||||
# 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()
|
||||
# --- 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)
|
||||
|
||||
# 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'])
|
||||
elif frequency == "Bi-Weekly":
|
||||
# Take every 10th trading day
|
||||
final_df = df_from_start.iloc[::10].copy()
|
||||
step_increment = float((monthly_goal * 12) / 26)
|
||||
|
||||
# 6. Finalize index for the strategy loop
|
||||
monthly_df.index = pd.to_datetime(monthly_df['date'])
|
||||
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)
|
||||
|
||||
if monthly_df.empty:
|
||||
# --- 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 []
|
||||
|
||||
# Helper for share calculation based on user toggle
|
||||
# --- 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)
|
||||
})
|
||||
# Debugging print
|
||||
print(f"Date: {i.strftime('%Y-%m')}, Target: {va_target_value:.2f}, Portfolio: {va_invested:.2f}, Diff: {diff:.2f}")
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
return history
|
||||
+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