Upload strategy_ensemble.py
Browse files- strategy_ensemble.py +100 -0
strategy_ensemble.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Strategy Ensemble - Dynamic capital allocation across strategies."""
|
| 2 |
+
import numpy as np
|
| 3 |
+
import pandas as pd
|
| 4 |
+
from typing import Dict, List, Optional
|
| 5 |
+
import warnings
|
| 6 |
+
warnings.filterwarnings('ignore')
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class StrategyEnsemble:
|
| 10 |
+
"""Multi-strategy system with dynamic capital allocation."""
|
| 11 |
+
|
| 12 |
+
def __init__(self):
|
| 13 |
+
self.strategies = {
|
| 14 |
+
'momentum': {'weight': 0.25, 'sharpe': 0.8, 'track_record': []},
|
| 15 |
+
'mean_reversion': {'weight': 0.25, 'sharpe': 0.6, 'track_record': []},
|
| 16 |
+
'arbitrage': {'weight': 0.20, 'sharpe': 1.2, 'track_record': []},
|
| 17 |
+
'ml_alpha': {'weight': 0.15, 'sharpe': 1.0, 'track_record': []},
|
| 18 |
+
'sentiment': {'weight': 0.10, 'sharpe': 0.5, 'track_record': []},
|
| 19 |
+
'options': {'weight': 0.05, 'sharpe': 0.9, 'track_record': []}
|
| 20 |
+
}
|
| 21 |
+
self.capital = 1_000_000
|
| 22 |
+
self.allocation_history = []
|
| 23 |
+
|
| 24 |
+
def update_performance(self, strategy_returns: Dict[str, float]):
|
| 25 |
+
"""Update rolling Sharpe for each strategy."""
|
| 26 |
+
for name, ret in strategy_returns.items():
|
| 27 |
+
if name in self.strategies:
|
| 28 |
+
self.strategies[name]['track_record'].append(ret)
|
| 29 |
+
# Keep last 63 days
|
| 30 |
+
if len(self.strategies[name]['track_record']) > 63:
|
| 31 |
+
self.strategies[name]['track_record'] = self.strategies[name]['track_record'][-63:]
|
| 32 |
+
|
| 33 |
+
# Update Sharpe
|
| 34 |
+
records = self.strategies[name]['track_record']
|
| 35 |
+
if len(records) > 5:
|
| 36 |
+
mean_ret = np.mean(records)
|
| 37 |
+
std_ret = np.std(records)
|
| 38 |
+
self.strategies[name]['sharpe'] = mean_ret / std_ret * np.sqrt(252) if std_ret > 0 else 0
|
| 39 |
+
|
| 40 |
+
def allocate_capital(self, max_weight: float = 0.35, min_weight: float = 0.02) -> Dict[str, float]:
|
| 41 |
+
"""
|
| 42 |
+
Dynamic capital allocation based on:
|
| 43 |
+
- Recent Sharpe ratio
|
| 44 |
+
- Strategy correlation (for diversification)
|
| 45 |
+
- Risk budget
|
| 46 |
+
"""
|
| 47 |
+
# Get Sharpe ratios
|
| 48 |
+
sharpes = {name: max(s['sharpe'], 0.1) for name, s in self.strategies.items()}
|
| 49 |
+
|
| 50 |
+
# Square Sharpe for more differentiation
|
| 51 |
+
weights = {name: s ** 2 for name, s in sharpes.items()}
|
| 52 |
+
total = sum(weights.values())
|
| 53 |
+
|
| 54 |
+
if total > 0:
|
| 55 |
+
weights = {name: w / total for name, w in weights.items()}
|
| 56 |
+
else:
|
| 57 |
+
n = len(weights)
|
| 58 |
+
weights = {name: 1.0 / n for name in weights}
|
| 59 |
+
|
| 60 |
+
# Apply constraints
|
| 61 |
+
for name in weights:
|
| 62 |
+
weights[name] = np.clip(weights[name], min_weight, max_weight)
|
| 63 |
+
|
| 64 |
+
# Renormalize
|
| 65 |
+
total = sum(weights.values())
|
| 66 |
+
weights = {name: w / total for name, w in weights.items()}
|
| 67 |
+
|
| 68 |
+
self.allocation_history.append(weights)
|
| 69 |
+
return weights
|
| 70 |
+
|
| 71 |
+
def allocate_dollar(self, total_capital: float, weights: Dict[str, float]) -> Dict[str, float]:
|
| 72 |
+
"""Convert weight allocation to dollar amounts."""
|
| 73 |
+
return {name: total_capital * w for name, w in weights.items()}
|
| 74 |
+
|
| 75 |
+
def combine_signals(self, signals: Dict[str, np.ndarray], weights: Optional[Dict[str, float]] = None) -> np.ndarray:
|
| 76 |
+
"""Combine signals from multiple strategies."""
|
| 77 |
+
if weights is None:
|
| 78 |
+
weights = self.allocate_capital()
|
| 79 |
+
|
| 80 |
+
n_samples = len(list(signals.values())[0])
|
| 81 |
+
combined = np.zeros(n_samples)
|
| 82 |
+
|
| 83 |
+
for name, signal in signals.items():
|
| 84 |
+
if name in weights:
|
| 85 |
+
combined += weights[name] * signal
|
| 86 |
+
|
| 87 |
+
return combined
|
| 88 |
+
|
| 89 |
+
def get_performance_report(self) -> pd.DataFrame:
|
| 90 |
+
"""Generate strategy performance report."""
|
| 91 |
+
report = []
|
| 92 |
+
for name, strat in self.strategies.items():
|
| 93 |
+
report.append({
|
| 94 |
+
'strategy': name,
|
| 95 |
+
'current_weight': self.allocation_history[-1].get(name, 0) if self.allocation_history else strat['weight'],
|
| 96 |
+
'sharpe': strat['sharpe'],
|
| 97 |
+
'max_weight': max(a.get(name, 0) for a in self.allocation_history) if self.allocation_history else 0,
|
| 98 |
+
'track_record_len': len(strat['track_record'])
|
| 99 |
+
})
|
| 100 |
+
return pd.DataFrame(report)
|