Files
Immanuel Edunsin 7d02222da7 Add files via upload
2025-07-23 14:44:57 +01:00

221 lines
7.2 KiB
Python

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 = "<YOUR_OANDA_TOKEN>"
# account_id = "<YOUR_OANDA_ACCOUNT_ID>"
#
# [slack]
# webhook = "<YOUR_SLACK_WEBHOOK_URL>"
#
# 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}))