from flask import Flask, render_template,request, jsonify from datetime import datetime, timedelta from engine import DataEngine, StrategyEngine import concurrent.futures from flask_caching import Cache import csv, os, logging from concurrent.futures import ThreadPoolExecutor import pandas as pd app = Flask(__name__) cache = Cache(app, config={'CACHE_TYPE': 'SimpleCache'}) # The CSV is in the root directory (same level as app.py) CSV_PATH = os.path.join(os.path.dirname(__file__), 'instruments.csv') @app.route('/settings') def settings(): instruments = [] last_updated = "Never" if os.path.exists(CSV_PATH): # 1. Get the last modified time from the Synology filesystem mtime = os.path.getmtime(CSV_PATH) last_updated = datetime.fromtimestamp(mtime).strftime('%Y-%m-%d %H:%M:%S') # 2. Read the data df = pd.read_csv(CSV_PATH) df = df.fillna('') instruments = df.to_dict(orient='records') return render_template('settings.html', instruments=instruments, last_updated=last_updated) @app.route('/settings/save', methods=['POST']) def save_settings(): try: names = request.form.getlist('name[]') cusips = request.form.getlist('cusip[]') providers = request.form.getlist('provider[]') # Create a new DataFrame from the web form data new_data = { 'name': [s.strip() for s in names if s.strip()], 'cusip': [u.strip() for u in cusips if u.strip()], 'provider': [p.strip() for p in providers if p.strip()] } df = pd.DataFrame(new_data) # Save directly back to the root folder df.to_csv(CSV_PATH, index=False) return redirect('/settings') except Exception as e: print(f"Error saving CSV: {e}") return "Internal Server Error", 500 @cache.memoize(timeout=3600) def fetch_and_calculate(config): engine = DataEngine(config['symbol'], config['url'], config['provider']) df = engine.fetch_data() if df is not None: metrics = engine.calculate_table_metrics(df) if metrics: metrics['symbol'] = config['symbol'] return metrics return None @app.route('/') def index(): return render_template('index.html') @app.route('/api/summary') def get_summary(): engine = DataEngine() instruments = engine.load_instruments_from_csv('instruments.csv') results = [] latest_mtime = 0 # To track the actual freshest data file for item in instruments: # Update engine target engine.symbol = item['symbol'].strip().upper() engine.file_path = os.path.join(engine.cache_dir, f"{engine.symbol}.csv") # --- NEW: UPDATE LATEST_MTIME HERE --- if os.path.exists(engine.file_path): file_time = os.path.getmtime(engine.file_path) if file_time > latest_mtime: latest_mtime = file_time # ------------------------------------- metrics = engine.get_local_metrics() display_name = item.get('name') or engine.symbol if metrics: metrics.update({ 'name': display_name, 'symbol': engine.symbol }) results.append(metrics) else: # IMPORTANT: If metrics are None, we still need to send # an object so the frontend knows the row exists! results.append({ 'name': display_name, 'symbol': engine.symbol, 'last_close': 'N/A', 'error': True }) # Format the latest sync time found if latest_mtime > 0: last_mod_time = datetime.fromtimestamp(latest_mtime).strftime('%d/%b %H:%M:%S') else: last_mod_time = "Never" return jsonify({ "last_sync": last_mod_time, "data": results # Fixed the 'data_list' syntax error here }) @app.route('/api/sync', methods=['POST']) def run_sync(): # ✅ THIS RUNS THE FULL fetch_data() WITH NETWORK ACCESS engine = DataEngine() report = engine.global_sync() return jsonify(report) @app.route('/api/backtest', methods=['POST']) def api_backtest(): data = request.get_json() or {} symbol = data.get('symbol', '').strip().upper() if not symbol: return jsonify({"error": "Symbol is required"}), 400 try: # 1. Initialize Engine (Uses the absolute path logic we fixed) data_eng = DataEngine(symbol=symbol) # 2. Smart Check: Request URL if file is missing if not os.path.exists(data_eng.file_path): print(f"--- ⚠️ {symbol} not found in cache. Triggering auto-fetch... ---") # This will use the default Yahoo template if not in instruments.csv data_eng.fetch_data() # 3. Verify it worked before handing off to Strategy if not os.path.exists(data_eng.file_path): return jsonify({"error": f"Failed to retrieve data for {symbol}"}), 404 # 4. Run Strategy Logic (Restored) strat_eng = StrategyEngine(data_eng) history = strat_eng.run_simulation( start_date=data.get('startDate', '2024-01-01'), monthly_goal=float(data.get('monthly_target', 0)), initial_inv=float(data.get('initial_inv', 0)), frequency=data.get('frequency', 'Monthly'), allow_sell=data.get('allow_sell') is True, allow_fractional=data.get('allow_fractional') is True ) return jsonify(history) except Exception as e: app.logger.error(f"Backtest Error: {str(e)}") return jsonify({"error": f"Analysis failed: {str(e)}"}), 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__': app.run(debug=True, host='0.0.0.0', port=5000)