Asset Classes
Index Options
Trades
To get historical trade data, call the History<TradeBar> method with a security's Symbol.
To get historical trade data, call the history method with the TradeBar type and a security's Symbol.
This method returns a DataFrame with columns for the open, high, low, close, and volume.
public class IndexOptionTradeBarHistoryAlgorithm : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2024, 12, 19);
SetEndDate(2024, 12, 31);
// Get the Symbol of a security.
var index = AddIndex("SPX");
var symbol = OptionChain(index.Symbol).OrderBy(c => c.OpenInterest).Last().Symbol;
// Get the 5 trailing daily TradeBar objects of the security.
var history = History<TradeBar>(symbol, 5, Resolution.Daily);
// Iterate through each TradeBar and calculate its dollar volume.
foreach (var bar in history)
{
var t = bar.EndTime;
var dollarVolume = bar.Close * bar.Volume;
}
}
} class IndexOptionTradeBarHistoryAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 12, 19)
self.set_end_date(2024, 12, 31)
# Get the Symbol of a security.
index = self.add_index('SPX')
symbol = sorted(self.option_chain(index.symbol), key=lambda c: c.open_interest)[-1].symbol
# Get the 5 trailing daily TradeBar objects of the security in DataFrame format.
history = self.history(TradeBar, symbol, 5, Resolution.DAILY)
| close | high | low | open | volume | |||||
|---|---|---|---|---|---|---|---|---|---|
| expiry | strike | type | symbol | time | |||||
| 2024-12-20 | 4000.0 | 1 | SPX 32NKZCP89T8EM|SPX 31 | 2024-12-12 15:15:00 | 0.05 | 0.07 | 0.05 | 0.07 | 5773.0 |
| 2024-12-13 15:15:00 | 0.05 | 0.10 | 0.05 | 0.05 | 3052.0 | ||||
| 2024-12-16 15:15:00 | 0.03 | 0.10 | 0.03 | 0.10 | 6423.0 | ||||
| 2024-12-17 15:15:00 | 0.03 | 0.05 | 0.03 | 0.05 | 6085.0 | ||||
| 2024-12-18 15:15:00 | 0.85 | 1.00 | 0.03 | 0.03 | 10551.0 |
# Calculate the daily returns. daily_returns = history.close.pct_change().iloc[1:]
expiry strike type symbol time
2024-12-20 4000.0 1 SPX 32NKZCP89T8EM|SPX 31 2024-12-13 15:15:00 0.000000
2024-12-16 15:15:00 -0.400000
2024-12-17 15:15:00 0.000000
2024-12-18 15:15:00 27.333333
If you intend to use the data in the DataFrame to create TradeBar objects, request that the history request returns the data type you need.
Otherwise, LEAN consumes unnecessary computational resources populating the DataFrame.
To get a list of TradeBar objects instead of a DataFrame, call the history[TradeBar] method.
# Get the 5 trailing daily TradeBar objects of the security in TradeBar format.
history = self.history[TradeBar](symbol, 5, Resolution.DAILY)
# Iterate through the TradeBar objects and access their volumes.
for trade_bar in history:
t = trade_bar.end_time
volume = trade_bar.volume
Request minute, hour, or daily resolution data. Otherwise, the history request won't return any data.
Quotes
To get historical quote data, call the History<QuoteBar> method with a security's Symbol.
To get historical quote data, call the history method with the QuoteBar type and a security's Symbol.
This method returns a DataFrame with columns for the open, high, low, close, and size of the bid and ask quotes.
The columns that don't start with "bid" or "ask" are the mean of the quote prices on both sides of the market.
public class IndexOptionQuoteBarHistoryAlgorithm : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2024, 12, 19);
SetEndDate(2024, 12, 31);
// Get the Symbol of a security.
var index = AddIndex("SPX");
var symbol = OptionChain(index.Symbol).OrderBy(c => c.OpenInterest).Last().Symbol;
// Get the 5 trailing minute QuoteBar objects of the security.
var history = History<QuoteBar>(symbol, 5, Resolution.Minute);
// Iterate through the QuoteBar objects and calculate the spread.
foreach (var bar in history)
{
var t = bar.EndTime;
var spread = bar.Ask.Close - bar.Bid.Close;
}
}
} class IndexOptionQuoteBarHistoryAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 12, 19)
self.set_end_date(2024, 12, 31)
# Get the Symbol of a security.
index = self.add_index('SPX')
symbol = sorted(self.option_chain(index.symbol), key=lambda c: c.open_interest)[-1].symbol
# Get the 5 trailing minute QuoteBar objects of the security in DataFrame format.
history = self.history(QuoteBar, symbol, 5, Resolution.MINUTE)
| askclose | askhigh | asklow | askopen | asksize | bidclose | bidhigh | bidlow | bidopen | bidsize | close | high | low | open | |||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| expiry | strike | type | symbol | time | ||||||||||||||
| 2024-12-20 | 4000.0 | 1 | SPX 32NKZCP89T8EM|SPX 31 | 2024-12-18 15:11:00 | 1.45 | 9.8 | 1.15 | 1.15 | 11.0 | 0.90 | 0.90 | 0.3 | 0.90 | 87.0 | 1.175 | 5.350 | 0.725 | 1.025 |
| 2024-12-18 15:12:00 | 1.40 | 9.6 | 1.40 | 1.45 | 10.0 | 0.85 | 0.95 | 0.7 | 0.90 | 63.0 | 1.125 | 5.275 | 1.050 | 1.175 | ||||
| 2024-12-18 15:13:00 | 1.40 | 4.9 | 1.40 | 1.40 | 122.0 | 0.80 | 0.90 | 0.3 | 0.85 | 104.0 | 1.100 | 2.900 | 0.850 | 1.125 | ||||
| 2024-12-18 15:14:00 | 1.25 | 4.9 | 1.25 | 1.40 | 59.0 | 0.60 | 0.80 | 0.3 | 0.80 | 95.0 | 0.925 | 2.850 | 0.775 | 1.100 | ||||
| 2024-12-18 15:15:00 | 1.05 | 4.9 | 1.00 | 1.25 | 48.0 | 0.60 | 0.85 | 0.3 | 0.60 | 10.0 | 0.825 | 2.875 | 0.650 | 0.925 |
# Calculate the spread at each minute. spread = history.askclose - history.bidclose
expiry strike type symbol time
2024-12-20 4000.0 1 SPX 32NKZCP89T8EM|SPX 31 2024-12-18 15:11:00 0.55
2024-12-18 15:12:00 0.55
2024-12-18 15:13:00 0.60
2024-12-18 15:14:00 0.65
2024-12-18 15:15:00 0.45
dtype: float64
If you intend to use the data in the DataFrame to create QuoteBar objects, request that the history request returns the data type you need.
Otherwise, LEAN consumes unnecessary computational resources populating the DataFrame.
To get a list of QuoteBar objects instead of a DataFrame, call the history[QuoteBar] method.
# Get the 5 trailing minute QuoteBar objects of the security in QuoteBar format.
history = self.history[QuoteBar](symbol, 5, Resolution.MINUTE)
# Iterate through each QuoteBar and calculate the dollar volume on the bid.
for quote_bar in history:
t = quote_bar.end_time
bid_dollar_volume = quote_bar.last_bid_size * quote_bar.bid.close
Request minute, hour, or daily resolution data. Otherwise, the history request won't return any data.
Slices
To get historical Slice data, call the Historyhistory method without passing any Symbol objects.
This method returns Slice objects, which contain data points from all the datasets in your algorithm.
If you omit the resolution argument, it uses the resolution that you set for each security and dataset when you created the subscriptions.
public class SliceHistoryAlgorithm : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2024, 12, 1);
SetEndDate(2024, 12, 31);
// Add some securities and datasets.
var index = AddIndex("SPX");
var contract = OptionChain(index.Symbol).OrderBy(c => c.OpenInterest).Last();
AddOptionContract(contract.Symbol);
// Get the historical Slice objects over the last 5 days for all the subcriptions in your algorithm.
var history = History(5, Resolution.Daily);
// Iterate through each historical Slice.
foreach (var slice in history)
{
// Iterate through each TradeBar in this Slice.
foreach (var kvp in slice.Bars)
{
var symbol = kvp.Key;
var bar = kvp.Value;
}
}
}
} class SliceHistoryAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 12, 1)
self.set_end_date(2024, 12, 31)
# Add some securities and datasets.
index = self.add_index('SPX')
contract = sorted(self.option_chain(index.symbol), key=lambda c: c.open_interest)[-1]
self.add_option_contract(contract.symbol)
# Get the historical Slice objects over the last 5 days for all the subcriptions in your algorithm.
history = self.history(5, Resolution.DAILY)
# Iterate through each historical Slice.
for slice_ in history:
# Iterate through each TradeBar in this Slice.
for symbol, trade_bar in slice_.bars.items():
close = trade_bar.close
Open Interest
To get historical open interest data, call the History<OpenInterest> method with a security's Symbol.
To get historical open interest data, call the history method with the OpenInterest type and a security's Symbol.
This method returns a DataFrame with a single column.
public class IndexOptionOpenInterestHistoryAlgorithm : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2024, 12, 19);
SetEndDate(2024, 12, 31);
// Get the Symbol of a security.
var index = AddIndex("SPX");
var symbol = OptionChain(index.Symbol).OrderBy(c => c.OpenInterest).Last().Symbol;
// Get the 5 trailing daily OpenInterest objects of the security.
var history = History<OpenInterest>(symbol, 5, Resolution.Daily);
// Iterate through each OpenInterest and get it's value.
foreach (var oi in history)
{
var t = oi.EndTime;
var openInterest = oi.Value;
}
}
} class IndexOptionOpenInterestHistoryAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 12, 19)
self.set_end_date(2024, 12, 31)
# Get the Symbol of a security.
index = self.add_index('SPX')
symbol = sorted(self.option_chain(index.symbol), key=lambda c: c.open_interest)[-1].symbol
# Get the 5 trailing daily OpenInterest objects of the security in DataFrame format.
history = self.history(OpenInterest, symbol, 5, Resolution.DAILY)
| openinterest | |||||
|---|---|---|---|---|---|
| expiry | strike | type | symbol | time | |
| 2024-12-20 | 4000.0 | 1 | SPX 32NKZCP89T8EM|SPX 31 | 2024-12-12 23:00:00 | 306249.0 |
| 2024-12-15 23:00:00 | 305821.0 | ||||
| 2024-12-16 23:00:00 | 301048.0 | ||||
| 2024-12-17 23:00:00 | 299501.0 | ||||
| 2024-12-18 23:00:00 | 294504.0 |
# Calculate the daily change in open interest. oi_delta = history.openinterest.diff().iloc[1:]
expiry strike type symbol time
2024-12-20 4000.0 1 SPX 32NKZCP89T8EM|SPX 31 2024-12-15 23:00:00 -428.0
2024-12-16 23:00:00 -4773.0
2024-12-17 23:00:00 -1547.0
2024-12-18 23:00:00 -4997.0
Name: openinterest, dtype: float64
If you intend to use the data in the DataFrame to create OpenInterest objects, request that the history request returns the data type you need.
Otherwise, LEAN consumes unnecessary computational resources populating the DataFrame.
To get a list of OpenInterest objects instead of a DataFrame, call the history[OpenInterest] method.
# Get the 5 trailing daily OpenInterest objects of the security in OpenInterest format.
history = self.history[OpenInterest](symbol, 5, Resolution.DAILY)
# Iterate through the TradeBar objects and access their volumes.
for oi in history:
t = oi.end_time
open_interest = oi.value
Daily Option Chains
To get historical daily Option chain data, call the History method with the Option Symbol object.
The data this method returns contains information on all the currently tradable contracts, not just the contracts that pass your filter.
To get historical daily Option chain data, call the history method with the Option Symbol object.
The data this method returns contains information on all the currently tradable contracts, not just the contracts that pass your filter.
If you pass flatten=True, this method returns a DataFrame with columns for the data point attributes.
public class IndexOptionDailyOptionChainHistoryAlgorithm : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2024, 12, 23);
SetEndDate(2024, 12, 31);
// Add an Index Option universe.
var option = AddIndexOption("SPX");
// Get the trailing 5 daily Option chains.
var history = History<OptionUniverse>(option.Symbol, 5);
// Iterate through each day of the history.
foreach (var optionUniverse in history)
{
var t = optionUniverse.EndTime;
// Select the 2 contracts with the most volume.
var mostTraded = optionUniverse.Select(c => c as OptionUniverse).OrderByDescending(c => c.Volume).Take(2);
}
}
} class IndexOptionDailyOptionChainHistoryAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 12, 23)
self.set_end_date(2024, 12, 31)
# Add an Index Option universe.
option = self.add_index_option('SPX')
# Get the trailing 5 daily Option chains in DataFrame format.
history = self.history(option.symbol, 5, flatten=True)
| close | delta | gamma | high | impliedvolatility | low | open | openinterest | rho | theta | underlying | value | vega | volume | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| time | symbol | ||||||||||||||
| 2024-12-12 | SPX YOGZ798QKLRI|SPX 31 | 5879.60 | 1.000000 | 0.000000 | 5897.45 | 0.000000 | 5865.00 | 5866.45 | 5280.0 | 0.051183 | -0.030094 | SPX: ¤6,084.20 | 5879.60 | 0.000000 | 9.0 |
| SPX YP8JPVHGPQ9A|SPX 31 | 5878.95 | 1.000000 | 0.000000 | 5898.45 | 0.000000 | 5864.30 | 5866.55 | 68.0 | 0.203532 | -0.029968 | SPX: ¤6,084.20 | 5878.95 | 0.000000 | 0.0 | |
| SPX YQ70D5ADE4VI|SPX 31 | 5876.70 | 1.000000 | 0.000000 | 5892.70 | 0.000000 | 5862.20 | 5863.70 | 42.0 | 0.392162 | -0.029810 | SPX: ¤6,084.20 | 5876.70 | 0.000000 | 0.0 | |
| SPX YQYKVRJ3J9DA|SPX 31 | 5873.70 | 1.000000 | 0.000000 | 5889.75 | 0.000000 | 5856.85 | 5860.50 | 35.0 | 0.541633 | -0.029685 | SPX: ¤6,084.20 | 5873.70 | 0.000000 | 0.0 | |
| SPX YRP5YAENLMPA|SPX 31 | 5873.15 | 0.999970 | 0.000000 | 5888.45 | 1.651861 | 5857.20 | 5859.35 | 1.0 | 0.680416 | -0.032476 | SPX: ¤6,084.20 | 5873.15 | 0.004516 | 0.0 | |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2024-12-18 | SPX 32RLQ2G18QHCE|SPX 31 | 4740.75 | -0.999435 | 0.000002 | 4751.20 | 0.268745 | 4727.55 | 4738.45 | 0.0 | -44.100768 | 1.613144 | SPX: ¤6,050.31 | 4740.75 | 0.077120 | 0.0 |
| SPX 32XJE2QF1AJ7Y|SPX 31 | 5445.85 | -0.675418 | 0.000079 | 5466.35 | 0.753369 | 5424.20 | 5445.05 | 28.0 | -101.357524 | -0.721391 | SPX: ¤6,050.31 | 5445.85 | 21.834520 | 0.0 | |
| SPX 337HSSRKH55N2|SPX 31 | 5001.30 | -0.997608 | 0.000006 | 5012.40 | 0.139114 | 4990.20 | 4999.30 | 3.0 | -214.914871 | 1.611553 | SPX: ¤6,050.31 | 5001.30 | 0.638336 | 0.0 | |
| SPX 33HG7ISPWZS26|SPX 31 | 4583.80 | -0.992253 | 0.000017 | 4595.60 | 0.118943 | 4574.20 | 4582.00 | 10.0 | -303.860365 | 1.514502 | SPX: ¤6,050.31 | 4583.80 | 2.232716 | 0.0 | |
| SPX 33REM8TVCUEHA|SPX 31 | 4193.20 | -0.975511 | 0.000043 | 4205.90 | 0.111797 | 4185.60 | 4191.90 | 8.0 | -379.486892 | 1.404206 | SPX: ¤6,050.31 | 4193.20 | 6.947694 | 0.0 |
# Select the 2 contracts with the greatest volume each day.
most_traded = history.groupby('time').apply(lambda x: x.nlargest(2, 'volume')).reset_index(level=1, drop=True).volume
time symbol
2024-12-12 SPX YOGZ79IHUCOE|SPX 31 20200.0
SPX 32NKZCPHP4626|SPX 31 18417.0
2024-12-13 SPX YOGZ79IHUCOE|SPX 31 7109.0
SPX 32OCJVBQ3CMGE|SPX 31 5930.0
2024-12-14 SPX YOGZ79IHUCOE|SPX 31 17443.0
SPX 32NKZCRZNW3RI|SPX 31 16458.0
2024-12-17 SPX YOGZ7C245MVI|SPX 31 32796.0
SPX 32NKZCS1BFG9A|SPX 31 30285.0
2024-12-18 SPX YOGZ79IHUCOE|SPX 31 14183.0
SPX YOGZ7C245MVI|SPX 31 13335.0
Name: volume, dtype: float64
To get the data in the format of OptionUniverse objects instead of a DataFrame, call the history[OptionUniverse] method.
# Get the historical OptionUniverse data over the last 30 days.
history = self.history[OptionUniverse](option.symbol, timedelta(30))
# Iterate through each daily Option chain.
for option_universe in history:
t = option_universe.end_time
# Select the contract with the most volume.
most_traded = sorted(option_universe, key=lambda contract: contract.volume)[-1]
Indicators
To get historical indicator values, call the IndicatorHistoryindicator_history method with an indicator and the security's Symbol.
public class IndexOptionIndicatorHistoryAlgorithm : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2024, 12, 19);
SetEndDate(2024, 12, 31);
// Get the Symbol of the Option contract.
var underlying = AddIndex("SPX").Symbol;
var symbol = OptionChain(underlying).OrderBy(c => c.OpenInterest).Last().Symbol;
// Get the 21-day SMA values of the contract for the last 5 trading days.
var history = IndicatorHistory(new SimpleMovingAverage(21), symbol, 5, Resolution.Daily);
// Get the maximum of the SMA values.
var maxSMA = history.Max(indicatorDataPoint => indicatorDataPoint.Current.Value);
}
} class IndexOptionIndicatorHistoryAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 12, 19)
self.set_end_date(2024, 12, 31)
# Get the Symbol of the Option contract.
underlying = self.add_index('SPX').symbol
symbol = sorted(self.option_chain(underlying), key=lambda c: c.open_interest)[-1].symbol
# Get the 21-day SMA values of the contract for the last 5 trading days.
history = self.indicator_history(SimpleMovingAverage(21), symbol, 5, Resolution.DAILY)
To organize the data into a DataFrame, use the data_frame property of the result.
# Organize the historical indicator data into a DataFrame to enable pandas wrangling. history_df = history.data_frame
| current | rollingsum | |
|---|---|---|
| 2024-12-12 15:15:00 | 0.617857 | 12.975 |
| 2024-12-13 15:15:00 | 0.580952 | 12.200 |
| 2024-12-16 15:15:00 | 0.535714 | 11.250 |
| 2024-12-17 15:15:00 | 0.477381 | 10.025 |
| 2024-12-18 15:15:00 | 0.472619 | 9.925 |
# Get the maximum of the SMA values. sma_max = history_df.current.max()
The IndicatorHistoryindicator_history method resets your indicator, makes a history request, and updates the indicator with the historical data.
Just like with regular history requests, the IndicatorHistoryindicator_history method supports time periods based on a trailing number of bars, a trailing period of time, or a defined period of time.
If you don't provide a resolution argument, it defaults to match the resolution of the security subscription.
To make the IndicatorHistoryindicator_history method update the indicator with an alternative price field instead of the close (or mid-price) of each bar, pass a selector argument.
// Get the historical values of an indicator over the last 30 days, applying the indicator to the security's volume. var history = IndicatorHistory(indicator, symbol, TimeSpan.FromDays(30), selector: Field.Volume);
# Get the historical values of an indicator over the last 30 days, applying the indicator to the security's volume. history = self.indicator_history(indicator, symbol, timedelta(30), selector=Field.VOLUME)
Some indicators require the prices of multiple securities to compute their value (for example, the indicators for the Greeks and implied volatility).
In this case, pass a list of the Symbol objects to the method.
public class IndexOptionMultiAssetIndicatorHistoryAlgorithm : QCAlgorithm
{
public override void Initialize()
{
SetStartDate(2024, 12, 19);
SetEndDate(2024, 12, 31);
// Get the Symbol of the underlying asset.
var underlying = AddIndex("SPX").Symbol;
// Get the Option contract Symbol.
var option = OptionChain(underlying).OrderBy(c => c.OpenInterest).Last().Symbol;
// Get the Symbol of the mirror contract.
var mirror = QuantConnect.Symbol.CreateOption(
underlying, option.ID.Market, option.ID.OptionStyle,
option.ID.OptionRight == OptionRight.Put ? OptionRight.Call : OptionRight.Put,
option.ID.StrikePrice, option.ID.Date
);
// Create the indicator.
var indicator = new ImpliedVolatility(option, RiskFreeInterestRateModel, new DividendYieldProvider(underlying), mirror, OptionPricingModelType.ForwardTree);
// Get the historical values of the indicator over the last 60 trading minutes.
var history = IndicatorHistory(indicator, new[] {underlying, option, mirror}, 60, Resolution.Minute);
// Get the average IV value.
var avgIV = history.Average(indicatorDataPoint => indicatorDataPoint.Current.Value);
}
} class IndexOptionMultiAssetIndicatorHistoryAlgorithm(QCAlgorithm):
def initialize(self) -> None:
self.set_start_date(2024, 12, 19)
self.set_end_date(2024, 12, 31)
# Get the Symbol of the underlying asset.
underlying = self.add_index('SPX').symbol
# Get the Option contract Symbol.
option = sorted(self.option_chain(underlying), key=lambda c: c.open_interest)[-1].symbol
# Get the Symbol of the mirror contract.
mirror = Symbol.create_option(
underlying, option.id.market, option.id.option_style,
OptionRight.CALL if option.id.option_right == OptionRight.PUT else OptionRight.PUT,
option.id.strike_price, option.id.date
)
# Create the indicator.
indicator = ImpliedVolatility(
option, self.risk_free_interest_rate_model, DividendYieldProvider(underlying),
mirror, OptionPricingModelType.FORWARD_TREE
)
# Get the historical values of the indicator over the last 60 trading minutes.
history = self.indicator_history(indicator, [underlying, option, mirror], 60, Resolution.MINUTE)
# Get the average IV value.
iv_avg = history.data_frame.current.mean()
If you already have a list of Slice objects, you can pass them to the IndicatorHistoryindicator_history method to avoid the internal history request.
var slices = History(new[] {symbol}, 30, Resolution.Daily);
var history = IndicatorHistory(indicator, slices);
Examples
The following examples demonstrate some common practices for trading Index Options with historical data.
Example 1: Standard-Weekly Contracts Cointegration
The following example analyzes the cointegration relationship between the front-month ATM SPX and SPXW calls. By measuring their spread divergence, we trade mean reversal on their spread convergence.
using MathNet.Numerics.LinearRegression;
public class IndexOptionHistoricalDataAlgorithm : QCAlgorithm
{
private Symbol _spxContract, _spxwContract;
private decimal[] _cointegrationVector = new[] { 0m, 0m };
private decimal _threshold = 2m;
private decimal _spreadMean = 0m;
private decimal _spreadSd = 1m;
public override void Initialize()
{
SetStartDate(2024, 9, 1);
SetEndDate(2024, 12, 31);
SetCash(10000000);
// Select the Index Options to analyze and trade by week.
Schedule.On(
DateRules.WeekStart(),
TimeRules.At(9, 15),
SelectContract
);
}
public override void OnData(Slice slice)
{
if (_spxwContract != null && slice.QuoteBars.TryGetValue(_spxwContract, out var y) && slice.QuoteBars.TryGetValue(_spxContract, out var x1))
{
// Obtain the current spread to see if there is any price divergence to trade.
var spread = CalculateSpread(y.Close, x1.Close, _cointegrationVector);
var z = (spread - _spreadMean) / _spreadSd;
// If the spread diverges above or below the threshold, trade to bet on mean reversal.
if (z > _threshold && !Portfolio[_spxContract].IsLong)
{
MarketOrder(_spxContract, 10 * _cointegrationVector[1]);
MarketOrder(_spxwContract, -10);
}
else if (z < -_threshold && !Portfolio[_spxwContract].IsLong)
{
MarketOrder(_spxContract, -10 * _cointegrationVector[1]);
MarketOrder(_spxwContract, 10);
}
// If prices converge, exit positions.
if ((Portfolio[_spxContract].IsLong && z < 0m) || (Portfolio[_spxwContract].IsLong && z > 0m))
{
Liquidate();
}
}
}
private void SelectContract()
{
var index = QuantConnect.Symbol.Create("SPX", SecurityType.Index, Market.USA);
// Obtain the SPX ATM call contract since it is the most liquid with which to trade.
var spx = QuantConnect.Symbol.CreateCanonicalOption(index);
var spxContracts = OptionChain(spx).Where(x => x.Expiry <= Time.AddDays(30)).ToList();
if (spxContracts.Count == 0)
{
_spxContract = null;
_spxwContract = null;
return;
}
var expiry = spxContracts.Max(x => x.Expiry);
_spxContract = spxContracts.Where(x => x.Expiry == expiry && x.Right == OptionRight.Call)
.OrderBy(x => Math.Abs(x.Strike - x.UnderlyingLastPrice))
.First().Symbol;
// Obtain the SPXW contract with the same strike, right, and expiry.
var spxw = QuantConnect.Symbol.CreateCanonicalOption(index, "SPXW", Market.USA, "?SPXW");
_spxwContract = OptionChain(spxw).Where(x => x.Expiry == expiry && x.Right == OptionRight.Call && x.Strike == _spxContract.ID.StrikePrice).FirstOrDefault()?.Symbol;
if (_spxwContract != null)
{
// Subscribe to the contracts we will trade
AddIndexOptionContract(_spxContract);
AddIndexOptionContract(_spxwContract);
// Obtain the historical data and find their cointegration relationship.
var history = History<QuoteBar>(new[] { _spxContract, _spxwContract }, 1000, Resolution.Minute).Where(x => x.ContainsKey(_spxContract) && x.ContainsKey(_spxwContract)).ToList();
_cointegrationVector = CalculateCointegrationVector(history);
// Obtain the mean and SD of the spread between the options.
var residual = history.Select(x => CalculateSpread(x[_spxwContract].Close, x[_spxContract].Close, _cointegrationVector)).ToList();
_spreadMean = residual.Average();
_spreadSd = Convert.ToDecimal(Math.Sqrt(residual.Sum(x => Math.Pow((double)(x - _spreadMean), 2) / residual.Count)));
}
}
private decimal[] CalculateCointegrationVector(IEnumerable<DataDictionary<QuoteBar>> history)
{
// Use log price to eliminate the compounding effect.
var y = history.Select(x => Math.Log((double)x[_spxwContract].Close)).ToArray();
var x1 = history.Select(x => Math.Log((double)x[_spxContract].Close)).ToArray();
// Create a matrix for the regression analysis.
var n = new[] { y.Length, x1.Length }.Min();
var designMatrix = new double[n][];
for (int i = 0; i < n; i++)
{
designMatrix[i] = new[] { x1[i] };
}
try
{
// Perform regression using MathNet.Numerics
var coefficients = MultipleRegression.QR(designMatrix, y.TakeLast(n).ToArray(), intercept: true);
// The coefficients array will contain the intercept and the coefficients for each independent variable.
// Format: [intercept, x1]
return new[] { (decimal)coefficients[0], (decimal)coefficients[1] };
}
catch
{
return _cointegrationVector;
}
}
private decimal CalculateSpread(decimal y, decimal x1, decimal[] cointegrationVector)
{
// Using the cointegration vector to calculate the spread.
return LogPrice(y) - (cointegrationVector[0] + LogPrice(x1) * cointegrationVector[1]);
}
private decimal LogPrice(decimal price)
{
return Convert.ToDecimal(Math.Log((double)price));
}
} from sklearn.linear_model import LinearRegression
class IndexOptionHistoricalDataAlgorithm(QCAlgorithm):
_threshold = 2
_cointegration_vector = [0, 0]
_spread_mean = 0
_spread_sd = 1
_spx_contract = None
_spxw_contract = None
def initialize(self) -> None:
self.set_start_date(2024, 9, 1)
self.set_end_date(2024, 12, 31)
self.set_cash(10000000)
self._index = Symbol.create("SPX", SecurityType.INDEX, Market.USA)
self._spx = Symbol.create_canonical_option(self._index)
self._spxw = Symbol.create_canonical_option(self._index, "SPXW", Market.USA, "?SPXW")
# Select the Index Options to analyze and trade by week.
self.schedule.on(self.date_rules.week_start(), self.time_rules.at(9, 15), self.select_contract)
def on_data(self, slice: Slice) -> None:
if not (self._spx_contract and self._spxw_contract):
return
spxw = slice.quote_bars.get(self._spxw_contract)
spx = slice.quote_bars.get(self._spx_contract)
if not (spxw and spx):
return
# Obtain the current spread to see if there is any price divergence to trade.
spread = self.calculate_spread(spxw.close, spx.close, self._cointegration_vector)
z = (spread - self._spread_mean) / self._spread_sd
# If the spread diverges above or below the threshold, trade to bet on mean reversion.
if z > self._threshold and not self.portfolio[self._spx_contract].is_long:
spx_qty = int(10 * self._cointegration_vector[1])
spxw_qty = -10
self.market_order(self._spx_contract, spx_qty)
self.market_order(self._spxw_contract, spxw_qty)
elif z < -self._threshold and not self.portfolio[self._spxw_contract].is_long:
spx_qty = int(-10 * self._cointegration_vector[1])
spxw_qty = 10
self.market_order(self._spx_contract, spx_qty)
self.market_order(self._spxw_contract, spxw_qty)
# If prices converge, exit positions.
if ((self.portfolio[self._spx_contract].is_long and z < 0) or
(self.portfolio[self._spxw_contract].is_long and z > 0)):
self.liquidate()
def select_contract(self) -> None:
self._spx_contract = None
self._spxw_contract = None
# Obtain the SPX ATM call contract since it is the most liquid to trade with.
spx_contracts = [x for x in self.option_chain(self._spx) if x.expiry <= self.time + timedelta(30)]
if not spx_contracts:
return
# Select the furthest expiry within the next 30 days
expiry = max(x.expiry for x in spx_contracts)
self._spx_contract = sorted(
[x for x in spx_contracts
if x.expiry == expiry and x.right == OptionRight.CALL],
key=lambda x: abs(x.strike - x.underlying_last_price)
)[0].symbol
# Obtain the SPXW contract with the same strike, right, and expiry.
spxw_contract = next(
(
x for x in self.option_chain(self._spxw)
if (x.expiry == expiry and
x.right == OptionRight.CALL and
x.strike == self._spx_contract.id.strike_price)
),
None
)
if not spxw_contract:
return
self._spxw_contract = spxw_contract.symbol
# Subscribe to the contracts we will trade
self.add_index_option_contract(self._spx_contract)
self.add_index_option_contract(self._spxw_contract)
# Obtain the historical data and find their cointegration relationship.
history = self.history(QuoteBar, [self._spx_contract, self._spxw_contract], 1000).close.droplevel([0, 1, 2]).unstack(0)
# Run linear regression to determine cointegration relationship
self._cointegration_vector = self.calculate_cointegration_vector(history)
residual = self.calculate_spread(history[self._spxw_contract], history[self._spx_contract], self._cointegration_vector)
self._spread_mean = np.mean(residual)
self._spread_sd = np.std(residual)
def calculate_cointegration_vector(self, history):
# Prepare log price series
y = np.log(history[self._spxw_contract])
x1 = np.log(history[self._spx_contract])
# Linear regression for cointegration
lr = LinearRegression().fit(x1.to_frame(), y)
return [lr.intercept_, lr.coef_[0]]
def calculate_spread(self, y, x1, cointegration_vector):
return np.log(y) - (cointegration_vector[0] + np.log(x1) * cointegration_vector[1])