Binomial Option Pricing Model
評価ルール
取引注文を出さない、2 段階 Cox–Ross–Rubinstein 方式のオプション評価モジュールです。CalculatePrice(spot, volatility, timeToExpiry) は設定済み行使価格、コール/プット、欧州/米国方式、資産区分に対して価格を返します。金利とボラティリティは年率の小数で、0.05 は 5%。1 年は 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 個の終値から 母標準偏差(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()