← ZebraTrader home

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

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


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