Building Strategies
Strategies in portwine are the heart of your backtesting system. They define how your portfolio allocates capital based on market conditions.
Strategies are the core components of portwine that define how your portfolio allocates capital based on market conditions.
Online Architecture
Portwine uses an online architecture where strategies process data one time period at a time. This means:
- Single Time Period Data: Each call to
step()receives data for only the current trading day - No Historical Context: Strategies do not have access to previous time periods' data unless they explicitly store it
- Self-Contained Indicators: All technical indicators, moving averages, momentum calculations, and other analysis must be implemented within the strategy itself
- State Management: Strategies must maintain their own state (price history, indicators, etc.) across time periods
This architecture is designed for:
- Real-time trading: Strategies can be deployed in live trading environments
- Memory efficiency: Only current data is processed at each step
- Scalability: Strategies can handle large datasets without loading everything into memory
Why This Matters
# ❌ WRONG - This won't work in portwine
def step(self, current_date: pd.Timestamp, daily_data: Dict[str, Dict]) -> Dict[str, float]:
# This assumes you have access to historical data
# But portwine only gives you current day data!
historical_prices = get_historical_prices(ticker, start_date, end_date) # Not available
sma_20 = calculate_sma(historical_prices, 20) # Can't calculate without history
return allocations
# ✅ CORRECT - Store and update indicators within the strategy
class ProperStrategy(StrategyBase):
def __init__(self, tickers: List[str]):
super().__init__(tickers)
# Store price history within the strategy
self.price_history = {ticker: [] for ticker in tickers}
self.sma_20 = {ticker: None for ticker in tickers}
def step(self, current_date: pd.Timestamp, daily_data: Dict[str, Dict]) -> Dict[str, float]:
# Update price history with current data
for ticker in self.tickers:
if daily_data.get(ticker):
self.price_history[ticker].append(daily_data[ticker]['close'])
# Calculate indicators using stored history
if len(self.price_history[ticker]) >= 20:
recent_prices = self.price_history[ticker][-20:]
self.sma_20[ticker] = sum(recent_prices) / len(recent_prices)
# Use the calculated indicators for allocation decisions
allocations = self.calculate_allocations()
return allocations
Strategy Basics
All strategies in portwine inherit from StrategyBase and implement a step method. This method receives the current date and market data, then returns allocation weights.
from portwine import StrategyBase
class MyStrategy(StrategyBase):
def __init__(self, tickers):
super().__init__(tickers)
# Initialize your strategy state here
def step(self, current_date, daily_data):
"""
Process daily data and return allocations
Parameters
----------
current_date : datetime
Current trading date
daily_data : dict
Dictionary with ticker -> OHLCV data
Returns
-------
dict
Ticker -> allocation weight (0.0 to 1.0)
"""
# Your strategy logic here
allocations = {}
for ticker in self.tickers:
allocations[ticker] = 0.0
return allocations
The Step Method
The step method is called for each trading day and receives:
current_date: The current trading date as a datetime objectdaily_data: A dictionary where keys are tickers and values are OHLCV data dictionaries
Daily Data Format
daily_data = {
'AAPL': {
'open': 150.0,
'high': 152.0,
'low': 149.0,
'close': 151.0,
'volume': 1000000
},
'GOOGL': {
'open': 2800.0,
'high': 2820.0,
'low': 2790.0,
'close': 2810.0,
'volume': 500000
},
'FRED:SP500': {
'open': 4500.0,
'high': 4510.0,
'low': 4495.0,
'close': 4505.0,
'volume': None # Some alternative data may not have volume
},
'SENTIMENT:AAPL': {
'sentiment_score': 0.75,
'confidence': 0.92,
'source_count': 150
},
'EARNINGS:GOOGL': {
'eps': 2.45,
'revenue': 75000000000,
'guidance': 'positive'
}
# ... more tickers including alternative data sources
}
Important Notes:
- The
daily_datadictionary is guaranteed to contain keys for all tickers in your strategy, even if no market data exists for that day (values will beNone) - Alternative data sources appear as separate tickers with the format
"SOURCE:TICKER"(e.g.,"FRED:SP500","SENTIMENT:AAPL") - Alternative data sources can define their own schemas - they are not limited to OHLCV format and can include any fields relevant to their data type
- Each alternative data loader should document its schema so you know what fields are available
- Some alternative data sources may not have all OHLCV fields (e.g., volume might be
None) - Always check for
Nonevalues and handle missing data gracefully in your strategy logic
Example: Simple Equal Weight Strategy
class EqualWeightStrategy(StrategyBase):
def __init__(self, tickers):
super().__init__(tickers)
def step(self, current_date, daily_data):
# Equal weight allocation
weight = 1.0 / len(self.tickers)
return {ticker: weight for ticker in self.tickers}
Example: Moving Average Crossover
class MACrossoverStrategy(StrategyBase):
def __init__(self, tickers, short_window=10, long_window=50):
super().__init__(tickers)
self.short_window = short_window
self.long_window = long_window
self.price_history = {ticker: [] for ticker in tickers}
def step(self, current_date, daily_data):
# Update price history
for ticker in self.tickers:
if daily_data.get(ticker):
self.price_history[ticker].append(daily_data[ticker]['close'])
allocations = {}
for ticker in self.tickers:
prices = self.price_history[ticker]
if len(prices) >= self.long_window:
short_ma = sum(prices[-self.short_window:]) / self.short_window
long_ma = sum(prices[-self.long_window:]) / self.long_window
# Buy signal when short MA > long MA
if short_ma > long_ma:
allocations[ticker] = 1.0 / len(self.tickers)
else:
allocations[ticker] = 0.0
else:
allocations[ticker] = 0.0
return allocations
Strategy State Management
Strategies can maintain state between calls to step:
class StatefulStrategy(StrategyBase):
def __init__(self, tickers):
super().__init__(tickers)
self.position_history = []
self.last_rebalance_date = None
def step(self, current_date, daily_data):
# Use state to make decisions
if self.should_rebalance(current_date):
self.last_rebalance_date = current_date
# ... rebalancing logic
# ... rest of strategy logic
Best Practices
1. Handle Missing Data
def step(self, current_date, daily_data):
allocations = {}
for ticker in self.tickers:
if ticker in daily_data and daily_data[ticker] is not None:
# Process valid data
allocations[ticker] = self.calculate_weight(ticker, daily_data[ticker])
else:
# Handle missing data
allocations[ticker] = 0.0
return allocations
2. Validate Allocations
def step(self, current_date, daily_data):
allocations = self.calculate_allocations(daily_data)
# Ensure weights sum to 1.0 (or 0.0 for cash)
total_weight = sum(allocations.values())
if total_weight > 0:
# Normalize weights
for ticker in allocations:
allocations[ticker] /= total_weight
return allocations
3. Use Efficient Data Structures
def __init__(self, tickers):
super().__init__(tickers)
# Pre-allocate data structures
self.price_history = {ticker: [] for ticker in tickers}
self.signals = {ticker: 0.0 for ticker in tickers}
Advanced Features
Alternative Data Support
Strategies can access alternative data through the backtester:
def step(self, current_date, daily_data):
# Access alternative data with custom schemas
if 'FRED:SP500' in daily_data:
sp500_data = daily_data['FRED:SP500']
# Use alternative data in your strategy
if sp500_data and sp500_data['close'] > 4500:
# SP500 is above threshold, adjust allocations
pass
# Access sentiment data with custom schema
if 'SENTIMENT:AAPL' in daily_data:
sentiment_data = daily_data['SENTIMENT:AAPL']
if sentiment_data and sentiment_data['sentiment_score'] > 0.7:
# High sentiment, consider overweighting
pass
# Access earnings data with custom schema
if 'EARNINGS:GOOGL' in daily_data:
earnings_data = daily_data['EARNINGS:GOOGL']
if earnings_data and earnings_data['guidance'] == 'positive':
# Positive guidance, bullish signal
pass
Calendar Awareness
Strategies can be aware of trading calendars:
def step(self, current_date, daily_data):
# Check if it's a rebalancing day
if current_date.weekday() == 4: # Friday
# Weekly rebalancing logic
pass
Next Steps
- Learn about backtesting your strategies
- Explore data management
- Check out performance analysis