GLD / XAUTZAR Pair Simulation — 3-Month Analysis
Period: 2026-03-28 → 2026-06-28 (45 aligned trading days)
Instruments: GLD (NewGold ETF, JSE via StandardBank) × XAUTZAR (spot gold in ZAR via VALR)
Script: zebra/utilities/gld_xautzar_analysis.py
HTML report: output/gld_xautzar_analysis.html / s3://zebratrader.com/gld_xautzar_analysis.html
Data Sources
| Leg | Source | Unit in DB | Normalisation |
|---|---|---|---|
| GLD | Local SQLite candles, StandardBankOst, resolution=1d |
ZAR-cents | ÷100 → ZAR |
| XAUTZAR (simulation) | Yahoo Finance: gold futures (GC=F) × USDZAR=X | ZAR | none |
| XAUTZAR (validation) | VALR candles in DB, resolution=1d | ZAR | none |
VALR only had 8 valid (non-stale) actual XAUTZAR data points in this window — data loader started 2026-05-23. The synthetic Yahoo Finance series tracks within ~1% of actual VALR prices for the validated period.
Strategy Configuration (mirrors config/live_gld_xaut.json)
| Parameter | Value |
|---|---|
| Trade size | R20,000 notional |
| Model | SpreadZScore |
| Lookback | 20 days |
| Entry |z| threshold | > 1.5σ |
| Exit z threshold | 0.0 (mean-reversion) |
| GLD fee per side | R57.50 (SB base fee) |
| XAUTZAR taker fee | 0.2% of notional (VALR) |
| Long GLD interest p.a. | 13.5% |
| Short GLD interest p.a. | 11.5% |
| Hedge ratio (OLS β) | 0.00897 |
| Live config ratio | 0.009 (nearly exact match) |
Hedge Ratio Calibration
OLS regression of GLD (ZAR) on XAUTZAR (ZAR) gives β = 0.008968.
The natural price ratio mean(GLD) / mean(XAUTZAR) = 0.009229.
The live config uses hedging_ratio = 0.009 — well-calibrated.
GLD represents 1/100th of a troy ounce of gold. At GLD ≈ R619 and XAUTZAR ≈ R67,595:
β = 619 / 67,595 ≈ 0.00916 (approx, varies daily)
OLS β = 0.00897 (accounts for persistent ETF discount vs spot)
The ~5–8% persistent discount of GLD vs synthetic XAUTZAR is typical ETF tracking error (management fees, JSE/VALR bid-ask spread, settlement lag).
Cointegration Results
| Test | Statistic | p-value | Result |
|---|---|---|---|
| ADF (spread stationarity) | — | 0.000 | Stationary ✓ |
| Engle-Granger cointegration | — | 0.000 | Cointegrated ✓ |
Both tests pass at the 1% significance level. The spread GLD − β·XAUTZAR is mean-reverting over this period, which is the fundamental requirement for a pairs strategy to work.
Spread statistics: mean = 19.52, std = 5.43.
Simulation Results
| Metric | Value |
|---|---|
| Trades executed | 3 |
| Wins | 2 |
| Win rate | 66.7% |
| Total return | +0.96% |
| Final equity | R20,191 (from R20,000) |
| Avg net P&L per trade | R63.82 |
| Total fees paid | R578.44 |
| Net interest | −R15.89 |
| Sharpe ratio (annualised) | 1.65 |
| Max drawdown | −0.78% |
Key Observations
1. Calibration is correct
The OLS-fitted β (0.00897) matches the live config's hedging_ratio = 0.009 to within 0.3%. The spread normalises correctly once GLD cents are divided by 100. No unit mismatch in the live path (fixed in the ZAR consistency audit, commit 81ff250).
2. Trade frequency is low
3 trades in 45 days reflects the conservative 1.5σ entry threshold on a 20-day lookback. With only 45 data points, the rolling window needs 20 bars to warm up, leaving 25 bars of tradeable signal. Entry z = 1.2σ would likely generate 5–7 trades on the same data.
3. Fees are the main drag
Gross PnL ≈ R772, fees ≈ R578 (75% of gross). Fee structure per round trip: - GLD: 2 × R57.50 = R115 - XAUTZAR: 2 × 0.2% × (0.00897 × entry_xautzar × gld_units) ≈ R77 per trade
Increasing trade size from R20,000 to R50,000 would reduce fee drag from ~3.8% to ~1.5% per round trip, as GLD's flat fee (R57.50) doesn't scale with size.
4. Interest cost is negligible at this holding period
Total interest cost of −R15.89 across 3 trades. At 13.5% p.a., the daily cost on R20,000 is ~R7.40/day. Trades held for 1–3 days incur R7–22 in interest — small relative to the spread move captured.
5. Sharpe of 1.65 is strong for a 45-day window
Annualised Sharpe of 1.65 is statistically meaningful but should be treated cautiously given only 3 completed trades. A 6-month window with more trades would give higher confidence.
Improvement Levers
| Change | Expected impact |
|---|---|
| Increase trade size R20k → R50k | Reduce fee drag from ~3.8% to ~1.5% per round trip |
| Lower entry z from 1.5 → 1.2 | More trades, lower avg entry quality — test carefully |
| Shorten lookback from 20 → 15 days | Faster z-score adaptation, earlier entries |
| Enable VALR 10× leverage | Amplify returns without scaling flat GLD fee |
| Add momentum filter (velocity_filter=true) | Avoid entering into trending spread moves |
| Extend XAUTZAR history | Data loader only running since 2026-05-23; more actual data improves backtesting confidence |
Architecture Notes
Cross-institution mechanics
- GLD is traded via SB Ruby gateway (
run_sb_server.shon port 4567) - XAUTZAR is traded via VALR REST API (no gateway required, Python adapter)
- Both legs run in the same container — VALR only needs
requests, already inrequirements-lambda.txt - Live cron triggers both legs atomically per tick via
run_live_cron.sh
Unit flow (end-to-end)
DB (GLD candles) → cents → ÷100 in SpreadZScore → ZAR
DB (XAUTZAR candles) → ZAR (stored directly by VALR adapter)
Spread = GLD_zar − β × XAUTZAR_zar ← both in ZAR, dimensionally consistent
Live state
State file: live_state/live_gld_xaut.json — persists run_id across cron ticks so multi-day positions remain linked in the DB.
Fees in live execution
Cfd.get_trade_fee now correctly handles both fee models:
- SB (GLD): taker_fee = 0 → falls back to base_fee = R57.50
- VALR (XAUTZAR): taker_fee = 0.002 → returns 0.002 × price × volume
Running the Analysis
# From project root
python3 zebra/utilities/gld_xautzar_analysis.py
# Outputs: output/gld_xautzar_analysis.html
# Uploads to: s3://zebratrader.com/gld_xautzar_analysis.html
Requires: yfinance, statsmodels, plotly, boto3 (all in requirements.txt, NOT in requirements-lambda.txt).
Related Files
| File | Purpose |
|---|---|
config/live_gld_xaut.json |
Live trading config (entry_z=1.5, lookback=20, ratio=0.009) |
zebra/utilities/gld_xautzar_analysis.py |
3-month simulation + HTML report generator |
zebra/institution/valr.py |
VALR broker adapter |
zebra/model/spread_z_score.py |
SpreadZScore model (normalises SB cents → ZAR) |
run_live_cron.sh |
Cron script — includes GLD/XAUTZAR as third pair |
data_loader.py |
Fetches XAUTZAR candles from VALR (VALR_SYMBOLS = ["XAUTZAR"]) |
doc/income_strategies.md |
Broader income strategy context including cross-institution carry |