Problem Description
Solution
In QuantConnect's LEAN engine, stock splits and dividend payments are handled in two ways: automatically by the engine for portfolio accounting, and programmatically via event handlers for custom strategy logic.
1. Automatic Handling (Portfolio Accounting)
By default, LEAN automatically processes these corporate actions to ensure your backtest remains accurate:
- Splits: The engine automatically adjusts your holdings quantity and the cost basis. For example, in a 2-for-1 split, your share count doubles, and the price halves. The total equity value remains the same.
- Dividends: The engine automatically credits the cash amount to your portfolio's cash book on the ex-dividend date (or payment date depending on the data feed configuration).
2. Programmatic Handling (Event Handlers)
If your strategy needs to react to these events (e.g., stop trading before a split, reinvest dividends immediately, or log tax implications), you should use the specific event handlers: on_splits and on_dividends.
To ensure you see the raw price drops associated with these events (rather than retrospectively smoothed prices), it is often best to set the Data Normalization Mode to Raw.
Strategy Implementation
The following algorithm demonstrates how to configure raw data and implement the specific event handlers to track and react to corporate actions.
from AlgorithmImports import *
class CorporateActionsAlgorithm(QCAlgorithm):
def initialize(self):
self.set_start_date(2020, 1, 1)
self.set_end_date(2021, 1, 1)
self.set_cash(100000)
# Add an equity known for splits/dividends (e.g., Apple)
self.symbol = self.add_equity("AAPL", Resolution.DAILY).symbol
# IMPORTANT: Set Data Normalization to Raw.
# 'Adjusted' (default) smooths past prices, hiding the gap caused by splits/dividends.
# 'Raw' ensures the price actually drops on ex-dates, triggering realistic PnL fluctuations
# that the engine's accounting logic will then correct.
self.securities[self.symbol].set_data_normalization_mode(DataNormalizationMode.RAW)
def on_data(self, slice: Slice):
# Simple logic to stay invested to experience the events
if not self.portfolio.invested:
self.set_holdings(self.symbol, 0.5)
def on_dividends(self, dividends: Dividends):
"""
Event handler for dividend payments.
This is triggered on the ex-dividend date.
"""
if self.symbol in dividends:
dividend = dividends[self.symbol]
# Log details
self.log(f"Dividend Event: {self.time} >> Symbol: {dividend.symbol} "
f"| Distribution: ${dividend.distribution} "
f"| Total Payout: ${dividend.distribution * self.portfolio[self.symbol].quantity}")
# Example Logic: Reinvest the dividend income immediately
# Note: The cash is credited automatically by LEAN, so we just place a buy order.
# We calculate how many shares we can buy with the dividend income.
income = dividend.distribution * self.portfolio[self.symbol].quantity
current_price = self.securities[self.symbol].price
if current_price > 0:
shares_to_buy = income / current_price
self.buy(self.symbol, shares_to_buy)
def on_splits(self, splits: Splits):
"""
Event handler for stock splits.
This is triggered on the split date.
"""
if self.symbol in splits:
split = splits[self.symbol]
# Log details
self.log(f"Split Event: {self.time} >> Symbol: {split.symbol} "
f"| Factor: {split.split_factor} "
f"| Type: {split.type}")
# Example Logic:
# The engine automatically adjusts Quantity.
# Here we might just log the new quantity to verify.
self.log(f"New Quantity after split: {self.portfolio[self.symbol].quantity}")
# If you have limit orders or stop losses, you might need to cancel/replace them
# here if the exchange doesn't handle them automatically.
self.transactions.cancel_open_orders(split.symbol)
Key Implementation Details
-
set_data_normalization_mode(DataNormalizationMode.RAW):- If you leave this as default (
Adjusted), the historical price is mathematically adjusted backward. You won't see a price drop on the chart, and technical indicators (like SMA) won't break. - If you use
Raw, the price drops on the ex-date. This is required if your strategy relies on the actual market price (e.g., arbitrage or options pricing).
- If you leave this as default (
-
on_dividends:- The
dividendsobject is a dictionary-like collection. dividend.distributiongives the cash amount per share.- LEAN adds the cash to
self.portfolio.cashautomatically; you do not need to manually add the cash.
- The
-
on_splits:- The
splitsobject contains thesplit_factor. - A factor of 0.25 implies a 4-for-1 split (price is multiplied by 0.25, quantity divided by 0.25).
- LEAN adjusts
self.portfolio[symbol].quantityandaverage_priceautomatically.
- The
Q&A
Q: Do I need to manually adjust my cash when a dividend is paid in QuantConnect?
A: No. The LEAN engine automatically calculates the total dividend payout based on your holdings and credits your algorithm's cash book. You only need to use on_dividends if you wish to execute specific logic, such as reinvesting that cash immediately (DRIP).
Q: What happens to my Stop Loss orders during a stock split?
A: In a live trading environment, most brokerages automatically adjust or cancel open orders during a split. In a backtest, open orders are not automatically adjusted by default logic in all cases. It is best practice to use the on_splits handler to cancel open orders (self.transactions.cancel_open_orders(symbol)) and reissue them at the new price levels to ensure accuracy.
Q: Why do my indicators look broken after a stock split?
A: This happens if you are using DataNormalizationMode.RAW. Raw data reflects the actual price drop (e.g., price going from $100 to $50 in a 2:1 split). This sudden drop causes moving averages and other indicators to skew. To fix this, use DataNormalizationMode.ADJUSTED (the default), which retrospectively adjusts past data to create a smooth price curve for technical analysis.