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

145 lines
5.2 KiB
Python

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")