Binomial Option Pricing Model
定价规则
不交易的两步 Cox–Ross–Rubinstein 期权定价模块,不提交任何订单。CalculatePrice(spot, volatility, timeToExpiry) 按配置的执行价、看涨/看跌、欧式/美式及资产类别返回价格。利率和波动率为年化小数,0.05 表示 5%。一年按 365 天,期限也支持分钟和小时。
dt = T / 2,u = exp(volatility * sqrt(dt))、d = 1/u、p = (exp(b * dt) - d) / (u - d)。Stock、Index、Currency 使用 b = r - q(q 为股息率或外币利率),Future 使用 b = 0。每次向后计算乘以 exp(-r * dt)。欧式到期行权;美式在每个节点比较继续持有和内在价值。零期限返回内在价值;零波动率按确定性持有成本路径并遵循相同行权规则。未支持的类别/风格及非法概率明确报错,不强制截断。
EvaluatePrice(candle) 对每根已完成足只处理一次。得到 Length 个收盘后,以总体 SD(close) / mean(close) * sqrt(365 天 / 足间隔) 估计年化波动率。计算配置的 TimeToExpiry,更新 LastOptionPrice、记录日志,有图表时绘制价格足及标准差。这是价格水平的滚动离散度估计,不是收益率波动率估计或交易信号。
参数
| 参数 | 默认值 |
|---|---|
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()