Add XAU/USD Gold scalping strategy + dashboard focused on gold
This commit is contained in:
+253
-290
@@ -1,16 +1,10 @@
|
||||
"""
|
||||
Forex Quant Dashboard — Streamlit App
|
||||
Monitor signals, performance, and live prices from anywhere.
|
||||
|
||||
Deploy to Streamlit Community Cloud for free:
|
||||
1. Push this folder to GitHub
|
||||
2. Go to https://streamlit.io/cloud
|
||||
3. Connect repo → Deploy
|
||||
Default focus: XAU/USD Gold Scalping (5m, 5-15 min holds)
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import warnings
|
||||
@@ -25,36 +19,23 @@ from plotly.subplots import make_subplots
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from data.fx_data import get_forex_data, AVAILABLE_PAIRS
|
||||
from strategies.momentum import add_indicators, generate_signals, calculate_performance
|
||||
from strategies.xau_scalp import add_indicators_xau, generate_signals_xau, calculate_performance_xau
|
||||
|
||||
st.set_page_config(
|
||||
page_title="Forex Quant Monitor",
|
||||
page_icon="📊",
|
||||
page_title="XAU Scalp Monitor",
|
||||
page_icon="🥇",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
# ─── Color scheme ───
|
||||
COLORS = {
|
||||
"bg": "#0E1117",
|
||||
"card": "#1A1D23",
|
||||
"green": "#00C853",
|
||||
"red": "#FF1744",
|
||||
"blue": "#448AFF",
|
||||
"yellow": "#FFD600",
|
||||
"text": "#E0E0E0",
|
||||
}
|
||||
COLORS = {"bg": "#0E1117", "card": "#1A1D23", "green": "#00C853",
|
||||
"red": "#FF1744", "blue": "#448AFF", "yellow": "#FFD600", "text": "#E0E0E0"}
|
||||
|
||||
st.markdown("""
|
||||
<style>
|
||||
.stApp { background-color: #0E1117; }
|
||||
.css-1r6slb0 { background-color: #1A1D23; }
|
||||
.metric-card {
|
||||
background: #1A1D23;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #2D3039;
|
||||
}
|
||||
.metric-card { background: #1A1D23; padding: 1rem; border-radius: 8px; border: 1px solid #2D3039; }
|
||||
.metric-value { font-size: 1.8rem; font-weight: 700; }
|
||||
.metric-label { font-size: 0.8rem; color: #9E9E9E; }
|
||||
.positive { color: #00C853; }
|
||||
@@ -63,193 +44,197 @@ st.markdown("""
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# ─── Sidebar ───
|
||||
st.sidebar.title("📊 Forex Monitor")
|
||||
st.sidebar.title("🥇 XAU Scalp Monitor")
|
||||
st.sidebar.markdown("---")
|
||||
|
||||
# Pair selector
|
||||
pair = st.sidebar.selectbox("Pair", AVAILABLE_PAIRS, index=0)
|
||||
pair = st.sidebar.selectbox("Instrument", AVAILABLE_PAIRS, index=AVAILABLE_PAIRS.index("XAU_USD"))
|
||||
|
||||
# Timeframe
|
||||
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=4)
|
||||
format_func=lambda x: tf_options[x], index=1) # default 5m
|
||||
|
||||
# Date range
|
||||
years_back = st.sidebar.slider("History", 1, 5, 2)
|
||||
# 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("Strategy Params")
|
||||
atr_min = st.sidebar.slider("Min ATR %", 0.01, 0.50, 0.05, 0.01)
|
||||
use_macd = st.sidebar.checkbox("MACD Filter", value=True)
|
||||
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("Data: Yahoo Finance (free)")
|
||||
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 every 60s", value=False)
|
||||
auto_refresh = st.sidebar.checkbox("Auto-refresh 60s", value=False)
|
||||
|
||||
if auto_refresh:
|
||||
st.sidebar.info("🔄 Refreshing...")
|
||||
st.sidebar.info("🔄 Auto-refreshing...")
|
||||
st.rerun(60)
|
||||
|
||||
# ─── Load Data ───
|
||||
@st.cache_data(ttl=300) # 5 min cache
|
||||
def load_data(pr, tf_str, yrs):
|
||||
"""Load forex data with caching."""
|
||||
df = get_forex_data(pr, tf_str, years_back=yrs, cache=True, source="yahoo")
|
||||
if df.empty or len(df) < 50:
|
||||
@st.cache_data(ttl=120)
|
||||
def load_data(pr, tf_str, days):
|
||||
df = get_forex_data(pr, tf_str, years_back=max(0.01, days/365), cache=True)
|
||||
if df.empty or len(df) < 60:
|
||||
return None
|
||||
df = add_indicators(df)
|
||||
df = generate_signals(df, atr_min_pct=atr_min, use_macd_filter=use_macd)
|
||||
# Use scalping strategy for gold, momentum for others
|
||||
if "XAU" in pr or "XAG" in pr:
|
||||
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
|
||||
|
||||
@st.cache_data(ttl=300)
|
||||
def load_all_pairs_data(tf_str, yrs):
|
||||
"""Load latest data for all pairs (for overview)."""
|
||||
results = {}
|
||||
for p in AVAILABLE_PAIRS:
|
||||
try:
|
||||
df = get_forex_data(p, tf_str, years_back=yrs, cache=True, source="yahoo")
|
||||
if not df.empty and len(df) > 20:
|
||||
results[p] = df
|
||||
except Exception:
|
||||
continue
|
||||
return results
|
||||
|
||||
# ─── Main Dashboard ───
|
||||
|
||||
# Row 1: Live Prices Overview
|
||||
st.subheader("💰 Live Prices Overview")
|
||||
st.subheader("💰 Live Prices")
|
||||
|
||||
with st.spinner("Loading market data..."):
|
||||
all_data = load_all_pairs_data("1h", 1)
|
||||
|
||||
if all_data:
|
||||
cols = st.columns(4)
|
||||
for i, (p, df) in enumerate(sorted(all_data.items())):
|
||||
latest = df.iloc[-1]
|
||||
prev = df.iloc[-2]
|
||||
change = latest["close"] - prev["close"]
|
||||
change_pct = change / prev["close"] * 100
|
||||
|
||||
with cols[i % 4]:
|
||||
color = COLORS["green"] if change >= 0 else COLORS["red"]
|
||||
arrow = "▲" if change >= 0 else "▼"
|
||||
st.markdown(f"""
|
||||
<div class="metric-card">
|
||||
<div class="metric-label">{p.replace('_', '/')}</div>
|
||||
<div class="metric-value">{latest['close']:.5f}</div>
|
||||
<div style="color:{color}">
|
||||
{arrow} {change:.5f} ({change_pct:+.3f}%)
|
||||
</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
else:
|
||||
st.warning("Could not load price data. Check internet connection.")
|
||||
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:
|
||||
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"""
|
||||
<div class="metric-card">
|
||||
<div style="font-weight:700;">🥇 {label}</div>
|
||||
<div class="metric-value">{l['close']:.2f}</div>
|
||||
<div style="color:{color}">{arrow} {chg:+.3f}%</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
except:
|
||||
pass
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# Row 2: Main Strategy Chart
|
||||
st.subheader(f"📈 {pair.replace('_', '/')} — Strategy Analysis")
|
||||
# ─── Main Chart ───
|
||||
is_gold = "XAU" in pair
|
||||
asset_label = "XAU/USD Gold" if is_gold else pair.replace("_", "/")
|
||||
st.subheader(f"📈 {asset_label} — {'Scalping' if is_gold else 'Momentum'} Strategy")
|
||||
|
||||
data = load_data(pair, tf, years_back)
|
||||
data = load_data(pair, tf, days_back)
|
||||
|
||||
if data is not None:
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
# Price + signals chart
|
||||
fig = make_subplots(
|
||||
rows=3, cols=1,
|
||||
shared_xaxes=True,
|
||||
vertical_spacing=0.05,
|
||||
row_heights=[0.55, 0.25, 0.20],
|
||||
subplot_titles=(f"{pair.replace('_', '/')} Price & Signals", "MACD", "RSI"),
|
||||
)
|
||||
fig = make_subplots(rows=3, cols=1, shared_xaxes=True,
|
||||
vertical_spacing=0.04, row_heights=[0.50, 0.25, 0.25],
|
||||
subplot_titles=(f"{asset_label} Price & Signals", "MACD (Fast)", "RSI"))
|
||||
|
||||
# Candlestick chart
|
||||
fig.add_trace(go.Candlestick(
|
||||
x=data["time"],
|
||||
open=data["open"],
|
||||
high=data["high"],
|
||||
low=data["low"],
|
||||
close=data["close"],
|
||||
name="Price",
|
||||
showlegend=False,
|
||||
), row=1, col=1)
|
||||
# Candlestick
|
||||
fig.add_trace(go.Candlestick(x=data["time"], open=data["open"], high=data["high"],
|
||||
low=data["low"], close=data["close"], name="Price",
|
||||
showlegend=False), row=1, col=1)
|
||||
|
||||
# Buy/Sell markers
|
||||
buy_signals = data[data["signal"] == 1]
|
||||
fig.add_trace(go.Scatter(
|
||||
x=buy_signals["time"],
|
||||
y=buy_signals["close"],
|
||||
mode="markers",
|
||||
marker=dict(symbol="triangle-up", size=12, color=COLORS["green"]),
|
||||
name="Enter Long",
|
||||
), row=1, col=1)
|
||||
# Buy/Sell signals
|
||||
buys = data[data["signal"] == 1]
|
||||
sells = data[data["signal"] == -1]
|
||||
if not buys.empty:
|
||||
fig.add_trace(go.Scatter(x=buys["time"], y=buys["close"],
|
||||
mode="markers", marker=dict(symbol="triangle-up", size=10, color=COLORS["green"]),
|
||||
name="🟢 Buy"), row=1, col=1)
|
||||
if not sells.empty:
|
||||
fig.add_trace(go.Scatter(x=sells["time"], y=sells["close"],
|
||||
mode="markers", marker=dict(symbol="triangle-down", size=10, color=COLORS["red"]),
|
||||
name="🔴 Sell"), row=1, col=1)
|
||||
|
||||
# MAs
|
||||
fig.add_trace(go.Scatter(
|
||||
x=data["time"], y=data["ma_fast"],
|
||||
line=dict(color=COLORS["blue"], width=1),
|
||||
name="MA-8",
|
||||
), row=1, col=1)
|
||||
fig.add_trace(go.Scatter(
|
||||
x=data["time"], y=data["ma_mid"],
|
||||
line=dict(color=COLORS["yellow"], width=1),
|
||||
name="MA-21",
|
||||
), row=1, col=1)
|
||||
# EMAs for gold, MAs for forex
|
||||
if is_gold:
|
||||
for col_name, label, color in [("ema_5", "EMA-5", "#00E5FF"), ("ema_8", "EMA-8", COLORS["blue"]),
|
||||
("ema_13", "EMA-13", COLORS["yellow"]), ("ema_21", "EMA-21", "#FF9100")]:
|
||||
if col_name in data.columns:
|
||||
fig.add_trace(go.Scatter(x=data["time"], y=data[col_name],
|
||||
line=dict(color=color, width=1), name=label), row=1, col=1)
|
||||
else:
|
||||
for col_name, label, color in [("ma_fast", "MA-8", COLORS["blue"]), ("ma_mid", "MA-21", COLORS["yellow"])]:
|
||||
if col_name in data.columns:
|
||||
fig.add_trace(go.Scatter(x=data["time"], y=data[col_name],
|
||||
line=dict(color=color, width=1), name=label), row=1, col=1)
|
||||
|
||||
# SL/TP lines
|
||||
if "sl_price" in data.columns:
|
||||
sl_data = data.dropna(subset=["sl_price"])
|
||||
if not sl_data.empty:
|
||||
fig.add_trace(go.Scatter(x=sl_data["time"], y=sl_data["sl_price"],
|
||||
line=dict(color=COLORS["red"], width=0.5, dash="dot"), name="Stop Loss",
|
||||
opacity=0.4), row=1, col=1)
|
||||
tp_data = data.dropna(subset=["tp_price"])
|
||||
if not tp_data.empty:
|
||||
fig.add_trace(go.Scatter(x=tp_data["time"], y=tp_data["tp_price"],
|
||||
line=dict(color=COLORS["green"], width=0.5, dash="dot"), name="Take Profit",
|
||||
opacity=0.4), row=1, col=1)
|
||||
|
||||
# MACD
|
||||
fig.add_trace(go.Bar(
|
||||
x=data["time"], y=data["macd_hist"],
|
||||
marker_color=np.where(data["macd_hist"] >= 0, COLORS["green"], COLORS["red"]),
|
||||
name="MACD Hist",
|
||||
), row=2, col=1)
|
||||
fig.add_trace(go.Scatter(
|
||||
x=data["time"], y=data["macd"],
|
||||
line=dict(color=COLORS["blue"], width=1.5),
|
||||
name="MACD",
|
||||
), row=2, col=1)
|
||||
fig.add_trace(go.Scatter(
|
||||
x=data["time"], y=data["macd_signal"],
|
||||
line=dict(color=COLORS["yellow"], width=1.5),
|
||||
name="Signal",
|
||||
), row=2, col=1)
|
||||
if "macd" in data.columns:
|
||||
fig.add_trace(go.Bar(x=data["time"], y=data["macd_hist"],
|
||||
marker_color=np.where(data["macd_hist"] >= 0, COLORS["green"], COLORS["red"]),
|
||||
name="MACD Hist"), row=2, col=1)
|
||||
fig.add_trace(go.Scatter(x=data["time"], y=data["macd"],
|
||||
line=dict(color=COLORS["blue"], width=1.5), name="MACD"), row=2, col=1)
|
||||
fig.add_trace(go.Scatter(x=data["time"], y=data["macd_signal"],
|
||||
line=dict(color=COLORS["yellow"], width=1.5), name="Signal"), row=2, col=1)
|
||||
|
||||
# RSI
|
||||
fig.add_trace(go.Scatter(
|
||||
x=data["time"], y=data["rsi"],
|
||||
line=dict(color=COLORS["blue"], width=1.5),
|
||||
name="RSI",
|
||||
), row=3, col=1)
|
||||
fig.add_hline(y=70, line_dash="dash", line_color=COLORS["red"], row=3, col=1)
|
||||
fig.add_hline(y=30, line_dash="dash", line_color=COLORS["green"], row=3, col=1)
|
||||
if "rsi" in data.columns:
|
||||
fig.add_trace(go.Scatter(x=data["time"], y=data["rsi"],
|
||||
line=dict(color=COLORS["blue"], width=1.5), name="RSI"), row=3, col=1)
|
||||
fig.add_hline(y=70, line_dash="dash", line_color=COLORS["red"], row=3, col=1)
|
||||
fig.add_hline(y=30, line_dash="dash", line_color=COLORS["green"], row=3, col=1)
|
||||
|
||||
fig.update_layout(
|
||||
height=650,
|
||||
template="plotly_dark",
|
||||
hovermode="x unified",
|
||||
margin=dict(l=0, r=0, t=30, b=0),
|
||||
legend=dict(orientation="h", y=1.02, x=0),
|
||||
)
|
||||
fig.update_layout(height=650, template="plotly_dark", hovermode="x unified",
|
||||
margin=dict(l=0, r=0, t=30, b=0),
|
||||
legend=dict(orientation="h", y=1.02, x=0))
|
||||
fig.update_xaxes(rangeslider_visible=False)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
with col2:
|
||||
# Strategy metrics
|
||||
perf = calculate_performance(data)
|
||||
perf = calculate_performance_xau(data) if is_gold else (
|
||||
__import__('strategies.momentum', fromlist=['calculate_performance']).calculate_performance(data))
|
||||
|
||||
st.markdown("### 📊 Performance")
|
||||
metrics = [
|
||||
("Return", f"{perf['total_return_pct']:+.2f}%", "positive" if perf['total_return_pct'] > 0 else "negative"),
|
||||
("Buy & Hold", f"{perf['buy_hold_return_pct']:+.2f}%", "positive" if perf['buy_hold_return_pct'] > 0 else "negative"),
|
||||
("Sharpe", f"{perf['sharpe_ratio']}", "positive" if perf['sharpe_ratio'] > 1 else "neutral" if perf['sharpe_ratio'] > 0 else "negative"),
|
||||
("Max Drawdown", f"{perf['max_drawdown_pct']:.2f}%", "negative"),
|
||||
("Win Rate", f"{perf['win_rate_pct']:.1f}%", "positive" if perf['win_rate_pct'] > 50 else "negative"),
|
||||
("Trades", f"{perf['num_trades']}", "neutral"),
|
||||
("Exposure", f"{perf['exposure_pct']:.1f}%", "neutral"),
|
||||
("Return", f"{perf.get('total_return_pct', 0):+.2f}%", "positive" if perf.get('total_return_pct', 0) > 0 else "negative"),
|
||||
("Buy & Hold", f"{perf.get('buy_hold_return_pct', 0):+.2f}%", "positive" if perf.get('buy_hold_return_pct', 0) > 0 else "negative"),
|
||||
("Sharpe", f"{perf.get('sharpe_ratio', 'N/A')}", "positive" if isinstance(perf.get('sharpe_ratio'), (int,float)) and perf['sharpe_ratio'] > 1 else "negative"),
|
||||
("Max DD", f"{perf.get('max_drawdown_pct', 0):.2f}%", "negative"),
|
||||
("Win Rate", f"{perf.get('win_rate_pct', 0):.1f}%", "positive" if perf.get('win_rate_pct', 50) > 50 else "negative"),
|
||||
]
|
||||
if is_gold:
|
||||
metrics += [
|
||||
("Trades", f"{perf.get('num_trades', 0)}", "neutral"),
|
||||
("Avg Hold", f"{perf.get('avg_hold_bars', 0)} bars", "neutral"),
|
||||
("Avg Trade", f"{perf.get('avg_trade_pct', 0):+.3f}%", "positive" if perf.get('avg_trade_pct', 0) > 0 else "negative"),
|
||||
("Exposure", f"{perf.get('exposure_pct', 0):.1f}%", "neutral"),
|
||||
]
|
||||
else:
|
||||
metrics += [("Trades", f"{perf.get('num_trades', 0)}", "neutral"),
|
||||
("Exposure", f"{perf.get('exposure_pct', 0):.1f}%", "neutral")]
|
||||
|
||||
for label, value, cls in metrics:
|
||||
st.markdown(f"""
|
||||
<div style="display:flex; justify-content:space-between; padding:4px 0; border-bottom:1px solid #2D3039;">
|
||||
@@ -258,162 +243,140 @@ if data is not None:
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.markdown("---")
|
||||
if is_gold and "exit_reasons" in perf and perf["exit_reasons"]:
|
||||
st.markdown("---")
|
||||
st.markdown("### 🚪 Exit Reasons")
|
||||
total_exits = sum(perf["exit_reasons"].values())
|
||||
for reason, count in sorted(perf["exit_reasons"].items(), key=lambda x: -x[1]):
|
||||
pct = count / total_exits * 100 if total_exits > 0 else 0
|
||||
emoji = {"stop_loss": "🔴", "take_profit": "🟢", "timeout": "⏰", "reversal": "🔄"}.get(reason, "⚪")
|
||||
st.markdown(f"{emoji} **{reason}**: {count} ({pct:.0f}%)")
|
||||
|
||||
# Current signal
|
||||
latest_signal = data["signal"].iloc[-1]
|
||||
latest_position = data["position"].iloc[-1]
|
||||
latest_rsi = data["rsi"].iloc[-1]
|
||||
latest_atr = data["atr_pct"].iloc[-1]
|
||||
st.markdown("---")
|
||||
latest = data.iloc[-1]
|
||||
pos = latest.get("position", 0)
|
||||
signal_icon = "🟢" if pos == 1 else "🔴" if pos == -1 else "⚪"
|
||||
signal_text = "LONG" if pos == 1 else "SHORT" if pos == -1 else "FLAT"
|
||||
rsi_val = latest.get("rsi", 50)
|
||||
atr_val = latest.get("atr_pct", 0)
|
||||
|
||||
st.markdown("### 🔔 Current Status")
|
||||
signal_icon = "🟢" if latest_position == 1 else "🔴" if latest_position == -1 else "⚪"
|
||||
signal_text = "LONG" if latest_position == 1 else "SHORT" if latest_position == -1 else "FLAT"
|
||||
|
||||
st.markdown(f"""
|
||||
<div class="metric-card" style="text-align:center;">
|
||||
<div style="font-size:2rem;">{signal_icon}</div>
|
||||
<div style="font-size:1.5rem; font-weight:700;">{signal_text}</div>
|
||||
<div style="color:#9E9E9E;">RSI: {latest_rsi:.1f} | ATR%: {latest_atr:.3f}%</div>
|
||||
<div style="color:#9E9E9E;">Price: {latest['close']:.2f} | RSI: {rsi_val:.1f} | ATR%: {atr_val:.4f}%</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
else:
|
||||
st.error(f"Could not load data for {pair}. Try a different pair or timeframe.")
|
||||
st.error(f"Could not load data for {pair}.")
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# Row 3: Equity Curve + Drawdown
|
||||
# ─── Equity Curve ───
|
||||
st.subheader("💰 Equity Curve")
|
||||
|
||||
if data is not None:
|
||||
col1, col2 = st.columns([2, 1])
|
||||
df = data.copy()
|
||||
df["returns"] = df["close"].pct_change()
|
||||
df["strategy_returns"] = df["position"].shift(1) * df["returns"]
|
||||
df["equity"] = 10000 * (1 + df["strategy_returns"]).cumprod()
|
||||
df["buy_hold"] = 10000 * (1 + df["returns"]).cumprod()
|
||||
|
||||
with col1:
|
||||
# Compute equity curve from signals
|
||||
df = data.copy()
|
||||
df["returns"] = df["close"].pct_change()
|
||||
df["strategy_returns"] = df["position"].shift(1) * df["returns"]
|
||||
df["trades"] = df["position"].diff().abs().clip(0)
|
||||
df["strategy_returns"] -= df["trades"] * 0.0001 / df["close"]
|
||||
df["equity"] = 10000 * (1 + df["strategy_returns"]).cumprod()
|
||||
df["buy_hold"] = 10000 * (1 + df["returns"]).cumprod()
|
||||
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.05, row_heights=[0.7, 0.3])
|
||||
fig.add_trace(go.Scatter(x=df["time"], y=df["equity"], line=dict(color=COLORS["green"], width=2), name="Strategy"), row=1, col=1)
|
||||
fig.add_trace(go.Scatter(x=df["time"], y=df["buy_hold"], line=dict(color="#9E9E9E", width=1, dash="dash"), name="Buy & Hold"), row=1, col=1)
|
||||
|
||||
fig = make_subplots(
|
||||
rows=2, cols=1,
|
||||
shared_xaxes=True,
|
||||
vertical_spacing=0.05,
|
||||
row_heights=[0.7, 0.3],
|
||||
)
|
||||
peak = df["equity"].expanding().max()
|
||||
dd = (df["equity"] - peak) / peak * 100
|
||||
fig.add_trace(go.Scatter(x=df["time"], y=dd, fill="tozeroy", line=dict(color=COLORS["red"], width=1), name="Drawdown"), row=2, col=1)
|
||||
|
||||
fig.add_trace(go.Scatter(
|
||||
x=df["time"], y=df["equity"],
|
||||
line=dict(color=COLORS["green"], width=2),
|
||||
name="Strategy",
|
||||
), row=1, col=1)
|
||||
|
||||
fig.add_trace(go.Scatter(
|
||||
x=df["time"], y=df["buy_hold"],
|
||||
line=dict(color="#9E9E9E", width=1, dash="dash"),
|
||||
name="Buy & Hold",
|
||||
), row=1, col=1)
|
||||
|
||||
# Drawdown
|
||||
peak = df["equity"].expanding().max()
|
||||
dd = (df["equity"] - peak) / peak * 100
|
||||
fig.add_trace(go.Scatter(
|
||||
x=df["time"], y=dd,
|
||||
fill="tozeroy",
|
||||
line=dict(color=COLORS["red"], width=1),
|
||||
name="Drawdown %",
|
||||
), row=2, col=1)
|
||||
|
||||
fig.update_layout(
|
||||
height=400,
|
||||
template="plotly_dark",
|
||||
hovermode="x unified",
|
||||
margin=dict(l=0, r=0, t=10, b=0),
|
||||
legend=dict(orientation="h", y=1.02, x=0),
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
with col2:
|
||||
st.markdown("### 📋 Recent Signals")
|
||||
sig_cols = ["time", "close", "rsi", "atr_pct", "position", "signal"]
|
||||
recent = data[sig_cols].tail(20).copy()
|
||||
recent["position"] = recent["position"].map({1: "LONG", 0: "FLAT", -1: "SHORT"})
|
||||
recent["signal"] = recent["signal"].map({1: "🟢 BUY", 0: "⚪", -1: "🔴 SELL"})
|
||||
recent = recent.rename(columns={
|
||||
"time": "Time", "close": "Price", "rsi": "RSI",
|
||||
"atr_pct": "ATR%", "position": "Pos", "signal": "Signal"
|
||||
})
|
||||
recent["Time"] = recent["Time"].dt.strftime("%m/%d %H:%M")
|
||||
st.dataframe(recent, use_container_width=True, hide_index=True)
|
||||
fig.update_layout(height=350, template="plotly_dark", hovermode="x unified",
|
||||
margin=dict(l=0, r=0, t=10, b=0), legend=dict(orientation="h", y=1.02, x=0))
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# Row 4: Multi-Pair Heatmap
|
||||
st.subheader("🌍 Multi-Pair Comparison")
|
||||
# ─── Recent Signals ───
|
||||
st.subheader("📋 Recent Activity")
|
||||
|
||||
with st.spinner("Loading all pairs..."):
|
||||
comparison_data = {}
|
||||
for p in AVAILABLE_PAIRS:
|
||||
try:
|
||||
d = load_data(p, "1d", 2)
|
||||
if d is not None:
|
||||
perf = calculate_performance(d)
|
||||
comparison_data[p] = perf
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
if comparison_data:
|
||||
comp_df = pd.DataFrame(comparison_data).T
|
||||
comp_df.index.name = "Pair"
|
||||
|
||||
col1, col2 = st.columns([1, 2])
|
||||
if data is not None:
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
metrics_select = st.selectbox("Metric", ["total_return_pct", "sharpe_ratio", "max_drawdown_pct", "win_rate_pct"])
|
||||
metric_labels = {
|
||||
"total_return_pct": "Total Return %",
|
||||
"sharpe_ratio": "Sharpe Ratio",
|
||||
"max_drawdown_pct": "Max Drawdown %",
|
||||
"win_rate_pct": "Win Rate %",
|
||||
}
|
||||
sig_cols = ["time", "close", "rsi", "atr_pct", "signal"]
|
||||
if is_gold:
|
||||
sig_cols += ["sl_price", "tp_price", "exit_reason"]
|
||||
else:
|
||||
sig_cols += ["position"]
|
||||
|
||||
fig = px.bar(
|
||||
comp_df.sort_values(metrics_select, ascending=False),
|
||||
y=metrics_select,
|
||||
color=metrics_select,
|
||||
color_continuous_scale=["red", "yellow", "green"],
|
||||
title=f"{metric_labels[metrics_select]} by Pair",
|
||||
text_auto=".1f",
|
||||
)
|
||||
fig.update_layout(
|
||||
template="plotly_dark",
|
||||
height=400,
|
||||
margin=dict(l=0, r=0, t=30, b=0),
|
||||
showlegend=False,
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=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], use_container_width=True, hide_index=True)
|
||||
|
||||
with col2:
|
||||
st.markdown("### 📊 Comparison Table")
|
||||
display = comp_df[[
|
||||
"total_return_pct", "buy_hold_return_pct",
|
||||
"sharpe_ratio", "max_drawdown_pct",
|
||||
"win_rate_pct", "num_trades", "exposure_pct"
|
||||
]].round(2)
|
||||
display.columns = [
|
||||
"Return%", "BH Return%", "Sharpe", "Max DD%",
|
||||
"Win Rate%", "Trades", "Exposure%"
|
||||
]
|
||||
st.dataframe(display, use_container_width=True)
|
||||
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)
|
||||
|
||||
st.markdown("---")
|
||||
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, use_container_width=True, 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
|
||||
st.markdown("---")
|
||||
st.caption("""
|
||||
**Forex Quant Monitor** — Data from Yahoo Finance | Strategy: Momentum + Volatility Filter
|
||||
Built with Streamlit | Deploy free on streamlit.io/cloud
|
||||
**XAU Scalp Monitor** — Data: Yahoo Finance | Strategy: Gold Scalping (5-15 min holds)
|
||||
Deployed on Streamlit Community Cloud · Fully automated · Free forever
|
||||
""")
|
||||
|
||||
@@ -23,6 +23,7 @@ from config import RAW_DIR, OANDA_KEY, OANDA_ACCOUNT, OANDA_ENV
|
||||
|
||||
# Yahoo ticker format for forex: EURUSD=X
|
||||
YAHOO_PAIRS = {
|
||||
# Forex pairs
|
||||
"EUR_USD": "EURUSD=X",
|
||||
"GBP_USD": "GBPUSD=X",
|
||||
"USD_JPY": "USDJPY=X",
|
||||
@@ -37,6 +38,10 @@ YAHOO_PAIRS = {
|
||||
"AUD_JPY": "AUDJPY=X",
|
||||
"CHF_JPY": "CHFJPY=X",
|
||||
"EUR_CHF": "EURCHF=X",
|
||||
# Commodities
|
||||
"XAU_USD": "GC=F", # Gold Futures (~= spot XAU/USD)
|
||||
"XAG_USD": "SI=F", # Silver Futures
|
||||
"BTC_USD": "BTC-USD", # Bitcoin
|
||||
}
|
||||
|
||||
TIMEFRAMES_YAHOO = {
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
"""
|
||||
Gold Scalping Strategy - XAU/USD
|
||||
|
||||
Optimized for 1m-5m charts with 5-15 minute hold times.
|
||||
Focuses on micro-momentum and mean reversion in gold's volatile moves.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
try:
|
||||
import talib
|
||||
HAS_TALIB = True
|
||||
except ImportError:
|
||||
HAS_TALIB = False
|
||||
|
||||
|
||||
def _ema(values, period):
|
||||
if HAS_TALIB: return talib.EMA(values.astype(float), timeperiod=period)
|
||||
return pd.Series(values).ewm(span=period, adjust=False).mean().values
|
||||
|
||||
|
||||
def _sma(values, period):
|
||||
if HAS_TALIB: return talib.SMA(values.astype(float), timeperiod=period)
|
||||
return pd.Series(values).rolling(period).mean().values
|
||||
|
||||
|
||||
def _rsi(values, period=7):
|
||||
if HAS_TALIB: return talib.RSI(values.astype(float), timeperiod=period)
|
||||
series = pd.Series(values)
|
||||
delta = series.diff()
|
||||
gain = delta.where(delta > 0, 0).rolling(period).mean()
|
||||
loss = (-delta.where(delta < 0, 0)).rolling(period).mean()
|
||||
rs = gain / loss.replace(0, np.nan)
|
||||
return (100 - (100 / (1 + rs))).values
|
||||
|
||||
|
||||
def _macd(values, fast=6, slow=13, signal=5):
|
||||
if HAS_TALIB: return talib.MACD(values.astype(float), fast, slow, signal)
|
||||
ema_f, ema_s = _ema(values, fast), _ema(values, slow)
|
||||
macd = ema_f - ema_s
|
||||
sig = _ema(macd, signal)
|
||||
return macd, sig, macd - sig
|
||||
|
||||
|
||||
def _atr(high, low, close, period=10):
|
||||
if HAS_TALIB: return talib.ATR(high.astype(float), low.astype(float), close.astype(float), timeperiod=period)
|
||||
h, l, c = pd.Series(high), pd.Series(low), pd.Series(close)
|
||||
tr = pd.concat([h - l, (h - c.shift()).abs(), (l - c.shift()).abs()], axis=1).max(axis=1)
|
||||
return tr.rolling(period).mean().values
|
||||
|
||||
|
||||
def _stoch(high, low, close, k=5, d=3):
|
||||
if HAS_TALIB: return talib.STOCH(high.astype(float), low.astype(float), close.astype(float),
|
||||
fastk_period=k, slowk_period=d, slowd_period=d)
|
||||
low_k = pd.Series(low).rolling(k).min()
|
||||
high_k = pd.Series(high).rolling(k).max()
|
||||
k_vals = 100 * (pd.Series(close) - low_k) / (high_k - low_k).replace(0, np.nan)
|
||||
return k_vals.values, k_vals.rolling(d).mean().values
|
||||
|
||||
|
||||
def add_indicators_xau(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Add scalping indicators for XAU/USD."""
|
||||
df = df.copy()
|
||||
close = df["close"].values.astype(float)
|
||||
high = df["high"].values.astype(float)
|
||||
low = df["low"].values.astype(float)
|
||||
volume = df["volume"].values.astype(float)
|
||||
|
||||
# Fast EMAs
|
||||
df["ema_5"] = _ema(close, 5)
|
||||
df["ema_8"] = _ema(close, 8)
|
||||
df["ema_13"] = _ema(close, 13)
|
||||
df["ema_21"] = _ema(close, 21)
|
||||
|
||||
# MACD (faster)
|
||||
macd, macd_sig, macd_hist = _macd(close, 6, 13, 5)
|
||||
df["macd"] = macd
|
||||
df["macd_signal"] = macd_sig
|
||||
df["macd_hist"] = macd_hist
|
||||
|
||||
# RSI (faster)
|
||||
df["rsi"] = _rsi(close, 7)
|
||||
|
||||
# Stochastic
|
||||
df["stoch_k"], df["stoch_d"] = _stoch(high, low, close, 5, 3)
|
||||
|
||||
# ATR
|
||||
df["atr"] = _atr(high, low, close, 10)
|
||||
df["atr_pct"] = df["atr"] / close * 100
|
||||
|
||||
# Price delta rankings
|
||||
df["price_change"] = df["close"].pct_change()
|
||||
df["price_rank_5"] = df["price_change"].rolling(5).apply(
|
||||
lambda x: (x.iloc[-1] > 0 and x.iloc[-1] >= x.quantile(0.8)) or
|
||||
(x.iloc[-1] < 0 and x.iloc[-1] <= x.quantile(0.2)),
|
||||
raw=False
|
||||
)
|
||||
|
||||
# Volume confirmation
|
||||
df["volume_ma"] = _sma(volume, 20)
|
||||
df["volume_ratio"] = volume / df["volume_ma"].replace(0, np.nan)
|
||||
|
||||
# Momentum score (composite)
|
||||
df["mom_score"] = 0.0
|
||||
df["mom_score"] += (df["ema_5"] > df["ema_8"]).astype(float) * 0.2
|
||||
df["mom_score"] += (df["ema_8"] > df["ema_13"]).astype(float) * 0.15
|
||||
df["mom_score"] += (df["ema_13"] > df["ema_21"]).astype(float) * 0.15
|
||||
df["mom_score"] += ((df["macd_hist"] > 0) & (df["macd_hist"] > df["macd_hist"].shift(1))).astype(float) * 0.2
|
||||
df["mom_score"] += (df["rsi"] > 50).astype(float) * 0.15
|
||||
df["mom_score"] += (df["close"] > df["ema_8"]).astype(float) * 0.15
|
||||
|
||||
df["mom_score_rev"] = 0.0
|
||||
df["mom_score_rev"] += (df["ema_5"] < df["ema_8"]).astype(float) * 0.2
|
||||
df["mom_score_rev"] += (df["ema_8"] < df["ema_13"]).astype(float) * 0.15
|
||||
df["mom_score_rev"] += (df["ema_13"] < df["ema_21"]).astype(float) * 0.15
|
||||
df["mom_score_rev"] += ((df["macd_hist"] < 0) & (df["macd_hist"] < df["macd_hist"].shift(1))).astype(float) * 0.2
|
||||
df["mom_score_rev"] += (df["rsi"] < 50).astype(float) * 0.15
|
||||
df["mom_score_rev"] += (df["close"] < df["ema_8"]).astype(float) * 0.15
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def generate_signals_xau(
|
||||
df: pd.DataFrame,
|
||||
mom_threshold: float = 0.55, # Min momentum score to enter
|
||||
atr_min_pct: float = 0.02, # Min volatility
|
||||
atr_max_pct: float = 0.40, # Max volatility (avoid crazy moves)
|
||||
rsi_low: float = 35,
|
||||
rsi_high: float = 65,
|
||||
min_vol_ratio: float = 1.0,
|
||||
atr_sl_mult: float = 0.8, # Stop loss as ATR multiple
|
||||
atr_tp_mult: float = 1.2, # Take profit as ATR multiple
|
||||
max_hold_bars: int = 15, # Max hold in bars
|
||||
trail_start: int = 3, # Start trailing after N bars
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Generate scalping signals with proper SL/TP simulation.
|
||||
"""
|
||||
df = df.copy()
|
||||
df["signal"] = 0
|
||||
df["position"] = 0
|
||||
df["entry_price"] = np.nan
|
||||
df["sl_price"] = np.nan
|
||||
df["tp_price"] = np.nan
|
||||
df["exit_reason"] = ""
|
||||
|
||||
if len(df) < 60:
|
||||
return df
|
||||
|
||||
atr = df["atr"].values
|
||||
close = df["close"].values
|
||||
rsi = df["rsi"].values
|
||||
|
||||
# Valid volatility zone
|
||||
valid_vol = (df["atr_pct"] >= atr_min_pct) & (df["atr_pct"] <= atr_max_pct)
|
||||
|
||||
# Potential entries (raw signals without position management)
|
||||
raw_long = (
|
||||
(df["mom_score"] >= mom_threshold) &
|
||||
valid_vol &
|
||||
(rsi < rsi_high) &
|
||||
(df["volume_ratio"] >= min_vol_ratio)
|
||||
)
|
||||
|
||||
raw_short = (
|
||||
(df["mom_score_rev"] >= mom_threshold) &
|
||||
valid_vol &
|
||||
(rsi > (100 - rsi_high)) &
|
||||
(df["volume_ratio"] >= min_vol_ratio)
|
||||
)
|
||||
|
||||
# Simulate trading with proper SL/TP
|
||||
pos = 0
|
||||
entry_bar = 0
|
||||
entry_px = 0.0
|
||||
sl_px = 0.0
|
||||
tp_px = 0.0
|
||||
direction = 0 # 1=long, -1=short
|
||||
|
||||
for i in range(len(df)):
|
||||
if pos == 0:
|
||||
# ─── LOOK FOR ENTRY ───
|
||||
if raw_long.iloc[i]:
|
||||
pos = 1
|
||||
direction = 1
|
||||
entry_bar = i
|
||||
entry_px = close[i]
|
||||
sl_px = entry_px - atr[i] * atr_sl_mult
|
||||
tp_px = entry_px + atr[i] * atr_tp_mult
|
||||
df.loc[df.index[i], "signal"] = 1
|
||||
df.loc[df.index[i], "entry_price"] = entry_px
|
||||
df.loc[df.index[i], "sl_price"] = sl_px
|
||||
df.loc[df.index[i], "tp_price"] = tp_px
|
||||
elif raw_short.iloc[i]:
|
||||
pos = -1
|
||||
direction = -1
|
||||
entry_bar = i
|
||||
entry_px = close[i]
|
||||
sl_px = entry_px + atr[i] * atr_sl_mult
|
||||
tp_px = entry_px - atr[i] * atr_tp_mult
|
||||
df.loc[df.index[i], "signal"] = -1
|
||||
df.loc[df.index[i], "entry_price"] = entry_px
|
||||
df.loc[df.index[i], "sl_price"] = sl_px
|
||||
df.loc[df.index[i], "tp_price"] = tp_px
|
||||
|
||||
else:
|
||||
# ─── MANAGE POSITION ───
|
||||
bars_held = i - entry_bar
|
||||
|
||||
# Trail stop
|
||||
if bars_held >= trail_start:
|
||||
if direction == 1:
|
||||
trail_px = close[i] - atr[i] * atr_sl_mult * 0.5
|
||||
if trail_px > sl_px:
|
||||
sl_px = trail_px
|
||||
else:
|
||||
trail_px = close[i] + atr[i] * atr_sl_mult * 0.5
|
||||
if trail_px < sl_px:
|
||||
sl_px = trail_px
|
||||
|
||||
# Check exits
|
||||
exit_now = False
|
||||
reason = ""
|
||||
|
||||
if direction == 1:
|
||||
if close[i] <= sl_px:
|
||||
exit_now, reason = True, "stop_loss"
|
||||
elif close[i] >= tp_px:
|
||||
exit_now, reason = True, "take_profit"
|
||||
else:
|
||||
if close[i] >= sl_px:
|
||||
exit_now, reason = True, "stop_loss"
|
||||
elif close[i] <= tp_px:
|
||||
exit_now, reason = True, "take_profit"
|
||||
|
||||
if not exit_now and bars_held >= max_hold_bars:
|
||||
exit_now, reason = True, "timeout"
|
||||
|
||||
# Reversal
|
||||
if not exit_now:
|
||||
if direction == 1 and raw_short.iloc[i]:
|
||||
exit_now, reason = True, "reversal"
|
||||
elif direction == -1 and raw_long.iloc[i]:
|
||||
exit_now, reason = True, "reversal"
|
||||
|
||||
if exit_now:
|
||||
df.loc[df.index[i], "position"] = 0
|
||||
df.loc[df.index[i], "exit_reason"] = reason
|
||||
pos = 0
|
||||
direction = 0
|
||||
else:
|
||||
df.loc[df.index[i], "position"] = direction
|
||||
df.loc[df.index[i], "sl_price"] = sl_px
|
||||
df.loc[df.index[i], "tp_price"] = tp_px
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def calculate_performance_xau(df: pd.DataFrame) -> dict:
|
||||
"""Calculate scalping strategy metrics."""
|
||||
df = df.copy()
|
||||
|
||||
pos_series = df["position"]
|
||||
close = df["close"].values
|
||||
|
||||
# Simple return calculation per bar
|
||||
df["bar_return"] = df["close"].pct_change()
|
||||
|
||||
# Entry returns
|
||||
entries = df[df["signal"] != 0].index
|
||||
exits = df[df["exit_reason"] != ""].index
|
||||
|
||||
trade_returns = {}
|
||||
for e_idx, entry_idx in enumerate(entries):
|
||||
# Find the matching exit
|
||||
valid_exits = [x for x in exits if x > entry_idx]
|
||||
if valid_exits:
|
||||
exit_idx = valid_exits[0]
|
||||
ret = close[df.index.get_loc(exit_idx)] / close[df.index.get_loc(entry_idx)] - 1
|
||||
trade_returns[entry_idx] = {"exit": exit_idx, "return": ret, "hold": df.index.get_loc(exit_idx) - df.index.get_loc(entry_idx)}
|
||||
|
||||
trade_returns_list = [v["return"] for v in trade_returns.values()]
|
||||
hold_times = [v["hold"] for v in trade_returns.values()]
|
||||
num_trades = len(trade_returns_list)
|
||||
|
||||
# Overall returns
|
||||
df["strategy_returns"] = pos_series.shift(1) * df["bar_return"]
|
||||
total_return = (1 + df["strategy_returns"]).prod() - 1
|
||||
buy_hold_return = (1 + df["bar_return"]).prod() - 1
|
||||
|
||||
# Sharpe
|
||||
sharpe = np.nan
|
||||
if df["strategy_returns"].std() > 0:
|
||||
bars_per_year = 252 * 24 * 60
|
||||
sharpe = round(df["strategy_returns"].mean() / df["strategy_returns"].std() * np.sqrt(bars_per_year), 2)
|
||||
|
||||
# Max drawdown
|
||||
equity = (1 + df["strategy_returns"]).cumprod()
|
||||
peak = equity.expanding().max()
|
||||
dd = (equity - peak) / peak
|
||||
max_dd = dd.min()
|
||||
|
||||
win_rate = sum(1 for r in trade_returns_list if r > 0) / num_trades * 100 if num_trades > 0 else 0
|
||||
avg_hold_bars = np.mean(hold_times) if hold_times else 0
|
||||
avg_trade_return = np.mean(trade_returns_list) * 100 if trade_returns_list else 0
|
||||
best_trade = max(trade_returns_list) * 100 if trade_returns_list else 0
|
||||
worst_trade = min(trade_returns_list) * 100 if trade_returns_list else 0
|
||||
|
||||
exit_counts = df["exit_reason"].value_counts().to_dict()
|
||||
|
||||
return {
|
||||
"total_return_pct": round(total_return * 100, 2),
|
||||
"buy_hold_return_pct": round(buy_hold_return * 100, 2),
|
||||
"sharpe_ratio": sharpe,
|
||||
"max_drawdown_pct": round(max_dd * 100, 2),
|
||||
"win_rate_pct": round(win_rate, 1),
|
||||
"num_trades": num_trades,
|
||||
"avg_hold_bars": round(avg_hold_bars, 1),
|
||||
"avg_trade_pct": round(avg_trade_return, 3),
|
||||
"best_trade_pct": round(best_trade, 3),
|
||||
"worst_trade_pct": round(worst_trade, 3),
|
||||
"exposure_pct": round((pos_series != 0).mean() * 100, 1),
|
||||
"exit_reasons": {k: v for k, v in exit_counts.items() if k},
|
||||
}
|
||||
|
||||
|
||||
# ──────────────────────────────────────────────
|
||||
# Quick test
|
||||
# ──────────────────────────────────────────────
|
||||
|
||||
if __name__ == "__main__":
|
||||
from data.fx_data import get_forex_data
|
||||
|
||||
print("Loading XAU/USD 1m data...")
|
||||
df = get_forex_data("XAU_USD", "1m", years_back=0.02, cache=True)
|
||||
if df.empty or len(df) < 100:
|
||||
print("Trying 5m...")
|
||||
df = get_forex_data("XAU_USD", "5m", years_back=0.1, cache=True)
|
||||
|
||||
if df.empty:
|
||||
print("No data.")
|
||||
exit(1)
|
||||
|
||||
print(f"Loaded {len(df):,} candles ({df['time'].min():%m/%d %H:%M} → {df['time'].max():%m/%d %H:%M})")
|
||||
|
||||
df = add_indicators_xau(df)
|
||||
df = generate_signals_xau(df)
|
||||
|
||||
perf = calculate_performance_xau(df)
|
||||
print("\n📊 XAU/USD Scalping Performance:")
|
||||
for k, v in perf.items():
|
||||
if isinstance(v, dict):
|
||||
print(f" {k}:", {kk: vv for kk, vv in v.items()})
|
||||
else:
|
||||
print(f" {k}: {v}")
|
||||
|
||||
# Show last signals
|
||||
signals = df[df["signal"] != 0].tail(10)
|
||||
if not signals.empty:
|
||||
print(f"\n🔔 Last {len(signals)} signals:")
|
||||
cols = ["time", "close", "rsi", "atr_pct", "signal", "sl_price", "tp_price", "exit_reason"]
|
||||
print(signals[[c for c in cols if c in signals.columns]].to_string(index=False))
|
||||
Reference in New Issue
Block a user