Position sizing, margin, and leverage

The engine uses an explicit model for position sizing, margin, and leverage. It also adds a way to adjust sizing for optimization and grid search without changing that core behavior.

Key concepts

Before looking at the sizing flow, a few concepts come up throughout this page.

  • Equity is the total value of the portfolio at a given point in time. It includes available cash, any reserved margin while it is active, and the unrealized profit or loss of open positions.
  • Available cash is the part of equity that is not reserved. It is used to fund new entries and pay fees.
  • Notional value is the economic size of a position. In practice, it is price × quantity.
  • Required margin is the part of equity reserved to keep a position open.
  • Maximum theoretical leverage is the implicit leverage allowed by the configured margin percentage.
  • Sizing price is the price used to calculate the quantity or budget of an entry.
  • Execution price is the actual price at which the order is filled.
  • Margin liquidation is the automatic reduction of a position when equity becomes less than or equal to the required margin.

The distinction between sizing price and execution price matters. An order can be sized from one price and filled later at another. Once the order is filled, the engine always uses the actual execution price for executed notional, debited fees, and the final solvency check.

How entry size is calculated

When an entry is created, sizing happens in two steps. The engine first decides which sizing mode applies. It then calculates the quantity from the price selected to size the order.

The global sizing mode is defined in the [backtest] block with default_qty_type. There are three modes. percent_of_equity expresses size as a percentage of equity. fixed expresses size as an absolute asset quantity. cash expresses size as a monetary budget.

You can override that global sizing directly in [[entry]] or [[order]]. If qty is set, that local quantity is used. If qty_percent is set, that local percentage is used. These two keys are mutually exclusive. If neither is present, the engine falls back to the global sizing from [backtest].

Sizing defined in [backtest]

default_qty_type selects the sizing mode. default_qty_value then provides the value used by that mode.

If default_qty_type is omitted, percent_of_equity is used. In that mode, if default_qty_value is also omitted, the engine starts from 100. long_size_multiplier and short_size_multiplier both default to 1 until you change them.

[backtest]
default_qty_type  = "percent_of_equity"
default_qty_value = 100

In fixed or cash mode, default_qty_value has no usable fallback. There therefore needs to be either a default_qty_value in [backtest] or a local sizing rule on the order with qty or qty_percent.

percent_of_equity

When default_qty_type = "percent_of_equity", default_qty_value is a shared base expressed as a percentage of equity.

[backtest]
default_qty_type  = "percent_of_equity"
default_qty_value = 40

With this configuration, the target entry is close to 40% of equity.

You can then adjust that base separately for longs and shorts with long_size_multiplier and short_size_multiplier.

[backtest]
default_qty_type      = "percent_of_equity"
default_qty_value     = 40
long_size_multiplier  = 1.5
short_size_multiplier = 0.5

Here, the base is 40. The multiplier for the relevant side is then applied. On the long side, that gives 40 × 1.5 = 60. On the short side, it gives 40 × 0.5 = 20.

The target exposure therefore becomes close to 60% of equity for longs and 20% for shorts.

These multipliers do not change the required margin or the maximum theoretical leverage. They only adjust the global sizing, and only when the global sizing mode is percent_of_equity and the order does not already define qty or qty_percent.

In this mode, sizing always starts from total equity, not only from available cash. The quantity that is actually opened can end up slightly below the target percentage because the engine accounts for fees and then rounds to the minimum contract size.

For example, default_qty_value = 60 sets a base exposure of 60% of equity. A long multiplier of 1.5 brings long exposure to roughly 0.9x, while shorts stay around 0.6x. The margin settings mainly define required margin and the theoretical leverage ceiling, not entry size.

[backtest]
symbol               = "BINANCE:BTCUSDT"
timeframe            = "240"
start_date           = 2024-01-01
end_date             = 2025-01-01
initial_capital      = 1000
default_qty_value    = 60
margin_long          = 50
margin_short         = 100
long_size_multiplier = 1.5

The same logic also works well in grid search. The next example varies long exposure while keeping long margin fixed at 50.

[backtest]
symbol                     = "BINANCE:BTCUSDT"
timeframe                  = "240"
start_date                 = 2024-01-01
end_date                   = 2025-01-01
initial_capital            = 1000
margin_long                = 50
margin_short               = 100
default_qty_value.start    = 20
default_qty_value.stop     = 80
default_qty_value.step     = 10
long_size_multiplier.start = 1.0
long_size_multiplier.stop  = 2.0
long_size_multiplier.step  = 0.25

fixed

When default_qty_type = "fixed", default_qty_value is an absolute asset quantity.

[backtest]
default_qty_type  = "fixed"
default_qty_value = 0.01

[[entry]]
id       = "buy"
order_id = "main"

With this configuration, the target entry is 0.01 asset units.

If an order does not define its own local sizing, the engine uses default_qty_value. If that value is missing, an entry that relies on the global fixed mode cannot be sized.

In this mode, long_size_multiplier and short_size_multiplier have no effect. The requested size is already an absolute quantity.

cash

When default_qty_type = "cash", default_qty_value is a nominal monetary budget.

[backtest]
default_qty_type  = "cash"
default_qty_value = 500

[[entry]]
id       = "buy"
order_id = "main"

With this configuration, 500 units of the quote currency are first converted into an asset quantity using the price selected to size the order. That quantity is then filled at the actual execution price.

In this mode, long_size_multiplier and short_size_multiplier do not apply. Global sizing depends only on the requested budget.

The budget remains nominal. If the fill price differs from the price used to calculate the quantity, the executed notional can differ from the initial budget. If default_qty_value is missing, an entry that relies on the global cash mode cannot be sized.

qty and qty_percent in [[entry]] and [[order]]

In [[entry]] and [[order]], qty always means an absolute asset quantity. qty_percent always means a percentage of equity, never a percentage of an existing position.

For [[entry]] and [[order]], the documentation does not cap qty_percent at 100. A value above 100 can target higher exposure, subject to margin rules and final order acceptance.

These two keys are mutually exclusive. You cannot use them together in the same block.

[backtest]
default_qty_type     = "percent_of_equity"
default_qty_value    = 40
long_size_multiplier = 1.5

[[entry]]
id       = "buy_fixed_qty"
order_id = "main"
qty      = 0.25

[[entry]]
id          = "buy_local_pct"
order_id    = "main"
qty_percent = 25

In this example, the first order opens 0.25 asset units. The second targets 25% of equity. Neither order uses the global sizing defined in [backtest].

When qty is present, the order is treated as a fixed-quantity entry. When qty_percent is present, the order is treated as a percent_of_equity entry, even if the global mode is fixed or cash.

In other words, qty and qty_percent replace the global sizing. They do not extend it.

In [[close]] and [[exit]], qty_percent no longer means a percentage of equity. It then means a percentage of the position that is already open. If a position is 2 BTC, qty_percent = 25 targets a close of about 0.5 BTC, before rounding.

The difference between those two blocks comes from the way the engine resolves qty and qty_percent. In [[exit]], the two keys remain mutually exclusive. In [[close]], they can coexist. If qty evaluates to a strictly positive value, qty is used. Otherwise, the engine uses qty_percent.

Sizing price and execution price

Quantity is not always calculated from the same price that later fills the order. This mainly matters whenever size depends on a budget or on a percentage of equity.

For a market order that fills on the close, the order is sized and executed at the same price. For a market order that fills on the next bar open, the engine keeps the signal-bar sizing price and then fills the order at the opening price. For a limit order, the sizing price is the limit price. For a stop market order, it is the stop price. For a stop-limit order, it is the limit price.

The final notional can therefore differ from the initial estimate when the sizing price differs from the real execution price.

[backtest]
default_qty_type  = "cash"
default_qty_value = 500

[[entry]]
id       = "buy_limit"
order_id = "main"
limit    = "close * 0.99"

In this example, quantity is calculated from the limit price and then filled when the order is executed. If the final fill happens at another price, executed notional, debited fees, and the final solvency check all follow the actual execution price.

What margin changes, and what it does not change

Margin does not define the requested entry size.

Entry size is determined first by default_qty_type, default_qty_value, the side-specific multipliers, and, when needed, by qty or qty_percent.

margin_long and margin_short come later. These settings define the required margin, the maximum theoretical leverage, and the liquidation conditions.

This separation matters when you read the results. A lower margin setting does not, by itself, create a larger entry quantity. It mainly changes the solvency conditions under which that entry is accepted, maintained, or liquidated.

How margin is applied

margin_long and margin_short

margin_long and margin_short are margin percentages written directly as percentages in the TOML file.

margin_long = 50 means 50% margin. margin_long = 0.5 means 0.5% margin.

These settings have two effects. First, they determine the required margin for open positions. Second, they set the maximum theoretical leverage.

Required margin is calculated from notional value:

  • for a long, notional value × (margin_long / 100)
  • for a short, notional value × (margin_short / 100).

Maximum theoretical leverage is read the same way:

  • for a long, 100 / margin_long
  • for a short, 100 / margin_short.

This means, for example, that a margin of 100 is theoretically close to 1x leverage, a margin of 50 to about 2x, a margin of 25 to about 4x, and a margin of 0.5 to about 200x.

The following example targets long exposure close to 1x, with long margin at 50% and short margin at 100%. Here, margin_long = 50 does not double entry size. It mainly lowers required margin to about half of notional value and sets the theoretical long ceiling near 2x.

[backtest]
symbol          = "BINANCE:BTCUSDT"
timeframe       = "240"
start_date      = 2024-01-01
end_date        = 2025-01-01
initial_capital = 1000
margin_long     = 50
margin_short    = 100

When a margin value is 0, margin logic is disabled on that side. Margin liquidation no longer applies there.

If only one margin is 0, only that side runs without margin. The other side keeps its own margin logic. The overall behavior can therefore remain mixed.

If margin_long = 0 and margin_short = 0, the engine runs without margin on both sides. In that case, every entry must be fully covered by available cash, including fees.

For example:

[backtest]
symbol          = "BINANCE:BTCUSDT"
timeframe       = "240"
start_date      = 2024-01-01
end_date        = 2025-01-01
initial_capital = 1000
margin_long     = 0
margin_short    = 0

How entry solvency is checked

Once the order size has been calculated, the engine checks whether the entry remains solvent.

When margin_long = 0 and margin_short = 0, the engine is in strict spot mode. The order is accepted only if available cash covers both the position value and the fees.

When margin is active, the engine compares equity with the required margin after the entry. With a margin below 100, this check acts as a direct margin check and does not add fees to that comparison. With a margin equal to 100, the final solvency decision can be made after the fill, from the actual execution price and the actual fees debited.

That is why an entry can be filled and then liquidated immediately on the same bar. Quantity is not automatically reduced to make the order fit within the margin rules.

The percent_of_equity case needs one clarification here. default_qty_value and the side multipliers define the target exposure. margin_long and margin_short mainly define the required margin and the maximum theoretical leverage. A lower margin setting does not, by itself, increase the entry quantity.

How a margin liquidation happens

A margin liquidation happens when equity becomes less than or equal to the required margin. This rule exists only on sides where margin is active, meaning margin_long > 0 for long positions and margin_short > 0 for short positions.

The check is not limited to the bar close. The engine first checks the open, then the adverse intrabar extreme: the low for a long and the high for a short. A liquidation can therefore happen inside the bar, even if price later moves back before the close.

When that threshold is crossed, part of the position can be reduced to restore the required margin. Depending on rounding and minimum contract size, that partial reduction can become a full close.

With margin_long = 100 or margin_short = 100, the setup is still theoretically close to 1x, but an immediate margin call can still appear. This usually happens when an entry targets almost 100% of equity, and the actual fill price plus fees shift the balance during the final check. In that situation, the estimated liquidation price can remain undefined (NaN) even though a liquidation risk still exists.

Concrete example of margin, leverage, and liquidation

Take a simple case, intentionally ignoring fees and rounding so the numbers stay easy to read. Assume an initial equity of 1,000 USDT and a long Bitcoin exposure of 4,000 USDT, with margin_long = 20. A 20% margin corresponds to a theoretical setup close to 5x, because 100 / 20 = 5. At entry, required margin is therefore 4,000 × 20% = 800 USDT.

The open exposure is 4,000 USDT, but that full value does not need to be covered by equity to keep the position open. As long as equity stays strictly above required margin, the position can remain open.

If Bitcoin rises by 2%, unrealized profit is 80 USDT (4,000 × 2%). Equity then moves from 1,000 to 1,080 USDT, while the position notional value becomes close to 4,080 USDT. Required margin therefore becomes 816 USDT (4,080 × 20%). Equity remains above required margin.

If price falls by 2%, unrealized loss is 80 USDT (4,000 × 2%). Equity drops to 920 USDT, while notional value falls to about 3,920 USDT. Required margin then becomes 784 USDT (3,920 × 20%). Here again, equity remains above required margin.

The critical point appears when equity meets required margin. In this simplified example, that happens around a 6.25% decline, because the loss then reaches 250 USDT (4,000 × 6.25%). Equity therefore falls to 750 USDT (1,000 - 250) and notional value to 3,750 USDT (4,000 - 250). Required margin also becomes 750 USDT (3,750 × 20%). At that point, the threshold where a margin liquidation can begin is reached.

This example highlights two things. First, a 20% margin does not force the position to 5x: it mainly defines required margin and the maximum theoretical leverage. Second, liquidation depends on the relationship between equity and required margin, not only on the percentage decline in price.