Lab: Same Strategy, Three Position-Sizing Models
What this lab establishes
Section titled “What this lab establishes”Three backtests. Identical entry signals, identical exits, identical costs, identical stop, identical universe, identical date range. The only thing that differs is how large each position is.
By the end you will have three equity curves that look nothing like each other, produced by the same trading rules — and a clear answer to the question of how much of a system’s result comes from its entries.
Budget an hour: about fifteen minutes to run, and forty-five to look properly at what came back.
Prerequisites
Section titled “Prerequisites”- Risk per trade and stop distance
- Volatility-based position sizing
- Position sizing with
SetPositionSize()
The shared signal set
Section titled “The shared signal set”Complete runnable AFL
// lab-shared-signals.afl// Part 34 - Lab: Same Strategy, Three Position-Sizing Models//// The reference copy of everything the three sizing variants have in common:// the account, the execution assumptions, the costs, the universe filter, the// entry and exit rules, the ranking rule and the stop. Nothing here decides how// large a position is.//// Run this file as a SCAN to inspect the signals themselves. Do NOT backtest// it: with no position size set, the backtester puts 100% of the account into// the first symbol that signals and the result is meaningless. The three// sizing variants are the runnable backtests.//// The block between the two marker lines below is copied verbatim into// lab-sizing-fixed-shares.afl, lab-sizing-percent-equity.afl and// lab-sizing-atr-risk.afl. Compare them before you run anything: if the blocks// have drifted apart, the comparison is measuring the wrong thing.
// ===== SHARED BLOCK BEGIN - byte-identical in all four lab files ============//// ASSUMPTIONS - the same in every variant, so that the comparison is fair:// - Daily bars, split- and dividend-adjusted end-of-day data.// - Signals are read on the close of the signal bar. Every order fills at// the NEXT bar's open. SetTradeDelays(1,1,1,1) enforces that.// - Commission 0.1% of trade value, charged on entry and on exit.// - Slippage is NOT modelled. Every variant is flattered by the same amount.// - ExitAtStop = 1: stops are checked against the bar's High-Low range and// filled at the stop level. Optimistic, and optimistic identically in all// three variants, which is what keeps the comparison honest.// - Whole shares, one currency, no margin, long only.// - Starting capital 100,000 in whatever currency your database is in.
// ---- Account and execution ------------------------------------------------MaxPositions = 10;
SetOption( "InitialEquity", 100000 );SetOption( "MaxOpenPositions", MaxPositions );SetOption( "AllowPositionShrinking", True );SetOption( "CommissionMode", 1 ); // 1 = percent of trade valueSetOption( "CommissionAmount", 0.1 ); // 0.1% each waySetOption( "ActivateStopsImmediately", True );
SetTradeDelays( 1, 1, 1, 1 );BuyPrice = Open;SellPrice = Open;RoundLotSize = 1;
// ---- Universe filter ------------------------------------------------------MinTurnover = 2000000;Liquid = MA( Close * Volume, 50 ) > MinTurnover;
// ---- Signals --------------------------------------------------------------TrendPeriod = 200;EntryPeriod = 50;
Trend = Close > MA( Close, TrendPeriod );Buy = Cross( Close, MA( Close, EntryPeriod ) ) AND Trend AND Liquid;Sell = Cross( MA( Close, EntryPeriod ), Close );
// Ranking, used when more symbols signal on one bar than there are free slots.PositionScore = 100 - RSI( 14 );
// ---- The stop -------------------------------------------------------------// Three ATRs below the entry price, sampled on the signal bar and held for the// life of the trade. Identical in all three variants: the exit is not what is// being compared.AtrPeriod = 20;StopAtrMult = 3.0;MinAtrPct = 0.5;
UsableAtr = Max( ATR( AtrPeriod ), Close * MinAtrPct / 100 );StopDistance = StopAtrMult * UsableAtr;
// SetTradeDelays shifts Buy/Sell/Short/Cover and nothing else, so the stop// amount is shifted here by hand to make the value read on the entry bar the// one computed on the signal bar.StopAtEntry = Nz( Ref( StopDistance, -1 ), 1 );ApplyStop( stopTypeLoss, stopModePoint, StopAtEntry, 1 );//// ===== SHARED BLOCK END =====================================================This is the reference copy of everything the three variants have in common: the account, the execution assumptions, the costs, the universe filter, the entry and exit rules, the ranking rule and the stop. Nothing in it decides how large a position is.
Two details in the shared block that are easy to get wrong
Section titled “Two details in the shared block that are easy to get wrong”The stop amount is shifted by hand.
Fragment — not a complete formula
StopAtEntry = Nz( Ref( StopDistance, -1 ), 1 );ApplyStop( stopTypeLoss, stopModePoint, StopAtEntry, 1 );SetTradeDelays() shifts Buy, Sell, Short and Cover — and nothing else. So an array
computed on the signal bar is still lined up with the undelayed signal. Shifting it by hand makes
the value read on the entry bar the one that was computed on the signal bar, which is what you
meant.
The ATR has a floor.
Fragment — not a complete formula
UsableAtr = Max( ATR( AtrPeriod ), Close * MinAtrPct / 100 );Without it, a symbol whose ATR has collapsed to nearly zero produces a stop distance of nearly zero — and in model 3, a position size of nearly infinity. The floor is a modelling decision, and it is the kind that has to be made before the run rather than after it goes wrong.
Model 1: fixed shares
Section titled “Model 1: fixed shares”Complete runnable AFL
// lab-sizing-fixed-shares.afl// Part 34 - Lab: Same Strategy, Three Position-Sizing Models// SIZING MODEL 1 OF 3: A FIXED NUMBER OF SHARES//// Buy the same share count every time, whatever the share costs and whatever// the account is worth. The oldest sizing rule there is, and the one that puts// wildly different amounts of money and wildly different amounts of risk into// each position without ever saying so.//// Everything except the four lines under "SIZING MODEL 1" is shared with the// other two variants and with lab-shared-signals.afl.
// ===== SHARED BLOCK BEGIN - byte-identical in all four lab files ============//// ASSUMPTIONS - the same in every variant, so that the comparison is fair:// - Daily bars, split- and dividend-adjusted end-of-day data.// - Signals are read on the close of the signal bar. Every order fills at// the NEXT bar's open. SetTradeDelays(1,1,1,1) enforces that.// - Commission 0.1% of trade value, charged on entry and on exit.// - Slippage is NOT modelled. Every variant is flattered by the same amount.// - ExitAtStop = 1: stops are checked against the bar's High-Low range and// filled at the stop level. Optimistic, and optimistic identically in all// three variants, which is what keeps the comparison honest.// - Whole shares, one currency, no margin, long only.// - Starting capital 100,000 in whatever currency your database is in.
// ---- Account and execution ------------------------------------------------MaxPositions = 10;
SetOption( "InitialEquity", 100000 );SetOption( "MaxOpenPositions", MaxPositions );SetOption( "AllowPositionShrinking", True );SetOption( "CommissionMode", 1 ); // 1 = percent of trade valueSetOption( "CommissionAmount", 0.1 ); // 0.1% each waySetOption( "ActivateStopsImmediately", True );
SetTradeDelays( 1, 1, 1, 1 );BuyPrice = Open;SellPrice = Open;RoundLotSize = 1;
// ---- Universe filter ------------------------------------------------------MinTurnover = 2000000;Liquid = MA( Close * Volume, 50 ) > MinTurnover;
// ---- Signals --------------------------------------------------------------TrendPeriod = 200;EntryPeriod = 50;
Trend = Close > MA( Close, TrendPeriod );Buy = Cross( Close, MA( Close, EntryPeriod ) ) AND Trend AND Liquid;Sell = Cross( MA( Close, EntryPeriod ), Close );
// Ranking, used when more symbols signal on one bar than there are free slots.PositionScore = 100 - RSI( 14 );
// ---- The stop -------------------------------------------------------------// Three ATRs below the entry price, sampled on the signal bar and held for the// life of the trade. Identical in all three variants: the exit is not what is// being compared.AtrPeriod = 20;StopAtrMult = 3.0;MinAtrPct = 0.5;
UsableAtr = Max( ATR( AtrPeriod ), Close * MinAtrPct / 100 );StopDistance = StopAtrMult * UsableAtr;
// SetTradeDelays shifts Buy/Sell/Short/Cover and nothing else, so the stop// amount is shifted here by hand to make the value read on the entry bar the// one computed on the signal bar.StopAtEntry = Nz( Ref( StopDistance, -1 ), 1 );ApplyStop( stopTypeLoss, stopModePoint, StopAtEntry, 1 );//// ===== SHARED BLOCK END =====================================================
// ---- SIZING MODEL 1: FIXED SHARES ------------------------------------------// 200 shares is 4,000 in a share priced at 20 and 100,000 - the entire account -// in a share priced at 500. The rule does not know the difference. Watch the// Detailed log to see how often "position size shrinking" rescues it.FixedShares = 200;
SetPositionSize( FixedShares, spsShares );Fragment — not a complete formula
FixedShares = 200;SetPositionSize( FixedShares, spsShares );Buy the same share count every time, whatever the share costs and whatever the account is worth. The oldest sizing rule there is.
What it actually does: 200 shares is 4,000 currency units in a share priced at 20, and 100,000 — the entire account — in a share priced at 500. The rule has no idea. It puts wildly different amounts of money, and wildly different amounts of risk, into each position without ever saying so.
What to watch for: how often AllowPositionShrinking rescues it. With that option on, a
position too large for the remaining cash is reduced rather than skipped, which means the effective
sizing rule is “200 shares, or whatever I can afford, whichever is smaller” — and that is a rule
nobody chose.
Model 2: percent of equity
Section titled “Model 2: percent of equity”Complete runnable AFL
// lab-sizing-percent-equity.afl// Part 34 - Lab: Same Strategy, Three Position-Sizing Models// SIZING MODEL 2 OF 3: A FIXED PERCENTAGE OF PORTFOLIO EQUITY//// Every position is the same fraction of the account: a tenth of equity, ten// positions, fully invested when the book is full. Equal money, deliberately// unequal risk - a tenth of equity in a placid utility and a tenth in a// small-cap miner are not the same bet.//// Everything except the three lines under "SIZING MODEL 2" is shared with the// other two variants and with lab-shared-signals.afl.
// ===== SHARED BLOCK BEGIN - byte-identical in all four lab files ============//// ASSUMPTIONS - the same in every variant, so that the comparison is fair:// - Daily bars, split- and dividend-adjusted end-of-day data.// - Signals are read on the close of the signal bar. Every order fills at// the NEXT bar's open. SetTradeDelays(1,1,1,1) enforces that.// - Commission 0.1% of trade value, charged on entry and on exit.// - Slippage is NOT modelled. Every variant is flattered by the same amount.// - ExitAtStop = 1: stops are checked against the bar's High-Low range and// filled at the stop level. Optimistic, and optimistic identically in all// three variants, which is what keeps the comparison honest.// - Whole shares, one currency, no margin, long only.// - Starting capital 100,000 in whatever currency your database is in.
// ---- Account and execution ------------------------------------------------MaxPositions = 10;
SetOption( "InitialEquity", 100000 );SetOption( "MaxOpenPositions", MaxPositions );SetOption( "AllowPositionShrinking", True );SetOption( "CommissionMode", 1 ); // 1 = percent of trade valueSetOption( "CommissionAmount", 0.1 ); // 0.1% each waySetOption( "ActivateStopsImmediately", True );
SetTradeDelays( 1, 1, 1, 1 );BuyPrice = Open;SellPrice = Open;RoundLotSize = 1;
// ---- Universe filter ------------------------------------------------------MinTurnover = 2000000;Liquid = MA( Close * Volume, 50 ) > MinTurnover;
// ---- Signals --------------------------------------------------------------TrendPeriod = 200;EntryPeriod = 50;
Trend = Close > MA( Close, TrendPeriod );Buy = Cross( Close, MA( Close, EntryPeriod ) ) AND Trend AND Liquid;Sell = Cross( MA( Close, EntryPeriod ), Close );
// Ranking, used when more symbols signal on one bar than there are free slots.PositionScore = 100 - RSI( 14 );
// ---- The stop -------------------------------------------------------------// Three ATRs below the entry price, sampled on the signal bar and held for the// life of the trade. Identical in all three variants: the exit is not what is// being compared.AtrPeriod = 20;StopAtrMult = 3.0;MinAtrPct = 0.5;
UsableAtr = Max( ATR( AtrPeriod ), Close * MinAtrPct / 100 );StopDistance = StopAtrMult * UsableAtr;
// SetTradeDelays shifts Buy/Sell/Short/Cover and nothing else, so the stop// amount is shifted here by hand to make the value read on the entry bar the// one computed on the signal bar.StopAtEntry = Nz( Ref( StopDistance, -1 ), 1 );ApplyStop( stopTypeLoss, stopModePoint, StopAtEntry, 1 );//// ===== SHARED BLOCK END =====================================================
// ---- SIZING MODEL 2: PERCENT OF EQUITY -------------------------------------// 100 / MaxPositions, so a full book is fully invested. Because the percentage// is applied to CURRENT portfolio equity, this model compounds: positions grow// after a good run and shrink after a bad one, without any rule saying so.
SetPositionSize( 100 / MaxPositions, spsPercentOfEquity );Fragment — not a complete formula
SetPositionSize( 100 / MaxPositions, spsPercentOfEquity );Every position is the same fraction of the account: a tenth of equity, ten positions, fully invested when the book is full.
Equal money, deliberately unequal risk. A tenth of equity in a placid utility and a tenth in a small-cap miner are not the same bet — the second can move three times as far in a day.
It compounds. Because the percentage applies to current portfolio equity, positions grow after a good run and shrink after a bad one, with no rule saying so. That is usually desirable, and it is also what makes the participation problem from Part 30 grow with the backtest.
Model 3: ATR risk-based
Section titled “Model 3: ATR risk-based”Complete runnable AFL
// lab-sizing-atr-risk.afl// Part 34 - Lab: Same Strategy, Three Position-Sizing Models// SIZING MODEL 3 OF 3: RISK-BASED SIZING FROM THE ATR STOP//// Size each position so that the distance to the stop - which the shared block// already defined as three ATRs - costs the same percentage of equity in every// symbol. Equal risk, deliberately unequal money.//// Everything except the block under "SIZING MODEL 3" is shared with the other// two variants and with lab-shared-signals.afl.
// ===== SHARED BLOCK BEGIN - byte-identical in all four lab files ============//// ASSUMPTIONS - the same in every variant, so that the comparison is fair:// - Daily bars, split- and dividend-adjusted end-of-day data.// - Signals are read on the close of the signal bar. Every order fills at// the NEXT bar's open. SetTradeDelays(1,1,1,1) enforces that.// - Commission 0.1% of trade value, charged on entry and on exit.// - Slippage is NOT modelled. Every variant is flattered by the same amount.// - ExitAtStop = 1: stops are checked against the bar's High-Low range and// filled at the stop level. Optimistic, and optimistic identically in all// three variants, which is what keeps the comparison honest.// - Whole shares, one currency, no margin, long only.// - Starting capital 100,000 in whatever currency your database is in.
// ---- Account and execution ------------------------------------------------MaxPositions = 10;
SetOption( "InitialEquity", 100000 );SetOption( "MaxOpenPositions", MaxPositions );SetOption( "AllowPositionShrinking", True );SetOption( "CommissionMode", 1 ); // 1 = percent of trade valueSetOption( "CommissionAmount", 0.1 ); // 0.1% each waySetOption( "ActivateStopsImmediately", True );
SetTradeDelays( 1, 1, 1, 1 );BuyPrice = Open;SellPrice = Open;RoundLotSize = 1;
// ---- Universe filter ------------------------------------------------------MinTurnover = 2000000;Liquid = MA( Close * Volume, 50 ) > MinTurnover;
// ---- Signals --------------------------------------------------------------TrendPeriod = 200;EntryPeriod = 50;
Trend = Close > MA( Close, TrendPeriod );Buy = Cross( Close, MA( Close, EntryPeriod ) ) AND Trend AND Liquid;Sell = Cross( MA( Close, EntryPeriod ), Close );
// Ranking, used when more symbols signal on one bar than there are free slots.PositionScore = 100 - RSI( 14 );
// ---- The stop -------------------------------------------------------------// Three ATRs below the entry price, sampled on the signal bar and held for the// life of the trade. Identical in all three variants: the exit is not what is// being compared.AtrPeriod = 20;StopAtrMult = 3.0;MinAtrPct = 0.5;
UsableAtr = Max( ATR( AtrPeriod ), Close * MinAtrPct / 100 );StopDistance = StopAtrMult * UsableAtr;
// SetTradeDelays shifts Buy/Sell/Short/Cover and nothing else, so the stop// amount is shifted here by hand to make the value read on the entry bar the// one computed on the signal bar.StopAtEntry = Nz( Ref( StopDistance, -1 ), 1 );ApplyStop( stopTypeLoss, stopModePoint, StopAtEntry, 1 );//// ===== SHARED BLOCK END =====================================================
// ---- SIZING MODEL 3: ATR RISK-BASED ----------------------------------------// Position value / equity = RiskPercent * Price / StopDistance, so that a move// of StopDistance against the position costs RiskPercent of equity.//// The cap matters. Without it, a symbol whose ATR has collapsed asks for a// position several times the size of the account, and "allow position size// shrinking" quietly turns the risk rule into a bet-the-farm rule.RiskPercent = 1.0;MaxSizePercent = 20;
SizePercent = RiskPercent * Close / StopDistance;SizePercent = Min( SizePercent, MaxSizePercent );
// Shifted by the buy delay for the same reason the stop was.SizeAtEntry = Nz( Ref( SizePercent, -1 ), 0 );
SetPositionSize( SizeAtEntry, spsPercentOfEquity );Fragment — not a complete formula
RiskPercent = 1.0;MaxSizePercent = 20;
SizePercent = RiskPercent * Close / StopDistance;SizePercent = Min( SizePercent, MaxSizePercent );
SizeAtEntry = Nz( Ref( SizePercent, -1 ), 0 );SetPositionSize( SizeAtEntry, spsPercentOfEquity );Size each position so that the distance to the stop — three ATRs, defined in the shared block — costs the same percentage of equity in every symbol.
The arithmetic. You want a move of StopDistance against the position to cost RiskPercent of
equity. A position worth V loses V × StopDistance / Price when price falls by StopDistance.
Setting that equal to RiskPercent × Equity / 100 and solving for V / Equity gives
RiskPercent × Price / StopDistance, expressed as a percentage. That is the line.
Equal risk, deliberately unequal money. A volatile symbol gets a small position and a placid one gets a large position, so that a stop-out costs the same either way.
Running all three
Section titled “Running all three”- Run each of the three variants over the same universe and the same date range.
- Change nothing else between runs — not the settings dialog, not the watch list, not the report options.
- Record the figures below from each report.
| Fixed shares | % of equity | ATR risk | |
|---|---|---|---|
| Number of trades | |||
| Net Profit % | |||
| Annual Return % | |||
| Max. system % drawdown | |||
| CAR/MaxDD | |||
| Exposure % | |||
| Winners % | |||
| Largest single loss (from the trade list) | |||
| Largest position value (from the trade list) |
What this proves about entry signals
Section titled “What this proves about entry signals”Here is the conclusion the lab exists to deliver:
The entry signals were identical in all three runs. Same bars, same symbols, same conditions. And the three results are not variations on a theme — they are different systems, with different risk, different drawdowns and, quite possibly, different signs.
Two consequences follow, and both are uncomfortable if you have spent most of your effort on entries.
A backtest result is not a property of the rules. It is a property of the rules plus the sizing plus the constraints plus the costs. Quoting a return figure without the sizing model is quoting an incomplete specification.
Comparing two published systems’ returns is close to meaningless unless they used the same sizing model. Most of the difference between them may be sizing.
Verification checklist
Section titled “Verification checklist”Before you interpret anything, confirm the comparison is actually fair.
- The shared block is byte-identical in all four files. Diff them. If they have drifted, the comparison is measuring the drift.
- The date range and universe are identical. Check the Analysis window, not your memory.
- The signal count is identical. Run the shared file as a Scan and compare the signal total against each variant’s “signals generated”. Entry rules that differ would invalidate everything.
- The trade counts differ — they should. If they are identical, sizing is not binding and the lab has nothing to show you; increase the account size difference or reduce the slot count.
- Hand-check one trade in each variant: same symbol, same entry date, same entry price, different share count. If the entry date differs, something other than sizing changed.
- Look at the largest position in each trade list against the volume on its entry bar. Model 1 and model 3 can both produce positions nobody could have filled.
Same signals, three sizing rules, three different systems. Fixed shares varies the money and the risk without saying so. Percent of equity equalises the money and leaves the risk unequal, and it compounds. ATR risk-based equalises the risk and leaves the money unequal — provided it is capped, because uncapped it converts a low-volatility symbol into a bet on the whole account. The drawdowns differ more than the returns do, and none of it changed a single entry signal. A backtest result is a property of the whole specification, not of the rules.
Check your understanding
Sources for this lesson
6 verified · checked 2026-08-31
- 01AFL Function Reference — SetPositionSize§ spsShares, spsPercentOfEquity, spsValueamibroker.com/guide/afl/setpositionsize.html2026-08-31
- 02AFL Function Reference — ApplyStopamibroker.com/guide/afl/applystop.html2026-08-31
- 03AFL Function Reference — SetTradeDelaysamibroker.com/guide/afl/settradedelays.html2026-08-31
- 04AFL Function Reference — SetOption§ AllowPositionShrinkingamibroker.com/guide/afl/setoption.html2026-08-31
- 05AmiBroker User's Guide — Portfolio-level backtestingamibroker.com/guide/h_portfolio.html2026-08-31
- 06AFL Function Reference — ATRamibroker.com/guide/afl/atr.html2026-08-31
Every technical claim on this page was checked against the official AmiBroker documentation on the date shown. Where the course disagrees with folklore, the source is how you can tell which one to trust.