波动率风险溢价
该策略通过卖出期权获取波动率风险溢价,假设隐含波动率平均高于实现波动率。通过 delta 对冲以分离溢价。
空头期权头寸在严格的风险控制和定期再对冲下管理。
细节
- 数据:期权隐含波动率和实现波动率。
- 入场:当隐含 > 实现时卖出价外期权。
- 出场:到期回购或波动率飙升时平仓。
- 工具:指数或外汇期权。
- 风险:delta 对冲和 vega 止损。
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Derivatives;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Volatility risk premium strategy. Sells an out-of-the-money option while its Black-Scholes implied
/// volatility exceeds the realized volatility of the underlying, keeps the short delta-hedged with the
/// underlying on every bar and buys the option back at expiration, on a realized volatility spike or on
/// a vega stop.
/// </summary>
public class VolatilityRiskPremiumStrategy : Strategy
{
private const string _sellComment = "Sell option";
private const string _hedgeComment = "Delta hedge";
private const string _closeHedgeComment = "Close delta hedge";
private const string _buyBackComment = "Buy back option: ";
private readonly StrategyParam<Security> _option;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _realizedVolPeriod;
private readonly StrategyParam<decimal> _tradingHoursPerYear;
private readonly StrategyParam<decimal> _spikeRatio;
private readonly StrategyParam<decimal> _vegaStopPoints;
private readonly StrategyParam<decimal> _dividendYield;
private BlackScholes _model;
private StandardDeviation _returnDeviation;
private TimeSpan _timeFrame;
private decimal _annualization;
private decimal? _previousClose;
private decimal? _realizedVol;
private DateTime? _underlyingTime;
private decimal _underlyingClose;
private DateTime? _optionTime;
private decimal _optionClose;
private DateTime? _matchedTime;
private DateTime? _buyBackTime;
private decimal? _lastImpliedVol;
private decimal _entryImpliedVol;
private decimal _entryRealizedVol;
/// <summary>
/// Initializes a new instance of the <see cref="VolatilityRiskPremiumStrategy"/>.
/// </summary>
public VolatilityRiskPremiumStrategy()
{
_option = Param<Security>(nameof(Option))
.SetDisplay("Option", "Option contract to sell; its underlying is the strategy security", "General")
.SetRequired();
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Bars on which volatility is measured, the option is priced and the hedge is rebalanced", "General");
_realizedVolPeriod = Param(nameof(RealizedVolPeriod), 24)
.SetGreaterThanZero()
.SetDisplay("Realized Vol Period", "Number of bar-to-bar log returns in the realized volatility window", "Volatility")
.SetOptimize(12, 96, 12);
_tradingHoursPerYear = Param(nameof(TradingHoursPerYear), 8760m)
.SetGreaterThanZero()
.SetDisplay("Trading Hours Per Year", "Hours a year the underlying trades; realized volatility is annualized by the bars they hold (8760 round the clock, about 6240 for FX)", "Volatility");
_spikeRatio = Param(nameof(SpikeRatio), 2m)
.SetGreaterThanZero()
.SetDisplay("Spike Ratio", "Realized volatility, as a multiple of its level at the sale, that counts as a spike", "Risk")
.SetOptimize(1.5m, 3m, 0.5m);
_vegaStopPoints = Param(nameof(VegaStopPoints), 5m)
.SetGreaterThanZero()
.SetDisplay("Vega Stop (vol points)", "Rise of implied volatility above its level at the sale that stops out the short", "Risk")
.SetOptimize(2m, 10m, 1m);
_dividendYield = Param(nameof(DividendYield), 0m)
.SetDisplay("Dividend Yield", "Annual dividend yield of the underlying, or the foreign rate for FX options", "Option Model");
}
/// <summary>
/// Option contract to sell. Its <see cref="Security.UnderlyingSecurityId"/> must be the strategy security,
/// which supplies the realized volatility and carries the delta hedge; its <see cref="Security.ExpiryDate"/>
/// is the exact expiration moment in UTC and its <see cref="Security.Multiplier"/> the units of the
/// underlying per contract.
/// </summary>
public Security Option
{
get => _option.Value;
set => _option.Value = value;
}
/// <summary>
/// Time-frame candles on which realized volatility is measured, the option is priced and the hedge is rebalanced.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Number of bar-to-bar log returns of the underlying in the realized volatility window.
/// </summary>
public int RealizedVolPeriod
{
get => _realizedVolPeriod.Value;
set => _realizedVolPeriod.Value = value;
}
/// <summary>
/// Hours a year in which the underlying trades. Realized volatility is annualized by the number of bars that
/// fit in them: 8760 for a round-the-clock market, about 6240 for FX traded five days a week.
/// </summary>
public decimal TradingHoursPerYear
{
get => _tradingHoursPerYear.Value;
set => _tradingHoursPerYear.Value = value;
}
/// <summary>
/// Realized volatility, as a multiple of its level when the option was sold, at which the option is bought back.
/// </summary>
public decimal SpikeRatio
{
get => _spikeRatio.Value;
set => _spikeRatio.Value = value;
}
/// <summary>
/// Rise of implied volatility above its level when the option was sold, in volatility points, at which the
/// short is stopped out: the loss it caps is this many vegas of the position.
/// </summary>
public decimal VegaStopPoints
{
get => _vegaStopPoints.Value;
set => _vegaStopPoints.Value = value;
}
/// <summary>
/// Annual continuous dividend yield of the underlying used by the Black-Scholes model; for FX options, the foreign
/// rate. The model discounts the strike with <see cref="Strategy.RiskFreeRate"/>.
/// </summary>
public decimal DividendYield
{
get => _dividendYield.Value;
set => _dividendYield.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security != null)
yield return (Security, CandleType);
if (Option != null)
yield return (Option, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ClearState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
// Reject an incomplete contract before any subscription starts.
ValidateOption();
base.OnStarted2(time);
ClearState();
_timeFrame = (TimeSpan)CandleType.Arg;
// Variance per bar times the bars traded in a year; time to expiry stays in calendar time, as the option model needs.
_annualization = (decimal)Math.Sqrt((double)TradingHoursPerYear * TimeSpan.TicksPerHour / _timeFrame.Ticks);
_model = new BlackScholes(Option, Security, this, Option.ExpiryDate)
{
// The annual risk-free rate every strategy carries discounts the strike.
RiskFree = RiskFreeRate,
Dividend = DividendYield,
};
_returnDeviation = new StandardDeviation { Length = RealizedVolPeriod };
Indicators.Add(_returnDeviation);
var underlying = SubscribeCandles(CandleType);
underlying.Bind(ProcessUnderlyingCandle).Start();
SubscribeCandles(CandleType, security: Option)
.Bind(ProcessOptionCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, underlying);
DrawOwnTrades(area);
}
}
private void ValidateOption()
{
var option = Option;
if (Security is null || option is null || option.Id.EqualsIgnoreCase(Security.Id))
throw new InvalidOperationException("Option must be an option contract distinct from the strategy security.");
if (option.Type != SecurityTypes.Option || option.OptionType is null || option.Strike is not > 0m || option.ExpiryDate is null)
throw new InvalidOperationException("Option must be an option contract with its type, a positive strike and an expiry.");
if (!option.UnderlyingSecurityId.EqualsIgnoreCase(Security.Id))
throw new InvalidOperationException("Option must be written on the strategy security, which measures realized volatility and carries the hedge.");
if (!CandleType.IsTFCandles || CandleType.Arg is not TimeSpan frame || frame <= TimeSpan.Zero)
throw new InvalidOperationException("CandleType must be a time-frame candle type for the Option bars.");
}
private void ClearState()
{
_model = null;
_returnDeviation = null;
_timeFrame = default;
_annualization = 0m;
_previousClose = null;
_realizedVol = null;
_underlyingTime = null;
_underlyingClose = 0m;
_optionTime = null;
_optionClose = 0m;
_matchedTime = null;
_buyBackTime = null;
_lastImpliedVol = null;
_entryImpliedVol = 0m;
_entryRealizedVol = 0m;
}
private void ProcessUnderlyingCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished || _underlyingTime is DateTime last && candle.OpenTime <= last)
return;
var close = candle.ClosePrice;
if (_previousClose is decimal previous && previous > 0m && close > 0m)
{
var logReturn = (decimal)Math.Log((double)(close / previous));
var deviation = _returnDeviation.Process(logReturn, candle.OpenTime, true).GetValue<decimal>();
_realizedVol = _returnDeviation.IsFormed ? deviation * _annualization : null;
}
_previousClose = close;
_underlyingTime = candle.OpenTime;
_underlyingClose = close;
ProcessUnderlyingBar();
ProcessMatchedBar();
}
private void ProcessOptionCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished || _optionTime is DateTime last && candle.OpenTime <= last)
return;
_optionTime = candle.OpenTime;
_optionClose = candle.ClosePrice;
ProcessMatchedBar();
}
// Expiration, the spike exit and the hedge need no option price, so they never wait for the option to trade.
// A hedge left without its option no longer offsets anything and is closed.
private void ProcessUnderlyingBar()
{
if (_realizedVol is not decimal realizedVol || !IsFormedAndOnlineAndAllowTrading() || HasActiveOrders())
return;
var optionPosition = GetPositionValue(Option, Portfolio) ?? 0m;
if (optionPosition < 0m)
ManageShort(_underlyingTime.Value, realizedVol, -optionPosition);
else if (optionPosition == 0m)
CloseHedge();
}
private void ManageShort(DateTime openTime, decimal realizedVol, decimal shortContracts)
{
var time = openTime + _timeFrame;
// The last bar that closes before expiration is the last chance to buy the option back.
if (Option.ExpiryDate.Value - time <= _timeFrame)
BuyBack(openTime, shortContracts, "expiration");
else if (realizedVol >= SpikeRatio * _entryRealizedVol)
BuyBack(openTime, shortContracts, "volatility spike");
else if (_lastImpliedVol is decimal impliedVol)
Rebalance(time, impliedVol, shortContracts);
}
// The sale and the vega stop price the option, so they need its close from the same bar as the underlying's.
private void ProcessMatchedBar()
{
if (_underlyingTime is not DateTime openTime || _optionTime != openTime || _matchedTime == openTime)
return;
_matchedTime = openTime;
var time = openTime + _timeFrame;
var remaining = Option.ExpiryDate.Value - time;
if (_realizedVol is not decimal realizedVol || remaining <= TimeSpan.Zero || ImpliedVolatility(time) is not decimal impliedVol)
return;
_lastImpliedVol = impliedVol;
// One option trade per bar: a bar that bought the option back does not sell it again.
if (_buyBackTime == openTime || !IsFormedAndOnlineAndAllowTrading() || HasActiveOrders())
return;
var optionPosition = GetPositionValue(Option, Portfolio) ?? 0m;
if (optionPosition < 0m && impliedVol - _entryImpliedVol >= VegaStopPoints / 100m)
BuyBack(openTime, -optionPosition, "vega stop");
else if (optionPosition == 0m && Position == 0m)
TrySell(time, remaining, realizedVol, impliedVol);
}
private void TrySell(DateTime time, TimeSpan remaining, decimal realizedVol, decimal impliedVol)
{
var strike = Option.Strike.Value;
var isOutOfTheMoney = Option.OptionType == OptionTypes.Call ? strike > _underlyingClose : strike < _underlyingClose;
if (impliedVol <= realizedVol || !isOutOfTheMoney || remaining <= _timeFrame)
return;
LogInfo($"Selling {Volume} option(s): IV={impliedVol} above RV={realizedVol}.");
_entryImpliedVol = impliedVol;
_entryRealizedVol = realizedVol;
SendOrder(Sides.Sell, Volume, Option, _sellComment);
Rebalance(time, impliedVol, Volume);
}
private void BuyBack(DateTime openTime, decimal shortContracts, string reason)
{
LogInfo($"Buying back {shortContracts} option(s) on {reason}: IV={_lastImpliedVol}, RV={_realizedVol}.");
_buyBackTime = openTime;
SendOrder(Sides.Buy, shortContracts, Option, _buyBackComment + reason);
CloseHedge();
}
private void Rebalance(DateTime time, decimal impliedVol, decimal shortContracts)
{
if (_model.Delta(time, impliedVol, _underlyingClose) is not decimal delta)
return;
var multiplier = Option.Multiplier is decimal contractSize && contractSize > 0m ? contractSize : 1m;
// Holding delta units of the underlying per unit of the short option neutralizes it: long for a call, short for a put.
var target = shortContracts * multiplier * delta;
var step = Security.VolumeStep ?? 0m;
if (step > 0m)
target = Math.Round(target / step, MidpointRounding.AwayFromZero) * step;
var change = target - Position;
if (change != 0m)
SendOrder(change > 0m ? Sides.Buy : Sides.Sell, Math.Abs(change), Security, _hedgeComment);
}
private void CloseHedge()
{
var position = Position;
if (position != 0m)
SendOrder(position > 0m ? Sides.Sell : Sides.Buy, Math.Abs(position), Security, _closeHedgeComment);
}
private decimal? ImpliedVolatility(DateTime time)
=> DerivativesHelper.ImpliedVolatility(_optionClose, deviation => _model.Premium(time, deviation, _underlyingClose)) / 100m;
private bool HasActiveOrders()
=> Orders.Any(order => order.State is not (OrderStates.Done or OrderStates.Failed));
private void SendOrder(Sides side, decimal volume, Security security, string comment)
{
var order = CreateOrder(side, 0m, volume, security);
order.Comment = comment;
RegisterOrder(order);
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.BusinessEntities")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal, Func, Nullable, MidpointRounding, InvalidOperationException
from StockSharp.Messages import DataType, CandleStates, OrderStates, Sides, SecurityTypes, OptionTypes
from StockSharp.BusinessEntities import Security
from StockSharp.Algo.Derivatives import BlackScholes, DerivativesHelper
from StockSharp.Algo.Indicators import StandardDeviation, DecimalIndicatorValue
from StockSharp.Algo.Strategies import Strategy
SELL_COMMENT = "Sell option"
HEDGE_COMMENT = "Delta hedge"
CLOSE_HEDGE_COMMENT = "Close delta hedge"
BUY_BACK_COMMENT = "Buy back option: "
class volatility_risk_premium_strategy(Strategy):
"""
Volatility risk premium strategy. Sells an out-of-the-money option while its Black-Scholes implied
volatility exceeds the realized volatility of the underlying, keeps the short delta-hedged with the
underlying on every bar and buys the option back at expiration, on a realized volatility spike or on
a vega stop.
"""
def __init__(self):
super(volatility_risk_premium_strategy, self).__init__()
self._option = self.Param[Security]("Option", None) \
.SetDisplay("Option", "Option contract to sell; its underlying is the strategy security", "General") \
.SetRequired()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Bars on which volatility is measured, the option is priced and the hedge is rebalanced", "General")
self._realized_vol_period = self.Param("RealizedVolPeriod", 24) \
.SetGreaterThanZero() \
.SetDisplay("Realized Vol Period", "Number of bar-to-bar log returns in the realized volatility window", "Volatility")
self._trading_hours_per_year = self.Param("TradingHoursPerYear", 8760.0) \
.SetGreaterThanZero() \
.SetDisplay("Trading Hours Per Year", "Hours a year the underlying trades; realized volatility is annualized by the bars they hold (8760 round the clock, about 6240 for FX)", "Volatility")
self._spike_ratio = self.Param("SpikeRatio", 2.0) \
.SetGreaterThanZero() \
.SetDisplay("Spike Ratio", "Realized volatility, as a multiple of its level at the sale, that counts as a spike", "Risk")
self._vega_stop_points = self.Param("VegaStopPoints", 5.0) \
.SetGreaterThanZero() \
.SetDisplay("Vega Stop (vol points)", "Rise of implied volatility above its level at the sale that stops out the short", "Risk")
self._dividend_yield = self.Param("DividendYield", 0.0) \
.SetDisplay("Dividend Yield", "Annual dividend yield of the underlying, or the foreign rate for FX options", "Option Model")
self._clear_state()
@property
def Option(self):
return self._option.Value
@Option.setter
def Option(self, value):
self._option.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def RealizedVolPeriod(self):
return self._realized_vol_period.Value
@RealizedVolPeriod.setter
def RealizedVolPeriod(self, value):
self._realized_vol_period.Value = value
@property
def TradingHoursPerYear(self):
return self._trading_hours_per_year.Value
@TradingHoursPerYear.setter
def TradingHoursPerYear(self, value):
self._trading_hours_per_year.Value = value
@property
def SpikeRatio(self):
return self._spike_ratio.Value
@SpikeRatio.setter
def SpikeRatio(self, value):
self._spike_ratio.Value = value
@property
def VegaStopPoints(self):
return self._vega_stop_points.Value
@VegaStopPoints.setter
def VegaStopPoints(self, value):
self._vega_stop_points.Value = value
@property
def DividendYield(self):
return self._dividend_yield.Value
@DividendYield.setter
def DividendYield(self, value):
self._dividend_yield.Value = value
def GetWorkingSecurities(self):
result = []
if self.Security is not None:
result.append((self.Security, self.CandleType))
if self.Option is not None:
result.append((self.Option, self.CandleType))
return result
def OnReseted(self):
super(volatility_risk_premium_strategy, self).OnReseted()
self._clear_state()
def OnStarted2(self, time):
# Reject an incomplete contract before any subscription starts.
self._validate_option()
super(volatility_risk_premium_strategy, self).OnStarted2(time)
self._clear_state()
self._time_frame = self.CandleType.Arg
# Variance per bar times the bars traded in a year; time to expiry stays in calendar time, as the option model needs.
self._annualization = Decimal(Math.Sqrt(float(self.TradingHoursPerYear) * float(TimeSpan.TicksPerHour) / float(self._time_frame.Ticks)))
self._model = BlackScholes(self.Option, self.Security, self, self.Option.ExpiryDate)
# The annual risk-free rate every strategy carries discounts the strike.
self._model.RiskFree = self.RiskFreeRate
self._model.Dividend = Decimal(self.DividendYield)
self._return_deviation = StandardDeviation()
self._return_deviation.Length = self.RealizedVolPeriod
self.Indicators.Add(self._return_deviation)
underlying = self.SubscribeCandles(self.CandleType)
underlying.Bind(self._process_underlying_candle).Start()
# The second positional argument is isFinishedOnly, not the instrument.
self.SubscribeCandles(self.CandleType, security=self.Option) \
.Bind(self._process_option_candle) \
.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, underlying)
self.DrawOwnTrades(area)
def _validate_option(self):
option = self.Option
if self.Security is None or option is None or str(option.Id).lower() == str(self.Security.Id).lower():
raise InvalidOperationException("Option must be an option contract distinct from the strategy security.")
if option.Type != SecurityTypes.Option or option.OptionType is None or option.Strike is None or option.Strike <= 0 or option.ExpiryDate is None:
raise InvalidOperationException("Option must be an option contract with its type, a positive strike and an expiry.")
if option.UnderlyingSecurityId is None or str(option.UnderlyingSecurityId).lower() != str(self.Security.Id).lower():
raise InvalidOperationException("Option must be written on the strategy security, which measures realized volatility and carries the hedge.")
frame = self.CandleType.Arg
if not self.CandleType.IsTFCandles or not isinstance(frame, TimeSpan) or frame <= TimeSpan.Zero:
raise InvalidOperationException("CandleType must be a time-frame candle type for the Option bars.")
def _clear_state(self):
self._model = None
self._return_deviation = None
self._time_frame = TimeSpan.Zero
self._annualization = Decimal.Zero
self._previous_close = None
self._realized_vol = None
self._underlying_time = None
self._underlying_close = Decimal.Zero
self._option_time = None
self._option_close = Decimal.Zero
self._matched_time = None
self._buy_back_time = None
self._last_implied_vol = None
self._entry_implied_vol = Decimal.Zero
self._entry_realized_vol = Decimal.Zero
def _process_underlying_candle(self, candle):
if candle.State != CandleStates.Finished or (self._underlying_time is not None and candle.OpenTime <= self._underlying_time):
return
close = candle.ClosePrice
if self._previous_close is not None and self._previous_close > 0 and close > 0:
log_return = Decimal(Math.Log(Decimal.ToDouble(close / self._previous_close)))
indicator_input = DecimalIndicatorValue(self._return_deviation, log_return, candle.OpenTime)
indicator_input.IsFinal = True
deviation = self._return_deviation.Process(indicator_input).GetValue[Decimal](None)
self._realized_vol = deviation * self._annualization if self._return_deviation.IsFormed else None
self._previous_close = close
self._underlying_time = candle.OpenTime
self._underlying_close = close
self._process_underlying_bar()
self._process_matched_bar()
def _process_option_candle(self, candle):
if candle.State != CandleStates.Finished or (self._option_time is not None and candle.OpenTime <= self._option_time):
return
self._option_time = candle.OpenTime
self._option_close = candle.ClosePrice
self._process_matched_bar()
# Expiration, the spike exit and the hedge need no option price, so they never wait for the option to trade.
# A hedge left without its option no longer offsets anything and is closed.
def _process_underlying_bar(self):
realized_vol = self._realized_vol
if realized_vol is None or not self.IsFormedAndOnlineAndAllowTrading() or self._has_active_orders():
return
option_position = self._option_position()
if option_position < 0:
self._manage_short(self._underlying_time, realized_vol, -option_position)
elif option_position == 0:
self._close_hedge()
def _manage_short(self, open_time, realized_vol, short_contracts):
time = open_time + self._time_frame
# The last bar that closes before expiration is the last chance to buy the option back.
if self.Option.ExpiryDate - time <= self._time_frame:
self._buy_back(open_time, short_contracts, "expiration")
elif realized_vol >= Decimal(self.SpikeRatio) * self._entry_realized_vol:
self._buy_back(open_time, short_contracts, "volatility spike")
elif self._last_implied_vol is not None:
self._rebalance(time, self._last_implied_vol, short_contracts)
# The sale and the vega stop price the option, so they need its close from the same bar as the underlying's.
def _process_matched_bar(self):
open_time = self._underlying_time
if open_time is None or self._option_time != open_time or self._matched_time == open_time:
return
self._matched_time = open_time
time = open_time + self._time_frame
remaining = self.Option.ExpiryDate - time
realized_vol = self._realized_vol
if realized_vol is None or remaining <= TimeSpan.Zero:
return
implied_vol = self._implied_volatility(time)
if implied_vol is None:
return
self._last_implied_vol = implied_vol
# One option trade per bar: a bar that bought the option back does not sell it again.
if self._buy_back_time == open_time or not self.IsFormedAndOnlineAndAllowTrading() or self._has_active_orders():
return
option_position = self._option_position()
if option_position < 0 and implied_vol - self._entry_implied_vol >= Decimal(self.VegaStopPoints) / Decimal(100):
self._buy_back(open_time, -option_position, "vega stop")
elif option_position == 0 and self.Position == 0:
self._try_sell(time, remaining, realized_vol, implied_vol)
def _try_sell(self, time, remaining, realized_vol, implied_vol):
strike = self.Option.Strike
if self.Option.OptionType == OptionTypes.Call:
is_out_of_the_money = strike > self._underlying_close
else:
is_out_of_the_money = strike < self._underlying_close
if implied_vol <= realized_vol or not is_out_of_the_money or remaining <= self._time_frame:
return
self.LogInfo("Selling {0} option(s): IV={1} above RV={2}.".format(self.Volume, implied_vol, realized_vol))
self._entry_implied_vol = implied_vol
self._entry_realized_vol = realized_vol
self._send_order(Sides.Sell, self.Volume, self.Option, SELL_COMMENT)
self._rebalance(time, implied_vol, self.Volume)
def _buy_back(self, open_time, short_contracts, reason):
self.LogInfo("Buying back {0} option(s) on {1}: IV={2}, RV={3}.".format(short_contracts, reason, self._last_implied_vol, self._realized_vol))
self._buy_back_time = open_time
self._send_order(Sides.Buy, short_contracts, self.Option, BUY_BACK_COMMENT + reason)
self._close_hedge()
def _rebalance(self, time, implied_vol, short_contracts):
delta = self._model.Delta(time, implied_vol, self._underlying_close)
if delta is None:
return
contract_size = self.Option.Multiplier
multiplier = contract_size if contract_size is not None and contract_size > 0 else Decimal.One
# Holding delta units of the underlying per unit of the short option neutralizes it: long for a call, short for a put.
target = short_contracts * multiplier * delta
step = self.Security.VolumeStep if self.Security.VolumeStep is not None else Decimal.Zero
if step > 0:
target = Math.Round(target / step, MidpointRounding.AwayFromZero) * step
change = target - self.Position
if change != 0:
self._send_order(Sides.Buy if change > 0 else Sides.Sell, Math.Abs(change), self.Security, HEDGE_COMMENT)
def _close_hedge(self):
position = self.Position
if position != 0:
self._send_order(Sides.Sell if position > 0 else Sides.Buy, Math.Abs(position), self.Security, CLOSE_HEDGE_COMMENT)
def _implied_volatility(self, time):
model = self._model
asset_price = self._underlying_close
premium = Func[Decimal, Nullable[Decimal]](lambda deviation: model.Premium(time, deviation, asset_price))
percent = DerivativesHelper.ImpliedVolatility(self._option_close, premium)
return None if percent is None else percent / Decimal(100)
def _option_position(self):
position = self.GetPositionValue(self.Option, self.Portfolio)
return position if position is not None else Decimal.Zero
def _has_active_orders(self):
for order in self.Orders:
if order.State != OrderStates.Done and order.State != OrderStates.Failed:
return True
return False
def _send_order(self, side, volume, security, comment):
order = self.CreateOrder(side, Decimal.Zero, volume, security)
order.Comment = comment
self.RegisterOrder(order)
def CreateClone(self):
return volatility_risk_premium_strategy()