mirror of
https://github.com/BrentNeale1/fx-quant.git
synced 2026-08-04 22:27:44 +00:00
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:
+82
-1
@@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user