---
title: "4.5.6 NautilusTrader and LEAN in live trading"
description: "The live system's strategy in NautilusTrader: backtest, sandbox with a real TradingNode, startup reconciliation and going live; LEAN with its CLI."
url: "https://pythonparatrading.com/en/python-trading-course/level-4/nautilustrader-and-lean-live-trading/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-4/nautilustrader-and-lean-live-trading.png"
---

# 4.5.6 · NautilusTrader or LEAN live: from backtest to production with the same code

> The promise of NautilusTrader and LEAN is that the backtest strategy is the one that trades. This lesson puts it to the test with the rule of the level 4 live system: it writes it once as a NautilusTrader strategy, runs it in the backtest engine, compares it with the live account in the data and runs it again in a live node with simulated execution, with no network. Then it reviews what changes when connecting a real broker and how to do the same with LEAN.

- Course: [Level 4: Portfolios, risk, execution and live trading](https://pythonparatrading.com/en/python-trading-course/level-4/)
- Module: 4.5 · Live trading system
- Duration: 75 min
- Prerequisites: [2.4.5 · NautilusTrader: architecture, data, strategies and the same code in backtest and live](https://pythonparatrading.com/en/python-trading-course/level-2/nautilustrader-architecture-and-strategies/), [2.4.6 · LEAN/QuantConnect and Zipline Reloaded; bt for asset allocation](https://pythonparatrading.com/en/python-trading-course/level-2/lean-zipline-reloaded-and-bt/), [4.5.5 · Fault tolerance: idempotency, persistent state and reconciliation at startup](https://pythonparatrading.com/en/python-trading-course/level-4/fault-tolerance-idempotency-and-reconciliation/)

## By the end of this lesson

- Write a NautilusTrader strategy that works unchanged for the backtest, the sandbox and live trading, and know which parts cannot live inside it.
- Compare a backtest with a live account and attribute the difference to reference prices, execution timing and costs.
- Set up a live `TradingNode` with a custom data client and the `SandboxExecutionClient`, and understand the differences of live mode: queues, real clock, submit rate limits and identifiers.
- Explain and test NautilusTrader's reconciliation at startup with a position the system did not know about.
- Prepare going live with NautilusTrader (adapters, test environment, persistence) and with LEAN (`lean live deploy`, broker events and state between deployments).

## What “the same code” means

Lesson 2.4.5 assembled NautilusTrader piece by piece and ended with a `TradingNode` skeleton for Interactive Brokers,
not run. Lesson 2.4.6 did the same with LEAN and its CLI. Both tools promise the same thing: the strategy tested in
the backtest is the one that trades live, without rewriting it. This lesson checks that promise with a concrete rule,
the one of the live system in lessons 4.5.1 to 4.5.5, and takes it as far as possible without a network or an
account: all the way to a real live node, with its real clock, its queues and its risk engine, and simulated
execution.

What changes between environments is not the strategy, but what surrounds it:

| | Backtest | Sandbox | Live |
|---|---|---|---|
| Engine | `BacktestEngine` | `TradingNode` | `TradingNode` |
| Clock | the data's (simulated) | the machine's | the machine's |
| Data | historical list | live data client | live data client |
| Execution | simulated market | simulated market (`SandboxExecutionClient`) | broker client |
| Reconciliation at startup | does not exist | with the simulated one (empty) | with the broker |
| Network and broker failures | none | none | those of lessons 4.5.2 and 4.5.5 |

The intermediate step, the **sandbox**, is what the NautilusTrader documentation calls live data with simulated
execution: the whole production node, except that orders do not leave the machine. Here, in addition, the live data
are supplied by a custom client that replays history, so that the lesson can run without a network and always give
the same result.

## The system's rule, as a strategy

The system's rule (README of the level 4 data): each session, with prices up to the previous day, weight
`min(2.5% / vol_63, 25%)` in the ETFs with positive 20-session momentum, normalized if they add up to more than 100%;
the difference with the position is traded if it exceeds 0.4% of equity. The ten ETFs are fictional and their price is
a rescaled index of the universe, as in the data generator. The daily bars carry the timestamp of **each session's
actual close** according to the NYSE calendar (lesson 4.5.4): 20:00 UTC in summer, 21:00 in winter and 17:00 or 18:00
on the three early closes in the period (July 3, November 28 and December 24).

```python
import math
import warnings

import exchange_calendars as xc
import numpy as np
import pandas as pd
from pandas.errors import Pandas4Warning

# the engine calls Timestamp.utcnow(), deprecated in pandas 3:
# one warning per run, beyond our control (lesson 2.4.5)
warnings.filterwarnings("ignore", message="Timestamp.utcnow",
                        category=Pandas4Warning)

from nautilus_trader.model.currencies import USD
from nautilus_trader.model.data import Bar, BarType
from nautilus_trader.model.identifiers import (
    InstrumentId, Symbol, Venue)
from nautilus_trader.model.instruments import Equity
from nautilus_trader.model.objects import Money, Price, Quantity

ETF = {"ORUS": ("RV_EEUU", 640.0), "ORSC": ("RV_EEUU_P", 232.0),
       "OREU": ("RV_EUR", 61.0), "OREM": ("RV_EMG", 49.0),
       "ORTN": ("RF_T710", 96.0), "ORTL": ("RF_T20", 88.0),
       "ORIG": ("RF_CIG", 109.0), "ORAU": ("MP_ORO", 312.0),
       "ORCM": ("MP_AMP", 24.0), "ORRE": ("INM_EEUU", 93.0)}
u = pd.read_parquet("universo4_precios.parquet").set_index("fecha")
base = u.loc["2025-08-29"]
precios = pd.DataFrame({t: u[c] * p / base[c]
                        for t, (c, p) in ETF.items()})
precios = precios.loc["2025-07-01":"2025-12-31"].round(2)
cierres = (xc.get_calendar("XNYS").schedule
           .loc[precios.index[0]:precios.index[-1], "close"])

VENUE = Venue("XNYS")
inst = {t: Equity(InstrumentId(Symbol(t), VENUE), Symbol(t), USD,
                  2, Price.from_str("0.01"), Quantity.from_int(1),
                  0, 0) for t in ETF}
TIPO = {t: BarType.from_str(f"{t}.XNYS-1-DAY-LAST-EXTERNAL")
        for t in ETF}
barras = []
for d, ts in zip(precios.index, cierres):
    for t in ETF:
        px = Price(precios.loc[d, t], 2)
        barras.append(Bar(TIPO[t], px, px, px, px,
                          Quantity.from_int(10_000_000),
                          ts.value, ts.value))
print(len(precios), "sessions ·", len(barras), "bars")
print(pd.Series(cierres.dt.strftime("%H:%M UTC")).value_counts()
      .to_dict())
```
```output
128 sessions · 1280 bars
{'20:00 UTC': 86, '21:00 UTC': 39, '18:00 UTC': 2, '17:00 UTC': 1}
```

One hundred twenty-eight sessions from July 1 (the first ones serve to warm up the 63-session volatility) and one bar
per ETF and session. The bars are built directly with `Bar(...)` instead of with `BarDataWrangler` because, with
pandas 3, the *wrangler* fails on a `DataFrame` whose columns all have the same type (`buffer source array is
read-only`, because of copy-on-write); the one in lesson 2.4.5 worked because it mixed types.

The strategy is a subclass of `Strategy` like those in 2.4.5, with two decisions that come from thinking about live
trading from the start:

- **Buys wait for sells.** With a cash account, the money from a sale is not available until it is executed. The
  strategy sends the sells, waits in `on_order_filled` until all of them are closed and then computes the buys with
  the free cash the account reports. In the backtest, everything happens at the same instant; live, seconds pass
  between one and the other, and a strategy that buys at the same time with money from unexecuted sales runs out of
  balance.
- **The position is queried, not stored.** The size of each order comes from `self.portfolio.net_position(...)` and
  from the account balance, which live come from the broker after reconciliation (lesson 4.5.5), not from a counter
  of its own.

```python
from nautilus_trader.config import StrategyConfig
from nautilus_trader.model.enums import OrderSide
from nautilus_trader.trading.strategy import Strategy


class MomVolConfig(StrategyConfig, frozen=True):
    tickers: tuple
    desde: str = "2025-10-13"        # first trading session
    vol_obj: float = 0.025
    tope: float = 0.25
    umbral: float = 0.004


class MomVol(Strategy):
    def __init__(self, config: MomVolConfig) -> None:
        super().__init__(config)
        self.cierres = {t: [] for t in config.tickers}
        self.recibidas, self.ventas, self.compras = 0, set(), []

    def iid(self, t):
        return InstrumentId(Symbol(t), VENUE)

    def on_start(self) -> None:
        for t in self.config.tickers:
            self.subscribe_bars(TIPO[t])

    def on_bar(self, bar: Bar) -> None:
        t = bar.bar_type.instrument_id.symbol.value
        self.cierres[t].append(float(bar.close))
        self.recibidas += 1
        fecha = pd.Timestamp(bar.ts_event, unit="ns", tz="UTC")
        if (self.recibidas % len(self.config.tickers) == 0
                and fecha.strftime("%Y-%m-%d") >= self.config.desde):
            self.rebalancear()          # all 10 bars are in

    def pesos(self):
        w = {}
        for t in self.config.tickers:
            h = np.array(self.cierres[t][:-1])     # up to the previous day
            mom = h[-1] / h[-21] - 1
            vol = np.std(h[-64:][1:] / h[-64:][:-1] - 1,
                         ddof=1) * math.sqrt(252)
            w[t] = (min(self.config.vol_obj / vol, self.config.tope)
                    if mom > 0 else 0.0)
        s = sum(w.values())
        return {t: x / s for t, x in w.items()} if s > 1 else w

    def rebalancear(self) -> None:
        hoy = {t: c[-1] for t, c in self.cierres.items()}
        pos = {t: float(self.portfolio.net_position(self.iid(t)))
               for t in hoy}
        cuenta = self.portfolio.account(VENUE)
        nav = float(cuenta.balance_total(USD)) + sum(
            pos[t] * hoy[t] for t in hoy)
        self.ventas, self.compras = set(), []
        for t, w in self.pesos().items():
            dif = math.floor(w * nav / hoy[t]) - pos[t]
            if abs(dif) * hoy[t] < self.config.umbral * nav:
                continue
            if dif < 0:
                o = self.order_factory.market(
                    self.iid(t), OrderSide.SELL,
                    Quantity.from_int(int(-dif)))
                self.ventas.add(o.client_order_id)
                self.submit_order(o)
            else:
                self.compras.append((t, int(dif), hoy[t]))
        if not self.ventas:
            self.comprar()

    def on_order_filled(self, event) -> None:
        cid = event.client_order_id
        if cid in self.ventas and self.cache.order(cid).is_closed:
            self.ventas.discard(cid)
            if not self.ventas:          # all sells closed
                self.comprar()

    def comprar(self) -> None:
        libre = float(self.portfolio.account(VENUE).balance_free(USD))
        for t, dif, px in self.compras:
            q = min(dif, int(libre / px))
            if q > 0:
                libre -= q * px
                self.submit_order(self.order_factory.market(
                    self.iid(t), OrderSide.BUY, Quantity.from_int(q)))
        self.compras = []
```

## Backtest: the same rule versus the live account

The backtest engine with a cash account of 1,000,000 USD, like the live account in the data, which started trading on
October 13, 2025:

```python
from nautilus_trader.backtest.engine import BacktestEngine
from nautilus_trader.config import BacktestEngineConfig, LoggingConfig
from nautilus_trader.model.enums import AccountType, OmsType

def informe_fills(trader):
    f = trader.generate_order_fills_report()
    f["fecha"] = (pd.to_datetime(f["ts_last"])
                  .dt.tz_convert("America/New_York")
                  .dt.strftime("%Y-%m-%d"))
    f["sym"] = f["instrument_id"].str.split(".").str[0]
    f["q"] = (f["filled_qty"].astype(float)
              * np.where(f["side"] == "BUY", 1, -1))
    f["px"] = f["avg_px"].astype(float)
    return f.reset_index()           # client_order_id, as a column

motor = BacktestEngine(BacktestEngineConfig(
    logging=LoggingConfig(log_level="ERROR")))
motor.add_venue(venue=VENUE, oms_type=OmsType.NETTING,
                account_type=AccountType.CASH, base_currency=USD,
                starting_balances=[Money(1_000_000, USD)])
for i in inst.values():
    motor.add_instrument(i)
motor.add_data(barras)
motor.add_strategy(MomVol(MomVolConfig(tickers=tuple(ETF))))
motor.run()
fb = informe_fills(motor.trader)
print(len(fb), "fills from", fb["fecha"].min(),
      "to", fb["fecha"].max())
print(fb[["fecha", "sym", "side", "q", "px"]].head(4)
      .to_string(index=False))
```
```output
228 fills from 2025-10-13 to 2025-12-31
     fecha  sym side      q     px
2025-10-13 ORUS  BUY  212.0 665.07
2025-10-13 ORSC  BUY  435.0 235.23
2025-10-13 OREU  BUY 1920.0  63.15
2025-10-13 ORTN  BUY 1442.0  98.08
```

The live system in the data ran the same rule with different machinery: it decided at 15:45 with the mid price at
that moment, sent market, limit or market-on-close orders and, on Mondays, half of the signals at the next open.
Comparing each day's closing position in the backtest with the statement of the live account (with the ORAU split
undone, lesson 4.5.5):

```python
pos_bt = (fb.pivot_table(index="fecha", columns="sym", values="q",
                         aggfunc="sum").fillna(0).cumsum())
ext = pd.read_csv("vivo4_extracto_posiciones.csv")
pos_re = ext.pivot(index="fecha", columns="instrumento",
                   values="cantidad").fillna(0)
pos_re.loc[pos_re.index >= "2025-11-17", "ORAU"] /= 3
pos_bt = (pos_bt.reindex(pos_re.index).ffill().fillna(0)
          .reindex(columns=pos_re.columns, fill_value=0))
dif = (pos_bt - pos_re).abs()
titulos = dif.sum(axis=1)
valor = (dif * precios.set_axis(precios.index.strftime("%Y-%m-%d"))
         .reindex(dif.index)[dif.columns]).sum(axis=1)
print("sessions:", len(dif), "| differing shares per day, median:",
      int(titulos.median()))
print("differing value per day, median: "
      f"{valor.median():,.0f} USD; maximum {valor.max():,.0f} USD "
      f"({valor.idxmax()}, {pd.Timestamp(valor.idxmax()):%A})")
```
```output
sessions: 56 | differing shares per day, median: 17
differing value per day, median: 1,443 USD; maximum 494,437 USD (2025-10-13, Monday)
```

The same rule in NautilusTrader reproduces the live account: the median difference is 17 shares a day across the ten
ETFs, some 1,400 USD of value in a portfolio of one million. They come from the price used to compute each order (the
mid at 15:45 in the live system; the close in the backtest), which changes how the quantity is rounded. The large
differences fall on Mondays, and the largest, almost 500,000 USD, on the first day: on Monday, October 13, the live
system sent half of the buy orders to Tuesday's open, and for one day its portfolio was half the backtest's.

What separates the money is something else:

```python
cuenta = motor.trader.generate_account_report(VENUE)
cuenta.index = (cuenta.index.tz_convert("America/New_York")
                .strftime("%Y-%m-%d"))
caja_bt = cuenta["total"].astype(float).groupby(level=0).last()
px_dia = (precios.set_axis(precios.index.strftime("%Y-%m-%d"))
          .reindex(pos_bt.index)[pos_bt.columns])
pat_bt = (pos_bt * px_dia).sum(axis=1) + caja_bt.reindex(
    pos_bt.index).ffill()

efe = pd.read_csv("vivo4_extracto_efectivo.csv")
caja_re = efe.groupby("fecha")["saldo"].last()
pat_re = ext.groupby("fecha")["valor"].sum() + caja_re.reindex(
    pos_re.index).ffill()
brecha = pat_re - pat_bt
ops = pd.read_csv("vivo4_extracto_operaciones.csv")
otros = efe[efe["concepto"].isin(["intereses",
                                  "tasa_datos_mercado"])]
print(f"backtest: {pat_bt.iloc[-1]:,.2f} | live: "
      f"{pat_re.iloc[-1]:,.2f} | gap: {brecha.iloc[-1]:,.2f}")
print(f"commissions: {-ops['comision'].sum():,.2f} | interest and "
      f"fees: {otros['importe'].sum():+,.2f}")
resto = (brecha.iloc[-1] + ops["comision"].sum()
         - otros["importe"].sum())
print(f"remainder (execution price and timing): {resto:,.2f}")
```
```output
backtest: 1,044,525.05 | live: 1,042,306.50 | gap: -2,218.55
commissions: -596.38 | interest and fees: +483.60
remainder (execution price and timing): -2,105.77
```

The live account ends 2,218.55 USD below the backtest, 0.22% of capital over 56 sessions. Commissions are 596.38, and
interest net of the data fee adds 483.60 in its favor; the rest, about 2,100 USD, is price: the spread and slippage of
each market execution, the limit price of the passive orders and the difference between executing at 15:50, at
Tuesday's open or at the close. It is the measurement of lesson 4.5.3 seen from the equity side. The header figure
follows the gap day by day: it grows in steps, one for each rebalance. On two days it recovers part of it (Tuesday,
November 4 and Tuesday, December 23): those are days when the live system executed Monday's orders at the open and
its portfolio was different for a day, for the better. The trend is one-way: the cost of executing is paid on every
trade and is not given back.

The backtest, then, has validated the logic (it decides the same as the live system) and has measured what it does
not model. For it to be right in money as well, it needs a commission model with the broker's fee schedule and
slippage like the one measured (lessons 2.4.5 and 2.4.7). What follows is the other half: that the code that decides
works the same inside the live engine.

## Sandbox: the same class in a live node

A `TradingNode` needs a data client and an execution client. For execution, NautilusTrader ships the
`SandboxExecutionClient`, which puts a simulated market (the backtest's `SimulatedExchange`) behind the interface of a
live client. For data, a custom client, a subclass of `LiveMarketDataClient`, which, when the strategy subscribes,
replays the lesson's bars as if they came from the market:

```mermaid
flowchart TB
  R[Replay client] --> DE[DataEngine]
  DE --> ST[MomVol]
  DE --> SX[Sandbox]
  ST --> RE[RiskEngine]
  RE --> EE[ExecEngine]
  EE --> SX
  SX --> EE
  EE --> ST
```

Three things came up when setting it up with version 1.231.0, and all three are real differences between the
backtest and live mode:

1. **The sandbox does not see the bars.** It subscribes on the bus to the `data.*.XNYS.*` topics, which match quotes
   (`data.quotes.XNYS.ORUS`) and trades, but not bars (`data.bars.ORUS.XNYS-1-DAY-…`). With bars only, all orders
   are rejected with “no market”. The lesson's client publishes, before each bar, a quote with bid and ask at the
   close; the official sandbox examples, for the same reason, subscribe to quotes[^1].
2. **The live engine works with queues.** Data, commands and events go into `asyncio` queues and are processed in
   turn. If the client publishes the sessions all at once, a day's orders reach the simulator when it already has the
   prices of later days. The client waits for the system to finish with each session (bars processed and no order
   left open) before publishing the next one: events, not timed waits. Live, a day passes between one close and the
   next.
3. **The risk engine measures with the real clock.** `LiveRiskEngineConfig` limits submission to 100 orders per
   second by default. Replaying months in seconds, the limit trips and orders come out `DENIED`; in the backtest it
   does not happen because the clock is the data's. The cell raises the limit for the replay only.

```python
import asyncio

from nautilus_trader.adapters.sandbox.config import (
    SandboxExecutionClientConfig)
from nautilus_trader.adapters.sandbox.factory import (
    SandboxLiveExecClientFactory)
from nautilus_trader.common.providers import InstrumentProvider
from nautilus_trader.config import (
    LiveDataClientConfig, LiveRiskEngineConfig, TradingNodeConfig)
from nautilus_trader.live.data_client import LiveMarketDataClient
from nautilus_trader.live.factories import LiveDataClientFactory
from nautilus_trader.live.node import TradingNode
from nautilus_trader.model.data import QuoteTick
from nautilus_trader.model.identifiers import ClientId


class ReplayConfig(LiveDataClientConfig, frozen=True):
    pass


class Replay(LiveMarketDataClient):
    """Publishes the lesson's bars, one session each time the
    node has finished with the previous one."""
    nodo, fin = None, None

    def __init__(self, loop, msgbus, cache, clock):
        super().__init__(loop=loop, client_id=ClientId("REPLAY"),
                         venue=VENUE, msgbus=msgbus, cache=cache,
                         clock=clock,
                         instrument_provider=InstrumentProvider())
        self.suscritas = 0

    async def _connect(self):
        pass

    async def _disconnect(self):
        pass

    async def _subscribe_bars(self, command):
        self.suscritas += 1
        if self.suscritas == len(ETF):       # the strategy is ready
            self.create_task(self.reproducir())

    def ocupado(self, n):
        est = Replay.nodo.trader.strategies()[0]
        c = Replay.nodo.cache
        return (est.recibidas < n or est.compras
                or c.orders_total_count() > c.orders_closed_count())

    async def reproducir(self):
        unidades = Quantity.from_int(1_000_000)
        for n, b in enumerate(barras, 1):
            iid = b.bar_type.instrument_id
            self._handle_data(QuoteTick(iid, b.close, b.close,
                                        unidades, unidades,
                                        b.ts_event, b.ts_init))
            self._handle_data(b)
            while self.ocupado(n):         # events, not time
                await asyncio.sleep(0)
        Replay.fin.set()


class ReplayFactory(LiveDataClientFactory):
    @staticmethod
    def create(loop, name, config, msgbus, cache, clock):
        return Replay(loop, msgbus, cache, clock)


config = TradingNodeConfig(
    trader_id="CURSO-001",
    logging=LoggingConfig(log_level="ERROR"),
    risk_engine=LiveRiskEngineConfig(
        max_order_submit_rate="100000/00:00:01"),  # replay only
    data_clients={"REPLAY": ReplayConfig()},
    exec_clients={"XNYS": SandboxExecutionClientConfig(
        venue="XNYS", starting_balances=["1000000 USD"],
        base_currency="USD", account_type="CASH")},
    timeout_post_stop=0.5,
)
nodo = TradingNode(config=config)
for i in inst.values():
    nodo.cache.add_instrument(i)
nodo.add_data_client_factory("REPLAY", ReplayFactory)
nodo.add_exec_client_factory("XNYS", SandboxLiveExecClientFactory)
nodo.trader.add_strategy(MomVol(MomVolConfig(tickers=tuple(ETF))))
nodo.build()
Replay.nodo, Replay.fin = nodo, asyncio.Event()

tarea = asyncio.create_task(nodo.run_async())
await Replay.fin.wait()
await nodo.stop_async()
await asyncio.wait_for(tarea, 30)

fs = informe_fills(nodo.trader)
cols = ["fecha", "sym", "q", "px"]
igual = (fs[cols].sort_values(cols).reset_index(drop=True)
         .equals(fb[cols].sort_values(cols).reset_index(drop=True)))
print(len(fs), "fills in the sandbox; the same as in the "
      "backtest?", igual)
estados = pd.Series([o.status_string() for o in nodo.cache.orders()])
print(estados.value_counts().to_dict())
print("order ids shared with the backtest:",
      int(fs["client_order_id"].isin(fb["client_order_id"]).sum()))
```
```output
228 fills in the sandbox; the same as in the backtest? True
{'FILLED': 228}
order ids shared with the backtest: 0
```

The same fills, share for share and price for price, with the strategy running inside the live node: its queued
`DataEngine`, its real-time `RiskEngine`, its `ExecutionEngine` and an execution client that behaves like a broker's.
What is **not** the same is the identifiers: the `client_order_id` that NautilusTrader generates carries the date and
time of the clock (`O-20251013-…` in the backtest, with the data's time; in the node, with the time of the machine
running it). That is why the cell does not print them: they change on every run. It is a detail with consequences: an
idempotency key derived from the clock does not survive a restart (lesson 4.5.5); for that, NautilusTrader offers
`order_id_tag` and cache persistence.

The default configuration of live mode shows what the node watches over besides the strategy:

```python
from nautilus_trader.config import LiveExecEngineConfig

ex, rk, nd = LiveExecEngineConfig(), LiveRiskEngineConfig(), \
    TradingNodeConfig()
for nombre, valor in [
        ("reconciliation at startup", ex.reconciliation),
        ("history window (min)", ex.reconciliation_lookback_mins),
        ("generate missing orders", ex.generate_missing_orders),
        ("check in-flight orders every (ms)",
         ex.inflight_check_interval_ms),
        ("…and treat them as lost after (ms)",
         ex.inflight_check_threshold_ms),
        ("order submit rate limit", rk.max_order_submit_rate),
        ("max reconciliation wait (s)",
         nd.timeout_reconciliation),
        ("load saved state", nd.load_state)]:
    print(f"{nombre:38} {valor}")
```
```output
reconciliation at startup              True
history window (min)                   None
generate missing orders                True
check in-flight orders every (ms)      2000
…and treat them as lost after (ms)     5000
order submit rate limit                100/00:00:01
max reconciliation wait (s)            30.0
load saved state                       False
```

Reconciliation at startup enabled and with no history limit (the documentation recommends leaving it that way to ask
the venue for everything it has[^2]), in-flight orders checked every 2 seconds and queried if 5 pass with no
response, 100 orders per second as a brake, and 30 seconds of leeway to reconcile before giving up. No saved state:
persistence (a Redis cache) has to be configured separately.

*Fig. — Start-up of a NautilusTrader 1.231.0 TradingNode with the default configuration: client connection, reconciliation with the venue before the strategies are started and in-flight order checks every 2 seconds, querying the venue after 5 seconds without a reply (schematic).* Timeline in seconds with four rows. Clients connect in the first second; reconciliation requests the venue reports (with a 30 s limit); then the strategies start. An order sent at second 4 with no reply is checked every 2 seconds and, after 5, the venue is queried (highlighted).

The figure puts those values on a timeline (the phase durations are illustrative): the strategies do not start until
reconciliation finishes, and a sent order that gets no response is neither taken as good nor as lost, but checked
every 2 seconds and, after 5, the venue is asked about it. It is the rule of lesson 4.5.2 (“query before resending”),
turned into configuration.

## Reconciliation at startup

Lesson 4.5.5 reconciled by hand. NautilusTrader does it on its own when starting the node, before activating the
strategies: it asks the execution client for three reports (order status, fills and positions) and aligns its cache
with them. To test it without a broker, it is enough to have a sandbox client whose position report says something
the system does not know: that the account holds 187 ORAU, like the live account in the data before the split.

A detail of version 1.231.0: the node's constructor only passes the portfolio (`portfolio`) to a client factory named
exactly `SandboxLiveExecClientFactory` (it compares the class name), so the custom factory carries that name.

```python
from decimal import Decimal

from nautilus_trader.adapters.sandbox.execution import (
    SandboxExecutionClient)
from nautilus_trader.core.uuid import UUID4
from nautilus_trader.execution.reports import PositionStatusReport
from nautilus_trader.live.factories import LiveExecClientFactory
from nautilus_trader.model.enums import PositionSide


class BrokerConPosicion(SandboxExecutionClient):
    async def generate_position_status_reports(self, command):
        ahora = self._clock.timestamp_ns()
        return [PositionStatusReport(
            account_id=self.account_id,
            instrument_id=InstrumentId(Symbol("ORAU"), VENUE),
            position_side=PositionSide.LONG,
            quantity=Quantity.from_int(187), report_id=UUID4(),
            ts_last=ahora, ts_init=ahora,
            avg_px_open=Decimal("366.98"))]


class SandboxLiveExecClientFactory(LiveExecClientFactory):
    @staticmethod
    def create(loop, name, config, portfolio, msgbus, cache, clock):
        return BrokerConPosicion(loop=loop, clock=clock, cache=cache,
                                 portfolio=portfolio, msgbus=msgbus,
                                 config=config)


class Quieta(Strategy):
    arrancada = None

    def on_start(self):
        Quieta.arrancada.set()


async def arrancar(estrategia):
    cfg = TradingNodeConfig(
        trader_id="CURSO-002",
        logging=LoggingConfig(log_level="ERROR"),
        exec_engine=LiveExecEngineConfig(
            reconciliation=True, reconciliation_startup_delay_secs=0),
        exec_clients={"XNYS": SandboxExecutionClientConfig(
            venue="XNYS", starting_balances=["1000000 USD"],
            base_currency="USD", account_type="CASH")},
        timeout_post_stop=0.5)
    n = TradingNode(config=cfg)
    for i in inst.values():
        n.cache.add_instrument(i)
    n.add_exec_client_factory("XNYS", SandboxLiveExecClientFactory)
    n.trader.add_strategy(estrategia)
    n.build()
    Quieta.arrancada = asyncio.Event()
    t = asyncio.create_task(n.run_async())
    await Quieta.arrancada.wait()       # after reconciliation
    filas = [(p.instrument_id.value, p.side.name,
              p.quantity.as_double(), p.strategy_id.value)
             for p in n.cache.positions()]
    ords = [(o.strategy_id.value, o.type_string(),
             o.side_string(), o.status_string(),
             [type(e).__name__ for e in o.events])
            for o in n.cache.orders()]
    await n.stop_async()
    await asyncio.wait_for(t, 30)
    return filas, ords

pos, ordenes = await arrancar(Quieta())
print("positions:", pos)
for o in ordenes:
    print("order:", o)
```
```output
positions: [('ORAU.XNYS', 'LONG', 187.0, 'EXTERNAL')]
order: ('EXTERNAL', 'LIMIT', 'BUY', 'FILLED', ['OrderInitialized', 'OrderAccepted', 'OrderFilled'])
```

The node starts, requests the reports and finds a position of 187 ORAU that its cache did not have. For the cache to
reflect the broker, it generates an order with strategy `EXTERNAL` (initialized, accepted and filled in the same
step) and the corresponding position; only then does it call the strategy's `on_start`. According to the
documentation, when there is a price to do it with, the generated order is a limit order, and a market order only as
a last resort[^2].

An `EXTERNAL` position belongs to no strategy: it counts for the portfolio and risk, but nobody manages it. If the
strategy has to take charge of what the broker says is there (the normal case after a restart without saved state),
it declares so with `external_order_claims`:

```python
class QuietaQueReclama(Quieta):
    pass

reclama = QuietaQueReclama(StrategyConfig(
    strategy_id="MomVol", order_id_tag="001",
    external_order_claims=["ORAU.XNYS"]))
pos, ordenes = await arrancar(reclama)
print("positions:", pos)
print("order:", ordenes[0][:4])
```
```output
positions: [('ORAU.XNYS', 'LONG', 187.0, 'MomVol-001')]
order: ('MomVol-001', 'LIMIT', 'BUY', 'FILLED')
```

With the claim, the generated order and position become `MomVol-001`'s, and the strategy manages them as if it had
opened them itself. It is the automatic version of lesson 4.5.5, with the same limits: NautilusTrader's
reconciliation compares orders, fills and positions that the broker reports, but knows nothing about a signal that
asked for 7 shares and produced 14 (the duplicate order) nor about a split if the adapter does not report it. Those
checks are still the system's job.

## Going live with NautilusTrader

With the strategy tested in backtest and in sandbox, going live changes two things: the data and execution clients
become those of the broker's adapter, and the node needs persistent state. The configuration can be built and checked
without connecting; this is the one for Binance on its test network (*testnet*), with the classes of the installed
version:

```python
from nautilus_trader.adapters.binance import (
    BINANCE, BinanceAccountType, BinanceDataClientConfig,
    BinanceExecClientConfig)
from nautilus_trader.adapters.binance.common.enums import (
    BinanceEnvironment)
from nautilus_trader.config import CacheConfig, DatabaseConfig

real = TradingNodeConfig(
    trader_id="CURSO-003",
    logging=LoggingConfig(log_level="INFO"),
    exec_engine=LiveExecEngineConfig(reconciliation=True,
                                     inflight_check_interval_ms=2000),
    cache=CacheConfig(database=DatabaseConfig(type="redis"),
                      flush_on_start=False),
    data_clients={BINANCE: BinanceDataClientConfig(
        account_type=BinanceAccountType.SPOT,
        environment=BinanceEnvironment.TESTNET)},
    exec_clients={BINANCE: BinanceExecClientConfig(
        account_type=BinanceAccountType.SPOT,
        environment=BinanceEnvironment.TESTNET,
        max_retries=3)},
)
c = real.exec_clients[BINANCE]
print(BINANCE, c.account_type.value, c.environment.value,
      "| keys in the config:", c.api_key is not None)
print("cache in", real.cache.database.type,
      "| reconciliation:", real.exec_engine.reconciliation)
```
```output
BINANCE SPOT TESTNET | keys in the config: False
cache in redis | reconciliation: True
```

The keys do not go in the configuration (`api_key` is `None`): the adapter reads them from environment variables when
creating the client, and for the testnet they are testnet keys, different from those of the live account (lesson
4.4.5). The Redis cache stores orders, positions and accounts, so that a restart starts from the previous state and
reconciliation only has to fill in what happened while the node was stopped; the documentation recommends persisting
all events so as not to depend on the history the venue keeps[^2]. Starting it already requires a network, Redis and
keys:

```python skip title="momvol_en_real.py"
from nautilus_trader.adapters.binance import (
    BinanceLiveDataClientFactory, BinanceLiveExecClientFactory)

nodo = TradingNode(config=real)
nodo.add_data_client_factory(BINANCE, BinanceLiveDataClientFactory)
nodo.add_exec_client_factory(BINANCE, BinanceLiveExecClientFactory)
nodo.trader.add_strategy(MomVol(MomVolConfig(
    tickers=("BTCUSDT", "ETHUSDT"),   # with venue and bars via config
    external_order_claims=["BTCUSDT.BINANCE", "ETHUSDT.BINANCE"])))
nodo.build()
try:
    nodo.run()           # blocks; Ctrl+C for an orderly stop
finally:
    nodo.dispose()
```

The lesson's `MomVol` class would not work as is for crypto: its `VENUE` and its bar types are fixed to the NYSE, and
the daily decision assumes a close that does not exist in crypto. That does not break the promise of the same code;
it says that the strategy must receive from its configuration everything that depends on the market (instruments, bar
type, decision time). For equities with Interactive Brokers, the skeleton from lesson 2.4.5 and the official sandbox
example with IB data and simulated execution are the way: first sandbox with the broker's real data, then a paper
account, then money.

## LEAN live

LEAN follows the same path with different machinery. The algorithm (a subclass of `QCAlgorithm`, lesson 2.4.6) is
the same; what changes is the command and the broker. Locally, `lean live deploy` starts the engine in a Docker
container with the `quantconnect/lean` image and, if given no options, asks through a wizard which broker and which
data provider to use. The CLI requires membership in a paid QuantConnect organization. This course does not run it
(there is no Docker in its environment):

```bash
lean live deploy "MomVol" --brokerage "Paper Trading" --data-provider-live "Interactive Brokers"
lean live deploy "MomVol" --environment "ib-papel" --detach
lean cloud live deploy "MomVol" --push --open
```

The first uses QuantConnect's paper broker with Interactive Brokers data (the CLI page documents the rest of the IB
options: user, account and delayed data). The second takes all its configuration from an environment in `lean.json`
with `live-mode` enabled and leaves the container in the background. The third deploys to the QuantConnect cloud.
Results are written in real time, in JSON, to `MomVol/live/<date and time>`[^3].

What is specific to live mode lives in the algorithm:

```python skip title="main.py"
from AlgorithmImports import *
import json


class MomVol(QCAlgorithm):

    def initialize(self) -> None:
        self.set_cash(1_000_000)
        self._etfs = [self.add_equity(t, Resolution.DAILY).symbol
                      for t in ["SPY", "IWM", "EFA", "EEM", "IEF",
                                "TLT", "LQD", "GLD", "DBC", "VNQ"]]
        self.schedule.on(
            self.date_rules.every_day("SPY"),
            self.time_rules.before_market_close("SPY", 15),
            self._rebalanceo)
        # own state: LEAN does not keep it between deployments
        self._clave = f"{self.project_id}/momvol.json"
        self._estado = {"rebalanceos": 0}
        almacen = self.object_store
        if self.live_mode and almacen.contains_key(self._clave):
            self._estado = json.loads(almacen.read(self._clave))

    def _rebalanceo(self) -> None:
        ...                                   # the same rule
        self._estado["rebalanceos"] += 1
        if self.live_mode:
            self.object_store.save(self._clave,
                                   json.dumps(self._estado))

    def on_brokerage_disconnect(self) -> None:
        self.debug("No connection to the broker")

    def on_brokerage_reconnect(self) -> None:
        self.debug("Connection restored: LEAN syncs the orders")

    def on_brokerage_message(self, message_event) -> None:
        self.debug(f"Broker: {message_event.message}")
```

Three sentences from QuantConnect's documentation sum up the model[^4]: “There is no automatic state management of
live strategies”; the live portfolio is loaded from the broker's positions, including open orders; and the algorithm
must not assume it starts with no positions, because it may have those of a previous deployment. On reconnecting, LEAN
syncs the state of the orders, but does not generate events for what happened during the outage (a limit order
filled while disconnected appears in the portfolio without its `on_order_event`). It is the same problem as in
lessons 4.5.2 and 4.5.5, half solved by the engine: what the broker reports gets aligned automatically; the
algorithm's own state (here, a counter in the Object Store) is the responsibility of whoever writes it.

| | NautilusTrader 1.231.0 | LEAN |
|---|---|---|
| Engine language | Rust and Cython; strategy in Python | C#; algorithm in C# or Python |
| Local deployment | a Python process (`TradingNode`) | Docker container (`lean live deploy`) |
| Simulated execution with real data | `SandboxExecutionClient` | “Paper Trading” broker |
| Reconciliation at startup | orders, fills and positions; unknowns as `EXTERNAL` | the broker's portfolio and open orders |
| Own state between restarts | Redis cache (`CacheConfig`) | Object Store |
| Cost of the tool | LGPL-3.0, no fee | Apache-2.0 engine; the CLI requires a paid organization |

## Before putting money in

What the lesson has tested and what remains, as a checklist for any engine:

| Step | What it proves | Done in the lesson |
|---|---|---|
| Backtest with the rule | the logic decides as intended | yes: positions like those of the live account |
| Backtest versus live (or paper) | how much the unmodeled costs cost | yes: 0.22% over 56 sessions |
| Sandbox with replayed data | the strategy works inside the live engine | yes: 228 identical fills |
| Reconciliation with foreign positions | startup picks up what the broker says | yes: 187 ORAU as `EXTERNAL` and claimed |
| Sandbox with the broker's real data | data in the real order and format | no: needs a network |
| Broker paper account | real orders, rejections and fills | no: needs an account |
| Live with minimum size | all of the above with money | no |
| Restart mid-session | persistent state and reconciliation | partial: lesson 4.5.5 |

Each step can reveal something the previous one could not, just as the sandbox here revealed three differences the
backtest was hiding. Skipping one means accepting that the money will find it.

## Exercises

1. Change the strategy's threshold from 0.4% to 1% of equity and run the backtest again. How many fewer fills are
   there and how much does the final equity change? What can you not know from this backtest about the effect on the
   live account?

**Solution.**

```python
def backtest(**kw):
    m = BacktestEngine(BacktestEngineConfig(
        logging=LoggingConfig(log_level="ERROR")))
    m.add_venue(venue=VENUE, oms_type=OmsType.NETTING,
                account_type=AccountType.CASH, base_currency=USD,
                starting_balances=[Money(1_000_000, USD)])
    for i in inst.values():
        m.add_instrument(i)
    m.add_data(barras)
    m.add_strategy(MomVol(MomVolConfig(tickers=tuple(ETF), **kw)))
    m.run()
    f = informe_fills(m.trader)
    p = (f.pivot_table(index="fecha", columns="sym", values="q",
                       aggfunc="sum").fillna(0).cumsum())
    caja = m.trader.generate_account_report(VENUE)["total"]
    fin = (p.iloc[-1] * precios.iloc[-1][p.columns]).sum()
    return len(f), fin + float(caja.iloc[-1])

for umbral in (0.004, 0.01):
    n, pat = backtest(umbral=umbral)
    print(f"threshold {umbral:.1%}: {n} fills, "
          f"equity {pat:,.2f}")
```
```output
threshold 0.4%: 228 fills, equity 1,044,525.05
threshold 1.0%: 195 fills, equity 1,046,976.45
```

With a 1% threshold there are 33 fewer fills (195 versus 228) and the cost-free equity rises by 2,451 USD. That
difference is not an advantage of the threshold: over 56 sessions and a single path, the less tightly adjusted
portfolio ending up higher is as likely as it ending up lower. What this backtest cannot tell you is how much it saves
on the live account, because it has no costs: with the gap measured in the lesson (about 2,100 USD of price and 596
of commissions over 228 fills), each avoided fill is worth on the order of 10 USD, and that only becomes a reliable
figure with a calibrated cost model or with another season of live trading.

2. Run the sandbox node with the default submit limit (remove `risk_engine` from `TradingNodeConfig`). What happens?
   Is the result deterministic?

**Solution.**

Some of the orders come out `DENIED` with “Exceeded MAX_ORDER_SUBMIT_RATE” and the strategy ends up with a different
portfolio. It is not deterministic: how many are denied depends on how many orders fit in one second of real clock
time, that is, on the speed of the machine and its load at that moment. That is why the lesson raises the limit for
the replay (and this exercise is not run here): the result of a lesson, like that of a test, cannot depend on the
clock. Live, the strategy sends at most ten orders a day and the default limit is a useful protection against a bug
that fires orders in a loop.

3. Write `on_stop` for `MomVol` so that, when the node stops, it leaves no live orders but does not close the
   portfolio either. Why is the `close_all_positions` used by the strategies of lesson 2.4.5 not a good idea?

**Solution.**

```python
class MomVolConParada(MomVol):
    def on_stop(self) -> None:
        for t in self.config.tickers:
            self.cancel_all_orders(self.iid(t))   # nothing live

m = BacktestEngine(BacktestEngineConfig(
    logging=LoggingConfig(log_level="ERROR")))
m.add_venue(venue=VENUE, oms_type=OmsType.NETTING,
            account_type=AccountType.CASH, base_currency=USD,
            starting_balances=[Money(1_000_000, USD)])
for i in inst.values():
    m.add_instrument(i)
m.add_data(barras)
m.add_strategy(MomVolConParada(MomVolConfig(tickers=tuple(ETF))))
m.run()
abiertas = m.cache.positions_open()
print(len(abiertas), "open positions after stopping:",
      sorted(p.instrument_id.symbol.value for p in abiertas))
```
```output
1 open positions after stopping: ['ORAU']
```

After stopping, the ORAU position remains open (on December 31 the rule had sold everything else, just like the live
account) and there are no live orders. In a backtest, closing everything at the end serves to measure a realized
result; live, `on_stop` runs at every stop of the node (an update, a scheduled restart), and closing the portfolio
each time would mean selling and buying back, with its costs and its risk, for a reason that has nothing to do with
the strategy. What the stop must do is leave no orders that could fill with nobody watching; on startup,
reconciliation recovers the portfolio.

[^1]: NautilusTrader 1.231.0, `nautilus_trader/adapters/sandbox/execution.py`: `connect()` subscribes to
    `data.*.{venue}.*` and `on_data()` passes quotes, trades, book and bars to the simulated market; the bus topics for
    bars are `data.bars.{bar_type}`. Official examples: `examples/sandbox/interactive_brokers_sandbox.py` (strategy
    with `subscribe_quote_ticks=True`). Verified by running the lesson's code.

[^2]: NautilusTrader 1.231.0, `docs/concepts/reconciliation.md`: “Leave `reconciliation_lookback_mins` unset”,
    `EXTERNAL` orders with the `RECONCILIATION` tag, `external_order_claims`, price hierarchy of the generated orders
    (“MARKET order (last resort)”) and the recommendation to persist events in the cache database.

[^3]: LEAN CLI, “lean live deploy” (https://www.quantconnect.com/docs/v2/lean-cli/api-reference/lean-live-deploy) and
    “QuantConnect Paper Trading”
    (https://www.quantconnect.com/docs/v2/lean-cli/live-trading/brokerages/quantconnect-paper-trading): container with
    the `quantconnect/lean` image, default wizard, options `--brokerage`, `--data-provider-live`, `--environment`,
    `--detach`, results in `<project>/live/<date>` and the paid organization requirement. Accessed October 22, 2026.

[^4]: QuantConnect, “Live Trading > Key Concepts”
    (https://www.quantconnect.com/docs/v2/writing-algorithms/live-trading/key-concepts) and “Live Trading >
    Brokerages” (https://www.quantconnect.com/docs/v2/writing-algorithms/live-trading/brokerages): state, loading the
    portfolio from the broker, `on_brokerage_disconnect`, `on_brokerage_reconnect` and `on_brokerage_message`; Object
    Store at https://www.quantconnect.com/docs/v2/writing-algorithms/object-store.

## Self-assessment

1. The lesson's strategy, run in the `BacktestEngine` with the live system's rule, ends December 31 with 1,044,525 USD; the live account in the data, with 1,042,307. Where does most of the difference come from?

   - A. From the costs the backtest does not have (commissions, bid-ask spread and slippage) and from executing at a different time and a different price than the close
   - B. From a bug in the backtest
   - C. From the duplicate order
   - D. From the ORAU split

   *Answer A: From the costs the backtest does not have (commissions, bid-ask spread and slippage) and from executing at a different time and a different price than the close.* The positions of the backtest and the live account differ by a few shares a day, because of the reference price (mid at 15:45 versus the close) and Monday's orders. What separates the money is the 596 USD of commissions, the spread and the slippage of each execution. The split does not change the value of the position.

2. NautilusTrader 1.231.0's `SandboxExecutionClient` receives the data the node publishes. If the data client only publishes bars, what happens to market orders?

   - A. They are filled at the bar's close, as in the backtest
   - B. They are rejected with “no market”: the sandbox listens to quotes, trades and the book, not bars
   - C. They stay pending until the next bar
   - D. The node does not start

   *Answer B: They are rejected with “no market”: the sandbox listens to quotes, trades and the book, not bars.* The sandbox client subscribes to `data.*.{venue}.*`, which matches the quote and trade topics (`data.quotes.XNYS.ORUS`) but not the bar topics (`data.bars.ORUS.XNYS-1-DAY-…`). That is why the official sandbox examples subscribe to quotes; in the lesson, the data client publishes a quote with each bar.

3. Replaying 128 sessions in a live node without waiting between them, many orders come out `DENIED` with “Exceeded MAX_ORDER_SUBMIT_RATE”. Why does it not happen in the backtest?

   - A. Because the backtest has no risk engine
   - B. Because the sandbox has a different limit
   - C. Because the limit (100 orders per second by default) is measured with the clock: in the backtest the clock is the data's and the orders are days apart; live, it is the real clock and the accelerated replay packs hundreds of orders into one second
   - D. Because the backtest orders are limit orders

   *Answer C: Because the limit (100 orders per second by default) is measured with the clock: in the backtest the clock is the data's and the orders are days apart; live, it is the real clock and the accelerated replay packs hundreds of orders into one second.* The risk engine is the same in both modes, but its clock is not. An accelerated replay is not a real market; live, this strategy sends at most 10 orders a day. The lesson raises the limit for the replay and says so.

4. When the live node starts, the broker reports a position of 187 ORAU that the system's cache does not have. What does NautilusTrader do with reconciliation enabled?

   - A. Ignores the position
   - B. Sells the position
   - C. Refuses to start
   - D. Generates an order and a fill with strategy `EXTERNAL` so that the cache reflects the broker's position; with `external_order_claims`, a strategy can claim it as its own

   *Answer D: Generates an order and a fill with strategy `EXTERNAL` so that the cache reflects the broker's position; with `external_order_claims`, a strategy can claim it as its own.* Reconciliation aligns the cache with the venue's reports before starting the strategies. What it does not recognize it creates with strategy `EXTERNAL`; if a strategy declares the instrument in `external_order_claims`, the order and the position become its own.

5. A LEAN algorithm keeps in a variable the number of days it has been in a position. The live deployment is restarted. What happens?

   - A. LEAN restores the variable
   - B. LEAN closes the positions on startup
   - C. The algorithm cannot be restarted
   - D. The portfolio is loaded from the broker's positions, but the variable starts from zero: the algorithm's own state has to be saved (for example, in the Object Store) and rebuilt

   *Answer D: The portfolio is loaded from the broker's positions, but the variable starts from zero: the algorithm's own state has to be saved (for example, in the Object Store) and rebuilt.* QuantConnect's documentation says so: there is no automatic state management of live strategies, the portfolio is loaded from the broker's positions and the algorithm must not assume it starts with no positions. Everything else must be persisted by the algorithm itself.

## Key points

- “The same code” holds for the strategy: the class that decides does not change between backtest, sandbox and live. What changes is the clock, the data and execution clients, the real-time risk engine and the reconciliation.
- On the live account in the data, the backtest of the same rule gets the positions right to within a few shares; the money difference (2,219 USD over 56 sessions, 0.22% of capital) is costs and execution timing.
- A `TradingNode` with a data client that replays history and the `SandboxExecutionClient` runs the strategy with the live engine and gives the same 228 fills as the backtest, as long as the data arrive at its pace.
- The sandbox shows what the backtest hides: queues and a real clock, per-second submit limits, identifiers with the machine's time and a simulator that needs quotes.
- On startup, NautilusTrader reconciles the cache with the broker before activating the strategies; anything unknown appears as `EXTERNAL` and can be claimed.
- LEAN deploys the same algorithm with `lean live deploy` in Docker; it loads portfolio and orders from the broker, signals disconnections with `on_brokerage_disconnect` and leaves its own state to the algorithm.

## Resources

### Research

- [Volatility Targeting in Python](https://pythonparatrading.com/en/volatility-targeting-2/) · Research-0004 · 7 min

### Documentation

- [NautilusTrader 1.231.0: Live Trading (guide in the repository, tag v1.231.0)](https://github.com/nautechsystems/nautilus_trader/blob/v1.231.0/docs/concepts/live.md) · github.com
- [NautilusTrader 1.231.0: Execution Reconciliation](https://github.com/nautechsystems/nautilus_trader/blob/v1.231.0/docs/concepts/reconciliation.md) · github.com
- [NautilusTrader 1.231.0: sandbox examples (Interactive Brokers, Binance, Bybit…)](https://github.com/nautechsystems/nautilus_trader/tree/v1.231.0/examples/sandbox) · github.com
- [LEAN CLI: lean live deploy (options)](https://www.quantconnect.com/docs/v2/lean-cli/api-reference/lean-live-deploy) · quantconnect.com
- [QuantConnect: Live Trading, Key Concepts (state and restarts)](https://www.quantconnect.com/docs/v2/writing-algorithms/live-trading/key-concepts) · quantconnect.com

### Data

- [Level 4 data: statement positions of the live account (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/vivo4_extracto_posiciones.csv) · Download · vivo4_extracto_posiciones.csv

Previous lesson: [4.5.5 · Fault tolerance: idempotency, persistent state and reconciliation at startup](https://pythonparatrading.com/en/python-trading-course/level-4/fault-tolerance-idempotency-and-reconciliation/)
