Strategy Custom Syntax
This comprehensive guide covers every aspect of writing trading strategies for the backtesting engine.
Table of Contents
- Strategy Structure - Function Signatures
- Backtest Config
- Data Access
- Technical Indicators
- Portfolio Management
- Order Management
- Memory and State
- Advanced Patterns
- Performance Considerations
- Limitations and Gotchas
Strategy Structure - Function Signatures
Strategy functions define the lifecycle of your trading logic, controlling when and how your strategy executes. These functions are automatically discovered and called by the backtesting engine at specific moments during simulation, creating a structured framework for strategy development.
The engine validates that required functions are present and properly defined before execution begins. Each function receives different parameters depending on its purpose, allowing access to market data, portfolio state, and strategy memory.
Required Function
on_bar_close is the core trading function called every minute during market hours. This is where your main trading logic resides:
def on_bar_close(market, portfolio, memory, session):
"""Main trading logic - executes every minute"""
# Access market data
spy_data = market['SPY']
current_price = spy_data.close
# Check portfolio state
current_position = portfolio.position('SPY')
# Make trading decisions
orders = []
if current_position == 0 and current_price > spy_data.sma(20):
orders.append({
'symbol': 'SPY',
'action': 'buy',
'quantity': 100
})
return ordersThis function must return either a list of order dictionaries or None if no trades are desired.
Optional Functions
Initialization and Cleanup
on_strategy_start runs once at the beginning, before any market data processing:
def on_strategy_start(portfolio, memory, session):
"""Initialize strategy parameters and state"""
memory['rsi_period'] = 14
memory['position_size'] = 0.25
print(f"Strategy started with ${portfolio.initial_capital:,}")on_strategy_end runs once at completion, after all market data is processed:
def on_strategy_end(portfolio, memory, results):
"""Final cleanup and reporting"""
print(f"Final return: {results['total_return_pct']:.2f}%")
print(f"Total trades: {results['total_trades']}")Daily Market Events
on_market_open executes at 9:30 AM ET each trading day:
def on_market_open(market, portfolio, memory, session):
"""Daily market open logic"""
memory['daily_trades'] = 0
print(f"Market open: {session.current_date()}")on_market_close executes at 4:00 PM ET each trading day:
def on_market_close(market, portfolio, memory, session):
"""Daily market close logic"""
daily_pnl = portfolio.daily_pnl()
memory['daily_results'].append(daily_pnl)Function Parameters
market (or data_contexts) provides access to market data for all configured symbols. Use market['SYMBOL'] to access price data and technical indicators.
portfolio manages your positions, cash, and trading operations. Query current positions, account equity, and execute trades through this interface.
memory is a persistent dictionary that maintains state between function calls. Store strategy parameters, intermediate calculations, and custom variables here.
session contains timing and simulation metadata including current timestamp, progress indicators, and market schedule information. This parameter is optional and automatically provided when functions expect it.
results (on_strategy_end only) contains final backtest metrics including returns, trades, and performance statistics.
Backtest Config
BACKTEST_CONFIG is a required dictionary that defines the parameters for your backtesting simulation. Every strategy must include this configuration at the top level to specify when to run, starting capital, and which assets to trade.
BACKTEST_CONFIG = {
'start_date': '20230610', # YYYYMMDD format
'end_date': '20230615', # YYYYMMDD format
'initial_capital': 100000, # Starting capital in USD
'symbols': ['SPY', 'AAPL'] # List of symbols to trade
}Optional Parameters
start_date and end_date define the simulation timeframe. Use YYYY-MM-DD format ('2023-06-10'). The start date must precede the end date, and both must be valid calendar dates.
initial_capital sets your starting portfolio value in USD. Minimum value is $1,000.
symbols is a list of asset symbols your strategy can trade. Each symbol must have available market data for your specified date range.
commission_per_share (coming soon!)
slippage_bps (coming soon!)
Date Format Flexibility
While both date formats work, YYYYMMDD is recommended for consistency:
# Both formats are valid
'start_date': '20230610' # Recommended
'start_date': '2023-06-10' # Also acceptedThe engine validates your configuration before execution, checking for required fields, valid dates, sufficient capital, and data availability for all symbols.
Data Access
DataContext Object
Access market data through market['SYMBOL'].
Price Properties
aapl = market['AAPL']
# Current bar OHLCV data
open_price = aapl.open # float: Opening price
high = aapl.high # float: High price
low = aapl.low # float: Low price
close = aapl.close # float: Closing price
volume = aapl.volume # int: Volume
timestamp = aapl.timestamp # pd.Timestamp: Bar timestampData Availability
- Minute-level data: All properties represent 1-minute bars
- Market hours only: Data available during regular trading hours
- Point-in-time: Only data up to current bar (no look-ahead bias)
Symbol Validation
def on_bar_close(market, portfolio, memory, session):
# Always check if symbol exists
if 'AAPL' not in market:
return None
aapl = market['AAPL']
# Now safe to use aaplTechnical Indicators
All indicators return float values or NaN when insufficient data.
Simple Moving Average (SMA)
sma_value = symbol.sma(period)Parameters:
period(int): Number of bars for average
Returns:
float: SMA valueNaN: Ifcurrent_index + 1 < period
Example:
spy = market['SPY']
sma_10 = spy.sma(10) # 10-period SMA
sma_50 = spy.sma(50) # 50-period SMA
# Check for valid data
if not pd.isna(sma_10) and not pd.isna(sma_50):
if sma_10 > sma_50:
# Golden cross logic
passExponential Moving Average (EMA)
ema_value = symbol.ema(period)Parameters:
period(int): Number of bars for EMA
Returns:
float: EMA value using pandas ewm withspan=period, adjust=FalseNaN: Ifcurrent_index + 1 < period
Example:
ema_12 = spy.ema(12)
ema_26 = spy.ema(26)
# MACD-like logic
if not pd.isna(ema_12) and not pd.isna(ema_26):
macd_line = ema_12 - ema_26Relative Strength Index (RSI)
rsi_value = symbol.rsi(period=14)Parameters:
period(int, optional): Lookback period, default 14
Returns:
float: RSI value between 0 and 100NaN: Ifcurrent_index + 1 < period + 1
Example:
rsi = spy.rsi() # Default 14-period
rsi_21 = spy.rsi(21) # Custom 21-period
# Overbought/oversold levels
if not pd.isna(rsi):
if rsi < 30:
# Oversold condition
pass
elif rsi > 70:
# Overbought condition
passHistorical Data Access
values = symbol.history(field, periods)Parameters:
field(string): Column name -'open','high','low','close','volume'periods(int): Number of historical bars to retrieve
Returns:
list[float]: Historical values, most recent last- Returns available data if
periods> available bars
Example:
# Get last 5 closing prices
close_prices = spy.history('close', 5)
# close_prices = [100.0, 101.0, 99.5, 102.0, 103.0]
# oldest ----------> newest
# Check for trend
if len(close_prices) >= 5:
if close_prices[-1] > close_prices[0]: # Recent > oldest
# Uptrend over 5 bars
pass
# Volume analysis
volumes = spy.history('volume', 10)
avg_volume = sum(volumes) / len(volumes)Portfolio Management
Position Queries
# Get current position size
shares = portfolio.position(symbol)Parameters:
symbol(string): Symbol name (case-sensitive)
Returns:
int: Number of shares owned (positive) or sold short (negative)0: No position
Example:
spy_shares = portfolio.position('SPY')
aapl_shares = portfolio.position('AAPL')
if spy_shares > 0:
print(f"Long {spy_shares} shares of SPY")
elif spy_shares < 0:
print(f"Short {abs(spy_shares)} shares of SPY")
else:
print("No SPY position")Portfolio Value Queries
# Total portfolio value
total_value = portfolio.equity()
# Available cash for trading
cash = portfolio.buying_power()
# Initial starting capital (constant)
initial = portfolio.initial_capitalMethods:
| Method | Returns | Description |
|---|---|---|
equity() |
float |
Total portfolio value (cash + positions) |
buying_power() |
float |
Available cash for purchases |
initial_capital |
float |
Starting capital (property, not method) |
Example:
def on_bar_close(market, portfolio, memory, session):
total_value = portfolio.equity()
cash = portfolio.buying_power()
# Calculate position sizing as % of portfolio
position_value = total_value * 0.1 # 10% of portfolio
spy_price = market['SPY'].close
shares_to_buy = int(position_value / spy_price)
if cash >= shares_to_buy * spy_price:
return {'symbol': 'SPY', 'action': 'buy', 'quantity': shares_to_buy}Order Management
Order Format
Return order dictionaries from on_bar_close() to execute trades.
Single Order
order = {
'symbol': 'AAPL', # Required: Symbol name
'action': 'buy', # Required: 'buy' or 'sell'
'quantity': 100 # Required: Number of shares or 'all'
}
return orderMultiple Orders
orders = [
{'symbol': 'SPY', 'action': 'buy', 'quantity': 100},
{'symbol': 'QQQ', 'action': 'sell', 'quantity': 50},
{'symbol': 'AAPL', 'action': 'sell', 'quantity': 'all'}
]
return ordersOrder Fields
| Field | Type | Required | Values | Description |
|---|---|---|---|---|
symbol |
string | Yes | Valid symbol | Must match symbol in market |
action |
string | Yes | 'buy', 'sell' |
Order direction |
quantity |
int/string | Yes | Positive integer or 'all' |
Shares to trade |
Special Quantity Values
Sell All Shares
# Sell entire position
return {'symbol': 'SPY', 'action': 'sell', 'quantity': 'all'}Dynamic Quantity Calculation
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
current_position = portfolio.position('SPY')
# Sell half of current position
if current_position > 0:
half_position = current_position // 2
if half_position > 0:
return {'symbol': 'SPY', 'action': 'sell', 'quantity': half_position}Order Validation
Orders are validated before execution:
- Symbol exists: Must be in market
- Valid action: Must be 'buy' or 'sell'
- Valid quantity: Must be positive integer or 'all'
- Sufficient cash: For buy orders
- Sufficient shares: For sell orders
Invalid orders are ignored (no error thrown).
Memory and State
Use the memory (or state) dictionary to persist data between on_bar_close() calls.
Basic Usage
def on_strategy_start(market, portfolio, memory, session):
memory['trade_count'] = 0
memory['last_signal'] = None
memory['price_history'] = []
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
# Access previous values
memory['price_history'].append(spy.close)
# Keep only last 100 prices
if len(memory['price_history']) > 100:
memory['price_history'] = memory['price_history'][-100:]
# Update counters
if some_trade_condition:
memory['trade_count'] += 1
return some_orderAdvanced State Management
def on_strategy_start(market, portfolio, memory, session):
memory['signals'] = {
'rsi': [],
'sma_cross': [],
'volume_spike': []
}
memory['positions'] = {}
memory['last_rebalance'] = None
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
current_time = spy.timestamp
# Store multiple signal types
memory['signals']['rsi'].append({
'timestamp': current_time,
'value': spy.rsi(14),
'signal': 'buy' if spy.rsi(14) < 30 else 'sell' if spy.rsi(14) > 70 else 'hold'
})
# Track position changes
current_position = portfolio.position('SPY')
if current_position != memory['positions'].get('SPY', 0):
memory['positions']['SPY'] = current_position
print(f"SPY position changed to {current_position}")Memory Best Practices
- Initialize in
on_strategy_start(): Set up all memory structures - Use descriptive keys:
memory['sma_crossover_signals']notmemory['signals'] - Limit memory growth: Clean up old data to prevent memory issues
- Store computation results: Cache expensive calculations
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
# Cache expensive computation
cache_key = f"custom_indicator_{spy.timestamp}"
if cache_key not in memory:
# Expensive calculation
memory[cache_key] = compute_complex_indicator(spy)
indicator_value = memory[cache_key]Advanced Patterns
Multi-Symbol Strategies
def on_bar_close(market, portfolio, memory, session):
orders = []
# Analyze each symbol
for symbol in ['SPY', 'QQQ', 'IWM']:
if symbol not in market:
continue
ctx = market[symbol]
rsi = ctx.rsi(14)
position = portfolio.position(symbol)
# Individual symbol logic
if rsi < 30 and position == 0:
orders.append({'symbol': symbol, 'action': 'buy', 'quantity': 100})
elif rsi > 70 and position > 0:
orders.append({'symbol': symbol, 'action': 'sell', 'quantity': 'all'})
return orders if orders else NoneSector Rotation Strategy
def on_bar_close(market, portfolio, memory, session):
sectors = {
'tech': ['QQQ', 'XLK'],
'finance': ['XLF'],
'energy': ['XLE'],
'healthcare': ['XLV']
}
# Calculate momentum for each sector
sector_momentum = {}
for sector, symbols in sectors.items():
total_momentum = 0
count = 0
for symbol in symbols:
if symbol in market:
ctx = market[symbol]
sma_20 = ctx.sma(20)
sma_50 = ctx.sma(50)
if not pd.isna(sma_20) and not pd.isna(sma_50):
momentum = (sma_20 - sma_50) / sma_50 # Relative momentum
total_momentum += momentum
count += 1
if count > 0:
sector_momentum[sector] = total_momentum / count
# Find best performing sector
if sector_momentum:
best_sector = max(sector_momentum, key=sector_momentum.get)
memory['current_sector'] = best_sector
# Trade logic based on sector rotation
# ... implementation detailsRisk Management Patterns
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
position = portfolio.position('SPY')
# Stop loss management
if position > 0:
current_price = spy.close
# Initialize stop price
if 'stop_price' not in memory:
memory['stop_price'] = current_price * 0.95 # 5% stop loss
# Trailing stop
if current_price > memory['entry_price'] * 1.02: # 2% profit
new_stop = current_price * 0.98 # Trail by 2%
memory['stop_price'] = max(memory['stop_price'], new_stop)
# Execute stop loss
if current_price <= memory['stop_price']:
memory.pop('stop_price', None) # Clear stop
return {'symbol': 'SPY', 'action': 'sell', 'quantity': 'all'}
# Position sizing based on volatility
if position == 0:
recent_prices = spy.history('close', 20)
if len(recent_prices) >= 20:
volatility = pd.Series(recent_prices).pct_change().std()
base_size = 1000 # Base position size
# Reduce size if high volatility
vol_adjusted_size = int(base_size / (1 + volatility * 10))
# Entry condition with vol-adjusted sizing
if spy.rsi(14) < 30:
memory['entry_price'] = spy.close
return {'symbol': 'SPY', 'action': 'buy', 'quantity': vol_adjusted_size}Regime Detection
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
# Detect market regime using multiple timeframes
sma_50 = spy.sma(50)
sma_200 = spy.sma(200)
rsi = spy.rsi(14)
# Price regime
if not pd.isna(sma_50) and not pd.isna(sma_200):
if sma_50 > sma_200:
price_regime = 'bull'
else:
price_regime = 'bear'
else:
price_regime = 'unknown'
# Volatility regime
recent_prices = spy.history('close', 20)
if len(recent_prices) >= 20:
returns = pd.Series(recent_prices).pct_change().dropna()
vol = returns.std()
if vol < 0.01:
vol_regime = 'low_vol'
elif vol > 0.02:
vol_regime = 'high_vol'
else:
vol_regime = 'normal_vol'
else:
vol_regime = 'unknown'
# Adapt strategy based on regime
memory['regime'] = {'price': price_regime, 'vol': vol_regime}
# Regime-specific logic
if price_regime == 'bull' and vol_regime == 'low_vol':
# Aggressive long strategy
if rsi < 40 and portfolio.position('SPY') == 0:
return {'symbol': 'SPY', 'action': 'buy', 'quantity': 200}
elif price_regime == 'bear' or vol_regime == 'high_vol':
# Conservative or short strategy
if portfolio.position('SPY') > 0:
return {'symbol': 'SPY', 'action': 'sell', 'quantity': 'all'}Performance Considerations
Computational Efficiency
- Cache Indicator Calculations
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
# Don't recalculate on every bar
if 'cached_sma' not in memory:
memory['cached_sma'] = {}
current_time = spy.timestamp
cache_key = f"sma_20_{current_time}"
if cache_key not in memory['cached_sma']:
memory['cached_sma'][cache_key] = spy.sma(20)
sma_20 = memory['cached_sma'][cache_key]- Limit Memory Growth
def on_bar_close(market, portfolio, memory, session):
# Keep only last N bars of data
max_history = 100
if 'price_history' not in memory:
memory['price_history'] = []
spy = market['SPY']
memory['price_history'].append(spy.close)
# Trim old data
if len(memory['price_history']) > max_history:
memory['price_history'] = memory['price_history'][-max_history:]- Efficient Multi-Symbol Processing
def on_bar_close(market, portfolio, memory, session):
orders = []
# Pre-calculate common indicators once
for symbol, ctx in market.items():
# Only calculate if we don't have position or considering entry
position = portfolio.position(symbol)
if position != 0 or symbol in memory.get('watchlist', []):
rsi = ctx.rsi(14)
# Process symbol...Memory Management
- Clean Up Old Data
def on_bar_close(market, portfolio, memory, session):
current_time = market['SPY'].timestamp
# Remove cached indicators older than 1 day
if 'indicator_cache' in memory:
cutoff_time = current_time - pd.Timedelta(days=1)
memory['indicator_cache'] = {
k: v for k, v in memory['indicator_cache'].items()
if v.get('timestamp', current_time) > cutoff_time
}- Use Circular Buffers for Fixed-Size History
class CircularBuffer:
def __init__(self, size):
self.size = size
self.data = []
self.index = 0
def append(self, value):
if len(self.data) < self.size:
self.data.append(value)
else:
self.data[self.index] = value
self.index = (self.index + 1) % self.size
def get_all(self):
if len(self.data) < self.size:
return self.data[:]
else:
return self.data[self.index:] + self.data[:self.index]
def on_strategy_start(market, portfolio, memory, session):
memory['price_buffer'] = CircularBuffer(50) # Keep only 50 pricesLimitations and Gotchas
Data Limitations
- No External Libraries
# ❌ NOT ALLOWED
import pandas as pd
import numpy as np
import talib
# ✅ USE BUILT-IN FUNCTIONALITY
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
prices = spy.history('close', 20)
avg_price = sum(prices) / len(prices) # Manual average- No Look-Ahead Bias
# ❌ WRONG - Using future data
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
# This only gives you data UP TO current bar
prices = spy.history('close', 5)
# prices[4] is current bar, prices[0] is 4 bars ago- Minute-Level Only
# Data is minute-by-minute during market hours
# No daily, weekly, or hourly aggregations available
# You must implement higher timeframe logic manuallyIndicator Limitations
- NaN Handling
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
sma = spy.sma(50)
# ❌ WRONG - May crash on NaN
if sma > spy.close:
pass
# ✅ CORRECT - Check for NaN first
if not pd.isna(sma) and sma > spy.close:
pass
# ✅ ALTERNATIVE - Use math.isnan()
import math
if not math.isnan(sma) and sma > spy.close:
pass- Insufficient Data Periods
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
# SMA(200) needs 200 bars of data
# Will return NaN for first 199 bars
sma_200 = spy.sma(200)
# Check data availability
if pd.isna(sma_200):
return None # Skip trading until enough dataOrder Limitations
- Case-Sensitive Symbols
# ❌ WRONG
return {'symbol': 'aapl', 'action': 'buy', 'quantity': 100}
# ✅ CORRECT
return {'symbol': 'AAPL', 'action': 'buy', 'quantity': 100}- Invalid Quantity Values
# ❌ WRONG
return {'symbol': 'SPY', 'action': 'buy', 'quantity': 0} # Zero
return {'symbol': 'SPY', 'action': 'buy', 'quantity': -100} # Negative
return {'symbol': 'SPY', 'action': 'buy', 'quantity': 1.5} # Float
# ✅ CORRECT
return {'symbol': 'SPY', 'action': 'buy', 'quantity': 100} # Positive int
return {'symbol': 'SPY', 'action': 'sell', 'quantity': 'all'} # Special value- Symbol Availability
def on_bar_close(market, portfolio, memory, session):
# ❌ WRONG - Assumes symbol exists
aapl = market['AAPL']
# ✅ CORRECT - Check first
if 'AAPL' in market:
aapl = market['AAPL']
else:
return NonePerformance Gotchas
- Expensive Operations Every Bar
# ❌ INEFFICIENT - Recalculating every bar
def on_bar_close(market, portfolio, memory, session):
for symbol in market:
long_sma = market[symbol].sma(200) # Expensive!
# ... logic
# ✅ EFFICIENT - Cache and only recalculate when needed
def on_bar_close(market, portfolio, memory, session):
if 'sma_cache' not in memory:
memory['sma_cache'] = {}
for symbol in market:
if symbol not in memory['sma_cache']:
memory['sma_cache'][symbol] = market[symbol].sma(200)- Growing Memory Usage
# ❌ MEMORY LEAK - Unbounded growth
def on_bar_close(market, portfolio, memory, session):
if 'all_prices' not in memory:
memory['all_prices'] = []
memory['all_prices'].append(market['SPY'].close) # Grows forever!
# ✅ BOUNDED - Fixed size
def on_bar_close(market, portfolio, memory, session):
if 'recent_prices' not in memory:
memory['recent_prices'] = []
memory['recent_prices'].append(market['SPY'].close)
if len(memory['recent_prices']) > 1000: # Keep only last 1000
memory['recent_prices'] = memory['recent_prices'][-1000:]Common Bugs
- Off-by-One in History
# ❌ WRONG - Misunderstanding history order
prices = spy.history('close', 5)
# prices[0] is OLDEST, prices[4] is NEWEST (current bar)
latest_price = prices[0] # WRONG - This is 5 bars ago!
# ✅ CORRECT
latest_price = prices[-1] # Last item is most recent
oldest_price = prices[0] # First item is oldest- State Initialization
# ❌ WRONG - Not initializing in on_strategy_start
def on_bar_close(market, portfolio, memory, session):
memory['counter'] += 1 # May crash if not initialized!
# ✅ CORRECT - Initialize first
def on_strategy_start(market, portfolio, memory, session):
memory['counter'] = 0
def on_bar_close(market, portfolio, memory, session):
memory['counter'] += 1 # Safe- Division by Zero
def on_bar_close(market, portfolio, memory, session):
spy = market['SPY']
# ❌ POTENTIAL ERROR
ratio = spy.close / spy.open # open could be 0!
# ✅ SAFE
if spy.open != 0:
ratio = spy.close / spy.open
else:
ratio = 1.0 # or handle appropriatelyQuick Reference Links
- Strategy Quick Start - Essential syntax guide
- Tutorial Examples - Step-by-step learning
- API Documentation - Complete API reference
Need help? Check the tutorial examples or consult the API documentation for endpoint-specific details.