Rank liquid S&P/ASX 200 stocks by risk-adjusted 12-month momentum, hold the top 20 in equal weight, rebalance monthly, and go to cash whenever the index is below its 200-day average. One person, one small VPS, Python, an Interactive Brokers paper account. Every rule below was argued out in the GAIOS Brainroom and every number comes from a script you can download.
General information only — not personal financial advice, not a recommendation to buy or sell any security. Author: Sabour Khosravi, GoldNet Group. Use the Ask the agent button (bottom right) for verified answers, or the Hint buttons in each section for what to check next.
Growth of 1 unit (log scale) and drawdown from peak, monthly samples. Hover for values. Generated by asx_momentum.py backtest --capital 25000.
| Variant | CAGR | Sharpe (rf 0) | Max DD | Exposure | Turnover /mo | 2020 crash DD |
|---|---|---|---|---|---|---|
| v1 binary regime, AUD 25k, top 20 | 4.6% | 0.39 | −29.4% | 67% | 16% | −15.3% |
| v1 binary, AUD 50k | 5.5% | 0.44 | −27.9% | 67% | 16% | −15.3% |
| v1 binary, AUD 20k, top 15 @ 8% cap | 5.0% | 0.39 | −32.8% | 66% | 17% | — |
| Shadow: graded 50% when regime off, AUD 25k | 9.4% | 0.64 | −26.9% | 83% | 14% | −26.4% |
| No regime filter, AUD 25k | 13.9% | 0.77 | −38.5% | 99% | 12% | −39.0% |
| S&P/ASX 200 price index | 3.4% | 0.30 | −36.5% | 100% | — | −36.5% |
Read the deltas between rows, not the levels. The 25k vs 50k rows differ only by the AUD 6 minimum. The binary vs graded vs none rows differ only by what happens when the index is under its 200-day average: binary gives up return in recoveries (2012, 2019, 2023) to survive fast crashes (2020). The room chose survival and auditability; the graded variant is tracked in shadow so that choice can be revisited with live data rather than a biased backtest.
| Year | 2011 | 2012 | 2013 | 2014 | 2015 | 2016 | 2017 | 2018 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | 2026 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| v1 | -2.2% | -1.3% | +22.7% | -1.3% | -7.2% | +7.0% | +17.5% | -1.1% | +8.3% | -3.0% | +30.6% | -16.5% | -8.8% | +28.8% | +16.4% | +6.7% |
| ASX 200 price | -15.8% | +14.4% | +15.1% | +0.4% | -3.2% | +7.0% | +7.4% | -6.9% | +20.6% | -1.5% | +13.0% | -5.5% | +7.8% | +7.5% | +6.8% | +0.3% |
6-month lookback: no better, more turnover. Top 30: slightly higher Sharpe than top 20. Top 10: worse (Sharpe 0.43, deeper drawdown). Doubling costs: about −1 point of CAGR. Trailing 15% stop: CAGR 10.7% → 6.6%. Trailing 25%: 9.4%. Sector cap 40%: neutral. Full numbers in robustness.json below.
As of the close on 14 September 2026: S&P/ASX 200 at 8,741.2 vs 200-day SMA 8,819.0 → regime OFF → v1 book is 100% cash. The model's top 20 that day, for information only and not a recommendation: SGM, PLS, S32, WOW, NWH, BHP, BSL, RIO, AZJ, REH, LSF, SFR, GGP, MIN, MND, APA, IGO, DBI, WDS, DNL.
Top 10 constituents were 48.8% of the ASX 200 at 31 Aug 2026, dominated by banks and miners. Defence: 7.5% per-name cap, 20 names, concentration logged; sector cap flag available.
AUD 2m median daily dollar volume gate, AUD 1 price floor, monthly cadence, limit orders in the continuous session only.
Per-clip cost model, no drift trades, AUD 25k floor, hard AUD 500 minimum parcel in the executor.
Pre-open 07:00; opening auction from 09:59 in a randomised, letter-staggered sequence to ~10:09; continuous to 16:00; closing auction 16:10 ± 30 s. The bot never trades in an auction: sends at 10:20 and 10:35.
Sells first, buys second, sized to settled cash plus filled proceeds; 3% cash buffer; margin-type account with zero borrowing so proceeds can be reused. A cash account must wait T+2.
Ranking uses dividend-adjusted prices. Franking credits are in no price series; they are an unmodelled, holder-dependent tax item, never counted as return. Nothing here is "franking-aware".
Suspended: held, excluded from ranking, sold after resumption unless re-ranked in. Splits: positions reconciled against IBKR quantities before every plan. Demerger entities and index-removed names: sold at the next rebalance.
Each name's last close is reconciled against the IBKR bar; >0.5% mismatch skips it for the month. Stale index (>3 days) or >10% of the universe failing → no plan, hold everything, alert. The regime flag cannot flip on missing data.
The system runs unattended; absence is not an override. One control: a PAUSE file. A missed send is re-run next day and logged; two missed rebalances in six months is a kill.
12-1 momentum has been public since Jegadeesh & Titman (1993) and an ASX ETF runs a related process at scale. At AUD 25–50k in names trading AUD 2m+ a day, the footprint is negligible.
.env chmod 600 under a dedicated user; IBKR API bound to 127.0.0.1; no keys in code or logs; account IDs redacted; Telegram token in .env only.
A binary filter sits in cash for months. It is designed to be hard. It is never relaxed after losses — that ruling is on the record.
Paper net expectancy ≤ 0 after 6 months · slippage > 2× the assumption · two missed rebalances · the regime filter failed to reduce drawdown versus the index during paper · any manual override.
ib_insync. Live orders need --live and ASX_LIVE=1 — the two-key rule.asx-momentum.timer 17:30 Sydney weekdays (refresh, signals, stage the monthly plan) and asx-momentum-send.timer 10:20 Sydney (send the staged plan). Idempotent.events.jsonl (HEARTBEAT, ORDER_SUBMITTED, ORDER_STATUS, RECON MISMATCH, plan summaries) and Telegram via notify.sh on plan staged, failures, mismatches, the drawdown rule and regime flips./opt/asx-momentum, create a venv, pip install yfinance pandas numpy pyarrow ib_insync requests lxml.python asx_momentum.py fetch (first download, a few minutes) then python asx_momentum.py backtest --capital 25000 — check your numbers match this page.python asx_momentum.py signals > data/signals_today.json then python ibkr_executor.py --signals data/signals_today.json (dry run) — read the plan by hand..env (chmod 600) with TG_TOKEN, TG_CHAT; run ./notify.sh hello and see it arrive.systemctl enable --now both timers. Watch the first nightly HEARTBEAT.score = (close.shift(21) / close.shift(252) - 1) / (daily_returns.rolling(252).std() * sqrt(252))
score = score.where(liquid & (mom > 0)) # AUD 2m median $vol, price ≥ 1, absolute leg
regime = index.ffill() > index.ffill().rolling(200).mean()
each month (first trading day, signals from the prior close):
if not regime: book = {} # 100% cash, no exceptions
else: keep = held ∩ top40; book = (keep + top20)[:20]; w = min(1/20, 7.5%)
skip any trade < max(AUD 500, 1% NAV) # no drift trades
cost per clip = max(0.08% × value, AUD 6) + 0.10% # IBKR AU fixed + slippage
orders: DAY limit, continuous session only; sells 10:20, buys 10:35
The design was put to the GAIOS Brainroom (session bs-3824e276045e) on 14 September 2026 with a blind first round: Claude chaired, Grok and Codex took sealed positions, then a critique round and a red-team round. Twenty-six objections were raised and every one was ruled with evidence or an owner. The chair reversed its own initial preference for a graded regime filter after the 2020 crash test; the trailing stop, sector cap and cost model all changed because a member objected and the numbers agreed.
| Ruling | Decision |
|---|---|
| D1–D3 | ASX 200 universe with liquidity gate; risk-adjusted 12-1 signal; top 20 equal weight, 7.5% cap, keep-in-top-40 buffer. |
| D4 (reversed) | Binary index regime filter; graded 50% is a pre-registered shadow variant only. |
| D5 | Monthly; prior-close signals; limit orders in the continuous session; never auctions. |
| D8–D9 | No trailing stop; 20% portfolio drawdown review rule; sector cap flag off, concentration logged. |
| D10 | Six-month paper gate with pre-registered thresholds and kill criteria. |
| D11–D12 | AUD 25k practical floor; per-clip IBKR cost model; no drift trades. |
| D13–D16 | Unattended operation and PAUSE file; corporate-action rules; T+2 sequencing; data-integrity kill. |
| D17–D20 | Crowding accepted and labelled; secrets checklist; absolute leg explicit; non-script figures labelled "assumption". |
Dissent on record: Grok holds that v1's edge may be thin after live costs and that the point-in-time walk-forward may not pass. The design agrees that is exactly what the gate exists to find out.
Five questions the agent will also ask you. Click to reveal the ruling.