Problem Description
Solution
To implement a portfolio model that allocates a specific, fixed percentage (e.g., 5%) to every trade signal regardless of the number of active signals or total portfolio value, you need to create a custom Portfolio Construction Model.
Standard models (like EqualWeightingPortfolioConstructionModel) typically calculate weights based on the count of active insights to utilize 100% of the capital. To enforce a "hard" 5% limit per trade, we override the CreateTargets method.
Strategy Overview
- Custom Portfolio Model: We define
FixedFractionPortfolioConstructionModel. It takes afractionparameter (0.05). When it receives anInsight, it creates aPortfolioTargetof exactly that size. - Alpha Model: For demonstration, I will use a simple Momentum logic (2-day positive returns) to generate signals.
- Execution: We use
ImmediateExecutionModelto fill orders immediately.
Complete Strategy Code
# region imports
from AlgorithmImports import *
# endregion
class FixedAllocationStrategy(QCAlgorithm):
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_end_date(2023, 1, 1)
self.set_cash(100000)
# 1. Select a Universe of liquid ETFs to test the allocation
self.tickers = ["SPY", "QQQ", "IWM", "TLT", "GLD", "VNQ", "EEM", "LQD"]
self.symbols = [self.add_equity(ticker, Resolution.DAILY).symbol for ticker in self.tickers]
# 2. Set the Custom Portfolio Construction Model
# We pass 0.05 to allocate exactly 5% per signal
self.set_portfolio_construction(FixedFractionPortfolioConstructionModel(fraction=0.05))
# 3. Set Execution Model (Immediate fill)
self.set_execution(ImmediateExecutionModel())
# 4. Set a Null Alpha Model (We will emit insights manually for clarity,
# or you could use a framework Alpha model)
self.set_alpha(ConstantAlphaModel(InsightType.PRICE, InsightDirection.FLAT, timedelta(days=1)))
# Schedule a daily method to generate insights
self.schedule.on(self.date_rules.every_day("SPY"),
self.time_rules.after_market_open("SPY", 30),
self.generate_momentum_insights)
def generate_momentum_insights(self):
"""
Simple logic: If an asset is up for 2 consecutive days, go Long.
"""
insights = []
# Get history for all symbols
history = self.history(self.symbols, 3, Resolution.DAILY)
if history.empty: return
for symbol in self.symbols:
if symbol not in history.index.get_level_values(0): continue
# Get price series
closes = history.loc[symbol]['close']
if len(closes) < 3: continue
# Check 2-day momentum
# Today > Yesterday AND Yesterday > DayBefore
if closes.iloc[-1] > closes.iloc[-2] and closes.iloc[-2] > closes.iloc[-3]:
# Generate an Insight
# The Portfolio Model will see this and assign exactly 5%
insight = Insight.price(symbol, timedelta(days=5), InsightDirection.UP)
insights.append(insight)
# Emit insights to the framework
if len(insights) > 0:
self.emit_insights(insights)
class FixedFractionPortfolioConstructionModel(PortfolioConstructionModel):
"""
Custom Model: Allocates a fixed % of Total Portfolio Value to every active Insight.
"""
def __init__(self, fraction: float = 0.05):
super().__init__()
self.fraction = fraction
def CreateTargets(self, algorithm, insights):
"""
Called by the Engine when new insights are available.
Returns a list of PortfolioTargets.
"""
targets = []
for insight in insights:
# 1. Handle Flat/Neutral signals (Liquidate)
if insight.Direction == InsightDirection.FLAT:
targets.append(PortfolioTarget.percent(algorithm, insight.Symbol, 0.0))
continue
# 2. Determine direction (1 for Long, -1 for Short)
direction = 1 if insight.Direction == InsightDirection.UP else -1
# 3. Calculate fixed weight
# If fraction is 0.05, Long = 0.05, Short = -0.05
weight = direction * self.fraction
# 4. Create the target
targets.append(PortfolioTarget.percent(algorithm, insight.Symbol, weight))
return targets
Key Implementation Details
-
FixedFractionPortfolioConstructionModelClass:- This class inherits from
PortfolioConstructionModel. - The
CreateTargetsmethod is the core engine. It iterates through everyInsightreceived from your Alpha model. - Instead of calculating a weight based on the total number of insights (which would fluctuate), it applies the hardcoded
self.fraction(0.05) to every single target.
- This class inherits from
-
PortfolioTarget.percent:- This helper creates a target that tells the Execution model: "Make sure
Symbolrepresents X% of my total portfolio value." - If you have $100,000 and the fraction is 0.05, it will buy $5,000 worth of shares.
- This helper creates a target that tells the Execution model: "Make sure
-
Risk Note:
- This model does not check if you have enough cash. If you generate 25 signals at 5% each, you will attempt to allocate 125% of your equity, resulting in margin usage (leverage).
- If you wish to prevent leverage, you would need to add logic inside
CreateTargetsto checkalgorithm.Portfolio.MarginRemainingor cap the sum of targets.
Q&A: Portfolio Construction in QuantConnect
Q: How does this differ from EqualWeightingPortfolioConstructionModel?
A: The EqualWeighting model looks at all active insights and divides 100% by that count. If you have 2 signals, it allocates 50% each. If you have 50 signals, it allocates 2% each. The FixedFraction model above allocates 5% regardless of whether you have 1 signal or 20.
Q: What happens if I have existing positions?
A: The PortfolioTarget system is state-aware. If you already hold 5% of SPY and the model emits a new 5% target for SPY, no new trade occurs (because you are already at the target). If the price moves and your holding drops to 4.8%, the Execution model will buy 0.2% to rebalance back to 5%.
Q: Can I use this for Shorting?
A: Yes. The code direction = 1 if insight.Direction == InsightDirection.UP else -1 handles this. If the signal is DOWN, the weight becomes -0.05, and the algorithm will short sell 5% of the portfolio value.