Portfolio Allocation & Multi-Strategy Management¶
Source Files:
- /rustybt/portfolio/allocator.py (769 lines)
- /rustybt/portfolio/allocation.py (801 lines)
Last Verified: 2025-10-16
Overview¶
RustyBT's portfolio allocation system enables sophisticated multi-strategy portfolio management with:
- Strategy Isolation: Independent ledgers prevent position interference
- Capital Allocation: Dynamic or fixed capital allocation across strategies
- Performance Tracking: Per-strategy metrics (Sharpe ratio, drawdown, volatility)
- Rebalancing: Scheduled or threshold-based capital reallocation
- Allocation Algorithms: Fixed, Dynamic, Risk Parity, Kelly Criterion, Drawdown-Based
This system is designed for hedge fund-style portfolio management where multiple uncorrelated strategies operate independently with isolated capital.
Table of Contents¶
- Core Concepts
- PortfolioAllocator - Multi-Strategy Manager
- StrategyAllocation - Per-Strategy Tracking
- StrategyPerformance - Performance Metrics
- Allocation Algorithms
- FixedAllocation
- DynamicAllocation
- RiskParityAllocation
- [KellyCriterionAllocation](#kellycr
iterionallocation) - DrawdownBasedAllocation 6. AllocationRebalancer - Rebalancing Scheduler 7. Production Examples 8. Best Practices
Core Concepts¶
Multi-Strategy Execution Flow¶
┌──────────────────────────────────────────────────────────────┐
│ PortfolioAllocator │
│ (Total Capital: $1M) │
└───────────────────┬──────────────────────────────────────────┘
│
┌───────────┼───────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Strategy │ │Strategy │ │Strategy │
│ A │ │ B │ │ C │
│$400k │ │$350k │ │$250k │
│(40%) │ │(35%) │ │(25%) │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Ledger A │ │Ledger B │ │Ledger C │
│(Isolated│ │(Isolated│ │(Isolated│
│Positions│ │Positions│ │Positions│
└─────────┘ └─────────┘ └─────────┘
Bar-by-Bar Execution (Synchronized)¶
# For each bar (timestamp, data):
for timestamp, market_data in data_feed:
# 1. Update all strategy ledgers with latest prices
allocator.update_ledgers(market_data)
# 2. Execute all strategies sequentially at same timestamp
allocator.execute_bar(timestamp, market_data)
# → Strategy A: handle_data(ledger_A, data)
# → Strategy B: handle_data(ledger_B, data)
# → Strategy C: handle_data(ledger_C, data)
# 3. Update performance metrics for each strategy
# → Track returns, volatility, Sharpe, drawdown
# 4. Aggregate portfolio-level metrics
# → Total value, weighted Sharpe, correlation matrix
Strategy Isolation Mechanism¶
Each strategy has complete isolation: - Separate DecimalLedger: Independent cash and positions - No Cross-Strategy Access: Cannot see other strategies' positions - Capital Transfers Only Through PortfolioAllocator: Controlled rebalancing
# Strategy A cannot access Strategy B's ledger
# Each strategy sees only its own ledger:
def handle_data(context, data, ledger): # This ledger belongs to THIS strategy only
# Can only trade with this strategy's capital
ledger.order(asset, amount) # Uses this strategy's cash
PortfolioAllocator¶
Source: rustybt/portfolio/allocator.py:287-769
The main multi-strategy portfolio manager that coordinates strategy execution, capital allocation, and performance tracking.
Class Definition¶
class PortfolioAllocator:
"""Portfolio allocator for multi-strategy management.
Manages:
- Multiple strategies with isolated ledgers
- Capital allocation and rebalancing
- Synchronized bar-by-bar execution
- Portfolio-level performance metrics
"""
Constructor¶
Source: allocator.py:338-356
def __init__(
self,
total_capital: Decimal,
name: str = "Portfolio"
) -> None:
"""Initialize portfolio allocator.
Args:
total_capital: Total capital to allocate across strategies
name: Portfolio name for logging
"""
Parameters:
- total_capital (Decimal): Total portfolio capital (e.g., Decimal("1000000") for $1M)
- name (str): Portfolio identifier for logging (default: "Portfolio")
Attributes:
- strategies (dict[str, StrategyAllocation]): Active strategies
- allocated_capital (Decimal): Total allocated capital (≤ total_capital)
- current_timestamp (pd.Timestamp | None): Current execution timestamp
- execution_count (int): Number of bars executed
Example 1: Basic Portfolio Setup¶
from decimal import Decimal
from rustybt.portfolio.allocator import PortfolioAllocator
# Create portfolio with $1M capital
portfolio = PortfolioAllocator(
total_capital=Decimal("1000000"),
name="HedgeFund_Alpha"
)
# Portfolio starts empty
print(portfolio.strategies) # {}
print(portfolio.allocated_capital) # Decimal('0')
Adding Strategies¶
add_strategy()¶
Source: allocator.py:358-429
def add_strategy(
self,
strategy_id: str,
strategy: Any, # TradingAlgorithm
allocation_pct: Decimal,
metadata: dict[str, Any] | None = None,
) -> StrategyAllocation:
"""Add strategy to portfolio with capital allocation.
Args:
strategy_id: Unique identifier for strategy
strategy: TradingAlgorithm instance
allocation_pct: Allocation percentage (0.3 = 30%)
metadata: Optional metadata for strategy
Returns:
StrategyAllocation instance
Raises:
ValueError: If allocation would exceed 100% or strategy_id exists
"""
Validation:
- Sum of allocations must be ≤ 100% (≤ Decimal("1.0"))
- Strategy IDs must be unique
- Creates independent DecimalLedger for each strategy
Example 2: Adding Multiple Strategies¶
from rustybt.algorithm import TradingAlgorithm
# Define strategies
class MomentumStrategy(TradingAlgorithm):
def handle_data(self, context, data, ledger):
# Momentum logic
pass
class MeanReversionStrategy(TradingAlgorithm):
def handle_data(self, context, data, ledger):
# Mean reversion logic
pass
class TrendFollowingStrategy(TradingAlgorithm):
def handle_data(self, context, data, ledger):
# Trend following logic
pass
# Create portfolio
portfolio = PortfolioAllocator(
total_capital=Decimal("1000000"),
name="MultiStrategy_Fund"
)
# Add strategies with different allocations
alloc_momentum = portfolio.add_strategy(
strategy_id="momentum",
strategy=MomentumStrategy(),
allocation_pct=Decimal("0.40"), # 40%
metadata={"description": "Short-term momentum", "lookback": 20}
)
alloc_mean_rev = portfolio.add_strategy(
strategy_id="mean_reversion",
strategy=MeanReversionStrategy(),
allocation_pct=Decimal("0.35"), # 35%
metadata={"description": "Mean reversion RSI", "threshold": 30}
)
alloc_trend = portfolio.add_strategy(
strategy_id="trend_following",
strategy=TrendFollowingStrategy(),
allocation_pct=Decimal("0.25"), # 25%
metadata={"description": "Long-term trend", "ma_window": 200}
)
# Check allocations
print(f"Allocated: ${portfolio.allocated_capital:,.0f}") # Allocated: $1,000,000
print(f"Remaining: ${portfolio.total_capital - portfolio.allocated_capital:,.0f}") # Remaining: $0
# Access strategy allocations
print(alloc_momentum)
# StrategyAllocation(id=momentum, capital=400000, value=400000, return=0.00%, state=running)
Example 3: Partial Allocation (Reserve Cash)¶
# Allocate only 80% of capital (reserve 20% cash)
portfolio = PortfolioAllocator(
total_capital=Decimal("1000000"),
name="Conservative_Fund"
)
portfolio.add_strategy("strategy_a", StrategyA(), Decimal("0.40")) # 40%
portfolio.add_strategy("strategy_b", StrategyB(), Decimal("0.30")) # 30%
portfolio.add_strategy("strategy_c", StrategyC(), Decimal("0.10")) # 10%
# Total allocated: 80%
# Reserved cash: 20% = $200,000
print(portfolio.allocated_capital) # Decimal('800000')
print(portfolio.total_capital - portfolio.allocated_capital) # Decimal('200000')
Example 4: Over-Allocation Error (Validation)¶
portfolio = PortfolioAllocator(total_capital=Decimal("1000000"))
portfolio.add_strategy("strat_a", StrategyA(), Decimal("0.60")) # 60%
portfolio.add_strategy("strat_b", StrategyB(), Decimal("0.30")) # 30%
# Try to add 30% more (total would be 120%)
try:
portfolio.add_strategy("strat_c", StrategyC(), Decimal("0.30")) # Would exceed 100%
except ValueError as e:
print(e)
# Allocation would exceed 100%: current=90.0%, new=30.0%, total=120.0%
Executing Strategies¶
execute_bar()¶
Source: allocator.py:514-582
def execute_bar(
self,
timestamp: pd.Timestamp,
data: dict[str, Any]
) -> None:
"""Execute all active strategies for current bar (synchronized).
All strategies process the same bar simultaneously (sequentially in code,
but conceptually at the same timestamp).
Args:
timestamp: Current bar timestamp
data: Market data for all assets
"""
Execution Flow:
1. Set current_timestamp to bar timestamp
2. Iterate through all strategies
3. Skip paused/stopped strategies
4. Call strategy.handle_data(ledger, data) for each active strategy
5. Update performance metrics for each strategy
6. Log portfolio-level summary
Example 5: Bar-by-Bar Execution Loop¶
import pandas as pd
# Setup portfolio
portfolio = PortfolioAllocator(total_capital=Decimal("1000000"))
portfolio.add_strategy("momentum", MomentumStrategy(), Decimal("0.50"))
portfolio.add_strategy("mean_rev", MeanReversionStrategy(), Decimal("0.50"))
# Simulate bar-by-bar execution
timestamps = pd.date_range("2024-01-01", "2024-12-31", freq="D")
for timestamp in timestamps:
# Fetch market data for this bar
market_data = {
"AAPL": {"price": Decimal("150.00"), "volume": 1000000},
"GOOGL": {"price": Decimal("140.00"), "volume": 800000},
# ... more assets
}
# Execute all strategies at this timestamp
portfolio.execute_bar(timestamp, market_data)
# → momentum.handle_data(ledger_momentum, market_data)
# → mean_rev.handle_data(ledger_mean_rev, market_data)
# Portfolio automatically tracks:
# - Per-strategy returns
# - Per-strategy performance metrics
# - Portfolio-level aggregates
# After backtest, get metrics
metrics = portfolio.get_portfolio_metrics()
print(metrics)
# {
# 'total_value': '1050000',
# 'portfolio_return': '5.00%',
# 'num_strategies': 2,
# 'active_strategies': 2,
# 'weighted_avg_sharpe': '1.20'
# }
Managing Strategy Lifecycle¶
pause_strategy() / resume_strategy()¶
Source: allocator.py:486-512
def pause_strategy(self, strategy_id: str) -> None:
"""Pause strategy execution (keeps positions, stops trading)."""
def resume_strategy(self, strategy_id: str) -> None:
"""Resume paused strategy."""
remove_strategy()¶
Source: allocator.py:431-484
def remove_strategy(
self,
strategy_id: str,
liquidate: bool = True
) -> Decimal:
"""Remove strategy from portfolio.
Args:
strategy_id: Strategy to remove
liquidate: If True, liquidate all positions before removing
Returns:
Capital returned to portfolio
"""
Example 6: Strategy Lifecycle Management¶
# Add 3 strategies
portfolio.add_strategy("strat_a", StrategyA(), Decimal("0.33"))
portfolio.add_strategy("strat_b", StrategyB(), Decimal("0.33"))
portfolio.add_strategy("strat_c", StrategyC(), Decimal("0.34"))
# Execute for some time
for timestamp, data in data_feed:
portfolio.execute_bar(timestamp, data)
# Pause strategy B (e.g., during high volatility)
portfolio.pause_strategy("strat_b")
# Continue execution (only A and C execute)
for timestamp, data in data_feed:
portfolio.execute_bar(timestamp, data) # B is skipped
# Resume strategy B
portfolio.resume_strategy("strat_b")
# Remove strategy C (liquidate positions, return capital)
final_value = portfolio.remove_strategy("strat_c", liquidate=True)
print(f"Strategy C final value: ${final_value:,.2f}")
# Strategy C final value: $345,000.00
# Now capital from C is returned to portfolio (unallocated)
print(portfolio.allocated_capital) # Reduced by C's capital
Rebalancing¶
rebalance()¶
Source: allocator.py:584-667
def rebalance(
self,
new_allocations: dict[str, Decimal],
reason: str = "Manual rebalancing"
) -> None:
"""Rebalance capital between strategies.
Capital Transfer Logic:
- If new_allocation > old_allocation: transfer cash to strategy
- If new_allocation < old_allocation: reduce positions, return cash
Args:
new_allocations: Dict of {strategy_id: new_allocation_pct}
reason: Reason for rebalancing (for logging)
Raises:
ValueError: If allocations don't sum to valid amount or strategy not found
"""
Example 7: Manual Rebalancing¶
# Initial allocations
portfolio.add_strategy("momentum", MomentumStrategy(), Decimal("0.40")) # 40%
portfolio.add_strategy("mean_rev", MeanReversionStrategy(), Decimal("0.35")) # 35%
portfolio.add_strategy("trend", TrendFollowingStrategy(), Decimal("0.25")) # 25%
# After 6 months, momentum strategy outperforming
# Rebalance: increase momentum, reduce others
new_allocations = {
"momentum": Decimal("0.50"), # 40% → 50% (+10%)
"mean_rev": Decimal("0.30"), # 35% → 30% (-5%)
"trend": Decimal("0.20"), # 25% → 20% (-5%)
}
portfolio.rebalance(
new_allocations=new_allocations,
reason="Increase momentum allocation due to strong performance"
)
# Portfolio automatically:
# - Transfers $100k from mean_rev to momentum
# - Transfers $50k from trend to momentum
# - Updates allocated_capital for each strategy
# - Validates capital conservation
Example 8: Performance-Based Rebalancing¶
# Rebalance based on recent performance
def rebalance_by_performance(portfolio):
"""Allocate more to recent winners."""
strategy_metrics = portfolio.get_strategy_metrics()
# Get returns for each strategy
returns = {
sid: Decimal(metrics['return_pct'].rstrip('%')) / 100
for sid, metrics in strategy_metrics.items()
}
# Calculate scores (min-max normalization)
min_return = min(returns.values())
max_return = max(returns.values())
if max_return > min_return:
scores = {
sid: (ret - min_return) / (max_return - min_return)
for sid, ret in returns.items()
}
else:
# All equal - use equal weighting
scores = {sid: Decimal("1") for sid in returns}
# Normalize to sum to 1.0
total_score = sum(scores.values())
new_allocations = {
sid: score / total_score
for sid, score in scores.items()
}
# Apply rebalancing
portfolio.rebalance(
new_allocations=new_allocations,
reason="Performance-based rebalancing (allocate to winners)"
)
# Execute rebalancing
rebalance_by_performance(portfolio)
Portfolio-Level Metrics¶
get_portfolio_metrics()¶
Source: allocator.py:669-716
def get_portfolio_metrics(self) -> dict[str, Any]:
"""Calculate portfolio-level performance metrics.
Returns:
Dictionary with portfolio metrics
"""
Returns:
- total_value (str): Total portfolio value
- total_cash (str): Total cash across all strategies
- portfolio_return (str): Portfolio return percentage
- num_strategies (int): Number of strategies
- active_strategies (int): Number of running strategies
- weighted_avg_sharpe (str): Weighted average Sharpe ratio
Example 9: Monitoring Portfolio Performance¶
# Get portfolio metrics
metrics = portfolio.get_portfolio_metrics()
print(f"Portfolio Value: {metrics['total_value']}") # Portfolio Value: 1,050,000
print(f"Return: {metrics['portfolio_return']}") # Return: 5.00%
print(f"Active Strategies: {metrics['active_strategies']}/{metrics['num_strategies']}") # 3/3
print(f"Weighted Sharpe: {metrics['weighted_avg_sharpe']}") # Weighted Sharpe: 1.35
# Get per-strategy metrics
strategy_metrics = portfolio.get_strategy_metrics()
for strategy_id, metrics in strategy_metrics.items():
print(f"\n{strategy_id}:")
print(f" Capital: {metrics['allocated_capital']}")
print(f" Value: {metrics['current_value']}")
print(f" Return: {metrics['return_pct']}")
print(f" Sharpe: {metrics['sharpe_ratio']}")
print(f" Max DD: {metrics['max_drawdown']}")
get_correlation_matrix()¶
Source: allocator.py:735-768
def get_correlation_matrix(self) -> pd.DataFrame | None:
"""Calculate correlation matrix between strategies.
Returns:
DataFrame with correlation matrix or None if insufficient data
"""
Example 10: Strategy Correlation Analysis¶
# Get correlation matrix
corr_matrix = portfolio.get_correlation_matrix()
if corr_matrix is not None:
print("Strategy Correlation Matrix:")
print(corr_matrix)
# momentum mean_rev trend
# momentum 1.00 -0.35 0.60
# mean_rev -0.35 1.00 -0.20
# trend 0.60 -0.20 1.00
# Ideal: Low correlations indicate good diversification
# High correlations (>0.7) may indicate redundant strategies
StrategyAllocation¶
Source: rustybt/portfolio/allocator.py:30-76
Tracks allocation details for a single strategy.
Class Definition¶
@dataclass
class StrategyAllocation:
"""Allocation details for a single strategy.
Each strategy has:
- Independent ledger for isolated capital
- Allocated capital amount
- Performance tracker
- State management
"""
strategy_id: str
strategy: Any # TradingAlgorithm instance
allocated_capital: Decimal
ledger: Any # DecimalLedger instance
performance: StrategyPerformance
state: StrategyState = StrategyState.INITIALIZING
metadata: dict[str, Any] = field(default_factory=dict)
created_at: pd.Timestamp = field(default_factory=pd.Timestamp.now)
Properties¶
@property
def current_value(self) -> Decimal:
"""Current portfolio value for this strategy."""
return self.ledger.portfolio_value
@property
def return_pct(self) -> Decimal:
"""Return percentage since inception."""
if self.allocated_capital > Decimal("0"):
return (self.current_value - self.allocated_capital) / self.allocated_capital
return Decimal("0")
@property
def is_active(self) -> bool:
"""Check if strategy is actively trading."""
return self.state == StrategyState.RUNNING
StrategyState Enum¶
Source: allocator.py:20-28
class StrategyState(Enum):
"""Strategy lifecycle states."""
INITIALIZING = "initializing"
RUNNING = "running"
PAUSED = "paused"
LIQUIDATING = "liquidating"
STOPPED = "stopped"
StrategyPerformance¶
Source: rustybt/portfolio/allocator.py:78-285
Tracks performance metrics for individual strategy.
Class Definition¶
class StrategyPerformance:
"""Performance tracker for individual strategy.
Tracks:
- Returns over time
- Volatility (rolling and cumulative)
- Sharpe ratio
- Maximum drawdown
- Win rate and profit factor
"""
Constructor¶
def __init__(
self,
strategy_id: str,
lookback_window: int = 252, # Trading days for rolling metrics
) -> None:
"""Initialize performance tracker.
Args:
strategy_id: Unique strategy identifier
lookback_window: Number of periods for rolling metrics (252 = ~1 year daily)
"""
Performance Metrics¶
Source: allocator.py:172-284
volatility (property)¶
@property
def volatility(self) -> Decimal:
"""Calculate annualized volatility.
Formula:
σ_annual = std(daily_returns) × √252
Returns:
Annualized volatility
"""
sharpe_ratio (property)¶
@property
def sharpe_ratio(self) -> Decimal:
"""Calculate Sharpe ratio.
Formula: (mean_return - risk_free_rate) / volatility
Assumes risk-free rate = 0 for simplicity
Returns:
Sharpe ratio (annualized)
"""
win_rate (property)¶
@property
def win_rate(self) -> Decimal:
"""Calculate win rate (% of winning periods).
Returns:
Win rate as decimal (0.6 = 60%)
"""
profit_factor (property)¶
@property
def profit_factor(self) -> Decimal:
"""Calculate profit factor (total profit / total loss).
Returns:
Profit factor (>1 = profitable, <1 = unprofitable)
"""
Example 11: Accessing Strategy Performance¶
# Get strategy allocation
alloc = portfolio.strategies["momentum"]
# Access performance metrics
perf = alloc.performance
print(f"Strategy: {perf.strategy_id}")
print(f"Observations: {len(perf.portfolio_values)}")
print(f"Current Value: ${perf.portfolio_values[-1]:,.2f}")
print(f"Peak Value: ${perf.peak_value:,.2f}")
print(f"Current DD: {float(perf.current_drawdown):.2%}")
print(f"Max DD: {float(perf.max_drawdown):.2%}")
print(f"Volatility: {float(perf.volatility):.2%}")
print(f"Sharpe Ratio: {float(perf.sharpe_ratio):.2f}")
print(f"Win Rate: {float(perf.win_rate):.2%}")
print(f"Profit Factor: {float(perf.profit_factor):.2f}")
# Get full metrics dictionary
metrics_dict = perf.get_metrics()
print(metrics_dict)
# {
# 'strategy_id': 'momentum',
# 'num_observations': 252,
# 'current_value': '450000.00',
# 'peak_value': '460000.00',
# 'current_drawdown': '-2.17%',
# 'max_drawdown': '-8.50%',
# 'mean_return': '12.50%',
# 'volatility': '18.00%',
# 'sharpe_ratio': '0.69',
# 'win_rate': '55.00%',
# 'profit_factor': '1.35',
# 'winning_periods': 138,
# 'losing_periods': 113
# }
Allocation Algorithms¶
All allocation algorithms inherit from AllocationAlgorithm base class.
AllocationAlgorithm (Base Class)¶
Source: rustybt/portfolio/allocation.py:29-128
class AllocationAlgorithm(ABC):
"""Abstract base class for capital allocation algorithms.
All allocation algorithms must:
1. Implement calculate_allocations() method
2. Return allocations as Dict[strategy_id, allocation_pct]
3. Ensure allocations sum to <= 1.0 (100%)
4. Handle edge cases (zero volatility, insufficient data)
5. Use Decimal precision for all calculations
"""
@abstractmethod
def calculate_allocations(
self,
strategies: dict[str, StrategyPerformance]
) -> dict[str, Decimal]:
"""Calculate allocation percentages for each strategy."""
pass
AllocationConstraints¶
Source: allocation.py:130-152
@dataclass
class AllocationConstraints:
"""Constraints for capital allocation.
Enforces:
- Global min/max allocation per strategy
- Per-strategy overrides
- Sum <= 1.0 constraint
"""
default_min: Decimal = Decimal("0.0")
default_max: Decimal = Decimal("1.0")
strategy_min: dict[str, Decimal] = field(default_factory=dict)
strategy_max: dict[str, Decimal] = field(default_factory=dict)
FixedAllocation¶
Source: rustybt/portfolio/allocation.py:154-210
Static percentage allocation per strategy (buy-and-hold allocation).
Class Definition¶
class FixedAllocation(AllocationAlgorithm):
"""Fixed allocation: static percentages per strategy.
Use case: Conservative allocation when you have predefined strategy weights.
Example:
strategy1: 40%
strategy2: 30%
strategy3: 30%
"""
Constructor¶
def __init__(
self,
allocations: dict[str, Decimal],
constraints: AllocationConstraints | None = None,
) -> None:
"""Initialize fixed allocation.
Args:
allocations: Fixed allocation percentages per strategy
constraints: Optional constraints
Raises:
ValueError: If allocations sum to > 100%
"""
Example 12: Fixed Allocation¶
from rustybt.portfolio.allocation import FixedAllocation
# Define fixed allocations
fixed_algo = FixedAllocation(
allocations={
"momentum": Decimal("0.40"),
"mean_reversion": Decimal("0.35"),
"trend_following": Decimal("0.25"),
}
)
# Calculate allocations (ignores performance)
allocations = fixed_algo.calculate_allocations(strategy_performances)
print(allocations)
# {
# 'momentum': Decimal('0.40'),
# 'mean_reversion': Decimal('0.35'),
# 'trend_following': Decimal('0.25')
# }
# Use with portfolio rebalancing
portfolio.rebalance(allocations, reason="Fixed allocation strategy")
DynamicAllocation¶
Source: rustybt/portfolio/allocation.py:212-300
Performance-based allocation favoring recent winners (momentum-based).
Formula¶
Constructor¶
def __init__(
self,
lookback_window: int = 60, # 60 days ~3 months
min_allocation: Decimal = Decimal("0.05"), # 5% minimum
constraints: AllocationConstraints | None = None,
) -> None:
"""Initialize dynamic allocation.
Args:
lookback_window: Number of periods for performance calculation
min_allocation: Minimum allocation for any strategy (avoids zero allocation)
constraints: Optional constraints
"""
Example 13: Dynamic Allocation (Momentum-Based)¶
from rustybt.portfolio.allocation import DynamicAllocation
# Create dynamic allocator
dynamic_algo = DynamicAllocation(
lookback_window=60, # 3 months
min_allocation=Decimal("0.05"), # 5% minimum per strategy
)
# Get strategy performances
strategies = {
sid: alloc.performance
for sid, alloc in portfolio.strategies.items()
}
# Calculate allocations based on recent performance
allocations = dynamic_algo.calculate_allocations(strategies)
print(allocations)
# Winners get more, losers get less (but at least 5%)
# {
# 'momentum': Decimal('0.50'), # Best performer
# 'mean_reversion': Decimal('0.30'), # Middle
# 'trend_following': Decimal('0.20') # Worst performer
# }
# Apply rebalancing
portfolio.rebalance(allocations, reason="Dynamic allocation (favor recent winners)")
RiskParityAllocation¶
Source: rustybt/portfolio/allocation.py:302-405
Allocate inversely proportional to volatility (equal risk contribution).
Formula¶
Constructor¶
def __init__(
self,
lookback_window: int = 252, # 1 year daily data
min_volatility: Decimal = Decimal("0.001"), # Minimum vol to avoid division by zero
constraints: AllocationConstraints | None = None,
) -> None:
"""Initialize risk parity allocation.
Args:
lookback_window: Number of periods for volatility calculation
min_volatility: Minimum volatility threshold (avoids division by zero)
constraints: Optional constraints
"""
Example 14: Risk Parity Allocation¶
from rustybt.portfolio.allocation import RiskParityAllocation
# Create risk parity allocator
risk_parity = RiskParityAllocation(
lookback_window=252, # 1 year
min_volatility=Decimal("0.001"),
)
# Calculate allocations (inverse volatility weighting)
allocations = risk_parity.calculate_allocations(strategies)
# Strategy with lower volatility gets higher allocation
# Strategy with higher volatility gets lower allocation
print(allocations)
# {
# 'momentum': Decimal('0.25'), # σ = 25%
# 'mean_reversion': Decimal('0.40'), # σ = 15% (lower vol → higher allocation)
# 'trend_following': Decimal('0.35') # σ = 18%
# }
# Apply rebalancing
portfolio.rebalance(allocations, reason="Risk parity (equal risk contribution)")
KellyCriterionAllocation¶
Source: rustybt/portfolio/allocation.py:407-543
Growth-optimal allocation based on Kelly criterion.
Formula¶
Constructor¶
def __init__(
self,
lookback_window: int = 252, # 1 year
kelly_fraction: Decimal = Decimal("0.5"), # Half-Kelly (conservative)
min_variance: Decimal = Decimal("0.0001"),
constraints: AllocationConstraints | None = None,
) -> None:
"""Initialize Kelly criterion allocation.
Args:
lookback_window: Number of periods for return/variance calculation
kelly_fraction: Fraction of Kelly to use (0.5 = half-Kelly, conservative)
min_variance: Minimum variance threshold
constraints: Optional constraints
"""
Example 15: Kelly Criterion Allocation¶
from rustybt.portfolio.allocation import KellyCriterionAllocation
# Create Kelly allocator (use half-Kelly for safety)
kelly = KellyCriterionAllocation(
lookback_window=252,
kelly_fraction=Decimal("0.5"), # Half-Kelly (more conservative)
)
# Calculate growth-optimal allocations
allocations = kelly.calculate_allocations(strategies)
# High return + low variance → higher allocation
# Low return or high variance → lower allocation
print(allocations)
# {
# 'momentum': Decimal('0.45'), # High Sharpe (μ/σ² high)
# 'mean_reversion': Decimal('0.35'), # Medium Sharpe
# 'trend_following': Decimal('0.20') # Lower Sharpe
# }
# Apply rebalancing
portfolio.rebalance(allocations, reason="Kelly criterion (growth optimal)")
Warning: Full Kelly can be aggressive. Use fractional Kelly (0.25-0.5) for safety.
DrawdownBasedAllocation¶
Source: rustybt/portfolio/allocation.py:545-634
Reduce allocation to strategies in drawdown (risk-averse).
Formula¶
Constructor¶
def __init__(
self,
max_drawdown_threshold: Decimal = Decimal("0.20"), # 20% max drawdown
recovery_bonus: Decimal = Decimal("0.1"), # 10% bonus for recovering strategies
constraints: AllocationConstraints | None = None,
) -> None:
"""Initialize drawdown-based allocation.
Args:
max_drawdown_threshold: Drawdown threshold for penalty
recovery_bonus: Bonus allocation for strategies recovering from drawdown
constraints: Optional constraints
"""
Example 16: Drawdown-Based Allocation¶
from rustybt.portfolio.allocation import DrawdownBasedAllocation
# Create drawdown-based allocator
drawdown_algo = DrawdownBasedAllocation(
max_drawdown_threshold=Decimal("0.20"), # 20% max
recovery_bonus=Decimal("0.1"), # 10% bonus for recovery
)
# Calculate allocations (penalize strategies in drawdown)
allocations = drawdown_algo.calculate_allocations(strategies)
# Strategy in drawdown gets lower allocation
# Recovering strategy gets bonus allocation
print(allocations)
# {
# 'momentum': Decimal('0.50'), # No drawdown, at peak
# 'mean_reversion': Decimal('0.35'), # Small drawdown (-5%)
# 'trend_following': Decimal('0.15') # Large drawdown (-15%), penalized
# }
# Apply rebalancing
portfolio.rebalance(allocations, reason="Drawdown-based (reduce exposure to losers)")
AllocationRebalancer¶
Source: rustybt/portfolio/allocation.py:645-801
Rebalancing scheduler with frequency and drift-based triggers.
Class Definition¶
class AllocationRebalancer:
"""Rebalancing scheduler for allocation algorithms.
Manages:
- Rebalancing frequency (daily, weekly, monthly)
- Cooldown periods (prevent excessive rebalancing)
- Threshold-based triggers (rebalance if allocation drifts > X%)
"""
Constructor¶
def __init__(
self,
algorithm: AllocationAlgorithm,
frequency: RebalancingFrequency = RebalancingFrequency.MONTHLY,
cooldown_days: int = 7, # Minimum days between rebalances
drift_threshold: Decimal | None = None, # Rebalance if drift > threshold
) -> None:
"""Initialize rebalancing scheduler.
Args:
algorithm: Allocation algorithm to use
frequency: Rebalancing frequency
cooldown_days: Minimum days between rebalances
drift_threshold: Optional drift threshold for threshold-based rebalancing
"""
RebalancingFrequency Enum¶
Source: allocation.py:636-643
class RebalancingFrequency(Enum):
"""Rebalancing frequency options."""
DAILY = "daily"
WEEKLY = "weekly"
MONTHLY = "monthly"
CUSTOM = "custom"
should_rebalance()¶
Source: allocation.py:684-744
def should_rebalance(
self,
current_time: pd.Timestamp,
current_allocations: dict[str, Decimal] | None = None,
target_allocations: dict[str, Decimal] | None = None,
) -> tuple[bool, str]:
"""Check if rebalancing should occur.
Returns:
Tuple of (should_rebalance, reason)
"""
Example 17: Monthly Rebalancing with Risk Parity¶
from rustybt.portfolio.allocation import (
RiskParityAllocation,
AllocationRebalancer,
RebalancingFrequency,
)
# Create risk parity algorithm
risk_parity = RiskParityAllocation(lookback_window=252)
# Create rebalancer (monthly, with 7-day cooldown)
rebalancer = AllocationRebalancer(
algorithm=risk_parity,
frequency=RebalancingFrequency.MONTHLY,
cooldown_days=7,
)
# In backtest loop
for timestamp, data in data_feed:
# Execute strategies
portfolio.execute_bar(timestamp, data)
# Check if should rebalance
current_allocs = {
sid: alloc.allocated_capital / portfolio.total_capital
for sid, alloc in portfolio.strategies.items()
}
should_rebal, reason = rebalancer.should_rebalance(
current_time=timestamp,
current_allocations=current_allocs,
)
if should_rebal:
# Calculate new allocations using risk parity
strategies = {
sid: alloc.performance
for sid, alloc in portfolio.strategies.items()
}
new_allocations = rebalancer.rebalance(strategies, timestamp)
# Apply rebalancing
portfolio.rebalance(new_allocations, reason=reason)
Example 18: Drift-Based Rebalancing¶
# Rebalance if allocation drifts > 5% from target
rebalancer = AllocationRebalancer(
algorithm=risk_parity,
frequency=RebalancingFrequency.MONTHLY,
cooldown_days=7,
drift_threshold=Decimal("0.05"), # 5% drift threshold
)
# Check for drift
target_allocs = {"momentum": Decimal("0.33"), "mean_rev": Decimal("0.33"), "trend": Decimal("0.34")}
current_allocs = {"momentum": Decimal("0.40"), "mean_rev": Decimal("0.30"), "trend": Decimal("0.30")}
should_rebal, reason = rebalancer.should_rebalance(
current_time=timestamp,
current_allocations=current_allocs,
target_allocations=target_allocs,
)
# Momentum drifted from 33% to 40% (7% drift > 5% threshold)
print(should_rebal) # True
print(reason) # "Allocation drift (7.0% > 5.0%)"
Production Examples¶
Example 19: Complete Multi-Strategy Portfolio¶
from decimal import Decimal
import pandas as pd
from rustybt.portfolio.allocator import PortfolioAllocator
from rustybt.portfolio.allocation import (
RiskParityAllocation,
AllocationRebalancer,
RebalancingFrequency,
)
# 1. Create portfolio
portfolio = PortfolioAllocator(
total_capital=Decimal("1000000"),
name="HedgeFund_Diversified"
)
# 2. Add strategies
portfolio.add_strategy(
"momentum_short_term",
MomentumStrategy(lookback=20),
Decimal("0.25"),
metadata={"description": "Short-term momentum (20-day)"}
)
portfolio.add_strategy(
"mean_reversion_rsi",
MeanReversionStrategy(threshold=30),
Decimal("0.25"),
metadata={"description": "Mean reversion RSI"}
)
portfolio.add_strategy(
"trend_ma_crossover",
TrendFollowingStrategy(fast=50, slow=200),
Decimal("0.25"),
metadata={"description": "Moving average crossover"}
)
portfolio.add_strategy(
"pairs_trading",
PairsTradingStrategy(pairs=[("AAPL", "GOOGL")]),
Decimal("0.25"),
metadata={"description": "Statistical arbitrage pairs"}
)
# 3. Setup rebalancing (monthly risk parity)
risk_parity = RiskParityAllocation(lookback_window=252)
rebalancer = AllocationRebalancer(
algorithm=risk_parity,
frequency=RebalancingFrequency.MONTHLY,
cooldown_days=7,
)
# 4. Backtest execution
timestamps = pd.date_range("2023-01-01", "2024-12-31", freq="D")
for timestamp in timestamps:
# Fetch market data
market_data = fetch_market_data(timestamp)
# Execute all strategies
portfolio.execute_bar(timestamp, market_data)
# Check rebalancing
current_allocs = {
sid: alloc.allocated_capital / portfolio.total_capital
for sid, alloc in portfolio.strategies.items()
}
should_rebal, reason = rebalancer.should_rebalance(
current_time=timestamp,
current_allocations=current_allocs,
)
if should_rebal:
strategies = {
sid: alloc.performance
for sid, alloc in portfolio.strategies.items()
}
new_allocations = rebalancer.rebalance(strategies, timestamp)
portfolio.rebalance(new_allocations, reason=reason)
# 5. Final results
print("\n=== Final Portfolio Results ===")
metrics = portfolio.get_portfolio_metrics()
print(f"Total Value: {metrics['total_value']}")
print(f"Return: {metrics['portfolio_return']}")
print(f"Weighted Sharpe: {metrics['weighted_avg_sharpe']}")
print("\n=== Per-Strategy Results ===")
strategy_metrics = portfolio.get_strategy_metrics()
for sid, metrics in strategy_metrics.items():
print(f"\n{sid}:")
print(f" Return: {metrics['return_pct']}")
print(f" Sharpe: {metrics['sharpe_ratio']}")
print(f" Max DD: {metrics['max_drawdown']}")
print(f" Win Rate: {metrics['win_rate']}")
print("\n=== Correlation Matrix ===")
corr_matrix = portfolio.get_correlation_matrix()
if corr_matrix is not None:
print(corr_matrix)
Example 20: Adaptive Allocation (Switch Based on Market Regime)¶
from rustybt.portfolio.allocation import (
RiskParityAllocation,
DynamicAllocation,
DrawdownBasedAllocation,
)
# Define market regime detector
class MarketRegime(Enum):
BULL = "bull"
BEAR = "bear"
SIDEWAYS = "sideways"
HIGH_VOL = "high_volatility"
def detect_market_regime(market_data) -> MarketRegime:
"""Detect current market regime based on indicators."""
# Implement regime detection logic
# (VIX levels, moving averages, volatility, etc.)
pass
# Create allocators for different regimes
allocators = {
MarketRegime.BULL: DynamicAllocation(lookback_window=60), # Momentum in bull market
MarketRegime.BEAR: DrawdownBasedAllocation(), # Defensive in bear market
MarketRegime.SIDEWAYS: RiskParityAllocation(), # Balanced in sideways market
MarketRegime.HIGH_VOL: DrawdownBasedAllocation(), # Conservative in high vol
}
# In backtest loop
for timestamp, data in data_feed:
portfolio.execute_bar(timestamp, data)
# Detect current regime
regime = detect_market_regime(data)
# Select appropriate allocator
allocator = allocators[regime]
# Check rebalancing (monthly)
if timestamp.day == 1: # First of month
strategies = {sid: alloc.performance for sid, alloc in portfolio.strategies.items()}
new_allocations = allocator.calculate_allocations(strategies)
portfolio.rebalance(
new_allocations,
reason=f"Regime-based rebalancing ({regime.value})"
)
Best Practices¶
1. Strategy Isolation¶
DO:
- Use separate ledgers for each strategy
- Never share positions between strategies
- Transfer capital only through PortfolioAllocator.rebalance()
DON'T:
- Access other strategies' ledgers directly
- Modify positions from outside strategy's handle_data()
2. Allocation Constraints¶
DO:
- Set min/max allocation per strategy (e.g., 5-40%)
- Ensure allocations sum to ≤ 100%
- Use AllocationConstraints to enforce limits
from rustybt.portfolio.allocation import AllocationConstraints
constraints = AllocationConstraints(
default_min=Decimal("0.05"), # 5% minimum
default_max=Decimal("0.40"), # 40% maximum
strategy_min={
"core_strategy": Decimal("0.20"), # Core strategy gets at least 20%
},
)
risk_parity = RiskParityAllocation(constraints=constraints)
3. Rebalancing Frequency¶
DO: - Use monthly or quarterly rebalancing for most strategies - Include cooldown periods (7-30 days) to prevent excessive rebalancing - Use drift-based triggers (5-10% threshold) for opportunistic rebalancing
DON'T: - Rebalance daily (high transaction costs) - Rebalance without cooldown (over-trading)
4. Performance Monitoring¶
DO: - Track per-strategy and portfolio-level metrics - Monitor correlation between strategies (aim for < 0.7) - Log rebalancing events with reasons
# Log all metrics after each month
if timestamp.day == 1:
metrics = portfolio.get_portfolio_metrics()
strategy_metrics = portfolio.get_strategy_metrics()
corr_matrix = portfolio.get_correlation_matrix()
logger.info("monthly_metrics", **metrics)
for sid, m in strategy_metrics.items():
logger.info("strategy_metrics", strategy_id=sid, **m)
if corr_matrix is not None:
logger.info("correlation_matrix", matrix=corr_matrix.to_dict())
5. Capital Allocation¶
DO: - Start with equal allocation (33.33% each for 3 strategies) - Use risk parity for uncorrelated strategies - Reserve 10-20% cash buffer for opportunities
DON'T: - Allocate 100% immediately (keep some dry powder) - Over-allocate to single strategy (> 50%)
6. Strategy Lifecycle¶
DO: - Pause underperforming strategies (drawdown > 20%) - Remove strategies after consistent underperformance (6+ months) - Add new strategies with small initial allocation (5-10%)
# Example: Pause strategy if drawdown > 20%
for sid, alloc in portfolio.strategies.items():
if alloc.performance.current_drawdown < Decimal("-0.20"):
portfolio.pause_strategy(sid)
logger.warning("strategy_paused_drawdown", strategy_id=sid, dd=alloc.performance.current_drawdown)
7. Allocation Algorithm Selection¶
Use Case Guide:
| Scenario | Recommended Algorithm | Reason |
|---|---|---|
| Predefined weights | FixedAllocation |
Simple, buy-and-hold |
| Momentum-based | DynamicAllocation |
Allocate to recent winners |
| Diversification | RiskParityAllocation |
Equal risk contribution |
| Growth maximization | KellyCriterionAllocation |
Optimal growth rate |
| Risk-averse | DrawdownBasedAllocation |
Reduce exposure to losers |
| Market regime | Adaptive (switch algorithms) | Match regime characteristics |
8. Error Handling¶
DO: - Catch strategy execution errors (don't let one strategy crash portfolio) - Validate allocations before rebalancing - Log all exceptions with strategy context
# PortfolioAllocator.execute_bar() handles this automatically
try:
allocation.strategy.handle_data(allocation.ledger, data)
except Exception as e:
logger.error(
"strategy_execution_failed",
portfolio=self.name,
strategy_id=strategy_id,
error=str(e),
exc_info=True,
)
# Optionally pause failed strategy
# allocation.state = StrategyState.PAUSED
Cross-References¶
- Order Management:
docs/api/order-management/order-types.md - Execution Pipeline:
docs/api/order-management/execution/execution-pipeline.md - Transaction Costs:
docs/api/order-management/transaction-costs/slippage-models-verified.md - Risk Management:
docs/api/portfolio-management/risk-management.md - Order Aggregation:
docs/api/portfolio-management/order-aggregation.md - Performance Metrics:
docs/api/analytics/performance-metrics.md
Summary¶
RustyBT's portfolio allocation system provides institutional-grade multi-strategy management with:
✅ Strategy Isolation: Independent ledgers prevent interference ✅ Flexible Allocation: Fixed, dynamic, risk parity, Kelly, drawdown-based algorithms ✅ Performance Tracking: Per-strategy metrics (Sharpe, drawdown, volatility, win rate) ✅ Automated Rebalancing: Frequency-based or drift-based triggers ✅ Production-Ready: Comprehensive logging, error handling, validation
This system enables you to: - Run multiple uncorrelated strategies simultaneously - Dynamically allocate capital based on performance or risk - Monitor individual strategy performance - Rebalance portfolio based on market conditions - Build hedge fund-style diversified portfolios
Key Takeaway: Use PortfolioAllocator as the foundation for multi-strategy portfolios, choose the appropriate AllocationAlgorithm for your objectives, and let AllocationRebalancer handle periodic capital reallocation.