Quantitative Finance / Systematic Research
Copula-GARCH Pairs Trading: JSE Banks
Standard Bank against its peers, tested on data the model has not seen
A pairs-trading study of South African banks. Returns are filtered through ARMA-GARCH models, linked with a Student t copula, and the strategy trades the conditional probability that Standard Bank has moved too far relative to its peers. Every rule was fixed on 2010 to 2019, validated once on 2020 to March 2026, and has been scored live on unseen sessions since 1 April 2026.
0.3%
Annualised CAGR
0.08
Annualised Sharpe
−15.1%
Max drawdown
Walk-forward backtest, 2010 to March 2026 · after costs
Prices refresh after each JSE close. Change the settings to rerun the backtest on the server; the fitted models stay as they were estimated.
Problem
South African banks move closely together, so a day where one moves much further than the others looks like an opportunity. Whether it is depends on each bank’s volatility at the time and on how strongly they co-move in the tails, and a backtest only means something if the model never sees the days it trades.
What I built
A rebuild of an honours group project that traded Standard Bank against Nedbank. The original report estimated and evaluated on the same data and ignored costs. This version refits on data up to each trading window, charges costs on every position change, and separates the data into a design period (2010 to 2019) for choosing rules, a validation period (2020 to March 2026) run once per rule against a pass mark written down beforehand, and live sessions since 1 April 2026 that are reported but never used to choose anything. A Python service on Azure pulls the latest prices from Yahoo Finance and VIX from FRED, filters them with the fitted models and reruns the backtest for whatever settings are chosen in the dashboard.
Methods
- Daily adjusted closes for Standard Bank, Nedbank, FirstRand, Absa, Capitec and Investec since 2005, with single-day bad prints removed
- For each series, 36 ARMA-GARCH candidates (GARCH, GJR or EGARCH; normal or Student t errors; with or without lagged VIX), chosen by BIC on training data only
- Copula family chosen by AIC on the probability transforms. Student t was selected in every fit
- Headline strategy: Standard Bank against an equal-weight basket of FirstRand, Absa, Nedbank and Capitec. Specifications chosen each December, parameters and copula re-estimated at every month end (202 fits), five-day hold after the conditional probability passes its 90th percentile
- Also tested: the original report rule against Nedbank, the best of 78 design-period variants, and two filters that skip trades on news days, one from trading volume and one from GDELT article counts queried on BigQuery
- Learned exits for the basket trade: on each held day, a logistic classifier and a fitted Q-iteration (reinforcement learning) model use GDELT coverage and tone, trading volume, volatility and the trade so far to decide whether to keep holding. Refitted yearly from 2016
- A rolling study of lag-1 autocorrelation across all 15 bank pairs, to see whether the reversal these strategies rely on is still there
- A position set at today’s close earns tomorrow’s return. The Python filters match the R estimates to within 1e-7
Technical architecture
- R research pipeline produces the fitted models, histories and terminal states as JSON
- FastAPI service runs the GARCH recursions and copula h-functions in NumPy, caches live prices and serves the backtest API from Azure App Service
- React dashboard in this site’s typography and palette, embedded here and usable on its own
Results
- The basket strategy passed validation, but only just: a Sharpe ratio of 0.06 after costs from 2020 to March 2026, against 0.08 in design. Before costs it was 0.64 in validation, so the signal held up but 10 bps per leg absorbs almost all of it
- Over the whole walk-forward period, 2010 to March 2026, it returned 0.3% a year after costs (Sharpe 0.08, 262 trades). Holding Standard Bank and the basket returned 10.5% a year
- The original report rule returned −0.9% a year (Sharpe −0.22). The best design variant and both news filters failed validation. The filters reduced losses mainly by trading less
- Both learned exits cleared their pass mark (validation Sharpe 0.08 and 0.14 against the basket rule’s 0.06), by margins too small to tell from chance (t = 0.24 and 0.68), and both did worse than the plain rule in design. The classifier did better without the news features than with them, so news and sentiment added nothing measurable
- The study shows why: across the 15 bank pairs, the average one-year lag-1 autocorrelation of return gaps was −0.16 in 2011 and close to zero from 2020. Next-day reversals after an extreme signal averaged about 16 bps before 2020 and turned slightly negative after
- Live figures since 1 April 2026 are shown above and in the preview. They change after each JSE close, and with only months of unseen data they are not yet a meaningful test
Research context
- Extends a BSc Honours group project with Matlhatse Makgopa. All results are backtests or a live paper record, not realised trading performance or investment advice.
- Slippage, short borrow fees and securities transfer tax are not modelled, so live trading costs would be higher than shown.
Technology
- R
- rugarch
- VineCopula
- Python
- FastAPI
- React
- scikit-learn
- BigQuery
- GDELT
- Yahoo Finance
- FRED
- Azure