AmiBroker Parity Test
Nick Final Code v1.1A trade-by-trade comparison between our backtesting engine and AmiBroker running the same strategy. The goal is to verify that entry dates, entry prices, exit dates, and exit prices agree — confirming that the engine's signal evaluation, execution timing, and hold period are all correct.
Universe
SP500 NOSURVIVOR
1,289 tickers, point-in-time
Period
2020–2021
2020-01-01 → 2021-06-30
Round Trips (engine)
…
738 fills total
Positions / Size
4 × 25%
$100k initial, 0% commission
Bar Mapping: Engine vs AmiBroker
With SetTradeDelays(1,1,1,1), AmiBroker evaluates signals on bar i and executes trades on bar i+1. Bar i is therefore identical to our engine's ranking_date = T-1. This is why no Ref(-1) is needed — the delay already provides the one-bar lag.
Why no Ref(-1) on PositionScore or Buy conditions
The original AFL had PositionScore = Ref(ROC(Close,60), -1) to avoid look-ahead. But with delays=1, the signal bar already is T-1 — the delay provides the lag automatically. Adding Ref(-1) on top creates T-2 data, one bar staler than our engine. The parity AFL removes it so that both systems rank on the same T-1 data.
Nick's AmiBroker Instructions
Steps
- 1Open AmiBroker with your Norgate data subscription active.
- 2Create a new formula (Analysis → Formula Editor) and paste the AFL code on the right.
- 3In Analysis → Settings, set Periodicity to Daily.
- 4Set the backtest date range: From 2020-01-01 To 2021-06-30.
- 5Set the watchlist/database to the SP500 Norgate database (historical constituents). Make sure UseHistorical is on in the AFL — it is set in the code.
- 6Verify in Analysis → Settings: Commission = $0, backtestRegularRaw mode (set in code).
- 7Click Backtest and wait for it to complete.
- 8In the Results pane, switch to the Trades tab. Right-click → Export → save as ami_trades.csv. Required columns: Symbol, Entry Date, Entry Price, Exit Date, Exit Price.
- 9Send back: ami_trades.csv. We will run the diff script and compare it to the engine fills.
Parity AFL (paste into AmiBroker)
// ============================================================
// PARITY AFL: Nick Final Code v1.1 → api-v2 engine match
// Backtest 600: SP500 historical constituents, $SPX (Norgate)
// 2020-01-01 → 2021-06-30 | $100k | 4 positions | 25% each
// Commission: 0 | Slippage: 0 | Round lots
//
// delays=1 → signal bar = T-1 (matches engine's ranking_date)
// no Ref(-1) → indicators computed at signal bar = correct T-1 data
// BuyPrice=O → entry at next-bar open = engine's Open[T]
// ApplyStop(2,True) → exit at Close[T+2] = engine's max_hold_days=2
// ============================================================
SetBacktestMode( backtestRegularRaw );
SetTradeDelays( 1, 1, 1, 1 );
SetOption( "InitialEquity", 100000 );
SetOption( "MaxOpenPositions", 4 );
SetOption( "CommissionMode", 3 );
SetOption( "CommissionAmount", 0.00 );
SetOption( "AllowSameBarExit", False );
SetOption( "PriceBoundChecking", False );
SetOption( "UsePrevBarEquityForPosSizing", True );
SetPositionSize( 25, spsPercentOfEquity );
RoundLotSize = 1;
BuyPrice = O;
SellPrice = O;
// ── Universe: point-in-time SP500 constituents via Norgate ───
#include_once "Formulas\Norgate Data\Norgate Data Functions.afl"
inindex = NorgateIndexConstituentTimeSeries( "$SPX" );
Buy = Sell = Short = Cover = 0;
// ── Ranking: ROC(60) at signal bar (= T-1 data) ─────────────
// No Ref(-1) here. With delays=1, the signal bar IS T-1.
// Adding Ref(-1) would compute at T-2, one bar staler than our engine.
PositionScore = ROC( Close, 60 );
// ── Entry filters (T-1 data — signal bar) ────────────────────
priceReq = Close > 5;
BuyCondition = RSI( 2 ) <= 10
AND Close < MA( Close, 20 )
AND priceReq
AND inindex;
Buy = ExRem( BuyCondition, Sell );
// ── Time exit: Close[T+2] matches max_hold_days=2 ────────────
// ApplyStop with ExitAtStop=True exits at close of bar E+2
// where E is the execution bar (= T). N=2 → Close[T+2]. ✓
// Do NOT use the original loop — it exits at Open[T+3].
ApplyStop( stopTypeNBar, stopModeBars, 2, True );What We're Comparing
Each row maps an engine behaviour to its AmiBroker equivalent
| Dimension | Our Engine | AmiBroker (parity AFL) | Expected |
|---|---|---|---|
| Signal evaluation date | ranking_date = T-1 (prior bar close) | Signal bar i — same calendar day as T-1 | Identical day |
| Ranking indicator | momentum: ROC(close, 60) at T-1 | PositionScore = ROC(Close, 60) at bar i | Same value (no Ref lag) |
| Entry condition | RSI(2)≤10 AND close_vs_sma(20)<0 AND close>5 | RSI(2)≤10 AND Close<MA(Close,20) AND Close>5 | Same signal |
| Entry execution | Open[T] (rebalance day open) | Open[i+1] with delays=1, BuyPrice=O | Same price |
| Hold period | max_hold_days=2 (bars_held≥2) | ApplyStop(stopTypeNBar, stopModeBars, 2, True) | 2 full bars after entry |
| Exit price | Close[T+2] via _get_current_prices() | Close of bar i+3 (ExitAtStop=True) | Same close price |
| Commission | 0% (commission_pct=0) | CommissionAmount=0.00 | Identical |
| Position size | 25% of prev-bar equity ÷ open price | SetPositionSize(25, spsPercentOfEquity) + UsePrevBarEquityForPosSizing=True | ≈ same shares (rounding may differ) |
| Universe membership | Point-in-time via membership CSV | NorgateIndexConstituentTimeSeries("$SPX") | Mostly same — some divergence possible |
Engine Fills — First 20 Rows
From backtest 600 validation export. These are the reference rows Nick's export should match.
Mismatch Interpretation Guide
| What you see in the diff | Most likely cause | How to diagnose |
|---|---|---|
| Entry price differs by a few cents | Rounding or open-price source difference | Compare Open column in Norgate data vs our parquet source for that date |
| Entry price differs by >0.5% | BuyPrice not set to O in AFL, or delays mismatch | Verify BuyPrice=O and SetTradeDelays(1,1,1,1) are in effect |
| Exit date off by exactly +1 bar | ApplyStop interacting with delays (exits at Open[T+3] not Close[T+2]) | Switch to SetTradeDelays(1,1,0,0) + SellPrice=C + DaysInMarket=3 in loop |
| Exit price differs, same exit date | AFL using Open vs engine using Close | Verify ExitAtStop=True in ApplyStop call; check SellPrice in trade report |
| Trade in engine, missing from AmiBroker | Stock not in Norgate $SPX on that date, or delisted stock excluded | Check if ticker was in $SPX constituent list on entry date via Norgate |
| Trade in AmiBroker, missing from engine | Entry signal passed AFL but not engine (e.g. min_price edge case at open) | Check that day's Open price vs $5 filter; our engine checks prior close |
| Shares differ by ≤1 share | Integer rounding difference (round lot) | Acceptable — both engines use floor division. No action needed. |
| Trade count differs by >10% | Universe composition mismatch (Norgate $SPX ≠ our SP500_NOSURVIVOR) | Expected — Norgate $SPX is point-in-time for the live index; our set includes more expired tickers |
| All trades off by same N calendar days | Date range not set to exactly 2020-01-01 in AmiBroker | Verify Analysis range start date is 2020-01-01, not 2020-01-02 or similar |
Running the Diff (once Nick sends ami_trades.csv)
# 1. Download engine fills curl "https://thalosx.duckdns.org/api/backtests/600/validation-export/fills" \ -H "Authorization: Bearer 0822f0e129705582eb1e0f616d3e4649f41da69476653bcea6e9576ba14eb06e" \ -o engine_fills_600.csv # 2. Run the diff python3 apps/api-v2/diff_ami_fills.py \ --ami ami_trades.csv \ --engine engine_fills_600.csv \ --price-tol 0.01 \ --out diff_600 # Output: diff_600_mismatches.csv # diff_600_missing_from_ami.csv # diff_600_extra_in_ami.csv