NautilusTrader
Concepts
These docs track the unreleased nightly build and may change without notice. Switch to the latest stable docs.

Reports

This guide explains the portfolio analysis and reporting capabilities provided by the ReportProvider class, and how these reports are used for PnL accounting and backtest post-run analysis.

Overview

ReportProvider turns cached orders, fills, positions, and account states into pandas DataFrames for analysis and visualization. These reports help you evaluate strategy performance, analyze execution quality, and verify PnL accounting. The same reports are available in backtesting and live trading, which keeps performance evaluation and strategy comparison consistent across both.

Reports can be generated using two approaches:

  • Backtest methods: BacktestEngine.generate_orders_report() and its siblings read the engine's own cache. BacktestNode exposes the same methods, taking the run config ID as the first argument.
  • ReportProvider directly: pass any collection of orders or positions, such as a live node's cache or a filtered cache query.

Every method returns an empty DataFrame when no matching data exists.

Report generation requires pandas, which nautilus_trader.analysis imports lazily: the module imports without pandas installed, and the ImportError surfaces when you generate a report. The visualization extra installs it.

Available reports

The ReportProvider class offers several static methods to generate reports from trading data. Each report returns a pandas DataFrame with specific columns and indexing for easy analysis.

Orders report

Generates a full view of all orders:

from nautilus_trader.analysis import ReportProvider

# From a completed backtest run
orders_report = engine.generate_orders_report()

# Or from any cache, such as a live node's
orders_report = ReportProvider.generate_orders_report(cache.orders())

Returns pd.DataFrame. Columns include:

ColumnDescription
client_order_idIndex - unique order identifier.
instrument_idTrading instrument.
strategy_idStrategy that created the order.
trader_idTrader identifier.
account_idAccount identifier (if assigned).
venue_order_idVenue‑assigned order ID (if accepted).
sideBUY or SELL.
typeMARKET, LIMIT, etc.
statusCurrent order status.
quantityOriginal order quantity (string).
filled_qtyAmount filled (string).
priceLimit price (string, order‑type dependent).
avg_pxAverage fill price (string, if filled).
time_in_forceTime‑in‑force instruction.
ts_initOrder initialization timestamp (Unix nanoseconds).
ts_lastLast update timestamp (Unix nanoseconds).

Additional columns vary by order type, such as trigger_price for stop orders and expire_time_ns for GTD orders. See Order.to_dict() for the complete field list.

Order fills report

Provides a summary of filled orders (one row per order):

# From a completed backtest run
fills_report = engine.generate_order_fills_report()

# Or from any cache
fills_report = ReportProvider.generate_order_fills_report(cache.orders())

This report includes only orders with filled_qty > 0 and contains the same columns as the orders report, but filtered to executed orders only. Note that ts_init and ts_last are converted to datetime objects in this report for easier analysis.

Fills report

Details individual fill events (one row per fill):

# From a completed backtest run
fills_report = engine.generate_fills_report()

# Or from any cache
fills_report = ReportProvider.generate_fills_report(cache.orders())

Returns pd.DataFrame. Columns include:

ColumnDescription
client_order_idIndex - order identifier.
trade_idUnique trade/fill identifier.
venue_order_idVenue‑assigned order ID.
instrument_idTrading instrument.
strategy_idStrategy that created the order.
account_idAccount identifier.
position_idAssociated position ID (if applicable).
order_sideBUY or SELL.
order_typeOrder type (MARKET, LIMIT, etc.).
last_pxFill execution price (string).
last_qtyFill execution quantity (string).
currencyCurrency of the fill.
liquidity_sideMAKER or TAKER.
commissionCommission amount and currency (string).
ts_eventFill timestamp (datetime).
ts_initInitialization timestamp (datetime).

See OrderFilled.to_dict() for the complete field list; the report drops its type column.

Positions report

Position analysis including snapshots:

# From a completed backtest run, which includes snapshots automatically
positions_report = engine.generate_positions_report()

# Or from any cache
positions_report = ReportProvider.generate_positions_report(
    positions=cache.positions(),
    snapshots=cache.position_snapshots(),  # Needed for NETTING OMS totals
)

Returns pd.DataFrame. Columns include:

ColumnDescription
position_idIndex - unique position identifier.
instrument_idTrading instrument.
strategy_idStrategy that managed the position.
trader_idTrader identifier.
account_idAccount identifier.
opening_order_idOrder ID that opened the position.
closing_order_idOrder ID that closed the position.
entryEntry side (BUY or SELL).
sidePosition side (LONG, SHORT, or FLAT).
quantityCurrent position size (string).
peak_qtyMaximum size reached (string).
avg_px_openAverage entry price (float).
avg_px_closeAverage exit price (float, if closed).
commissionsCommissions paid, one entry per currency (list).
realized_pnlRealized profit/loss in the cost currency (string).
realized_returnRealized return as a ratio (float), so 0.05 is 5%.
ts_initPosition initialization timestamp (Unix nanoseconds).
ts_openedOpening timestamp (datetime).
ts_lastLast update timestamp (Unix nanoseconds).
ts_closedClosing timestamp (datetime or NA).
duration_nsPosition duration in nanoseconds.
is_snapshotWhether this is a historical snapshot.

Snapshot rows are indexed by a generated ID derived from the original position ID, so use is_snapshot rather than the index to separate archived cycles from live positions. See Position.to_dict() for the complete field list; the report drops signed_qty, base_currency, quote_currency, and settlement_currency.

Account report

Tracks account balance and margin changes over time:

from nautilus_trader.model import Venue

venue = Venue("BINANCE")

# From a completed backtest run
account_report = engine.generate_account_report(venue=venue)

# Or from any cache
account_report = ReportProvider.generate_account_report(cache.account_for_venue(venue))

BacktestEngine.generate_account_report() requires venue or account_id and raises ValueError when both are omitted. account_id takes precedence when both are supplied, and an unknown account yields an empty DataFrame.

Returns pd.DataFrame. Columns include:

ColumnDescription
ts_eventIndex - timestamp of account state change.
account_idAccount identifier.
account_typeType of account (e.g., SPOT, MARGIN).
base_currencyBase currency for the account.
totalTotal balance amount (string).
freeAvailable balance (string).
lockedBalance locked in orders (string).
currencyCurrency of the balance.
reportedWhether balance was reported by venue.
marginsMargin information (list, if applicable).
infoAdditional venue‑specific information.

Each row represents a balance entry; accounts with multiple currencies produce multiple rows per account state event.

PnL accounting considerations

Accurate PnL accounting requires careful consideration of several factors:

Position-based PnL

  • Realized PnL: Calculated when positions are partially or fully closed.
  • Unrealized PnL: Marked-to-market using current prices. Position.unrealized_pnl(last) marks an open position at a given Price.
  • Commission impact: Only included when in the position's cost currency. See Positions for how base‑currency commissions on spot pairs adjust position size instead.

PnL calculations depend on the OMS type. In NETTING OMS, position snapshots preserve historical PnL when positions reopen. Always include snapshots in reports for accurate total PnL calculation. In HEDGING OMS, snapshots are not used since each position has a unique ID and is never reopened.

Multi-currency accounting

When dealing with multiple currencies:

  • Each position tracks PnL in its cost currency: quote for linear contracts, base for inverse contracts, and settlement for quanto contracts.
  • Portfolio aggregation requires currency conversion. Portfolio.realized_pnls(target_currency=...) does this with cached exchange rates; see Supported conversions.
  • Commission currencies may differ from the position's cost currency.
from decimal import Decimal

# Accessing PnL across positions
for position in cache.positions_closed():
    realized = position.realized_pnl  # Money in the position's cost currency, or None

    if realized is None or realized.currency == base_currency:
        continue

    # Converting by hand: cache.get_xrate() returns a float, so wrap rates as Decimal
    rate = Decimal(str(my_fx_rates[(realized.currency, base_currency)]))
    converted = realized.as_decimal() * rate

Snapshot considerations

For NETTING OMS, an accurate instrument total adds the realized PnL of every archived cycle to the live position. See Position snapshotting for how the execution engine archives a closed cycle.

from decimal import Decimal

from nautilus_trader.model import Money

pnl_by_currency = {}

for position in cache.positions(instrument_id=instrument_id):
    # Archived cycles are stored under the live position's ID
    snapshots = cache.position_snapshots(position_id=position.id)

    for pnl in (position.realized_pnl, *(s.realized_pnl for s in snapshots)):
        if pnl is None:
            continue

        running = pnl_by_currency.get(pnl.currency, Decimal(0))
        pnl_by_currency[pnl.currency] = running + pnl.as_decimal()

# Create Money objects for each currency
total_pnls = [Money.from_decimal(amount, currency) for currency, amount in pnl_by_currency.items()]

Backtest post-run analysis

After a backtest completes, analysis is available through result statistics and generated reports.

Accessing backtest results

# After backtest run
engine.run()

# Access result statistics
result = engine.get_result()

# Generate reports from the backtest engine
fills_report = engine.generate_fills_report()
venue = engine.list_venues()[0]
account_report = engine.generate_account_report(venue=venue)

# Or access data directly for custom analysis
orders = engine.cache.orders()
positions = engine.cache.positions()
snapshots = engine.cache.position_snapshots()

Portfolio statistics

The backtest result provides performance metrics:

# Access backtest result statistics
result = engine.get_result()

# Get different categories of statistics
stats_pnls = result.stats_pnls  # Keyed by currency code, then statistic name
stats_returns = result.stats_returns  # Keyed by statistic name
stats_general = result.stats_general  # Keyed by statistic name

Each statistic appears in exactly one category, determined by the input it consumes: realized PnLs, returns, or positions.

See the Portfolio guide for the default statistic set, how each category is derived, and the difference between position returns and portfolio returns.

Visualization

NautilusTrader provides interactive tearsheets and plots via Plotly:

from nautilus_trader.analysis import create_tearsheet

# After backtest run
engine.run()

# Generate interactive HTML tearsheet
create_tearsheet(engine, output_path="tearsheet.html")

This creates an interactive HTML report with:

  • Equity curve
  • Drawdown analysis
  • Monthly returns heatmap
  • Performance statistics table
  • Returns distribution

For more control, generate individual plots:

import pandas as pd

from nautilus_trader.analysis import create_equity_curve

returns = pd.Series(
    [0.01, -0.005, 0.002],
    index=pd.date_range("2024-01-01", periods=3, tz="UTC"),
)
fig = create_equity_curve(returns, title="My Strategy Equity")
fig.show()  # Display in browser
fig.write_image("equity.png")  # Export to PNG (requires kaleido)

Install visualization dependencies:

uv pip install "nautilus_trader[visualization]"

Report generation patterns

Live trading

During live trading, generate reports periodically:

from datetime import timedelta

from nautilus_trader.analysis import ReportProvider
from nautilus_trader.common import DataActor
from nautilus_trader.common import TimeEvent


class ReportingActor(DataActor):
    def on_start(self) -> None:
        # Schedule periodic reporting
        self.clock.set_timer(
            name="generate_reports",
            interval=timedelta(minutes=30),
            callback=self.generate_reports,
        )

    def generate_reports(self, event: TimeEvent) -> None:
        # Generate and log reports
        positions_report = ReportProvider.generate_positions_report(
            positions=self.cache.positions(),
            snapshots=self.cache.position_snapshots(),
        )

        # Save or transmit report
        positions_report.to_csv(f"positions_{event.ts_event}.csv")

Performance analysis

For backtest analysis:

import pandas as pd

# Run the backtest
engine.run()

# Collect results
positions_closed = engine.cache.positions_closed()
result = engine.get_result()
stats_pnls = result.stats_pnls
stats_returns = result.stats_returns
stats_general = result.stats_general

# Create summary dictionary
results = {
    "total_positions": len(positions_closed),
    "pnl_total": stats_pnls.get("USD", {}).get("PnL (total)"),
    "win_rate": stats_pnls.get("USD", {}).get("Win Rate"),
    "sharpe_ratio": stats_returns.get("Sharpe Ratio (252 days)"),
    "profit_factor": stats_returns.get("Profit Factor"),
    "long_ratio": stats_general.get("Long Ratio"),
}

# Display results
results_df = pd.DataFrame([results])
print(results_df.T)  # Transpose for vertical display

Reports are generated from in-memory data structures. For large-scale analysis or long-running systems, consider persisting reports to a database for efficient querying. See the Cache guide for persistence options.

Integration with other components

The ReportProvider works with several system components:

  • Cache: Source of all trading data (orders, positions, accounts) for reports.
  • Portfolio: Computes its statistics from the same cache data independently, not from these reports.
  • BacktestEngine: Exposes the report methods used for post-run analysis and visualization.
  • Position snapshots: Required for accurate PnL reporting in NETTING OMS.
  • Visualization - Interactive tearsheets and charts from backtest results.
  • Portfolio - Portfolio statistics and performance metrics.
  • Backtesting - Running backtests that generate reports.
  • Cache - Cache system that stores data for reports.

On this page