Files
fx-arb-dashboard/optimize.py
T

115 lines
4.4 KiB
Python
Raw Normal View History

2025-07-23 14:44:57 +01:00
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))