Problem Description
Solution
To start a backtest with a pre-existing portfolio in QuantConnect, you cannot simply place a market order in initialize because that would simulate a new purchase at the start of the backtest (incurring fees and market impact).
Instead, you must directly manipulate the Portfolio object to set the quantity and average cost basis.
Key Concepts
- Security Seeding: You must ensure the security has a valid price before setting the holdings. By default, securities added in
initializehave a price of $0 until the first data slice arrives. We use aSecurityInitializerto fetch the last known price immediately. - Direct Portfolio Access: Use
self.portfolio[symbol].set_holdings(average_price, quantity)to inject the position directly into the portfolio state without generating an order.
Python Implementation
Here is a complete algorithm demonstrating how to initialize a portfolio with 500 shares of SPY and 100 shares of AAPL.
# region imports
from AlgorithmImports import *
# endregion
class PreExistingPortfolioAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2023, 1, 1)
self.set_end_date(2023, 6, 1)
self.set_cash(100000) # Set Strategy Cash
# 1. Set up the Security Initializer
# This is crucial: It tells LEAN to look up the last known price for any asset
# added to the universe immediately. Without this, the price would be 0
# in Initialize, and we couldn't calculate the position value.
self.set_security_initializer(
BrokerageModelSecurityInitializer(
self.brokerage_model,
FuncSecuritySeeder(self.get_last_known_prices)
)
)
# 2. Add the Equities
spy = self.add_equity("SPY", Resolution.MINUTE).symbol
aapl = self.add_equity("AAPL", Resolution.MINUTE).symbol
# 3. Set the Initial Holdings
# We access the specific security in the portfolio and set the quantity and average cost.
# Example 1: 500 shares of SPY at the current market price (seeded price)
current_spy_price = self.securities[spy].price
self.portfolio[spy].set_holdings(average_price=current_spy_price, quantity=500)
# Example 2: 100 shares of AAPL at a specific historical cost basis (e.g., $120)
# This is useful if you want to simulate a position bought long ago.
self.portfolio[aapl].set_holdings(average_price=120.00, quantity=100)
# Debug logging to verify
self.debug(f"Initial SPY Holdings: {self.portfolio[spy].quantity} @ ${self.portfolio[spy].average_price}")
self.debug(f"Initial AAPL Holdings: {self.portfolio[aapl].quantity} @ ${self.portfolio[aapl].average_price}")
self.debug(f"Total Portfolio Value: ${self.portfolio.total_portfolio_value}")
def on_data(self, data: Slice):
# Simple logic to demonstrate the portfolio is active
if not self.portfolio.invested:
return
# Example: Liquidate if SPY drops 5% from our initial cost
if self.securities["SPY"].close < self.portfolio["SPY"].average_price * 0.95:
self.liquidate("SPY")
self.debug("Liquidated SPY due to stop loss")
Explanation of the Code
set_security_initializer: This block wraps the default brokerage model with aFuncSecuritySeeder. Whenadd_equityis called, this seeder immediately fetches the most recent historical data point before the Start Date. This populatesself.securities[symbol].price.self.portfolio[symbol].set_holdings(...):average_price: The price at which you "bought" the stock. You can useself.securities[symbol].priceto use the market price at the start of the backtest, or a hardcoded float (e.g.,120.00) to simulate a position held from a previous time.quantity: The number of shares held. Use a negative number for short positions.
- Result: When the backtest begins processing data, your equity curve will reflect the fluctuations of these existing positions immediately, without executing buy orders in the transaction log.
Q&A: Initializing Portfolios in QuantConnect
Q: Does set_holdings in initialize trigger a transaction fee?
A: No. When using self.portfolio[symbol].set_holdings() directly within the initialize method (as shown above), it manipulates the internal state of the portfolio. It does not create an Order object, does not appear in the "Orders" tab, and does not incur transaction fees or slippage.
Q: Why is my stock price $0 when I try to set holdings?
A: By default, LEAN does not load price data until the OnData loop begins. To access prices in initialize, you must attach a SecuritySeeder (specifically FuncSecuritySeeder(self.get_last_known_prices)) to the security initializer.
Q: Can I set initial holdings for Options or Crypto?
A: Yes. The logic is identical. For Crypto, ensure you add the correct pair (e.g., BTCUSD). For Options, you must add the specific option contract symbol and then set holdings on that specific contract symbol.
Q: How does this affect the "Cash" balance?
A: Setting holdings via self.portfolio does not automatically deduct cash from your starting capital in the backtest logic shown above. If you start with $100k cash and inject $50k worth of stock, your TotalPortfolioValue will be $150k. If you want to reflect that the cash was already spent, you should manually adjust the cash:
cost = 500 * current_spy_price
self.portfolio.cash_book["USD"].add_amount(-cost)