Backtest and Auto-Trade Options with WealthLab
Updated: 3 July 2023 by Cone
This article assumes a solid understanding of options and backtesting with C# Coded strategies. If you are new to options, we recommend familiarizing yourself with the basics before delving into the intricacies of programming option strategies in WealthLab. For seasoned options traders looking to enhance their trading arsenal, this article provides a valuable resource to further refine and optimize their approach.
Disclaimer! The examples on this page are not recommended strategies and serve only as informational examples for your own strategy code.
Backtesting with Synthetic Option Pricing
Starting with WealthLab 8, Build 38, you can create option trading strategies without historical option data using OptionSynthetic static class methods. The term synthetic indicates that the option prices are synthesized using a Black-Scholes option model. Prices generated from the model do not represent actual market prices, but can be representative for the purpose of backtesting and developing live trading strategies.
Futures Mode Required
For backtests to report the correct profit for option trades, Futures Mode must be enabled in Preferences (F12) > Backtest > Other Settings.
I recommend leaving Futures Mode enabled. The only time you might need to turn it off is to backtest stock symbols that match a futures symbol in "Markets & Symbols", which can happen if using wildcards for futures symbols.
Also note that it's not required to enter option symbols in Markets & Symbols for U.S. options returned by one of the integrated option data/broker providers.
GetOptionsSymbol
OptionSynthetic.GetOptionsSymbol()
facilitates identifying contract(s) by right, strike, and expiration in a WealthLab strategy. For more parameter details, see GetOptionSymbol in the Live Option Pricing section later in this article.
Since synthetic options work without option chains, OptionSynthetic.GetOptionsSymbol()
returns strike prices in $5 increments. Option expiration is identified using a combination of the currentDate, minDaysAhead, useWeeklies, and allowExpired parameters. Specifically, the next expiration at least minDaysAhead after the currentDate is returned. If useWeeklies is false, only regular monthly expirations are considered, otherwise only the non-monthly expirations are returned.
Use the symbol returned by GetOptionsSymbol()
in a GetHistory()
statement to obtain an option contract's price series.
GetHistory
OptionSynthetic.GetHistory()
generates BarHistory objects for specified option contracts using the Black-Scholes option model to create option pricing based on the underlying stock's (or futures') BarHistory and Implied Volatility, IV.
Without live option (and underlying) prices, option traders understand that it's impossible to calculate IV that would match a live option contract's IV. Consequently, for synthetic option pricing we use a "best guess" constant IV (a value between 0.3 and 0.5 often works well for non-volatile periods) or a varying estimate based on HV, the Historical Volatility indicator.
In the chart below, you'll see Bar Histories for the stock underlier TTD (top pane), real option pricing based on the bid/ask midpoint (middle pane), and synthetic option pricing based on the known Implied Volatility on the last bar of the chart (lower pane). Prices often match within $0.05!
Example 1 - Call Option Trading on Underlier Signals
The example below demonstrates how to identify, create, and trade a synthetic option contract for an expiration at least 3 days in the future based on signals (moving average crossovers) in the underlying. When the underlier's fast moving average crosses above its slow average, one call is purchased. The call position is sold at the close of expiration or when the moving averages cross in the opposite direction.
using WealthLab.Backtest;
using System;
using WealthLab.Core;
using WealthLab.Data;
using WealthLab.Indicators;
using System.Collections.Generic;
namespace WealthScript9
{
public class SyntheticOptionBacktest : UserStrategyBase
{
public override void Initialize(BarHistory bars)
{
StartIndex = 40;
_sma2 = SMA.Series(bars.Close, StartIndex);
_sma1 = SMA.Series(bars.Close, 20);
PlotIndicator(_sma1);
PlotIndicator(_sma2, WLColor.Red);
}
public override void Execute(BarHistory bars, int idx)
{
Position opt = FindOpenPositionAllSymbols(_hash);
if (opt == null)
{
if (_sma1.CrossesOver(_sma2, idx))
{
// identify an option contract symbol
string osym = OptionSynthetic.GetOptionsSymbol(bars, OptionType.Call, bars.Close[idx], bars.DateTimes[idx], 3, allowExpired: true);
_hash = Math.Abs(osym.GetHashCode());
//get the option bars
_obars = OptionSynthetic.GetHistory(bars, osym, 0.4);
_syms.Add(osym);
_expiry = OptionSynthetic.GetSymbolExpiry(osym);
// buy 1 contract
Transaction t = PlaceTrade(_obars, TransactionType.Buy, OrderType.Market, 0, _hash, "option Buy");
t.Quantity = 1;
}
}
else
{
// close the position at expiry
if (bars.DateTimes[idx].AddDays(1) >= _expiry)
ClosePosition(opt, OrderType.MarketClose, 0, "Expired");
// .. or when the underlier crosses
if (_sma1.CrossesUnder(_sma2, idx))
ClosePosition(opt, OrderType.Market);
}
}
public override void Cleanup(BarHistory bars)
{
// don't overload the chart with panes - plot only the last 5 contracts that were traded in Single Symbol Mode
if (!Backtester.Strategy.SingleSymbolMode)
return;
// Indicate the trades
foreach (Position p in GetPositionsAllSymbols())
{
if (p.Bars == bars || _syms.IndexOf(p.Symbol) < _syms.Count - 5)
continue;
PlotBarHistory(p.Bars, p.Symbol);
if (!p.NSF)
{
if (p.EntryBar > 0)
DrawTextVAlign("▲", p.EntryBar, p.Bars.Low[p.EntryBar] * 0.95, VerticalAlignment.Top, WLColor.NeonBlue, 16, 0, 0, p.Symbol, true);
if (p.ExitBar > 0)
DrawTextVAlign("▼", p.ExitBar, p.Bars.High[p.ExitBar] * 1.05, VerticalAlignment.Bottom, WLColor.NeonFuschia, 16, 0, 0, p.Symbol, true);
}
}
}
double _strike;
DateTime _expiry;
BarHistory _obars; // the option's BarHistory
SMA _sma1;
SMA _sma2;
TimeSeries _iv; // implied volatility estimate
int _hash = -1;
UniqueList<string> _syms = new UniqueList<string>();
}
}
Example 2 - Sell Covered Calls
Covered call option strategies involve buying/holding shares of the underlier and selling a short call for each 100 shares owned when the market outlook is neutral or possibly bearish. Trading risk for only the short call position is theoretically infinite, but the short call option is "covered" by the underlying shares, which can be delivered/sold to the call buyer at the strike price.
No matter how high (or low) the shares trade, the call seller stands only to gain the premium collected for the selling the call option(s) plus any stock gain from the purchase price up to the strike price.
The Covered Call strategy programmed below buys the underlier on a "golden cross" signal and sells the shares on the opposite signal. On the signal to buy the underlying shares, call options are sold short. Call position(s) are closed (covered) on the expiration date or when the underlier position is sold.
For the backtest, we select ATM (at-the-money) calls that have an expiration at least 30 days and specify to allowExpired contracts.
Finally note that the same value is assigned to both the underlying and option contract's Transaction.Weight
. The helps to ensure, but doesn't guarantee, that both Positions are put added in a Portfolio backtest. Otherwise, when buying power is limited, portfolio positions are processed randomly and you could wind up with stock and no options or even worse - uncovered short calls!
using WealthLab.Backtest;
using System;
using WealthLab.Core;
using WealthLab.Data;
using WealthLab.Indicators;
using System.Collections.Generic;
namespace WealthScript5
{
public class CoveredCalls : UserStrategyBase
{
public override void Initialize(BarHistory bars)
{
StartIndex = 200;
_sma2 = SMA.Series(bars.Close, StartIndex);
_sma1 = SMA.Series(bars.Close, 50);
PlotIndicator(_sma1);
PlotIndicator(_sma2, WLColor.Red);
// use RSI for Transaction.Weight
_rsi = RSI.Series(bars.Close, 14);
}
public override void Execute(BarHistory bars, int idx)
{
Position p = FindOpenPosition(1); // reference to underlying position, if exists
bool exitNextBar = false;
// underlying strategy
if (!HasOpenPosition(bars, PositionType.Long))
{
if (_sma1.CrossesOver(_sma2, idx))
{
// buy 200 shrs underlying
Transaction t1 = PlaceTrade(bars, TransactionType.Buy, OrderType.Market, 0, 1, "Buy underlier");
t1.Quantity = 200;
t1.Weight = _rsi[idx];
// covered call strategy: if holding the underlying, sell call(s) when price closes below the fast moving avg
// the synthetic option symbol with at least 30 days to expiration
_osym = OptionSynthetic.GetOptionsSymbol(bars, OptionType.Call, bars.Close[idx], bars.DateTimes[idx], 30, allowExpired: true);
_hash = Math.Abs(_osym.GetHashCode());
_expiry = OptionSynthetic.GetSymbolExpiry(_osym);
//get the option bars
_obars = OptionSynthetic.GetHistory(bars, _osym, 0.45);
_syms.Add(_obars.Symbol);
// short 1 call per 100 shares owned (covered calls)
Transaction t2 = PlaceTrade(_obars, TransactionType.Short, OrderType.Market, 0, _hash, "short calls");
t2.Quantity = t1.Quantity / 100;
t2.Weight = _rsi[idx];
}
}
else
{
if (_sma1.CrossesUnder(_sma2, idx))
{
exitNextBar = true;
ClosePosition(p, OrderType.Market);
}
// take care of the call option too
Position opt = FindOpenPositionAllSymbols(_hash);
if (opt != null)
{
// close the position at expiry
if (bars.DateTimes[idx].AddDays(1) >= _expiry)
ClosePosition(opt, OrderType.Market, 0, "Expired");
// .. or when the underlier is sold
if (exitNextBar)
ClosePosition(opt, OrderType.Market);
}
}
}
public override void Cleanup(BarHistory bars)
{
// don't overload the chart with panes - plot only the last 5 contracts that were traded in Single Symbol Mode
if (!Backtester.Strategy.SingleSymbolMode)
return;
// Indicate the trades
foreach (Position p in GetPositionsAllSymbols())
{
if (p.Bars == bars || _syms.IndexOf(p.Symbol) < _syms.Count - 5)
continue;
PlotBarHistory(p.Bars, p.Symbol);
if (!p.NSF)
{
if (p.EntryBar > 0)
DrawTextVAlign("▲", p.EntryBar, p.Bars.Low[p.EntryBar] * 0.95, VerticalAlignment.Top, WLColor.NeonBlue, 16, 0, 0, p.Symbol, true);
if (p.ExitBar > 0)
DrawTextVAlign("▼", p.ExitBar, p.Bars.High[p.ExitBar] * 1.05, VerticalAlignment.Bottom, WLColor.NeonFuschia, 16, 0, 0, p.Symbol, true);
}
}
}
string _osym;
DateTime _expiry;
BarHistory _obars; // the option's BarHistory
SMA _sma1;
SMA _sma2;
int _hash = -1;
UniqueList<string> _syms = new UniqueList<string>();
RSI _rsi;
}
}
Live Option Pricing and Trading
Currently, the following WealthLab-integrated providers support all or some features for option chains, historical data for non-expired contracts, and option trading:
- Interactive Brokers
- Tradier
- TD Ameritrade
- IQFeed (data only)
Of these, Interactive Brokers probably has the largest user base at WealthLab, so we'll use examples targeting the IBHistorical.Instance. It's important to know Interactive Brokers, or IB, supports only intraday data for options, while Tradier's basic data service supports daily data only. Check the Provider's Help docs for specific examples.
Always consider the following when setting up Interactive Brokers for testing and trading options:
- Intraday (only) - you must have data permissions for both instruments - options and underlying contracts.
- Interval - shorter intervals increase the time required collect data
- Data Range - long data ranges greatly increase the time required collect data
- What to Request - Since option trade data tends to be illiquid, Bid/Ask MidPoint is a better representation of option pricing through the day. Select it in WealthLab's IB Configuration.
GetOptionsSymbol()
Option Symbol Format
Each data provider has its own option symbol format that is returned by GetOptionsSymbol()
. Similar to the synthetic method for GetOptionsSymbol()
, specify parameters as required for the desired contract. The provider will automatically find a corresponding option symbol from the option chain.
public virtual string GetOptionsSymbol(BarHistory underlierBars, OptionType optionType, double price, DateTime currentDate, int minDaysAhead = 0, bool useWeeklies = false, bool allowExpired = false, bool closestStrike = true, double multiplier = 100)
Let's go over the parameters and what they mean:
Parameter | Description |
---|---|
underlierBars | Pass the BarHistory reference (bars) of the underlier for symbol and scale information. |
optionType | OptionType is a public enum OptionType { Call, Put }; |
price | The price should be near the desired strike. To find a strike that's 20% higher than the current price, multiply by 1.2. You don't need to specify the exact strike price. See closestStrike below. |
currentDate | We recommend passing the current date for this parameter and using the next 3 parameters to identify the desired expiration. |
minDaysAhead | This sets the minimum number of days allowed to expiration from the currentDate parameter. For example, if currentDate = 14 June and minDaysAhead = 5, an expiration on or after 19 June be selected. |
useWeeklies | useWeeklies must be false to return regular [monthly] expirations. Otherwise, only contracts with weekly expirations will be returned, which do not include regular expirations. |
allowExpired | Backtesting using expired contract data is possible with data that you downloaded while the contract was trading. However, since option chains are available only for non-expired contracts, you must set allowExpired to true to identify past expirations and locate strikes contracts within $1 of the price parameter for prices below 20.00, otherwise within $5. Set allowExpired to false for live trading. |
closestStrike | true finds the strike closest to price for both Calls and Puts; false finds the next strike higher than price for a Call, or the next strike lower than price for a Put. |
multiplier | If there's a need to identify a contract with a multiplier different than 100, specify it here. |
Example 3 - Plot At-The-Money Call
The following script uses GetOptionsSymbol()
to find the contract from an option chain with the following properties:
- Call is specified using OptionType.Call
- Strike price closest to the last bar's closing price
- Expiration - not weeklies, regular only
- And, the expiry must be at least 10 days in the future
Data Providers access the Option Chain behind the scenes to return the most appropriate contract symbol as a string in the optSym
variable, which you can use to GetHistory()
from the provider.
Because IB only returns intraday options data, note how the script checks the scale of the underlier's chart bars. If the scale is intraday, use GetHistory()
for the option symbol just as you would for any secondary symbol request. But, if the bars are Daily, the code shows how to access 30-minute bars for the previous 12 months, scale them to Daily bars, and finally synchronize with the Daily chart bars. You can reuse this programming pattern.
using WealthLab.Backtest;
using System;
using WealthLab.Core;
using WealthLab.Data;
using WealthLab.Indicators;
using System.Collections.Generic;
using WealthLab.InteractiveBrokers;
namespace WealthScript1
{
public class ATMOption : UserStrategyBase
{
public override void Initialize(BarHistory bars)
{
double price = bars.Close[bars.Count - 1];
DateTime currentDate = bars.DateTimes[bars.Count - 1];
int minDaysToExpiry = 10;
bool weeklies = false;
bool closestStrike = true;
bool allowExpired = false;
string optSym = IBHistorical.Instance.GetOptionsSymbol(bars, OptionType.Call, price, currentDate, minDaysToExpiry, weeklies, allowExpired, closestStrike);
DrawHeaderText("ATM Call: " + optSym, WLColor.NeonGreen, 14);
if (bars.Scale.IsIntraday)
{
_obars = GetHistory(bars, optSym);
}
else // assuming Daily
{
// request 3 months of 30 minute bars and synchronize with the non-intraday scale
_obars = WLHost.Instance.GetHistory(optSym, HistoryScale.Minute30, currentDate.AddMonths(-12), currentDate, 0, null);
if (_obars != null)
{
// compress to Daily bars
_obars = BarHistoryCompressor.ToDaily(_obars);
// sync the bars with the non-intraday data
_obars = BarHistorySynchronizer.Synchronize(_obars, bars);
}
}
if (_obars == null)
{
WriteToDebugLog("Could not get option data, check the Log Viewer for more info.");
return;
}
PlotBarHistory(_obars, optSym);
}
//execute the strategy rules here, this is executed once for each bar in the backtest history
public override void Execute(BarHistory bars, int idx)
{ }
BarHistory _obars; // the option's BarHistory
}
}
Option Greeks and IV
Passing an option symbol to a call for GetGreeks()
will return a OptionGreek
object, which contains values for the "greeks" and other information like Implied Volatility, IV, for the contract.
**Note! ** Data for greeks may not be available in which case
GetGreeks()
will return null.
The next example uses code similar to the previous example to find the ATM call and put symbols, passes them to GetGreeks()
, and displays the result - the values of an OptionGreek
object. The greeks may not be available at all times (e.g., after market hours), and it's possible to get a null
result.
Example 4 - GetGreeks()
using WealthLab.Backtest;
using System;
using WealthLab.Core;
using WealthLab.Data;
using WealthLab.Indicators;
using System.Collections.Generic;
using WealthLab.InteractiveBrokers;
namespace WealthScript2
{
public class ATMOption : UserStrategyBase
{
public override void Initialize(BarHistory bars)
{
double price = bars.Close[bars.Count - 1];
DateTime currentDate = bars.DateTimes[bars.Count - 1];
int minDaysToExpiry = 10;
bool weeklies = false;
bool closestStrike = true;
bool allowExpired = false;
// ATM Call
string optSym = IBHistorical.Instance.GetOptionsSymbol(bars, OptionType.Call, price, currentDate, minDaysToExpiry, weeklies, allowExpired, closestStrike);
OptionGreek greek = IBHistorical.Instance.GetGreeks(optSym);
DisplayTheGreeks(optSym, greek, WLColor.NeonGreen);
// ATM Put
optSym = IBHistorical.Instance.GetOptionsSymbol(bars, OptionType.Put, price, currentDate, minDaysToExpiry, weeklies, allowExpired, closestStrike);
greek = IBHistorical.Instance.GetGreeks(optSym);
DisplayTheGreeks(optSym, greek, WLColor.NeonGreen);
}
void DisplayTheGreeks(string optSym, OptionGreek greek, WLColor clr, int fontSize = 14)
{
if (greek == null)
{
DrawHeaderText(String.Format("Greeks for {0} are null", optSym), clr, 14);
return;
}
DrawHeaderText("Symbol = " + greek.Symbol, clr, 14);
DrawHeaderText("MidPoint = " + greek.OptionPrice.ToString("N2"), clr, 14);
DrawHeaderText("IV = " + greek.IV.ToString("N2"), clr, 14);
DrawHeaderText("Delta = " + greek.Delta.ToString("N2"), clr, 14);
DrawHeaderText("Gamma = " + greek.Gamma.ToString("N2"), clr, 14);
DrawHeaderText("Theta = " + greek.Theta.ToString("N2"), clr, 14);
DrawHeaderText("Vega = " + greek.Vega.ToString("N2"), clr, 14);
DrawHeaderText("Underlying = " + greek.UnderlyingPrice.ToString("N2"), clr, 14);
}
//execute the strategy rules here, this is executed once for each bar in the backtest history
public override void Execute(BarHistory bars, int idx)
{ }
BarHistory _obars; // the option's BarHistory
}
}
Calculated Option Price and IV
Interactive Brokers' API contains methods to calculate current implied volatility (IV) and option prices using Black-Scholes and the price of the underlying. To find the current IV, the option price is also required. Likewise, to calculate an option price, IV is required.
Knowing the actual IV can be useful to use for more accurate OptionSynthetic pricing. The following example demonstrates.
Example 5 - Calculated Option Price and IV
using WealthLab.Backtest;
using System;
using WealthLab.Core;
using WealthLab.Data;
using WealthLab.Indicators;
using System.Collections.Generic;
using WealthLab.InteractiveBrokers;
namespace WealthScript2
{
public class CalculatedGreeks : UserStrategyBase
{
public override void Initialize(BarHistory bars)
{
double price = bars.Close[bars.Count - 1];
DateTime currentDate = bars.DateTimes[bars.Count - 1];
int minDaysToExpiry = 10;
bool weeklies = false;
bool closestStrike = true;
bool allowExpired = false;
string optSym = IBHistorical.Instance.GetOptionsSymbol(bars, OptionType.Call, price, currentDate, minDaysToExpiry, weeklies, allowExpired, closestStrike);
DrawHeaderText("ATM Call: " + optSym, WLColor.NeonGreen, 14);
if (bars.Scale.IsIntraday)
{
_obars = GetHistory(bars, optSym);
}
else // assuming Daily
{
// request 3 months of 30 minute bars and synchronize with the non-intraday scale
_obars = WLHost.Instance.GetHistory(optSym, HistoryScale.Minute30, currentDate.AddMonths(-3), currentDate, 0, null);
if (_obars != null)
{
// compress to Daily bars
_obars = BarHistoryCompressor.ToDaily(_obars);
// sync the bars with the non-intraday data
_obars = BarHistorySynchronizer.Synchronize(_obars, bars);
}
}
double iv = 0;
if (_obars == null)
{
WriteToDebugLog("Could not get option data, check the Log Viewer for more info.");
return;
}
else
{
PlotBarHistory(_obars, optSym);
DrawHeaderText("Option Last Value: " + _obars.LastValue.ToString("N2"), WLColor.NeonOrange, 14);
DrawHeaderText("bars Last Value: " + bars.LastValue.ToString("N2"), WLColor.NeonOrange, 14);
//use the price of the option symbol and underlying to calculate IV
iv = IBHistorical.Instance.CalculateIV(optSym, _obars.LastValue, bars.LastValue);
DrawHeaderText("Calculated IV = " + iv.ToString("N4"), WLColor.NeonRed, 14);
}
if (iv > 0)
{
//use IV and underlying price to calculate the options price
double op = IBHistorical.Instance.CalculateOptionPrice(optSym, iv, bars.LastValue); // priceUnderlying
DrawHeaderText("Calculated Option Price = " + op.ToString("N2"), WLColor.NeonRed, 14);
//use IV to create the synthetic contract - compare it to the live prices
BarHistory osynth = OptionSynthetic.GetHistory(bars, "!" + optSym, iv);
PlotBarHistory(osynth, optSym, WLColor.Cyan);
}
}
//execute the strategy rules here, this is executed once for each bar in the backtest history
public override void Execute(BarHistory bars, int idx)
{ }
BarHistory _obars; // the option's BarHistory
}
}
Long Straddle Strategy
While a Long Straddle itself isn't news, the way it's implemented in the strategy below is novel.
A long straddle options strategy simultaneously holds long positions for calls and puts at the same strike price and expiration. If the stock makes a big move in either direction, this market neutral option combination produces a position that can often result in large profits.
The example code builds on WealthLab strategy programming elements above to create a Spread/Straddle Strategy Template that can be modified and reused for option spread and straddle strategy types.
A key component of the template is a method called PutOnOptionTrade()
. Its arguments include TransactionType, OptionType, strike, minDaysToExpiry, etc., each of which must specify both sides of a spread/straddle trade according to the option strategy. Here are a couple of examples:
// Long Straddle, 60 days to expiration; 0 strike parameters are shortcuts specifying ATM strikes
PutOnOptionTrade(bars, idx, 60, 1, TransactionType.Buy, OptionType.Call, 0, TransactionType.Buy, OptionType.Put, 0, null);
// Bull Put Spread, 30 days to expiration
double strike1 = Math.Ceiling(bars.Close[idx] / 5) * 5;
double strike2 = strike1 - (strike1 > 100 ? 10 : 5);
PutOnOptionTrade(bars, idx, 30, 1, TransactionType.Short, OptionType.Put, strike1, TransactionType.Buy, OptionType.Put, strike2, null);
To customize the strategy, you only need to come up with the entry condition and call PutOnOptionTrade()
to open a spread or straddle position. The strategy holds the positions to expiration, but the exit logic can also be modified easily - to use a profit target, for example.
Finally, the strategy template "as is" employs OptionSynthetic for options pricing using expired contracts. But by substituting a data provider instance (e.g., IBHistorical.Instance) for the last null parameter in PutOnOptionTrade()
, the strategy switches to use live symbol/data when non-expired (live) contracts are specified.
Example 6 - Long Straddle Strategy (Spread and Straddle Template)
using WealthLab.Backtest;
using System;
using WealthLab.Core;
using WealthLab.Data;
using WealthLab.Indicators;
using System.Collections.Generic;
// using WealthLab.InteractiveBrokers; // IBHistorical.Instance
// using WealthLab.Tradier; // TradierHistorical.Instance
// using WealthLab.IQFeed; // IQFeedHistorical.Instance
namespace WealthScript2
{
public class OptionSpreadAndStrangleWrapper : UserStrategyBase
{
public override void Initialize(BarHistory bars)
{
StartIndex = 21;
//set up a daily indicator using Daily bars (CMO example)
BarHistory daily = WLHost.Instance.GetHistory(bars.Symbol, HistoryScale.Daily, bars.DateTimes[0].Date.AddDays(-365), DateTime.Now, 0, null);
_ind = CMO.Series(daily.Close, 10);
_ind = TimeSeriesSynchronizer.Synchronize(_ind, bars);
PlotTimeSeriesLine(_ind, "CMO", "ind", WLColor.NeonBlue);
_spreadOrStrangleProfit = new TimeSeries(bars.DateTimes);
PlotTimeSeriesHistogramTwoColor(_spreadOrStrangleProfit, "Trade Profit", "SP", WLColor.LawnGreen, WLColor.OrangeRed);
}
public override void Execute(BarHistory bars, int idx)
{
Position pos1 = FindOpenPositionAllSymbols(_hash1);
Position pos2 = FindOpenPositionAllSymbols(_hash2);
DateTime dte = bars.DateTimes[idx];
if (pos1 == null && pos2 == null)
{
if (_ind.CrossesOver(50, idx)) /*** modify entry condition here ***/
{
//sample parameters for a Long Strangle: ATM Call and Put, min 60 days to expiration
PutOnOptionTrade(bars, idx, 60, 1, TransactionType.Buy, OptionType.Call, 0, TransactionType.Buy, OptionType.Put, 0, null);
}
}
else if (pos1 == null || pos1.NSF)
{
// ensure the backtest had cash to put on the pair, otherwise exit the orphan
ClosePosition(pos2, OrderType.MarketClose, 0, "Orphan1");
}
else if (pos2 == null || pos2.NSF)
{
// ensure the backtest had cash to put on the pair, otherwise exit the orphan
ClosePosition(pos1, OrderType.MarketClose, 0, "Orphan2");
}
else
{
_spreadOrStrangleProfit[idx] = pos1.ProfitAsOf(idx) + pos2.ProfitAsOf(idx);
bool exitTrade = false;
string reason = "expired";
if (_spreadOrStrangleProfit[idx] > 2000)
{
exitTrade = true;
reason = "profit";
}
// close the trade on expiration or before on some other condition
if (dte.AddDays(1) >= _expiry || exitTrade)
{
ClosePosition(pos1, OrderType.MarketClose, 0, reason);
ClosePosition(pos2, OrderType.MarketClose, 0, reason);
}
}
}
// For any spread or straddle that uses the same expiration date, pass 0 for striks to get ATM strikes
private void PutOnOptionTrade(BarHistory bars, int idx, int minDaysToExpiry, int contractsPerLeg,
TransactionType ttype1, OptionType otype1, double strike1,
TransactionType ttype2, OptionType otype2, double strike2,
DataProviderBase dpInstance = null)
{
if (contractsPerLeg < 1)
throw new InvalidOperationException("contractsPerLeg needs to be 1 or more");
DrawBarAnnotation(TextShape.ArrowDown, idx, true, WLColor.Gold, 20);
DrawBarAnnotation(TextShape.ArrowUp, idx, false, WLColor.Gold, 20);
double ATM = Math.Round(bars.Close[idx] / 5) * 5;
if (strike1 < 5) strike1 = ATM;
if (strike2 < 5) strike2 = ATM;
BarHistory obars1 = GetOptionHistory(bars, idx, otype1, strike1, minDaysToExpiry, dpInstance);
BarHistory obars2 = GetOptionHistory(bars, idx, otype2, strike2, minDaysToExpiry, dpInstance);
if (obars1 != null && obars2 != null)
{
_expiry = OptionSynthetic.GetSymbolExpiry(obars1.Symbol);
_hash1 = Math.Abs(obars1.Symbol.GetHashCode());
Transaction t = PlaceTrade(obars1, ttype1, OrderType.Market, 0, _hash1);
t.Quantity = contractsPerLeg;
t.Weight = _ind[idx];
_hash2 = Math.Abs(obars2.Symbol.GetHashCode());
t = PlaceTrade(obars2, ttype2, OrderType.Market, 0, _hash2);
t.Quantity = contractsPerLeg;
t.Weight = _ind[idx];
}
}
// this routine is a wrapper to return synthetic bars if allowExpired is true, or live options pricing for future expirations if false
private BarHistory GetOptionHistory(BarHistory bars, int bar, OptionType optionType, double price, int minDaysToExpiry, DataProviderBase dpInstance = null)
{
bool liveTrade = dpInstance != null && bars.DateTimes[bar] > DateTime.Now.AddDays(-minDaysToExpiry);
DateTime currDte = bars.DateTimes[bar];
BarHistory obars = null;
//if allowExpired is true use OptionSynthetic, it's not a live trade
string optSym = liveTrade ? dpInstance.GetOptionsSymbol(bars, optionType, price, currDte, minDaysToExpiry)
: OptionSynthetic.GetOptionsSymbol(bars, optionType, price, currDte, minDaysToExpiry, allowExpired: true);
//create the option bars and save it to the bars cache
if (!liveTrade)
{
obars = OptionSynthetic.GetHistory(bars, optSym, _iv);
}
else if (bars.Scale.IsIntraday)
obars = GetHistory(bars, optSym);
else
{
// request 1 year of 30 minute bars and synchronize with the non-intraday scale
obars = WLHost.Instance.GetHistory(optSym, HistoryScale.Minute30, currDte.AddMonths(-12), currDte, 0, null);
if (obars != null)
{
// compress to Daily bars
obars = BarHistoryCompressor.ToDaily(obars);
// sync the bars with the non-intraday data
obars = BarHistorySynchronizer.Synchronize(obars, bars);
}
}
if (liveTrade)
PlotBarHistory(obars, obars.Symbol);
return obars;
}
DateTime _expiry;
double _iv = 0.45;
TimeSeries _ind;
TimeSeries _spreadOrStrangleProfit;
int _hash1 = -1;
int _hash2 = -1;
}
}
We ran the sample code on 2-years of 30-minute bars of NVDA using the IBHistorical.Instance for the final monster trade depicted in the chart below :
Note that the synthetic symbols are indicated using a "!" prefix in the Trades list. The final trade lacks the prefix because we used the IBHistorical.Instance and the strategy switched to the live contracts for the future expiration.
It never hurts to be reminded that past results are no guarantee of future returns, and admittedly we cherry-picked NVDA due to recent and typical high volatility, especially around earnings events. Nevertheless, in a short 2 years, it only took 9 NVDA long straddles to boost the account value from $10,000 to nearly $28,000.
Disclaimer! The examples on this page are not recommended strategies and serve only as informational examples for your own strategy code.