AmiBroker Parity Test

Nick Final Code v1.1

A 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.

Monday
Tuesday
Wednesday
Thursday
Friday
Engine
T-1
Signal eval
ROC(60), RSI(2), MA(20)
Engine
T
Entry
Open[T]
Engine
T+1
bars_held = 1
no exit
Engine
T+2
bars_held = 2
Exit Close[T+2]
Engine
AmiBroker
bar i
Signal bar
Buy=1 set here
AmiBroker
bar i+1
Execution
Open[i+1]
AmiBroker
bar i+2
Hold day 1
no exit
AmiBroker
bar i+3
ApplyStop N=2
Exit Close[i+3]
AmiBroker
T-1 = bar i
Open[T] = Open[i+1]
No exit either
Close[T+2] = Close[i+3]

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

Send this section to Nick

Steps

  1. 1Open AmiBroker with your Norgate data subscription active.
  2. 2Create a new formula (Analysis → Formula Editor) and paste the AFL code on the right.
  3. 3In Analysis → Settings, set Periodicity to Daily.
  4. 4Set the backtest date range: From 2020-01-01 To 2021-06-30.
  5. 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.
  6. 6Verify in Analysis → Settings: Commission = $0, backtestRegularRaw mode (set in code).
  7. 7Click Backtest and wait for it to complete.
  8. 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.
  9. 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

DimensionOur EngineAmiBroker (parity AFL)Expected
Signal evaluation dateranking_date = T-1 (prior bar close)Signal bar i — same calendar day as T-1Identical day
Ranking indicatormomentum: ROC(close, 60) at T-1PositionScore = ROC(Close, 60) at bar iSame value (no Ref lag)
Entry conditionRSI(2)≤10 AND close_vs_sma(20)<0 AND close>5RSI(2)≤10 AND Close<MA(Close,20) AND Close>5Same signal
Entry executionOpen[T] (rebalance day open)Open[i+1] with delays=1, BuyPrice=OSame price
Hold periodmax_hold_days=2 (bars_held≥2)ApplyStop(stopTypeNBar, stopModeBars, 2, True)2 full bars after entry
Exit priceClose[T+2] via _get_current_prices()Close of bar i+3 (ExitAtStop=True)Same close price
Commission0% (commission_pct=0)CommissionAmount=0.00Identical
Position size25% of prev-bar equity ÷ open priceSetPositionSize(25, spsPercentOfEquity) + UsePrevBarEquityForPosSizing=True≈ same shares (rounding may differ)
Universe membershipPoint-in-time via membership CSVNorgateIndexConstituentTimeSeries("$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.

Download all 738 rows
Loading fills…

Mismatch Interpretation Guide

What you see in the diffMost likely causeHow to diagnose
Entry price differs by a few centsRounding or open-price source differenceCompare 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 mismatchVerify BuyPrice=O and SetTradeDelays(1,1,1,1) are in effect
Exit date off by exactly +1 barApplyStop 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 dateAFL using Open vs engine using CloseVerify ExitAtStop=True in ApplyStop call; check SellPrice in trade report
Trade in engine, missing from AmiBrokerStock not in Norgate $SPX on that date, or delisted stock excludedCheck if ticker was in $SPX constituent list on entry date via Norgate
Trade in AmiBroker, missing from engineEntry signal passed AFL but not engine (e.g. min_price edge case at open)Check that day&apos;s Open price vs $5 filter; our engine checks prior close
Shares differ by ≤1 shareInteger 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 daysDate range not set to exactly 2020-01-01 in AmiBrokerVerify 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