Skip to content
Level 4 · Trading System ResearcherLessonPart 28 · page 2 of 830 min
30Minutes
8AFL functions
5Sources
StandardRequires
AFL functions taught here8

Position Sizing with SetPositionSize()

Here is a fact that stops more first portfolio backtests than any other, stated in the User’s Guide in almost these words: unless less than 100% of your funds go into a single security, only one position can ever be open. You can set MaxOpenPositions to fifty. Without a position size that leaves room for a second trade, you will get one.

That is the entry point to this lesson. Sizing is not a refinement you add later. It is the thing that decides whether a portfolio backtest is a portfolio backtest at all.

PositionSize is a reserved array variable, introduced in version 3.9, and its encoding is worth knowing even though you will rarely assign it directly, because SetPositionSize() writes into it and the numbers show up in diagnostics.

Value Interpretation
above 0 A currency amount. PositionSize = 1000; invests 1000 per trade.
−1 to −100 A percentage of current portfolio equity. −25 means 25%, −100 means all of it.
−1000 to 0 Percent of portfolio equity (the range SetPositionSize() encodes into)
−2000 to −1000 Percent of the currently open position, used by scaling
below −2000 A share count

The documentation is explicit that assigning these encoded values directly is possible but that new code should use SetPositionSize() for clarity. It is also explicit that a later plain PositionSize = ... assignment clobbers whatever SetPositionSize() set, because they are the same underlying variable. If a formula does both, the last one wins and the first one is invisible.

The signature is short: SetPositionSize( size, method ). Both arguments are arrays, which matters more than it looks.

Constant Value What size means
spsNoChange 0 Leave the size already set for this bar alone
spsValue 1 A currency amount
spsPercentOfEquity 2 A percentage of portfolio-level equity
spsPercentOfPosition 3 A percentage of the currently open position — scaling in and out only
spsShares 4 A number of shares or contracts, which must be greater than zero

Because method is an array, it can vary bar by bar, which is what makes the documented scaling idiom work:

Fragment — not a complete formula

// Default to 100 shares, but on a scale-out bar reduce the position by half.
SetPositionSize( 100, spsShares );
SetPositionSize( 50, IIf( Buy == sigScaleOut, spsPercentOfPosition, spsNoChange ) );

Pyramiding and scaling are a portfolio-backtester-only feature and a subject of their own; what matters here is that spsNoChange exists precisely so that a second call can apply to some bars and leave the rest alone.

The most common sizing rule in the whole of AmiBroker is three lines, and the User’s Guide prints it as the canonical construction:

Fragment — not a complete formula

PosQty = 5;
SetOption( "MaxOpenPositions", PosQty );
SetPositionSize( 100 / PosQty, spsPercentOfEquity );

Five slots, each holding a fifth of current equity. As equity grows the positions grow with it; as it falls they shrink. Two properties follow that are worth saying out loud. It compounds, which is what makes a long test’s terminal figure so sensitive to the early trades. And it never leaves the account fully invested in practice, because commissions and whole-share rounding always leave a little cash behind.

If less than 100% of cash is invested, the remainder earns the annual interest rate from Settings. Set that rate to zero unless you specifically intend to model a cash yield, or a share of your reported return will be interest you never negotiated with anybody.

Equal weight equalises capital. It does not equalise risk: a fifth of equity in a quiet utility and a fifth in a volatile miner are two very different bets. Risk-based sizing starts from the other end — decide what you are willing to lose, then work out the share count that loses exactly that if the stop is hit.

The User’s Guide gives the arithmetic directly, in the form of a worked example: with a stop two average true ranges away, on a stock at 50 with the stop at 45, a 1,000 risk divided by the 5 per share of stop distance gives 200 shares. Those 200 shares are 10,000 of exposure — the allocation is ten times the risk. That distinction is the entire point of the method, and it is the one people lose first.

Written as a percentage of equity rather than a fixed sum, which is what makes it compound with the account, the same idea becomes the construction the guide uses for volatility sizing:

Fragment — not a complete formula

// Risk RiskPercent of portfolio equity per trade, with the stop StopDistance away.
// Position value / equity = RiskPercent * price / StopDistance.
StopDistance = 3 * Ref( ATR( 20 ), -1 );
RiskWeight = 1.0 * SafeDivide( Close, StopDistance, 0 );
SetPositionSize( Min( RiskWeight, 25 ), spsPercentOfEquity );

Three details in three lines. Ref( ..., -1 ) because the average true range of the bar the order fills on had not finished forming when the decision was made. SafeDivide because an instrument with a zero range would otherwise ask for an infinite position. And Min( ..., 25 ) because a very tight stop asks for a very large position, and an unbounded risk rule concentrates the account into whichever symbol happens to be quietest that week.

Several settings decide what happens when the computed size collides with reality. All of them can silently produce “my system took no trades”.

SetOption( "AllowPositionShrinking", ... ) decides the case where the requested size exceeds available cash. On, the position is entered with the size shrunk to what the cash allows. Off — and off is a real choice, not an oversight — the trade is not entered at all.

SetOption( "MinShares", n ) and SetOption( "MinPosValue", n ) set floors. If the funds will not buy that many shares, or that much value, the trade is not entered. These are useful — a 40-unit position is not a trade anybody would place — and they are also a common cause of a portfolio that mysteriously stops trading after a drawdown, because the slot value fell below the floor.

RoundLotSize is the block size. Zero means “use the default from Settings”; if that is also zero, fractional share counts are allowed, which is convenient and unrealistic for most equity markets. Set it to 1 for whole shares.

SetOption( "UsePrevBarEquityForPosSizing", True ) switches percentage sizing from current intraday equity to the previous bar’s closing equity. The default is False. Think about which one your account could actually have achieved: sizing a Wednesday-open order using Wednesday’s intraday equity assumes you knew Wednesday’s marks before Wednesday happened.

Take a single fixed sequence of trades and change nothing about them except how much money was committed to each. The table below does exactly that. The eight trade results are invented for this illustration — they are not from any market, any symbol or any backtest — and their only job is to be identical across the three columns.

One invented sequence of eight trades, three sizing rules

Synthetic figures, before costs, for illustration only. Terminal profit: 12,799 / 5,540 / 6,250. Maximum drawdown along the way: 27.9% / 7.2% / 6.8%.
BarStart+12%−8%+25%−15%+6%−20%+35%−10%
Full equity each trade100,000112,000103,040128,800109,480116,04992,839125,333112,799
Quarter of equity100,000103,000100,940107,249103,227104,77599,537108,246105,540
Fixed 25,000 per trade100,000103,000101,000107,250103,500105,000100,000108,750106,250
Synthetic figures, before costs, for illustration only. Terminal profit: 12,799 / 5,540 / 6,250. Maximum drawdown along the way: 27.9% / 7.2% / 6.8%.

Same trades, same order, same entries and exits. The profit differs by more than a factor of two and the worst drawdown by a factor of four. Nothing about the entry rule was involved in producing that spread.

The honest way to state the general point is this: sizing controls the scale of the distribution of outcomes, and the entry rule controls its shape. A backtest that changes both at once cannot tell you which one moved. That is why the sizing bench below fixes the signals and varies only the method, and it is why Part 34’s lab runs three sizing models over one signal set rather than three systems.

Complete runnable AFL

position-sizing-bench.afl
// position-sizing-bench.afl
// Part 28 - Position Sizing with SetPositionSize()
//
// One set of signals, four documented ways of turning each signal into a number
// of shares. Run it as a portfolio BACKTEST ("Apply to" a watch list), then run
// it again as an OPTIMIZATION over SizingMethod to get all four in one table.
//
// ASSUMPTIONS
// Fill price Next bar's open, taken exactly. No slippage, spread or partial
// fill. Costs are whatever Settings says - set them on purpose.
// Delays One bar on every signal.
// Sizing input Everything the sizing arithmetic reads is from the bar BEFORE
// the entry bar, so the size could genuinely have been decided
// the previous evening. See section 4.
// Liquidity Not modelled. A size this formula computes may be larger than
// the volume that traded. Part 30 deals with that properly.
// Account Cash account, no margin, no interest assumptions beyond the
// Settings default.
//
// The four methods produce different equity curves from IDENTICAL signals. That
// difference is a property of the sizing rule, not evidence about the entries.
// ------------------------------------------------------- 1. account and mode
SetOption( "InitialEquity", 100000 );
SetOption( "AllowPositionShrinking", True );
SetOption( "MinPosValue", 1000 ); // veto trades too small to be real
RoundLotSize = 1; // whole shares only
PosQty = 10;
SetOption( "MaxOpenPositions", PosQty );
SetTradeDelays( 1, 1, 1, 1 );
BuyPrice = Open;
SellPrice = Open;
ShortPrice = Open;
CoverPrice = Open;
// ------------------------------------------------------------ 2. the signals
// Kept plain on purpose. The subject of this formula is the sizing, so the
// entries must not be interesting enough to argue about.
FastMa = MA( Close, 20 );
SlowMa = MA( Close, 100 );
Buy = Cross( FastMa, SlowMa );
Sell = Cross( SlowMa, FastMa );
// ------------------------------------------------- 3. the sizing parameters
DollarsPerTrade = 10000; // method 1
PercentOfEquity = 100 / PosQty; // method 2 - equal weight across PosQty slots
FixedShares = 100; // method 3
RiskPercent = 1.0; // method 4 - percent of equity risked per trade
AtrPeriod = 20;
AtrMultiple = 3;
MaxWeight = 25; // method 4 cap, percent of equity in one position
// --------------------------------------------- 4. inputs known before entry
// The entry happens on the bar AFTER the signal bar. Anything this formula
// reads on the signal bar is therefore known before the order is placed, which
// is what makes the size defensible. Using the entry bar's own ATR or its own
// Open would mean sizing with information that had not printed when the
// decision was made.
RefPrice = Close; // the close that produced the signal
StopDistance = AtrMultiple * ATR( AtrPeriod );
// SafeDivide keeps a zero-range instrument from producing an infinite size.
RiskWeight = RiskPercent * SafeDivide( RefPrice, StopDistance, 0 );
RiskWeight = Min( RiskWeight, MaxWeight );
// -------------------------------------------------------- 5. the four methods
// The documented method constants are spsValue (=1), spsPercentOfEquity (=2),
// spsPercentOfPosition (=3, scaling only) and spsShares (=4). Note that they are
// NOT numbered in the order the documentation lists them - spsShares is 4 and
// spsPercentOfPosition is 3 - so never write the numbers from memory.
SizingMethod = Optimize( "Sizing method", 2, 1, 4, 1 );
if( SizingMethod == 1 )
{
// Fixed dollar value per trade, written with the reserved variable, which is
// how the User's Guide's own examples express it: a positive number IS a
// dollar amount. The newer spelling SetPositionSize( DollarsPerTrade,
// spsValue ) encodes into exactly this variable and means the same thing.
// Fixed dollars do not compound: a 40% drawdown does not shrink the next
// bet, so the account can be ground down by a size it can no longer afford.
PositionSize = DollarsPerTrade;
}
if( SizingMethod == 2 )
{
// Equal weight. The standard portfolio pairing with MaxOpenPositions.
SetPositionSize( PercentOfEquity, spsPercentOfEquity );
}
if( SizingMethod == 3 )
{
// Fixed share count. Equalises nothing: 100 shares of a $5 stock and 100
// shares of a $500 stock are two completely different bets.
SetPositionSize( FixedShares, spsShares );
}
if( SizingMethod == 4 )
{
// Risk-based sizing expressed as a percentage of portfolio equity, which
// is the construction the User's Guide gives for volatility sizing. Risking
// RiskPercent of equity over a stop StopDistance away means holding a
// position worth RiskPercent * price / StopDistance percent of equity.
SetPositionSize( RiskWeight, spsPercentOfEquity );
// The stop that the sizing assumes must actually exist, or the risk figure
// is fiction. ExitAtStop = 2 checks the bar's High-Low range but exits on
// the NEXT bar's trade price rather than pretending to fill at the level.
ApplyStop( stopTypeLoss, stopModePoint, StopDistance, 2 );
}
// ---------------------------------------------------------- 6. ranking ties
// With more signals than slots, something has to choose. Ranking by liquidity
// says "prefer the ones we could actually have traded", which is an execution
// argument rather than a return-seeking one. It also systematically prefers the
// largest names in the universe - a bias you are choosing, so state it.
PositionScore = MA( Close * Volume, 50 );

Download position-sizing-bench.afl120 lines

Four methods, one set of signals. Run it as a portfolio Backtest with the default method, then as an Optimization over SizingMethod to get all four rows in one table. Because the signals are identical in every row, every difference in that table is attributable to the sizing rule and to nothing else.

Two things to look at when you have it. First, the trade counts. They will not be equal: fixed-share sizing on a high-priced symbol can exhaust the cash that percentage sizing would have spread across three positions, so some entries are refused. Second, Exposure %. A sizing rule that leaves cash idle produces a much lower exposure, which inflates Risk Adjusted Return % without anything good having happened.

Without a PositionSize that leaves room for a second trade, a portfolio backtest holds one position regardless of MaxOpenPositions. SetPositionSize( size, method ) expresses that size four documented ways, whose constants are not numbered in the order they are listed, so write the names. Equal weight is the standard pairing with MaxOpenPositions and it compounds. Risk-based sizing separates the amount risked from the amount allocated, and it depends on a stop that gaps can defeat. AllowPositionShrinking, MinShares, MinPosValue and RoundLotSize can all refuse a trade outright, and the Detailed log names which one did. And a change of sizing rule can move a result further than a change of entry rule, which is why the two must never be varied in the same experiment.

Check your understanding

Question 1. A formula sets MaxOpenPositions to 20 and never assigns a position size. How many positions can the portfolio backtest hold at once?
SetOption( "MaxOpenPositions", 20 );
Buy  = Cross( MA( Close, 20 ), MA( Close, 100 ) );
Sell = Cross( MA( Close, 100 ), MA( Close, 20 ) );
Show the answer and why

Answer: One

The default puts effectively all available funds into the first entry, so nothing is left for a second. The User’s Guide states the requirement directly: to trade more than one symbol at a time you must set a position size below 100%.

Question 2. Which call sizes a position at one hundred shares?
Show the answer and why

Answer: SetPositionSize( 100, spsShares )

spsShares is 4, not 3 — writing 3 selects spsPercentOfPosition, which is meaningful only on scale-in and scale-out bars. PositionSize = 100 is a currency amount of 100, not a share count.

Question 3. A risk-based rule sizes each position so that a 3 ATR stop would cost 1% of equity. During a quiet period one symbol’s ATR collapses. What does the unbounded formula ask for, and which guard prevents it?
RiskWeight = 1.0 * SafeDivide( Close, 3 * Ref( ATR( 20 ), -1 ), 0 );
Show the answer and why

Answer: A larger position, potentially most of the account; a Min() cap prevents it

Position weight is inversely proportional to stop distance, so a tiny stop asks for an enormous position. SafeDivide only handles an exactly zero denominator; the cap is what keeps the account from concentrating into whichever symbol is quietest.

Question 4. Which of these can cause a backtest to report far fewer trades than the signal count, without any error being shown? Select all that apply.
Show the answer and why

Answer: AllowPositionShrinking turned off, with a requested size larger than available cash, MinPosValue set above the value of one slot, MinShares set above what the cash will buy

All three of those refuse the trade silently. RoundLotSize of 1 only rounds down to whole shares; it refuses nothing on its own, though it can push a marginal position below a MinShares or MinPosValue floor.

Sources for this lesson

5 verified · checked 2026-08-31

  1. 01AFL Function Reference — SetPositionSizeamibroker.com/guide/afl/setpositionsize.html2026-08-31
  2. 02AmiBroker User's Guide — Back-testing your trading ideas§ Position sizingamibroker.com/guide/h_backtest.html2026-08-31
  3. 03AmiBroker User's Guide — Portfolio-level backtesting§ Setting up position sizeamibroker.com/guide/h_portfolio.html2026-08-31
  4. 04AFL Function Reference — SetOptionamibroker.com/guide/afl/setoption.html2026-08-31
  5. 05AmiBroker User's Guide — System test report window§ Known differences between old and new backtesteramibroker.com/guide/w_report.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.