Skip to content

Latest commit

 

History

History
516 lines (382 loc) · 20.3 KB

File metadata and controls

516 lines (382 loc) · 20.3 KB

Cache

The Cache is a central in-memory database that automatically stores and manages all trading-related data. Think of it as your trading system’s memory – storing everything from market data to order history to custom calculations.

The Cache serves multiple key purposes:

  1. Stores market data:

    • Stores recent market history (e.g., order books, quotes, trades, bars).
    • Gives you access to both current and historical market data for your strategy.
  2. Tracks trading data:

    • Maintains complete Order history and current execution state.
    • Tracks all Positions and Account information.
    • Stores Instrument definitions and Currency information.
  3. Stores custom data:

    • Any user-defined objects or data can be stored in the Cache for later use.
    • Enables data sharing between different strategies.

How Cache works

Built-in types:

  • Data is automatically added to the Cache as it flows through the system.
  • In live contexts, updates happen asynchronously - which means there might be a small delay between an event occurring and it appearing in the Cache.
  • All data flows through the Cache before reaching your strategy’s callbacks – see the diagram below:
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐     ┌───────────────────────┐
│                 │     │                 │     │                 │     │                       │
│                 │     │                 │     │                 │     │   Strategy callback:  │
│      Data       ├─────►   DataEngine    ├─────►     Cache       ├─────►                       │
│                 │     │                 │     │                 │     │   on_data(...)        │
│                 │     │                 │     │                 │     │                       │
└─────────────────┘     └─────────────────┘     └─────────────────┘     └───────────────────────┘

Basic example

Within a strategy, you can access the Cache through self.cache. Here’s a typical example:

:::note Anywhere you find self, it refers mostly to the Strategy itself. :::

def on_bar(self, bar: Bar) -> None:
    # Current bar is provided in the parameter 'bar'

    # Get historical bars from Cache
    last_bar = self.cache.bar(self.bar_type, index=0)        # Last bar (practically the same as the 'bar' parameter)
    previous_bar = self.cache.bar(self.bar_type, index=1)    # Previous bar
    third_last_bar = self.cache.bar(self.bar_type, index=2)  # Third last bar

    # Get current position information
    if self.last_position_opened_id is not None:
        position = self.cache.position(self.last_position_opened_id)
        if position.is_open:
            # Check position details
            current_pnl = position.unrealized_pnl

    # Get all open orders for our instrument
    open_orders = self.cache.orders_open(instrument_id=self.instrument_id)

Configuration

The Cache’s behavior and capacity can be configured through the CacheConfig class. You can provide this configuration either to a BacktestEngine or a TradingNode, depending on your environment context.

Here's a basic example of configuring the Cache:

from nautilus_trader.config import CacheConfig, BacktestEngineConfig, TradingNodeConfig

# For backtesting
engine_config = BacktestEngineConfig(
    cache=CacheConfig(
        tick_capacity=10_000,  # Store last 10,000 ticks per instrument
        bar_capacity=5_000,    # Store last 5,000 bars per bar type
    ),
)

# For live trading
node_config = TradingNodeConfig(
    cache=CacheConfig(
        tick_capacity=10_000,
        bar_capacity=5_000,
    ),
)

:::tip By default, the Cache keeps the last 10,000 bars for each bar type and 10,000 trade ticks per instrument. These limits provide a good balance between memory usage and data availability. Increase them if your strategy needs more historical data. :::

Configuration Options

The CacheConfig class supports these parameters:

from nautilus_trader.config import CacheConfig

cache_config = CacheConfig(
    database: DatabaseConfig | None = None,  # Database configuration for persistence
    encoding: str = "msgpack",               # Data encoding format ('msgpack' or 'json')
    timestamps_as_iso8601: bool = False,     # Store timestamps as ISO8601 strings
    buffer_interval_ms: int | None = None,   # Buffer interval for batch operations
    use_trader_prefix: bool = True,          # Use trader prefix in keys
    use_instance_id: bool = False,           # Include instance ID in keys
    flush_on_start: bool = False,            # Clear database on startup
    drop_instruments_on_reset: bool = True,  # Clear instruments on reset
    tick_capacity: int = 10_000,             # Maximum ticks stored per instrument
    bar_capacity: int = 10_000,              # Maximum bars stored per each bar-type
)

:::note Each bar type maintains its own separate capacity. For example, if you're using both 1-minute and 5-minute bars, each will store up to bar_capacity bars. When bar_capacity is reached, the oldest data is automatically removed from the Cache. :::

Database Configuration

For persistence between system restarts, you can configure a database backend.

When is it useful to use persistence?

  • Long-running systems: If you want your data to survive system restarts, upgrading, or unexpected failures, having a database configuration helps to pick up exactly where you left off.
  • Historical insights: When you need to preserve past trading data for detailed post-analysis or audits.
  • Multi-node or distributed setups: If multiple services or nodes need to access the same state, a persistent store helps ensure shared and consistent data.
from nautilus_trader.config import DatabaseConfig

config = CacheConfig(
    database=DatabaseConfig(
        type="redis",      # Database type
        host="localhost",  # Database host
        port=6379,         # Database port
        timeout=2,         # Connection timeout (seconds)
    ),
)

Using the Cache

Accessing Market data

The Cache provides a comprehensive interface for accessing different types of market data, including order books, quotes, trades, bars. All market data in the cache are stored with reverse indexing — meaning the most recent data is at index 0.

Bar(s) access

# Get a list of all cached bars for a bar type
bars = self.cache.bars(bar_type)  # Returns List[Bar] or an empty list if no bars found

# Get the most recent bar
latest_bar = self.cache.bar(bar_type)  # Returns Bar or None if no such object exists

# Get a specific historical bar by index (0 = most recent)
second_last_bar = self.cache.bar(bar_type, index=1)  # Returns Bar or None if no such object exists

# Check if bars exist and get count
bar_count = self.cache.bar_count(bar_type)  # Returns number of bars in cache for the specified bar type
has_bars = self.cache.has_bars(bar_type)    # Returns bool indicating if any bars exist for the specified bar type

Quote ticks

# Get quotes
quotes = self.cache.quote_ticks(instrument_id)                     # Returns List[QuoteTick] or an empty list if no quotes found
latest_quote = self.cache.quote_tick(instrument_id)                # Returns QuoteTick or None if no such object exists
second_last_quote = self.cache.quote_tick(instrument_id, index=1)  # Returns QuoteTick or None if no such object exists

# Check quote availability
quote_count = self.cache.quote_tick_count(instrument_id)  # Returns the number of quotes in cache for this instrument
has_quotes = self.cache.has_quote_ticks(instrument_id)    # Returns bool indicating if any quotes exist for this instrument

Trade ticks

# Get trades
trades = self.cache.trade_ticks(instrument_id)         # Returns List[TradeTick] or an empty list if no trades found
latest_trade = self.cache.trade_tick(instrument_id)    # Returns TradeTick or None if no such object exists
second_last_trade = self.cache.trade_tick(instrument_id, index=1)  # Returns TradeTick or None if no such object exists

# Check trade availability
trade_count = self.cache.trade_tick_count(instrument_id)  # Returns the number of trades in cache for this instrument
has_trades = self.cache.has_trade_ticks(instrument_id)    # Returns bool indicating if any trades exist

Order Book

# Get current order book
book = self.cache.order_book(instrument_id)  # Returns OrderBook or None if no such object exists

# Check if order book exists
has_book = self.cache.has_order_book(instrument_id)  # Returns bool indicating if an order book exists

# Get count of order book updates
update_count = self.cache.book_update_count(instrument_id)  # Returns the number of updates received

Price access

from nautilus_trader.core.rust.model import PriceType

# Get current price by type; Returns Price or None.
price = self.cache.price(
    instrument_id=instrument_id,
    price_type=PriceType.MID,  # Options: BID, ASK, MID, LAST
)

Bar types

from nautilus_trader.core.rust.model import PriceType, AggregationSource

# Get all available bar types for an instrument; Returns List[BarType].
bar_types = self.cache.bar_types(
    instrument_id=instrument_id,
    price_type=PriceType.LAST,  # Options: BID, ASK, MID, LAST
    aggregation_source=AggregationSource.EXTERNAL,
)

Simple example

class MarketDataStrategy(Strategy):
    def on_start(self):
        # Subscribe to 1-minute bars
        self.bar_type = BarType.from_str(f"{self.instrument_id}-1-MINUTE-LAST-EXTERNAL")  # example of instrument_id = "EUR/USD.FXCM"
        self.subscribe_bars(self.bar_type)

    def on_bar(self, bar: Bar) -> None:
        bars = self.cache.bars(self.bar_type)[:3]
        if len(bars) < 3:   # Wait until we have at least 3 bars
            return

        # Access last 3 bars for analysis
        current_bar = bars[0]    # Most recent bar
        prev_bar = bars[1]       # Second to last bar
        prev_prev_bar = bars[2]  # Third to last bar

        # Get latest quote and trade
        latest_quote = self.cache.quote_tick(self.instrument_id)
        latest_trade = self.cache.trade_tick(self.instrument_id)

        if latest_quote is not None:
            current_spread = latest_quote.ask_price - latest_quote.bid_price
            self.log.info(f"Current spread: {current_spread}")

Trading Objects

The Cache provides comprehensive access to all trading objects within the system, including:

  • Orders
  • Positions
  • Accounts
  • Instruments

Orders

Orders can be accessed and queried through multiple methods, with flexible filtering options by venue, strategy, instrument, and order side.

Basic Order Access
# Get a specific order by its client order ID
order = self.cache.order(ClientOrderId("O-123"))

# Get all orders in the system
orders = self.cache.orders()

# Get orders filtered by specific criteria
orders_for_venue = self.cache.orders(venue=venue)                       # All orders for a specific venue
orders_for_strategy = self.cache.orders(strategy_id=strategy_id)        # All orders for a specific strategy
orders_for_instrument = self.cache.orders(instrument_id=instrument_id)  # All orders for an instrument
Order State Queries
# Get orders by their current state
open_orders = self.cache.orders_open()          # Orders currently active at the venue
closed_orders = self.cache.orders_closed()      # Orders that have completed their lifecycle
emulated_orders = self.cache.orders_emulated()  # Orders being simulated locally by the system
inflight_orders = self.cache.orders_inflight()  # Orders submitted (or modified) to venue, but not yet confirmed

# Check specific order states
exists = self.cache.order_exists(client_order_id)            # Checks if an order with the given ID exists in the cache
is_open = self.cache.is_order_open(client_order_id)          # Checks if an order is currently open
is_closed = self.cache.is_order_closed(client_order_id)      # Checks if an order is closed
is_emulated = self.cache.is_order_emulated(client_order_id)  # Checks if an order is being simulated locally
is_inflight = self.cache.is_order_inflight(client_order_id)  # Checks if an order is submitted or modified, but not yet confirmed
Order Statistics
# Get counts of orders in different states
open_count = self.cache.orders_open_count()          # Number of open orders
closed_count = self.cache.orders_closed_count()      # Number of closed orders
emulated_count = self.cache.orders_emulated_count()  # Number of emulated orders
inflight_count = self.cache.orders_inflight_count()  # Number of inflight orders
total_count = self.cache.orders_total_count()        # Total number of orders in the system

# Get filtered order counts
buy_orders_count = self.cache.orders_open_count(side=OrderSide.BUY)  # Number of currently open BUY orders
venue_orders_count = self.cache.orders_total_count(venue=venue)      # Total number of orders for a given venue

Positions

The Cache maintains a record of all positions and offers several ways to query them.

Position Access
# Get a specific position by its ID
position = self.cache.position(PositionId("P-123"))

# Get positions by their state
all_positions = self.cache.positions()            # All positions in the system
open_positions = self.cache.positions_open()      # All currently open positions
closed_positions = self.cache.positions_closed()  # All closed positions

# Get positions filtered by various criteria
venue_positions = self.cache.positions(venue=venue)                       # Positions for a specific venue
instrument_positions = self.cache.positions(instrument_id=instrument_id)  # Positions for a specific instrument
strategy_positions = self.cache.positions(strategy_id=strategy_id)        # Positions for a specific strategy
long_positions = self.cache.positions(side=PositionSide.LONG)             # All long positions
Position State Queries
# Check position states
exists = self.cache.position_exists(position_id)        # Checks if a position with the given ID exists
is_open = self.cache.is_position_open(position_id)      # Checks if a position is open
is_closed = self.cache.is_position_closed(position_id)  # Checks if a position is closed

# Get position and order relationships
orders = self.cache.orders_for_position(position_id)       # All orders related to a specific position
position = self.cache.position_for_order(client_order_id)  # Find the position associated with a specific order
Position Statistics
# Get position counts in different states
open_count = self.cache.positions_open_count()      # Number of currently open positions
closed_count = self.cache.positions_closed_count()  # Number of closed positions
total_count = self.cache.positions_total_count()    # Total number of positions in the system

# Get filtered position counts
long_positions_count = self.cache.positions_open_count(side=PositionSide.LONG)              # Number of open long positions
instrument_positions_count = self.cache.positions_total_count(instrument_id=instrument_id)  # Number of positions for a given instrument

Accounts

# Access account information
account = self.cache.account(account_id)       # Retrieve account by ID
account = self.cache.account_for_venue(venue)  # Retrieve account for a specific venue
account_id = self.cache.account_id(venue)      # Retrieve account ID for a venue
accounts = self.cache.accounts()               # Retrieve all accounts in the cache

Instruments and Currencies

Instruments
# Get instrument information
instrument = self.cache.instrument(instrument_id) # Retrieve a specific instrument by its ID
all_instruments = self.cache.instruments()        # Retrieve all instruments in the cache

# Get filtered instruments
venue_instruments = self.cache.instruments(venue=venue)              # Instruments for a specific venue
instruments_by_underlying = self.cache.instruments(underlying="ES")  # Instruments by underlying

# Get instrument identifiers
instrument_ids = self.cache.instrument_ids()                   # Get all instrument IDs
venue_instrument_ids = self.cache.instrument_ids(venue=venue)  # Get instrument IDs for a specific venue
Currencies
# Get currency information
currency = self.cache.load_currency("USD")  # Loads currency data for USD

Custom Data

The Cache can also store and retrieve custom data types in addition to built-in market data and trading objects. You can keep any user-defined data you want to share between system components (mostly Actors / Strategies).

Basic Storage and Retrieval

# Call this code inside Strategy methods (`self` refers to Strategy)

# Store data
self.cache.add(key="my_key", value=b"some binary data")

# Retrieve data
stored_data = self.cache.get("my_key")  # Returns bytes or None

For more complex use cases, the Cache can store custom data objects that inherit from the nautilus_trader.core.Data base class.

:::warning The Cache is not designed to be a full database replacement. For large datasets or complex querying needs, consider using a dedicated database system. :::

Best Practices and Common Questions

Cache vs. Portfolio Usage

The Cache and Portfolio components serve different but complementary purposes in NautilusTrader:

Cache:

  • Maintains the historical knowledge and current state of the trading system.
  • Updates immediately for local state changes (initializing an order to be submitted)
  • Updates asynchronously as external events occur (order is filled).
  • Provides complete history of trading activity and market data.
  • All data a strategy has received (events/updates) is stored in Cache.

Portfolio:

  • Aggregated position/exposure and account information.
  • Provides current state without history.

Example:

class MyStrategy(Strategy):
    def on_position_changed(self, event: PositionEvent) -> None:
        # Use Cache when you need historical perspective
        position_history = self.cache.position_snapshots(event.position_id)

        # Use Portfolio when you need current real-time state
        current_exposure = self.portfolio.net_exposure(event.instrument_id)

Cache vs. Strategy variables

Choosing between storing data in the Cache versus strategy variables depends on your specific needs:

Cache Storage:

  • Use for data that needs to be shared between strategies.
  • Best for data that needs to persist between system restarts.
  • Acts as a central database accessible to all components.
  • Ideal for state that needs to survive strategy resets.

Strategy Variables:

  • Use for strategy-specific calculations.
  • Better for temporary values and intermediate results.
  • Provides faster access and better encapsulation.
  • Best for data that only your strategy needs.

Example:

Example that clarifies how you might store data in the Cache so multiple strategies can access the same information.

import pickle

class MyStrategy(Strategy):
    def on_start(self):
        # Prepare data you want to share with other strategies
        shared_data = {
            "last_reset": self.clock.timestamp_ns(),
            "trading_enabled": True,
            # Include any other fields that you want other strategies to read
        }

        # Store it in the cache with a descriptive key
        # This way, multiple strategies can call self.cache.get("shared_strategy_info")
        # to retrieve the same data
        self.cache.add("shared_strategy_info", pickle.dumps(shared_data))

How another strategy (running in parallel) can retrieve cached data above:

import pickle

class AnotherStrategy(Strategy):
    def on_start(self):
        # Load the shared data from the same key
        data_bytes = self.cache.get("shared_strategy_info")
        if data_bytes is not None:
            shared_data = pickle.loads(data_bytes)
            self.log.info(f"Shared data retrieved: {shared_data}")