Cross-Asset Arbitrage Engine
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import coint, adfuller
class ArbitrageEngine:
@staticmethod
def cointegration_test(series_a: pd.Series, series_b: pd.Series) -> dict:
"""Test if two series are cointegrated (mean-reverting spread)."""
score, pvalue, _ = coint(series_a.dropna(), series_b.dropna())
return {
"cointegrated": pvalue < 0.05,
"p_value": round(pvalue, 4),
"test_stat": round(score, 4),
"signal": "COINTEGRATED — pairs trade viable" if pvalue < 0.05 else "NOT cointegrated — avoid pairs trade",
}
@staticmethod
def hedge_ratio(series_a: pd.Series, series_b: pd.Series) -> dict:
"""OLS hedge ratio for pairs trade construction."""
from numpy.polynomial.polynomial import polyfit
b, a = np.polyfit(series_b, series_a, 1)
spread = series_a - b * series_b
adf_stat, adf_p, *_ = adfuller(spread.dropna())
return {
"hedge_ratio": round(b, 6),
"intercept": round(a, 6),
"spread_stationary": adf_p < 0.05,
"spread_adf_p": round(adf_p, 4),
"entry_rule": f"Buy A, sell {abs(b):.4f} B when z-score < -2. Reverse when z-score > 2.",
}
@staticmethod
def triangular_arb_check(rates: dict) -> dict:
"""
Check for triangular arbitrage opportunity.
rates: {"EURUSD": 1.0850, "GBPUSD": 1.2650, "EURGBP": 0.8570}
"""
try:
eurusd = rates["EURUSD"]
gbpusd = rates["GBPUSD"]
eurgbp = rates["EURGBP"]
# Path 1: USD → EUR → GBP → USD
implied_eurgbp = eurusd / gbpusd
arb_1 = (implied_eurgbp / eurgbp - 1) * 10000 # in pips
# Path 2: USD → GBP → EUR → USD
implied_eurusd = eurgbp * gbpusd
arb_2 = (implied_eurusd / eurusd - 1) * 10000
return {
"implied_eurgbp": round(implied_eurgbp, 5),
"actual_eurgbp": eurgbp,
"arb_pips": round(arb_1, 1),
"opportunity": abs(arb_1) > 2,
"direction": "Buy EURGBP" if arb_1 < -2 else "Sell EURGBP" if arb_1 > 2 else "No arb",
"note": "Account for spread + execution latency. Sub-2pip arbs rarely executable.",
}
except KeyError:
return {"error": "Need EURUSD, GBPUSD, EURGBP rates"}
@staticmethod
def spread_z_score_signals(spread: pd.Series, window: int = 60,
entry_z: float = 2.0, exit_z: float = 0.5) -> pd.DataFrame:
"""Generate entry/exit signals from spread z-score."""
mean = spread.rolling(window).mean()
std = spread.rolling(window).std()
z = (spread - mean) / std.replace(0, np.nan)
signals = pd.DataFrame(index=spread.index)
signals["z_score"] = z
signals["signal"] = 0
signals.loc[z < -entry_z, "signal"] = 1 # Buy spread
signals.loc[z > entry_z, "signal"] = -1 # Sell spread
signals.loc[z.abs() < exit_z, "signal"] = 0 # Exit
return signals
@staticmethod
def scan_cointegrated_pairs(prices: pd.DataFrame, max_pvalue: float = 0.05) -> list[dict]:
"""Scan all pair combinations for cointegration."""
symbols = prices.columns.tolist()
results = []
for i, a in enumerate(symbols):
for b in symbols[i+1:]:
try:
test = ArbitrageEngine.cointegration_test(prices[a], prices[b])
if test["cointegrated"]:
hr = ArbitrageEngine.hedge_ratio(prices[a], prices[b])
results.append({"pair": f"{a}/{b}", **test, **hr})
except: continue
return sorted(results, key=lambda x: x["p_value"])
1---2name: cross-asset-arbitrage-engine3description: Statistical arbitrage, triangular arbitrage, basis trades, and convergence detection across instruments. Use this skill whenever the user asks about "arbitrage", "stat arb", "pairs trading", "triangular arbitrage", "convergence trade", "mean reversion pair", "cointegration", "basis trade", "spread trading", "relative value", "mispricing detection", or any cross-asset relative value strategy. Works with pair-correlation-engine and mt5-chart-browser.4---56# Cross-Asset Arbitrage Engine78```python9import numpy as np10import pandas as pd11from statsmodels.tsa.stattools import coint, adfuller1213class ArbitrageEngine:1415 @staticmethod16 def cointegration_test(series_a: pd.Series, series_b: pd.Series) -> dict:17 """Test if two series are cointegrated (mean-reverting spread)."""18 score, pvalue, _ = coint(series_a.dropna(), series_b.dropna())19 return {20 "cointegrated": pvalue < 0.05,21 "p_value": round(pvalue, 4),22 "test_stat": round(score, 4),23 "signal": "COINTEGRATED — pairs trade viable" if pvalue < 0.05 else "NOT cointegrated — avoid pairs trade",24 }2526 @staticmethod27 def hedge_ratio(series_a: pd.Series, series_b: pd.Series) -> dict:28 """OLS hedge ratio for pairs trade construction."""29 from numpy.polynomial.polynomial import polyfit30 b, a = np.polyfit(series_b, series_a, 1)31 spread = series_a - b * series_b32 adf_stat, adf_p, *_ = adfuller(spread.dropna())33 return {34 "hedge_ratio": round(b, 6),35 "intercept": round(a, 6),36 "spread_stationary": adf_p < 0.05,37 "spread_adf_p": round(adf_p, 4),38 "entry_rule": f"Buy A, sell {abs(b):.4f} B when z-score < -2. Reverse when z-score > 2.",39 }4041 @staticmethod42 def triangular_arb_check(rates: dict) -> dict:43 """44 Check for triangular arbitrage opportunity.45 rates: {"EURUSD": 1.0850, "GBPUSD": 1.2650, "EURGBP": 0.8570}46 """47 try:48 eurusd = rates["EURUSD"]49 gbpusd = rates["GBPUSD"]50 eurgbp = rates["EURGBP"]51 # Path 1: USD → EUR → GBP → USD52 implied_eurgbp = eurusd / gbpusd53 arb_1 = (implied_eurgbp / eurgbp - 1) * 10000 # in pips54 # Path 2: USD → GBP → EUR → USD55 implied_eurusd = eurgbp * gbpusd56 arb_2 = (implied_eurusd / eurusd - 1) * 1000057 return {58 "implied_eurgbp": round(implied_eurgbp, 5),59 "actual_eurgbp": eurgbp,60 "arb_pips": round(arb_1, 1),61 "opportunity": abs(arb_1) > 2,62 "direction": "Buy EURGBP" if arb_1 < -2 else "Sell EURGBP" if arb_1 > 2 else "No arb",63 "note": "Account for spread + execution latency. Sub-2pip arbs rarely executable.",64 }65 except KeyError:66 return {"error": "Need EURUSD, GBPUSD, EURGBP rates"}6768 @staticmethod69 def spread_z_score_signals(spread: pd.Series, window: int = 60,70 entry_z: float = 2.0, exit_z: float = 0.5) -> pd.DataFrame:71 """Generate entry/exit signals from spread z-score."""72 mean = spread.rolling(window).mean()73 std = spread.rolling(window).std()74 z = (spread - mean) / std.replace(0, np.nan)75 signals = pd.DataFrame(index=spread.index)76 signals["z_score"] = z77 signals["signal"] = 078 signals.loc[z < -entry_z, "signal"] = 1 # Buy spread79 signals.loc[z > entry_z, "signal"] = -1 # Sell spread80 signals.loc[z.abs() < exit_z, "signal"] = 0 # Exit81 return signals8283 @staticmethod84 def scan_cointegrated_pairs(prices: pd.DataFrame, max_pvalue: float = 0.05) -> list[dict]:85 """Scan all pair combinations for cointegration."""86 symbols = prices.columns.tolist()87 results = []88 for i, a in enumerate(symbols):89 for b in symbols[i+1:]:90 try:91 test = ArbitrageEngine.cointegration_test(prices[a], prices[b])92 if test["cointegrated"]:93 hr = ArbitrageEngine.hedge_ratio(prices[a], prices[b])94 results.append({"pair": f"{a}/{b}", **test, **hr})95 except: continue96 return sorted(results, key=lambda x: x["p_value"])97```9899100---