Skip to content
All library documents

Building Synthetic Instruments from Market Price Formulas

Article NautilusTrader

Summary

The document explains how a trading platform defines local synthetic instruments by applying formulas to prices from one or more component instruments. These derived prices can feed strategies and data actors, support derived quotes, trades, and bars, and trigger emulated orders. Synthetic instruments are analytical objects and cannot themselves be traded directly.

It outlines formula syntax, operator precedence, numeric and boolean type rules, built-in functions, and compile-time limits. Components must exist in the cache and receive at least one relevant tick before the synthetic can publish; each new component tick is combined with the latest cached values from the others. The guide also describes changing formulas and reports microbenchmark timings for compilation and evaluation on a particular machine and software setup. Those measurements are environment-specific, and the result depends on the freshness and availability of component feeds. Formula validation rejects unknown inputs, type errors, capacity overflows, and non-finite prices.

Key ideas

  • Synthetic instruments derive numeric prices from formulas over component instruments.
  • They can support analysis, derived market data, and emulated order triggers, but cannot be traded directly.
  • The formula engine supports arithmetic, comparisons, conditionals, local assignments, and built-in numeric functions.
  • A synthetic requires its components to exist and each to have produced a tick before it publishes.
  • Reported speed measurements describe one hardware and software configuration and may not generalize.

Tags

Full text
# Synthetics


# Synthetics

**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:

- Enabling `DataActor` and `Strategy` components to subscribe to quote or trade feeds.
- Triggering emulated orders from derived prices.
- Constructing bars from synthetic quotes or trades.

:::info
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.
:::

## Formula language

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`.

### Supported syntax

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.
:::

### Operator precedence

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.

### Built-in functions

| 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 must have the same type. Only the selected branch evaluates. |

### Type rules

- Component inputs are numeric.
- Arithmetic operators require numeric operands and return numeric results.
- `<`, `<=`, `>`, `>=` 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.
- Local variables must be assigned before use.
- Local variable names must start with an ASCII letter or `_` and then use ASCII letters, digits,
  or `_`.
- The final formula result must be numeric. A formula that ends with an assignment or produces a
  boolean result is invalid for a synthetic instrument.

### Limits

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.                                                |
| Nesting depth   | 128   | Maximum syntax nesting and expression tree depth; the top-level expression counts as one level. |

A weighted sum of 8 components uses a peak stack depth of 3 and zero locals. A weighted sum of
N components builds a tree of depth N + 1, so the nesting depth limit caps a weighted sum at
127 components.

### Examples

```python
# 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)"
```

## Creating a synthetic instrument

Make sure all component instruments already exist in the cache, and subscribe to each
component's quote or trade feed as well as the synthetic's. Synthetic quotes derive only from
component quotes, and synthetic trades only from component trades.

When a component tick arrives, the engine combines it with the latest cached prices of the other
components to calculate the synthetic price. Until **every component has produced at least one
tick**, the synthetic publishes nothing.

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.

```python
from nautilus_trader.model 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_quotes(self._synthetic_id)
```

:::note
The synthetic `instrument_id` in the example above is `{symbol}.SYNTH`, which produces
`BTC-ETH:BINANCE.SYNTH`.
:::

## Updating formulas

You can update a synthetic formula at any time.

```python
synthetic = self.cache.synthetic(self._synthetic_id)

new_formula = "(BTCUSDT.BINANCE + ETHUSDT.BINANCE) / 2"
synthetic.change_formula(new_formula)

self.update_synthetic(synthetic)
```

## Trigger instrument IDs

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.

```python
order = self.order_factory.limit(
    instrument_id=InstrumentId.from_str("ETHUSDT.BINANCE"),
    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.submit_order(order)
```

## Performance

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):

### Evaluation (hot path)

| 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 |

### Evaluation scaling (weighted sum)

| Components | Time  |
| ---------- | ----- |
| 2          | 14 ns |
| 4          | 18 ns |
| 8          | 28 ns |

### Compilation (cold path)

| 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 |

## Error handling

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](/docs/python-api-latest/model/instruments.html#nautilus_trader.model.SyntheticInstrument)
for input requirements and exceptions.

## Related guides

- [Instruments](instruments/) - Instrument definitions and venue-specific instrument types.
- [Data](data/) - Market data types that reference instruments.
- [Orders](orders/) - Orders can use synthetic instrument IDs for emulation triggers.

Shown in full with attribution under the source's licence. Licence: LGPL-3.0

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.