Selected work

Quantitative Finance / Risk

JSE Portfolio Risk: Out-of-Sample Tests

Portfolio rules, VaR models and stress tests for JSE equities, checked on data they did not see

A rebuild of an R Shiny portfolio app that picked weights from 5,000 random draws by in-sample Sharpe ratio. Six construction rules, four value-at-risk models and a set of stress scenarios were fixed on 2008 to 2016, tested once on 2017 to September 2026 against pass marks written down beforehand, and have been scored live since 1 October 2026.

−0.09

Original method Sharpe

14.8%

Min-variance volatility

1 of 4

VaR models passed

Validation backtest, 2017 to September 2026 · after costs

Interactive previewOpen full screen ↗

Prices refresh after each JSE close. The portfolio builder fits weights, VaR and stress tests on the server for any set of shares.

Problem

Optimised portfolios and risk numbers look precise when they are estimated and judged on the same data. The original app chose weights with hindsight, compounded log returns as if they were simple returns, and measured shortfall against a target rather than a loss, so its outputs said little about how a portfolio would actually behave.

What I built

Every rule now rebalances at each month end using only prices up to that day, holds until the next month end while weights drift, and pays 10 bps a side plus 0.25% securities transfer tax on purchases. The universe is the original app's 267 tickers, narrowed each month to the 40 most traded. The design period chose each rule's lookback; the validation period was run once, and Sharpe ratio differences were tested with a HAC test rather than read off the table. A Python service on Azure refreshes prices from Yahoo Finance, extends the live record and fits portfolios on request.

Methods

  • Rules: the original random-weights method, equal weight, inverse volatility, minimum variance, equal risk contribution and maximum Sharpe, with Ledoit-Wolf covariance and a 10% cap on any share
  • Benchmarked against the Satrix 40 ETF, with the three-month Treasury bill rate from FRED as the risk-free rate
  • One-day 99% and 97.5% VaR and expected shortfall from historical simulation, a normal model, EWMA and filtered historical simulation on a GJR-GARCH model with Student t errors refitted every month
  • VaR backtests: Kupiec unconditional coverage, Christoffersen independence and conditional coverage, the Basel traffic light on rolling 250-day windows, and the Acerbi-Szekely Z2 test for expected shortfall
  • Stress tests: the 2008 crisis, December 2015, Covid and the April 2025 tariff shock replayed on current holdings, plus correlations at 0.9, doubled volatility, a 20% fall in the rand and the largest holding going to zero
  • A monthly job downloads fresh prices, reruns the pipeline and redeploys the dashboard

Technical architecture

  • Python research pipeline writes the backtests, risk-model tests and stress results as JSON
  • FastAPI service shares the same library, serves the live record since 1 October 2026 and runs the portfolio builder on Azure App Service
  • React dashboard in this site's typography and palette, embedded here and usable on its own

Results

  • Minimum variance did what it is meant to: 14.8% annual volatility against 18.5% for equal weight and 18.8% for the Satrix 40. It also returned −0.6% a year, and its Sharpe ratio was significantly below equal weight's (t = −2.94)
  • Maximum Sharpe was the best rule, 8.4% a year with a Sharpe ratio of 0.18 against 0.09 for equal weight, but the difference is indistinguishable from noise (t = 0.42)
  • The original app's method returned 3.1% a year (Sharpe −0.09), significantly worse than equal weight (t = −2.36), with about six times the turnover
  • No rule beat the Satrix 40, which returned 9.3% a year (Sharpe 0.22)
  • Only filtered historical simulation passed the VaR and ES backtests, for all three portfolios tested: 25 breaches of the 99% VaR for equal weight against 24 expected. The normal, EWMA and plain historical models all failed, mostly by breaching too often and in clusters
  • Live figures since 1 October 2026 are in the preview. With weeks of unseen data they are not yet a meaningful test

Research context

  • All results are backtests or a live paper record, not realised performance or investment advice.
  • The universe is today's list of tickers, so shares that delisted earlier are missing. This survivorship bias flatters every rule.

Technology

  • Python
  • NumPy
  • SciPy
  • scikit-learn
  • arch
  • FastAPI
  • React
  • Yahoo Finance
  • FRED
  • Azure

Links