Add pivot point support/resistance chart to dashboard

Add Classic Pivot Points (P, R1-R3, S1-S3) computed from previous day's
OHLC data, displayed as horizontal lines on an interactive Plotly.js
candlestick chart at /chart with instrument/granularity selectors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Brent Neale
2026-02-17 14:28:58 +10:00
parent f4734f57c7
commit d843e63e7b
4 changed files with 234 additions and 1 deletions
+82 -1
View File
@@ -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
# ---------------------------------------------------------------------------
+40
View File
@@ -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