Every cost the engine applies, how it is calculated, and where the model stops.
Order types
A strategy can place five kinds of order. How to write each one is on Strategies API, Pine Script, Blocks and Rust. This section is how the engine fills them.
| Order | When it can fill | At what price |
|---|---|---|
| Market entry | At the next bar's open | The open |
| Limit entry | Inside a bar, from the next bar on | Its price, or a better open |
| Stop entry | Inside a bar, from the next bar on | Its price, or a worse open |
| Stop loss and target | Inside a bar, from the bar after the entry | Their level, or the open on a gap |
| Exit, whole or part | At the next bar's open | The open |
Nothing fills on the bar that decided it. A market order waits for the next open. A resting order starts to rest on the next bar.
Limit and stop entries. A buy limit fills at its price or lower, a sell limit at its price or higher. A buy stop fills once the market trades at its price or higher, a sell stop at its price or lower. The order rests until it fills, its number of bars runs out, the strategy cancels it, or another order in its OCO group fills. A new order with the same name replaces the old one.
Inside a bar the engine keeps a fixed order. First the stop or target of the open position. Then the exits and entries at the open. Then the resting orders, piece by piece: the whole bar on OHLC, each minute on the M1 proxy, each quote on TICK. On TICK a buy fills at the ask that reached it and a sell at the bid.
A fill opens a position when nothing is open. Against a position held the other way, it closes that position at the same price and opens the new one. Against a position held the same way it does not fill, and the order keeps resting. A position opened this way has its stop and target checked from the next bar. That flatters a limit bought into a bar that kept falling. Read a limit strategy on M1 or TICK before you trust it.
Stops that move. A position can carry a trailing stop and break even. The trail follows the best price since the entry at a fixed distance. It can wait until the position is a set distance in profit. Break even moves the stop to the entry, plus a lock in your favour if you set one, once the position is a set distance in profit. The stop only moves toward profit. Each piece of a bar is checked against the stop as it stood, and only then moves it. The order of a piece's high and low is not known, so a move never counts inside the piece that made it.
Size. An entry uses the run's lot size unless the strategy gives its own: lots, units, cash, a percentage of the balance, or the percentage of the balance the stop would lose. The size is measured when the signal becomes an order, at that bar's close and with the balance after its fills. The balance is cash, without the profit of an open position. A resting order keeps the size it was placed with. A size that comes to no lots places nothing, and does not close a position held the other way either.
Closing part of a position. An exit can close a fraction, a number of lots or a number of units. The part fills at the next open, like every exit. The rest stays open with its entry, its stop, its target and its moving stop. The part is a trade of its own in the result, with the position's id and its share of the entry commission. As much as is held, or more, closes all of it.
Margin. Every entry is checked against free margin when it fills, a resting one too. One the account cannot fund is refused and counted, as Margin below explains. A refused resting order does not rest again, and the rest of its OCO group is cancelled with it. A position it was about to reverse is still closed.
What the engine does not do.
- More than one position. A market entry in the direction already held is dropped. A resting one waits.
- Partial fills. An order fills whole or not at all. The one exception is the L2 tier, where a market entry larger than the visible book can fill only what the book showed.
- Stop limit orders, which turn into a limit once they are touched.
- Exits at a price of their own. A position leaves at its stop, at its target, or at the open after an exit.
The grid, basket and cBot engines keep their own fill loop, described at the end of this page.
What a lot is
Every money figure below is price move × contract size × lots, and contract size depends on the asset class. Lot size in the run configuration is counted in lots, not in units, so the same 0.1 is 10,000 units of a currency pair and a tenth of one share of a stock.
| Class | Units per lot |
|---|---|
| Forex | 100,000 units of the base currency |
| Metal | 100 ounces for XAUUSD, 5,000 for XAGUSD |
| Index | 1 unit per index point |
| Crypto | 1 coin |
| Equity | 1 share |
| CFD | 1 unit, except WTIUSD and BCOUSD at 1,000, NGCUSD at 10,000, and the archive's LIGHT.CMD/USD, BRENT.CMD/USD, GAS.CMD/USD and COPPER.CMD/USD at 100 and DIESEL.CMD/USD at 4 |
The class the run actually used is recorded with the result. See Asset Classes for how an instrument is placed and what happens to one the engine cannot place.
Spread
A buy fills at the ask and a sell at the bid. Where those two prices come from depends on what the data carries.
Bars with both sides. A range downloaded from the archive keeps the bid and the ask of every minute, and every bar built from those minutes keeps both too. The engine fills on them, so a run pays the spread the market showed at that minute: tight in a quiet session, wide around a release.
| Fill | Price on a bar with both sides |
|---|---|
| Entry at the next bar's open | The ask open for a buy, the bid open for a sell |
| Signal exit or reversal at the open | The bid open to close a long, the ask open to close a short |
| Close at the end of the data | The bid close for a long, the ask close for a short |
| Stop or target | Its level, reached on the bid for a long and on the ask for a short |
| Limit or stop entry | Its price, reached on the ask for a buy and on the bid for a sell |
| Liquidation at zero equity | The price where the money runs out, with no spread |
So a long pays the spread twice even when it stops out: it buys at the ask, and its stop is hit when the bid reaches it. On the M1 proxy each minute is read on its own bid and ask. A moving stop follows the bid high for a long and the ask low for a short.
Ticks. On the tick tier every fill is a real quote: a buy at the ask, a sell at the bid, and a stop or target is tested against the bid for a long and the ask for a short. What is still open at the end of the data closes at the last quote, the bid for a long and the ask for a short.
Bars without an ask side. A CSV import with one price series, a data plugin, London Strategic Edge minutes, generated bars, and minutes downloaded before the engine kept both sides all carry one price. A fill on such a bar is the mid plus or minus half a spread. The spread is drawn for each fill from the average spread you set on the custom symbol, from x to y pips, from a stream seeded from the run seed, so a rerun is the same run. Synthetic bars use a default of 0.5 to 1.5 pips. On these bars the spread is charged only where the engine picks the price: the entry, a signal exit and the close at the end of the data. A stop or a target fills at its level against the mid, so a trade that stops out pays the spread once. Real data with no ask side and no average spread on its symbol is refused before the run starts, with spread_missing (see Troubleshooting). There is no spread band in the run settings any more.
The run names the spread model it used, and counts the bars that had no ask side. Export the report to read it.
Commission
Commission per lot in the run configuration is the round turn: what one lot costs to open and close, charged half on the entry fill and half on the exit fill, commissionPerLot × lots ÷ 2 each. The default is 3.0 with a default lot size of 0.1, so a round turn on the default run costs 0.30. A broker that quotes its commission per side wants twice that number in the field.
That default is a forex number. A lot is 100,000 units of a currency pair and one share of a stock, so 3.0 per lot is a plausible standard lot commission and, on an equity run, three dollars per share traded. Set it from your broker's schedule for the class you are running, converted to per lot as the table above defines a lot.
Commission is global to the run. The Commission per lot field on a custom instrument, under Data, Instruments, is stored and not read.
Swap
Charged per night held:
swap = nights × swap_pips × pip_size × (lots × contract_size)nights counts the UTC date boundaries crossed between the entry and the exit, each weighted by the rollover calendar a forex broker keeps. The rollover out of Wednesday counts three, because it moves the value date from Friday to Monday and so pays for the weekend. The rollovers out of Friday and Saturday count nothing, because the market is shut. Every other rollover counts one, and across a whole week the weights sum to seven. A position opened at 23:50 on a Monday and closed at 00:10 is charged one night. The same hold opened on a Wednesday is charged three, and opened on a Friday, none. The calendar is the same for every instrument, crypto included.
The pips value is signed, following the cTrader convention: a positive swap is a credit that adds to the result, a negative swap is a charge. Long and short have separate values in the run configuration, and both default to 0, so a run that does not set them pays no financing at all.
There is no per instrument swap: the Swap long and Swap short fields under Data, Instruments are stored and not read.
The intrabar problem
When a single bar spans both your stop and your target, the bar alone cannot say which was hit first. The engine does not guess:
- Pessimistic takes the stop. This is the default, and it is the honest one.
- Optimistic takes the target.
On the M1 proxy tier the bar is walked one minute at a time, so which level came first is decided by the minutes, and the priority only settles a single minute that holds both. On the tick tier the question does not arise: the ticks are replayed in order, so whichever was actually touched first is what fills.
If a strategy's result changes a lot between pessimistic and optimistic, the strategy is sensitive to intrabar path and the bar data cannot answer it. Run it on ticks.
Gaps
A bar that opens beyond a protective level never traded at that level, so the fill is the bar's open rather than the level:
- A stop gapped through fills at the open, which is worse than the stop. A long fills at the lower of the two prices, a short at the higher.
- A target gapped through also fills at the open, which is better than the target, because a limit order fills at its price or better.
An open that is already past one of the two levels settles the ordering question as well, whatever the intrabar priority says: the level the open passed is the one that happened.
This applies on the OHLC tier, and on the M1 proxy tier to the first minute of a bar, which is where a gap can be. Later minutes are inside a bar that has already started trading, so they fill at the level. The tick tier does not need the rule, because every tick is a quote that existed.
Slippage
Not modelled beyond the gap rule above, with one setting as the exception. There is no market impact, so a stop touched inside a bar's range fills exactly at your level. The spread is in the fill price, as Spread above explains, and a gap costs the open. At the L2 tier a market order walks the order book, and a resting limit waits for its place in the queue at its price. Neither moves the book.
The exception is Slip beyond the touch in the run settings (touch_slip_pips), shown on the TICK tier. An entry larger than the size offered at the best price fills that many pips worse. It defaults to zero, it applies to entries only and never to a stop, a target or an exit, and the size is compared only where the vendor's size unit is known, which today is forex. It is a stated assumption, not a model of the book.
When the account runs out of money
Equity is marked at every bar close. When it reaches zero or goes below, the run stops there:
- the open position is closed at the price where its loss equals exactly the cash that was left, so a trade cannot report a loss larger than the money that existed
- the equity point for that bar is replaced by the one recorded after that close, so the curve does not carry a momentary drawdown of several hundred per cent
- the result records that the account was wiped out, and the bar it happened on
Two things this is not. It is not a margin model: no maintenance requirement is checked, so a real broker would have closed the position earlier and at a better price. And it is not negative balance protection, it is the engine refusing to report a loss on a position nobody could still have been holding. The closing commission and swap are charged on the liquidating fill, so the final balance can sit slightly below zero.
A run that ends this way is shorter than the window you asked for, and the report says so. That is what separates it from a strategy that simply stopped signalling.
The grid, basket and cBot engines
Three strategies keep their own fill loop: Grid Basket (cTrader MDB), Lot Doubling + Hedge (benchmark), and a C# strategy that places its own orders. Three of the rules above differ for them. On a bar with both sides they trade at that bar's bid and ask: a buy at the ask and a sell at the bid, at the open or the close where they trade, and their spread filters see the real spread. A basket's take profit is checked on the side that fills it. On a bar without an ask side the spread is a constant, the midpoint of the symbol's average spread, charged on both sides of every leg. Commission is charged per lot, half on each side, as usual. No swap is charged at all, and there is no zero equity stop, so those runs can carry on past insolvency.
Costs that change over time
Commission and swap both change, and a multi year run that applies one value throughout is wrong in a direction that always favours the strategy: brokers cut commissions over time, so today's rate applied to 2015 undercharges every trade in 2015, and swap follows interest rates, so a 2021 swap applied across 2023 charges nothing for a year when carrying a position cost real money.
Costs over time, on the Account and costs tab of the settings window, takes a list of periods, each with the date its rates came into force. The engine asks the schedule for the costs at the bar it is simulating rather than reading them once at the start, and swap is charged per night at that night's rate, so a position held across a change pays the old rate for the nights before it and the new rate for the nights after.
Two rules that matter more than they look:
- A date before the first period is not zero. It is the first period's costs, carried backwards, because a schedule that begins in 2018 says nothing about 2017 and the alternative is a run that trades 2017 for free. The report says when this happened.
- Leaving it empty is a real answer. The flat commission and swap above apply to the whole run, which is what every run did before schedules existed, and the report states which numbers it used and where they came from.
The numbers are typed rather than fetched, because there is nowhere to fetch them from. Dukascopy publishes current values only and no vendor sells the history as a series, so your broker's own specification archive is the source.
Margin
The account's leverage binds. Required margin is the notional value of a position times the rate the instrument requires, and an entry the account cannot fund is refused, not resized and not stopped: the strategy asked to open a position the account could not carry, and the honest simulation of that is that the position does not exist. Refusals are counted and the report says how many there were, because a skipped entry nobody records is how a backtest becomes one nobody can reproduce.
The rate is the instrument's own margin percent when a custom symbol declares one, and one over the account leverage when it does not. Leaving that field empty means not declared: fill it in only when your broker states a requirement for that instrument.
Free margin is equity less what open positions already tie up. Equity and not balance, because an account carrying an unrealised loss has less to commit than its balance suggests, and that is precisely the moment a real broker refuses.
Maintenance margin is not modelled. A real broker watches a maintenance level and closes a position well before the account reaches zero, so a real account is stopped out earlier and at a better price than this engine's stop out. The grid, basket and cBot engines hold a basket rather than one position and do not check margin at all; their results say so rather than refusing to run.
What this means for your numbers
A backtest is a model of execution, not a record of it. Set commission and swap to your broker's real values, and to the right values for the asset class, then treat the output as a hypothesis. The costs above are the ones the engine knows; the ones under Not implemented in Asset Classes are real money that this model does not charge you.