Skip to main content

Strategy Definition Reference

This page explains every part of a strategy definition: how to describe your structure, control entry/adjustment/exit behavior, bring in external data, and configure simulator/core settings.

Coming from an older version? See the Migration Guide for v1→v2 and v2→v3 changes. Many fields accept expressions evaluated by the Script Engine; time windows and anchors are described in Timing; valuation tools are covered in Options Valuation Model.

Use the AI Job Editor or work with AI Agents to configure these fields. For a guided first run, see Create and run a backtest; for full strategies with analysis, visit the Deltaray blog.

Find a section

SectionWhat it controls
Top level fieldsStrategy name, description, and symbol selection
BacktestRun name, dates, and starting capital
StructureExpirations, legs, quantities, and strike selection
EntryEntry schedule, filters, sizing, and captured variables
ExitProfit target, stop loss, maximum duration, and exit conditions
AdjustmentConditions for moving, removing, or adding legs and updating variables
External CSV DataImported signals and indicators
SettingsFills, costs, position monitoring, margin, and selection constraints
Expressions Quick ReferenceExpression types and evaluation timing

When schedules coincide at the same simulation timestamp, MesoSim processes exits → adjustments → entries. Each operation still follows its own schedule and conditions.

The examples below show either a complete illustrative definition or selected sections. Merge section examples into your strategy while retaining its other required fields.

Full Strategy Definition

This illustrative definition shows how the main sections fit together. For a runnable starting point, choose a built-in strategy from the Strategy Library.

Complete Strategy Definition
{
"StrategyName": "[FULL]",
"Description": "",
"Backtest": {
"Name": "generate",
"Start": "2021-01-01T00:00:00",
"End": "2021-12-31T00:00:00",
"Cash": 10000.0
},
"Symbol": "SPX",
"Structure": {
"Name": "ShortStrangle",
"Expirations": [
{
"Name": "exp",
"DTE": "160",
"Min": 140,
"Max": 190,
"Roots": {
"Include": [
"SPXW",
"SPX"
],
"Exclude": null
}
}
],
"Legs": [
{
"Name": "short_call",
"Qty": "-1",
"ExpirationName": "exp",
"StrikeSelector": {
"Min": 5,
"Max": 15,
"BidPrice": null,
"AskPrice": null,
"MidPrice": null,
"Delta": "10",
"StrikePrice": null,
"Complex": null
},
"OptionType": "Call"
},
{
"Name": "short_put",
"Qty": "-1",
"ExpirationName": "exp",
"StrikeSelector": {
"Min": 5,
"Max": 15,
"BidPrice": null,
"AskPrice": null,
"MidPrice": null,
"Delta": "-1 * leg_short_call_delta",
"StrikePrice": null,
"Complex": null
},
"OptionType": "Put"
}
]
},
"Entry": {
"Schedule": {
"AfterMarketOpenMinutes": null,
"BeforeMarketCloseMinutes": 30,
"Every": "day"
},
"Conditions": [],
"VarDefines": {
"initial_theta": "pos_theta"
},
"AbortConditions": [
"pos_theta < 20"
],
"QtyMultiplier": "1",
"ReentryDays": 1,
"Concurrency": {
"MaxPositionsInFlight": 2,
"EntryShiftDays": 3
}
},
"Adjustment": {
"Schedule": {
"AfterMarketOpenMinutes": null,
"BeforeMarketCloseMinutes": 30,
"Every": "day"
},
"ConditionalAdjustments": {
"pos_delta > 5": {
"MoveLegAdjustment": {
"LegName": "short_call",
"ExpirationName": null,
"Expirations": null,
"StrikeSelector": {
"Min": null,
"Max": null,
"BidPrice": null,
"AskPrice": null,
"MidPrice": null,
"Delta": "abs(pos_delta - leg_short_call_delta) / abs(leg_short_call_qty)",
"StrikePrice": null,
"Complex": null
}
},
"RemoveLegsAdjustment": null,
"AddLegsAdjustment": null,
"UpdateVarsAdjustment": null
},
"pos_delta < -5": {
"MoveLegAdjustment": {
"LegName": "short_put",
"ExpirationName": null,
"Expirations": null,
"StrikeSelector": {
"Min": null,
"Max": null,
"BidPrice": null,
"AskPrice": null,
"MidPrice": null,
"Delta": "abs(pos_delta - leg_short_put_delta) / abs(leg_short_put_qty)",
"StrikePrice": null,
"Complex": null
}
},
"RemoveLegsAdjustment": null,
"AddLegsAdjustment": null,
"UpdateVarsAdjustment": null
}
},
"MaxAdjustmentCount": 5
},
"Exit": {
"Schedule": {
"AfterMarketOpenMinutes": null,
"BeforeMarketCloseMinutes": 30,
"Every": "day"
},
"MaxDaysInTrade": 90,
"ProfitTarget": "pos_theta * 160 * 0.5",
"StopLoss": "pos_theta * 160 * 0.5 * 3",
"Conditions": [
"initial_theta > pos_theta * 4"
]
},
"ExternalData": null,
"Settings": {
"Sim": {
"FillModel": "AtMidPrice",
"SlippageAmt": 0,
"Commission": {
"CommissionModel": "FixedFee",
"OptionFee": 1.5,
"DeribitCommissionSettings": null
},
"PositionMonitor": {
"TraceCollectionInterval": "Hourly"
}
},
"Core": {
"LegSelectionConstraint": "UniqueInPosition",
"ExpirationSelectionConstraint": "AbortOnReuse",
"Margin": {
"Model": "RegT",
"HouseMultiplier": null,
"RegTMode": "CBOEPermissive",
"PMConfig": null
}
},
"User": null
},
"MesoSimVersion": "__VERSION__"
}

Sections

Top level fields

The following top-level fields and sections control the job:

  • StrategyName:
    The name of the strategy.
  • Description:
    Optional description of the strategy.
  • Symbol:
    Specifies the underlying of the trade.
  • MesoSimVersion:
    Execution engine’s version. Automatically populated.

Backtest

Controls the backtest run itself:

  • Name:
    User-provided name of the backtest. If set to generate, a memorable name is created.
  • Start:
    First date and time of the simulation. Format: YYYY-MM-DDTHH:mm:ss
  • End:
    Last date and time of the simulation. Format: YYYY-MM-DDTHH:mm:ss
  • Cash:
    Initial cash (expression). Must evaluate to a number greater than 0.

Expressions in this section

FieldTypeWhen evaluatedNotes
Backtest.CashnumberAt validation and job startMust be > 0; expression allowed

Structure

This section defines the combination of option contracts that are traded together to create a structure.

Each Option Contract is uniquely defined by its:

  • Underlying instrument (such as SPX, GLD, or RUT)
  • Expiration (e.g., 2022-05-18)
  • Type: Put or Call
  • Strike (e.g., 3500)

Currently, MesoSim doesn't support structures created for multiple Underlyings; hence the underlying instrument can be defined top-level via the Symbol parameter.

Expirations

Expiration selection is made dynamically during options trading. Similarly, during backtesting, the traded expirations are dynamically selected at a given simulation time. MesoSim specifies expiries by adding calendar days to the current simulation time. That is if we started our simulation back in 2008. January 2, and we specify that we are planning to trade options 30 days out (DTE: days till expiration), then option contracts will be selected that expire around 2008 February.

The Structure.Expirations define a list of expirations that are used during trading. At least one should be provided, but multiple expiries are also supported:

{
"Expirations": [
{
"Name": "front",
"DTE": "90",
"Min": 50,
"Max": null
},
{
"Name": "back",
"DTE": "expiration_front_dte + 60",
"Min": 140,
"Max": 190
}
]
}

The above snippet defines two expirations with unique names: front and back. Later, during leg definitions, these names will be used to refer to expiries defined in this section (Structure.Legs.ExpirationName references Structure.Expirations). It is a good practice to keep things simple and expressive; hence front is considered a good name. The Name field is mandatory for every Expiration.

The DTE field defines how many days out should an option contract be selected. As simulation time passes and entry is considered, the DTE statement is evaluated (by the Lua Script Engine) to find an option contract to trade. Note that expiry selection using DTE is not strict: the closest expiry will be chosen for the given DTE. Referring to other leg's DTE field is possible via the expiration_NAME_dte variable. As defined above, the back DTE will be calculated once the front 's exact DTE is found by adding 60 days to it.

The DTE field is mandatory for every Expiration.

The Min and Max are optional fields and are used to create a subset of the available expirations at any given time. Using these fields, one can avoid choosing expiries that are either too far out or too close to the current simulation time. Defining a narrow range will result in less (or zero) trades than a loose range. As Min and Max are both optional, they can be turned off by setting them to null.

The Roots field enables users to filter (include or exclude) specific OCC Option Symbols, such as SPXW, SPX, or - in early days - SPXPM, SZP, etc.

The Include field doubles as a Priority List: The order of the listed symbols is taken into account during Expiration selection. For example, when the following root filter is specified:

{
"Roots": {
"Include": [
"SPXW",
"SPX"
],
"Exclude": null
}
}

Then, in case of multiple matching Expirations at the given DTE, the first item in the Include list will be chosen: SPXW.

When no Include has been specified, the Roots are ordered in the following manner:

  • SPX: SPXW, SPX, lexicographic order of the rest
  • GLD: single root: GLD
  • RUT: RUTW, RUT, lexicographic order of the rest
  • VIX: VIX, lexicographic order of the rest

For historical crypto conventions, see Legacy crypto settings.

Legs

The section “Legs” defines option contracts to trade as part of the structure. Each leg has its unique Name, associated expiration (ExpirationName), option type (OptionType), target quantity (Qty), and a strike selector (StrikeSelector):

{
"Legs": [
{
"Name": "short_call",
"Qty": "-1",
"ExpirationName": "front",
"StrikeSelector": {
"Min": 5,
"Max": 15,
"BidPrice": null,
"AskPrice": null,
"MidPrice": null,
"Delta": "10",
"StrikePrice": null,
"Complex": null
},
"OptionType": "Call"
}
]
}
  • Name:
    The unique name of the leg. Later, this name will be used when adjustments are made to the structure. Additionally, it makes job inspection and debugging easier.

  • Qty:
    Defines the number of contracts to be traded. If negative, a short position is taken. Index and equity options trade in whole contracts.

  • ExpirationName:
    Reference back to the expiration defined in the Expirations section.

  • OptionType:
    Defines the option type to be traded. Either Put or Call.

  • StrikeSelector:
    Defines how strikes are selected. Exactly one selector must be defined per leg.

  • Min / Max:
    Optional constraints referring to the selector’s metric (price or delta). Set to null to disable.

  • BidPrice / AskPrice / MidPrice:
    Select the strike closest to the specified quote-based value. For an adjustment after Entry.VarDefines has defined initial_theta, an example target is: BidPrice=(initial_theta * 60 * 0.33) / 100

  • Delta:
    Select the strike closest to a non-negative, per-contract delta magnitude on the 0–100 scale. For example, 10 targets an absolute contract delta of 0.10. Leg and position delta variables include quantity and sign; convert them to a per-contract magnitude when using them as a target.

  • StrikePrice:
    Directly target a strike by expression. See Select a leg at a fixed strike distance for an example.

    Example – StrikePrice selector to offset another leg by 25 points:

    {
    "StrikePrice": "leg_short_put_strike + 25"
    }
  • Complex:
    The complex strike selector iterates through all the contracts within the given expiration and chooses the strike that best aligns with the specified criteria. 

    {
    "Complex": {
    "Statement": "leg_long_strike",
    "Target": "underlying_price + 20",
    "Constraints": [
    "leg_long_strike > underlying_price"
    ]
    }
    }

    The processing begins with walking through all contracts within the specified expiration and the Constraints are evaluated (if they present). If all Constraints evaluate to true (or no if constraints are specified) then the Statement is calculated and stored in the inclusion list. Once all contracts are processed, the Target statement is evaluated. Finally, the contract that is closest to the Target is selected from the inclusion list.

    The Complex StrikeSelector snippet shown above selects contracts that are 20 points higher than the At The Money strike, while ensuring that the chosen contract will always have a strike price higher than the current price of the underlying. 

    Complex Strike Selector Note

    This selector iterates over all contracts it can be used to create spreads dynamically and balance more complex structures (such as BWBs) based on custom criteria. For examples, see Finding Spreads.

Expressions in this section

FieldTypeWhen evaluatedNotes
Structure.Expirations[].DTEnumberDuring structure resolutionTarget DTE per alias; must be ≥ 0
Structure.Expirations[].Min / MaxnumberDuring structure resolutionOptional DTE constraints; non‑negative; Min ≤ Max
Structure.Legs[].QtynumberEntry and AddLegs adjustmentBase quantity, scaled by Entry.QtyMultiplier
Structure.Legs[].LegGroupIdnumberEntry and adjustmentsInteger order-group identifier, including order-based slippage groups
Structure.Legs[].StrikeSelector.[BidPrice / AskPrice / MidPrice / Delta / StrikePrice]numberWhen selecting strikesExactly one selector per leg
Structure.Legs[].StrikeSelector.Min / MaxnumberWhen selecting strikesOptional constraints for strike selection

Entry

The Entry section specifies when and how entries are made. A schedule is required. ReentryDays controls the delay after an exit, while EntryShiftDays spaces entries apart. Both accept fractional days.

{
"Entry": {
"Schedule": {
"AfterMarketOpenMinutes": null,
"BeforeMarketCloseMinutes": 30,
"Every": "day"
},
"Conditions": [],
"QtyMultiplier": "1",
"VarDefines": {
"initial_theta": "pos_theta"
},
"ReentryDays": 1
}
}

Schedule

Defines when entry is considered using time relative to the session open (AfterMarketOpenMinutes) or close (BeforeMarketCloseMinutes). These anchors follow the exchange session, including early closes. Choose offsets and a cadence compatible with the selected market-data resolution; execution is limited to its supported times. See Timing for timing functions.

Daily and weekday schedules require exactly one of AfterMarketOpenMinutes or BeforeMarketCloseMinutes. Intraday schedules can use both to bound a window, one to bound one side, or neither for the regular session. A window must contain a supported execution time. Offset expressions are evaluated when the schedule is initialized; use conditions for changing intraday rules.

Intraday cadence is anchored to the session open; the bounds filter that cadence rather than restart it. For example, Every: "30min" with AfterMarketOpenMinutes: 45 first considers an equity/index entry 60 minutes after open.

The Every field defines the frequency when entry is attempted:

  • day: For strategies running once a day
  • A comma-separated weekday list, such as mon,wed,fri: once on each selected trading day.
  • 5min: Intra-day mode, the Entry, Exit or Adjustment is considered every 5 minutes
  • 30min: Intra-day mode, the Entry, Exit or Adjustment is considered every 30 minutes
  • Other positive Nmin intervals must be compatible with the selected market-data resolution.
Examples:
  1. Try to enter every day, 30 minutes before close:

    {
    "Schedule": {
    "AfterMarketOpenMinutes": null,
    "BeforeMarketCloseMinutes": 30,
    "Every": "day"
    }
    }
  2. Try to enter 30 minutes after open every Mon, Wed, Fri:

    {
    "Schedule": {
    "AfterMarketOpenMinutes": 30,
    "BeforeMarketCloseMinutes": null,
    "Every": "mon,wed,fri"
    }
    }

Conditions

This section specifies a list of statements, any of which need to become true to enter the position. This field can filter trades based on the variables available via the Script Engine.

For example, using conditions, it becomes possible to enter only on down days:

{
"Conditions": [
"underlying_price < underlying_today_open"
]
}

Conditions are combined with OR: any true expression allows an entry attempt. An empty list imposes no additional condition; the schedule, concurrency, re-entry delay and other entry checks still apply. To require several criteria together, combine them in one expression with Lua and. For example, when my_entry_signal is supplied by External Data:

{
"Conditions": [
"underlying_price < underlying_today_open and my_entry_signal == 1"
]
}

Expressions in this section

FieldTypeWhen evaluatedNotes
Entry.Schedule.AfterMarketOpenMinutesnumberAt schedule initializationRelative offset applied to each session
Entry.Schedule.BeforeMarketCloseMinutesnumberAt schedule initializationRelative offset applied to each session
Entry.Conditions[]booleanOn Entry scheduleAny true triggers entry attempt
Entry.AbortConditions[]booleanAfter leg selection, sizing and Entry.VarDefinesChecked before placing orders; any true aborts entry
Entry.VarDefines{}numberBefore abort checks and again after entry fillsRecords variables for later use (e.g., initial_theta)
Entry.QtyMultipliernumberAfter selection at entry and again when adding legsScales new leg quantities using current values
Entry.ReentryDaysnumberBefore entry selectionMinimum elapsed days since the last exit; fractions allowed
Entry.Concurrency.MaxPositionsInFlightnumberOn Entry scheduleGoverns maximum concurrent positions
Entry.Concurrency.EntryShiftDaysnumberOn Entry scheduleMinimum elapsed days since the last entry; fractions allowed
warning

Entry.Conditions, ReentryDays and Concurrency are checked before legs are selected. Selected-leg, expiration and position variables are unavailable there; they are not zero-valued placeholders. Use Entry.AbortConditions for filters based on the selected structure.

Variable Definitions

The VarDefines section enables the user to capture the state during entry. Definitions are evaluated in order before AbortConditions, then evaluated again after entry fills. A later definition can use an earlier one. The resulting values are available for later Adjustment and Exit expressions.

For example, capture the structure’s Theta at entry, then compare it with the current Theta in an adjustment or exit condition:

{
"VarDefines": {
"initial_theta": "pos_theta"
}
}

AbortConditions

This section specifies a list of statements; when any of which evaluates to true, the entry is aborted. This field is evaluated after the leg selection is complete. Therefore, it can filter trades based on the initial state of the structure to be taken.

For example, using AbortConditions, it becomes possible to enter only when a reasonable amount of Theta is gained:

{
"AbortConditions": [
"pos_theta < 40"
]
}

When multiple conditions are specified, the entry is aborted when any of the statements become true.

QtyMultiplier

Entry.QtyMultiplier scales the selected legs’ quantities using an expression, for example to size a structure from account NAV. At initial entry it runs after leg selection and before Entry.VarDefines, so it can use selected-leg and position variables but cannot use a variable first defined in that entry’s VarDefines.

The same expression is evaluated again when AddLegsAdjustment sizes new legs, using values available at that time. It is not a multiplier fixed for the position’s lifetime. MoveLegAdjustment preserves the moved leg’s existing quantity.

See Set quantities dynamically for sizing examples.

ReentryDays

Entry.ReentryDays sets the minimum elapsed time since the last position exit before another entry is allowed. It defaults to 0 and accepts fractional days: 1 means 24 hours, while 0.5 means 12 hours. See the fractional-day guidance below when choosing a value for live trading.

Concurrency

The Entry.Concurrency section contains the settings for the parallel positions in flight. The way of concurrency is controlled using two variables:

  1. MaxPositionsInFlight:
    Defines how many parallel positions should be taken at the maximum. The number of parallel positions can be less than this if the entry conditions do not enable position entry.
  2. EntryShiftDays:
    Minimum elapsed days between entries. Fractional values are valid: 1 requires 24 hours after the last entry, while 0.5 requires 12 hours.
{
"Entry": {
"Concurrency": {
"MaxPositionsInFlight": 4,
"EntryShiftDays": 3
}
}
}

Fractional entry delays

Both delays use elapsed time rather than calendar dates. With a daily entry schedule, 1 allows the next day's entry only once a full 24 hours has passed since the relevant entry or exit.

Allow time for live execution

For daily strategies in live trading, a fractional value such as 0.5 is often a better choice than 1. If an entry scheduled for 10:00 is recorded at 10:01, EntryShiftDays: 1 blocks the next day's 10:00 entry because only 23 hours and 59 minutes have passed. The same elapsed-time rule applies to ReentryDays, measured from the last exit.

Using 0.5 gives room for the time needed to enter or exit. Keep the daily entry schedule to control when an entry is considered; with an intraday schedule, a shorter delay can also allow another entry within the same day.

Exit

Exit is optional. It defines early-exit criteria for a position; without it, normal expiration and settlement still apply. An Exit closes the remaining structure. To remove selected legs, use RemoveLegsAdjustment.

Schedule

Exit schedules follow the same rules as the Entry schedule. Additional to the schedule specification described in the Entry selection, it is possible to run the algorithm in intraday mode and find exits opportunistically by specifying a 5min value for the Every field:

{
"Schedule": {
"AfterMarketOpenMinutes": 30,
"BeforeMarketCloseMinutes": null,
"Every": "5min"
}
}

ProfitTarget, StopLoss and MaxDaysInTrade are initialized at entry. Their expressions are not recalculated at every exit check. Use Exit.Conditions for rules that must follow changing values. For a normal close, Exit.VarDefines runs after the exit decision and before closing fills; for a position closed by expiration, it runs after settlement. It does not supply inputs to the exit decision.

Maximum days in trade

Trades will be held for this many days unless other conditions (Profit Target, Stop Loss, Conditions) cause an early exit.

Profit target

The desired profit target where a trade should be exited. This field takes an expression, which allows describing complex scenarios. As an example:

{
"ProfitTarget": "pos_theta * 160 * 0.5"
}

Defines a profit target as the projected total theta obtained by holding to the position for 160 days multiplied by a 50% discount factor.

Stop loss

When the loss of our overall structure reaches the value defined by the stop loss expression, an early exit will be performed. It is a common practice to set the StopLoss to a multiplier of the Profit Target:

{
"StopLoss": "pos_theta * 160 * 0.5 * 3"
}

Conditions

Exit conditions are defined similarly to Entry conditions. Let’s say we want to exit when the theta potential of the position degrades to 25%. This could be achieved by defining a variable at entry, then using that variable in the exit condition:

{
"Entry": {
"VarDefines": {
"initial_theta": "pos_theta"
}
},
"Exit": {
"Conditions": [
"initial_theta > pos_theta * 4"
]
}
}

When comparing timing values (e.g., leg_*_dte, days_in_trade), avoid exact equality checks because these are decimals; prefer tolerant comparisons (for example, leg_long_put_dte <= 1) or cast to integer (int(leg_long_put_dte) == 1).

Expressions in this section

FieldTypeWhen evaluatedNotes
Exit.Schedule.AfterMarketOpenMinutesnumberAt schedule initializationRelative offset applied to each session
Exit.Schedule.BeforeMarketCloseMinutesnumberAt schedule initializationRelative offset applied to each session
Exit.MaxDaysInTradenumberInitialized at entry; checked on Exit scheduleMust be > 0
Exit.ProfitTargetnumberInitialized at entry; checked on Exit scheduleExit threshold (profit)
Exit.StopLossnumberInitialized at entry; checked on Exit scheduleExit threshold (loss)
Exit.Conditions[]booleanOn Exit scheduleAny true expression triggers exit
Exit.VarDefines{}numberBefore normal closing fills; after settlement when the position has expiredRecord variables at exit

Adjustment

The optional Adjustment section changes an open position or updates its variables when a ConditionalAdjustments condition is met. It requires a schedule, using the same rules as the Entry schedule.

MaxAdjustmentCount limits adjustment actions, including UpdateVarsAdjustment. A block containing several actions can consume several counts. Before each matching block, MesoSim checks the count: if it has already reached the limit, the position is closed instead of executing that block.

The following snippet contains two conditional adjustments. Please note that not all the fields of the StrikeSelector are shown. For the strike-selector fields, see Legs.

{
"Adjustment": {
"Schedule": {
"BeforeMarketCloseMinutes": 30,
"Every": "day"
},
"ConditionalAdjustments": {
"pos_delta > 5": {
"MoveLegAdjustment": {
"LegName": "short_call",
"StrikeSelector": {
"Delta": "abs(pos_delta - leg_short_call_delta) / abs(leg_short_call_qty)"
}
}
},
"pos_delta < -5": {
"MoveLegAdjustment": {
"LegName": "short_put",
"StrikeSelector": {
"Delta": "abs(pos_delta - leg_short_put_delta) / abs(leg_short_put_qty)"
}
}
}
},
"MaxAdjustmentCount": 5
}
}

In the above example, we create two Conditional Adjustments.

The ConditionalAdjustments section is a JSON Map (aka. dictionary), which maps keys (such as the pos_delta < -5 statement) to values (such as MoveLegAdjustment structure).

In MesoSim, the keys of this map are statements executed by the Script Engine. The statements must evaluate to bool (true or false) to signal the simulator if the adjustment should be activated or not. In the above example, we have two entries (key-value pairs) in the map:

  • When the structure delta moves beyond 5, we move the short_call leg.
  • When the structure delta moves below -5, we move the short_put leg.

ConditionalAdjustments are evaluated in their definition order. All conditions are checked before the matching adjustment blocks execute in that order. A change made by one block does not cause another condition to be rechecked during that pass.

Within each matching block, actions run in this order: RemoveLegsAdjustment → MoveLegAdjustment → AddLegsAdjustment → UpdateVarsAdjustment. If removing legs closes the whole position, no later action in that block runs. The [FEAT-MultiLegAdjustment] template in the Strategy Library demonstrates multiple adjustments.

MoveLegAdjustment

MoveLegAdjustment closes the named leg and replaces it using a new strike selector. It can change the strike, the expiration, or both, while preserving the leg’s quantity.

The delta-balancing examples above use the same targets as the built-in SPX-ShortStrangle-Adjusting template. To move the short call, subtract its current delta from the position delta, then take the absolute value and divide by its absolute quantity:

abs(pos_delta - leg_short_call_delta) / abs(leg_short_call_qty)

For example, a short put with total delta +10 and two short calls with combined delta −4 give pos_delta = +6. The target is abs(6 - (-4)) / abs(-2) = 5 per call contract. Replacing the calls near that delta brings their combined delta near −10, balancing the put. The available strikes determine how close the result is to neutral.

The mandatory StrikeSelector is used to specify the new strike, while the optional Expirations and ExpirationName can move the leg in time.

For example:

{
"ConditionalAdjustments": {
"pos_delta > 5": {
"MoveLegAdjustment": {
"LegName": "short_call",
"StrikeSelector": {
"Delta": "abs(pos_delta - leg_short_call_delta) / abs(leg_short_call_qty)"
},
"ExpirationName": "exp2",
"Expirations": [
{
"Name": "exp2",
"DTE": "100",
"Min": 80,
"Max": 120,
"Roots": null
}
]
},
"RemoveLegsAdjustment": null,
"AddLegsAdjustment": null
}
}
}

Check the resulting Greeks in Backtest Results and inspect the action and captured variables in the Events Viewer. See Rebalance delta with Adjustments for a worked strategy.

RemoveLegsAdjustment

RemoveLegsAdjustment closes the named legs, realizing their profit or loss. Supply the names in LegNames:

{
"RemoveLegsAdjustment": {
"LegNames": [
"shorts"
]
}
}

If all remaining legs are removed, the position closes. Any subsequent entry must still meet the entry schedule and rules.

RemoveLegsAdjustment can be combined with MoveLegAdjustment. If the two are coupled together, then first, the RemoveLeg action will be taken, then the MoveLeg will be executed. This setup enables balancing the structure after the leg is removed.

AddLegsAdjustment

AddLegsAdjustment adds one or more legs, with optional Expirations, AbortConditions and VarDefines. It selects and sizes the new legs before checking abort conditions; any true abort condition prevents the addition. VarDefines runs after the new-leg fills, so a variable first defined there cannot be used in the same action’s abort condition.

This example adds a long put only when its price does not exceed the short put’s price captured at entry. First add this variable to Entry.VarDefines:

{
"Entry": {
"VarDefines": {
"initial_short_put_price": "leg_short_put_price"
}
}
}

Then place the following action under your adjustment condition:

{
"AddLegsAdjustment": {
"Legs": [
{
"Name": "long_put",
"Qty": "1",
"ExpirationName": "exp2",
"StrikeSelector": {
"StrikePrice": "leg_short_put_strike + 25"
},
"OptionType": "Put"
}
],
"Expirations": [
{
"Name": "exp2",
"DTE": "100",
"Min": 90,
"Max": 110,
"Roots": null
}
],
"AbortConditions": [
"leg_long_put_price > initial_short_put_price"
]
}
}

The selection fields follow the corresponding top-level definitions:

New legs are scaled by Entry.QtyMultiplier, re-evaluated at adjustment time. When Expirations is omitted, use an expiration name already defined for the position.

The following built-in templates demonstrate the Add Legs Adjustment functionality:

  • SPX-AddLegAdjustment
  • SPX-AddPDSAdjustment

For additional patterns to construct and balance multi‑leg structures dynamically, see Finding Spreads.

UpdateVarsAdjustment

UpdateVarsAdjustment records or updates custom variables without placing an order. It runs when its condition matches on the adjustment schedule. For example, capture the current Greeks when the absolute position delta exceeds 10:

{
"ConditionalAdjustments": {
"abs(pos_delta) > 10": {
"UpdateVarsAdjustment": {
"VarDefines": {
"observed_delta": "pos_delta",
"observed_theta": "pos_theta"
}
}
}
}
}

Each matching execution updates these values and counts toward MaxAdjustmentCount. This records the state; it does not rebalance the position. To capture state specifically after moving a leg, put VarDefines directly in MoveLegAdjustment, as shown in Events export.

Variable update timing

Each adjustment action accepts VarDefines. Definitions run in their written order, so later definitions can use earlier ones.

ActionWhen its VarDefines runs
RemoveLegsAdjustmentAfter the specified legs have been closed and removed
MoveLegAdjustmentAfter the replacement leg fills
AddLegsAdjustmentAfter the added legs fill, following that action’s abort checks
UpdateVarsAdjustmentIn its action slot, after any other actions in the same block

For captures at the beginning or end of a position, use Entry.VarDefines or Exit.VarDefines. Record only the variables needed for your analysis; see Events export.

Expressions in this section

FieldTypeWhen evaluatedNotes
Adjustment.Schedule.AfterMarketOpenMinutesnumberAt schedule initializationRelative offset applied to each session
Adjustment.Schedule.BeforeMarketCloseMinutesnumberAt schedule initializationRelative offset applied to each session
Adjustment.ConditionalAdjustments{ condition }booleanOn Adjustment scheduleKey is a condition; true triggers adjustments
MoveLegAdjustment.StrikeSelector.[…]numberWhen selecting strikesSame semantics as Structure.Legs[].StrikeSelector
AddLegsAdjustment.Expirations[].(DTE/Min/Max)numberDuring adjustment expiration resolutionSame semantics as Structure.Expirations
AddLegsAdjustment.Legs[].(Qty/StrikeSelector)numberWhen selecting and sizing legsSame semantics as Structure.Legs
AddLegsAdjustment.AbortConditions[]booleanAfter new-leg selection and sizing, before fillsAny true expression aborts the addition
*.VarDefines{}numberIn the action’s variable-update phase, as listed aboveUpdate or record variables
Adjustment.MaxAdjustmentCountnumberBefore each matching adjustment blockMust be ≥ 0; counts actions, including variable updates

Indicators

Indicators were removed in v3 (see the Migration Guide). Use External CSV Data to bring your own variables into the simulation.

External CSV Data

Load data from the CSV file and make it available to the backtest.

The ExternalData.CsvUrl allows users to bring their data and use it in the backtest. The CSV columns become variables in the supported expression scopes after the file is loaded. See variable availability.

Requirements:

  • The CSV file must have a header row with the names of the columns/variables.
  • The first column must be named date or datetime and can contain date or datetime values
  • The rest of the columns must have unique Lua variable names (letters, digits and underscores, starting with a letter or underscore) that do not conflict with the simulator's internal variables
  • The CSV file must be at most 2 MiB
  • The CSV file must be publicly accessible either via Github Gist or Google Sheets's "Publish to the web" feature
  • Data columns must contain numeric values, and timestamps must be strictly increasing.
  • Include data at least two days before the backtest start and two days after its end. A sample more than five days old causes a data error.
Behavior

A date-only value or a midnight timestamp is treated as sampled at EOD. Therefore, the values set for the day will be available the next day.

For other timestamps, the value is available at that timestamp and afterward until a newer sample is available. Supply data with timestamps that reflect when the information was actually available.

The system caches the CSV file for 1 minute between validations and downloads to avoid excessive requests. For public Gist and Google Sheets URLs this is a sliding cache: repeated requests can delay refresh. After changing the file, allow at least one minute without requesting the same URL, or use a new URL.

Further references:

  • The [FEAT-ExternalData-Csv] template demonstrates the usage of this feature
  • Use External Data explains how to prepare, publish, and use a CSV.

Settings

Simulation and core configuration are defined in the Settings object. Settings.Sim.TearsheetGeneration is deprecated and ignored; see Tearsheets for on-demand reports.

tip

Preferences supply saved simulation settings for a Templated run. Review the Core settings in the Strategy Definition.

Settings definition:

{
"Settings": {
"Sim": {
"FillModel": "AtMidPrice",
"SlippageAmt": 0,
"Commission": {
"CommissionModel": "FixedFee",
"OptionFee": 1.5
},
"PositionMonitor": {
"TraceCollectionInterval": "Hourly"
}
},
"Core": {
"LegSelectionConstraint": "UniqueInPosition",
"ExpirationSelectionConstraint": "AbortOnReuse",
"Margin": {
"Model": "RegT",
"HouseMultiplier": null,
"RegTMode": "CBOEPermissive"
}
}
}
}
Margin (Core)

Margin controls the margin calculation used during the simulation.

The Reg-T margin model enables the calculation and capturing of the margin requirement of complex options positions based on CBOE's Margin Manual. The margin requirement for each position is calculated in every simulation step and made accessible to the user through the pos_margin variable. The sum of all position margins is used to calculate account-level reporting. Use pos_margin for the current position; see Margin Report.

The PM-Like margin model tries to approximate brokerages' Portfolio Margin mode by projecting the Risk Graph's T+0 line to the user-specified boundaries (haircuts). The Portfolio Margin calculation is a highly complex subject and brokerages do not fully disclose their calculations, therefore the calculated margin using this mode is only an approximation.

  • Settings.Core.Margin.Model:
    When set to "None", no margin calculation is performed. "RegT" uses CBOE’s rules. "PMLike" approximates portfolio margin via T+0 projections.

These margin parameters are literal settings, not Lua expressions:

Field under Settings.Core.MarginValueMeaning
HouseMultiplierNumber or nullScales the margin requirement; null uses 1.0
RegTModeCBOEVanilla or CBOEPermissiveSelects the Reg-T calculation mode
PMConfig.LowerBoundPctNumber; default 10Lower price boundary, as a percentage below the current underlying price
PMConfig.UpperBoundPctNumber; default 10Upper price boundary, as a percentage above the current underlying price
PMConfig.IncludeRealizedPnLBoolean; default falseIncludes the position’s realized P&L in the T+0 projection used for PM-like margin

For example:

{
"PMConfig": {
"LowerBoundPct": 10,
"UpperBoundPct": 10,
"IncludeRealizedPnL": false
}
}

See Margin Report to interpret the captured results.

Commission:

Use the FixedFee commission model for index and equity options. Each contract traded is charged the amount specified in OptionFee. For the historical Deribit model, see Legacy crypto settings.

FillModel (Sim)

The FillModel parameter controls how fills are calculated for every entry, exit and NAV calculation:

  • AtBidAsk provides a pessimistic approach to fills
  • AtMidPrice is filling in the mid-point of the Bid-Ask spread.

Slippage is applied per contract. A numeric expression without order references is evaluated at job initialization. An expression using order is evaluated for each fill, in its order context.

For example, "SlippageAmt": "order.leg_spread / 2" uses half that leg's bid/ask spread. Available order properties are order.leg_spread, order.combo_group_spread, order.combo_group_leg_cnt and order.price_increment. Use property syntax, not a function call. General strategy variables are not available in this settings expression.

Settings.Core.ExpirationSelectionConstraint

The default, AbortOnReuse, aborts initial entry when distinct expiration aliases resolve to the same expiration. Use AllowReuse when those aliases may select the same expiration.

Settings.Core.LegSelectionConstraint

LegSelectionConstraint controls how contracts are chosen for each leg of a position. When set to FullyUnique, then each leg of every position must be unique. This holds true for position initiation and adjustment as well. In this behavior, if the StrikeSelector selects a contract that is already in use by another position, then the next closest contract is selected.

The default, UniqueInPosition, is less restrictive than FullyUnique: contracts for legs can be shared across multiple positions, but within a position, each leg must remain unique. This restriction holds true for entry and adjustments as well.

The None option enables the least restrictive mode of operation: it allows strike sharing within and across positions. When enabled, entries and adjustments can re-use strikes from existing legs (that is: two legs can end up in the same contract).

Please note that when an offsetting position is made, it is not closing the affected leg, but the two legs are tracked as separate entities.

PositionMonitor

The trace collection interval can be specified via the PositionMonitor settings:

{
"PositionMonitor": {
"TraceCollectionInterval": "Hourly"
}
}

Valid values for TraceCollectionInterval:

  • Off : Fastest
  • Daily: Fast
  • Hourly : Most resource intensive

See Position Monitor for sampling and chart guidance.

Expressions in this section

FieldTypeWhen evaluatedNotes
Settings.Sim.SlippageAmtnumberJob initialization, or each fill when using order propertiesMust be ≥ 0; dedicated order context
Settings.Sim.Commission.OptionFeenumberValidation and job startMust be ≥ 0 when provided; expression allowed

Legacy crypto settings

Historical crypto conventions and commission settings

See Crypto Simulation for the legacy instrument model. The following conventions apply to those historical simulations:

  • Quantities: fractional contracts are supported.
  • Schedules: UTC 00:00 is the session anchor; weekday lists can include sat and sun.
  • Roots: the default order is BTCD, BTCW, BTCM, BTCQ for BTCUSD and ETHD, ETHW, ETHM, ETHQ for ETHUSD. The final letter denotes daily, weekly, monthly or quarterly expiration.
  • Theta-based targets: Theta is quoted in USD while option prices use the underlying cryptocurrency. Convert with pos_theta / underlying_price when expressing a target in that cryptocurrency.
  • Commission: Deribit uses the following configurable fee model. Values are simulation inputs for the historical period.
{
"Commission": {
"CommissionModel": "Deribit",
"OptionFee": null,
"DeribitCommissionSettings": {
"MakerFeePctPerContract": 0.03,
"TakerFeePctPerContract": 0.03,
"MaxFeePctPerContract": 12.5,
"DeliveryFeePctPerContract": 0.015,
"WaiveBuySellComboOneSide": true,
"WaiveDailyOptionsDeliveryFees": true
}
}
}

WaiveBuySellComboOneSide waives the cheaper side’s commission for a combo containing both buys and sells. WaiveDailyOptionsDeliveryFees waives delivery fees for daily options held to expiration.

Expressions Quick Reference

Many fields accept Script Engine expressions. This table summarizes where expressions go, what type they must return, and when they are evaluated.

Field (path)Expected typeWhen evaluatedNotes
Backtest.CashnumberAt validation and job startMust be > 0; expression allowed
Entry.Schedule.AfterMarketOpenMinutesnumberAt schedule initializationRelative offset applied to each session
Entry.Schedule.BeforeMarketCloseMinutesnumberAt schedule initializationRelative offset applied to each session
Entry.Conditions[]booleanOn Entry scheduleIf any condition is true, an entry is attempted
Entry.AbortConditions[]booleanAfter leg selection, sizing and Entry.VarDefinesChecked before placing orders; if any true, the entry is aborted
Entry.VarDefines{}numberBefore abort checks and again after entry fillsRecords variables for later use (e.g., initial_theta)
Entry.QtyMultipliernumberAfter selection at entry and again when adding legsScales new leg quantities using current values
Entry.ReentryDaysnumberBefore entry selectionMinimum elapsed days since the last exit; fractions allowed
Entry.Concurrency.MaxPositionsInFlightnumberOn Entry scheduleGoverns maximum concurrent positions
Entry.Concurrency.EntryShiftDaysnumberOn Entry scheduleMinimum elapsed days since the last entry; fractions allowed
Structure.Expirations[].DTEnumberDuring structure resolutionControls target DTE for alias
Structure.Expirations[].Min / MaxnumberDuring structure resolutionOptional constraints for DTE selection
Structure.Legs[].QtynumberEntry and adjustments (add‑legs)Leg quantity expression
Structure.Legs[].StrikeSelector.[BidPrice / AskPrice / MidPrice / Delta / StrikePrice]numberWhen selecting strikesSpecify exactly one selector per leg
Structure.Legs[].StrikeSelector.Min / MaxnumberWhen selecting strikesOptional constraints for strike selection
Adjustment.ConditionalAdjustments{ key }booleanOn Adjustment scheduleEach key is a condition; true triggers the associated adjustments
Adjustment.*.VarDefines{}numberSee Variable update timingIncludes UpdateVarsAdjustment
Adjustment.MaxAdjustmentCountnumberBefore each matching adjustment blockCounts actions, including variable updates
Exit.Schedule.AfterMarketOpenMinutesnumberAt schedule initializationRelative offset applied to each session
Exit.Schedule.BeforeMarketCloseMinutesnumberAt schedule initializationRelative offset applied to each session
Exit.MaxDaysInTradenumberInitialized at entry; checked on Exit scheduleMaximum days in trade before forced exit
Exit.ProfitTargetnumberInitialized at entry; checked on Exit scheduleExit threshold (profit)
Exit.StopLossnumberInitialized at entry; checked on Exit scheduleExit threshold (loss)
Exit.Conditions[]booleanOn Exit scheduleAny true expression triggers exit
Exit.VarDefines{}numberBefore normal closing fills; after settlement when the position has expiredRecord variables at exit

Timing variables such as days_in_trade and leg_*_dte can be fractional. Use comparisons such as days_in_trade >= 5 when defining a threshold; see Timing.