VEEMANDocs
DocumentationSupportOpen Dashboard

Getting started

  • Introduction
  • Quick start
  • Core concepts

Importing data

  • Supported formats
  • How import works
  • Splitting a file
  • Troubleshooting

Building your portfolio

  • Workspace & equity
  • Weights & capital
  • Weekday filter
  • Keyframes
  • Currencies & FX
  • Library
  • Strike guard
  • Variants & comparison
  • History (undo/redo)
  • Shared spaces

Analytics

  • Metrics reference
  • Charts (deep-dive)
  • Monthly P&L
  • Correlations
  • Comparison
  • Monte Carlo
  • Equity Control
  • Optimization
  • IBKR reconciliation
  • Veeman AI

Research

  • Research overview
  • Define
  • Composing a backtest
  • How the engine trades
  • Results & archive
  • Parameter study
  • Rewind
  • Tactics
  • Indicators

Reference

  • Keyboard & command palette
  • Exporting data
  • Settings

Technical

  • Architecture
  • API reference
  • Security
  • Your data & privacy
  • Risk disclosure
← PreviousComposing a backtestNext →Results & archive

How the engine trades

This page describes, step by step and without shortcuts, what the backtest engine does with a recipe. Everything below is exactly how the engine computes; when a number on screen surprises you, the answer is here.

Three principles hold throughout:

  • No lookahead. Every decision uses only data that existed at that minute: the index, the VIX and the option quotes are each the last value published by that instant, never a print that arrived a few seconds later. A filter that would need the future is rejected when you compose the recipe (for example an opening range that ends after the entry).
  • No pure-mid fills. Unless you switch slippage off, every entry and every early exit costs you slippage: per position (the default) it is taken off the whole structure's fill, per leg you sell below the mid and buy above it.
  • One arithmetic. The same function values the position for stops and targets, for the exit fills, for settlement and for Rewind's P&L path, and the per-leg stop reads each leg at those same closing prices. They cannot disagree.

A run

The engine takes every trading day of the dataset inside the period and processes each day independently (in parallel, with a deterministic result). For each day it:

  1. applies the day filters; if one excludes the day, the day produces no trade;
  2. makes one or more entry attempts (fixed times, a floating window, re-entries), each with its own time-dependent filters;
  3. for each position opened, selects the strikes, prices the entry fills, then marks the position every minute until an exit rule fires or the day ends;
  4. books each exit with fills, slippage and commissions;
  5. adds up the day's P&L (every position, partial exit and adjustment of the day).

The output is a daily P&L series (one row per day of the period, zero on days without trades), the trade log at leg level, the statistics, the frozen recipe, the dataset version and the warnings.

The day cascade

Day filters run in this order. A day gets one outcome: the first step that excludes it. The filter funnel in the results counts days per step in the same order.

No indicator data that day
The recipe uses a custom indicator and its data has no bars for this market day: the data starts after it, ends before it, or has a gap. That counts the chart's index data and the data of every symbol the script reads with request.security(). Reading the last bar available would be days old, so the day isn't traded.
No indicator signal that day
Entries follow an indicator signal and no signal comes during the session (or inside the signal window): no event bar closes, the condition never starts to hold, or the only signals fall in the indicator's warm-up.
Day of week
Weekly frequency: the day isn't one of the selected weekdays.
Monthly occurrence
Monthly frequency: the day isn't the Nth occurrence of the chosen weekday (occurrence = ⌊(day of month − 1) ÷ 7⌋ + 1).
Outside the chosen dates
Specific dates: the day isn't in the list.
Blackout
The day is a blackout date.
Not an event day
An Only economic event rule is set and no rule hits this session.
Economic event
A Skip economic event rule hits this session.
VIX out of range
Only on days without minute VIX: the previous day's VIX close is outside Min/Max. (With minute VIX the level is checked at the entry minute, see below.)
No quote at the entry time
The index has no price that day.
Gap out of range
The overnight gap (today's open versus the previous close) fails the gap bounds.
Overnight VIX out of range
The VIX overnight move fails its bounds.
No valid entry
The day passed every day filter but no entry attempt went through (the funnel then lists why, per attempt).

The "previous close" of the first day of the period is the previous day of the dataset, not a blank: two runs that start on different dates are comparable on their common days. Before the very first day of the options archive, the index's official daily closes are used.

Economic events

An economic event rule is resolved once per run, on the calendar in force at that moment (see the economic event calendar): the recipe holds the rule, the dates come from the calendar. The resolution follows three rules.

  • Only scheduled events count. Releases and FOMC meetings are published in advance, so knowing in the morning that today is a CPI day is not looking ahead. An unscheduled event (the emergency cuts of 3 and 15 March 2020) was known to nobody that morning: it stays in the calendar as a fact but never decides a day. A release that was rescheduled (for example by a government shutdown) counts on the date it actually came out, since the new date was announced before.
  • An event belongs to a session. It is the trading session of its release date; if the release lands while the market is closed (a holiday, a weekend, or after the session's close, 13:00 on half-days) it belongs to the next session. A jobs report released on Good Friday counts for the following Monday.
  • Day before and day after are sessions. They are the previous and the next trading session of the event's session.

With Only and Skip rules together, a session is first checked against the Only set, then against the Skip set: the funnel counts each excluded session once, at the first of the two that excludes it. Like Blackout, the rule decides the first entry of the day, on both entry paths (clock and signals); a re-entry can only follow an entry that happened.

A Skip rule around the release works on minutes instead of sessions. On the day of each release, at the time the calendar records for it (not the usual one: a release moved to 10:00 counts at 10:00), it refuses every entry attempt from the release time minus the minutes before to the release time plus the minutes after, both ends included. Only the part of the window during the session counts: one that starts before 09:30 starts at 09:30, one that ends by 09:30 does nothing. The session itself goes ahead, so the day cascade doesn't count it; each refused attempt is counted as economic event (around the release) in the entry filters.

The close before economic events (an exit rule) is resolved the same way: on each session where a chosen release comes out between the open and the session's close, the close time is the release time minus N minutes (not earlier than 09:30); with several releases that day, the earliest counts. Positions opened before the release close at the first minute at or after the close time, exit reason Economic event; entry attempts from the close time through the release minute are refused (at hh:mm:00 the quotes are still the ones from before the news); positions opened after the release aren't touched. Like the recipe's other exits it acts on the main position: the adjustments of a Tactic keep their own exits.

A re-entry that falls inside either pause (a window, or the minutes between the close and the release) doesn't fail: it waits for the first minute after the pause, and in Rewind its entry cause says it waited for the release.

If an event in a rule has no dates loaded on the server, the run stops with a message instead of running unfiltered. If the period goes beyond the dates known for an event (before its history starts, or after the last date the calendar knows), the run says how many days couldn't be checked; a close before a release that finds no release during market hours in the whole run says so too. Each result records the calendar version it was resolved on and the sessions the rules skipped (or, with Only, the sessions they let through), the windows that paused entries and the early closes.

Entry attempts

Fixed times
One attempt per entry time (the main time plus the additional ones). Each time that succeeds opens its own position.
Floating window
Attempts every minute from Entry from to Entry until and stops at the first one that opens a position. If none does, the window counts as one refused attempt, under its most frequent reason.
Re-entry
After a position exits, a new attempt at exit time + delay, if the exit reason is among those chosen (target, stop, which includes the per-leg stop, time exit: minutes in trade, a clock-time exit or the close before an economic release), the exit time is inside the not-before / not-after bounds and the daily maximum isn't reached. Never after settlement, never at or after the session close, and never at or after an early-exit time (a new position would close a minute later, one trade a minute until the close). A re-entry that falls inside an economic event pause waits for the pause to end. After an exit decided by a market condition (technical indicator, custom indicator, short/long ratio, VIX9D/VIX ratio, position delta, tested short) the re-entry waits: at each attempt the engine asks whether that condition still holds at the attempt minute, with what is known at that minute (the index and the VIX at that minute, the chain the new position would be built from, the indicator's last closed bar), and moves the attempt one minute later until it no longer holds or the day ends. It never looks at the minute after. The short/long ratio is read with the prices the exit reads: in the per-position slippage model, the mids rounded to the nickel. A cooldown on that rule doesn't change the question. If another entry filter refuses an attempt while it waits, the re-entry ends for the day.
Indicator signal
Entries follow the signals of a custom indicator, in time order, with the time-dependent filters applied as for any attempt. An event signal is one instant per bar with the event, at the minute the bar closes. A condition signal is read at every minute from 09:31 to the last minute before the close (inside the signal window, if set): a level (≥, ≤, is on/off) gives one signal per run of consecutive minutes where it holds, starting at its first minute (the window's first minute if it already holds then); a crossing gives one instant per crossing minute. Each signal opens at most one position: at its first minute, or, for a level whose first minute the filters reject, at the next minute they accept while it still holds (if it never opens, the day counts one refused attempt under the most frequent reason). A signal that comes while the open positions are at the limit (one, unless Max open positions says more) is skipped and counted in the warnings; with Close the oldest the new entry is evaluated first and, only if it opens, the oldest position closes at that minute. With Re-enter while the signal holds, after a position exits the same level signal reopens from the next minute while it holds, waiting after an exit on a market condition as the re-entry does, never at or after the close or an early-exit time. At most, a day stops the day after that many positions (First signal of the day = 1). Signals at or before 9:30, or at or after the close, aren't attempted.
Open-position limit
With Max positions, all candidate entries of the day (fixed times, re-entries) are examined in time order. If the limit is full the entry is skipped, or, with Close the oldest, the new entry is evaluated first and, only if it opens, the oldest open position is closed at that minute (exit reason Closed at position limit) and the new one takes its place; the closed position's re-entries are cancelled. An entry refused by its filters never closes anything.

Entry filters

For each attempt, in this order (each refusal is counted in the funnel as an attempt, not a day). With Max positions, the open-position limit is checked first, before any of these: an attempt that finds the limit full is skipped without evaluating them. With Close the oldest it is evaluated normally, and makes room only if it passes.

  1. Market already closed: on a half-day, an entry at or after 13:00 is refused.
  2. No quote: no index price at the entry minute. This is always the case at 9:30 (the recipe form starts at 9:32, but a recipe can reach the engine by other routes): the index prints its first value at 9:30:01, so at 9:30:00 there is no price to pick strikes from, and the run's warnings say so.
  3. Economic event (around the release): the entry minute falls in a window around a release, or between the close before a release and the release (see Economic events).
  4. Intraday move: index at the entry minute versus the day's open.
  5. VIX at entry: with an overnight or intraday VIX move filter, VIX not open yet while the session's first VIX print isn't known; then the level (Min/Max), the intraday move, the VIX9D ÷ VIX ratio.
  6. Opening range breakout.
  7. Technical indicators (and their warm-up).
  8. Custom indicators (and their warm-up), see Custom indicators.
  9. Chain missing, strikes not quoted, spread too wide, short/long ratio, premium, not enough margin: these need the selected strikes, see below.

Directional move filters (gap, intraday, VIX moves)

A move filter has four optional bounds, all magnitudes (≥ 0): Min up, Max up, Min down, Max down, in % of the reference price or in points. The engine looks at the side the move actually took (a zero move counts as both sides):

  • if that side has bounds, the size of the move must be within them;
  • if it has none, the day passes only if the other side doesn't require its own direction, i.e. has no Min (a Max alone limits its side and leaves the opposite moves free).

So Min on both sides means "at least X in either direction", Max on both sides means "|move| ≤ X", and Min up = 0 means "only up moves", not "any day".

VIX values

VIX at a minute
The VIX at time t is the last value published by t, that is the close of the minute before. The VIX (and the VIX9D) updates every 15 seconds, at :01, :16, :31 and :46, so no print falls at t:00: the first print of minute t always arrives after the decision. Before the first session minute has closed, the last pre-market value.
VIX open
The first print after 09:30, usable from the end of the minute it was printed in: by 09:32 on most days, by 09:33 on almost all, a few minutes later on a handful. Until then the open isn't known, and the filters that measure from it wait for it.
Previous close
The previous day's VIX close (16:15).
Level filter
VIX at the entry minute within Min/Max (bounds inclusive). On days without minute VIX: the previous close, for the whole day.
Overnight move
VIX open versus the previous close. A day filter: a day where it fails has no entry. While the open isn't printed yet, attempts wait (VIX not open yet in the funnel): a floating window enters from the first minute the open is known, and a fixed time before it doesn't open. The pre-market value is never used in its place.
Intraday move
VIX at the entry minute versus the VIX open. It waits for the open like the overnight move.
VIX9D ÷ VIX
Both at the entry minute. Above 1 = inverted term structure (9-day expected volatility above 30-day).

When a value is missing (a day without minute VIX, VIX9D before its first minute closes at 09:32) the condition is ignored, never treated as a pass or a fail in silence: the warnings count how often it happened.

Opening range breakout

The range covers the minutes from the open to the range end. With Use intraday extremes the range is the highest and lowest index price in that window; without, the open and the price at the range end. At entry, a breakout up is an entry price above the high, a breakout down below the low. High or Low requires either, High only / Low only one direction, No breakout requires the price still inside the range.

Technical indicators

Computed on the SPX price at the entry minute:

RSI (14)
Wilder's RSI, the one broker charts show: the previous official closes plus the current price as today's close. The average gain and loss start as the simple mean of the first 14 changes, then follow Wilder's smoothing (each new change weighs 1/14). The engine keeps the last 250 closes, enough for the result to match a chart that uses the whole history. RSI = 100 − 100 ÷ (1 + average gain ÷ average loss). It needs at least 15 previous closes.
SMA (N days)
The current price plus the previous N − 1 official closes, averaged. N from 2 to 400.
EMA (N minutes)
Exponential average of the 1-minute prices with α = 2 ÷ (N + 1), warmed up on the previous session so long periods don't start cold. N from 2 to 1,000.

Without enough history for an indicator (the first days of the data), the entry is skipped and counted as indicator without enough history. Exit indicators use the same definitions, evaluated on every minute's price.

Custom indicators

Your own indicators (Pine Script, see Indicators) are computed once on the whole history of their chart (symbol and timeframe), in isolated workers, and cached: a run, or a study that repeats the recipe, reads the stored values. The engine reads them with one rule:

As-of reading
At time t the value of an output is the one of the last bar closed by t: a bar is known from the minute it closes, never before. A 1-minute bar of 10:30 closes at 10:31; a 5-minute bar of 10:25 closes at 10:30; a daily bar closes at the end of the session (16:01 on SPX, 13:01 on half days), so during the day the previous session's bar is read. Only outputs that can't look ahead (causal) can be read.
Thresholds (≥, ≤)
The value read at t compared with the target: a number, or another output read at t. Equality counts.
Crossings
The sign of (output − target) changes between the reading at t − 1 minute and the reading at t: crosses above from below to at-or-above, crosses below from above to at-or-below, crosses either. It holds only at the minute the crossing bar closes.
Events and bands
Outputs that are 1 or 0 (plotshape, plotchar, bgcolor, alertcondition): is on = ≥ 1, is off = ≤ 0, turns on / turns off = a crossing of ½. With location.absolute a shape's value is the price where it's drawn (empty elsewhere): is on holds on those bars, and an event entry follows them.
Missing values
A value the script leaves na, or no bar closed yet, makes the condition false.
Warm-up
Readings on bars before the indicator's first settled bar (its warm-up, from the start of its data) don't count: an entry attempt is refused as custom indicator still warming up, an exit rule doesn't hold, and positions opened while an exit indicator was still warming up are counted in the warnings.
Combining
Entry conditions all hold together; exit conditions form rules, each holding when all its conditions hold, and the position exits at the first minute any rule holds.
Event entries
Each bar where an event output is non-zero (and not na) closes at a minute that becomes an entry attempt, as above. A bar of the warm-up isn't attempted and is counted as still warming up.

The data covers the indices and ETFs (SPY, QQQ, IWM) Veeman has by the minute. A session without the indicator's bars isn't traded (no indicator data that day). If the script stops with a runtime error, its outputs are empty from that bar on and the warnings say where, also when the bar falls before the backtest period. A crossing that can't happen during the session (a daily indicator against a number, or against another daily reading: the daily bar closes at 16:01) is refused before the run.

Building the position

The entry snapshot

At the entry minute the engine reads the chain as of that minute: for each contract, the last quote with a timestamp not after the entry. A contract is tradable when it has an ask above zero, a mid and a delta. With Round strikes only multiples of the chosen number are tradable.

Strike selection

Independent legs are resolved first, then offset legs (wings). For each leg, among the tradable contracts of its type (with an offer, ask above zero; for a leg you sell, also with a bid, because selling where nobody bids can't be executed):

Delta
Minimise |abs(delta) − target|.
Fixed premium
Minimise |mid − target|.
% OTM
Minimise |strike − spot × (1 − %)| for puts, |strike − spot × (1 + %)| for calls.
Offset from a leg
Minimise |strike − (reference strike + offset)|.
Offset from spot
Minimise |strike − (spot + offset)|.
Absolute strike
Only for recipes that carry a fixed strike (sent through the API, for example): minimise |strike − strike given|. The composer has no such criterion: a leg with a fixed strike reopens there as delta 10, with a notice.

Ties go to the lowest strike, so the selection never depends on the order of the data. If a leg's type has no tradable contract, the attempt fails (strikes not quoted). A target strike that isn't quoted at the entry minute (an offset landing on a missing strike, say) takes the nearest quoted one, the same tie rule included: a 25-point wing can come out 20 or 30 points wide on that day.

With a fixed premium, a tie goes first to the strike nearest the money (closest to spot), and only then to the lowest. Cheap far strikes are quoted on the nickel grid, so ties are the norm there: 0.10/0.15 (mid 0.125) and 0.05/0.10 (mid 0.075) are both 0.025 from a 0.10 target, and a whole run of strikes can share that distance. At the same price the strike nearest the money protects the most; the lowest one would put a bought put at the far end of the run.

With Ignore wide spreads on, the attempt is refused if any selected leg has a spread above 100% of its mid. A spread of one tick (0.05 below $3, 0.10 from $3) is never wide; an ask of zero always is.

Entry fills

Each leg's entry price comes from the fill model (see Fills and costs): a short leg sells (below the mid), a long leg buys (above it). The entry credit, the quote of one structure in $ (as a broker quotes the combo), is

credit = Σ short prices × qty − Σ long prices × qty − position entry slippage

Positive = credit received, negative = debit paid. Then:

  • Short/long ratio: Σ short mids × qty ÷ Σ long mids × qty must be inside the bounds.
  • Premium filter: the credit must have the chosen direction and a magnitude within the bounds. The bounds are typed as the quote ($16) and stored in $ per contract, the quote × m, the contract multiplier of the recipe's underlying (100 on SPX: max $16 = $1,600). Exactly at the bound passes.
  • Sizing: contracts = Max contracts, or with Margin %: min(⌊ starting capital × % ÷ margin ⌋, Max contracts); fewer than one → not enough margin.

The margin per contract is the maximum loss at expiration, max(0, −min over S of P&L(S)) × m, where the payoff is evaluated at zero, at every strike and beyond the highest strike (the payoff is piecewise linear, so the worst case is on one of those points). It is undefined when the structure is net short calls or net short puts; margin sizing then falls back to fixed contracts with a warning.

Marking minute by minute

From the minute after entry to the minute before the close (15:59, 12:59 on half-days), the engine reads the series of only the selected contracts and, for every minute where all legs have a price, computes the cost to close one structure, as a quote in $:

cost = Σ short legs: buy price × qty − Σ long legs: sell price × qty

with the same fill model as the entry (closing side). The open P&L per structure is credit − cost. Minutes where a leg has no bid or ask are skipped rather than valued with a hole (a bid/ask of zero is a real price: a wing worth nothing). This is the only place where the value of a position is computed; values are rounded to 1e-9 so that a P&L exactly equal to a threshold always triggers it, whatever the order the data was read in.

The same pass gives the position delta (Σ long deltas × qty − Σ short deltas × qty) and the short and long premiums at mid used by the ratio exit.

Exits

Thresholds

Target and stop become thresholds on the quote of one structure (in $):

Target
% of premium × |credit|, or the level in $ as typed in the form (the recipe stores it in $ per contract, the quote × m: $5 = 500 on SPX, back to $5 ÷ m). The % wins if both are set.
Stop
−(stop % × |credit|), or −(the level in $ as typed): a stop of $20 is stored as $2,000 per SPX contract and fires when the open P&L reaches −$20.00. The % wins if both are set.
Per-leg stop
For each watched leg: −(stop % × the leg's entry price × its quantity), or −(the level in $ as typed, stored as $ per contract ÷ m). The % wins if both are set.

The target fires when the open P&L is ≥ its threshold, the stop when it is ≤ its level. With Two consecutive touches (target) or Ignore single-bar stop violation (stop), the condition must hold for two minutes in a row and fires on the second.

Per-leg stop

The per-leg stop compares each watched leg's own open P&L with its level, minute by minute: for a short leg, its entry price minus the price to buy it back; for a long leg, the price to sell it minus its entry price; times the leg quantity, on the quote of one structure, at the same closing prices the position is marked with. The watched legs are the ones chosen in the recipe, otherwise the short legs of the position (every leg if the structure has none). When any watched leg's P&L is ≤ its level, the whole position closes at that minute, every leg at its fill, with the exit reason leg stop. With Ignore single-bar stop violation the same leg must stay at or below its level for two minutes in a row. A leg filled at zero premium has no % level and isn't watched: the warnings count those trades. A bought leg can lose at most 100% of its premium, so a % level above 100 never fires on it; the warnings flag a recipe where that happens.

In the composer the per-leg stop is the Stop with Per leg switched on (see Target and stop): a recipe written there has a stop on the structure or a per-leg stop, never both. A saved recipe that has both still runs as written, each stop watching on its own, and at the same minute the structure stop wins.

Full exits

Every other exit rule is evaluated as a mask over the minutes ("is the rule true at this minute?") and fires at its first true minute:

Trailing stop
Armed from entry, or once the open P&L reaches activate after × |credit|. Then it tracks the peak P&L and fires when P&L ≤ peak − width × base, where base = |credit| (Fixed) or the current cost to close, floored at zero (Recalculated).
Early exit
First minute at or after the time.
Underlying move
Index minus entry price ≥ threshold up, or entry price minus index ≥ threshold down (in points, or in % of the entry price).
Delta
Position delta ≤ the lower level or ≥ the upper level. Each leg's delta is the dataset's at that minute: the provider's time convention changes the IV, theta and vega but not the delta, so it needs no conversion (see the dataset conventions). A minute where a quoted leg has no delta gives no position delta, and the rule doesn't fire there.
Tested short
For each short put: index ≤ strike + offset; for each short call: index ≥ strike − offset.
Minutes in trade
First minute at or after entry + N minutes.
Before an economic release
First minute at or after the release time − N minutes, only on the sessions where a chosen release comes out during market hours, and only for positions opened before the release (see Economic events).
Indicators
RSI ≤ / ≥ its levels, or price above/below the SMA or EMA.
Custom indicator
Any of its rules holds on the indicator's last closed bar at that minute (see Custom indicators).
Short/long ratio
Ratio ≤ the lower level or ≥ the upper level.
VIX
VIX or VIX9D moved by the threshold from its value at the entry minute, or VIX9D ÷ VIX ≤ / ≥ its levels.

Cooldown

A rule with a cooldown of N minutes isn't watched for the first N minutes of the trade, starting with the entry minute. Exits are checked once a minute, on the minute's prices: with entry at t, the check at t+1 reads the entry minute (the VIX printed during it, the option prices at its end). The cooldown skips the checks t+1 … t+N, so the rule is watched from minute t+N and the first possible exit is at t+N+1 (entry 10:03, N = 1 → watched from 10:04, first exit 10:05). The reference doesn't move: the VIX, the index and the premium of the entry minute. Each rule has its own cooldown; the stop's covers the per-leg stop and the leg groups' stops, the target's the groups' targets, the VIX exit's the VIX9D ÷ VIX ratio, and one cooldown covers every profit action. Clock-time exits, minutes in trade, the close before an economic release and timed actions have none.

Levels at the end of the cooldown
A target, stop, watched leg or profit action already past its level at the last skipped minute, and still past it when it fires, had no order waiting while the level was crossed: it books at that minute's fill, without the cap. A level crossed after the cooldown keeps Cap profits and Cap the stop-out as usual.
Trailing stop
The trail starts at the first check: the peak, and the activate after threshold, are measured from there.
Two consecutive minutes
Two consecutive touches and Ignore single-bar stop violation count only minutes after the cooldown.
Re-entry
After an exit on a market condition, the re-entry waits while that condition is still true, even though the cooldown would keep the new position open for N minutes; otherwise it would reopen a position every N + 1 minutes.

The exit cause records the cooldown, and whether the rule was already true when it ended.

When several rules fire at the same minute

The engine collects, for the current phase, the first minute of every rule, and takes the earliest. At the same minute, the adverse outcome wins: stop, then per-leg stop, then trailing stop, then the target and the other full exits (in the order listed above), and only then the partial actions.

Partial exits and moving levels

Profit actions and timed actions close part of the position and can move the levels, so the plan runs in phases: when an action fires, its contracts are closed at that minute (⌊ initial contracts × % ⌋, never more than what remains), the stop (and for timed actions the target) moves to its new level, and evaluation restarts from that minute with the updated thresholds (the per-leg stop doesn't move: it keeps watching the legs of the contracts still open). Each action fires once. Levels are always computed on the initial premium. A profit action is subject to Two consecutive touches like the target, and with Cap profits its partial close books at its level.

Leg groups

With leg groups, the legs are entered together and then each group runs its own exit plan: its own premium (the group's legs' credit), its own target and stop, the same other exit rules. Rules that read the position (delta, short/long ratio, tested short) read the group's own legs. The per-leg stop picks its legs on the whole position (by default its short legs, so a group made only of wings doesn't end up watching them); each group watches its own among those and closes that group when one hits its level. A group's target or stop left empty is the recipe's: a percentage applies to the group's premium, a level in $ stays as it is. Each group's exit is a separate row in the trade log; the statistics merge them back into one position.

Settlement

Whatever is still open at the end of the day settles at intrinsic value with the index's last print of the session (13:00 on half-days): max(index − strike, 0) for calls, max(strike − index, 0) for puts. No closing commission and no slippage are charged. If a position never had a minute with all legs quoted after entry, it settles directly.

The minute of the bell belongs to settlement only. At that time an expiring option is no longer bought back, and its quote is often meaningless: a put worth 1.45 at settlement can be quoted 54.80 / 75.60. No exit rule reads it (stop, per-leg stop, target, time exits, Tactics), and neither do the trade log's Max Profit and Max Loss. Whatever no rule has closed by 15:59 settles, just as no entry opens at the bell.

Fills and costs

The fill model

Per position (default)
Every leg fills at its mid rounded to the nickel (0.05, halves rounded up: 1.225 → 1.25). Slippage is a fixed amount of quote, in $, on the whole position (per group with leg groups): subtracted from the entry credit, and added to the cost of an early exit after any cap. The stop slippage applies to stop, per-leg stop and trailing exits, the exit slippage to every other early exit. Never at settlement.
Per leg
Each leg fills at mid ± (fraction × half-spread + a fixed amount in $): sells at mid − slip, buys at mid + slip, never below zero. Fraction 1 = at the bid (selling) or the ask (buying). The price is then put on the exchange tick (0.05 below $3, 0.10 from $3): the nearest one, and exactly halfway between two ticks the one against the trader (down when selling, up when buying). A wing quoted 0.00/0.05 is bought at 0.05. Separate fractions for opening and closing.

The marking used for stops and targets uses the same leg prices as the closing fills. In the per-position model the position slippage is not part of the marking (it is a cost of executing, not a level the market reached), so a target is triggered by the market and the slippage is then charged on the fill.

Commissions

commission = rate × Σ leg quantities × contracts, per transaction: the open rate at entry, the close rate at each early exit, nothing at settlement. A 1-2-1 butterfly pays four commissions per transaction, not three.

Booking an exit

For each exit event (the whole position, a partial close or a group):

  1. Gross at the fill: (credit − close value) × contracts × m, with m the contract multiplier of the underlying (100 on SPX).
  2. Cap: with Cap profits, a target (or profit action) books at most its threshold; with Cap the stop-out, a stop books no worse than its level, and a per-leg stop books the leg that hit it at its level (the other legs at their fill).
  3. Position slippage for that exit, × contracts × m (per-position model, early exits only).
  4. Commissions: the opening commission pro rata (contracts closed ÷ initial contracts) plus the closing commission of the contracts closed.
  5. Booked P&L = gross after cap − slippage − commissions.

Each row of the trade log carries this exit cause (the rule, its parameter and who set it, the threshold, the value observed, the booking): it is what Rewind tells you at the exit minute.

It also carries an entry cause, recorded when the position opens: the trigger (the scheduled time, the first minute of the floating window that passed every filter with the minutes held back by each filter, the re-entry after a given exit, or the entry signal), every active entry filter with the value read at that minute and its bounds, and the strike criterion of each leg with the value it found. The values are the ones the filters compared, so they always satisfy the rules: if the position exists, every filter passed. Rewind tells it at the entry minute.

Tactics in the backtest

When the recipe arms one or more Tactics, after the day's main positions are computed the engine replays each one minute by minute and runs the Tactics' rules mechanically: a fresh ladder of each Tactic for every trade, adjustments opened without confirmation, each with its own exits or held to settlement. Rungs of different Tactics that fire in the same minute open in the order the Tactics were armed. The recipe's target and stop act on the main structure only. Adjustments are separate rows in the trade log (marked Tactic), their P&L is part of the day's P&L and of the equity; win rate and the per-trade figures count the main positions only. Details in Tactics.

In a portfolio: the strike guard

A backtest runs alone. When it is part of an Analysis portfolio with other Veeman backtests or strategies imported from a file, the strike guard can re-run it so that it never opens a contract another strategy already holds at that minute. It acts at the moment the strategy has decided to enter (filters, premium, sizing and the re-entry wait have all passed): the chosen structure is shifted one strike step, or the entry is skipped, according to the strategy's rule. A shifted structure is priced with the same fill model at that minute and keeps its contract count; a shifted strike with no market skips the entry. The archived backtest itself is never re-run in place.

Warnings

The engine never drops a condition in silence: every approximation is reported, aggregated by type (never one line per day) with the count and the dates involved. The ones you may see:

Partial closes not executed
A profit or timed action asked for a percentage that didn't reach a whole contract. The stop move was still applied.
Technical-indicator exit missing
In the first days of the data there weren't enough prior closes to compute the exit indicator.
Positions closed early
Positions closed to make room for a new entry (open-position limit).
Delta beyond the level at entry
Positions the delta exit closed at their first check (the first minute, or the end of its cooldown) because their position delta was already beyond the level when they opened. Usually a sign mistake: a short call counts negative.
Delta level out of reach
A delta exit level the legs can never reach, so the rule never fires. Each leg moves the position delta between 0 and its quantity × 100, upward for long calls and short puts, downward for short calls and long puts: a lone short call stays between −100 and 0, so above 30 is out of reach.
Days without minute VIX
The intraday VIX filters and exits weren't applied on those days.
Days without the VIX open
VIX moves were measured from the 09:32 value.
VIX level from the prior close
On days without minute VIX the Min/Max filter used the previous close.
VIX value not found
An intraday VIX condition couldn't read its value at the requested time and was ignored.
VIX filter not applied
Days with neither minute VIX nor a previous close: they entered without the filter.
Entry premium filter
The premium filter refused every entry. It gives the range of the premiums it refused, as quotes in $ and in $ per contract; if every position opened in the other direction (a credit when the filter asks for a debit, or the reverse) it says to switch the direction.
Margin sizing not applied
The structure has an uncovered side; the run used fixed contracts. On calls the loss has no upper limit; on puts it is limited, but a broker margins uncovered puts with its own rules, which the backtest doesn't model.
Stop beyond the maximum loss
Trades whose stop loss was larger than the most the position could lose at expiration (on a debit structure, the debit paid): the stop practically never fires, only closing costs could push the marked loss past it.
Tactics builds skipped
An adjustment couldn't be built (chain or spot unavailable at the trigger minute).
Custom indicator exit warming up
Positions opened while an indicator of the custom exit was still in its warm-up: until it settled, that exit couldn't fire.
Indicator events outside market hours
Event bars that closed at or before 9:30, or at or after the close, so no entry was attempted. A daily bar always closes at or after the close.
Indicator signals skipped
Signals that came while the open positions were at their limit (one at a time, unless With a position open lets a signal open another).
Days outside the indicator's data
Market days the indicator's data doesn't cover (the chart's and the requested symbols'): when the data ends, when it starts, or the days without bars.
Indicator stopped with an error
The script hit a runtime error on a bar of the period, or before it: from that bar on its outputs are empty and its conditions don't hold.
Day-level data issues
For example an empty chain at entry: one line per type with the number of days.

Reproducibility

The engine is a pure function of the recipe and the data: the same recipe on the same dataset version gives the same result to the cent, whatever the number of threads. Tie-breaks are deterministic (lowest strike), equality with a threshold is exact, and the recipe and the dataset version are frozen inside every result and every saved backtest.

A recipe with custom indicators carries each indicator's script, inputs, chart and the version of the compiler that ran it. A saved backtest run again uses the current compiler on that same script: if a correction to the compiler changed the numbers, the recorded compiler version says so. New index data or a new compiler always means a new computation, never a result served from before.