import os import requests import random import streamlit as st import numpy as np import pandas as pd from dateutil import parser from oandapyV20 import API from oandapyV20.endpoints.pricing import PricingInfo from cip import theoretical_forward, deviation_bps from streamlit_autorefresh import st_autorefresh # Auto-refresh every 5 seconds st_autorefresh(interval=5_000, key="refresh") # Page configuration st.set_page_config(page_title="FX Arbitrage Dashboard", layout="wide") # --- Credentials via Streamlit secrets & Endpoints --- # Create a file at ~/.streamlit/secrets.toml (or ./fx_arbitrage/.streamlit/secrets.toml) with: # # [oanda] # token = "" # account_id = "" # # [slack] # webhook = "" # # Streamlit will auto-load this file into st.secrets OANDA_TOKEN = st.secrets["oanda"]["token"] OANDA_ACCOUNT_ID = st.secrets["oanda"]["account_id"] SLACK_WEBHOOK = st.secrets.get("slack", {}).get("webhook", "") PRACTICE_SWAP_API = "https://api-fxpractice.oanda.com" # Initialize OANDA client for spot data client = API(access_token=OANDA_TOKEN, environment="practice") # --- Sidebar controls --- st.sidebar.header("Settings") override_provider = st.sidebar.selectbox( "Forward-Rate Provider", ["Manual", "Swap-Points"] ) manual_bps = st.sidebar.slider( "Manual forward offset (bps)", -10.0, 10.0, 0.0, step=0.1 ) threshold_bps = st.sidebar.number_input( "Deviation threshold (bps)", 0.5, 10.0, 1.0, step=0.5 ) stop_loss_bps = st.sidebar.number_input( "Stop-loss threshold (bps)", 0.0, 20.0, 2.0, step=0.5 ) spread_bps = st.sidebar.number_input( "Spread cost per trade (bps)", 0.0, 5.0, 0.1, step=0.1 ) tenor_days = st.sidebar.number_input( "Tenor days", 1, 90, 30 ) pairs = st.sidebar.multiselect( "Currency pairs", ["EUR_USD", "GBP_USD", "USD_JPY"], default=["EUR_USD", "GBP_USD", "USD_JPY"] ) # Initialize histories if 'history' not in st.session_state: st.session_state.history = {pair: [] for pair in pairs} if 'fwd_history' not in st.session_state: st.session_state.fwd_history = {pair: [] for pair in pairs} # --- Utility functions --- def fetch_spot(pair): pricing = client.request( PricingInfo(accountID=OANDA_ACCOUNT_ID, params={"instruments": pair}) ) bid = float(pricing["prices"][0]["bids"][0]["price"]) ask = float(pricing["prices"][0]["asks"][0]["price"]) return (bid + ask) / 2 def fetch_swap_point(pair): """Fetch the daily swap-point for the given tenor.""" url = f"{PRACTICE_SWAP_API}/v3/accounts/{OANDA_ACCOUNT_ID}/instruments/{pair}/swap_rates" headers = {"Authorization": f"Bearer {OANDA_TOKEN}"} resp = requests.get(url, headers=headers) if resp.status_code != 200: return 0.0 for r in resp.json().get("swapRates", []): if r.get('tenor').endswith('D') and int(r.get('tenor')[:-1]) == tenor_days: lr = float(r.get("longRate", 0)) sr = float(r.get("shortRate", 0)) return lr - sr return 0.0 def simulate_pnl(spot0, obs_fwd, days, sims=500): pnls = [] for _ in range(sims): path = spot0 daily = [] for _ in range(days): shock = random.uniform(-0.005, 0.005) path *= (1 + shock) daily.append(1e6 * (path - obs_fwd)) pnls.append(daily) return np.array(pnls) # --- Data collection & metrics --- data_rows = [] for pair in pairs: spot_mid = fetch_spot(pair) # Determine observed forward if override_provider == "Manual": theo_fwd = theoretical_forward(spot_mid, 0.025, 0.005, tenor_days) obs_fwd = theo_fwd * (1 + manual_bps / 10_000) else: swap_pts = fetch_swap_point(pair) obs_fwd = spot_mid + swap_pts * tenor_days / 360 theo_fwd = theoretical_forward(spot_mid, 0.025, 0.005, tenor_days) dev_bps = deviation_bps(obs_fwd, theo_fwd) # update histories hist = st.session_state.history[pair] hist.append(dev_bps) if len(hist) > 50: hist.pop(0) st.session_state.history[pair] = hist fh = st.session_state.fwd_history[pair] fh.append(obs_fwd) if len(fh) > 50: fh.pop(0) st.session_state.fwd_history[pair] = fh # signal if dev_bps > threshold_bps: sig = "Rich → Sell forward" elif dev_bps < -threshold_bps: sig = "Cheap → Buy forward" else: sig = "No arbitrage" # PnL calculation cost = spread_bps / 10_000 * 1_000_000 raw_pnl = (obs_fwd - theo_fwd) * 1_000_000 pnl = raw_pnl - cost stop_amt = stop_loss_bps / 10_000 * 1_000_000 if pnl < -stop_amt: pnl = -stop_amt data_rows.append({ "Pair": pair, "Spot Mid": f"{spot_mid:.6f}", "Observed Forward": f"{obs_fwd:.6f}", "Theoretical Fwd": f"{theo_fwd:.6f}", "Deviation (bps)": f"{dev_bps:+.2f}", "Signal": sig, "PnL ($)": f"{pnl:,.0f}" }) if SLACK_WEBHOOK and sig != "No arbitrage": requests.post(SLACK_WEBHOOK, json={"text": f"Arb alert: {pair} {dev_bps:+.2f}bps → {sig}"}) # --- Summary Metrics --- col1, col2, col3, col4 = st.columns(4) # parse PnL values from data_rows pnls = [float(r["PnL ($)"].replace("$","").replace(",","") ) for r in data_rows] total_pnl = sum(pnls) win_rate = np.mean([1 if v>0 else 0 for v in pnls]) * 100 max_dd = min(pnls) current_dev= data_rows[0]["Deviation (bps)"] col1.metric("Total PnL", f"${total_pnl:,.0f}") col2.metric("Win Rate", f"{win_rate:.1f}%") col3.metric("Max Drawdown", f"${max_dd:,.0f}") col4.metric("Current Dev", f"{current_dev} bps") # --- Display dashboard --- st.title("FX Arbitrage Dashboard — Live") st.dataframe(pd.DataFrame(data_rows), use_container_width=True) st.markdown("**Auto-refreshes every 5s**") # Deviation & Forward History st.subheader("Deviation History (bps)") for pair in pairs: st.line_chart(pd.DataFrame({pair: st.session_state.history[pair]})) st.subheader("Observed Forward History") for pair in pairs: st.line_chart(pd.DataFrame({pair: st.session_state.fwd_history[pair]})) # PnL Distribution st.subheader(f"PnL Distribution at Day {tenor_days}") for pair in pairs: spot_mid = fetch_spot(pair) if override_provider == "Manual": theo_fwd = theoretical_forward(spot_mid, 0.025, 0.005, tenor_days) obs_fwd = theo_fwd * (1 + manual_bps / 10_000) else: swap_pts = fetch_swap_point(pair) obs_fwd = spot_mid + swap_pts * tenor_days / 360 sims = simulate_pnl(spot_mid, obs_fwd, tenor_days, sims=1000) st.write(f"{pair} PnL Histogram") st.bar_chart(pd.Series(sims[:, -1], name=pair)) # --- Equity Curve --- st.subheader("Equity Curve (last 50 bars)") for pair in pairs: # compute per-bar PnL from history and forward history pnl_series = [] for obs, dev in zip(st.session_state.fwd_history[pair], st.session_state.history[pair]): spot_val = fetch_spot(pair) theo_val = theoretical_forward(spot_val, 0.025, 0.005, tenor_days) raw = (obs - theo_val) * 1_000_000 cost = spread_bps/10_000 * 1_000_000 pnl_val = raw - cost stop_amt = stop_loss_bps/10_000 * 1_000_000 pnl_series.append(max(pnl_val, -stop_amt)) equity = np.cumsum(pnl_series) st.line_chart(pd.DataFrame({pair: equity}))