docs/concepts/synthetics.md
Synthetic instruments are locally defined instruments whose prices derive from other instruments.
They can combine components from one venue or many venues and expose the result as a standard
Nautilus instrument with the synthetic venue code SYNTH.
Synthetic instruments are useful for:
Actor and Strategy components to subscribe to quote or trade feeds.Synthetic instruments cannot be traded directly. They exist locally within the platform and serve as analytical tools. In the future, Nautilus may support trading component instruments based on synthetic instrument behavior.
Each synthetic instrument defines a derivation formula. Nautilus evaluates this formula with
its built-in numeric expression engine and converts the final numeric result to the synthetic
Price.
Formulas can reference component InstrumentId values directly, including IDs that contain /
and -.
| Construct | Example | Notes |
|---|---|---|
| Component reference | BTCUSDT.BINANCE | Use the raw InstrumentId text. |
| Component reference | AUD/USD.SIM | IDs containing / are valid. |
| Component reference | ETH-USDT-SWAP.OKX | IDs containing - are valid. |
| Numeric literal | 1, 0.5, 1.2e-3 | Evaluated with f64 semantics. |
| Boolean literal | true, false | Used in conditions and logical expressions. |
| Parentheses | (a + b) / 2 | Use parentheses to override precedence. |
| Unary operators | -x, !flag | Unary - negates numbers. Unary ! negates booleans. |
| Binary operators | + - * / % ^, == !=, < <= > >=, && || | Arithmetic is numeric. Logical operators are boolean. |
| Local assignment | spread = a - b; spread / 2 | Statements run from left to right. The formula must end with a value. |
| Comments | // line, /* block */ | Comments are ignored. |
:::note
New formulas should use raw InstrumentId values. For backward compatibility, formulas that
replace - with _ in component IDs remain accepted.
:::
The expression engine evaluates operators in the following order, from highest precedence to lowest precedence:
| Level | Operators | Notes |
|---|---|---|
| Highest | ^ | Exponentiation. Right associative. |
Unary -, unary ! | -2 ^ 2 evaluates as -(2 ^ 2). | |
*, /, % | Multiplication, division, and modulo. | |
+, - | Addition and subtraction. | |
<, <=, >, >= | Numeric comparisons. | |
==, != | Equality and inequality. Both sides must have the same type. | |
| Lowest | &&, || | Boolean operators. |
Assignments are not expression operators. Separate statements with ;, and make the last
statement the value you want the synthetic to produce.
| Function | Signature | Notes |
|---|---|---|
abs | abs(x) | Absolute value. |
ceil | ceil(x) | Ceiling. |
floor | floor(x) | Floor. |
round | round(x) | Round to the nearest integer using Rust f64 rules. |
min | min(x1, x2, ...) | Accepts one or more numeric arguments. |
max | max(x1, x2, ...) | Accepts one or more numeric arguments. |
if | if(condition, when_true, when_false) | The condition must be boolean. Both branches match. Only the selected branch evaluates. |
<, <=, >, >= require numeric operands and return boolean results.== and != accept any matching type (both numeric or both boolean) and return boolean
results.&&, ||, and unary ! require boolean operands.&& and || short-circuit. The right-hand side evaluates only when needed._ and then use letters, digits, or _.The expression engine enforces the following compile-time limits. Formulas that exceed them produce a clear error at construction time.
| Limit | Value | Description |
|---|---|---|
| Stack depth | 32 | Maximum number of intermediate values on the evaluation stack. |
| Local variables | 16 | Maximum number of distinct local variable names. |
These limits are generous for any realistic pricing formula. A weighted sum of 8 components uses a peak stack depth of 3 and zero locals.
# Simple spread
formula = "BTCUSDT.BINANCE - ETHUSDT.BINANCE"
# Average of two FX pairs
formula = "(AUD/USD.SIM + NZD/USD.SIM) / 2"
# Reuse an intermediate value
formula = "spread = BTCUSDT.BINANCE - ETHUSDT.BINANCE; spread / 2"
# Conditional output
formula = "if(BTCUSDT.BINANCE > ETHUSDT.BINANCE, BTCUSDT.BINANCE, ETHUSDT.BINANCE)"
Before defining a new synthetic instrument, make sure all component instruments already exist in the cache.
The following example creates a synthetic instrument with an actor or strategy. This synthetic
represents a simple spread between Bitcoin and Ethereum spot prices on Binance. It assumes that
BTCUSDT.BINANCE and ETHUSDT.BINANCE already exist in the cache.
from nautilus_trader.model.instruments import SyntheticInstrument
btcusdt_binance_id = InstrumentId.from_str("BTCUSDT.BINANCE")
ethusdt_binance_id = InstrumentId.from_str("ETHUSDT.BINANCE")
synthetic = SyntheticInstrument(
symbol=Symbol("BTC-ETH:BINANCE"),
price_precision=8,
components=[
btcusdt_binance_id,
ethusdt_binance_id,
],
formula=f"{btcusdt_binance_id} - {ethusdt_binance_id}",
ts_event=self.clock.timestamp_ns(),
ts_init=self.clock.timestamp_ns(),
)
self._synthetic_id = synthetic.id
self.add_synthetic(synthetic)
self.subscribe_quote_ticks(self._synthetic_id)
:::note
The synthetic instrument_id in the example above is {symbol}.SYNTH, which produces
BTC-ETH:BINANCE.SYNTH.
:::
You can update a synthetic formula at any time.
synthetic = self.cache.synthetic(self._synthetic_id)
new_formula = "(BTCUSDT.BINANCE + ETHUSDT.BINANCE) / 2"
synthetic.change_formula(new_formula)
self.update_synthetic(synthetic)
You can trigger emulated orders from synthetic prices. In the following example, a synthetic instrument releases an emulated order once the synthetic price reaches the trigger condition.
order = self.strategy.order_factory.limit(
instrument_id=ETHUSDT_BINANCE.id,
order_side=OrderSide.BUY,
quantity=Quantity.from_str("1.5"),
price=Price.from_str("30000.00000000"),
emulation_trigger=TriggerType.DEFAULT,
trigger_instrument_id=self._synthetic_id,
)
self.strategy.submit_order(order)
Formulas compile once at construction time and evaluate on every incoming component price tick. The expression engine uses a compile-once/eval-many architecture with a zero-allocation f64 stack, so evaluation adds negligible overhead to the tick-processing path.
Measured on Apple M4 Pro, rustc 1.94.1, release profile (opt-level 3):
| Formula pattern | Time |
|---|---|
(A + B) / 2.0 | 12 ns |
A * 0.4 + B * 0.3 + C * 0.2 + D * 0.1 | 18 ns |
if(A > B, A - B, B - A) | 12 ns |
spread = A - B; mid = ...; mid + ... | 19 ns |
max(min(A, B * 20), abs(A - B)) | 15 ns |
| Components | Time |
|---|---|
| 2 | 14 ns |
| 4 | 18 ns |
| 8 | 28 ns |
| Formula pattern | Time |
|---|---|
| Simple average | 675 ns |
| 4-input weighted | 1.4 us |
| Conditional | 1.0 us |
| With locals | 1.3 us |
| Hyphenated IDs | 755 ns |
Nautilus validates synthetic instruments at every boundary. Formula compilation rejects unknown symbols, type errors, and capacity overflows. Evaluation rejects wrong input counts and non-finite prices (NaN, Infinity) before they reach the formula.
See the
SyntheticInstrument API Reference
for input requirements and exceptions.