diff --git a/src/dashboard.py b/src/dashboard.py index d212d1e..438dc14 100644 --- a/src/dashboard.py +++ b/src/dashboard.py @@ -13,7 +13,8 @@ from pathlib import Path from functools import wraps import yaml -from flask import Flask, render_template, request, redirect, url_for, flash, Response +import pandas as pd +from flask import Flask, render_template, request, redirect, url_for, flash, Response, jsonify app = Flask(__name__, template_folder=str(Path(__file__).resolve().parent.parent / "templates")) app.secret_key = os.urandom(24) @@ -289,6 +290,86 @@ def killswitch(): return redirect(url_for("index")) +@app.route("/chart") +@requires_auth +def chart(): + """Candlestick chart with Classic Pivot Point support/resistance levels.""" + instrument = request.args.get("instrument", "EUR_USD") + granularity = request.args.get("granularity", "M15") + + # Validate inputs + if instrument not in VALID_INSTRUMENTS: + instrument = "EUR_USD" + if granularity not in VALID_GRANULARITIES: + granularity = "M15" + + ohlc_json = "[]" + pivot_json = "{}" + error_msg = None + + try: + from supabase import create_client + from dotenv import load_dotenv + + env_path = APP_ROOT / "config" / ".env" + if env_path.exists(): + load_dotenv(env_path) + + url = os.environ.get("SUPABASE_URL") + key = os.environ.get("SUPABASE_KEY") + + if url and key: + sb = create_client(url, key) + cfg = load_config() + table = cfg.get("supabase", {}).get("table", "fx_candles") + + resp = ( + sb.table(table) + .select("time,open,high,low,close") + .eq("instrument", instrument) + .eq("granularity", granularity) + .order("time", desc=True) + .limit(500) + .execute() + ) + + rows = resp.data if resp.data else [] + if rows: + df = pd.DataFrame(rows) + df["time"] = pd.to_datetime(df["time"]) + df = df.sort_values("time").set_index("time") + for col in ["open", "high", "low", "close"]: + df[col] = df[col].astype(float) + + # Compute pivot points + from data_engine import add_pivot_points + df = add_pivot_points(df) + + # Prepare OHLC JSON + ohlc_data = df[["open", "high", "low", "close"]].reset_index() + ohlc_data["time"] = ohlc_data["time"].dt.strftime("%Y-%m-%d %H:%M") + ohlc_json = ohlc_data.to_json(orient="records") + + # Prepare pivot levels (latest non-null values) + pivot_cols = ["pivot", "r1", "r2", "r3", "s1", "s2", "s3"] + latest = df[pivot_cols].dropna().iloc[-1] if df[pivot_cols].dropna().shape[0] > 0 else None + if latest is not None: + pivot_json = latest.to_json() + else: + error_msg = "Supabase credentials not configured." + except Exception as e: + error_msg = f"Could not load candle data: {e}" + + return render_template("chart.html", + ohlc_json=ohlc_json, + pivot_json=pivot_json, + instrument=instrument, + granularity=granularity, + instruments=VALID_INSTRUMENTS, + granularities=VALID_GRANULARITIES, + error_msg=error_msg) + + # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- diff --git a/src/data_engine.py b/src/data_engine.py index d897d03..91f7122 100644 --- a/src/data_engine.py +++ b/src/data_engine.py @@ -90,6 +90,46 @@ def add_vwap(df, period=20): return df +def add_pivot_points(df): + """ + Add Classic Pivot Point support/resistance levels. + + Resamples intraday OHLC data to daily bars, computes pivot levels from the + *previous* day's High/Low/Close, and merges them back onto the original + DataFrame so every intraday bar carries the current day's pivot levels. + + Columns added: pivot, r1, r2, r3, s1, s2, s3 + """ + # Need a DatetimeIndex to resample + df2 = df.copy() + daily = df2.resample("D").agg({"high": "max", "low": "min", "close": "last"}).dropna() + + # Compute pivots from previous day + daily["pivot"] = (daily["high"] + daily["low"] + daily["close"]) / 3 + daily["r1"] = 2 * daily["pivot"] - daily["low"] + daily["s1"] = 2 * daily["pivot"] - daily["high"] + daily["r2"] = daily["pivot"] + (daily["high"] - daily["low"]) + daily["s2"] = daily["pivot"] - (daily["high"] - daily["low"]) + daily["r3"] = daily["high"] + 2 * (daily["pivot"] - daily["low"]) + daily["s3"] = daily["low"] - 2 * (daily["high"] - daily["pivot"]) + + # Shift so today's bars use *yesterday's* pivot levels + pivot_cols = ["pivot", "r1", "r2", "r3", "s1", "s2", "s3"] + daily_pivots = daily[pivot_cols].shift(1) + + # Assign a date key to the original df for merging + df["_date"] = df.index.date + daily_pivots["_date"] = daily_pivots.index.date + + df = df.merge(daily_pivots, on="_date", how="left") + df.drop(columns=["_date"], inplace=True) + + # Restore the original DatetimeIndex + df.index = df2.index + + return df + + def build_all_features(df, config=None): """ Run a standard set of features. config is optional dict matching the diff --git a/templates/base.html b/templates/base.html index a3f5054..8f4b09c 100644 --- a/templates/base.html +++ b/templates/base.html @@ -24,6 +24,7 @@
diff --git a/templates/chart.html b/templates/chart.html new file mode 100644 index 0000000..a97d254 --- /dev/null +++ b/templates/chart.html @@ -0,0 +1,111 @@ +{% extends "base.html" %} +{% block title %}Chart - fx-quant{% endblock %} + +{% block content %} +