Skip to content

Commit b4cfaa0

Browse files
authored
Add backtest portfolio example (#2362)
1 parent ce7ee57 commit b4cfaa0

File tree

3 files changed

+307
-0
lines changed

3 files changed

+307
-0
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Portfolio Example
2+
3+
A simple strategy demonstrating how to use Portfolio in NautilusTrader.
4+
5+
The Portfolio is a central component that tracks the state of your trading account.
6+
It connects directly to the broker to get real-time positions, balances, and P&L.
7+
8+
## Example Highlights
9+
10+
The strategy shows portfolio information at four key points:
11+
12+
1. **Initial State**: Before any trades are executed
13+
2. **Position Open**: When a new position is created
14+
3. **Mid-Trade**: Two minutes after position opening
15+
4. **Final State**: After all positions are closed (when strategy stops)
16+
17+
To simulate these specific portfolio states, the strategy fires bracket order (a combination of an entry order
18+
with associated take-profit and stop-loss orders), allowing us to demonstrate the complete lifecycle of portfolio states.
19+
20+
## Additional info
21+
22+
Key differences between `Portfolio` and `Cache`:
23+
24+
`Portfolio`:
25+
26+
- Gets data directly from broker for maximum accuracy
27+
- Best for real-time position and risk management
28+
- Provides authoritative account state (margins, balances)
29+
- Should be used for critical trading decisions
30+
31+
`Cache`:
32+
33+
- Stores all trading data in system memory
34+
- Useful for quick access to historical data and market state
35+
- More efficient for frequent queries as it avoids broker round-trips
36+
- Updates automatically as new data arrives
37+
- Might have minimal delay compared to broker data
38+
39+
## Additional Resources
40+
41+
For more information about Portfolio in NautilusTrader, see:
42+
43+
- Portfolio API documentation - search the codebase for `Portfolio` class.
44+
- Portfolio concept guide - see the "Portfolio" section in the documentation for more details.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env python3
2+
# -------------------------------------------------------------------------------------------------
3+
# Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
4+
# https://nautechsystems.io
5+
#
6+
# Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
7+
# You may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
# -------------------------------------------------------------------------------------------------
16+
17+
from decimal import Decimal
18+
19+
import pandas as pd
20+
from strategy import DemoStrategy
21+
from strategy import DemoStrategyConfig
22+
23+
from nautilus_trader import TEST_DATA_DIR
24+
from nautilus_trader.backtest.engine import BacktestEngine
25+
from nautilus_trader.config import BacktestEngineConfig
26+
from nautilus_trader.config import LoggingConfig
27+
from nautilus_trader.model import TraderId
28+
from nautilus_trader.model.currencies import USD
29+
from nautilus_trader.model.data import Bar
30+
from nautilus_trader.model.data import BarType
31+
from nautilus_trader.model.enums import AccountType
32+
from nautilus_trader.model.enums import OmsType
33+
from nautilus_trader.model.identifiers import Venue
34+
from nautilus_trader.model.objects import Money
35+
from nautilus_trader.persistence.wranglers import BarDataWrangler
36+
from nautilus_trader.test_kit.providers import TestInstrumentProvider
37+
38+
39+
if __name__ == "__main__":
40+
41+
# Step 1: Configure and create backtest engine
42+
engine_config = BacktestEngineConfig(
43+
trader_id=TraderId("BACKTEST_TRADER-001"),
44+
logging=LoggingConfig(
45+
log_level="DEBUG", # Enable debug logging
46+
),
47+
)
48+
engine = BacktestEngine(config=engine_config)
49+
50+
# Step 2: Define exchange venue and add it to the engine
51+
# We use XCME (CME Exchange) and configure it with margin account
52+
XCME = Venue("XCME")
53+
engine.add_venue(
54+
venue=XCME,
55+
oms_type=OmsType.NETTING, # Order Management System type
56+
account_type=AccountType.MARGIN, # Type of trading account
57+
starting_balances=[Money(1_000_000, USD)], # Initial account balance
58+
base_currency=USD, # Base currency for account
59+
default_leverage=Decimal(1), # No leverage used for account
60+
)
61+
62+
# Step 3: Create instrument definition and add it to the engine
63+
# We use EURUSD futures contract for this example
64+
EURUSD_INSTRUMENT = TestInstrumentProvider.eurusd_future(
65+
expiry_year=2024,
66+
expiry_month=3,
67+
venue_name="XCME",
68+
)
69+
engine.add_instrument(EURUSD_INSTRUMENT)
70+
71+
# Step 4: Load and prepare market data
72+
73+
# Step 4a: Load bar data from CSV file -> into pandas DataFrame
74+
csv_file_path = rf"{TEST_DATA_DIR}/xcme/6EH4.XCME_1min_bars_20240101_20240131.csv.gz"
75+
df = pd.read_csv(csv_file_path, header=0, index_col=False)
76+
77+
# Step 4b: Restructure DataFrame into required format
78+
# Restructure DataFrame into required format
79+
df = (
80+
# Reorder columns to match required format
81+
df.reindex(columns=["timestamp_utc", "open", "high", "low", "close", "volume"])
82+
# Convert timestamp strings to datetime objects
83+
.assign(
84+
timestamp_utc=lambda dft: pd.to_datetime(
85+
dft["timestamp_utc"],
86+
format="%Y-%m-%d %H:%M:%S",
87+
),
88+
)
89+
# Rename timestamp column and set as index
90+
.rename(columns={"timestamp_utc": "timestamp"}).set_index("timestamp")
91+
)
92+
93+
# Step 4c: Define bar type for our data
94+
EURUSD_1MIN_BARTYPE = BarType.from_str(f"{EURUSD_INSTRUMENT.id}-1-MINUTE-LAST-EXTERNAL")
95+
96+
# Step 4d: Convert DataFrame rows into Bar objects
97+
wrangler = BarDataWrangler(EURUSD_1MIN_BARTYPE, EURUSD_INSTRUMENT)
98+
eurusd_1min_bars_list: list[Bar] = wrangler.process(df)
99+
100+
# Step 4e: Add the prepared data to the engine
101+
engine.add_data(eurusd_1min_bars_list)
102+
103+
# Step 5: Create and add our portfolio demonstration strategy
104+
strategy_config = DemoStrategyConfig(
105+
bar_type=EURUSD_1MIN_BARTYPE,
106+
instrument=EURUSD_INSTRUMENT,
107+
)
108+
strategy = DemoStrategy(config=strategy_config)
109+
engine.add_strategy(strategy)
110+
111+
# Step 6: Run the backtest
112+
engine.run()
113+
114+
# Step 7: Release system resources
115+
engine.dispose()
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# -------------------------------------------------------------------------------------------------
2+
# Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3+
# https://nautechsystems.io
4+
#
5+
# Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6+
# You may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
# -------------------------------------------------------------------------------------------------
15+
16+
17+
from nautilus_trader.common.enums import LogColor
18+
from nautilus_trader.model.data import Bar
19+
from nautilus_trader.model.data import BarType
20+
from nautilus_trader.model.enums import OrderSide
21+
from nautilus_trader.model.enums import TimeInForce
22+
from nautilus_trader.model.events import PositionOpened
23+
from nautilus_trader.model.instruments.base import Instrument
24+
from nautilus_trader.trading.config import StrategyConfig
25+
from nautilus_trader.trading.strategy import Strategy
26+
27+
28+
class DemoStrategyConfig(StrategyConfig, frozen=True):
29+
bar_type: BarType
30+
instrument: Instrument
31+
32+
33+
class DemoStrategy(Strategy):
34+
35+
def __init__(self, config: DemoStrategyConfig):
36+
super().__init__(config=config)
37+
38+
# Track if we've already placed an order
39+
self.order_placed = False
40+
41+
# Track total bars seen
42+
self.count_of_bars: int = 0
43+
self.show_portfolio_at_bar: int | None = 0
44+
45+
def on_start(self):
46+
"""
47+
Handle strategy start event.
48+
"""
49+
# Subscribe to market data
50+
self.subscribe_bars(self.config.bar_type)
51+
52+
# Show initial portfolio state
53+
self.show_portfolio_info("Portfolio state (Before trade)")
54+
55+
def on_bar(self, bar: Bar):
56+
"""
57+
Handle new bar event.
58+
"""
59+
# Increment total bars seen
60+
self.count_of_bars += 1
61+
62+
# Show portfolio state if we reached target bar
63+
if self.show_portfolio_at_bar == self.count_of_bars:
64+
self.show_portfolio_info("Portfolio state (2 minutes after position opened)")
65+
66+
# Only place one order for demonstration
67+
if not self.order_placed:
68+
# Prepare values for order
69+
last_price = bar.close
70+
tick_size = self.config.instrument.price_increment
71+
profit_price = self.config.instrument.make_price(last_price + (10 * tick_size))
72+
stoploss_price = self.config.instrument.make_price(last_price - (10 * tick_size))
73+
74+
# Create BUY MARKET order with PT and SL (both 10 ticks)
75+
bracket_order_list = self.order_factory.bracket(
76+
instrument_id=self.config.instrument.id,
77+
order_side=OrderSide.BUY,
78+
quantity=self.config.instrument.make_qty(1), # Trade size: 1 contract
79+
time_in_force=TimeInForce.GTC,
80+
tp_price=profit_price,
81+
sl_trigger_price=stoploss_price,
82+
)
83+
84+
# Submit order and remember it
85+
self.submit_order_list(bracket_order_list)
86+
self.order_placed = True
87+
self.log.info(f"Submitted bracket order: {bracket_order_list}", color=LogColor.GREEN)
88+
89+
def on_position_opened(self, event: PositionOpened):
90+
"""
91+
Handle position opened event.
92+
"""
93+
# Log position details
94+
self.log.info(f"Position opened: {event}", color=LogColor.GREEN)
95+
96+
# Show portfolio state when position is opened
97+
self.show_portfolio_info("Portfolio state (In position):")
98+
99+
# Set target bar number for next portfolio display
100+
self.show_portfolio_at_bar = self.count_of_bars + 2 # Show after 2 bars
101+
102+
def on_stop(self):
103+
"""
104+
Handle strategy stop event.
105+
"""
106+
# Show final portfolio state
107+
self.show_portfolio_info("Portfolio state (After trade)")
108+
109+
def show_portfolio_info(self, intro_message: str = ""):
110+
"""
111+
Display current portfolio information.
112+
"""
113+
if intro_message:
114+
self.log.info(f"====== {intro_message} ======")
115+
116+
# POSITION information
117+
self.log.info("Portfolio -> Position information:", color=LogColor.BLUE)
118+
is_flat = self.portfolio.is_flat(self.config.instrument.id)
119+
self.log.info(f"Is flat: {is_flat}", color=LogColor.BLUE)
120+
121+
net_position = self.portfolio.net_position(self.config.instrument.id)
122+
self.log.info(f"Net position: {net_position} contract(s)", color=LogColor.BLUE)
123+
124+
net_exposure = self.portfolio.net_exposure(self.config.instrument.id)
125+
self.log.info(f"Net exposure: {net_exposure}", color=LogColor.BLUE)
126+
127+
# -----------------------------------------------------
128+
129+
# P&L information
130+
self.log.info("Portfolio -> P&L information:", color=LogColor.YELLOW)
131+
132+
realized_pnl = self.portfolio.realized_pnl(self.config.instrument.id)
133+
self.log.info(f"Realized P&L: {realized_pnl}", color=LogColor.YELLOW)
134+
135+
unrealized_pnl = self.portfolio.unrealized_pnl(self.config.instrument.id)
136+
self.log.info(f"Unrealized P&L: {unrealized_pnl}", color=LogColor.YELLOW)
137+
138+
# -----------------------------------------------------
139+
140+
self.log.info("Portfolio -> Account information:", color=LogColor.CYAN)
141+
margins_init = self.portfolio.margins_init(self.config.instrument.venue)
142+
self.log.info(f"Initial margin: {margins_init}", color=LogColor.CYAN)
143+
144+
margins_maint = self.portfolio.margins_maint(self.config.instrument.venue)
145+
self.log.info(f"Maintenance margin: {margins_maint}", color=LogColor.CYAN)
146+
147+
balances_locked = self.portfolio.balances_locked(self.config.instrument.venue)
148+
self.log.info(f"Locked balance: {balances_locked}", color=LogColor.CYAN)

0 commit comments

Comments
 (0)