# region imports
from AlgorithmImports import *
# endregion
# ===========================================================================
# MULTI-ALPHA FRAMEWORK - STATUS REPORT
# ===========================================================================
# Projekt: 25806232 - Multi-Alpha Framework Defense+Seasonal
# Datum: 2025-10-23
# Session: Fortsetzung (Context-Limit)
# ===========================================================================
"""
PROJEKTZIEL:
Kombination von Defense (60%) + Seasonal (40%) Strategien im Framework
ERWARTET (Original Non-Framework):
- Profit: 475%, DD: 20.3%, Sharpe: 0.907, Trades: 1,183
BESTE VERSION: v2 (35ca7aca)
- Profit: 44.8%, DD: 21.7% ✅, Sharpe: 0.043, Trades: 806
AKTUELL: v4 (c1cbd4194)
- Profit: 73%, DD: 41.5%, Sharpe: 0.164, Trades: 25,986 ❌
HAUPTPROBLEM: 20x zu viele Trades!
NÄCHSTE TESTS:
1. Insight Period = Forever (statt 30 Tage)
2. Simplified PCM (ohne Per-Alpha Tracking)
3. Call-Frequency Logging
4. Separate Alpha Tests
ALTERNATIVEN:
- Option B: Hybrid (Alpha Models + Manual Scheduling)
- Option C: Original Code verwenden (funktioniert perfekt)
DETAILS: Siehe Docstring unten
"""
pass
from AlgorithmImports import *
from datetime import timedelta
from models.universe.multi_alpha_universe_model import MultiAlphaUniverseSelectionModel
from models.alpha.defense_alpha_model import DefenseAlphaModel
from models.alpha.seasonal_alpha_model import SeasonalAlphaModel
from models.alpha.intraday_momentum_alpha_model import IntradayMomentumAlphaModel
from models.portfolio.multi_alpha_pcm import MultiAlphaPortfolioConstructionModel
from models.risk.multi_alpha_risk_model import MultiAlphaRiskManagementModel
from models.execution.tagged_execution_model import TaggedImmediateExecutionModel
class MultiAlphaFramework(QCAlgorithm):
"""
Multi-Alpha Framework: Defense (40%) + Seasonal (40%) + Intraday (20%)
Architecture:
- Tag-based insights (no weight in alphas)
- Forever Period for Defense + Seasonal (explicit FLAT on exits)
- Short Period (30 min) for Intraday + EOD liquidation
- PCM handles all allocation (40/40/20 budget + rank/equal/volatility logic)
- Simple risk filters (leverage, position size, drawdown)
Backtest: 2015-2025 (10 years)
Target Trades: ~2,500 (vs original 1,183)
"""
def Initialize(self) -> None:
# Backtest Period - MATCHING ORIGINAL (2015-2025)
self.SetStartDate(2015, 1, 2)
self.SetEndDate(2025, 5, 16)
self.SetCash(100_000)
# Track which Alpha owns each symbol (for Capital Allocation Chart)
# Updated in OnOrderEvent, read in OnData - much more efficient!
self.symbol_to_alpha = {} # {Symbol: "Defense" or "Seasonal"}
# Brokerage
self.SetBrokerageModel(
BrokerageName.InteractiveBrokersBrokerage,
AccountType.Margin
)
# Warm-up (Defense needs 252 days, Intraday needs 90 days)
self.SetWarmUp(timedelta(days=260))
self.Debug("=== Multi-Alpha Framework Initialization ===")
# CRITICAL FIX: Defense assets need MINUTE resolution for Update() to run at 14:40!
# With Daily resolution, Update() only runs once per day at midnight
defense_symbols = ["TLT", "GLD", "DBC", "UUP", "UPRO"]
for symbol_str in defense_symbols:
security = self.AddEquity(symbol_str, Resolution.Minute)
security.SetDataNormalizationMode(DataNormalizationMode.Adjusted)
self.Debug(f" Added {symbol_str} with MINUTE resolution for Defense timing")
# Universe Selection (for Seasonal assets - Daily is fine)
universe = MultiAlphaUniverseSelectionModel()
self.SetUniverseSelection(universe)
# SPY: Special handling (Minute Resolution for Seasonal)
spy_symbol = universe.add_spy_with_minute_resolution(self)
self.Debug(" Universe: Defense (5 Minute) + SPY (Minute) + Seasonal (20 Daily)")
# Alpha Models (NO weights - tags only!)
defense_alpha = DefenseAlphaModel()
seasonal_alpha = SeasonalAlphaModel()
# TEMPORARILY DISABLED FOR TESTING
# intraday_alpha = IntradayMomentumAlphaModel(
# period=90,
# min_volatility=0.008,
# consolidation_period=30
# )
# Composite Alpha Model - WITHOUT INTRADAY FOR NOW
self.SetAlpha(CompositeAlphaModel(defense_alpha, seasonal_alpha))
self.Debug(" Alpha Models:")
self.Debug(" - DefenseAlphaModel (60% budget) - MATCHING ORIGINAL")
self.Debug(" - SeasonalAlphaModel (40% budget) - MATCHING ORIGINAL")
self.Debug(" - IntradayMomentumAlphaModel (0% budget) - DISABLED FOR COMPARISON")
# Portfolio Construction Model (60/40/0 budget allocation - matching original)
alpha_budgets = {
"DefenseAlphaModel": 0.6,
"SeasonalAlphaModel": 0.4,
"IntradayMomentumAlphaModel": 0.0
}
pcm = MultiAlphaPortfolioConstructionModel(alpha_budgets=alpha_budgets)
self.SetPortfolioConstruction(pcm)
self.Debug(" PCM Budget: 60% Defense / 40% Seasonal / 0% Intraday (Original allocation)")
# Risk Management Model
risk = MultiAlphaRiskManagementModel(
max_total_leverage=2.0,
max_single_position=0.25,
max_drawdown=0.30
)
self.SetRiskManagement(risk)
self.Debug(" Risk: Max Leverage 2.0x, Max Position 25%, Max DD 30%")
# Execution Model with Order Tags
self.SetExecution(TaggedImmediateExecutionModel())
self.Debug(" Execution: Tagged Immediate (preserves Alpha source in order tags)")
# Schedule EOD Liquidation for Intraday positions
# Important: Only liquidate SPY if NOT in Seasonal hold period (Oct 1 - Jan 31)
self.Schedule.On(
self.DateRules.EveryDay("SPY"),
self.TimeRules.BeforeMarketClose("SPY", 1),
self._close_intraday_positions
)
self.Debug(" Scheduled: EOD Liquidation (Intraday only, respects Seasonal hold period)")
# Chart: Capital Allocation by Alpha Strategy
capital_chart = Chart("Capital Allocation")
capital_chart.AddSeries(Series("Defense", SeriesType.Line, "%", Color.Blue))
capital_chart.AddSeries(Series("Seasonal", SeriesType.Line, "%", Color.Green))
capital_chart.AddSeries(Series("Total", SeriesType.Line, "%", Color.White))
self.AddChart(capital_chart)
self.Debug(" Charts: Capital Allocation (Defense/Seasonal/Total)")
self.Debug("=== Framework Initialization Complete ===")
def _close_intraday_positions(self) -> None:
"""
End-of-day liquidation for Intraday positions
CRITICAL: Only close SPY if NOT in Seasonal hold period (Oct 1 - Jan 31)
- During Oct-Jan: Seasonal owns SPY, don't liquidate
- Outside Oct-Jan: Intraday can be liquidated
"""
if self.IsWarmingUp:
return
# Check if SPY is in Seasonal hold period
if self._is_spy_seasonal_active():
# Seasonal period active - don't touch SPY
self.Debug("[EOD] SPY in Seasonal hold period (Oct-Jan), skipping liquidation")
return
# Outside seasonal period - liquidate SPY if held
if self.Portfolio["SPY"].Invested:
self.Liquidate("SPY")
self.Debug("[EOD] Liquidated Intraday SPY position")
def _is_spy_seasonal_active(self) -> bool:
"""
Check if SPY seasonal hold period is active (Oct 1 - Jan 31)
"""
month = self.Time.month
return month >= 10 or month == 1
def OnOrderEvent(self, orderEvent: OrderEvent) -> None:
"""
Log order events with timing information and track Alpha ownership
"""
if orderEvent.Status == OrderStatus.Filled:
# Get the order to check its tag
order = self.Transactions.GetOrderById(orderEvent.OrderId)
if order and order.Tag:
# Cache which Alpha owns this symbol
self.symbol_to_alpha[orderEvent.Symbol] = order.Tag
self.Debug(f"[TIMING|Order] {self.Time}: Order FILLED - {orderEvent.Symbol.Value}, "
f"Qty: {orderEvent.FillQuantity}, Tag: {order.Tag if order else 'N/A'}")
def OnEndOfDay(self, symbol: Symbol) -> None:
"""
Track capital allocation by Alpha strategy - ONLY ONCE PER DAY
Called at end of each trading day (much more efficient than OnData)
"""
if self.IsWarmingUp:
return
# Only process for SPY (one call per day instead of 390!)
if symbol.Value != "SPY":
return
# Calculate capital allocation by Alpha source (using cached tags)
portfolio_value = self.Portfolio.TotalPortfolioValue
if portfolio_value <= 0:
return
defense_value = 0.0
seasonal_value = 0.0
# Iterate through all holdings - use cached Alpha ownership
for sym, holding in self.Portfolio.items():
if not holding.Invested:
continue
# Use cached tag from OnOrderEvent (MUCH faster than searching orders!)
alpha_tag = self.symbol_to_alpha.get(sym, "Unknown")
if alpha_tag == "Defense":
defense_value += holding.HoldingsValue
elif alpha_tag == "Seasonal":
seasonal_value += holding.HoldingsValue
# Calculate percentages
defense_pct = defense_value / portfolio_value
seasonal_pct = seasonal_value / portfolio_value
total_invested_pct = (defense_value + seasonal_value) / portfolio_value
# Plot to chart (daily)
self.Plot("Capital Allocation", "Defense", defense_pct)
self.Plot("Capital Allocation", "Seasonal", seasonal_pct)
self.Plot("Capital Allocation", "Total", total_invested_pct)
# Log every Monday
if self.Time.weekday() == 0:
positions = len([h for h in self.Portfolio.Values if h.Invested])
self.Debug(f"[{self.Time.strftime('%Y-%m-%d')}] Portfolio: ${portfolio_value:,.0f}, "
f"Defense: {defense_pct:.1%}, Seasonal: {seasonal_pct:.1%}, "
f"Total: {total_invested_pct:.1%}, Positions: {positions}")
def OnEndOfAlgorithm(self) -> None:
"""
Summary at end of backtest
"""
portfolio_value = self.Portfolio.TotalPortfolioValue
starting_capital = 100_000
total_return = (portfolio_value - starting_capital) / starting_capital
# Count total trades
total_trades = len([order for order in self.Transactions.GetOrders() if order.Status == OrderStatus.Filled])
self.Debug("=== Backtest Complete ===")
self.Debug(f" Final Portfolio Value: ${portfolio_value:,.2f}")
self.Debug(f" Total Return: {total_return:.2%}")
self.Debug(f" Total Trades: {total_trades}")
# Statistics will be shown in QuantConnect UI
self.Debug("===================")
# region imports
from AlgorithmImports import *
# endregion
# Alpha Models Module
from .defense_alpha_model import DefenseAlphaModel
from .seasonal_alpha_model import SeasonalAlphaModel
from .intraday_momentum_alpha_model import IntradayMomentumAlphaModel
__all__ = [
'DefenseAlphaModel',
'SeasonalAlphaModel',
'IntradayMomentumAlphaModel'
]
from AlgorithmImports import *
from datetime import timedelta
from typing import List, Dict, Set
class DefenseAlphaModel(AlphaModel):
"""
Defense-First Alpha Model with UPRO
Strategy:
- Ranks defensive assets (TLT, GLD, DBC, UUP) by multi-period momentum
- Top 4 get ranks 1-4 (allocated 40%, 30%, 20%, 10% by PCM via tag parsing)
- If momentum < risk-free rate, that rank's weight goes to UPRO instead
- Rebalances monthly on first trading day
CRITICAL FIX (Daily Consolidator):
- Uses Daily consolidator to ensure history windows receive DAILY bars
- Works regardless of subscription resolution (Minute or Daily)
- Each AlphaModel manages its own consolidators independently
"""
def __init__(self):
self.defensive_symbols = ["TLT", "GLD", "DBC", "UUP"]
self.upro_symbol = "UPRO"
self.symbols = []
self.momentum_periods = [21, 63, 126, 252]
self.history_windows = {}
self.last_rebalance_month = -1
self.risk_free_monthly = 0.045 / 12 # ~4.5% annual
self.defensive_weights = [0.4, 0.3, 0.2, 0.1] # For info only, PCM uses ranks
# Track current allocation to emit FLAT insights on exit
self.current_allocation: Set[Symbol] = set()
# Track consolidators for cleanup
self.consolidators = {}
# Debug tracking
self.update_call_count = 0
self.last_debug_date = None
def Update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
if algorithm.IsWarmingUp:
return []
# Debug: Track Update() calls (once per day)
current_date = algorithm.Time.date()
if current_date != self.last_debug_date:
self.update_call_count += 1
self.last_debug_date = current_date
# Log Update() frequency
if self.update_call_count <= 5 or self.update_call_count % 20 == 0:
algorithm.Debug(f"[DefenseAlpha|DEBUG] Update() called {self.update_call_count} times total, date={current_date}")
# Rebalance on first trading day of month
current_month = algorithm.Time.month
if current_month != self.last_rebalance_month:
self.last_rebalance_month = current_month
# Debug: Check window state before rebalancing
self._debug_window_state(algorithm)
return self._generate_insights(algorithm)
return []
def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
for added in changes.AddedSecurities:
symbol = added.Symbol
# Only track Defense symbols
if symbol.Value not in self.defensive_symbols and symbol.Value != self.upro_symbol:
continue
self.symbols.append(symbol)
# Create rolling window for momentum calculation (253 days = max period + 1)
self.history_windows[symbol] = RollingWindow[float](max(self.momentum_periods) + 1)
# Warm up history with DAILY bars
history = algorithm.History(symbol, max(self.momentum_periods) + 1, Resolution.Daily)
if not history.empty:
for time, row in history.iterrows():
self.history_windows[symbol].Add(float(row['close']))
algorithm.Debug(f"[DefenseAlpha] {symbol.Value}: Warmed up with {len(history)} daily bars")
# Create Daily consolidator for this symbol
# This ensures we get DAILY bars regardless of subscription resolution
consolidator = TradeBarConsolidator(timedelta(days=1))
# Use closure to capture symbol
def create_handler(sym):
return lambda sender, bar: self._on_daily_bar(algorithm, sym, bar)
consolidator.DataConsolidated += create_handler(symbol)
algorithm.SubscriptionManager.AddConsolidator(symbol, consolidator)
self.consolidators[symbol] = consolidator
algorithm.Debug(f"[DefenseAlpha] {symbol.Value}: Daily consolidator added")
for removed in changes.RemovedSecurities:
symbol = removed.Symbol
if symbol in self.symbols:
self.symbols.remove(symbol)
if symbol in self.history_windows:
del self.history_windows[symbol]
if symbol in self.consolidators:
# Cleanup consolidator
algorithm.SubscriptionManager.RemoveConsolidator(symbol, self.consolidators[symbol])
del self.consolidators[symbol]
def _on_daily_bar(self, algorithm: QCAlgorithm, symbol: Symbol, bar: TradeBar) -> None:
"""
Called when a daily bar is consolidated
Updates history window with DAILY close price
"""
if symbol in self.history_windows:
self.history_windows[symbol].Add(float(bar.Close))
# Debug: Log first few daily bar updates
if not hasattr(self, '_daily_bar_count'):
self._daily_bar_count = {}
if symbol not in self._daily_bar_count:
self._daily_bar_count[symbol] = 0
self._daily_bar_count[symbol] += 1
if self._daily_bar_count[symbol] <= 3:
algorithm.Debug(f"[DefenseAlpha|DEBUG] {symbol.Value}: Daily bar #{self._daily_bar_count[symbol]} @ {bar.Time}, Close={bar.Close:.2f}")
def _debug_window_state(self, algorithm: QCAlgorithm) -> None:
"""
Debug: Log window state at rebalance time
"""
algorithm.Debug(f"[DefenseAlpha|DEBUG] === WINDOW STATE AT REBALANCE ({algorithm.Time.date()}) ===")
for symbol in self.symbols:
if symbol.Value in self.defensive_symbols:
if symbol in self.history_windows:
window = self.history_windows[symbol]
if window.IsReady and window.Count >= 252:
algorithm.Debug(f"[DefenseAlpha|DEBUG] {symbol.Value}: "
f"window[0]=${window[0]:.2f}, "
f"window[21]=${window[21]:.2f}, "
f"window[252]=${window[252]:.2f}, "
f"Count={window.Count}")
else:
algorithm.Debug(f"[DefenseAlpha|DEBUG] {symbol.Value}: NOT READY (Count={window.Count})")
else:
algorithm.Debug(f"[DefenseAlpha|DEBUG] {symbol.Value}: NO WINDOW")
def _compute_momentum_score(self, symbol: Symbol) -> float:
"""
Compute average momentum across multiple periods (21, 63, 126, 252 days)
Returns None if not ready
"""
if symbol not in self.history_windows:
return None
window = self.history_windows[symbol]
if not window.IsReady:
return None
current_price = window[0]
momentum_values = []
for period in self.momentum_periods:
if window.Count > period:
past_price = window[period]
if past_price > 0:
momentum = (current_price - past_price) / past_price
momentum_values.append(momentum)
if momentum_values:
return sum(momentum_values) / len(momentum_values)
return None
def _generate_insights(self, algorithm: QCAlgorithm) -> List[Insight]:
"""
Generate insights with tags for PCM to parse
Tag format: "Defense|Rank{N}|Score{X}"
"""
insights = []
period = timedelta(days=365) # Long period - FLAT insights will close positions
# Step 1: Calculate momentum for all defensive assets
momentum_scores = {}
for symbol in self.symbols:
if symbol.Value in self.defensive_symbols:
score = self._compute_momentum_score(symbol)
if score is not None:
momentum_scores[symbol] = score
if not momentum_scores:
algorithm.Debug("[DefenseAlpha] No momentum scores available, skipping rebalance")
return insights
# Step 2: Rank by momentum (best to worst)
sorted_assets = sorted(momentum_scores.items(), key=lambda x: x[1], reverse=True)
# Debug: Log all momentum scores
algorithm.Debug(f"[DefenseAlpha] === MOMENTUM SCORES (Month {algorithm.Time.month}) ===")
for symbol, score in sorted_assets:
pass_fail = "✓ PASS" if score > self.risk_free_monthly else "✗ FAIL"
algorithm.Debug(f"[DefenseAlpha] {symbol.Value}: {score:.6f} ({pass_fail}, threshold={self.risk_free_monthly:.6f})")
# Step 3: Track new allocation
new_allocation: Set[Symbol] = set()
upro_ranks = [] # Collect ranks that should go to UPRO
# Step 4: Generate insights for top 4 defensive assets
for i, (symbol, momentum_score) in enumerate(sorted_assets[:4]):
rank = i + 1 # Rank 1-4
if momentum_score > self.risk_free_monthly:
# Defensive asset has good momentum - allocate
tag = f"Defense|Rank{rank}|Score{momentum_score:.4f}"
insight = Insight.Price(symbol, period, InsightDirection.Up)
insight.Tag = tag
insights.append(insight)
new_allocation.add(symbol)
algorithm.Debug(f"[DefenseAlpha] {symbol.Value}: Rank {rank}, Score {momentum_score:.4f} (UP)")
else:
# Momentum weak - this rank goes to UPRO
upro_ranks.append(rank)
algorithm.Debug(f"[DefenseAlpha] {symbol.Value}: Rank {rank}, Score {momentum_score:.4f} (WEAK → UPRO)")
# Step 5: Allocate UPRO if any ranks were weak
if upro_ranks:
upro_symbol = None
for symbol in self.symbols:
if symbol.Value == self.upro_symbol:
upro_symbol = symbol
break
if upro_symbol:
# Tag with all ranks going to UPRO
ranks_str = ",".join([f"Rank{r}" for r in upro_ranks])
tag = f"Defense|{ranks_str}|Fallback"
insight = Insight.Price(upro_symbol, period, InsightDirection.Up)
insight.Tag = tag
insights.append(insight)
new_allocation.add(upro_symbol)
algorithm.Debug(f"[DefenseAlpha] UPRO: {ranks_str} (Fallback)")
# Step 6: Emit FLAT insights for positions we're exiting
for symbol in self.current_allocation:
if symbol not in new_allocation:
# Position being closed
flat_insight = Insight.Price(symbol, period, InsightDirection.Flat)
flat_insight.Tag = "Defense|Exit"
insights.append(flat_insight)
algorithm.Debug(f"[DefenseAlpha] {symbol.Value}: FLAT (Exit)")
# Update tracking
self.current_allocation = new_allocation
algorithm.Debug(f"[DefenseAlpha] Rebalance complete: {len(insights)} insights "
f"({len([i for i in insights if i.Direction == InsightDirection.Up])} UP, "
f"{len([i for i in insights if i.Direction == InsightDirection.Flat])} FLAT)")
return insights
from AlgorithmImports import *
from typing import List, Dict
from collections import OrderedDict
import numpy as np
from datetime import timedelta
from models.utils.fixed_size_dict import FixedSizeDict
from models.utils.vwap_calculator import VWAPCalculator
class IntradayMomentumAlphaModel(AlphaModel):
"""
Intraday Momentum Alpha Model - Copied from IntradayMomentumSP500Framework
Strategy:
- VWAP-based breakout detection
- Historical move analysis (90-day lookback)
- 30-minute consolidation period
- Max 1 trade per day
- Tag-based insights for PCM integration
ONLY CHANGE vs Original: Added tag to insights (Line ~249)
"""
def __init__(self,
period: int = 90,
min_volatility: float = 0.008,
consolidation_period: int = 30):
self.period = period
self.min_volatility = min_volatility
self.consolidation_period = consolidation_period
self.abs_move: Dict[datetime.date, List] = FixedSizeDict(max_size=period)
self.volatility_window = RollingWindow[float](10)
self.daily_returns = RollingWindow[float](period)
self.consolidators = {}
self.vwap_calculators = {}
self.current_opens = {}
self.prev_closes = {}
self.is_ready = False
self.trades_today = {}
def Update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
insights = []
if not self.is_ready:
return insights
for symbol in data.Keys:
if symbol in self.consolidators:
bar = data[symbol]
if bar is not None and bar.Volume > 0:
self.vwap_calculators[symbol].update(bar.Close, bar.Volume)
return insights
def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
for security in changes.AddedSecurities:
symbol = security.Symbol
# Only process SPY for intraday trading
if symbol.Value != "SPY":
continue
self.vwap_calculators[symbol] = VWAPCalculator()
self.current_opens[symbol] = None
self.prev_closes[symbol] = None
self.trades_today[symbol] = 0
consolidator = TradeBarConsolidator(timedelta(minutes=self.consolidation_period))
consolidator.DataConsolidated += lambda sender, bar, s=symbol: self._on_data_consolidated(algorithm, s, bar)
algorithm.SubscriptionManager.AddConsolidator(symbol, consolidator)
self.consolidators[symbol] = consolidator
daily_consolidator = TradeBarConsolidator(timedelta(days=1))
daily_consolidator.DataConsolidated += lambda sender, bar, s=symbol: self._on_daily_data(algorithm, s, bar)
algorithm.SubscriptionManager.AddConsolidator(symbol, daily_consolidator)
algorithm.Schedule.On(
algorithm.DateRules.EveryDay(symbol),
algorithm.TimeRules.At(10, 0),
lambda s=symbol: self._after_open(algorithm, s)
)
algorithm.Schedule.On(
algorithm.DateRules.EveryDay(symbol),
algorithm.TimeRules.BeforeMarketOpen(symbol, 1),
lambda s=symbol: self._before_market_open(algorithm, s)
)
self._warm_up(algorithm, symbol)
for security in changes.RemovedSecurities:
symbol = security.Symbol
if symbol in self.consolidators:
algorithm.SubscriptionManager.RemoveConsolidator(symbol, self.consolidators[symbol])
del self.consolidators[symbol]
del self.vwap_calculators[symbol]
del self.current_opens[symbol]
del self.prev_closes[symbol]
del self.trades_today[symbol]
def _warm_up(self, algorithm: QCAlgorithm, symbol: Symbol) -> None:
try:
history = algorithm.History(symbol, self.period + 10, Resolution.DAILY)
if not history.empty:
for row in history.itertuples():
if row.open != 0:
daily_return = row.close / row.open - 1
daily_volatility = (row.high - row.low) / row.open
self.daily_returns.Add(daily_return)
self.volatility_window.Add(daily_volatility)
except Exception as e:
algorithm.Debug(f"Fehler bei Tagesdaten-Warm-up für {symbol}: {e}")
self._warm_up_abs_move(algorithm, symbol)
self.is_ready = self.daily_returns.IsReady and len(self.abs_move) >= self.period
if self.is_ready:
algorithm.Debug("[IntradayAlpha] Warm-up complete")
def _warm_up_abs_move(self, algorithm: QCAlgorithm, symbol: Symbol) -> None:
try:
history = algorithm.History(symbol, timedelta(days=self.period * 2), Resolution.Minute)
if history.empty:
algorithm.Debug(f"[IntradayAlpha] No minute data for abs_move warm-up: {symbol}")
return
except Exception as e:
algorithm.Debug(f"[IntradayAlpha] Error in abs_move warm-up for {symbol}: {e}")
return
grouped_by_day = {}
for time_index, row in history.loc[symbol].iterrows():
dt = time_index.to_pydatetime()
date = dt.date()
if date not in grouped_by_day:
grouped_by_day[date] = []
grouped_by_day[date].append((dt, row["close"]))
abs_move_complete = OrderedDict()
for date, data in grouped_by_day.items():
data.sort()
if len(data) < 30:
continue
session_open = data[0][1]
current_bar_time = None
current_window = []
moves_for_day = []
for dt, price in data:
rounded_time = dt.replace(minute=(dt.minute // self.consolidation_period) * self.consolidation_period, second=0, microsecond=0)
if current_bar_time is None:
current_bar_time = rounded_time
if rounded_time == current_bar_time:
current_window.append(price)
else:
if current_window:
close_price = current_window[-1]
move = abs(close_price / session_open - 1)
moves_for_day.append((current_bar_time, move))
current_bar_time = rounded_time
current_window = [price]
if current_window:
close_price = current_window[-1]
move = abs(close_price / session_open - 1)
moves_for_day.append((current_bar_time, move))
if moves_for_day:
abs_move_complete[date] = moves_for_day
if len(abs_move_complete) >= self.period:
break
for date, move_list in abs_move_complete.items():
self.abs_move[date] = move_list
algorithm.Debug(f"[IntradayAlpha] Warm-up complete: {len(self.abs_move)} days loaded")
def _after_open(self, algorithm: QCAlgorithm, symbol: Symbol) -> None:
self.trades_today[symbol] = 0
if symbol in algorithm.Securities:
self.current_opens[symbol] = algorithm.Securities[symbol].Open
def _before_market_open(self, algorithm: QCAlgorithm, symbol: Symbol) -> None:
if symbol in algorithm.Securities:
self.prev_closes[symbol] = algorithm.Securities[symbol].Close
if symbol in self.vwap_calculators:
self.vwap_calculators[symbol].reset()
def _on_daily_data(self, algorithm: QCAlgorithm, symbol: Symbol, bar: TradeBar) -> None:
if bar.Open != 0:
daily_volatility = (bar.High - bar.Low) / bar.Open
self.volatility_window.Add(daily_volatility)
def _on_data_consolidated(self, algorithm: QCAlgorithm, symbol: Symbol, consolidated_bar: TradeBar) -> None:
if not self.is_ready:
return
if not self.current_opens.get(symbol):
return
if self.volatility_window.Count < 10:
return
vol10 = sum(self.volatility_window) / self.volatility_window.Count
if vol10 < self.min_volatility:
return
current_time = algorithm.Time
if current_time.date() not in self.abs_move:
self.abs_move[current_time.date()] = []
self.abs_move[current_time.date()].append((
current_time,
abs(consolidated_bar.Close / self.current_opens[symbol] - 1)
))
if len(self.abs_move) < self.period:
return
avg_move = [
move for date, moves in self.abs_move.items()
if date != current_time.date()
for time, move in moves
if time.hour == current_time.hour and time.minute == current_time.minute
]
if not avg_move:
return
mean_move = np.mean(avg_move)
prev_close = self.prev_closes.get(symbol)
session_open = self.current_opens.get(symbol)
if not prev_close or not session_open:
return
upper_bound = max(session_open, prev_close) * (1 + mean_move)
lower_bound = min(session_open, prev_close) * (1 - mean_move)
vwap_value = self.vwap_calculators[symbol].get_vwap()
if vwap_value is None:
vwap_value = lower_bound
if self.trades_today.get(symbol, 0) >= 1:
return
trade_direction = None
confidence = 0.5
duration = timedelta(minutes=self.consolidation_period)
if consolidated_bar.Close > upper_bound:
trade_direction = InsightDirection.Up
confidence = min(1.0, (consolidated_bar.Close - upper_bound) / upper_bound)
direction_str = "Upper"
elif consolidated_bar.Close < lower_bound:
trade_direction = InsightDirection.Down
confidence = min(1.0, (lower_bound - consolidated_bar.Close) / lower_bound)
direction_str = "Lower"
if trade_direction is not None:
# **ONLY CHANGE vs Original: Add Tag to Insight**
tag = f"Intraday|Breakout|{direction_str}|Strength{confidence:.2f}"
insight = Insight.Price(symbol, duration, trade_direction, confidence)
insight.Tag = tag # Tag added for Multi-Alpha PCM
algorithm.EmitInsights([insight])
self.trades_today[symbol] = self.trades_today.get(symbol, 0) + 1
algorithm.Debug(f"[IntradayAlpha] {symbol.Value}: {direction_str} breakout, confidence {confidence:.2f}")
from AlgorithmImports import *
from datetime import date, timedelta
from typing import List, Set
class SeasonalAlphaModel(AlphaModel):
"""
Seasonal Alpha Model - Calendar-based equity rotation
Strategy:
- Hold specific assets during their favorable calendar periods
- Equal-weight allocation (PCM handles via counting UP insights)
- SHY fallback when no assets in hold period
- Forever Period: Insights never expire, explicit FLAT on exits
- Subscribes to SPY Daily Consolidator (from Universe Model)
"""
def __init__(self):
self.symbols_map = {}
# Hold periods: (start_month, start_day, end_month, end_day)
self.hold_periods = {
"SPY": [(10, 1, 1, 31)], # Oct 1 - Jan 31
"QQQ": [(3, 11, 7, 20)], # Mar 11 - Jul 20
"IWM": [(10, 28, 11, 25)], # Oct 28 - Nov 25
"XLY": [(5, 20, 7, 31), (11, 1, 11, 30)], # May 20 - Jul 31, Nov 1-30
"XLK": [(1, 1, 2, 15), (5, 16, 7, 22)], # Jan 1 - Feb 15, May 16 - Jul 22
# "GLD": [(1, 1, 1, 31), (2, 1, 2, 28)], # REMOVED TO TEST DRAWDOWN EFFECT - GLD only in Defense now
"GER40": [(3, 15, 6, 5), (11, 1, 11, 30)], # Mar 15 - Jun 5, Nov 1-30 (DAX - matching Original)
"MTUM": [], # No hold period
"FDN": [], # No hold period
"ARKK": [(5, 13, 7, 21), (11, 1, 11, 30)], # May 13 - Jul 21, Nov 1-30
"DBA": [], # No hold period
"IHF": [(4, 1, 4, 30), (5, 1, 5, 31)], # Apr 1-30, May 1-31
"SLV": [(1, 1, 2, 20), (6, 20, 9, 1)], # Jan 1 - Feb 20, Jun 20 - Sep 1
"EWQ": [], # No hold period
"EVP": [], # No hold period
"XLE": [(3, 20, 4, 30), (9, 20, 11, 20)], # Mar 20 - Apr 30, Sep 20 - Nov 20
"EWU": [], # No hold period
"XLB": [(6, 20, 7, 28), (10, 1, 11, 5)], # Jun 20 - Jul 28, Oct 1 - Nov 5
"XRT": [(11, 1, 11, 30)], # Nov 1-30
"EWC": [] # No hold period
}
self.fallback_symbol = "SHY"
# Track current holdings to emit FLAT on exits
self.current_holdings: Set[str] = set()
# Track last check to avoid duplicate processing
self.last_check_date = None
# Subscribe to SPY Daily Consolidator
self.spy_daily_ready = False
def Update(self, algorithm: QCAlgorithm, data: Slice) -> List[Insight]:
"""
Check daily if holdings should change
Only emit insights when actual change detected
"""
insights = []
current_date = algorithm.Time.date()
if algorithm.IsWarmingUp:
return insights
# Only check once per day
if self.last_check_date == current_date:
return insights
self.last_check_date = current_date
# Determine which assets should be held today
assets_to_hold = self._get_assets_to_hold(current_date)
# Only generate insights if holdings changed
if assets_to_hold != self.current_holdings:
algorithm.Debug(f"[SeasonalAlpha] {current_date}: Holdings changing from {self.current_holdings} to {assets_to_hold}")
insights = self._generate_rebalance_insights(algorithm, assets_to_hold)
self.current_holdings = assets_to_hold
return insights
def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
"""
Map symbol objects to ticker strings
NOTE: SPY Daily Consolidator disabled - Seasonal works with Minute data directly
"""
for added in changes.AddedSecurities:
symbol = added.Symbol
symbol_str = symbol.Value
self.symbols_map[symbol_str] = symbol
for removed in changes.RemovedSecurities:
symbol = removed.Symbol
keys_to_remove = [k for k, v in self.symbols_map.items() if v == symbol]
for key in keys_to_remove:
del self.symbols_map[key]
def _get_assets_to_hold(self, current_date: date) -> Set[str]:
"""Determine which assets should be held on this date"""
assets_to_hold = set()
for asset_name, periods in self.hold_periods.items():
for start_month, start_day, end_month, end_day in periods:
if self._is_in_holding_period(current_date, start_month, start_day, end_month, end_day):
assets_to_hold.add(asset_name)
break
return assets_to_hold
def _is_in_holding_period(self, current_date: date, start_month: int, start_day: int,
end_month: int, end_day: int) -> bool:
"""
Check if current date falls within the holding period
IMPORTANT: Using ORIGINAL logic from Non-Framework code (Line 73-75)
This logic is BUGGY for year-spanning periods but we replicate it exactly
to match Original's behavior!
"""
try:
year = current_date.year
# ORIGINAL LOGIC (EXACT REPLICATION)
start_date = date(year, start_month, start_day)
end_date = date(year, end_month, end_day)
return start_date <= current_date <= end_date
except ValueError:
# Invalid date (e.g., Feb 30)
return False
def _generate_rebalance_insights(self, algorithm: QCAlgorithm, assets_to_hold: Set[str]) -> List[Insight]:
"""
Generate insights with tags for PCM to parse
Tag format: "Seasonal|HoldPeriod|{SYMBOL}|{DATES}"
Period: Forever (timedelta.max)
"""
algorithm.Debug(f"[TIMING|SeasonalAlpha] {algorithm.Time}: _generate_rebalance_insights STARTED")
insights = []
period = timedelta(days=365) # Long period - FLAT insights will close positions
# Step 1: Emit FLAT insights for symbols we're exiting
symbols_to_exit = self.current_holdings - assets_to_hold
for asset_name in symbols_to_exit:
if asset_name in self.symbols_map:
symbol = self.symbols_map[asset_name]
flat_insight = Insight.Price(symbol, period, InsightDirection.Flat)
flat_insight.Tag = "Seasonal|Exit"
insights.append(flat_insight)
algorithm.Debug(f"[SeasonalAlpha] FLAT: Exiting {asset_name}")
# Handle fallback exit
if len(self.current_holdings) == 0 and len(assets_to_hold) > 0:
# Exiting fallback (SHY)
if self.fallback_symbol in self.symbols_map:
fallback_symbol = self.symbols_map[self.fallback_symbol]
flat_insight = Insight.Price(fallback_symbol, period, InsightDirection.Flat)
flat_insight.Tag = "Seasonal|Exit|Fallback"
insights.append(flat_insight)
algorithm.Debug(f"[SeasonalAlpha] FLAT: Exiting fallback {self.fallback_symbol}")
# Step 2: Emit UP insights for new holdings
if assets_to_hold:
for asset_name in assets_to_hold:
if asset_name in self.symbols_map:
symbol = self.symbols_map[asset_name]
# Find which period we're in (for tag)
period_str = self._get_period_string(asset_name, algorithm.Time.date())
tag = f"Seasonal|HoldPeriod|{asset_name}|{period_str}"
insight = Insight.Price(symbol, period, InsightDirection.Up)
insight.Tag = tag
insights.append(insight)
algorithm.Debug(f"[SeasonalAlpha] UP: {asset_name} ({period_str})")
else:
algorithm.Debug(f"[SeasonalAlpha] WARNING: {asset_name} not in symbol map")
else:
# Fallback to SHY if no assets should be held
if self.fallback_symbol in self.symbols_map:
fallback_symbol = self.symbols_map[self.fallback_symbol]
tag = "Seasonal|Fallback|SHY"
insight = Insight.Price(fallback_symbol, period, InsightDirection.Up)
insight.Tag = tag
insights.append(insight)
algorithm.Debug(f"[SeasonalAlpha] UP: Fallback to {self.fallback_symbol}")
algorithm.Debug(f"[SeasonalAlpha] Generated {len(insights)} insights ({len([i for i in insights if i.Direction == InsightDirection.Up])} UP, {len([i for i in insights if i.Direction == InsightDirection.Flat])} FLAT)")
algorithm.Debug(f"[TIMING|SeasonalAlpha] {algorithm.Time}: _generate_rebalance_insights COMPLETED, returning {len(insights)} insights")
return insights
def _get_period_string(self, asset_name: str, current_date: date) -> str:
"""Get human-readable period string for tag"""
if asset_name not in self.hold_periods:
return "Unknown"
for start_month, start_day, end_month, end_day in self.hold_periods[asset_name]:
if self._is_in_holding_period(current_date, start_month, start_day, end_month, end_day):
return f"{start_month:02d}/{start_day:02d}-{end_month:02d}/{end_day:02d}"
return "Unknown"
from AlgorithmImports import *
from typing import List
class TaggedImmediateExecutionModel(ExecutionModel):
"""
OPTIMIZED Execution Model that preserves Alpha source tags in Order tags
Performance: O(1) instead of O(n) - no iteration through all insights!
Uses cached symbol_to_alpha mapping from main.py OnOrderEvent
This allows us to track which Alpha Model generated each order:
- Defense orders tagged with "Defense"
- Seasonal orders tagged with "Seasonal"
- Intraday orders tagged with "Intraday"
"""
def __init__(self):
self.targets_collection = PortfolioTargetCollection()
def Execute(self, algorithm: QCAlgorithm, targets: List[IPortfolioTarget]) -> None:
"""
Execute portfolio targets immediately with tags
OPTIMIZED: Uses algorithm.symbol_to_alpha (cached in OnOrderEvent)
instead of iterating through ALL insights every time!
Performance: O(targets) instead of O(all_insights)
"""
# Update targets collection
self.targets_collection.AddRange(targets)
# Execute orders with tags
for target in self.targets_collection.OrderByMarginImpact(algorithm):
# Get existing holdings
existing = algorithm.Securities[target.Symbol].Holdings.Quantity
# Calculate quantity to order
quantity = target.Quantity - existing
if quantity == 0:
continue
# Get alpha source tag from cached mapping (FAST!)
# If not in cache, determine from current insights (fallback)
alpha_tag = None
if hasattr(algorithm, 'symbol_to_alpha'):
alpha_tag = algorithm.symbol_to_alpha.get(target.Symbol, None)
if not alpha_tag:
# Fallback: Check current insights for this specific symbol only
for insight in algorithm.Insights:
if insight.Symbol == target.Symbol and not insight.IsExpired(algorithm.UtcTime):
if insight.Tag:
alpha_tag = insight.Tag.split('|')[0] if '|' in insight.Tag else insight.Tag
# Cache for next time (if dictionary exists)
if hasattr(algorithm, 'symbol_to_alpha'):
algorithm.symbol_to_alpha[target.Symbol] = alpha_tag
break
if not alpha_tag:
alpha_tag = "Unknown"
# Submit order with tag
ticket = algorithm.MarketOrder(target.Symbol, quantity, tag=alpha_tag)
if ticket:
algorithm.Debug(f"[Execution] {target.Symbol.Value}: {quantity} shares, Tag: {alpha_tag}")
# Clear processed targets
self.targets_collection.ClearFulfilled(algorithm)
# region imports
from AlgorithmImports import *
# endregion
# Portfolio Module
from .multi_alpha_pcm import MultiAlphaPortfolioConstructionModel
__all__ = ['MultiAlphaPortfolioConstructionModel']
from AlgorithmImports import *
from typing import Dict, List, Tuple
import numpy as np
class MultiAlphaPortfolioConstructionModel(PortfolioConstructionModel):
"""
Multi-Alpha Portfolio Construction Model
Budget Allocation:
- Defense: 40%
- Seasonal: 40%
- Intraday: 20%
Tag-based Allocation Logic:
- Defense: Parse Rank from tag, apply 40/30/20/10 weights (of 40% budget)
- Seasonal: Equal-weight across all UP insights (of 40% budget)
- Intraday: Volatility-based (of 20% budget)
SPY Conflict Resolution:
- If SPY in Seasonal Hold Period (Oct 1 - Jan 31): Seasonal takes priority
- Otherwise: Intraday can trade SPY
"""
def __init__(self, alpha_budgets: Dict[str, float] = None):
"""
Args:
alpha_budgets: Budget allocation per alpha model
Default: {"DefenseAlphaModel": 0.4, "SeasonalAlphaModel": 0.4, "IntradayMomentumAlphaModel": 0.2}
"""
if alpha_budgets is None:
self.alpha_budgets = {
"DefenseAlphaModel": 0.4,
"SeasonalAlphaModel": 0.4,
"IntradayMomentumAlphaModel": 0.2
}
else:
self.alpha_budgets = alpha_budgets
# Validate budgets sum to 1.0
budget_sum = sum(self.alpha_budgets.values())
if abs(budget_sum - 1.0) > 0.001:
raise ValueError(f"Alpha budgets must sum to 1.0, got {budget_sum}")
# Intraday: Volatility calculation (copied from Intraday PCM)
self.intraday_target_volatility = 0.02
self.intraday_max_leverage = 4.0
self.intraday_period = 90
self.daily_returns = {}
self.current_opens = {}
# Defense: Rank-based weights (40%, 30%, 20%, 10%)
self.defense_rank_weights = {1: 0.4, 2: 0.3, 3: 0.2, 4: 0.1}
def CreateTargets(self, algorithm: QCAlgorithm, insights: List[Insight]) -> List[PortfolioTarget]:
"""
Main entry point: Convert insights to portfolio targets
Steps:
1. Group insights by alpha model (via tag parsing)
2. Allocate Defense (rank-based)
3. **RISK-OFF MODE:** Check if Defense is fully defensive → Pause Seasonal
4. Allocate Seasonal (equal-weight) - only if NOT in Risk-Off Mode
5. Allocate Intraday (volatility-based)
6. Merge weights
7. **Normalize NET exposure** to ensure Total ≤ 100% (matching Original)
8. Resolve SPY conflicts
9. Convert weights to quantities
"""
algorithm.Debug(f"[TIMING|PCM] {algorithm.Time}: CreateTargets STARTED with {len(insights)} insights")
targets = []
if not insights:
algorithm.Debug(f"[TIMING|PCM] {algorithm.Time}: CreateTargets COMPLETED (no insights), returning 0 targets")
return targets
# Step 1: Group insights by alpha model
grouped = self._group_insights_by_alpha(insights)
# Debug
algorithm.Debug(f"[PCM] Processing {len(insights)} insights: Defense={len(grouped.get('Defense', []))}, Seasonal={len(grouped.get('Seasonal', []))}, Intraday={len(grouped.get('Intraday', []))}")
# Step 2: Calculate Defense weights
defense_weights = self._allocate_defense(algorithm, grouped.get('Defense', []))
# Step 3: **RISK-OFF MODE CHECK**
risk_off_mode = self._is_risk_off_mode(algorithm, defense_weights)
if risk_off_mode:
algorithm.Debug(f"[PCM] ⚠️ RISK-OFF MODE ACTIVE - Defense is fully defensive, Seasonal PAUSED")
# Step 4: Calculate Seasonal weights (skip if Risk-Off)
if risk_off_mode:
seasonal_weights = {} # PAUSE Seasonal during crisis
else:
seasonal_weights = self._allocate_seasonal(algorithm, grouped.get('Seasonal', []))
# Step 5: Calculate Intraday weights
intraday_weights = self._allocate_intraday(algorithm, grouped.get('Intraday', []))
# Step 3: Merge weights
all_weights = {}
for symbol, weight in defense_weights.items():
all_weights[symbol] = all_weights.get(symbol, 0.0) + weight
for symbol, weight in seasonal_weights.items():
all_weights[symbol] = all_weights.get(symbol, 0.0) + weight
for symbol, weight in intraday_weights.items():
all_weights[symbol] = all_weights.get(symbol, 0.0) + weight
# Step 3.5: Normalize to ensure NET exposure ≤ 100% (matching Original behavior)
all_weights = self._normalize_net_exposure(algorithm, all_weights)
# Step 4: Resolve SPY conflicts
all_weights = self._resolve_conflicts(algorithm, all_weights, seasonal_weights, intraday_weights)
# Step 5: Convert to portfolio targets
for symbol, weight in all_weights.items():
quantity = self._weight_to_quantity(algorithm, symbol, weight)
targets.append(PortfolioTarget(symbol, quantity))
algorithm.Debug(f"[PCM] Target: {symbol.Value} = {quantity} shares (weight={weight:.4f})")
algorithm.Debug(f"[TIMING|PCM] {algorithm.Time}: CreateTargets COMPLETED, returning {len(targets)} targets")
return targets
def OnSecuritiesChanged(self, algorithm: QCAlgorithm, changes: SecurityChanges) -> None:
"""
Initialize tracking for new securities
NOTE: Scheduling removed - was causing conflicts with main.py scheduling
"""
for security in changes.AddedSecurities:
symbol = security.Symbol
self._initialize_symbol(algorithm, symbol)
for security in changes.RemovedSecurities:
symbol = security.Symbol
if symbol in self.daily_returns:
del self.daily_returns[symbol]
if symbol in self.current_opens:
del self.current_opens[symbol]
# ========================================================================
# HELPER METHODS
# ========================================================================
def _group_insights_by_alpha(self, insights: List[Insight]) -> Dict[str, List[Insight]]:
"""
Group insights by alpha model based on tag prefix
Tag format: "Defense|...", "Seasonal|...", "Intraday|..."
"""
grouped = {'Defense': [], 'Seasonal': [], 'Intraday': []}
for insight in insights:
if not insight.Tag:
continue
if insight.Tag.startswith('Defense'):
grouped['Defense'].append(insight)
elif insight.Tag.startswith('Seasonal'):
grouped['Seasonal'].append(insight)
elif insight.Tag.startswith('Intraday'):
grouped['Intraday'].append(insight)
return grouped
def _allocate_defense(self, algorithm: QCAlgorithm, insights: List[Insight]) -> Dict[Symbol, float]:
"""
Defense Allocation: Rank-based (40%, 30%, 20%, 10%)
Tag format: "Defense|Rank{N}|Score{X}" or "Defense|Rank1,Rank2,...|Fallback" (UPRO)
Budget: 40% of total portfolio
"""
weights = {}
defense_budget = self.alpha_budgets.get("DefenseAlphaModel", 0.4)
for insight in insights:
if insight.Direction == InsightDirection.Flat:
# Exit signal - set to 0
weights[insight.Symbol] = 0.0
continue
# Parse tag for rank
tag = insight.Tag
parts = tag.split('|')
if 'Fallback' in tag:
# UPRO fallback - sum all ranks that went to UPRO
# Tag format: "Defense|Rank1,Rank2|Fallback"
rank_part = parts[1] # "Rank1,Rank2"
ranks = rank_part.split(',') # ["Rank1", "Rank2"]
upro_weight = 0.0
for rank_str in ranks:
rank_num = int(rank_str.replace('Rank', ''))
upro_weight += self.defense_rank_weights.get(rank_num, 0.0)
weights[insight.Symbol] = defense_budget * upro_weight
algorithm.Debug(f"[PCM] Defense UPRO: {ranks} → weight={weights[insight.Symbol]:.4f}")
else:
# Normal defense asset
# Tag format: "Defense|Rank{N}|Score{X}"
rank_part = parts[1] # "Rank1"
rank = int(rank_part.replace('Rank', ''))
rank_weight = self.defense_rank_weights.get(rank, 0.0)
weights[insight.Symbol] = defense_budget * rank_weight
algorithm.Debug(f"[PCM] Defense {insight.Symbol.Value}: Rank {rank} → weight={weights[insight.Symbol]:.4f}")
return weights
def _allocate_seasonal(self, algorithm: QCAlgorithm, insights: List[Insight]) -> Dict[Symbol, float]:
"""
Seasonal Allocation: Equal-weight
Tag format: "Seasonal|HoldPeriod|{SYMBOL}|{DATES}" or "Seasonal|Fallback|SHY"
Budget: 40% of total portfolio
"""
weights = {}
seasonal_budget = self.alpha_budgets.get("SeasonalAlphaModel", 0.4)
# Count UP insights
up_insights = [i for i in insights if i.Direction == InsightDirection.Up]
flat_insights = [i for i in insights if i.Direction == InsightDirection.Flat]
# FLAT insights - exit positions
for insight in flat_insights:
weights[insight.Symbol] = 0.0
# UP insights - equal-weight
if up_insights:
equal_weight = seasonal_budget / len(up_insights)
for insight in up_insights:
weights[insight.Symbol] = equal_weight
algorithm.Debug(f"[PCM] Seasonal {insight.Symbol.Value}: equal-weight → {equal_weight:.4f}")
return weights
def _allocate_intraday(self, algorithm: QCAlgorithm, insights: List[Insight]) -> Dict[Symbol, float]:
"""
Intraday Allocation: Volatility-based (copied from Intraday PCM)
Tag format: "Intraday|Breakout|{Direction}|Strength{X}"
Budget: 20% of total portfolio
"""
weights = {}
intraday_budget = self.alpha_budgets.get("IntradayMomentumAlphaModel", 0.2)
for insight in insights:
symbol = insight.Symbol
if insight.Direction == InsightDirection.Flat:
weights[symbol] = 0.0
continue
# Initialize if needed
if symbol not in self.daily_returns:
self._initialize_symbol(algorithm, symbol)
if not self._is_ready(symbol):
algorithm.Debug(f"[PCM] Intraday {symbol.Value}: not ready (volatility)")
continue
current_open = self.current_opens.get(symbol)
if not current_open or current_open == 0:
continue
# Calculate volatility
returns = list(self.daily_returns[symbol])
mean_return = np.mean(returns)
volatility = np.std(returns)
if volatility == 0:
continue
# Volatility-based leverage (capped)
leverage = min(self.intraday_max_leverage, self.intraday_target_volatility / volatility)
# Apply intraday budget limit
leverage_with_budget = leverage * intraday_budget
# Calculate weight
portfolio_value = algorithm.Portfolio.TotalPortfolioValue
if portfolio_value <= 0:
continue
quantity = int(portfolio_value * leverage_with_budget / current_open)
weight = quantity * current_open / portfolio_value
if insight.Direction == InsightDirection.Up:
weights[symbol] = weight
elif insight.Direction == InsightDirection.Down:
weights[symbol] = -weight
else:
weights[symbol] = 0.0
algorithm.Debug(f"[PCM] Intraday {symbol.Value}: vol={volatility:.4f}, leverage={leverage:.2f}, weight={weights[symbol]:.4f}")
return weights
def _is_risk_off_mode(self, algorithm: QCAlgorithm, defense_weights: Dict[Symbol, float]) -> bool:
"""
Risk-Off Mode Detection:
Returns True if Defense is holding ONLY defensive assets (TLT, GLD, DBC, UUP)
and NO UPRO (leveraged S&P500).
When Risk-Off = True → Seasonal should PAUSE to avoid equity exposure during crisis
Defensive Assets: TLT (bonds), GLD (gold), DBC (commodities), UUP (US dollar)
Risk Asset: UPRO (3x leveraged S&P500)
"""
defensive_symbols = {"TLT", "GLD", "DBC", "UUP"}
risk_symbol = "UPRO"
# Check what Defense is holding
defense_holdings = set()
for symbol, weight in defense_weights.items():
if weight > 0: # Currently allocated
defense_holdings.add(symbol.Value)
# Risk-Off if:
# 1. Defense has positions AND
# 2. NO UPRO AND
# 3. Only defensive assets
if not defense_holdings:
return False # No positions = not risk-off
has_upro = risk_symbol in defense_holdings
only_defensive = defense_holdings.issubset(defensive_symbols)
risk_off = (not has_upro) and only_defensive
if risk_off:
algorithm.Debug(f"[PCM] Risk-Off Detection: Holdings={defense_holdings}, UPRO={has_upro}, OnlyDefensive={only_defensive}")
return risk_off
def _normalize_net_exposure(self, algorithm: QCAlgorithm, all_weights: Dict[Symbol, float]) -> Dict[Symbol, float]:
"""
Normalize weights to ensure Total Allocation ≤ 100%
WICHTIG: Beide Strategien (Defense + Seasonal) sind Long-only!
Daher: Total Allocation = Summe aller Weights
Defense Budget: 60%
Seasonal Budget: 40%
Total: 100%
Wenn durch Timing, Rundung oder andere Faktoren Total > 100%:
→ Scale down ALL weights proportional
"""
if not all_weights:
return all_weights
# Calculate Total Allocation (Summe aller Weights)
# Bei Long-only: Total = Summe aller positiven Weights
total_allocation = sum(w for w in all_weights.values() if w > 0)
algorithm.Debug(f"[PCM] Total Allocation Before Normalization: {total_allocation:.2%}")
# If total allocation exceeds 100%, scale down proportionally
if total_allocation > 1.0:
scale_factor = 1.0 / total_allocation
normalized_weights = {symbol: weight * scale_factor for symbol, weight in all_weights.items()}
# Recalculate
new_total = sum(w for w in normalized_weights.values() if w > 0)
algorithm.Debug(f"[PCM] ⚠️ Total Allocation exceeded 100%! Scaled by {scale_factor:.3f}")
algorithm.Debug(f"[PCM] Total Allocation After Normalization: {new_total:.2%}")
return normalized_weights
return all_weights
def _resolve_conflicts(self, algorithm: QCAlgorithm, all_weights: Dict[Symbol, float],
seasonal_weights: Dict[Symbol, float], intraday_weights: Dict[Symbol, float]) -> Dict[Symbol, float]:
"""
SPY Conflict Resolution:
- If SPY in Seasonal Hold Period (Oct 1 - Jan 31): Seasonal takes priority
- Otherwise: Intraday takes priority
"""
# Find SPY symbol
spy_symbol = None
for symbol in all_weights.keys():
if symbol.Value == "SPY":
spy_symbol = symbol
break
if not spy_symbol:
return all_weights # No SPY, no conflict
# Check if both Seasonal and Intraday want SPY
has_seasonal = spy_symbol in seasonal_weights and seasonal_weights[spy_symbol] != 0
has_intraday = spy_symbol in intraday_weights and intraday_weights[spy_symbol] != 0
if not (has_seasonal and has_intraday):
return all_weights # No conflict
# Conflict detected - check hold period
if self._is_spy_seasonal_active(algorithm):
# Seasonal period active (Oct 1 - Jan 31) - Seasonal wins
all_weights[spy_symbol] = seasonal_weights[spy_symbol]
algorithm.Debug(f"[PCM] SPY Conflict: Seasonal priority (Hold Period active)")
else:
# Outside seasonal period - Intraday wins
all_weights[spy_symbol] = intraday_weights[spy_symbol]
algorithm.Debug(f"[PCM] SPY Conflict: Intraday priority (Hold Period inactive)")
return all_weights
def _is_spy_seasonal_active(self, algorithm: QCAlgorithm) -> bool:
"""
Check if SPY seasonal hold period is active (Oct 1 - Jan 31)
"""
current_date = algorithm.Time.date()
month = current_date.month
day = current_date.day
# Oct 1 - Dec 31
if month >= 10:
return True
# Jan 1 - Jan 31
if month == 1:
return True
return False
def _weight_to_quantity(self, algorithm: QCAlgorithm, symbol: Symbol, weight: float) -> int:
"""Convert weight to share quantity"""
if weight == 0:
return 0
portfolio_value = algorithm.Portfolio.TotalPortfolioValue
if portfolio_value <= 0:
return 0
price = algorithm.Securities[symbol].Price
if price <= 0:
return 0
quantity = int((weight * portfolio_value) / price)
return quantity
# ========================================================================
# INTRADAY VOLATILITY TRACKING (copied from Intraday PCM)
# ========================================================================
def _initialize_symbol(self, algorithm: QCAlgorithm, symbol: Symbol) -> None:
"""Initialize volatility tracking for symbol"""
self.daily_returns[symbol] = RollingWindow[float](self.intraday_period)
self.current_opens[symbol] = None
try:
history = algorithm.History(symbol, self.intraday_period + 10, Resolution.DAILY)
if not history.empty:
for row in history.itertuples():
if row.open != 0:
daily_return = row.close / row.open - 1
self.daily_returns[symbol].Add(daily_return)
except Exception as e:
algorithm.Debug(f"[PCM] Error in volatility warm-up for {symbol}: {e}")
def _update_open(self, algorithm: QCAlgorithm, symbol: Symbol) -> None:
"""Update opening price for volatility calculation"""
if symbol in algorithm.Securities:
self.current_opens[symbol] = algorithm.Securities[symbol].Open
def _update_daily_return(self, algorithm: QCAlgorithm, symbol: Symbol) -> None:
"""Update daily return for volatility calculation"""
if symbol in algorithm.Securities and symbol in self.current_opens:
current_price = algorithm.Securities[symbol].Price
current_open = self.current_opens[symbol]
if current_open and current_open != 0:
daily_return = current_price / current_open - 1
self.daily_returns[symbol].Add(daily_return)
def _is_ready(self, symbol: Symbol) -> bool:
"""Check if volatility data is ready"""
return (symbol in self.daily_returns and
self.daily_returns[symbol].IsReady)
# region imports
from AlgorithmImports import *
# endregion
# Risk Module
from .multi_alpha_risk_model import MultiAlphaRiskManagementModel
__all__ = ['MultiAlphaRiskManagementModel']
from AlgorithmImports import *
from typing import List
class MultiAlphaRiskManagementModel(RiskManagementModel):
"""
Multi-Alpha Risk Management Model
Simple portfolio-level risk filters:
- Max Total Leverage: 2.0 (200%)
- Max Single Position: 0.25 (25% of portfolio)
- Max Drawdown: 0.30 (30% emergency stop)
Philosophy: Simple, broad filters
- NOT complex per-alpha limits like Intraday Framework
- Scale down proportionally when limits exceeded
"""
def __init__(self,
max_total_leverage: float = 2.0,
max_single_position: float = 0.25,
max_drawdown: float = 0.30):
"""
Args:
max_total_leverage: Maximum total portfolio leverage (default 2.0 = 200%)
max_single_position: Maximum single position size as % of portfolio (default 0.25 = 25%)
max_drawdown: Maximum drawdown before emergency liquidation (default 0.30 = 30%)
"""
self.max_total_leverage = max_total_leverage
self.max_single_position = max_single_position
self.max_drawdown = max_drawdown
# Track highest portfolio value for drawdown calculation
self.highest_portfolio_value = 0.0
def ManageRisk(self, algorithm: QCAlgorithm, targets: List[PortfolioTarget]) -> List[PortfolioTarget]:
"""
Apply risk filters to portfolio targets
Steps:
1. Check drawdown - liquidate everything if exceeded
2. Check single position limits - scale down oversized positions
3. Check total leverage - scale down entire portfolio if needed
"""
if not targets:
return targets
portfolio_value = algorithm.Portfolio.TotalPortfolioValue
# Update highest value
if portfolio_value > self.highest_portfolio_value:
self.highest_portfolio_value = portfolio_value
# Step 1: Check for maximum drawdown breach
if self.highest_portfolio_value > 0:
current_drawdown = (self.highest_portfolio_value - portfolio_value) / self.highest_portfolio_value
if current_drawdown > self.max_drawdown:
algorithm.Debug(f"[Risk] MAX DRAWDOWN EXCEEDED: {current_drawdown:.2%} > {self.max_drawdown:.2%}")
algorithm.Debug(f"[Risk] EMERGENCY LIQUIDATION: Closing all positions")
# Emergency liquidation - flatten everything
return [PortfolioTarget(target.Symbol, 0) for target in targets]
# Step 2: Check single position limits
targets = self._limit_single_positions(algorithm, targets, portfolio_value)
# Step 3: Check total leverage
targets = self._limit_total_leverage(algorithm, targets, portfolio_value)
return targets
def _limit_single_positions(self, algorithm: QCAlgorithm, targets: List[PortfolioTarget],
portfolio_value: float) -> List[PortfolioTarget]:
"""
Scale down any positions exceeding max_single_position
"""
adjusted_targets = []
for target in targets:
symbol = target.Symbol
quantity = target.Quantity
if quantity == 0:
adjusted_targets.append(target)
continue
price = algorithm.Securities[symbol].Price
if price <= 0:
adjusted_targets.append(target)
continue
# Calculate position value
position_value = abs(quantity * price)
position_pct = position_value / portfolio_value
# Check if exceeds limit
if position_pct > self.max_single_position:
# Scale down to max limit
scale_factor = self.max_single_position / position_pct
new_quantity = int(quantity * scale_factor)
algorithm.Debug(f"[Risk] Single position limit exceeded: {symbol.Value} {position_pct:.2%} > {self.max_single_position:.2%}")
algorithm.Debug(f"[Risk] Scaling down {symbol.Value}: {quantity} → {new_quantity} shares")
adjusted_targets.append(PortfolioTarget(symbol, new_quantity))
else:
adjusted_targets.append(target)
return adjusted_targets
def _limit_total_leverage(self, algorithm: QCAlgorithm, targets: List[PortfolioTarget],
portfolio_value: float) -> List[PortfolioTarget]:
"""
Scale down entire portfolio if total leverage exceeds limit
"""
# Calculate total exposure
total_exposure = 0.0
for target in targets:
symbol = target.Symbol
quantity = target.Quantity
if quantity == 0:
continue
price = algorithm.Securities[symbol].Price
if price <= 0:
continue
total_exposure += abs(quantity * price)
# Calculate leverage
leverage = total_exposure / portfolio_value if portfolio_value > 0 else 0.0
# Check if exceeds limit
if leverage > self.max_total_leverage:
# Scale down entire portfolio proportionally
scale_factor = self.max_total_leverage / leverage
algorithm.Debug(f"[Risk] Total leverage exceeded: {leverage:.2f}x > {self.max_total_leverage:.2f}x")
algorithm.Debug(f"[Risk] Scaling down entire portfolio by {scale_factor:.2%}")
scaled_targets = []
for target in targets:
if target.Quantity == 0:
scaled_targets.append(target)
else:
new_quantity = int(target.Quantity * scale_factor)
scaled_targets.append(PortfolioTarget(target.Symbol, new_quantity))
return scaled_targets
return targets
# region imports
from AlgorithmImports import *
# endregion
# Universe Module
from AlgorithmImports import *
from datetime import timedelta
class MultiAlphaUniverseSelectionModel(ManualUniverseSelectionModel):
"""
Multi-Alpha Universe Selection Model
Symbole:
- Defense: TLT, GLD, DBC, UUP, UPRO (Daily)
- Seasonal: SPY, QQQ, IWM, XLY, XLK, GLD, MTUM, FDN, ARKK, SHY, DBA, IHF, SLV, EWQ, EVP, XLE, EWU, XLB, XRT, EWC, GER40 (Daily)
- Intraday: SPY (Minute)
SPY: Minute Resolution (für Intraday + Daily Consolidator für Seasonal)
Rest: Daily Resolution
"""
def __init__(self):
# Seasonal symbols only (Daily Resolution)
# NOTE: Defense symbols (TLT, GLD, DBC, UUP, UPRO) are added in main.py with Minute resolution!
daily_symbols = [
# Seasonal (GLD is in both Defense and Seasonal - added by main.py)
"QQQ", "IWM", "XLY", "XLK", "MTUM", "FDN", "ARKK",
"SHY", "DBA", "IHF", "SLV", "EWQ", "EVP", "XLE",
"EWU", "XLB", "XRT", "EWC",
"GER40" # DAX - German stock index (matching Original)
]
# Erstelle Symbol Objects (wird in Initialize später zu Securities)
symbols = [Symbol.Create(ticker, SecurityType.Equity, Market.USA) for ticker in daily_symbols]
super().__init__(symbols)
def add_spy_with_minute_resolution(self, algorithm):
"""
SPY separat hinzufügen mit Minute Resolution
Wird von main.py aufgerufen
NOTE: Daily Consolidator temporarily removed - was causing FATAL ERROR
Seasonal Alpha can work with Minute data directly
"""
if not hasattr(algorithm, '_spy_added'):
spy = algorithm.AddEquity("SPY", Resolution.Minute)
spy.SetDataNormalizationMode(DataNormalizationMode.Adjusted)
algorithm._spy_added = True
algorithm.Debug("[Universe] SPY added with Minute resolution (Daily consolidator disabled)")
return spy.Symbol
return None
# region imports
from AlgorithmImports import *
# endregion
# Utils Module
# region imports
from AlgorithmImports import *
# endregion
from collections import OrderedDict, deque
from typing import Deque
import datetime
class FixedSizeDict(OrderedDict):
def __init__(self, max_size: int) -> None:
self.max_size: int = max_size
super().__init__()
def __setitem__(self, key: datetime.date, value: Deque) -> None:
if len(self) >= self.max_size:
self.popitem(last=False)
super().__setitem__(key, value)
# region imports
from AlgorithmImports import *
# endregion
class VWAPCalculator:
def __init__(self):
self.cumulative_price_volume = 0.0
self.cumulative_volume = 0.0
self.current_vwap = None
def update(self, price: float, volume: float) -> None:
if volume > 0:
self.cumulative_price_volume += (price * volume)
self.cumulative_volume += volume
if self.cumulative_volume > 0:
self.current_vwap = self.cumulative_price_volume / self.cumulative_volume
def reset(self) -> None:
self.cumulative_price_volume = 0.0
self.cumulative_volume = 0.0
self.current_vwap = None
def get_vwap(self) -> float:
return self.current_vwap
from AlgorithmImports import *
class CombinedStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2015, 1, 1)
self.SetCash(100000)
# Verwende unterstütztes Brokerage-Modell
self.SetBrokerageModel(BrokerageName.INTERACTIVE_BROKERS_BROKERAGE, AccountType.MARGIN)
self.weight_defense = 0.6
self.weight_seasonal = 0.4
self.defense = DefenseFirstWithUPRO(self, self.weight_defense)
self.seasonal = SeasonalHoldPeriodStrategy(self, self.weight_seasonal)
chart = Chart("Capital")
chart.AddSeries(Series("Defense", SeriesType.Line, 0))
chart.AddSeries(Series("Seasonal", SeriesType.Line, 0))
chart.AddSeries(Series("Total", SeriesType.Line, 0))
self.AddChart(chart)
def OnData(self, data: Slice):
total_invested_ratio = self.Portfolio.TotalHoldingsValue / self.Portfolio.TotalPortfolioValue
self.Debug(f"{self.Time.date()}: Gesamt Investiert = {total_invested_ratio:.2%}")
for kvp in self.Portfolio:
if kvp.Value.Invested:
self.Debug(f" Holding: {kvp.Key} @ {kvp.Value.Quantity} (Value: {kvp.Value.HoldingsValue:.2f})")
self.defense.OnData(data)
self.seasonal.OnData(data)
self.Plot("Capital", "Total", total_invested_ratio)
self.Plot("Capital", "Defense", self.defense.GetCurrentExposure())
self.Plot("Capital", "Seasonal", self.seasonal.GetCurrentExposure())
def OnOrderEvent(self, orderEvent: OrderEvent):
if orderEvent.Status == OrderStatus.Filled:
self.Debug(f"[Order] {self.Time}: Filled {orderEvent.Symbol.Value}, Qty: {orderEvent.FillQuantity}, Price: {orderEvent.FillPrice}")
class SeasonalHoldPeriodStrategy:
def __init__(self, algo: QCAlgorithm, weight: float):
self.algo = algo
self.weight = weight
self.symbols = {
name: algo.AddEquity(name if name != "DAX" else "GER40", Resolution.Daily).Symbol
for name in [
"SPY", "QQQ", "IWM", "XLY", "XLK", "GLD", "MTUM", "FDN", "ARKK", "DAX",
"SHY", "DBA", "IHF", "SLV", "EWQ", "EVP", "XLE", "EWU", "XLB", "XRT", "EWC"
]
}
self.hold_periods = {
"SPY": [(10, 1, 1, 31)], "QQQ": [(3, 11, 7, 20)], "IWM": [(10, 28, 11, 25)],
"XLY": [(5, 20, 7, 31), (11, 1, 11, 30)], "XLK": [(1, 1, 2, 15), (5, 16, 7, 22)],
"GLD": [(1, 1, 1, 31), (2, 1, 2, 28)], "DAX": [(3, 15, 6, 5), (11, 1, 11, 30)],
"MTUM": [], "ARKK": [(5, 13, 7, 21), (11, 1, 11, 30)], "DBA": [], "IHF": [(4, 1, 4, 30), (5, 1, 5, 31)],
"SLV": [(1, 1, 2, 20), (6, 20, 9, 1)], "EWQ": [], "EVP": [], "XLE": [(3, 20, 4, 30), (9, 20, 11, 20)],
"EWU": [], "XLB": [(6, 20, 7, 28), (10, 1, 11, 5)], "XRT": [(11, 1, 11, 30)], "EWC": []
}
self.current_holdings = set()
def OnData(self, data: Slice):
today = self.algo.Time.date()
to_hold = set()
for name, periods in self.hold_periods.items():
for start_m, start_d, end_m, end_d in periods:
start = datetime(today.year, start_m, start_d).date()
end = datetime(today.year, end_m, end_d).date()
if start <= today <= end:
to_hold.add(name)
break
if to_hold != self.current_holdings:
for symbol in self.symbols.values():
if self.algo.Portfolio[symbol].Invested:
self.algo.Liquidate(symbol)
self.algo.Debug(f"[Seasonal] Liquidate {symbol.Value}")
if to_hold:
weight = self.weight / len(to_hold)
for name in to_hold:
symbol = self.symbols[name]
self.algo.SetHoldings(symbol, weight)
self.algo.Debug(f"[Seasonal] Holding {name} @ {weight:.2f}")
else:
fallback = self.symbols.get("SHY", None)
if fallback:
self.algo.SetHoldings(fallback, self.weight)
self.algo.Debug(f"[Seasonal] Fallback to SHY @ {self.weight:.2f}")
self.current_holdings = to_hold
def GetCurrentExposure(self):
exposure = sum([
self.algo.Portfolio[symbol].HoldingsValue
for symbol in self.symbols.values()
]) / self.algo.Portfolio.TotalPortfolioValue
return exposure
class DefenseFirstWithUPRO:
def __init__(self, algo: QCAlgorithm, weight: float):
self.algo = algo
self.weight = weight
self.defensive_symbols = ["TLT", "GLD", "DBC", "UUP"]
self.upro_symbol = "UPRO"
self.assets = [self.algo.AddEquity(ticker, Resolution.Daily).Symbol for ticker in self.defensive_symbols + [self.upro_symbol]]
self.defensive = self.assets[:4]
self.upro = self.assets[4]
self.momentum_periods = [21, 63, 126, 252]
self.history = {symbol: RollingWindow[float](max(self.momentum_periods) + 1) for symbol in self.assets}
self.last_rebalance_month = -1
history = self.algo.History(self.assets, max(self.momentum_periods) + 1, Resolution.Daily)
for symbol in self.assets:
if symbol in history.index.levels[0]:
for price in history.loc[symbol]["close"]:
self.history[symbol].Add(float(price))
self.algo.Schedule.On(self.algo.DateRules.MonthStart(self.upro),
self.algo.TimeRules.AfterMarketOpen(self.upro, 10),
self.Rebalance)
def OnData(self, data: Slice):
for symbol in self.assets:
if data.Bars.ContainsKey(symbol):
self.history[symbol].Add(float(data.Bars[symbol].Close))
def ComputeMomentumScore(self, symbol):
window = self.history[symbol]
if not window.IsReady:
return None
current = window[0]
momenta = [(current - window[p]) / window[p] for p in self.momentum_periods if window.Count > p]
return sum(momenta) / len(momenta) if momenta else None
def Rebalance(self):
if self.algo.Time.month == self.last_rebalance_month:
return
scores = {s: self.ComputeMomentumScore(s) for s in self.defensive}
scores = {k: v for k, v in scores.items() if v is not None}
sorted_assets = sorted(scores.items(), key=lambda x: x[1], reverse=True)
weights = [0.4, 0.3, 0.2, 0.1]
risk_free = 0.045 / 12
allocation = {}
for i, (symbol, score) in enumerate(sorted_assets):
if score > risk_free:
allocation[symbol] = weights[i]
else:
allocation[self.upro] = allocation.get(self.upro, 0) + weights[i]
total_weight = sum(allocation.values())
# Nur eigene Symbole liquidieren
for symbol in self.assets:
if self.algo.Portfolio[symbol].Invested and symbol not in allocation:
self.algo.Liquidate(symbol)
self.algo.Debug(f"[Defense] Liquidate {symbol.Value}")
for symbol, w in allocation.items():
target_pct = self.weight * (w / total_weight)
self.algo.Debug(f"[Defense] SetHoldings {symbol.Value} @ {target_pct:.2%}")
self.algo.SetHoldings(symbol, target_pct)
self.last_rebalance_month = self.algo.Time.month
def GetCurrentExposure(self):
exposure = sum([
self.algo.Portfolio[symbol].HoldingsValue
for symbol in self.assets
]) / self.algo.Portfolio.TotalPortfolioValue
return exposure
from AlgorithmImports import *
class MinimalTest(QCAlgorithm):
"""
Minimal test to check if basic framework works
"""
def Initialize(self):
self.SetStartDate(2020, 1, 1)
self.SetEndDate(2020, 12, 31)
self.SetCash(100000)
self.Debug("=== Minimal Test Started ===")
# Add single symbol
self.AddEquity("SPY", Resolution.Daily)
self.Debug("=== Minimal Test Initialized ===")
def OnData(self, data):
if not self.Portfolio.Invested:
self.SetHoldings("SPY", 1.0)
self.Debug("Bought SPY")