Skip to content

State Dictionary

The state dictionary is your strategy's memory, persisting data across function calls. It contains system information, configuration parameters, and your custom variables.

System Properties

These are automatically provided by the backtesting engine:

  • minute_index (int) - Current minute in backtest (0-based)
  • current_timestamp (datetime) - Current bar timestamp (Eastern Time)
  • session_info (dict) - Market session metadata
  • progress (float) - Backtest completion percentage (0.0-1.0)
  • total_minutes (int) - Total minutes in the backtest

Configuration Parameters (New in Hybrid Architecture)

These configuration parameters are automatically injected from the API/UI parameters:

  • symbols (list) - List of symbols being backtested
  • start_date (str) - Backtest start date
  • end_date (str) - Backtest end date
  • initial_capital (float) - Starting capital amount
  • backtest_config (dict) - Complete configuration metadata
python
def on_strategy_start(portfolio, state):
    # Access configuration parameters
    symbols = state.get('symbols', ['AAPL'])
    start_date = state.get('start_date', 'Not specified')
    end_date = state.get('end_date', 'Not specified')
    initial_capital = state.get('initial_capital', 100000)
    
    print(f"Trading {len(symbols)} symbols: {symbols}")
    print(f"Period: {start_date} to {end_date}")
    print(f"Capital: ${initial_capital:,.2f}")

def on_bar_close(data_contexts, portfolio, state):
    # Access system info
    current_minute = state['minute_index']
    timestamp = state['current_timestamp']
    progress = state['progress']
    
    print(f"Minute {current_minute}/{state['total_minutes']} ({progress*100:.1f}%)")

Custom Storage

Use state to store any data you need to persist between function calls:

Basic Usage

python
def on_strategy_start(portfolio, state):
    """Initialize custom state variables"""
    state['trade_count'] = 0
    state['max_portfolio_value'] = portfolio.initial_capital
    state['signals_history'] = []

def on_bar_close(data_contexts, portfolio, state):
    """Use and update custom state"""
    aapl = data_contexts['AAPL']
    
    # Update tracking variables
    state['max_portfolio_value'] = max(state['max_portfolio_value'], portfolio.equity)
    
    # Store signal history
    rsi = aapl.rsi(14)
    if rsi == rsi:  # Not NaN
        state['signals_history'].append(rsi)
        
        # Keep only last 100 values
        if len(state['signals_history']) > 100:
            state['signals_history'] = state['signals_history'][-100:]

Common Use Cases

Trade Counting and Tracking

python
def on_bar_close(data_contexts, portfolio, state):
    """Track trading activity"""
    # Initialize counters if first run
    if 'trades_today' not in state:
        state['trades_today'] = 0
        state['total_trades'] = 0
        state['winning_trades'] = 0
        state['losing_trades'] = 0
    
    aapl = data_contexts['AAPL']
    position = portfolio.position('AAPL')
    
    # Example trading logic
    if should_buy(aapl) and position == 0:
        state['trades_today'] += 1
        state['total_trades'] += 1
        return {"symbol": "AAPL", "action": "buy", "quantity": 100}

Signal History and Crossovers

python
def on_bar_close(data_contexts, portfolio, state):
    """Detect indicator crossovers"""
    aapl = data_contexts['AAPL']
    
    # Calculate current indicators
    sma_fast = aapl.sma(10)
    sma_slow = aapl.sma(20)
    
    # Get previous values from state
    prev_fast = state.get('prev_sma_fast', sma_fast)
    prev_slow = state.get('prev_sma_slow', sma_slow)
    
    # Detect golden cross (fast above slow for first time)
    if sma_fast > sma_slow and prev_fast <= prev_slow:
        print("Golden cross detected!")
        state['last_golden_cross'] = state['current_timestamp']
        
        if portfolio.position('AAPL') == 0:
            return {"symbol": "AAPL", "action": "buy", "quantity": 100}
    
    # Store current values for next iteration
    state['prev_sma_fast'] = sma_fast
    state['prev_sma_slow'] = sma_slow

Risk Management State

python
def on_strategy_start(portfolio, state):
    """Initialize risk management"""
    state['daily_loss_limit'] = portfolio.initial_capital * 0.02  # 2% daily loss limit
    state['position_size_multiplier'] = 1.0
    state['consecutive_losses'] = 0
    state['max_consecutive_losses'] = 0

def on_bar_close(data_contexts, portfolio, state):
    """Apply risk management rules"""
    # Check daily P&L
    daily_start_equity = state.get('daily_start_equity', portfolio.initial_capital)
    daily_pnl = portfolio.equity - daily_start_equity
    
    # Stop trading if daily loss limit hit
    if daily_pnl < -state['daily_loss_limit']:
        state['trading_halted'] = True
        return None
    
    # Skip trading if halted
    if state.get('trading_halted', False):
        return None
    
    # Adjust position size based on recent performance
    base_quantity = int(100 * state['position_size_multiplier'])
    
    # Your trading logic with adjusted size...

Performance Tracking

python
def on_market_open(portfolio, state, market_data):
    """Reset daily tracking"""
    state['daily_start_equity'] = portfolio.equity
    state['daily_trades'] = 0

def on_market_close(portfolio, state, market_data):
    """Calculate daily performance"""
    daily_pnl = portfolio.equity - state['daily_start_equity']
    daily_return = daily_pnl / state['daily_start_equity']
    
    # Track daily returns
    if 'daily_returns' not in state:
        state['daily_returns'] = []
    
    state['daily_returns'].append(daily_return)
    
    # Calculate rolling metrics
    if len(state['daily_returns']) >= 30:
        recent_returns = state['daily_returns'][-30:]
        avg_return = sum(recent_returns) / len(recent_returns)
        volatility = (sum((r - avg_return)**2 for r in recent_returns) / len(recent_returns)) ** 0.5
        
        state['30day_avg_return'] = avg_return
        state['30day_volatility'] = volatility
        
        print(f"30-day avg return: {avg_return*100:.2f}%")
        print(f"30-day volatility: {volatility*100:.2f}%")

Multi-Symbol State Management

python
def on_bar_close(data_contexts, portfolio, state):
    """Manage state for multiple symbols"""
    # Initialize symbol-specific state
    for symbol in data_contexts:
        if f'{symbol}_signals' not in state:
            state[f'{symbol}_signals'] = []
            state[f'{symbol}_last_trade'] = None
    
    orders = []
    
    for symbol, data in data_contexts.items():
        # Get symbol-specific state
        signals_key = f'{symbol}_signals'
        last_trade_key = f'{symbol}_last_trade'
        
        # Calculate signal
        rsi = data.rsi(14)
        if rsi == rsi:  # Not NaN
            state[signals_key].append(rsi)
            
            # Keep only recent signals
            if len(state[signals_key]) > 50:
                state[signals_key] = state[signals_key][-50:]
            
            # Trading logic
            if rsi < 30 and portfolio.position(symbol) == 0:
                orders.append({
                    "symbol": symbol,
                    "action": "buy",
                    "quantity": 50
                })
                state[last_trade_key] = state['current_timestamp']
    
    return orders if orders else None

Advanced Patterns

State-Based Strategy Modes

python
def on_strategy_start(portfolio, state):
    """Initialize strategy mode"""
    state['strategy_mode'] = 'accumulation'  # accumulation, trending, defensive
    state['mode_switch_date'] = None

def on_bar_close(data_contexts, portfolio, state):
    """Adapt strategy based on market conditions"""
    aapl = data_contexts['AAPL']
    
    # Switch modes based on volatility
    atr = aapl.atr(20)
    volatility = atr / aapl.close
    
    current_mode = state['strategy_mode']
    new_mode = current_mode
    
    if volatility > 0.03:  # High volatility
        new_mode = 'defensive'
    elif volatility < 0.01:  # Low volatility
        new_mode = 'accumulation'
    else:
        new_mode = 'trending'
    
    # Mode switch logic
    if new_mode != current_mode:
        print(f"Switching from {current_mode} to {new_mode} mode")
        state['strategy_mode'] = new_mode
        state['mode_switch_date'] = state['current_timestamp']
    
    # Execute different strategies based on mode
    if state['strategy_mode'] == 'accumulation':
        return accumulation_strategy(aapl, portfolio, state)
    elif state['strategy_mode'] == 'trending':
        return trending_strategy(aapl, portfolio, state)
    else:  # defensive
        return defensive_strategy(aapl, portfolio, state)

Rolling Window Calculations

python
def on_bar_close(data_contexts, portfolio, state):
    """Maintain rolling calculations"""
    aapl = data_contexts['AAPL']
    
    # Initialize rolling window
    if 'price_window' not in state:
        state['price_window'] = []
        state['volume_window'] = []
    
    # Add current data
    state['price_window'].append(aapl.close)
    state['volume_window'].append(aapl.volume)
    
    # Maintain window size (last 100 bars)
    window_size = 100
    if len(state['price_window']) > window_size:
        state['price_window'] = state['price_window'][-window_size:]
        state['volume_window'] = state['volume_window'][-window_size:]
    
    # Calculate custom metrics
    if len(state['price_window']) >= 20:
        # Custom volatility calculation
        prices = state['price_window'][-20:]
        returns = [(prices[i] - prices[i-1])/prices[i-1] for i in range(1, len(prices))]
        custom_volatility = (sum(r**2 for r in returns) / len(returns)) ** 0.5
        
        state['custom_volatility'] = custom_volatility
        
        # Use in trading decision
        if custom_volatility < 0.01:  # Low volatility
            return {"symbol": "AAPL", "action": "buy", "quantity": 100}

Best Practices

Memory Management

python
def on_bar_close(data_contexts, portfolio, state):
    """Manage state memory efficiently"""
    # Limit list sizes
    max_history = 1000
    for key in state:
        if isinstance(state[key], list) and len(state[key]) > max_history:
            state[key] = state[key][-max_history:]  # Keep recent data only
    
    # Clean up old entries
    current_time = state['current_timestamp']
    cutoff_time = current_time - timedelta(days=30)
    
    # Remove entries older than 30 days
    keys_to_clean = [k for k in state.keys() if k.endswith('_history')]
    for key in keys_to_clean:
        if isinstance(state[key], dict):
            state[key] = {k: v for k, v in state[key].items() 
                         if k >= cutoff_time}

Safe State Access

python
def on_bar_close(data_contexts, portfolio, state):
    """Safe state access patterns"""
    # Use get() with defaults
    trade_count = state.get('trade_count', 0)
    last_signal = state.get('last_signal', None)
    
    # Initialize if missing
    if 'initialized' not in state:
        state['initialized'] = True
        state['start_time'] = state['current_timestamp']
        state['counters'] = {'buys': 0, 'sells': 0}
    
    # Increment counters safely
    state['counters']['buys'] = state['counters'].get('buys', 0) + 1

Strategy Global Variables

The backtesting engine fully supports strategy-level global variables. You can define configuration variables, constants, and parameters directly in your strategy code, making your strategies self-contained and easy to understand.

Global Variable Support

python
# Strategy-specific globals - these work perfectly!
g_fast_period = 12
g_slow_period = 26
g_position_size = 500
g_risk_per_trade = 0.02

# Configuration constants
DEFAULT_SYMBOLS = ["AAPL", "GOOGL", "MSFT"]
MAX_POSITIONS = 3
TRADING_ENABLED = True

def on_strategy_start(portfolio, state):
    """Global variables are accessible in all lifecycle functions"""
    
    print(f"Strategy configured with SMA periods: {g_fast_period}/{g_slow_period}")
    print(f"Position size: {g_position_size}, Risk per trade: {g_risk_per_trade}")
    
    # Store in state if needed for later access
    state['fast_period'] = g_fast_period
    state['slow_period'] = g_slow_period
    state['max_positions'] = MAX_POSITIONS

def on_bar_close(data_contexts, portfolio, state):
    """Global variables work in all functions"""
    if not TRADING_ENABLED:
        return None
        
    # Direct access to globals
    for symbol in DEFAULT_SYMBOLS:
        if symbol in data_contexts:
            fast_sma = data_contexts[symbol].sma(g_fast_period)
            slow_sma = data_contexts[symbol].sma(g_slow_period)
            # ... trading logic using global variables

Naming Conventions

While any variable names work, these conventions are recommended:

python
# Recommended: Use descriptive prefixes
g_fast_period = 20          # 'g_' prefix for global parameters
DEFAULT_SYMBOLS = ["AAPL"]  # 'DEFAULT_' for fallback values
MAX_DRAWDOWN = 0.15         # ALL_CAPS for constants
trading_enabled = True      # lowercase for simple flags

# All of these patterns work and are supported!

Hybrid Architecture Pattern

For maximum flexibility, you can combine strategy globals with API parameter overrides:

python
# Strategy defaults - always available
g_fast_period = 20
g_slow_period = 50
g_symbols = ["AAPL"]

def on_strategy_start(portfolio, state):
    """Hybrid approach: globals + API overrides"""
    
    # API parameters can override strategy defaults
    fast_period = state.get('fast_period', g_fast_period)        # API override or global
    slow_period = state.get('slow_period', g_slow_period)        # API override or global  
    symbols = state.get('symbols', g_symbols)                    # API override or global
    
    print(f"Using periods: {fast_period}/{slow_period} for {symbols}")
    
    # Store final configuration
    state['final_fast_period'] = fast_period
    state['final_slow_period'] = slow_period
    state['final_symbols'] = symbols

def on_bar_close(data_contexts, portfolio, state):
    """Use either globals directly OR state values"""
    
    # Option 1: Use globals directly (simpler)
    fast_sma = data_contexts['AAPL'].sma(g_fast_period)
    
    # Option 2: Use state values (more flexible for API overrides)  
    final_fast_period = state['final_fast_period']
    fast_sma = data_contexts['AAPL'].sma(final_fast_period)
    
    # Both approaches work - choose based on your needs!

Benefits of Strategy Globals:

  • Natural Python: Define variables the standard Python way
  • Self-Contained: Strategy code includes all configuration
  • Easy Testing: Change parameters by editing the strategy file
  • Clear Intent: Configuration is visible at the top of your strategy
  • API Compatible: Still works with external parameter injection

Important Notes

  • Persistence: State persists across all function calls throughout the backtest
  • Types: Can store any Python object (numbers, strings, lists, dictionaries)
  • Memory: Be mindful of memory usage with large datasets
  • Thread Safety: Not needed - backtest runs in single thread
  • Initialization: Initialize variables in on_strategy_start when possible
  • System Keys: Don't modify system-provided keys (minute_index, symbols, etc.)
  • Configuration: Use state.get() with defaults for robust parameter access

The state dictionary is your strategy's long-term memory - use it wisely to build sophisticated, stateful trading logic with flexible configuration.

Test your trading strategies risk-free with professional backtesting.