171 lines
5.9 KiB
Python
171 lines
5.9 KiB
Python
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) |