AUD/NZD sized by the causal SOI z-score

Bet the Australian dollar against the New Zealand dollar according to the Southern Oscillation Index, since the same weather tends to be wet on one side of the Tasman and dry on the other. Small, and inseparable from luck.

Conclusion: Failed

Failed on the reward for the risk, the confidence range, profit concentration and standing out from the search. Indicative performance of 2.0% a year at 6.5% volatility, return for risk taken 0.31 (95% range (0.11) to 0.71).

Figure 1 · Growth of $1

2004–2026 · net of 15bp round-trip costs
What one dollar became, after costs: $1.58 by 2026. Shown against S&P 500, rebased to the same starting dollar.
Return for risk
0.31
range (0.11) to 0.71
Ann. return
2.0%
volatility 6.5%
Max drawdown
29.3%
over 269 months
Beats the search
4.4%
chance, after correction

Method

Hypothesis

The single cleanest FX expression of ENSO: the same anomaly wets eastern Australia and empties the South Island hydro lakes, so the two economies take the shock with opposite sign and the cross nets out most of the USD noise.

Rule

PAIR = AUDNZD
function positions(bar):
z = zscore(feature(soi_3m), expanding, at least 120 months)
return { PAIR: clip(z / 2, -1, 1) }
# With one instrument, scale_to_gross() reduces any non-zero weight to
# the same size, so only the sign of z affects the result.
# ── execution ────────────────────────────────────────────────────────
COST_PER_TRADE = 15 bps of the notional that changes hands
function on_bar(bar, book):
if not bar.is_month_end:
return # decisions are made monthly only
target = positions(bar) # the rule above
target = scale_to_gross(target, 1.0)
rebalance(book, target, bar)
function scale_to_gross(w, limit):
gross = sum(abs(w)) # total capital at work
if gross == 0:
return w # flat is a valid target
return w * limit / gross # leverage fixed, never implicit
function rebalance(book, target, bar):
for symbol in union(book.symbols, target.symbols):
delta = target[symbol] - book.weight(symbol)
if delta > 0:
buy(symbol, delta, fill = next_bar.close)
else if delta < 0:
sell(symbol, -delta, fill = next_bar.close)
book.charge(COST_PER_TRADE * abs(delta))
# Orders are filled at the next month's close, never the one the decision
# was made on. There is no stop_loss(), take_profit(), limit order or
# position cap: a holding changes only when positions() returns a
# different target at the next month end.

Evaluation

Table 1 · Performance

2004-02 – 2026-06 · 269 months, 269 invested
MeasureValue
Annualised return2.0%
Annualised volatility6.5%
Return for risk taken0.31
95% range(0.11) to 0.71
t-statistic1.49
Maximum drawdown29.3%
Hit rate53.2%
Annual turnover2.90×
Return skew(0.10)
Return kurtosis3.00
The range is measured by resampling the history in blocks, so runs of good and bad months stay intact.

Table 2 · Robustness

DiagnosticValueReads asBadOkayYayHmm
Design half (pre-2015)0.44return for risk≤ 00 – 0.40≥ 0.40
Holdout half (2015+)0.16return for risk≤ 00 – 0.40≥ 0.40
Top-5-month share of profit49.8%how much rode on a few months — lower is better≥ 40%25 – 40%< 25%
Top-2-year share of profit73.2%concentration
Distinct trading episodes1how many independent runs this really is< 5050 – 100≥ 100
Chance the edge is real93.0%non-normality corrected
Chance it beats the whole search4.4%chance of beating the whole search by luck
bad/ok boundary is the pre-registered criterion · ok/good is a margin above it, not a criterion · blank where none was registered
The last row asks whether the result would stand out from the hundred-odd ideas tried here, rather than being judged alone.

Table 3 · Pre-registered criteria

measured against (flat)
1FAILED2FAILED3FAILED4PASS5PASS6FAILED
CheckWhat it asksResult
1Reward large enough for the riskFAIL
2Confidence range clear of zeroFAIL
3Profit not concentrated in a few monthsFAIL
4Worked in both halves of the historypass
5Beat buying and holdingpass
6Stood out from the whole searchFAIL
These six checks were written down before the strategy was run.

Table 4 · Net return by calendar year

after costs
20042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026+24%-24%
YearNet return
20042.2%
2005(1.2%)
200624.0%
20075.0%
20086.5%
20099.5%
20105.4%
2011(0.2%)
2012(3.3%)
2013(15.4%)
20143.2%
2015(1.8%)
2016(1.9%)
20178.1%
20183.0%
2019(0.9%)
2020(4.7%)
2021(0.4%)
20220.5%
20230.8%
2024(0.2%)
20254.7%
20262.7%

Table 5 · Concentration detail

MeasureValue
Best years2006:+24%, 2009:+9%, 2017:+8%
Worst year2013:-15%

Reference

Instruments

last price and one-month change, live from Yahoo Finance
SymbolNotes
AUD/NZDAUD/NZD — spot exchange rate.

Sample

2004-02 – 2026-06 · 269 months

Sources

NOAA Physical Sciences Laboratory. Climate indices. ONI, Niño 3.4, MEI v2, DMI, PDO, AMO, TNA, QBO, solar flux. psl.noaa.gov/data/climateindices/list
Queensland Department of Agriculture and Fisheries. Southern Oscillation Index. The Long Paddock. Monthly, 1876–. www.longpaddock.qld.gov.au/soi/soi-data-files
Yahoo Finance. Historical market prices. Adjusted close, month-end. Retrieved with yfinance. finance.yahoo.com

Revision history

RevStageStatusChange
01DraftDirection and criteria fixed before the experiment ran
02TestedFailedFell short on the reward for the risk, the confidence range, profit concentration, standing out from the search; control (flat)