diff --git a/dashboard/app.py b/dashboard/app.py index b9d4066..b5d0965 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -1,7 +1,6 @@ """ -Forex Quant Dashboard โ Streamlit App -Monitor signals, performance, and live prices from anywhere. -Default focus: XAU/USD Gold Scalping (5m, 5-15 min holds) +XAU/USD Gold Signals โ Clean Monitor +Shows: current signal, entry, SL, TP. Nothing else. """ import sys from pathlib import Path @@ -13,400 +12,194 @@ warnings.filterwarnings("ignore") import streamlit as st import pandas as pd import numpy as np -import plotly.graph_objects as go -import plotly.express as px -from plotly.subplots import make_subplots -from datetime import datetime, timedelta, timezone +import yfinance as yf +from datetime import datetime, timezone -from data.fx_data import get_forex_data, AVAILABLE_PAIRS -from strategies.xau_scalp import add_indicators_xau, generate_signals_xau, calculate_performance_xau - -st.set_page_config( - page_title="XAU Scalp Monitor", - page_icon="๐ฅ", - layout="wide", - initial_sidebar_state="expanded", -) - -COLORS = {"bg": "#0E1117", "card": "#1A1D23", "green": "#00C853", - "red": "#FF1744", "blue": "#448AFF", "yellow": "#FFD600", "text": "#E0E0E0"} +st.set_page_config(page_title="XAU Signals", page_icon="๐ฅ", layout="centered") +# Hide streamlit branding st.markdown(""" """, unsafe_allow_html=True) -# โโโ Sidebar โโโ -st.sidebar.title("๐ฅ XAU Scalp Monitor") -st.sidebar.markdown("---") +# โโโ Import strategy โโโ +from strategies.xau_scalp import add_indicators_xau, generate_signals_xau, calculate_performance_xau -# Build pair list with XAU/USD first (avoids import edge case on Streamlit Cloud) -ALL_PAIRS = list(AVAILABLE_PAIRS) -if "XAU_USD" not in ALL_PAIRS: - ALL_PAIRS = ["XAU_USD"] + ALL_PAIRS -display_pairs = {p: p.replace("_", "/") for p in ALL_PAIRS} -pair = st.sidebar.selectbox("Instrument", ALL_PAIRS, index=ALL_PAIRS.index("XAU_USD"), - format_func=lambda x: display_pairs.get(x, x)) - -tf_options = {"1m": "1 Min", "5m": "5 Min", "15m": "15 Min", "30m": "30 Min", - "1h": "1 Hour", "4h": "4 Hour", "1d": "1 Day"} -tf = st.sidebar.selectbox("Timeframe", list(tf_options.keys()), - format_func=lambda x: tf_options[x], index=1) # default 5m - -# Volume of data -if tf == "1m": - default_days = 7 -elif tf == "5m": - default_days = 30 -elif tf in ("15m", "30m"): - default_days = 60 -else: - default_days = 90 - -days_back = st.sidebar.slider("Lookback (days)", 1, 180, default_days) - -st.sidebar.markdown("---") -st.sidebar.subheader("Scalping Params") -mom_thresh = st.sidebar.slider("Mom Threshold", 0.30, 0.80, 0.55, 0.05) -sl_mult = st.sidebar.slider("SL (ATR mult)", 0.5, 2.0, 1.2, 0.1) -tp_mult = st.sidebar.slider("TP (ATR mult)", 1.0, 3.0, 2.0, 0.1) -max_hold = st.sidebar.slider("Max Hold (bars)", 2, 30, 4) - -# Convert hold to minutes hint -hold_minutes = max_hold * (1 if tf == "1m" else 5 if tf == "5m" else 15 if tf == "15m" else 30) -st.sidebar.caption(f"โ {hold_minutes} min max hold") - -st.sidebar.markdown("---") -st.sidebar.caption(f"Data: Yahoo Finance (free)") -st.sidebar.caption(f"Updated: {datetime.now(timezone.utc):%Y-%m-%d %H:%M} UTC") -auto_refresh = st.sidebar.checkbox("Auto-refresh 60s", value=False) - -if auto_refresh: - st.sidebar.info("๐ Auto-refreshing...") - st.rerun(60) - -def _fetch_commodity(pair, tf, days): - """Direct Yahoo fetch for commodities (bypass YAHOO_PAIRS issues on Streamlit Cloud).""" - import yfinance as yf - tickers = {"XAU_USD": "GC=F", "XAG_USD": "SI=F"} - yf_tf = {"1m":"1m","5m":"5m","15m":"15m","30m":"30m","1h":"60m","4h":"60m","1d":"1d"} - raw = yf.download(tickers[pair], period=f"{max(1,days)}d", interval=yf_tf.get(tf,"5m"), progress=False) +# โโโ Fetch XAU/USD data directly โโโ +def fetch_gold(tf="5m", days=3): + raw = yf.download("GC=F", period=f"{max(1,days)}d", interval=tf, progress=False) if raw is None or raw.empty: return None if isinstance(raw.columns, pd.MultiIndex): raw.columns = raw.columns.get_level_values(0) df = raw.reset_index() df.columns = [c.lower().strip() for c in df.columns] - col_map = {"datetime":"time","dat":"time","date":"time", - "open":"open","high":"high","low":"low","close":"close","volume":"volume"} - df = df.rename(columns={k:v for k,v in col_map.items() if k in df.columns}) - df["pair"] = pair + m = {"datetime":"time","date":"time","open":"open","high":"high","low":"low","close":"close","volume":"volume"} + df = df.rename(columns={k:v for k,v in m.items() if k in df.columns}) df["time"] = pd.to_datetime(df["time"]) return df.sort_values("time").reset_index(drop=True) -# โโโ Load Data โโโ -@st.cache_data(ttl=120) -def load_data(pr, tf_str, days): - if pr in ("XAU_USD", "XAG_USD"): - df = _fetch_commodity(pr, tf_str, days) +def format_price(v): + return f"${v:,.2f}" if v == v else "โ" + +# โโโ Load & compute โโโ +df = fetch_gold("5m", 3) +if df is None or len(df) < 60: + st.error("Failed to load XAU/USD data. Try again in a minute.") + st.stop() + +df = add_indicators_xau(df) +df = generate_signals_xau(df, mom_threshold=0.55, atr_sl_mult=1.2, atr_tp_mult=2.0, max_hold_bars=4) + +latest = df.iloc[-1] +pos = latest.get("position", 0) +price = latest["close"] +rsi_val = latest.get("rsi", 50) +atr_val = latest.get("atr_pct", 0) + +# โโโ Find latest signal โโโ +signals = df[df["signal"] != 0] +last_signal = signals.iloc[-1] if not signals.empty else None + +# Also look at last 5 for history +recent_signals = signals.tail(10) if not signals.empty else pd.DataFrame() + +# โโโ UI โโโ +cols = st.columns([1, 2, 1]) +with cols[1]: + st.markdown(f"
{datetime.now(timezone.utc).strftime('%H:%M UTC')}
", unsafe_allow_html=True) + +# โโโ Current Price โโโ +st.markdown(f"Price (last 50 candles)
", unsafe_allow_html=True) -# โโโ Recent Signals โโโ -st.subheader("๐ Recent Activity") +chart_data = df[["time", "close"]].tail(50).copy() +chart_data.columns = ["t", "price"] +st.line_chart(chart_data.set_index("t"), height=150, color="#FFD600") -if data is not None: - col1, col2 = st.columns(2) +# โโโ Recent Signal History โโโ +if not recent_signals.empty: + st.markdown("---") + st.markdown("Recent Signals
", unsafe_allow_html=True) - with col1: - sig_cols = ["time", "close", "rsi", "atr_pct", "signal"] - if is_gold: - sig_cols += ["sl_price", "tp_price", "exit_reason"] - else: - sig_cols += ["position"] + hist = recent_signals[["time", "close", "signal", "exit_reason"]].copy() + hist["time"] = hist["time"].dt.strftime("%H:%M") + hist["signal"] = hist["signal"].map({1: "๐ข BUY", -1: "๐ด SELL"}) + hist = hist.rename(columns={"time": "T", "close": "Price", "signal": "Sig", "exit_reason": "Exit"}) + hist["Exit"] = hist["Exit"].replace("", "โ") + st.dataframe(hist, use_container_width=True, hide_index=True, height=200) +else: + st.markdown("---") + st.markdown("No signals generated in recent data.
", unsafe_allow_html=True) - recent = data[sig_cols].tail(30).copy() - recent["signal"] = recent["signal"].map({1: "๐ข BUY", -1: "๐ด SELL", 0: "โช"}) - - if is_gold and "exit_reason" in recent.columns: - recent["exit_reason"] = recent["exit_reason"].replace("", "-") - recent = recent.rename(columns={"time": "Time", "close": "Price", "rsi": "RSI", - "atr_pct": "ATR%", "signal": "Signal", - "sl_price": "SL", "tp_price": "TP", "exit_reason": "Exit"}) - recent["Time"] = recent["Time"].dt.strftime("%H:%M") - recent["Price"] = recent["Price"].round(2) - recent["SL"] = recent["SL"].round(2) - recent["TP"] = recent["TP"].round(2) - display_cols = ["Time", "Price", "RSI", "Signal", "SL", "TP", "Exit"] - else: - recent = recent.rename(columns={"time": "Time", "close": "Price", "rsi": "RSI", - "atr_pct": "ATR%", "signal": "Signal"}) - recent["Time"] = recent["Time"].dt.strftime("%H:%M" if tf in ("1m","5m","15m","30m") else "%m/%d %H:%M") - recent["Price"] = recent["Price"].round(5) if not is_gold else recent["Price"] - display_cols = ["Time", "Price", "RSI", "ATR%", "Signal"] - - st.markdown("**Recent candles & signals**") - st.dataframe(recent[display_cols], width="stretch", hide_index=True) - - with col2: - if is_gold and not data[data["signal"] != 0].empty: - signals = data[data["signal"] != 0].tail(20).copy() - st.markdown("**Trade exits breakdown**") - exit_data = signals[signals["exit_reason"] != ""].copy() - if not exit_data.empty: - exit_data["hold_bars"] = 0 - for i in range(len(exit_data)): - idx = exit_data.index[i] - prev_sig = signals[signals.index < idx] - if not prev_sig.empty: - entry_idx = prev_sig.index[-1] - exit_data.loc[idx, "hold_bars"] = signals.index.get_loc(idx) - signals.index.get_loc(entry_idx) - - exit_data["entry_time"] = "" - for i in range(len(exit_data)): - idx = exit_data.index[i] - prev = signals[signals.index < idx] - if not prev.empty: - exit_data.loc[idx, "entry_time"] = prev.iloc[-1]["time"] - - exit_display = exit_data[["time", "close", "exit_reason"]].tail(10).copy() - exit_display["time"] = exit_display["time"].dt.strftime("%H:%M") - exit_display = exit_display.rename(columns={"time": "Time", "close": "Price", "exit_reason": "Exit"}) - st.dataframe(exit_display, width="stretch", hide_index=True) - else: - st.info("No exits yet in recent data.") - else: - st.markdown("**Strategy metrics**") - if perf: - cols_left, cols_right = st.columns(2) - perf_items = [(k, v) for k, v in perf.items() if not isinstance(v, dict)] - mid = len(perf_items) // 2 - with cols_left: - for k, v in perf_items[:mid]: - st.metric(k.replace("_", " ").title(), v) - with cols_right: - for k, v in perf_items[mid:]: - st.metric(k.replace("_", " ").title(), v) - -# Footer +# โโโ Footer โโโ st.markdown("---") -st.caption(""" -**XAU Scalp Monitor** โ Data: Yahoo Finance | Strategy: Gold Scalping (5-15 min holds) -Deployed on Streamlit Community Cloud ยท Fully automated ยท Free forever -""") +st.markdown("Auto-refresh every 60s ยท Data: Yahoo Finance GC=F
", unsafe_allow_html=True) + +# Auto-refresh +st.rerun(60) diff --git a/dashboard/app_full.py b/dashboard/app_full.py new file mode 100644 index 0000000..b9d4066 --- /dev/null +++ b/dashboard/app_full.py @@ -0,0 +1,412 @@ +""" +Forex Quant Dashboard โ Streamlit App +Monitor signals, performance, and live prices from anywhere. +Default focus: XAU/USD Gold Scalping (5m, 5-15 min holds) +""" +import sys +from pathlib import Path +sys.path.insert(0, str(Path(__file__).parent.parent)) + +import warnings +warnings.filterwarnings("ignore") + +import streamlit as st +import pandas as pd +import numpy as np +import plotly.graph_objects as go +import plotly.express as px +from plotly.subplots import make_subplots +from datetime import datetime, timedelta, timezone + +from data.fx_data import get_forex_data, AVAILABLE_PAIRS +from strategies.xau_scalp import add_indicators_xau, generate_signals_xau, calculate_performance_xau + +st.set_page_config( + page_title="XAU Scalp Monitor", + page_icon="๐ฅ", + layout="wide", + initial_sidebar_state="expanded", +) + +COLORS = {"bg": "#0E1117", "card": "#1A1D23", "green": "#00C853", + "red": "#FF1744", "blue": "#448AFF", "yellow": "#FFD600", "text": "#E0E0E0"} + +st.markdown(""" + +""", unsafe_allow_html=True) + +# โโโ Sidebar โโโ +st.sidebar.title("๐ฅ XAU Scalp Monitor") +st.sidebar.markdown("---") + +# Build pair list with XAU/USD first (avoids import edge case on Streamlit Cloud) +ALL_PAIRS = list(AVAILABLE_PAIRS) +if "XAU_USD" not in ALL_PAIRS: + ALL_PAIRS = ["XAU_USD"] + ALL_PAIRS +display_pairs = {p: p.replace("_", "/") for p in ALL_PAIRS} +pair = st.sidebar.selectbox("Instrument", ALL_PAIRS, index=ALL_PAIRS.index("XAU_USD"), + format_func=lambda x: display_pairs.get(x, x)) + +tf_options = {"1m": "1 Min", "5m": "5 Min", "15m": "15 Min", "30m": "30 Min", + "1h": "1 Hour", "4h": "4 Hour", "1d": "1 Day"} +tf = st.sidebar.selectbox("Timeframe", list(tf_options.keys()), + format_func=lambda x: tf_options[x], index=1) # default 5m + +# Volume of data +if tf == "1m": + default_days = 7 +elif tf == "5m": + default_days = 30 +elif tf in ("15m", "30m"): + default_days = 60 +else: + default_days = 90 + +days_back = st.sidebar.slider("Lookback (days)", 1, 180, default_days) + +st.sidebar.markdown("---") +st.sidebar.subheader("Scalping Params") +mom_thresh = st.sidebar.slider("Mom Threshold", 0.30, 0.80, 0.55, 0.05) +sl_mult = st.sidebar.slider("SL (ATR mult)", 0.5, 2.0, 1.2, 0.1) +tp_mult = st.sidebar.slider("TP (ATR mult)", 1.0, 3.0, 2.0, 0.1) +max_hold = st.sidebar.slider("Max Hold (bars)", 2, 30, 4) + +# Convert hold to minutes hint +hold_minutes = max_hold * (1 if tf == "1m" else 5 if tf == "5m" else 15 if tf == "15m" else 30) +st.sidebar.caption(f"โ {hold_minutes} min max hold") + +st.sidebar.markdown("---") +st.sidebar.caption(f"Data: Yahoo Finance (free)") +st.sidebar.caption(f"Updated: {datetime.now(timezone.utc):%Y-%m-%d %H:%M} UTC") +auto_refresh = st.sidebar.checkbox("Auto-refresh 60s", value=False) + +if auto_refresh: + st.sidebar.info("๐ Auto-refreshing...") + st.rerun(60) + +def _fetch_commodity(pair, tf, days): + """Direct Yahoo fetch for commodities (bypass YAHOO_PAIRS issues on Streamlit Cloud).""" + import yfinance as yf + tickers = {"XAU_USD": "GC=F", "XAG_USD": "SI=F"} + yf_tf = {"1m":"1m","5m":"5m","15m":"15m","30m":"30m","1h":"60m","4h":"60m","1d":"1d"} + raw = yf.download(tickers[pair], period=f"{max(1,days)}d", interval=yf_tf.get(tf,"5m"), progress=False) + if raw is None or raw.empty: + return None + if isinstance(raw.columns, pd.MultiIndex): + raw.columns = raw.columns.get_level_values(0) + df = raw.reset_index() + df.columns = [c.lower().strip() for c in df.columns] + col_map = {"datetime":"time","dat":"time","date":"time", + "open":"open","high":"high","low":"low","close":"close","volume":"volume"} + df = df.rename(columns={k:v for k,v in col_map.items() if k in df.columns}) + df["pair"] = pair + df["time"] = pd.to_datetime(df["time"]) + return df.sort_values("time").reset_index(drop=True) + +# โโโ Load Data โโโ +@st.cache_data(ttl=120) +def load_data(pr, tf_str, days): + if pr in ("XAU_USD", "XAG_USD"): + df = _fetch_commodity(pr, tf_str, days) + else: + df = get_forex_data(pr, tf_str, years_back=max(0.01, days/365), cache=True) + if df is None or df.empty or len(df) < 60: + return None + if pr in ("XAU_USD", "XAG_USD"): + df = add_indicators_xau(df) + df = generate_signals_xau(df, mom_threshold=mom_thresh, atr_sl_mult=sl_mult, + atr_tp_mult=tp_mult, max_hold_bars=max_hold) + else: + from strategies.momentum import add_indicators, generate_signals + df = add_indicators(df) + df = generate_signals(df) + return df + +# โโโ Main Dashboard โโโ +st.subheader("๐ฐ Live Prices") + +with st.spinner("Loading market data..."): + key_pairs = ["XAU_USD", "EUR_USD", "GBP_USD", "USD_JPY", "XAG_USD"] + cols = st.columns(len(key_pairs)) + for i, p in enumerate(key_pairs): + try: + if p in ("XAU_USD", "XAG_USD"): + d = _fetch_commodity(p, "5m", 5) + else: + d = get_forex_data(p, "5m", 0.02, cache=True) + if d is not None and len(d) > 2: + l = d.iloc[-1]; pv = d.iloc[-2] + chg = (l["close"] - pv["close"]) / pv["close"] * 100 + arrow = "โฒ" if chg >= 0 else "โผ" + color = COLORS["green"] if chg >= 0 else COLORS["red"] + label = "XAU/USD" if p == "XAU_USD" else p.replace("_", "/") + with cols[i]: + st.markdown(f""" +