value averging model bulild, can use yahoo and csv fron other sources
This commit is contained in:
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
from flask import Flask, render_template, jsonify
|
from flask import Flask, render_template,request, jsonify
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from engine import DataEngine
|
from engine import DataEngine, StrategyEngine
|
||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
import csv, os, logging
|
import csv, os, logging
|
||||||
@@ -9,10 +9,6 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
cache = Cache(app, config={'CACHE_TYPE': 'SimpleCache'})
|
cache = Cache(app, config={'CACHE_TYPE': 'SimpleCache'})
|
||||||
|
|
||||||
import os
|
|
||||||
import csv
|
|
||||||
|
|
||||||
|
|
||||||
@cache.memoize(timeout=3600)
|
@cache.memoize(timeout=3600)
|
||||||
def fetch_and_calculate(config):
|
def fetch_and_calculate(config):
|
||||||
engine = DataEngine(config['symbol'], config['url'], config['provider'])
|
engine = DataEngine(config['symbol'], config['url'], config['provider'])
|
||||||
@@ -63,5 +59,57 @@ def run_sync():
|
|||||||
report = engine.global_sync()
|
report = engine.global_sync()
|
||||||
return jsonify(report)
|
return jsonify(report)
|
||||||
|
|
||||||
|
@app.route('/api/backtest', methods=['POST'])
|
||||||
|
def api_backtest():
|
||||||
|
data = request.get_json() or {}
|
||||||
|
|
||||||
|
# 1. Extract and Sanitize Symbol
|
||||||
|
symbol = data.get('symbol', '').strip().upper()
|
||||||
|
print(f"DEBUG: Processing {symbol}")
|
||||||
|
|
||||||
|
if not symbol:
|
||||||
|
return jsonify({"error": "Symbol is required"}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 2. Extract Numerical and Logic Inputs (Matching updated JS keys)
|
||||||
|
initial = float(data.get('initial_inv', 0))
|
||||||
|
monthly = float(data.get('monthly_target', 0))
|
||||||
|
start_date = data.get('start_date', '2024-01-01')
|
||||||
|
|
||||||
|
# Robust Boolean Check for toggles
|
||||||
|
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)
|
||||||
|
|
||||||
|
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,
|
||||||
|
allow_fractional=allow_frac
|
||||||
|
)
|
||||||
|
|
||||||
|
return jsonify(history)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print("CRITICAL ERROR in /api/backtest:")
|
||||||
|
print(traceback.format_exc())
|
||||||
|
return jsonify({"error": "Internal Server Error. Check terminal logs."}), 500
|
||||||
|
|
||||||
|
@app.route('/backtest') # This is the URL you will actually visit
|
||||||
|
def backtest_ui():
|
||||||
|
# This sends the HTML file to your browser
|
||||||
|
return render_template('val_avg_cal.html')
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(debug=True, host='0.0.0.0', port=5000)
|
app.run(debug=True, host='0.0.0.0', port=5000)
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+5936
File diff suppressed because it is too large
Load Diff
+2589
File diff suppressed because it is too large
Load Diff
+8306
File diff suppressed because it is too large
Load Diff
+3870
File diff suppressed because it is too large
Load Diff
@@ -1,56 +1,107 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import requests
|
import requests
|
||||||
import os
|
import os
|
||||||
|
import csv
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import datetime, time
|
from datetime import datetime, time
|
||||||
|
import yfinance as yf
|
||||||
from ta.trend import EMAIndicator
|
from ta.trend import EMAIndicator
|
||||||
from ta.momentum import StochasticOscillator
|
from ta.momentum import StochasticOscillator
|
||||||
|
import math
|
||||||
|
|
||||||
class DataEngine:
|
class DataEngine:
|
||||||
def __init__(self, symbol=None, url=None, provider=None, data_dir='data_cache'):
|
def __init__(self, symbol=None, url=None, provider=None, data_dir='data_cache'):
|
||||||
self.symbol = symbol
|
# 1. Clean the incoming symbol
|
||||||
self.url = url
|
self.symbol = symbol.strip().upper() if symbol else None
|
||||||
self.provider = provider
|
|
||||||
|
|
||||||
# Use your robust path logic
|
# 2. Setup centralized paths
|
||||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||||
self.cache_dir = os.path.join(base_path, data_dir) # Use data_dir variable
|
self.cache_dir = os.path.join(base_path, data_dir)
|
||||||
os.makedirs(self.cache_dir, exist_ok=True)
|
os.makedirs(self.cache_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 3. Load the master instrument list to find URLs/Providers
|
||||||
|
# This ensures the engine knows where to go for special tickers
|
||||||
|
self.master_instruments = self.load_instruments_from_csv('instruments.csv')
|
||||||
|
|
||||||
# 4. Only set file_path if we actually have a symbol
|
# 4. Find config from master list or use passed-in arguments
|
||||||
|
instrument_config = next((i for i in self.master_instruments if i['symbol'] == self.symbol), None)
|
||||||
|
|
||||||
|
if instrument_config:
|
||||||
|
self.url = instrument_config['url']
|
||||||
|
self.provider = instrument_config['provider']
|
||||||
|
else:
|
||||||
|
# Fallback to arguments if ticker isn't in the CSV list
|
||||||
|
self.url = url
|
||||||
|
self.provider = provider or 'yahoo'
|
||||||
|
|
||||||
|
# 5. Define final file path for centralized storage
|
||||||
if self.symbol:
|
if self.symbol:
|
||||||
self.file_path = os.path.join(self.cache_dir, f"{self.symbol}.csv")
|
self.file_path = os.path.join(self.cache_dir, f"{self.symbol}.csv")
|
||||||
else:
|
else:
|
||||||
self.file_path = None
|
self.file_path = None
|
||||||
|
|
||||||
|
self.ensure_data()
|
||||||
|
|
||||||
|
def ensure_data(self):
|
||||||
|
"""Checks if file exists; if not, downloads it."""
|
||||||
|
if os.path.exists(self.file_path):
|
||||||
|
return True # Data is already there
|
||||||
|
|
||||||
|
print(f"DEBUG: {self.symbol} not found in cache. Attempting download...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# For a generic ticker like SPY, we use yfinance
|
||||||
|
import yfinance as yf
|
||||||
|
df = yf.download(self.symbol, period="max")
|
||||||
|
|
||||||
|
if df.empty:
|
||||||
|
print(f"ERROR: No data found for {self.symbol}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Clean and save
|
||||||
|
# 1. If columns are MultiIndex (tuples), take just the first level (the price name)
|
||||||
|
if isinstance(df.columns, pd.MultiIndex):
|
||||||
|
df.columns = df.columns.get_level_values(0)
|
||||||
|
# 2. Reset index to turn 'Date' into a column
|
||||||
|
df.reset_index(inplace=True)
|
||||||
|
# 3. Now it is safe to lowercase the column names
|
||||||
|
df.columns = [str(c).lower() for c in df.columns]
|
||||||
|
|
||||||
|
df.to_csv(self.file_path, index=False)
|
||||||
|
print(f"DEBUG: Successfully cached {self.symbol}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR: Download failed for {self.symbol}: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def load_instruments_from_csv(self, file_path):
|
def load_instruments_from_csv(self, file_path):
|
||||||
import csv
|
|
||||||
instruments = []
|
instruments = []
|
||||||
|
|
||||||
# Updated templates for maximum historical reach
|
|
||||||
TEMPLATES = {
|
TEMPLATES = {
|
||||||
'jpm': "https://am.jpmorgan.com/FundsMarketingHandler/historicalData?cusip={cusip}&country=hk&role=per",
|
'jpm': "https://am.jpmorgan.com/FundsMarketingHandler/historicalData?cusip={cusip}&country=hk&role=per",
|
||||||
# period1=0 fetches from the earliest available date; interval=1d is daily
|
|
||||||
'yahoo': "https://query1.finance.yahoo.com/v8/finance/chart/{cusip}?period1=0&period2=9999999999&interval=1d&events=history",
|
'yahoo': "https://query1.finance.yahoo.com/v8/finance/chart/{cusip}?period1=0&period2=9999999999&interval=1d&events=history",
|
||||||
# FT remains 30-day window; Smart Append logic in fetch_data handles the history
|
|
||||||
'agi': "https://markets.ft.com/data/funds/tearsheet/historical?s={cusip}"
|
'agi': "https://markets.ft.com/data/funds/tearsheet/historical?s={cusip}"
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
abs_path = os.path.join(os.path.dirname(__file__), file_path)
|
# Get absolute path relative to this script
|
||||||
|
abs_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), file_path)
|
||||||
|
|
||||||
if not os.path.exists(abs_path):
|
if not os.path.exists(abs_path):
|
||||||
print(f"Error: {file_path} not found.")
|
print(f"Error: Master list {file_path} not found at {abs_path}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
with open(abs_path, mode='r', encoding='utf-8-sig') as csvfile:
|
with open(abs_path, mode='r', encoding='utf-8-sig') as csvfile:
|
||||||
reader = csv.DictReader(csvfile)
|
reader = csv.DictReader(csvfile)
|
||||||
|
# Clean header names (lowercase + remove whitespace)
|
||||||
reader.fieldnames = [name.strip().lower() for name in reader.fieldnames]
|
reader.fieldnames = [name.strip().lower() for name in reader.fieldnames]
|
||||||
|
|
||||||
for row in reader:
|
for row in reader:
|
||||||
symbol = row.get('symbol', '').strip()
|
# Use .get() with fallback to avoid KeyErrors
|
||||||
cusip = row.get('cusip', '').strip()
|
symbol = (row.get('symbol') or '').strip().upper()
|
||||||
provider = row.get('provider', 'jpm').strip().lower()
|
cusip = (row.get('cusip') or '').strip()
|
||||||
|
provider = (row.get('provider') or 'jpm').strip().lower()
|
||||||
|
|
||||||
if symbol and cusip:
|
if symbol and cusip:
|
||||||
template = TEMPLATES.get(provider, TEMPLATES['jpm'])
|
template = TEMPLATES.get(provider, TEMPLATES['jpm'])
|
||||||
@@ -60,14 +111,37 @@ class DataEngine:
|
|||||||
"symbol": symbol,
|
"symbol": symbol,
|
||||||
"url": url,
|
"url": url,
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
"cusip": cusip # Added this so sync_all can use it if needed
|
"cusip": cusip
|
||||||
})
|
})
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"CSV Loading Error: {e}")
|
print(f"CRITICAL: Failed to load instruments.csv: {e}")
|
||||||
|
|
||||||
return instruments
|
return instruments
|
||||||
# URL_CONFIG = load_instruments_from_csv('instruments.csv')
|
|
||||||
|
def _ensure_data_exists(self):
|
||||||
|
if not os.path.exists(self.file_path):
|
||||||
|
# Check if this symbol exists in our master CSV mapping
|
||||||
|
match = next((i for i in self.instruments if i['symbol'].upper() == self.symbol), None)
|
||||||
|
|
||||||
|
if match:
|
||||||
|
print(f"DEBUG: Found {self.symbol} in master list. Fetching from {match['provider']}...")
|
||||||
|
self._download_from_provider(match)
|
||||||
|
else:
|
||||||
|
print(f"DEBUG: {self.symbol} not in master list. Trying generic Yahoo Finance...")
|
||||||
|
self._download_generic_yahoo()
|
||||||
|
|
||||||
|
def _download_generic_yahoo(self):
|
||||||
|
"""Standard yfinance fallback"""
|
||||||
|
try:
|
||||||
|
df = yf.download(self.symbol, period="max")
|
||||||
|
if not df.empty:
|
||||||
|
df.reset_index(inplace=True)
|
||||||
|
df.columns = [c.lower() for c in df.columns]
|
||||||
|
df.to_csv(self.file_path, index=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Yahoo fallback failed: {e}")
|
||||||
|
|
||||||
def global_sync(self):
|
def global_sync(self):
|
||||||
"""Backup, Sync all instruments, and return a summary report."""
|
"""Backup, Sync all instruments, and return a summary report."""
|
||||||
# 1. Run Maintenance/Backup
|
# 1. Run Maintenance/Backup
|
||||||
@@ -348,4 +422,153 @@ class DataEngine:
|
|||||||
"last_ema100": get_ema_offset(100),
|
"last_ema100": get_ema_offset(100),
|
||||||
"last_ema200": get_ema_offset(200),
|
"last_ema200": get_ema_offset(200),
|
||||||
"kd_values": f"{k_val}/{d_val}" if k_val != "N/A" else "N/A"
|
"kd_values": f"{k_val}/{d_val}" if k_val != "N/A" else "N/A"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class StrategyEngine:
|
||||||
|
"""
|
||||||
|
Handles financial strategy simulations and backtesting.
|
||||||
|
This class takes a DataEngine instance to access files.
|
||||||
|
"""
|
||||||
|
def __init__(self, data_engine):
|
||||||
|
# 1. Save the engine object (The 'Supplier')
|
||||||
|
self.data_engine = data_engine
|
||||||
|
|
||||||
|
# 2. Extract the symbol from the supplier so the chef knows the name
|
||||||
|
# We don't need .strip() here because DataEngine already did it!
|
||||||
|
self.symbol = data_engine.symbol
|
||||||
|
|
||||||
|
def _find_file(self):
|
||||||
|
# Try the uppercase version first
|
||||||
|
upper_path = os.path.join(self.data_dir, f"{self.symbol}.csv")
|
||||||
|
# Try the lowercase version second
|
||||||
|
lower_path = os.path.join(self.data_dir, f"{self.symbol.lower()}.csv")
|
||||||
|
|
||||||
|
if os.path.exists(upper_path):
|
||||||
|
return upper_path
|
||||||
|
elif os.path.exists(lower_path):
|
||||||
|
return lower_path
|
||||||
|
|
||||||
|
# If neither exists, print a very specific message to your terminal
|
||||||
|
print(f"ERROR: Searched for {upper_path} AND {lower_path} - Neither found!")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def load_data(self):
|
||||||
|
df = pd.read_csv(self.file_path)
|
||||||
|
# Standardize column names to lowercase to avoid 'Price' vs 'price' issues
|
||||||
|
df.columns = [c.lower() for c in df.columns]
|
||||||
|
|
||||||
|
# Map common variations to a single 'price' column
|
||||||
|
if 'adj close' in df.columns:
|
||||||
|
df = df.rename(columns={'adj close': 'close'})
|
||||||
|
elif 'close' in df.columns:
|
||||||
|
df = df.rename(columns={'close': 'close'})
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def calculate_va_vs_dca(self, initial_inv, monthly_target, start_date, allow_sell=True, allow_fractional=True):
|
||||||
|
import math
|
||||||
|
|
||||||
|
# 1. Load and Prepare Data
|
||||||
|
df = pd.read_csv(self.data_engine.file_path)
|
||||||
|
df['date'] = pd.to_datetime(df['date'])
|
||||||
|
df = df.sort_values('date')
|
||||||
|
|
||||||
|
# 2. Identify the "Anchor Day" and the "Absolute Latest Day"
|
||||||
|
start_dt_obj = pd.to_datetime(start_date)
|
||||||
|
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:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Helper for share calculation based on user toggle
|
||||||
|
def get_shares(cash, prc):
|
||||||
|
if prc <= 0: return 0
|
||||||
|
return cash / prc if allow_fractional else math.floor(cash / prc)
|
||||||
|
|
||||||
|
# 2. Initial Setup
|
||||||
|
va_shares = 0
|
||||||
|
dca_shares = 0
|
||||||
|
va_invested = 0
|
||||||
|
dca_invested = 0
|
||||||
|
va_target_value = 0
|
||||||
|
history = []
|
||||||
|
|
||||||
|
# 3. Strategy Loop
|
||||||
|
for i, row in monthly_df.iterrows():
|
||||||
|
actual_date_str = i.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'
|
||||||
|
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 ---
|
||||||
|
|
||||||
|
# DCA Logic
|
||||||
|
dca_actual_inv = monthly_target
|
||||||
|
dca_new_shares = get_shares(dca_actual_inv, price)
|
||||||
|
|
||||||
|
# DVA Logic (Fixed Value Path)
|
||||||
|
va_target_value += monthly_target
|
||||||
|
|
||||||
|
# Gap calculation: Target vs. current value BEFORE this month's investment
|
||||||
|
current_va_val_pre = va_shares * price
|
||||||
|
diff = va_target_value - current_va_val_pre
|
||||||
|
|
||||||
|
# Apply Buy/Sell constraints
|
||||||
|
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+) ---
|
||||||
|
va_shares += va_new_shares
|
||||||
|
dca_shares += dca_new_shares
|
||||||
|
|
||||||
|
va_invested += actual_inv
|
||||||
|
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),
|
||||||
|
"dca_value": round(dca_shares * price, 2),
|
||||||
|
"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_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
|
||||||
|
})
|
||||||
|
# Debugging print
|
||||||
|
print(f"Date: {i.strftime('%Y-%m')}, Target: {va_target_value:.2f}, Portfolio: {va_invested:.2f}, Diff: {diff:.2f}")
|
||||||
|
|
||||||
|
return history
|
||||||
@@ -297,6 +297,24 @@
|
|||||||
window.onload = function() {
|
window.onload = function() {
|
||||||
loadData();
|
loadData();
|
||||||
};
|
};
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
callback: function(value) {
|
||||||
|
return '$' + value.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const vaROI = ((last.va_value - last.va_invested) / last.va_invested * 100).toFixed(2);
|
||||||
|
const dcaROI = ((last.dca_value - last.dca_invested) / last.dca_invested * 100).toFixed(2);
|
||||||
|
|
||||||
|
summaryText.innerHTML = `
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col"><strong>VA ROI:</strong> ${vaROI}%</div>
|
||||||
|
<div class="col"><strong>DCA ROI:</strong> ${dcaROI}%</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,446 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Value Averaging Pro | Analysis Tool</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@300;400;600;700&display=swap" rel="stylesheet">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body { font-family: 'Epilogue', sans-serif; background-color: #f4f7fe; color: #1a1f36; }
|
||||||
|
.card { border: none; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.04); background: #fff; }
|
||||||
|
.kpi-card { border-radius: 12px; padding: 20px; text-align: center; height: 100%; border: 1px solid #edf2f7; }
|
||||||
|
.next-inv-header { background: #4d55ff; color: white; border-radius: 12px 12px 0 0; padding: 15px; }
|
||||||
|
|
||||||
|
/* Table Styling */
|
||||||
|
.table thead th { background-color: #1a1f36; color: white; vertical-align: middle; border: none; }
|
||||||
|
.table-striped tbody tr:nth-of-type(odd) { background-color: rgba(244, 247, 254, 0.7); }
|
||||||
|
.badge-ret { border-radius: 50px; padding: 6px 12px; font-weight: 700; font-size: 0.85rem; }
|
||||||
|
.col-dva { background-color: #2dce89 !important; color: white !important; }
|
||||||
|
.col-dca { background-color: #11cdef !important; color: white !important; }
|
||||||
|
|
||||||
|
/* Tab Styling */
|
||||||
|
.nav-pills .nav-link { color: #4d55ff; font-weight: 600; padding: 12px 30px; }
|
||||||
|
.nav-pills .nav-link.active { background-color: #4d55ff !important; }
|
||||||
|
.chart-container { position: relative; height: 500px; width: 100%; }
|
||||||
|
</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="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>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="kpiArea" class="d-none mb-4">
|
||||||
|
<div class="d-flex justify-content-end mb-2">
|
||||||
|
<span id="syncBadge" class="badge rounded-pill bg-success px-3">
|
||||||
|
Data Synced: <span id="lastSyncDate">YYYY-MM-DD</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="row g-3 d-flex align-items-stretch">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card p-3 text-center h-100">
|
||||||
|
<h6 class="text-muted text-uppercase small">Target Value Goal</h6>
|
||||||
|
<h2 id="targetValueCard" class="fw-bold mb-1">$0.00</h2>
|
||||||
|
<small class="text-muted">Expected value</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card p-3 text-center border-primary h-100">
|
||||||
|
<h6 class="text-muted text-uppercase small">Next Recommended Move</h6>
|
||||||
|
<h2 id="nextInvAmt" class="fw-bold mb-1">$0.00</h2>
|
||||||
|
<div class="small text-muted mb-2">
|
||||||
|
Price: <span id="latestPriceDisplay" class="fw-bold text-dark">$0.00</span>
|
||||||
|
as of <span id="priceDateLabel" class="fw-bold text-dark">YYYY-MM-DD</span>
|
||||||
|
</div>
|
||||||
|
<p id="nextInvMsg" class="mb-0 small text-muted">Calculating...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4 d-flex flex-column">
|
||||||
|
<div class="card p-2 text-center flex-fill mb-2 d-flex justify-content-center">
|
||||||
|
<p class="text-muted small mb-0">Total Invested (DVA)</p>
|
||||||
|
<div id="totalSaved" class="h5 fw-bold mb-0">$0.00</div>
|
||||||
|
</div>
|
||||||
|
<div class="card p-2 text-center flex-fill d-flex justify-content-center">
|
||||||
|
<p class="text-muted small mb-0">Current Portfolio Value</p>
|
||||||
|
<div id="totalVal" class="h5 fw-bold mb-0">$0.00</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="resultsArea" class="d-none">
|
||||||
|
<ul class="nav nav-pills nav-justified mb-3 card p-2 flex-row" id="mainTabs">
|
||||||
|
<li class="nav-item"><button class="nav-link active" data-bs-toggle="pill" data-bs-target="#tab-chart">Performance Chart</button></li>
|
||||||
|
<li class="nav-item"><button class="nav-link" data-bs-toggle="pill" data-bs-target="#tab-table">Data Ledger</button></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="tab-content card p-4">
|
||||||
|
<div class="tab-pane fade show active" id="tab-chart">
|
||||||
|
<div class="chart-container">
|
||||||
|
<canvas id="mainChart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane fade" id="tab-table">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h5 class="fw-bold m-0">Historical Ledger</h5>
|
||||||
|
<button class="btn btn-success btn-sm fw-bold" onclick="exportToExcel()">📥 Export to Excel</button>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped align-middle border" id="ledgerTable">
|
||||||
|
<thead class="text-center">
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2">Date</th>
|
||||||
|
<th rowspan="2">Price</th>
|
||||||
|
<th colspan="2" class="col-dva">Investment & Shares</th> <th colspan="2" class="col-dca">Total Value & Balance</th> <th colspan="2" class="bg-secondary">Annualized Return</th>
|
||||||
|
</tr>
|
||||||
|
<tr class="small">
|
||||||
|
<th class="col-dva">DVA ($ / Δ Shares)</th> <th class="col-dca">DCA</th>
|
||||||
|
<th class="col-dva">DVA ($ / Total Shrs)</th> <th class="col-dca">DCA</th>
|
||||||
|
<th class="col-dva">DVA (%)</th>
|
||||||
|
<th class="col-dca">DCA (%)</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="tableBody" class="text-center"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
|
<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
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!payload.symbol) {
|
||||||
|
alert("Please enter a ticker symbol.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading State
|
||||||
|
el.btn.disabled = true;
|
||||||
|
el.btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Loading...';
|
||||||
|
|
||||||
|
const res = await fetch('/api/backtest', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) throw new Error(`Server returned ${res.status}`);
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// 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("Failed to run analysis. See console for details.");
|
||||||
|
} finally {
|
||||||
|
el.btn.disabled = false;
|
||||||
|
el.btn.innerHTML = originalText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Updates the summary cards with the DVA "Next Move" recommendation.
|
||||||
|
* Uses the Fixed Value Path logic: (Last Target + Monthly Increment) - Current Value.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Updates the Dashboard KPIs and Recommended Move
|
||||||
|
* @param {Array} data - The history array from the Python backend
|
||||||
|
* @param {number} monthlyTarget - The user-defined monthly investment increment
|
||||||
|
*/
|
||||||
|
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) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.innerText = val;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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)}`);
|
||||||
|
|
||||||
|
// 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";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
*/
|
||||||
|
function updateSyncBadge(dateString) {
|
||||||
|
const syncDateEl = document.getElementById('lastSyncDate');
|
||||||
|
const syncBadge = document.getElementById('syncBadge');
|
||||||
|
|
||||||
|
if (syncDateEl && syncBadge) {
|
||||||
|
syncDateEl.innerText = dateString;
|
||||||
|
|
||||||
|
// Calculate age of data
|
||||||
|
const dataDate = new Date(dateString);
|
||||||
|
const today = new Date();
|
||||||
|
const diffDays = Math.ceil(Math.abs(today - dataDate) / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
// Turn badge red if data is older than 5 days
|
||||||
|
if (diffDays > 5) {
|
||||||
|
syncBadge.classList.replace('bg-success', 'bg-danger');
|
||||||
|
} else {
|
||||||
|
syncBadge.classList.replace('bg-danger', 'bg-success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function renderDetailedChart(data) {
|
||||||
|
if(myChart) myChart.destroy();
|
||||||
|
const ctx = document.getElementById('mainChart').getContext('2d');
|
||||||
|
|
||||||
|
myChart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: data.map(d => d.date),
|
||||||
|
datasets: [
|
||||||
|
{ label: 'Portfolio Value', data: data.map(d => d.va_value), borderColor: '#2dce89', yAxisID: 'y', tension: 0.3, fill: true, backgroundColor: 'rgba(45, 206, 137, 0.05)' },
|
||||||
|
{ label: 'Total Invested', data: data.map(d => d.va_invested), borderColor: '#5e72e4', yAxisID: 'y', tension: 0, borderDash: [5,5] },
|
||||||
|
{ label: 'Return (%)', data: data.map(d => ((d.va_value-d.va_invested)/d.va_invested*100).toFixed(2)), borderColor: '#f5365c', yAxisID: 'y1', tension: 0.3 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true, maintainAspectRatio: false,
|
||||||
|
interaction: { mode: 'index', intersect: false }, // Critical for hover detail
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
let label = context.dataset.label || '';
|
||||||
|
let val = context.parsed.y;
|
||||||
|
return label.includes('%') ? `${label}: ${val}%` : `${label}: $${val.toLocaleString()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: { type: 'linear', position: 'left', ticks: { callback: v => '$' + v.toLocaleString() } },
|
||||||
|
y1: { type: 'linear', position: 'right', grid: { drawOnChartArea: false }, ticks: { callback: v => v + '%' } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Updated renderTable to remove text labels and fix ID reference
|
||||||
|
function renderTable(data) {
|
||||||
|
const tableBody = document.getElementById('tableBody');
|
||||||
|
const initialInput = parseFloat(document.getElementById('initial_inv').value) || 0;
|
||||||
|
const monthlyInput = parseFloat(document.getElementById('monthly_target').value) || 0;
|
||||||
|
|
||||||
|
if (!tableBody) 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;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td class="small fw-bold">${d.date}</td>
|
||||||
|
<td>$${d.price.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>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
$${dcaInvestedThisMonth.toLocaleString()}<br>
|
||||||
|
<small class="text-muted">+${d.dca_shares_trans.toFixed(2)}</small>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="fw-bold">
|
||||||
|
$${d.va_value.toLocaleString()}<br>
|
||||||
|
<small class="text-primary">${d.va_shares_total.toFixed(2)}</small>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
$${d.dca_value.toLocaleString()}<br>
|
||||||
|
<small class="text-primary">${d.dca_shares_total.toFixed(2)}</small>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<span class="badge ${vaAnnRet >= 0 ? 'bg-success' : 'bg-danger'}">
|
||||||
|
${vaAnnRet}%
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge ${dcaAnnRet >= 0 ? 'bg-info' : 'bg-danger'}">
|
||||||
|
${dcaAnnRet}%
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportToExcel() {
|
||||||
|
const table = document.getElementById("ledgerTable");
|
||||||
|
const wb = XLSX.utils.table_to_book(table);
|
||||||
|
XLSX.writeFile(wb, "Investment_Backtest_Results.xlsx");
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetAll() {
|
||||||
|
// 1. Reset Inputs to Defaults
|
||||||
|
const defaults = {
|
||||||
|
'symbol': 'SPY',
|
||||||
|
'initial_inv': 20000,
|
||||||
|
'monthly_target': 500,
|
||||||
|
'startDate': '2024-01-01'
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(defaults).forEach(id => {
|
||||||
|
const input = document.getElementById(id);
|
||||||
|
if (input) input.value = defaults[id];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Reset Toggles
|
||||||
|
const sellToggle = document.getElementById('allow_sell');
|
||||||
|
const fracToggle = document.getElementById('allow_fractional');
|
||||||
|
if (sellToggle) sellToggle.checked = true;
|
||||||
|
if (fracToggle) fracToggle.checked = true;
|
||||||
|
|
||||||
|
// 3. Hide Result Sections
|
||||||
|
document.getElementById('kpiArea')?.classList.add('d-none');
|
||||||
|
document.getElementById('resultsArea')?.classList.add('d-none');
|
||||||
|
|
||||||
|
// 4. Clear Data
|
||||||
|
const tableBody = document.getElementById('tableBody');
|
||||||
|
if (tableBody) tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
// 5. Clear Chart if it exists
|
||||||
|
if (window.detailedChart) {
|
||||||
|
window.detailedChart.destroy();
|
||||||
|
window.detailedChart = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Dashboard reset successfully.");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user