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
| Section | What it controls |
|---|---|
| Top level fields | Strategy name, description, and symbol selection |
| Backtest | Run name, dates, and starting capital |
| Structure | Expirations, legs, quantities, and strike selection |
| Entry | Entry schedule, filters, sizing, and captured variables |
| Exit | Profit target, stop loss, maximum duration, and exit conditions |
| Adjustment | Conditions for moving, removing, or adding legs and updating variables |
| External CSV Data | Imported signals and indicators |
| Settings | Fills, costs, position monitoring, margin, and selection constraints |
| Expressions Quick Reference | Expression 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 togenerate, 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
| Field | Type | When evaluated | Notes |
|---|---|---|---|
Backtest.Cash | number | At validation and job start | Must 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 theExpirationssection. -
OptionType:
Defines the option type to be traded. EitherPutorCall. -
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 tonullto 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,10targets 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
Constraintsare evaluated (if they present). If all Constraints evaluate to true (or no if constraints are specified) then theStatementis calculated and stored in the inclusion list. Once all contracts are processed, theTargetstatement 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 NoteThis 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
| Field | Type | When evaluated | Notes |
|---|---|---|---|
Structure.Expirations[].DTE | number | During structure resolution | Target DTE per alias; must be ≥ 0 |
Structure.Expirations[].Min / Max | number | During structure resolution | Optional DTE constraints; non‑negative; Min ≤ Max |
Structure.Legs[].Qty | number | Entry and AddLegs adjustment | Base quantity, scaled by Entry.QtyMultiplier |
Structure.Legs[].LegGroupId | number | Entry and adjustments | Integer order-group identifier, including order-based slippage groups |
Structure.Legs[].StrikeSelector.[BidPrice / AskPrice / MidPrice / Delta / StrikePrice] | number | When selecting strikes | Exactly one selector per leg |
Structure.Legs[].StrikeSelector.Min / Max | number | When selecting strikes | Optional 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 minutes30min: Intra-day mode, the Entry, Exit or Adjustment is considered every 30 minutes- Other positive
Nminintervals must be compatible with the selected market-data resolution.
-
Try to enter every day, 30 minutes before close:
{"Schedule": {"AfterMarketOpenMinutes": null,"BeforeMarketCloseMinutes": 30,"Every": "day"}} -
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
| Field | Type | When evaluated | Notes |
|---|---|---|---|
Entry.Schedule.AfterMarketOpenMinutes | number | At schedule initialization | Relative offset applied to each session |
Entry.Schedule.BeforeMarketCloseMinutes | number | At schedule initialization | Relative offset applied to each session |
Entry.Conditions[] | boolean | On Entry schedule | Any true triggers entry attempt |
Entry.AbortConditions[] | boolean | After leg selection, sizing and Entry.VarDefines | Checked before placing orders; any true aborts entry |
Entry.VarDefines{} | number | Before abort checks and again after entry fills | Records variables for later use (e.g., initial_theta) |
Entry.QtyMultiplier | number | After selection at entry and again when adding legs | Scales new leg quantities using current values |
Entry.ReentryDays | number | Before entry selection | Minimum elapsed days since the last exit; fractions allowed |
Entry.Concurrency.MaxPositionsInFlight | number | On Entry schedule | Governs maximum concurrent positions |
Entry.Concurrency.EntryShiftDays | number | On Entry schedule | Minimum elapsed days since the last entry; fractions allowed |
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:
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.EntryShiftDays:
Minimum elapsed days between entries. Fractional values are valid:1requires 24 hours after the last entry, while0.5requires 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.
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
| Field | Type | When evaluated | Notes |
|---|---|---|---|
Exit.Schedule.AfterMarketOpenMinutes | number | At schedule initialization | Relative offset applied to each session |
Exit.Schedule.BeforeMarketCloseMinutes | number | At schedule initialization | Relative offset applied to each session |
Exit.MaxDaysInTrade | number | Initialized at entry; checked on Exit schedule | Must be > 0 |
Exit.ProfitTarget | number | Initialized at entry; checked on Exit schedule | Exit threshold (profit) |
Exit.StopLoss | number | Initialized at entry; checked on Exit schedule | Exit threshold (loss) |
Exit.Conditions[] | boolean | On Exit schedule | Any true expression triggers exit |
Exit.VarDefines{} | number | Before normal closing fills; after settlement when the position has expired | Record 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_callleg. - When the structure delta moves below -5, we move the
short_putleg.
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:
- Legs: matches Structure.Legs
- Expirations: matches Structure.Expirations
- AbortConditions: matches Entry.AbortConditions
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.
| Action | When its VarDefines runs |
|---|---|
RemoveLegsAdjustment | After the specified legs have been closed and removed |
MoveLegAdjustment | After the replacement leg fills |
AddLegsAdjustment | After the added legs fill, following that action’s abort checks |
UpdateVarsAdjustment | In 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
| Field | Type | When evaluated | Notes |
|---|---|---|---|
Adjustment.Schedule.AfterMarketOpenMinutes | number | At schedule initialization | Relative offset applied to each session |
Adjustment.Schedule.BeforeMarketCloseMinutes | number | At schedule initialization | Relative offset applied to each session |
Adjustment.ConditionalAdjustments{ condition } | boolean | On Adjustment schedule | Key is a condition; true triggers adjustments |
MoveLegAdjustment.StrikeSelector.[…] | number | When selecting strikes | Same semantics as Structure.Legs[].StrikeSelector |
AddLegsAdjustment.Expirations[].(DTE/Min/Max) | number | During adjustment expiration resolution | Same semantics as Structure.Expirations |
AddLegsAdjustment.Legs[].(Qty/StrikeSelector) | number | When selecting and sizing legs | Same semantics as Structure.Legs |
AddLegsAdjustment.AbortConditions[] | boolean | After new-leg selection and sizing, before fills | Any true expression aborts the addition |
*.VarDefines{} | number | In the action’s variable-update phase, as listed above | Update or record variables |
Adjustment.MaxAdjustmentCount | number | Before each matching adjustment block | Must 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
dateordatetimeand 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.
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.
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.Margin | Value | Meaning |
|---|---|---|
HouseMultiplier | Number or null | Scales the margin requirement; null uses 1.0 |
RegTMode | CBOEVanilla or CBOEPermissive | Selects the Reg-T calculation mode |
PMConfig.LowerBoundPct | Number; default 10 | Lower price boundary, as a percentage below the current underlying price |
PMConfig.UpperBoundPct | Number; default 10 | Upper price boundary, as a percentage above the current underlying price |
PMConfig.IncludeRealizedPnL | Boolean; default false | Includes 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:
AtBidAskprovides a pessimistic approach to fillsAtMidPriceis 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
| Field | Type | When evaluated | Notes |
|---|---|---|---|
Settings.Sim.SlippageAmt | number | Job initialization, or each fill when using order properties | Must be ≥ 0; dedicated order context |
Settings.Sim.Commission.OptionFee | number | Validation and job start | Must 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
satandsun. - Roots: the default order is
BTCD, BTCW, BTCM, BTCQfor BTCUSD andETHD, ETHW, ETHM, ETHQfor 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_pricewhen expressing a target in that cryptocurrency. - Commission:
Deribituses 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 type | When evaluated | Notes |
|---|---|---|---|
Backtest.Cash | number | At validation and job start | Must be > 0; expression allowed |
Entry.Schedule.AfterMarketOpenMinutes | number | At schedule initialization | Relative offset applied to each session |
Entry.Schedule.BeforeMarketCloseMinutes | number | At schedule initialization | Relative offset applied to each session |
Entry.Conditions[] | boolean | On Entry schedule | If any condition is true, an entry is attempted |
Entry.AbortConditions[] | boolean | After leg selection, sizing and Entry.VarDefines | Checked before placing orders; if any true, the entry is aborted |
Entry.VarDefines{} | number | Before abort checks and again after entry fills | Records variables for later use (e.g., initial_theta) |
Entry.QtyMultiplier | number | After selection at entry and again when adding legs | Scales new leg quantities using current values |
Entry.ReentryDays | number | Before entry selection | Minimum elapsed days since the last exit; fractions allowed |
Entry.Concurrency.MaxPositionsInFlight | number | On Entry schedule | Governs maximum concurrent positions |
Entry.Concurrency.EntryShiftDays | number | On Entry schedule | Minimum elapsed days since the last entry; fractions allowed |
Structure.Expirations[].DTE | number | During structure resolution | Controls target DTE for alias |
Structure.Expirations[].Min / Max | number | During structure resolution | Optional constraints for DTE selection |
Structure.Legs[].Qty | number | Entry and adjustments (add‑legs) | Leg quantity expression |
Structure.Legs[].StrikeSelector.[BidPrice / AskPrice / MidPrice / Delta / StrikePrice] | number | When selecting strikes | Specify exactly one selector per leg |
Structure.Legs[].StrikeSelector.Min / Max | number | When selecting strikes | Optional constraints for strike selection |
Adjustment.ConditionalAdjustments{ key } | boolean | On Adjustment schedule | Each key is a condition; true triggers the associated adjustments |
Adjustment.*.VarDefines{} | number | See Variable update timing | Includes UpdateVarsAdjustment |
Adjustment.MaxAdjustmentCount | number | Before each matching adjustment block | Counts actions, including variable updates |
Exit.Schedule.AfterMarketOpenMinutes | number | At schedule initialization | Relative offset applied to each session |
Exit.Schedule.BeforeMarketCloseMinutes | number | At schedule initialization | Relative offset applied to each session |
Exit.MaxDaysInTrade | number | Initialized at entry; checked on Exit schedule | Maximum days in trade before forced exit |
Exit.ProfitTarget | number | Initialized at entry; checked on Exit schedule | Exit threshold (profit) |
Exit.StopLoss | number | Initialized at entry; checked on Exit schedule | Exit threshold (loss) |
Exit.Conditions[] | boolean | On Exit schedule | Any true expression triggers exit |
Exit.VarDefines{} | number | Before normal closing fills; after settlement when the position has expired | Record 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.