Add files via upload
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
import pandas as pd
|
||||
|
||||
# 1) Load the CSV
|
||||
df = pd.read_csv("optimization_results.csv")
|
||||
|
||||
# 2) Sort by total_pnl descending
|
||||
top = df.sort_values("total_pnl", ascending=False).head(10)
|
||||
|
||||
# 3) Print to console
|
||||
print("Top 10 parameter sets by Total PnL:\n", top.to_string(index=False))
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
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}))
|
||||
|
||||
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
import os
|
||||
import requests
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
from dateutil import parser
|
||||
from oandapyV20 import API
|
||||
from oandapyV20.endpoints.instruments import InstrumentsCandles
|
||||
from cip import theoretical_forward, deviation_bps
|
||||
|
||||
# === Configuration ===
|
||||
OANDA_TOKEN = os.getenv("OANDA_TOKEN")
|
||||
OANDA_ACCOUNT_ID = os.getenv("OANDA_ACCOUNT_ID")
|
||||
BASE_URL = "https://api-fxtrade.oanda.com" # production for swap rates
|
||||
|
||||
if not OANDA_TOKEN or not OANDA_ACCOUNT_ID:
|
||||
raise RuntimeError("Please set OANDA_TOKEN and OANDA_ACCOUNT_ID environment variables")
|
||||
|
||||
# Initialize OANDA API client (practice for spot data)
|
||||
api = API(access_token=OANDA_TOKEN, environment="practice")
|
||||
|
||||
# === Data Fetching ===
|
||||
|
||||
def fetch_spot_history(pair: str, days: int = 365) -> pd.Series:
|
||||
"""
|
||||
Fetch daily historical spot mid-prices for the FX pair.
|
||||
Returns a pandas Series indexed by date.
|
||||
"""
|
||||
req = InstrumentsCandles(
|
||||
instrument=pair,
|
||||
params={"granularity": "D", "count": days, "price": "M"}
|
||||
)
|
||||
data = api.request(req)["candles"]
|
||||
records = []
|
||||
for c in data:
|
||||
dt = parser.isoparse(c["time"]) # full timestamp
|
||||
o = float(c["mid"]["o"])
|
||||
c_ = float(c["mid"]["c"])
|
||||
records.append((dt.date(), (o + c_) / 2))
|
||||
series = pd.Series({d: s for d, s in records}).sort_index()
|
||||
return series
|
||||
|
||||
|
||||
def fetch_swap_history(pair: str, days: int = 365) -> pd.Series:
|
||||
"""
|
||||
Fetch daily historical swap-rates (forward-points) for the FX pair.
|
||||
Returns a pandas Series of daily forward-points (decimal) indexed by date.
|
||||
Falls back to zeros if endpoint unavailable (e.g., practice account).
|
||||
"""
|
||||
url = f"{BASE_URL}/v3/accounts/{OANDA_ACCOUNT_ID}/instruments/{pair}/swap_rates"
|
||||
headers = {"Authorization": f"Bearer {OANDA_TOKEN}"}
|
||||
params = {"count": days, "granularity": "D"}
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json().get("swapRates", [])
|
||||
records = []
|
||||
for r in data:
|
||||
dt = parser.isoparse(r["time"]).date()
|
||||
long_rate = float(r.get("longRate", 0))
|
||||
short_rate = float(r.get("shortRate", 0))
|
||||
records.append((dt, long_rate - short_rate))
|
||||
series = pd.Series({d: p for d, p in records}).sort_index()
|
||||
except Exception:
|
||||
# Practice environment may not support swap_rates; fallback to zeros
|
||||
print("Warning: swap_rates endpoint unavailable, falling back to zeros.")
|
||||
# Build zero series over requested date range
|
||||
df_spot = fetch_spot_history(pair, days=days)
|
||||
series = pd.Series(0.0, index=df_spot.index)
|
||||
return series
|
||||
|
||||
# === Backtest ===
|
||||
|
||||
def backtest(
|
||||
pair: str,
|
||||
tenor_days: int = 30,
|
||||
r_dom: float = 0.025,
|
||||
r_for: float = 0.005,
|
||||
notional: float = 1_000_000,
|
||||
spread_bps: float = 0.5,
|
||||
stop_loss_bps: float = 5.0,
|
||||
history_days: int = 365
|
||||
) -> None:
|
||||
"""
|
||||
Back-test FX CIP arbitrage using real swap-points.
|
||||
"""
|
||||
# Fetch data
|
||||
spot = fetch_spot_history(pair, days=history_days)
|
||||
swap_pts = fetch_swap_history(pair, days=history_days)
|
||||
|
||||
# Build DataFrame
|
||||
df = pd.DataFrame({"spot": spot})
|
||||
# theoretical forward
|
||||
df["theo_fwd"] = df["spot"].apply(lambda s: theoretical_forward(s, r_dom, r_for, tenor_days))
|
||||
# observed forward = spot + tenor * swap_pts/360
|
||||
df["swap_pts"] = swap_pts.reindex(df.index).fillna(method="ffill")
|
||||
df["obs_fwd"] = df["spot"] + df["swap_pts"] * tenor_days / 360
|
||||
|
||||
# deviation and signal
|
||||
df["dev_bps"] = (df["obs_fwd"] - df["theo_fwd"]) / df["theo_fwd"] * 10_000
|
||||
df["signal"] = 0
|
||||
df.loc[df["dev_bps"] > 0, "signal"] = -1 # sell forward if rich
|
||||
df.loc[df["dev_bps"] < 0, "signal"] = +1 # buy forward if cheap
|
||||
|
||||
# PnL with spread cost & stop-loss
|
||||
cost = spread_bps / 10_000 * notional
|
||||
df["exit_spot"] = df["spot"].shift(-tenor_days)
|
||||
df["raw_pnl"] = df["signal"] * (df["exit_spot"] - df["obs_fwd"]) * notional
|
||||
df["pnl"] = df["raw_pnl"] - df["signal"].abs() * cost
|
||||
stop_amt = stop_loss_bps / 10_000 * notional
|
||||
df.loc[df["pnl"] < -stop_amt, "pnl"] = -stop_amt
|
||||
|
||||
# drop incomplete
|
||||
trades = df.dropna(subset=["pnl"])
|
||||
|
||||
# metrics
|
||||
total_pnl = trades["pnl"].sum()
|
||||
num_trades = (trades["signal"] != 0).sum()
|
||||
win_rate = trades["pnl"].gt(0).mean() * 100 if num_trades else 0
|
||||
avg_pnl = trades["pnl"].mean() if num_trades else 0
|
||||
equity = trades["pnl"].cumsum()
|
||||
max_dd = (equity.cummax() - equity).max() if not equity.empty else 0
|
||||
|
||||
# output
|
||||
print(f"=== Backtest Results for {pair} ({tenor_days}d tenor) ===")
|
||||
print(f"Total PnL : ${total_pnl:,.0f}")
|
||||
print(f"Number of trades : {num_trades}")
|
||||
print(f"Win rate : {win_rate:.1f}%")
|
||||
print(f"Average PnL/trade : ${avg_pnl:,.0f}")
|
||||
print(f"Max Drawdown : ${max_dd:,.0f}")
|
||||
|
||||
# plot equity
|
||||
plt.figure(figsize=(10, 4))
|
||||
plt.plot(equity.index, equity.values)
|
||||
plt.title(f"Equity Curve ({pair}, {tenor_days}d)")
|
||||
plt.xlabel("Date")
|
||||
plt.ylabel("Cumulative PnL ($)")
|
||||
plt.grid(True)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
# === Main ===
|
||||
if __name__ == "__main__":
|
||||
backtest("EUR_USD")
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
def theoretical_forward(spot: float, r_dom: float, r_for: float, tenor_days: int) -> float:
|
||||
"""
|
||||
Calculate the theoretical forward rate:
|
||||
F = S * (1 + r_dom * (tenor_days/360)) / (1 + r_for * (tenor_days/360))
|
||||
"""
|
||||
return spot * (1 + r_dom * tenor_days / 360) / (1 + r_for * tenor_days / 360)
|
||||
|
||||
def deviation_bps(obs_fwd: float, theo_fwd: float) -> float:
|
||||
"""
|
||||
Compute the deviation between observed and theoretical forward,
|
||||
expressed in basis points.
|
||||
"""
|
||||
return (obs_fwd - theo_fwd) / theo_fwd * 10_000
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example inputs (replace these with your real data)
|
||||
spot_rate = 1.16910 # from PricingInfo closeout or mid
|
||||
observed_fwd = 1.16930 # placeholder forward outright
|
||||
r_domestic = 0.025 # e.g., 2.5% annual domestic interest
|
||||
r_foreign = 0.005 # e.g., 0.5% annual foreign interest
|
||||
tenor_in_days = 30 # for 1M tenor, approx 30 days
|
||||
|
||||
theo = theoretical_forward(spot_rate, r_domestic, r_foreign, tenor_in_days)
|
||||
dev = deviation_bps(observed_fwd, theo)
|
||||
|
||||
print(f"Theoretical 1M Forward: {theo:.6f}")
|
||||
print(f"Deviation: {dev:.2f} bps")
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"time": "2025-07-22T10:40:15.470477032Z",
|
||||
"prices": [
|
||||
{
|
||||
"type": "PRICE",
|
||||
"time": "2025-07-22T10:40:10.297022351Z",
|
||||
"bids": [
|
||||
{
|
||||
"price": "1.17025",
|
||||
"liquidity": 500000
|
||||
},
|
||||
{
|
||||
"price": "1.17024",
|
||||
"liquidity": 500000
|
||||
},
|
||||
{
|
||||
"price": "1.17023",
|
||||
"liquidity": 2000000
|
||||
},
|
||||
{
|
||||
"price": "1.17022",
|
||||
"liquidity": 2000000
|
||||
},
|
||||
{
|
||||
"price": "1.17021",
|
||||
"liquidity": 5000000
|
||||
},
|
||||
{
|
||||
"price": "1.17019",
|
||||
"liquidity": 10000000
|
||||
},
|
||||
{
|
||||
"price": "1.17016",
|
||||
"liquidity": 10000000
|
||||
}
|
||||
],
|
||||
"asks": [
|
||||
{
|
||||
"price": "1.17032",
|
||||
"liquidity": 500000
|
||||
},
|
||||
{
|
||||
"price": "1.17034",
|
||||
"liquidity": 2500000
|
||||
},
|
||||
{
|
||||
"price": "1.17035",
|
||||
"liquidity": 2000000
|
||||
},
|
||||
{
|
||||
"price": "1.17036",
|
||||
"liquidity": 5000000
|
||||
},
|
||||
{
|
||||
"price": "1.17039",
|
||||
"liquidity": 10000000
|
||||
},
|
||||
{
|
||||
"price": "1.17042",
|
||||
"liquidity": 10000000
|
||||
}
|
||||
],
|
||||
"closeoutBid": "1.17016",
|
||||
"closeoutAsk": "1.17042",
|
||||
"status": "tradeable",
|
||||
"tradeable": true,
|
||||
"quoteHomeConversionFactors": {
|
||||
"positiveUnits": "0.74144374",
|
||||
"negativeUnits": "0.74155370"
|
||||
},
|
||||
"instrument": "EUR_USD"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import os, json
|
||||
from oandapyV20 import API
|
||||
from oandapyV20.endpoints.accounts import AccountList
|
||||
|
||||
# 1. Read your practice token from env
|
||||
token = os.getenv("OANDA_TOKEN")
|
||||
|
||||
# 2. Initialize the client in practice mode
|
||||
client = API(access_token=token, environment="practice")
|
||||
|
||||
# 3. Create and send the AccountList request
|
||||
req = AccountList()
|
||||
resp = client.request(req)
|
||||
|
||||
# 4. Pretty-print the JSON so you can see your account IDs
|
||||
print(json.dumps(resp, indent=2))
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import warnings
|
||||
from urllib3.exceptions import NotOpenSSLWarning
|
||||
|
||||
# Silence the LibreSSL/OpenSSL warning
|
||||
warnings.filterwarnings("ignore", category=NotOpenSSLWarning)
|
||||
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from oandapyV20 import API
|
||||
from oandapyV20.endpoints.pricing import PricingInfo
|
||||
from cip import theoretical_forward, deviation_bps
|
||||
|
||||
# 1. Read credentials from environment variables
|
||||
# Ensure OANDA_TOKEN and OANDA_ACCOUNT_ID are exported in the same shell
|
||||
token = os.getenv("OANDA_TOKEN")
|
||||
account_id = os.getenv("OANDA_ACCOUNT_ID")
|
||||
|
||||
# Debug: verify credentials are loaded (remove after confirming)
|
||||
print("DEBUG: token →", token)
|
||||
print("DEBUG: account_id →", account_id)
|
||||
|
||||
# 2. Initialize OANDA client (practice environment)
|
||||
client = API(access_token=token, environment="practice")
|
||||
|
||||
# 3. Fetch spot pricing for EUR/USD
|
||||
pricing_req = PricingInfo(accountID=account_id, params={"instruments": "EUR_USD"})
|
||||
pricing_resp = client.request(pricing_req)
|
||||
print("\nSPOT PRICING:")
|
||||
print(json.dumps(pricing_resp, indent=2))
|
||||
|
||||
# 4. Compute spot mid price
|
||||
bid = float(pricing_resp["prices"][0]["bids"][0]["price"])
|
||||
ask = float(pricing_resp["prices"][0]["asks"][0]["price"])
|
||||
spot_mid = (bid + ask) / 2
|
||||
print(f"Spot mid: {spot_mid:.6f}")
|
||||
|
||||
# 5. Fetch all swap rates for EUR/USD via correct endpoint
|
||||
swap_url = "https://api-fxpractice.oanda.com/v3/instruments/EUR_USD/swap_rates"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
swap_resp = requests.get(swap_url, headers=headers)
|
||||
swap_data = swap_resp.json()
|
||||
print("\nSWAP RATES RESPONSE:")
|
||||
print(json.dumps(swap_data, indent=2))
|
||||
|
||||
# 6. Extract 1M swap-rate if available
|
||||
days = 30 # tenor in days for 1M
|
||||
swap_rates = swap_data.get("swapRates", [])
|
||||
if swap_rates:
|
||||
rate_1m = next((r for r in swap_rates if r.get("tenor") == "1M"), None)
|
||||
if rate_1m:
|
||||
print("\nObserved market 1M swap-rate object:")
|
||||
print(json.dumps(rate_1m, indent=2))
|
||||
# Compute observed forward outright: spot_mid + swap points
|
||||
fwd_pts = (rate_1m["longRate"] - rate_1m["shortRate"]) * days / 360
|
||||
obs_fwd = spot_mid + fwd_pts
|
||||
print(f"Observed 1M forward (spot + swap points): {obs_fwd:.6f}")
|
||||
else:
|
||||
print("\n⚠️ 1M tenor not found in swapRates; falling back to theoretical CIP")
|
||||
# placeholder interest rates
|
||||
r_domestic = 0.025 # e.g., USD OIS
|
||||
r_foreign = 0.005 # e.g., EUR OIS
|
||||
obs_fwd = theoretical_forward(spot_mid, r_domestic, r_foreign, days)
|
||||
print(f"Fallback observed forward: {obs_fwd:.6f}")
|
||||
else:
|
||||
print("\n⚠️ No swapRates data; using theoretical CIP as observed forward")
|
||||
# placeholder interest rates
|
||||
r_domestic = 0.025
|
||||
r_foreign = 0.005
|
||||
obs_fwd = theoretical_forward(spot_mid, r_domestic, r_foreign, days)
|
||||
print(f"Fallback observed forward: {obs_fwd:.6f}")
|
||||
|
||||
# 7. Compute theoretical forward and deviation
|
||||
# placeholder interest rates (update with live data when available)
|
||||
r_domestic = 0.025
|
||||
r_foreign = 0.005
|
||||
theo_fwd = theoretical_forward(spot_mid, r_domestic, r_foreign, days)
|
||||
dev_bps = deviation_bps(obs_fwd, theo_fwd)
|
||||
print(f"\nTheoretical 1M Forward: {theo_fwd:.6f}")
|
||||
print(f"Deviation : {dev_bps:.2f} bps")
|
||||
|
||||
# 8. Flag arbitrage signal if deviation exceeds threshold
|
||||
threshold = 2.0 # bps
|
||||
if abs(dev_bps) > threshold:
|
||||
direction = "Sell forward / Buy spot" if dev_bps > 0 else "Buy forward / Sell spot"
|
||||
print(f"⚠️ Arbitrage signal: {dev_bps:.2f} bps → {direction}")
|
||||
else:
|
||||
print("✅ No actionable arbitrage (deviation within threshold).")
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
threshold_bps,offset_bps,stop_loss_bps,spread_bps,total_pnl,num_trades,win_rate,avg_pnl,max_drawdown
|
||||
0.5,1.0,2.0,0.1,3035914.489215958,335,44.47761194029851,9062.431311092412,14800.0
|
||||
0.5,1.0,2.0,0.5,3029914.489215958,335,44.47761194029851,9044.520863331218,14800.0
|
||||
0.5,1.0,2.0,1.0,3022414.489215958,335,44.47761194029851,9022.132803629725,14800.0
|
||||
0.5,1.0,5.0,0.1,2980414.489215958,335,44.47761194029851,8896.759669301367,37000.0
|
||||
0.5,1.0,5.0,0.5,2974414.489215958,335,44.47761194029851,8878.849221540173,37000.0
|
||||
0.5,1.0,5.0,1.0,2966914.489215958,335,44.47761194029851,8856.46116183868,37000.0
|
||||
0.5,1.0,10.0,0.1,2888282.456922122,335,44.47761194029851,8621.738677379468,75079.10061016213
|
||||
0.5,1.0,10.0,0.5,2882242.456922122,335,44.47761194029851,8603.7088266332,75399.10061016213
|
||||
0.5,1.0,10.0,1.0,2874692.456922122,335,44.47761194029851,8581.171513200363,75799.10061016213
|
||||
0.5,2.0,2.0,0.1,3052218.607753026,335,44.776119402985074,9111.100321650823,14800.0
|
||||
0.5,2.0,2.0,0.5,3046218.607753026,335,44.47761194029851,9093.189873889629,14800.0
|
||||
0.5,2.0,2.0,1.0,3038718.607753026,335,44.47761194029851,9070.801814188137,14800.0
|
||||
0.5,2.0,5.0,0.1,2996718.607753026,335,44.776119402985074,8945.428679859779,37000.0
|
||||
0.5,2.0,5.0,0.5,2990718.607753026,335,44.47761194029851,8927.518232098584,37000.0
|
||||
0.5,2.0,5.0,1.0,2983218.607753026,335,44.47761194029851,8905.130172397092,37000.0
|
||||
0.5,2.0,10.0,0.1,2904773.3181278696,335,44.776119402985074,8670.96512873991,74165.65436484851
|
||||
0.5,2.0,10.0,0.5,2898693.3181278696,335,44.47761194029851,8652.815875008566,74485.65436484851
|
||||
0.5,2.0,10.0,1.0,2891110.4573677694,335,44.47761194029851,8630.180469754536,74885.65436484851
|
||||
0.5,3.0,2.0,0.1,3068522.72629009,335,44.776119402985074,9159.769332209224,14800.0
|
||||
0.5,3.0,2.0,0.5,3062522.72629009,335,44.776119402985074,9141.85888444803,14800.0
|
||||
0.5,3.0,2.0,1.0,3055022.72629009,335,44.776119402985074,9119.470824746537,14800.0
|
||||
0.5,3.0,5.0,0.1,3013118.457813414,335,44.776119402985074,8994.383456159445,37000.0
|
||||
0.5,3.0,5.0,0.5,3007078.457813414,335,44.776119402985074,8976.353605413176,37000.0
|
||||
0.5,3.0,5.0,1.0,2999528.457813414,335,44.776119402985074,8953.81629198034,37000.0
|
||||
0.5,3.0,10.0,0.1,2921299.887645775,335,44.776119402985074,8720.29817207694,74000.0
|
||||
0.5,3.0,10.0,0.5,2915219.887645775,335,44.776119402985074,8702.148918345598,74000.0
|
||||
0.5,3.0,10.0,1.0,2907619.887645775,335,44.776119402985074,8679.462351181417,74000.0
|
||||
0.5,4.0,2.0,0.1,3084826.8448271556,335,44.776119402985074,9208.43834276763,14800.0
|
||||
0.5,4.0,2.0,0.5,3078826.8448271556,335,44.776119402985074,9190.527895006435,14800.0
|
||||
0.5,4.0,2.0,1.0,3071326.8448271556,335,44.776119402985074,9168.139835304943,14800.0
|
||||
0.5,4.0,5.0,0.1,3029536.458259059,335,44.776119402985074,9043.392412713609,37000.0
|
||||
0.5,4.0,5.0,0.5,3023496.458259059,335,44.776119402985074,9025.36256196734,37000.0
|
||||
0.5,4.0,5.0,1.0,3015946.458259059,335,44.776119402985074,9002.825248534506,37000.0
|
||||
0.5,4.0,10.0,0.1,2937917.0644606417,335,44.776119402985074,8769.90168495714,74000.0
|
||||
0.5,4.0,10.0,0.5,2931797.0644606417,335,44.776119402985074,8751.633028240722,74000.0
|
||||
0.5,4.0,10.0,1.0,2924147.0644606417,335,44.776119402985074,8728.797207345198,74000.0
|
||||
0.5,5.0,2.0,0.1,3101154.4587047044,335,44.776119402985074,9257.177488670759,14800.0
|
||||
0.5,5.0,2.0,0.5,3095130.963364221,335,44.776119402985074,9239.196905564839,14800.0
|
||||
0.5,5.0,2.0,1.0,3087630.963364221,335,44.776119402985074,9216.808845863347,14800.0
|
||||
0.5,5.0,5.0,0.1,3045954.4587047044,335,44.776119402985074,9092.401369267774,37000.0
|
||||
0.5,5.0,5.0,0.5,3039914.4587047044,335,44.776119402985074,9074.371518521506,37000.0
|
||||
0.5,5.0,5.0,1.0,3032364.4587047044,335,44.776119402985074,9051.83420508867,37000.0
|
||||
0.5,5.0,10.0,0.1,2954551.9901967905,335,44.776119402985074,8819.558179691912,74000.0
|
||||
0.5,5.0,10.0,0.5,2948431.9901967905,335,44.776119402985074,8801.289522975494,74000.0
|
||||
0.5,5.0,10.0,1.0,2940781.9901967905,335,44.776119402985074,8778.453702079973,74000.0
|
||||
1.0,1.0,2.0,0.1,1233700.1759173332,160,19.1044776119403,3682.687092290547,7200.0
|
||||
1.0,1.0,2.0,0.5,1231100.1759173332,160,19.1044776119403,3674.925898260696,7200.0
|
||||
1.0,1.0,2.0,1.0,1227850.1759173332,160,19.1044776119403,3665.224405723383,7200.0
|
||||
1.0,1.0,5.0,0.1,1205200.1759173332,160,19.1044776119403,3597.612465424875,18000.0
|
||||
1.0,1.0,5.0,0.5,1202600.1759173332,160,19.1044776119403,3589.8512713950245,18000.0
|
||||
1.0,1.0,5.0,1.0,1199350.1759173332,160,19.1044776119403,3580.149778857711,18000.0
|
||||
1.0,1.0,10.0,0.1,1157700.1759173332,160,19.1044776119403,3455.821420648756,42775.720814244356
|
||||
1.0,1.0,10.0,0.5,1155100.1759173332,160,19.1044776119403,3448.0602266189053,42895.720814244356
|
||||
1.0,1.0,10.0,1.0,1151850.1759173332,160,19.1044776119403,3438.3587340815916,43045.720814244356
|
||||
1.0,2.0,2.0,0.1,3052218.607753026,335,44.776119402985074,9111.100321650823,14800.0
|
||||
1.0,2.0,2.0,0.5,3046218.607753026,335,44.47761194029851,9093.189873889629,14800.0
|
||||
1.0,2.0,2.0,1.0,3038718.607753026,335,44.47761194029851,9070.801814188137,14800.0
|
||||
1.0,2.0,5.0,0.1,2996718.607753026,335,44.776119402985074,8945.428679859779,37000.0
|
||||
1.0,2.0,5.0,0.5,2990718.607753026,335,44.47761194029851,8927.518232098584,37000.0
|
||||
1.0,2.0,5.0,1.0,2983218.607753026,335,44.47761194029851,8905.130172397092,37000.0
|
||||
1.0,2.0,10.0,0.1,2904773.3181278696,335,44.776119402985074,8670.96512873991,74165.65436484851
|
||||
1.0,2.0,10.0,0.5,2898693.3181278696,335,44.47761194029851,8652.815875008566,74485.65436484851
|
||||
1.0,2.0,10.0,1.0,2891110.4573677694,335,44.47761194029851,8630.180469754536,74885.65436484851
|
||||
1.0,3.0,2.0,0.1,3068522.72629009,335,44.776119402985074,9159.769332209224,14800.0
|
||||
1.0,3.0,2.0,0.5,3062522.72629009,335,44.776119402985074,9141.85888444803,14800.0
|
||||
1.0,3.0,2.0,1.0,3055022.72629009,335,44.776119402985074,9119.470824746537,14800.0
|
||||
1.0,3.0,5.0,0.1,3013118.457813414,335,44.776119402985074,8994.383456159445,37000.0
|
||||
1.0,3.0,5.0,0.5,3007078.457813414,335,44.776119402985074,8976.353605413176,37000.0
|
||||
1.0,3.0,5.0,1.0,2999528.457813414,335,44.776119402985074,8953.81629198034,37000.0
|
||||
1.0,3.0,10.0,0.1,2921299.887645775,335,44.776119402985074,8720.29817207694,74000.0
|
||||
1.0,3.0,10.0,0.5,2915219.887645775,335,44.776119402985074,8702.148918345598,74000.0
|
||||
1.0,3.0,10.0,1.0,2907619.887645775,335,44.776119402985074,8679.462351181417,74000.0
|
||||
1.0,4.0,2.0,0.1,3084826.8448271556,335,44.776119402985074,9208.43834276763,14800.0
|
||||
1.0,4.0,2.0,0.5,3078826.8448271556,335,44.776119402985074,9190.527895006435,14800.0
|
||||
1.0,4.0,2.0,1.0,3071326.8448271556,335,44.776119402985074,9168.139835304943,14800.0
|
||||
1.0,4.0,5.0,0.1,3029536.458259059,335,44.776119402985074,9043.392412713609,37000.0
|
||||
1.0,4.0,5.0,0.5,3023496.458259059,335,44.776119402985074,9025.36256196734,37000.0
|
||||
1.0,4.0,5.0,1.0,3015946.458259059,335,44.776119402985074,9002.825248534506,37000.0
|
||||
1.0,4.0,10.0,0.1,2937917.0644606417,335,44.776119402985074,8769.90168495714,74000.0
|
||||
1.0,4.0,10.0,0.5,2931797.0644606417,335,44.776119402985074,8751.633028240722,74000.0
|
||||
1.0,4.0,10.0,1.0,2924147.0644606417,335,44.776119402985074,8728.797207345198,74000.0
|
||||
1.0,5.0,2.0,0.1,3101154.4587047044,335,44.776119402985074,9257.177488670759,14800.0
|
||||
1.0,5.0,2.0,0.5,3095130.963364221,335,44.776119402985074,9239.196905564839,14800.0
|
||||
1.0,5.0,2.0,1.0,3087630.963364221,335,44.776119402985074,9216.808845863347,14800.0
|
||||
1.0,5.0,5.0,0.1,3045954.4587047044,335,44.776119402985074,9092.401369267774,37000.0
|
||||
1.0,5.0,5.0,0.5,3039914.4587047044,335,44.776119402985074,9074.371518521506,37000.0
|
||||
1.0,5.0,5.0,1.0,3032364.4587047044,335,44.776119402985074,9051.83420508867,37000.0
|
||||
1.0,5.0,10.0,0.1,2954551.9901967905,335,44.776119402985074,8819.558179691912,74000.0
|
||||
1.0,5.0,10.0,0.5,2948431.9901967905,335,44.776119402985074,8801.289522975494,74000.0
|
||||
1.0,5.0,10.0,1.0,2940781.9901967905,335,44.776119402985074,8778.453702079973,74000.0
|
||||
2.0,1.0,2.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
2.0,1.0,2.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
2.0,1.0,2.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
2.0,1.0,5.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
2.0,1.0,5.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
2.0,1.0,5.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
2.0,1.0,10.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
2.0,1.0,10.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
2.0,1.0,10.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
2.0,2.0,2.0,0.1,1234607.5833340343,130,18.507462686567163,3685.395771146371,5800.0
|
||||
2.0,2.0,2.0,0.5,1232127.5833340343,130,18.507462686567163,3677.9927860717444,5800.0
|
||||
2.0,2.0,2.0,1.0,1229027.5833340343,130,18.507462686567163,3668.739054728461,5800.0
|
||||
2.0,2.0,5.0,0.1,1214207.5833340343,130,18.507462686567163,3624.5002487583115,14500.0
|
||||
2.0,2.0,5.0,0.5,1211727.5833340343,130,18.507462686567163,3617.0972636836846,14500.0
|
||||
2.0,2.0,5.0,1.0,1208627.5833340343,130,18.507462686567163,3607.843532340401,14500.0
|
||||
2.0,2.0,10.0,0.1,1180207.5833340343,130,18.507462686567163,3523.0077114448786,29000.0
|
||||
2.0,2.0,10.0,0.5,1177727.5833340343,130,18.507462686567163,3515.6047263702517,29000.0
|
||||
2.0,2.0,10.0,1.0,1174627.5833340343,130,18.507462686567163,3506.350995026968,29000.0
|
||||
2.0,3.0,2.0,0.1,3068522.72629009,335,44.776119402985074,9159.769332209224,14800.0
|
||||
2.0,3.0,2.0,0.5,3062522.72629009,335,44.776119402985074,9141.85888444803,14800.0
|
||||
2.0,3.0,2.0,1.0,3055022.72629009,335,44.776119402985074,9119.470824746537,14800.0
|
||||
2.0,3.0,5.0,0.1,3013118.457813414,335,44.776119402985074,8994.383456159445,37000.0
|
||||
2.0,3.0,5.0,0.5,3007078.457813414,335,44.776119402985074,8976.353605413176,37000.0
|
||||
2.0,3.0,5.0,1.0,2999528.457813414,335,44.776119402985074,8953.81629198034,37000.0
|
||||
2.0,3.0,10.0,0.1,2921299.887645775,335,44.776119402985074,8720.29817207694,74000.0
|
||||
2.0,3.0,10.0,0.5,2915219.887645775,335,44.776119402985074,8702.148918345598,74000.0
|
||||
2.0,3.0,10.0,1.0,2907619.887645775,335,44.776119402985074,8679.462351181417,74000.0
|
||||
2.0,4.0,2.0,0.1,3084826.8448271556,335,44.776119402985074,9208.43834276763,14800.0
|
||||
2.0,4.0,2.0,0.5,3078826.8448271556,335,44.776119402985074,9190.527895006435,14800.0
|
||||
2.0,4.0,2.0,1.0,3071326.8448271556,335,44.776119402985074,9168.139835304943,14800.0
|
||||
2.0,4.0,5.0,0.1,3029536.458259059,335,44.776119402985074,9043.392412713609,37000.0
|
||||
2.0,4.0,5.0,0.5,3023496.458259059,335,44.776119402985074,9025.36256196734,37000.0
|
||||
2.0,4.0,5.0,1.0,3015946.458259059,335,44.776119402985074,9002.825248534506,37000.0
|
||||
2.0,4.0,10.0,0.1,2937917.0644606417,335,44.776119402985074,8769.90168495714,74000.0
|
||||
2.0,4.0,10.0,0.5,2931797.0644606417,335,44.776119402985074,8751.633028240722,74000.0
|
||||
2.0,4.0,10.0,1.0,2924147.0644606417,335,44.776119402985074,8728.797207345198,74000.0
|
||||
2.0,5.0,2.0,0.1,3101154.4587047044,335,44.776119402985074,9257.177488670759,14800.0
|
||||
2.0,5.0,2.0,0.5,3095130.963364221,335,44.776119402985074,9239.196905564839,14800.0
|
||||
2.0,5.0,2.0,1.0,3087630.963364221,335,44.776119402985074,9216.808845863347,14800.0
|
||||
2.0,5.0,5.0,0.1,3045954.4587047044,335,44.776119402985074,9092.401369267774,37000.0
|
||||
2.0,5.0,5.0,0.5,3039914.4587047044,335,44.776119402985074,9074.371518521506,37000.0
|
||||
2.0,5.0,5.0,1.0,3032364.4587047044,335,44.776119402985074,9051.83420508867,37000.0
|
||||
2.0,5.0,10.0,0.1,2954551.9901967905,335,44.776119402985074,8819.558179691912,74000.0
|
||||
2.0,5.0,10.0,0.5,2948431.9901967905,335,44.776119402985074,8801.289522975494,74000.0
|
||||
2.0,5.0,10.0,1.0,2940781.9901967905,335,44.776119402985074,8778.453702079973,74000.0
|
||||
3.0,1.0,2.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
3.0,1.0,2.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
3.0,1.0,2.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
3.0,1.0,5.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
3.0,1.0,5.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
3.0,1.0,5.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
3.0,1.0,10.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
3.0,1.0,10.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
3.0,1.0,10.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
3.0,2.0,2.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
3.0,2.0,2.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
3.0,2.0,2.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
3.0,2.0,5.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
3.0,2.0,5.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
3.0,2.0,5.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
3.0,2.0,10.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
3.0,2.0,10.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
3.0,2.0,10.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
3.0,3.0,2.0,0.1,1136323.1076457775,116,16.119402985074625,3392.0092765545596,5000.0
|
||||
3.0,3.0,2.0,0.5,1134163.1076457775,116,16.119402985074625,3385.56151536053,5000.0
|
||||
3.0,3.0,2.0,1.0,1131463.1076457775,116,16.119402985074625,3377.5018138679925,5000.0
|
||||
3.0,3.0,5.0,0.1,1117723.1076457775,116,16.119402985074625,3336.4868884948582,12500.0
|
||||
3.0,3.0,5.0,0.5,1115563.1076457775,116,16.119402985074625,3330.0391273008286,12500.0
|
||||
3.0,3.0,5.0,1.0,1112863.1076457775,116,16.119402985074625,3321.979425808291,12500.0
|
||||
3.0,3.0,10.0,0.1,1086723.1076457775,116,16.119402985074625,3243.9495750620226,25000.0
|
||||
3.0,3.0,10.0,0.5,1084563.1076457775,116,16.119402985074625,3237.5018138679925,25000.0
|
||||
3.0,3.0,10.0,1.0,1081863.1076457775,116,16.119402985074625,3229.4421123754555,25000.0
|
||||
3.0,4.0,2.0,0.1,3084826.8448271556,335,44.776119402985074,9208.43834276763,14800.0
|
||||
3.0,4.0,2.0,0.5,3078826.8448271556,335,44.776119402985074,9190.527895006435,14800.0
|
||||
3.0,4.0,2.0,1.0,3071326.8448271556,335,44.776119402985074,9168.139835304943,14800.0
|
||||
3.0,4.0,5.0,0.1,3029536.458259059,335,44.776119402985074,9043.392412713609,37000.0
|
||||
3.0,4.0,5.0,0.5,3023496.458259059,335,44.776119402985074,9025.36256196734,37000.0
|
||||
3.0,4.0,5.0,1.0,3015946.458259059,335,44.776119402985074,9002.825248534506,37000.0
|
||||
3.0,4.0,10.0,0.1,2937917.0644606417,335,44.776119402985074,8769.90168495714,74000.0
|
||||
3.0,4.0,10.0,0.5,2931797.0644606417,335,44.776119402985074,8751.633028240722,74000.0
|
||||
3.0,4.0,10.0,1.0,2924147.0644606417,335,44.776119402985074,8728.797207345198,74000.0
|
||||
3.0,5.0,2.0,0.1,3101154.4587047044,335,44.776119402985074,9257.177488670759,14800.0
|
||||
3.0,5.0,2.0,0.5,3095130.963364221,335,44.776119402985074,9239.196905564839,14800.0
|
||||
3.0,5.0,2.0,1.0,3087630.963364221,335,44.776119402985074,9216.808845863347,14800.0
|
||||
3.0,5.0,5.0,0.1,3045954.4587047044,335,44.776119402985074,9092.401369267774,37000.0
|
||||
3.0,5.0,5.0,0.5,3039914.4587047044,335,44.776119402985074,9074.371518521506,37000.0
|
||||
3.0,5.0,5.0,1.0,3032364.4587047044,335,44.776119402985074,9051.83420508867,37000.0
|
||||
3.0,5.0,10.0,0.1,2954551.9901967905,335,44.776119402985074,8819.558179691912,74000.0
|
||||
3.0,5.0,10.0,0.5,2948431.9901967905,335,44.776119402985074,8801.289522975494,74000.0
|
||||
3.0,5.0,10.0,1.0,2940781.9901967905,335,44.776119402985074,8778.453702079973,74000.0
|
||||
4.0,1.0,2.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
4.0,1.0,2.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
4.0,1.0,2.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
4.0,1.0,5.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
4.0,1.0,5.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
4.0,1.0,5.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
4.0,1.0,10.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
4.0,1.0,10.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
4.0,1.0,10.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
4.0,2.0,2.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
4.0,2.0,2.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
4.0,2.0,2.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
4.0,2.0,5.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
4.0,2.0,5.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
4.0,2.0,5.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
4.0,2.0,10.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
4.0,2.0,10.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
4.0,2.0,10.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
4.0,3.0,2.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
4.0,3.0,2.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
4.0,3.0,2.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
4.0,3.0,5.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
4.0,3.0,5.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
4.0,3.0,5.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
4.0,3.0,10.0,0.1,0.0,0,0.0,0.0,0.0
|
||||
4.0,3.0,10.0,0.5,0.0,0,0.0,0.0,0.0
|
||||
4.0,3.0,10.0,1.0,0.0,0,0.0,0.0,0.0
|
||||
4.0,4.0,2.0,0.1,744222.7130903826,90,11.343283582089553,2221.5603375832316,6948.339329446317
|
||||
4.0,4.0,2.0,0.5,742702.7130903827,90,11.343283582089553,2217.0230241503964,6988.339329446317
|
||||
4.0,4.0,2.0,1.0,740802.7130903827,90,11.343283582089553,2211.3513823593516,7038.339329446317
|
||||
4.0,4.0,5.0,0.1,728622.7130903827,90,11.343283582089553,2174.9931734041274,17748.339329446317
|
||||
4.0,4.0,5.0,0.5,727102.7130903827,90,11.343283582089553,2170.455859971292,17788.339329446317
|
||||
4.0,4.0,5.0,1.0,725202.7130903827,90,11.343283582089553,2164.784218180247,17838.339329446317
|
||||
4.0,4.0,10.0,0.1,702713.3203873425,90,11.343283582089553,2097.6517026487836,35748.33932944632
|
||||
4.0,4.0,10.0,0.5,701153.3203873425,90,11.343283582089553,2092.994986230873,35788.33932944632
|
||||
4.0,4.0,10.0,1.0,699203.3203873425,90,11.343283582089553,2087.174090708485,35838.33932944632
|
||||
4.0,5.0,2.0,0.1,3101154.4587047044,335,44.776119402985074,9257.177488670759,14800.0
|
||||
4.0,5.0,2.0,0.5,3095130.963364221,335,44.776119402985074,9239.196905564839,14800.0
|
||||
4.0,5.0,2.0,1.0,3087630.963364221,335,44.776119402985074,9216.808845863347,14800.0
|
||||
4.0,5.0,5.0,0.1,3045954.4587047044,335,44.776119402985074,9092.401369267774,37000.0
|
||||
4.0,5.0,5.0,0.5,3039914.4587047044,335,44.776119402985074,9074.371518521506,37000.0
|
||||
4.0,5.0,5.0,1.0,3032364.4587047044,335,44.776119402985074,9051.83420508867,37000.0
|
||||
4.0,5.0,10.0,0.1,2954551.9901967905,335,44.776119402985074,8819.558179691912,74000.0
|
||||
4.0,5.0,10.0,0.5,2948431.9901967905,335,44.776119402985074,8801.289522975494,74000.0
|
||||
4.0,5.0,10.0,1.0,2940781.9901967905,335,44.776119402985074,8778.453702079973,74000.0
|
||||
|
@@ -0,0 +1,37 @@
|
||||
threshold_bps,stop_loss_bps,spread_bps,total_pnl,num_trades,win_rate,avg_pnl,max_drawdown
|
||||
0.5,2.0,0.1,5153890.000000001,335,57.611940298507456,15384.74626865672,18295.0
|
||||
0.5,2.0,0.5,5146160.000000002,335,57.611940298507456,15361.67164179105,18335.0
|
||||
0.5,2.0,1.0,5136510.000000002,335,57.611940298507456,15332.865671641797,18385.0
|
||||
0.5,5.0,0.1,5111590.000000001,335,57.611940298507456,15258.477611940301,46495.0
|
||||
0.5,5.0,0.5,5103830.000000001,335,57.611940298507456,15235.313432835823,46535.0
|
||||
0.5,5.0,1.0,5094130.000000001,335,57.611940298507456,15206.358208955227,46585.0
|
||||
0.5,10.0,0.1,5042665.0,335,57.611940298507456,15052.731343283582,92680.00000000047
|
||||
0.5,10.0,0.5,5034665.0,335,57.611940298507456,15028.850746268658,92840.00000000047
|
||||
0.5,10.0,1.0,5024730.000000001,335,57.611940298507456,14999.194029850749,93025.00000000023
|
||||
1.0,2.0,0.1,5153890.000000001,335,57.611940298507456,15384.74626865672,18295.0
|
||||
1.0,2.0,0.5,5146160.000000002,335,57.611940298507456,15361.67164179105,18335.0
|
||||
1.0,2.0,1.0,5136510.000000002,335,57.611940298507456,15332.865671641797,18385.0
|
||||
1.0,5.0,0.1,5111590.000000001,335,57.611940298507456,15258.477611940301,46495.0
|
||||
1.0,5.0,0.5,5103830.000000001,335,57.611940298507456,15235.313432835823,46535.0
|
||||
1.0,5.0,1.0,5094130.000000001,335,57.611940298507456,15206.358208955227,46585.0
|
||||
1.0,10.0,0.1,5042665.0,335,57.611940298507456,15052.731343283582,92680.00000000047
|
||||
1.0,10.0,0.5,5034665.0,335,57.611940298507456,15028.850746268658,92840.00000000047
|
||||
1.0,10.0,1.0,5024730.000000001,335,57.611940298507456,14999.194029850749,93025.00000000023
|
||||
2.0,2.0,0.1,5153890.000000001,335,57.611940298507456,15384.74626865672,18295.0
|
||||
2.0,2.0,0.5,5146160.000000002,335,57.611940298507456,15361.67164179105,18335.0
|
||||
2.0,2.0,1.0,5136510.000000002,335,57.611940298507456,15332.865671641797,18385.0
|
||||
2.0,5.0,0.1,5111590.000000001,335,57.611940298507456,15258.477611940301,46495.0
|
||||
2.0,5.0,0.5,5103830.000000001,335,57.611940298507456,15235.313432835823,46535.0
|
||||
2.0,5.0,1.0,5094130.000000001,335,57.611940298507456,15206.358208955227,46585.0
|
||||
2.0,10.0,0.1,5042665.0,335,57.611940298507456,15052.731343283582,92680.00000000047
|
||||
2.0,10.0,0.5,5034665.0,335,57.611940298507456,15028.850746268658,92840.00000000047
|
||||
2.0,10.0,1.0,5024730.000000001,335,57.611940298507456,14999.194029850749,93025.00000000023
|
||||
3.0,2.0,0.1,5153890.000000001,335,57.611940298507456,15384.74626865672,18295.0
|
||||
3.0,2.0,0.5,5146160.000000002,335,57.611940298507456,15361.67164179105,18335.0
|
||||
3.0,2.0,1.0,5136510.000000002,335,57.611940298507456,15332.865671641797,18385.0
|
||||
3.0,5.0,0.1,5111590.000000001,335,57.611940298507456,15258.477611940301,46495.0
|
||||
3.0,5.0,0.5,5103830.000000001,335,57.611940298507456,15235.313432835823,46535.0
|
||||
3.0,5.0,1.0,5094130.000000001,335,57.611940298507456,15206.358208955227,46585.0
|
||||
3.0,10.0,0.1,5042665.0,335,57.611940298507456,15052.731343283582,92680.00000000047
|
||||
3.0,10.0,0.5,5034665.0,335,57.611940298507456,15028.850746268658,92840.00000000047
|
||||
3.0,10.0,1.0,5024730.000000001,335,57.611940298507456,14999.194029850749,93025.00000000023
|
||||
|
+114
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import itertools
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import requests
|
||||
from dateutil import parser
|
||||
from oandapyV20 import API
|
||||
from oandapyV20.endpoints.instruments import InstrumentsCandles
|
||||
from cip import theoretical_forward, deviation_bps
|
||||
|
||||
# === Configuration ===
|
||||
OANDA_TOKEN = os.getenv("OANDA_TOKEN")
|
||||
OANDA_ACCOUNT_ID = os.getenv("OANDA_ACCOUNT_ID")
|
||||
BASE_URL_SWAP = "https://api-fxpractice.oanda.com" # practice swap endpoint
|
||||
|
||||
if not OANDA_TOKEN or not OANDA_ACCOUNT_ID:
|
||||
raise RuntimeError("Please set OANDA_TOKEN and OANDA_ACCOUNT_ID environment variables")
|
||||
|
||||
# Initialize OANDA API client for spot
|
||||
api = API(access_token=OANDA_TOKEN, environment="practice")
|
||||
|
||||
# === Data fetching ===
|
||||
def fetch_spot_history(pair: str, days: int = 365) -> pd.Series:
|
||||
req = InstrumentsCandles(
|
||||
instrument=pair,
|
||||
params={"granularity": "D", "count": days, "price": "M"}
|
||||
)
|
||||
data = api.request(req)["candles"]
|
||||
records = [(parser.isoparse(c["time"]).date(), (float(c["mid"]["o"]) + float(c["mid"]["c"]))/2)
|
||||
for c in data]
|
||||
return pd.Series({d: s for d, s in records}).sort_index()
|
||||
|
||||
|
||||
def fetch_swap_history(pair: str, days: int = 365) -> pd.Series:
|
||||
url = f"{BASE_URL_SWAP}/v3/accounts/{OANDA_ACCOUNT_ID}/instruments/{pair}/swap_rates"
|
||||
headers = {"Authorization": f"Bearer {OANDA_TOKEN}"}
|
||||
params = {"count": days, "granularity": "D"}
|
||||
try:
|
||||
resp = requests.get(url, headers=headers, params=params)
|
||||
resp.raise_for_status()
|
||||
data = resp.json().get("swapRates", [])
|
||||
records = []
|
||||
for r in data:
|
||||
dt = parser.isoparse(r["time"]).date()
|
||||
lr = float(r.get("longRate", 0))
|
||||
sr = float(r.get("shortRate", 0))
|
||||
records.append((dt, lr - sr))
|
||||
return pd.Series({d: p for d, p in records}).sort_index()
|
||||
except Exception:
|
||||
# fallback zeros
|
||||
spot = fetch_spot_history(pair, days)
|
||||
return pd.Series(0.0, index=spot.index)
|
||||
|
||||
# === Backtest using real forward ===
|
||||
def run_backtest(
|
||||
spot: pd.Series,
|
||||
swap_pts: pd.Series,
|
||||
threshold_bps: float,
|
||||
stop_loss_bps: float,
|
||||
spread_bps: float,
|
||||
tenor_days: int = 30,
|
||||
r_dom: float = 0.025,
|
||||
r_for: float = 0.005,
|
||||
notional: float = 1_000_000
|
||||
) -> dict:
|
||||
df = pd.DataFrame({"spot": spot})
|
||||
df["swap_pts"] = swap_pts.reindex(df.index).fillna(method="ffill")
|
||||
df["theo_fwd"] = df["spot"].apply(lambda s: theoretical_forward(s, r_dom, r_for, tenor_days))
|
||||
df["obs_fwd"] = df["spot"] + df["swap_pts"] * tenor_days / 360
|
||||
df["dev_bps"] = (df["obs_fwd"] - df["theo_fwd"]) / df["theo_fwd"] * 10_000
|
||||
df["signal"] = 0
|
||||
df.loc[df["dev_bps"] > threshold_bps, "signal"] = -1
|
||||
df.loc[df["dev_bps"] < -threshold_bps, "signal"] = +1
|
||||
cost = spread_bps / 10_000 * notional
|
||||
df["exit_spot"] = df["spot"].shift(-tenor_days)
|
||||
df["raw_pnl"] = df["signal"] * (df["exit_spot"] - df["obs_fwd"]) * notional
|
||||
df["pnl"] = df["raw_pnl"] - df["signal"].abs() * cost
|
||||
stop_amt = stop_loss_bps / 10_000 * notional
|
||||
df.loc[df["pnl"] < -stop_amt, "pnl"] = -stop_amt
|
||||
trades = df.dropna(subset=["pnl"])
|
||||
total_pnl = trades["pnl"].sum()
|
||||
num_trades = (trades["signal"] != 0).sum()
|
||||
win_rate = trades["pnl"].gt(0).mean() * 100 if num_trades else 0
|
||||
avg_pnl = trades["pnl"].mean() if num_trades else 0
|
||||
equity = trades["pnl"].cumsum()
|
||||
max_dd = (equity.cummax() - equity).max() if not equity.empty else 0
|
||||
return {"total_pnl": total_pnl,
|
||||
"num_trades": num_trades,
|
||||
"win_rate": win_rate,
|
||||
"avg_pnl": avg_pnl,
|
||||
"max_drawdown": max_dd}
|
||||
|
||||
# === Optimization sweep ===
|
||||
if __name__ == "__main__":
|
||||
pair = "EUR_USD"
|
||||
spot = fetch_spot_history(pair, days=365)
|
||||
swap_pts= fetch_swap_history(pair, days=365)
|
||||
thresholds = [0.5, 1.0, 2.0, 3.0]
|
||||
stop_losses = [2.0, 5.0, 10.0]
|
||||
spreads = [0.1, 0.5, 1.0]
|
||||
tenor_days = 30
|
||||
results = []
|
||||
for th, sl, sp in itertools.product(thresholds, stop_losses, spreads):
|
||||
m = run_backtest(spot, swap_pts, th, sl, sp, tenor_days)
|
||||
results.append({"threshold_bps": th,
|
||||
"stop_loss_bps": sl,
|
||||
"spread_bps": sp,
|
||||
**m})
|
||||
df = pd.DataFrame(results)
|
||||
df.to_csv("optimization_results_real.csv", index=False)
|
||||
top = df.sort_values("total_pnl", ascending=False).head(10)
|
||||
print("Top 10 real-forward parameter sets:")
|
||||
print(top.to_string(index=False))
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
home = /Library/Developer/CommandLineTools/usr/bin
|
||||
include-system-site-packages = false
|
||||
version = 3.9.6
|
||||
@@ -0,0 +1,6 @@
|
||||
certifi==2025.7.14
|
||||
charset-normalizer==3.4.2
|
||||
idna==3.10
|
||||
oandapyV20==0.7.2
|
||||
requests==2.32.4
|
||||
urllib3==2.5.0
|
||||
@@ -0,0 +1,32 @@
|
||||
def pnl_path(spot_series, obs_forward, notional=1_000_000):
|
||||
"""
|
||||
Given a time series of spot prices (list of floats) and a
|
||||
locked-in forward price (obs_forward), returns a list of
|
||||
PnL values under a +1 lot trade.
|
||||
PnL_t = notional * (spot_t - obs_forward)
|
||||
"""
|
||||
return [notional * (s - obs_forward) for s in spot_series]
|
||||
|
||||
import random
|
||||
# e.g. 10 days of spot returns ±0.5%
|
||||
base = 1.16987
|
||||
path = []
|
||||
for _ in range(10):
|
||||
shock = random.uniform(-0.005, 0.005)
|
||||
base = base * (1 + shock)
|
||||
path.append(round(base, 6))
|
||||
|
||||
from risk import pnl_path
|
||||
|
||||
# assume obs_forward from your engine, e.g. 1.17105
|
||||
obs_forward = 1.17105
|
||||
pnls = pnl_path(path, obs_forward)
|
||||
print("Day-by-day PnL:", pnls)
|
||||
|
||||
import numpy as np
|
||||
|
||||
# compute daily PnL changes
|
||||
diffs = np.diff(pnls)
|
||||
# find the 5th percentile loss
|
||||
var95 = -np.percentile(diffs, 5)
|
||||
print(f"1-day 95% VaR: ${var95:,.2f}")
|
||||
@@ -0,0 +1,33 @@
|
||||
# simulate_forward.py
|
||||
|
||||
import random
|
||||
from cip import theoretical_forward, deviation_bps
|
||||
|
||||
# === Simulation parameters ===
|
||||
spot_mid = 1.16987 # last known spot mid
|
||||
r_domestic = 0.025 # e.g. USD OIS annual rate
|
||||
r_foreign = 0.005 # e.g. EUR OIS annual rate
|
||||
days = 30 # tenor in days for 1M
|
||||
threshold_bps = 2.0 # alert threshold in bps
|
||||
|
||||
# 1) Compute the “fair” 1M forward via CIP
|
||||
theo_fwd = theoretical_forward(spot_mid, r_domestic, r_foreign, days)
|
||||
print(f"Theoretical 1M forward: {theo_fwd:.6f}\n")
|
||||
|
||||
# 2) Run 10 simulated “observed” forwards with noise ±5 bps
|
||||
for i in range(1, 11):
|
||||
noise_bps = random.uniform(-5, 5)
|
||||
obs_fwd = theo_fwd * (1 + noise_bps / 10_000)
|
||||
dev = deviation_bps(obs_fwd, theo_fwd)
|
||||
|
||||
# 3) Determine if it breaches your threshold
|
||||
signal = ""
|
||||
if abs(dev) > threshold_bps:
|
||||
direction = "Sell forward / Buy spot" if dev > 0 else "Buy forward / Sell spot"
|
||||
signal = f" ⚠️ ARB SIGNAL: {dev:.2f} bps → {direction}"
|
||||
|
||||
# 4) Print the result for this trial
|
||||
print(
|
||||
f"Sim #{i:2d}: noise={noise_bps:+.2f} bps → "
|
||||
f"observed={obs_fwd:.6f} → dev={dev:+.2f} bps{signal}"
|
||||
)
|
||||
Reference in New Issue
Block a user