Binomial Option Pricing Model
Valuation rules
A nontrading two-step Cox–Ross–Rubinstein option valuation module. It never submits orders. CalculatePrice(spot, volatility, timeToExpiry) returns a price for configured strike, call/put, European/American exercise and asset class. Rates and volatility are annual fractions: 0.05 means 5%. Time is converted to years of 365 days; minutes and hours are supported.
For dt = T / 2, set u = exp(volatility * sqrt(dt)), d = 1/u, p = (exp(b * dt) - d) / (u - d). Carry b = r - q for Stock, Index and Currency (q is dividend yield/foreign rate); b = 0 for Future. Discount each backward step by exp(-r * dt). European options exercise at maturity; American options compare continuation with intrinsic value at every node. At zero tenor return intrinsic value; zero volatility follows the deterministic carry path with the same exercise rules. Unsupported classes/styles and invalid probabilities fail explicitly, without clamping.
EvaluatePrice(candle) processes each finished source candle once. After Length closes, estimate annual volatility as population SD(close) / mean(close) * sqrt(365 days / candle interval). Price the configured TimeToExpiry, expose LastOptionPrice, log the value and plot source candles/deviation where a chart is attached. This is a rolling price-level dispersion estimate, not a return-based volatility estimator or an execution signal.
Parameters
| Parameter | Default |
|---|---|
StrikePrice |
100 |
DividendYield |
0 |
AssetClass |
Stock |
OptionStyle |
European |
OptionType |
Call |
TimeToExpiry |
30d |
Length |
20 |
CandleType |
1m |
RiskFreeRate |
0.05 |
namespace StockSharp.Samples.Strategies;
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using Ecng.Logging;
using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
/// <summary>Two-step Cox-Ross-Rubinstein price calculator with explicit carry and exercise conventions.</summary>
public class BinomialOptionPricingModelStrategy : Strategy
{
private readonly StrategyParam<decimal> _strikePrice;
private readonly StrategyParam<decimal> _dividendYield;
private readonly StrategyParam<SecurityTypes> _assetClass;
private readonly StrategyParam<OptionStyles> _optionStyle;
private readonly StrategyParam<OptionTypes> _optionType;
private readonly StrategyParam<TimeSpan> _timeToExpiry;
private readonly StrategyParam<int> _length;
private readonly StrategyParam<DataType> _candleType;
private StandardDeviation _deviation;
private SimpleMovingAverage _average;
private DateTime _lastTime;
/// <summary>Positive option strike.</summary>
public decimal StrikePrice
{
get => _strikePrice.Value;
set => _strikePrice.Value = value;
}
/// <summary>Continuous dividend or foreign interest rate as a fraction.</summary>
public decimal DividendYield
{
get => _dividendYield.Value;
set => _dividendYield.Value = value;
}
/// <summary>Stock, Index, Currency or Future carry convention.</summary>
public SecurityTypes AssetClass
{
get => _assetClass.Value;
set => _assetClass.Value = value;
}
/// <summary>European maturity exercise or American tree-node exercise.</summary>
public OptionStyles OptionStyle
{
get => _optionStyle.Value;
set => _optionStyle.Value = value;
}
/// <summary>Call or put payoff.</summary>
public OptionTypes OptionType
{
get => _optionType.Value;
set => _optionType.Value = value;
}
/// <summary>Remaining tenor; minutes, hours and days are supported.</summary>
public TimeSpan TimeToExpiry
{
get => _timeToExpiry.Value;
set => _timeToExpiry.Value = value;
}
/// <summary>Population close deviation and mean window.</summary>
public int Length
{
get => _length.Value;
set => _length.Value = value;
}
/// <summary>Finished source candle interval.</summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>Latest formed theoretical option price.</summary>
public decimal? LastOptionPrice { get; private set; }
/// <summary>Initializes pricing parameters and the inherited fractional RiskFreeRate.</summary>
public BinomialOptionPricingModelStrategy()
{
_strikePrice = Param(nameof(StrikePrice), 100m).SetGreaterThanZero()
.SetDisplay("StrikePrice", "Positive option strike", "Parameters");
_dividendYield = Param(nameof(DividendYield), 0m)
.SetDisplay("DividendYield", "Continuous dividend or foreign interest rate as a fraction", "Parameters");
_assetClass = Param(nameof(AssetClass), SecurityTypes.Stock)
.SetDisplay("AssetClass", "Stock, Index, Currency or Future carry convention", "Parameters");
_optionStyle = Param(nameof(OptionStyle), OptionStyles.European)
.SetDisplay("OptionStyle", "European maturity exercise or American tree-node exercise", "Parameters");
_optionType = Param(nameof(OptionType), OptionTypes.Call)
.SetDisplay("OptionType", "Call or put payoff", "Parameters");
_timeToExpiry = Param(nameof(TimeToExpiry), TimeSpan.FromDays(30))
.SetDisplay("TimeToExpiry", "Remaining tenor; minutes, hours and days are supported", "Parameters");
_length = Param(nameof(Length), 20).SetGreaterThanZero()
.SetDisplay("Length", "Population close deviation and mean window", "Parameters");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(1).TimeFrame())
.SetDisplay("CandleType", "Finished source candle interval", "Parameters");
RiskFreeRate = 0.05m;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities() => [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_deviation = null;
_average = null;
_lastTime = default;
LastOptionPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
EnsureIndicators();
base.OnStarted2(time);
var candles = SubscribeCandles(CandleType);
candles.Bind(candle => EvaluatePrice(candle)).Start();
var area = CreateChartArea();
if (area is not null)
{
DrawCandles(area, candles);
DrawIndicator(area, _deviation);
}
}
private void EnsureIndicators()
{
if (_deviation is not null && _deviation.Length == Length) return;
_deviation = new StandardDeviation { Length = Length };
_average = new SimpleMovingAverage { Length = Length };
}
/// <summary>Evaluates one finished candle; returns null until the close window is formed or when the candle is ignored.</summary>
/// <param name="candle">UTC candle to add to the pricing window.</param>
/// <returns>The theoretical price, with no trading side effects.</returns>
public decimal? EvaluatePrice(ICandleMessage candle)
{
ArgumentNullException.ThrowIfNull(candle);
if (candle.State != CandleStates.Finished || candle.OpenTime <= _lastTime) return null;
if (candle.ClosePrice <= 0m) throw new ArgumentOutOfRangeException(nameof(candle), "A positive underlying close is required.");
if (CandleType.Arg is not TimeSpan frame || frame <= TimeSpan.Zero)
throw new InvalidOperationException("A positive time-frame candle interval is required for annualization.");
EnsureIndicators();
_lastTime = candle.OpenTime;
var deviation = _deviation.Process(new DecimalIndicatorValue(_deviation, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
var average = _average.Process(new DecimalIndicatorValue(_average, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
if (!_deviation.IsFormed || !_average.IsFormed) return null;
var sigma = (decimal)((double)(deviation.GetValue<decimal>() / average.GetValue<decimal>())
* Math.Sqrt(TimeSpan.FromDays(365).TotalSeconds / frame.TotalSeconds));
LastOptionPrice = CalculatePrice(candle.ClosePrice, sigma, TimeToExpiry);
this.AddInfoLog("Option price at {0:O}: {1}", candle.OpenTime, LastOptionPrice.Value);
return LastOptionPrice;
}
/// <summary>Calculates a two-step binomial value, including early exercise and zero-volatility paths.</summary>
/// <param name="spot">Positive spot or current futures price.</param>
/// <param name="volatility">Nonnegative annual fractional volatility.</param>
/// <param name="timeToExpiry">Nonnegative remaining tenor on an ACT/365 basis.</param>
/// <returns>Theoretical call or put price.</returns>
public decimal CalculatePrice(decimal spot, decimal volatility, TimeSpan timeToExpiry)
{
if (spot <= 0m || volatility < 0m || timeToExpiry < TimeSpan.Zero || StrikePrice <= 0m)
throw new ArgumentOutOfRangeException(nameof(spot), "Spot and strike must be positive; volatility and tenor must be nonnegative.");
if (OptionStyle is not (OptionStyles.European or OptionStyles.American) || !Enum.IsDefined(OptionType)
|| AssetClass is not (SecurityTypes.Stock or SecurityTypes.Index or SecurityTypes.Currency or SecurityTypes.Future))
throw new ArgumentException("Unsupported option style, payoff or underlying asset class.");
var s = (double)spot; var strike = (double)StrikePrice;
double Payoff(double value) => Math.Max(OptionType == OptionTypes.Call ? value - strike : strike - value, 0.0);
if (timeToExpiry == TimeSpan.Zero) return (decimal)Payoff(s);
var years = timeToExpiry.TotalDays / 365.0;
var dt = years / 2.0;
var rate = (double)RiskFreeRate;
var carry = AssetClass == SecurityTypes.Future ? 0.0 : rate - (double)DividendYield;
if (volatility == 0m)
{
var final = Payoff(s * Math.Exp(carry * years)) * Math.Exp(-rate * years);
if (OptionStyle == OptionStyles.American)
final = Math.Max(Payoff(s), Math.Max(final, Payoff(s * Math.Exp(carry * dt)) * Math.Exp(-rate * dt)));
return (decimal)final;
}
var up = Math.Exp((double)volatility * Math.Sqrt(dt));
var down = 1.0 / up;
var growth = Math.Exp(carry * dt);
var probability = (growth - down) / (up - down);
var discount = Math.Exp(-rate * dt);
if (!double.IsFinite(probability) || probability < 0.0 || probability > 1.0 || !double.IsFinite(up) || !double.IsFinite(discount))
throw new ArgumentException("Two-step inputs do not admit a finite risk-neutral probability in [0,1].");
var upper = discount * (probability * Payoff(s * up * up) + (1.0 - probability) * Payoff(s));
var lower = discount * (probability * Payoff(s) + (1.0 - probability) * Payoff(s * down * down));
if (OptionStyle == OptionStyles.American)
{
upper = Math.Max(upper, Payoff(s * up));
lower = Math.Max(lower, Payoff(s * down));
}
var price = discount * (probability * upper + (1.0 - probability) * lower);
if (OptionStyle == OptionStyles.American) price = Math.Max(price, Payoff(s));
return (decimal)price;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
clr.AddReference("StockSharp.BusinessEntities")
import math
from collections import deque
from System import TimeSpan, DateTime, DateTimeKind, Decimal, Nullable
from StockSharp.Messages import DataType, CandleStates, Sides, OrderStates, Unit, UnitTypes
from StockSharp.BusinessEntities import Subscription
from StockSharp.Algo.Strategies import Strategy
clr.AddReference("Ecng.Logging")
import Ecng.Logging
clr.ImportExtensions(Ecng.Logging)
from StockSharp.Messages import SecurityTypes, OptionTypes, OptionStyles
from StockSharp.Algo.Indicators import StandardDeviation, SimpleMovingAverage, DecimalIndicatorValue
class binomial_option_pricing_model_strategy(Strategy):
def __init__(self):
super(binomial_option_pricing_model_strategy, self).__init__()
self._strike_price = self.Param("StrikePrice", 100.0).SetGreaterThanZero().SetDisplay(
"StrikePrice", "Positive option strike", "Parameters")
self._dividend_yield = self.Param("DividendYield", 0.0).SetDisplay(
"DividendYield", "Continuous dividend or foreign interest rate as a fraction", "Parameters")
self._asset_class = self.Param("AssetClass", SecurityTypes.Stock).SetDisplay(
"AssetClass", "Stock, Index, Currency or Future carry convention", "Parameters")
self._option_style = self.Param("OptionStyle", OptionStyles.European).SetDisplay(
"OptionStyle", "European maturity exercise or American tree-node exercise", "Parameters")
self._option_type = self.Param("OptionType", OptionTypes.Call).SetDisplay(
"OptionType", "Call or put payoff", "Parameters")
self._time_to_expiry = self.Param("TimeToExpiry", TimeSpan.FromDays(30)).SetDisplay(
"TimeToExpiry", "Remaining tenor; minutes, hours and days are supported", "Parameters")
self._length = self.Param("Length", 20).SetGreaterThanZero().SetDisplay(
"Length", "Population close deviation and mean window", "Parameters")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(1))).SetDisplay(
"CandleType", "Finished source candle interval", "Parameters")
self.RiskFreeRate = Decimal(0.05)
self._deviation = self._average = None
self._last_time = DateTime.MinValue
self._last_option_price = None
@property
def StrikePrice(self):
return self._strike_price.Value
@StrikePrice.setter
def StrikePrice(self, value):
self._strike_price.Value = value
@property
def DividendYield(self):
return self._dividend_yield.Value
@DividendYield.setter
def DividendYield(self, value):
self._dividend_yield.Value = value
@property
def AssetClass(self):
return self._asset_class.Value
@AssetClass.setter
def AssetClass(self, value):
self._asset_class.Value = value
@property
def OptionStyle(self):
return self._option_style.Value
@OptionStyle.setter
def OptionStyle(self, value):
self._option_style.Value = value
@property
def OptionType(self):
return self._option_type.Value
@OptionType.setter
def OptionType(self, value):
self._option_type.Value = value
@property
def TimeToExpiry(self):
return self._time_to_expiry.Value
@TimeToExpiry.setter
def TimeToExpiry(self, value):
self._time_to_expiry.Value = value
@property
def Length(self):
return self._length.Value
@Length.setter
def Length(self, value):
self._length.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def LastOptionPrice(self):
return self._last_option_price
def OnReseted(self):
super(binomial_option_pricing_model_strategy, self).OnReseted()
self._deviation = self._average = None
self._last_time = DateTime.MinValue
self._last_option_price = None
def GetWorkingSecurities(self):
return [(self.Security, self.CandleType)]
def _ensure_indicators(self):
if self._deviation is not None and self._deviation.Length == self.Length:
return
self._deviation, self._average = StandardDeviation(), SimpleMovingAverage()
self._deviation.Length = self._average.Length = self.Length
def OnStarted2(self, time):
self._ensure_indicators()
super(binomial_option_pricing_model_strategy, self).OnStarted2(time)
candles = self.SubscribeCandles(self.CandleType)
candles.Bind(self._on_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, candles)
self.DrawIndicator(area, self._deviation)
def _on_candle(self, candle):
self.EvaluatePrice(candle)
def EvaluatePrice(self, candle):
if candle is None:
raise ValueError("A candle is required.")
if candle.State != CandleStates.Finished or candle.OpenTime <= self._last_time:
return None
if candle.ClosePrice <= 0:
raise ValueError("A positive underlying close is required.")
frame = self.CandleType.Arg
if not isinstance(frame, TimeSpan) or frame <= TimeSpan.Zero:
raise ValueError("A positive time-frame candle interval is required for annualization.")
self._ensure_indicators()
self._last_time = candle.OpenTime
deviation_input = DecimalIndicatorValue(self._deviation, candle.ClosePrice, candle.OpenTime)
deviation_input.IsFinal = True
average_input = DecimalIndicatorValue(self._average, candle.ClosePrice, candle.OpenTime)
average_input.IsFinal = True
deviation = self._deviation.Process(deviation_input)
average = self._average.Process(average_input)
if not self._deviation.IsFormed or not self._average.IsFormed:
return None
sigma = float(deviation.GetValue[Decimal](None) / average.GetValue[Decimal](None)) * math.sqrt(TimeSpan.FromDays(365).TotalSeconds / frame.TotalSeconds)
self._last_option_price = self.CalculatePrice(candle.ClosePrice, Decimal(sigma), self.TimeToExpiry)
self.AddInfoLog("Option price at {0:O}: {1}", candle.OpenTime, self._last_option_price)
return self._last_option_price
def CalculatePrice(self, spot, volatility, timeToExpiry):
if spot <= 0 or volatility < 0 or timeToExpiry < TimeSpan.Zero or self.StrikePrice <= 0:
raise ValueError("Spot and strike must be positive; volatility and tenor must be nonnegative.")
if self.OptionStyle not in (OptionStyles.European, OptionStyles.American) or self.OptionType not in (OptionTypes.Call, OptionTypes.Put) or self.AssetClass not in (SecurityTypes.Stock, SecurityTypes.Index, SecurityTypes.Currency, SecurityTypes.Future):
raise ValueError("Unsupported option style, payoff or underlying asset class.")
s, strike = float(spot), float(self.StrikePrice)
def payoff(value):
return max(value - strike if self.OptionType == OptionTypes.Call else strike - value, 0.0)
if timeToExpiry == TimeSpan.Zero:
return Decimal(payoff(s))
years = timeToExpiry.TotalDays / 365.0
dt = years / 2.0
rate = float(self.RiskFreeRate)
carry = 0.0 if self.AssetClass == SecurityTypes.Future else rate - float(self.DividendYield)
if volatility == 0:
final = payoff(s * math.exp(carry * years)) * math.exp(-rate * years)
if self.OptionStyle == OptionStyles.American:
final = max(payoff(s), final, payoff(s * math.exp(carry * dt)) * math.exp(-rate * dt))
return Decimal(final)
up = math.exp(float(volatility) * math.sqrt(dt))
down = 1.0 / up
probability = (math.exp(carry * dt) - down) / (up - down)
discount = math.exp(-rate * dt)
if not math.isfinite(probability) or probability < 0.0 or probability > 1.0 or not math.isfinite(up) or not math.isfinite(discount):
raise ValueError("Two-step inputs do not admit a finite risk-neutral probability in [0,1].")
upper = discount * (probability * payoff(s * up * up) + (1.0 - probability) * payoff(s))
lower = discount * (probability * payoff(s) + (1.0 - probability) * payoff(s * down * down))
if self.OptionStyle == OptionStyles.American:
upper = max(upper, payoff(s * up))
lower = max(lower, payoff(s * down))
price = discount * (probability * upper + (1.0 - probability) * lower)
if self.OptionStyle == OptionStyles.American:
price = max(price, payoff(s))
return Decimal(price)
def CreateClone(self):
return binomial_option_pricing_model_strategy()