From 891f0ea2b0fbc113e16e198c3b481bb6c5d2b516 Mon Sep 17 00:00:00 2001 From: tab888 Date: Mon, 2 Feb 2026 21:30:19 +0800 Subject: [PATCH] fix fetch data in signal page and the backtest export excel issue --- ... A.csv => ALLIANZ ORIENTAL INCOME CL A.csv | 0 __pycache__/engine.cpython-313.pyc | Bin 33919 -> 34860 bytes app.py | 54 +- data_cache/AAPL.csv | 1257 +++ data_cache/ASX.AX.csv | 8207 +++----------- data_cache/CBA.AX.csv | 8788 --------------- data_cache/CSL.AX.csv | 8812 ++------------- ...PMORGAN EVERGREEN.csv => HK0000055829.csv} | 0 .../{JPM KOREA.csv => LU0301634860.csv} | 1 + data_cache/LU0348783233:USD.csv | 22 + data_cache/PML.csv | 5936 ---------- data_cache/RIO.AX.csv | 9746 ----------------- data_cache/SPMO ETF - USD.csv | 2593 ----- data_cache/SPMO.csv | 5181 ++++----- data_cache/SPY.csv | 9059 ++------------- data_cache/TEMP.csv | 962 -- data_cache/VOO.csv | 5127 +++------ engine.py | 172 +- templates/index.html | 6 +- templates/settings.html | 161 + templates/val_avg_cal.html | 47 +- 21 files changed, 8328 insertions(+), 57803 deletions(-) rename data_cache/ALLIANZ ORIENTAL INCOME CL A.csv => ALLIANZ ORIENTAL INCOME CL A.csv (100%) create mode 100644 data_cache/AAPL.csv delete mode 100644 data_cache/CBA.AX.csv rename data_cache/{JPMORGAN EVERGREEN.csv => HK0000055829.csv} (100%) rename data_cache/{JPM KOREA.csv => LU0301634860.csv} (99%) create mode 100644 data_cache/LU0348783233:USD.csv delete mode 100644 data_cache/PML.csv delete mode 100644 data_cache/RIO.AX.csv delete mode 100644 data_cache/SPMO ETF - USD.csv delete mode 100644 data_cache/TEMP.csv create mode 100644 templates/settings.html diff --git a/data_cache/ALLIANZ ORIENTAL INCOME CL A.csv b/ALLIANZ ORIENTAL INCOME CL A.csv similarity index 100% rename from data_cache/ALLIANZ ORIENTAL INCOME CL A.csv rename to ALLIANZ ORIENTAL INCOME CL A.csv diff --git a/__pycache__/engine.cpython-313.pyc b/__pycache__/engine.cpython-313.pyc index baf4b140c059f54bcfaf3b30bfd17258620cf274..e5f319334138eca49bdb9933a6c1bb838d796e79 100644 GIT binary patch delta 8520 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 delta 8483 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 Arguments > Yahoo Fallback) + # 4. Resolve Config: Find this symbol in your CSV config = next((i for i in self.master_instruments if i['symbol'].upper() == self.symbol), None) if config: - self.url = config['url'] - self.provider = config['provider'] - elif url: - self.url = url - self.provider = provider or 'yahoo' - elif self.symbol: - # Automatic Fallback for missing tickers - self.url = f"https://query1.finance.yahoo.com/v8/finance/chart/{self.symbol}?interval=1d&range=2y" - self.provider = 'yahoo' + self.provider = config.get('provider', 'yahoo').lower() + # BUILD URL DYNAMICALLY based on Symbol + Provider + if self.provider == 'jpm': + self.url = f"https://am.jpmorgan.com/FundsMarketingHandler/historicalData?cusip={self.symbol}&country=hk&role=per" + elif self.provider == 'ft': + self.url = f"https://markets.ft.com/data/funds/tearsheet/historical?s={self.symbol}" + else: + self.url = f"https://query1.finance.yahoo.com/v8/finance/chart/{self.symbol}?interval=1d&range=5y" else: - self.url = None - self.provider = None + # Fallback for symbols not in your CSV + self.url = f"https://query1.finance.yahoo.com/v8/finance/chart/{self.symbol}?interval=1d&range=5y" if self.symbol else None + self.provider = 'yahoo' # 5. Define file path and auto-sync self.file_path = os.path.join(self.cache_dir, f"{self.symbol}.csv") if self.symbol else None - # This now handles the "24-hour check" automatically - if self.symbol: - self.ensure_data() - def ensure_data(self): """Checks if file exists and is fresh (less than 24h old).""" CACHE_EXPIRY = 24 * 3600 # 24 hours @@ -67,42 +67,40 @@ class DataEngine: # which uses the URLs from your TEMPLATES return self.fetch_data() - def load_instruments_from_csv(self, file_path): + def load_instruments_from_csv(self, file_path='instruments.csv'): instruments = [] - # Dynamic templates based on your preference + # Templates use {id} as a generic placeholder TEMPLATES = { - 'jpm': "https://am.jpmorgan.com/FundsMarketingHandler/historicalData?cusip={cusip}&country=hk&role=per", - 'yahoo': "https://query1.finance.yahoo.com/v8/finance/chart/{cusip}?period1=0&period2=9999999999&interval=1d&events=history", - 'agi': "https://markets.ft.com/data/funds/tearsheet/historical?s={cusip}" + 'jpm': "https://am.jpmorgan.com/FundsMarketingHandler/historicalData?cusip={id}&country=hk&role=per", + 'yahoo': "https://query1.finance.yahoo.com/v8/finance/chart/{id}?period1=0&period2=9999999999&interval=1d&events=history", + 'ft': "https://markets.ft.com/data/funds/tearsheet/historical?s={id}", + 'agi': "https://markets.ft.com/data/funds/tearsheet/historical?s={id}" } try: abs_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), file_path) - if not os.path.exists(abs_path): - return [] + if not os.path.exists(abs_path): return [] with open(abs_path, mode='r', encoding='utf-8-sig') as csvfile: reader = csv.DictReader(csvfile) - reader.fieldnames = [name.strip().lower() for name in reader.fieldnames] + reader.fieldnames = [n.strip().lower() for n in reader.fieldnames] for row in reader: - symbol = (row.get('symbol') or '').strip().upper() - cusip = (row.get('cusip') or '').strip() + # Use whatever identifier is available + uid = (row.get('cusip') or row.get('symbol') or row.get('ticker') or '').strip() + symbol = (row.get('symbol') or row.get('ticker') or row.get('cusip') or '').strip().upper() provider = (row.get('provider') or 'yahoo').strip().lower() - if symbol and cusip: - # Build URL from template + if symbol and uid: template = TEMPLATES.get(provider, TEMPLATES['yahoo']) - url = template.format(cusip=cusip) - instruments.append({ - "symbol": symbol, - "url": url, + "symbol": symbol, + "url": template.format(id=uid), "provider": provider, - "cusip": cusip + "name": row.get('name', symbol) }) except Exception as e: - print(f"CRITICAL: Failed to load instruments.csv: {e}") + print(f"โŒ Critical Load Error: {e}") return instruments def _ensure_data_exists(self): @@ -147,7 +145,12 @@ class DataEngine: try: self.symbol = item['symbol'] self.provider = item['provider'] - self.url = item['url'] + if self.provider == 'jpm': + self.url = f"https://am.jpmorgan.com/FundsMarketingHandler/historicalData?cusip={self.symbol}&country=hk&role=per" + elif self.provider == 'ft': + self.url = f"https://markets.ft.com/data/funds/tearsheet/historical?s={self.symbol}" + else: + self.url = f"https://query1.finance.yahoo.com/v8/finance/chart/{self.symbol}?interval=1d&range=5y" # FIX 2: Use 'self.cache_dir' to match your __init__ logic self.file_path = os.path.join(self.cache_dir, f"{self.symbol}.csv") @@ -174,67 +177,36 @@ class DataEngine: return report def run_pre_sync_maintenance(self): - """Backs up files and reports current data health.""" - import os - import shutil - import pandas as pd - from datetime import datetime - - # 1. Setup paths correctly + """Backs up files and keeps only the 5 most recent backup folders.""" base_dir = os.path.dirname(os.path.abspath(__file__)) - backup_dir = os.path.join(base_dir, 'backups') + backup_root = os.path.join(base_dir, 'backups') - # 2. Create the timestamped folder path FIRST + # 1. Create timestamped folder timestamp = datetime.now().strftime("%Y%m%d_%H%M") - current_backup_path = os.path.join(backup_dir, f"sync_backup_{timestamp}") - - # 3. Create the directories (safety-first) + current_backup_path = os.path.join(backup_root, f"sync_backup_{timestamp}") os.makedirs(current_backup_path, exist_ok=True) - print(f"\n--- Pre-Sync Health Check ({timestamp}) ---") - stats = [] + # 2. Perform Backup + if os.path.exists(self.cache_dir): + files = [f for f in os.listdir(self.cache_dir) if f.endswith('.csv')] + for filename in files: + shutil.copy2( + os.path.join(self.cache_dir, filename), + os.path.join(current_backup_path, filename) + ) + print(f"โœ… Backed up {len(files)} files to {current_backup_path}") - # 4. Check if cache exists to avoid errors - if not os.path.exists(self.cache_dir): - print(f"โš ๏ธ Cache directory not found at {self.cache_dir}") - return pd.DataFrame() - - # 5. Backup loop - for filename in os.listdir(self.cache_dir): - if filename.endswith(".csv"): - src = os.path.join(self.cache_dir, filename) - dst = os.path.join(current_backup_path, filename) - - try: - # Perform copy - shutil.copy2(src, dst) - - # Read data for health check - df = pd.read_csv(src) - - # Store stats - stats.append({ - "Fund": filename.replace(".csv", ""), - "Rows": len(df), - "Start": df['date'].min() if 'date' in df.columns else "N/A", - "End": df['date'].max() if 'date' in df.columns else "N/A" - }) - print(f"๐Ÿ“ฆ Backed up: {filename} ({len(df)} rows)") - - except Exception as e: - print(f"โš ๏ธ Could not backup {filename}: {e}") - continue - - # 6. Display and return report - if stats: - stats_df = pd.DataFrame(stats) - print("\n" + stats_df.to_string(index=False)) - print(f"\nโœ… All backups saved to: {current_backup_path}") - return stats_df - else: - print("๐Ÿ“ญ No CSV files found to backup.") - return pd.DataFrame() + # 3. Cleanup: Keep only last 5 backup folders + all_backups = sorted([ + os.path.join(backup_root, d) for d in os.listdir(backup_root) + if os.path.isdir(os.path.join(backup_root, d)) + ], key=os.path.getmtime) + while len(all_backups) > 5: + oldest = all_backups.pop(0) + shutil.rmtree(oldest) + print(f"๐Ÿงน Storage Cleanup: Removed old backup {os.path.basename(oldest)}") + def _parse_jpm(self, json_data): if isinstance(json_data, dict) and "historicalNAVList" in json_data: df = pd.DataFrame(json_data["historicalNAVList"]) @@ -591,14 +563,20 @@ class StrategyEngine: history.append({ "date": actual_date_str, "price": round(price, 2), + + # --- DISPLAY STRINGS (For the Web UI) --- + "dca_display": f"${round(dca_invested, 2):,.2f} ({dca_new_shares:+.4f})", + "va_display": f"${round(actual_inv, 2):,.2f} ({va_new_shares:+.4f})", + + # --- RAW DATA (Your existing variables kept consistent) --- "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), "va_invested": round(va_invested, 2), - "va_diff": round(actual_inv, 2), - "va_shares_trans": round(va_new_shares, 4), + "va_diff": round(actual_inv, 2), # This matches your ($) in the image + "va_shares_trans": round(va_new_shares, 4), # This matches your (ฮ” Shares) "va_shares_total": round(va_shares, 4), "va_target_value": round(va_target_value, 2) }) diff --git a/templates/index.html b/templates/index.html index cdd4447..731f836 100644 --- a/templates/index.html +++ b/templates/index.html @@ -255,7 +255,9 @@ // --- 3. Construct Row --- htmlContent += ` - ${item.symbol} + +

+ ${displayDate} ${item.last_close} @@ -330,7 +332,7 @@ } } - setInterval(checkStatus, 30000); + setInterval(checkStatus, 300000); checkStatus(); // Initial check // --- 5. Initial Load --- diff --git a/templates/settings.html b/templates/settings.html new file mode 100644 index 0000000..a24c85c --- /dev/null +++ b/templates/settings.html @@ -0,0 +1,161 @@ + + + + + Settings - CSV Master Editor + + + + + + + + + + + \ No newline at end of file diff --git a/templates/val_avg_cal.html b/templates/val_avg_cal.html index ba58bc5..e33ab34 100644 --- a/templates/val_avg_cal.html +++ b/templates/val_avg_cal.html @@ -244,6 +244,8 @@ if (!res.ok) { throw new Error(data.error || `Server returned ${res.status}`); } + // SAVE DATA GLOBALLY FOR EXCEL EXPORTER + window.currentBacktestData = data; // Update UI document.getElementById('kpiArea')?.classList.remove('d-none'); @@ -454,10 +456,49 @@ function updateSyncBadge(dateString) { } function exportToExcel() { - const table = document.getElementById("ledgerTable"); - const wb = XLSX.utils.table_to_book(table); - XLSX.writeFile(wb, "Investment_Backtest_Results.xlsx"); + // 1. Verify the global data exists + if (!window.currentBacktestData || window.currentBacktestData.length === 0) { + alert("No data available to export. Please run a backtest first."); + return; + } + const excelRows = window.currentBacktestData.map(row => { + // Calculate returns using the same logic as your renderTable + const vaAnnRet = row.va_invested > 0 + ? ((row.va_value - row.va_invested) / row.va_invested) + : 0; + + const dcaAnnRet = row.dca_invested > 0 + ? ((row.dca_value - row.dca_invested) / row.dca_invested) + : 0; + + return { + "Date": row.date, + "Price": row.price, + // DVA / Value Averaging Columns (Separated) + "DVA Investment ($)": row.va_diff, + "DVA Shares Change": row.va_shares_trans, + "DVA Total Shares": row.va_shares_total, + "DVA Target Value": row.va_target_value, + "DVA Portfolio Value": row.va_value, + // DCA Columns (Separated) + "DCA Investment ($)": row.dca_invested, + "DCA Shares Change": row.dca_shares_trans, + "DCA Total Shares": row.dca_shares_total, + "DCA Portfolio Value": row.dca_value, + // Performance + "DVA Return (%)": (vaAnnRet * 100).toFixed(2) + "%", + "DCA Return (%)": (dcaAnnRet * 100).toFixed(2) + "%" + } + }); + // 3. Convert JSON to Worksheet + const worksheet = XLSX.utils.json_to_sheet(excelRows); + + // 4. Create Workbook and Download + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, "Backtest Results"); + XLSX.writeFile(workbook, "Investment_Backtest_Results.xlsx"); } + // This checks if the Flask server is responding every 30 seconds async function checkStatus() { const indicator = document.getElementById('statusIndicator');