docs/concepts/reports.md
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.
The ReportProvider class in NautilusTrader generates structured analytical reports from
trading data, transforming raw 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.
Reports can be generated using two approaches:
trader.generate_orders_report().Reports provide consistent analytics across both backtesting and live trading environments, enabling reliable performance evaluation and strategy comparison.
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.
Generates a full view of all orders:
# Using Trader helper method (recommended)
orders_report = trader.generate_orders_report()
# Or using ReportProvider directly
from nautilus_trader.analysis import ReportProvider
orders = cache.orders()
orders_report = ReportProvider.generate_orders_report(orders)
Returns pd.DataFrame. Key columns include:
| Column | Description |
|---|---|
client_order_id | Index - unique order identifier. |
instrument_id | Trading instrument. |
strategy_id | Strategy that created the order. |
trader_id | Trader identifier. |
account_id | Account identifier (if assigned). |
venue_order_id | Venue‑assigned order ID (if accepted). |
side | BUY or SELL. |
type | MARKET, LIMIT, etc. |
status | Current order status. |
quantity | Original order quantity (string). |
filled_qty | Amount filled (string). |
price | Limit price (order‑type dependent). |
avg_px | Average fill price (if filled). |
time_in_force | Time‑in‑force instruction. |
ts_init | Order initialization timestamp (Unix nanoseconds). |
ts_last | Last update timestamp (Unix nanoseconds). |
Additional columns vary by order type (e.g., trigger_price for stop orders, expire_time for
GTD orders). See Order.to_dict() for the complete field list.
Provides a summary of filled orders (one row per order):
# Using Trader helper method (recommended)
fills_report = trader.generate_order_fills_report()
# Or using ReportProvider directly
orders = cache.orders()
fills_report = ReportProvider.generate_order_fills_report(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.
Details individual fill events (one row per fill):
# Using Trader helper method (recommended)
fills_report = trader.generate_fills_report()
# Or using ReportProvider directly
orders = cache.orders()
fills_report = ReportProvider.generate_fills_report(orders)
Returns pd.DataFrame. Key columns include:
| Column | Description |
|---|---|
client_order_id | Index - order identifier. |
trade_id | Unique trade/fill identifier. |
venue_order_id | Venue‑assigned order ID. |
instrument_id | Trading instrument. |
strategy_id | Strategy that created the order. |
account_id | Account identifier. |
position_id | Associated position ID (if applicable). |
order_side | BUY or SELL. |
order_type | Order type (MARKET, LIMIT, etc.). |
last_px | Fill execution price (string). |
last_qty | Fill execution quantity (string). |
currency | Currency of the fill. |
liquidity_side | MAKER or TAKER. |
commission | Commission amount and currency. |
ts_event | Fill timestamp (datetime). |
ts_init | Initialization timestamp (datetime). |
See OrderFilled.to_dict() for the complete field list.
Position analysis including snapshots:
# Using Trader helper method (recommended)
# Automatically includes snapshots for NETTING OMS
positions_report = trader.generate_positions_report()
# Or using ReportProvider directly
positions = cache.positions()
snapshots = cache.position_snapshots() # For NETTING OMS
positions_report = ReportProvider.generate_positions_report(
positions=positions,
snapshots=snapshots
)
Returns pd.DataFrame. Key columns include:
| Column | Description |
|---|---|
position_id | Index - unique position identifier. |
instrument_id | Trading instrument. |
strategy_id | Strategy that managed the position. |
trader_id | Trader identifier. |
account_id | Account identifier. |
opening_order_id | Order ID that opened the position. |
closing_order_id | Order ID that closed the position. |
entry | Entry side (BUY or SELL). |
side | Position side (LONG, SHORT, or FLAT). |
quantity | Current position size. |
peak_qty | Maximum size reached. |
avg_px_open | Average entry price. |
avg_px_close | Average exit price (if closed). |
commissions | List of commissions paid. |
realized_pnl | Realized profit/loss. |
realized_return | Return percentage. |
ts_init | Position initialization timestamp. |
ts_opened | Opening timestamp (datetime). |
ts_last | Last update timestamp. |
ts_closed | Closing timestamp (datetime or NA). |
duration_ns | Position duration in nanoseconds. |
is_snapshot | Whether this is a historical snapshot. |
Tracks account balance and margin changes over time:
# Using Trader helper method (recommended)
# Requires venue parameter
from nautilus_trader.model.identifiers import Venue
venue = Venue("BINANCE")
account_report = trader.generate_account_report(venue)
# Or using ReportProvider directly
account = cache.account(account_id)
account_report = ReportProvider.generate_account_report(account)
Returns pd.DataFrame. Columns include:
| Column | Description |
|---|---|
ts_event | Index - timestamp of account state change. |
account_id | Account identifier. |
account_type | Type of account (e.g., SPOT, MARGIN). |
base_currency | Base currency for the account. |
total | Total balance amount (string). |
free | Available balance (string). |
locked | Balance locked in orders (string). |
currency | Currency of the balance. |
reported | Whether balance was reported by venue. |
margins | Margin information (list, if applicable). |
info | Additional venue‑specific information. |
Each row represents a balance entry; accounts with multiple currencies produce multiple rows per account state event.
Accurate PnL accounting requires careful consideration of several factors:
:::warning
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.
:::
When dealing with multiple currencies:
# Accessing PnL across positions
for position in positions:
realized = position.realized_pnl # In settlement currency
unrealized = position.unrealized_pnl(last_price)
# Handle multi-currency aggregation (illustrative)
# Note: Currency conversion requires user-provided exchange rates
if position.settlement_currency != base_currency:
# Apply conversion rate from your data source
# rate = get_exchange_rate(position.settlement_currency, base_currency)
# realized_converted = realized.as_double() * rate
pass
For NETTING OMS:
from nautilus_trader.model.objects import Money
# Include snapshots for complete PnL (per currency)
pnl_by_currency = {}
# Add PnL from current positions
for position in cache.positions(instrument_id=instrument_id):
if position.realized_pnl:
currency = position.realized_pnl.currency
if currency not in pnl_by_currency:
pnl_by_currency[currency] = 0.0
pnl_by_currency[currency] += position.realized_pnl.as_double()
# Add PnL from historical snapshots
for snapshot in cache.position_snapshots(instrument_id=instrument_id):
if snapshot.realized_pnl:
currency = snapshot.realized_pnl.currency
if currency not in pnl_by_currency:
pnl_by_currency[currency] = 0.0
pnl_by_currency[currency] += snapshot.realized_pnl.as_double()
# Create Money objects for each currency
total_pnls = [Money(amount, currency) for currency, amount in pnl_by_currency.items()]
After a backtest completes, analysis is available through various reports and the portfolio analyzer.
# After backtest run
engine.run(start=start_time, end=end_time)
# Generate reports using Trader helper methods
orders_report = engine.trader.generate_orders_report()
positions_report = engine.trader.generate_positions_report()
fills_report = engine.trader.generate_fills_report()
# Or access data directly for custom analysis
orders = engine.cache.orders()
positions = engine.cache.positions()
snapshots = engine.cache.position_snapshots()
The portfolio analyzer provides performance metrics:
# Access portfolio analyzer
portfolio = engine.portfolio
# Get different categories of statistics
stats_pnls = portfolio.analyzer.get_performance_stats_pnls()
stats_returns = portfolio.analyzer.get_performance_stats_returns()
stats_general = portfolio.analyzer.get_performance_stats_general()
:::info For detailed information about available statistics and creating custom metrics, see the Portfolio guide. The Portfolio guide covers:
PortfolioStatistic.:::
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:
For more control, generate individual plots:
from nautilus_trader.analysis import create_equity_curve
returns = engine.portfolio.analyzer.returns()
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]"
During live trading, generate reports periodically:
import pandas as pd
class ReportingActor(Actor):
def on_start(self):
# Schedule periodic reporting
self.clock.set_timer(
name="generate_reports",
interval=pd.Timedelta(minutes=30),
callback=self.generate_reports
)
def generate_reports(self, event):
# Generate and log reports
positions_report = self.trader.generate_positions_report()
# Save or transmit report
positions_report.to_csv(f"positions_{event.ts_event}.csv")
For backtest analysis:
import pandas as pd
# Run the backtest
engine.run(start=start_time, end=end_time)
# Collect results
positions_closed = engine.cache.positions_closed()
stats_pnls = engine.portfolio.analyzer.get_performance_stats_pnls()
stats_returns = engine.portfolio.analyzer.get_performance_stats_returns()
stats_general = engine.portfolio.analyzer.get_performance_stats_general()
# Create summary dictionary
results = {
"total_positions": len(positions_closed),
"pnl_total": stats_pnls.get("PnL (total)"),
"sharpe_ratio": stats_returns.get("Sharpe Ratio (252 days)"),
"profit_factor": stats_general.get("Profit Factor"),
"win_rate": stats_general.get("Win Rate"),
}
# Display results
results_df = pd.DataFrame([results])
print(results_df.T) # Transpose for vertical display
:::info 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. :::
The ReportProvider works with several system components:
NETTING OMS.The ReportProvider generates reports from orders, fills, positions, and account
states as structured DataFrames for analysis and visualization. For accurate total
PnL in NETTING OMS, include position snapshots when generating reports.