Skip to content

Commit 76e01bb

Browse files
authored
Adds support for custom option pricing models (#9270)
* Initial solution * Made Greeks a non-abstract class * Add regression algorithms * Solve review comments * Minor fix * Minor fix * Remove unnecessary imports * Fix issues with regression algorithms * Solve review comments
1 parent 9e8c220 commit 76e01bb

13 files changed

Lines changed: 526 additions & 67 deletions
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using System.Linq;
19+
using QuantConnect.Data;
20+
using QuantConnect.Data.Market;
21+
using QuantConnect.Interfaces;
22+
using QuantConnect.Securities.Option;
23+
24+
namespace QuantConnect.Algorithm.CSharp
25+
{
26+
/// <summary>
27+
/// Regression algorithm to test the creation and usage of a custom option price model
28+
/// </summary>
29+
public class CustomOptionPriceModelRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
30+
{
31+
private Symbol _optionSymbol;
32+
private CustomOptionPriceModel _optionPriceModel;
33+
34+
public override void Initialize()
35+
{
36+
SetStartDate(2015, 12, 24);
37+
SetEndDate(2015, 12, 24);
38+
39+
var option = AddOption("GOOG");
40+
_optionSymbol = option.Symbol;
41+
42+
option.SetFilter(u => u.StandardsOnly().Strikes(-2, +2).Expiration(0, 180));
43+
_optionPriceModel = new CustomOptionPriceModel();
44+
option.SetPriceModel(_optionPriceModel);
45+
}
46+
47+
public override void OnData(Slice slice)
48+
{
49+
if (Portfolio.Invested)
50+
{
51+
return;
52+
}
53+
54+
if (slice.OptionChains.TryGetValue(_optionSymbol, out var chain))
55+
{
56+
var underlyingPrice = chain.Underlying.Price;
57+
var atmContract = chain
58+
.OrderByDescending(x => x.Expiry)
59+
.ThenBy(x => Math.Abs(chain.Underlying.Price - x.Strike))
60+
.ThenByDescending(x => x.Right)
61+
.FirstOrDefault();
62+
63+
if (atmContract != null && atmContract.TheoreticalPrice > 0)
64+
{
65+
MarketOrder(atmContract.Symbol, 1);
66+
}
67+
}
68+
}
69+
70+
public override void OnEndOfAlgorithm()
71+
{
72+
if (_optionPriceModel.EvaluationCount == 0)
73+
{
74+
throw new RegressionTestException("CustomOptionPriceModel.Evaluate() was never called");
75+
}
76+
}
77+
78+
private class CustomOptionPriceModel : IOptionPriceModel
79+
{
80+
public int EvaluationCount { get; private set; }
81+
public OptionPriceModelResult Evaluate(OptionPriceModelParameters parameters)
82+
{
83+
EvaluationCount++;
84+
var contract = parameters.Contract;
85+
var underlying = contract.UnderlyingLastPrice;
86+
var strike = contract.Strike;
87+
88+
decimal intrinsicValue;
89+
if (contract.Right == OptionRight.Call)
90+
{
91+
intrinsicValue = Math.Max(0, underlying - strike);
92+
}
93+
else
94+
{
95+
intrinsicValue = Math.Max(0, strike - underlying);
96+
}
97+
98+
var theoreticalPrice = intrinsicValue + 1.0m;
99+
return new OptionPriceModelResult(theoreticalPrice, new Greeks(0.5m, 0.1m, 0.2m, -0.05m, 0.1m, 2.0m));
100+
}
101+
}
102+
103+
/// <summary>
104+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
105+
/// </summary>
106+
public bool CanRunLocally { get; } = true;
107+
108+
/// <summary>
109+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
110+
/// </summary>
111+
public List<Language> Languages { get; } = new() { Language.CSharp, Language.Python };
112+
113+
/// <summary>
114+
/// Data Points count of all timeslices of algorithm
115+
/// </summary>
116+
public long DataPoints => 15023;
117+
118+
/// <summary>
119+
/// Data Points count of the algorithm history
120+
/// </summary>
121+
public int AlgorithmHistoryDataPoints => 0;
122+
123+
/// <summary>
124+
/// Final status of the algorithm
125+
/// </summary>
126+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
127+
128+
/// <summary>
129+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
130+
/// </summary>
131+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
132+
{
133+
{"Total Orders", "1"},
134+
{"Average Win", "0%"},
135+
{"Average Loss", "0%"},
136+
{"Compounding Annual Return", "0%"},
137+
{"Drawdown", "0%"},
138+
{"Expectancy", "0"},
139+
{"Start Equity", "100000"},
140+
{"End Equity", "99799"},
141+
{"Net Profit", "0%"},
142+
{"Sharpe Ratio", "0"},
143+
{"Sortino Ratio", "0"},
144+
{"Probabilistic Sharpe Ratio", "0%"},
145+
{"Loss Rate", "0%"},
146+
{"Win Rate", "0%"},
147+
{"Profit-Loss Ratio", "0"},
148+
{"Alpha", "0"},
149+
{"Beta", "0"},
150+
{"Annual Standard Deviation", "0"},
151+
{"Annual Variance", "0"},
152+
{"Information Ratio", "0"},
153+
{"Tracking Error", "0"},
154+
{"Treynor Ratio", "0"},
155+
{"Total Fees", "$1.00"},
156+
{"Estimated Strategy Capacity", "$2600000.00"},
157+
{"Lowest Capacity Asset", "GOOCV 30AKMEIPOX2DI|GOOCV VP83T1ZUHROL"},
158+
{"Portfolio Turnover", "5.49%"},
159+
{"Drawdown Recovery", "0"},
160+
{"OrderListHash", "1925127010d4a935c1efe4bce0375c15"}
161+
};
162+
}
163+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
2+
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
from AlgorithmImports import *
15+
16+
### <summary>
17+
### Regression algorithm to test the creation and usage of a custom option price model
18+
### </summary>
19+
class CustomOptionPriceModelRegressionAlgorithm(QCAlgorithm):
20+
21+
def initialize(self):
22+
self.set_start_date(2015, 12, 24)
23+
self.set_end_date(2015, 12, 24)
24+
self.set_cash(100000)
25+
26+
option = self.add_option("GOOG")
27+
self._option_symbol = option.symbol
28+
29+
option.set_filter(lambda u: u.standards_only().strikes(-2, +2).expiration(0, 180))
30+
self._option_price_model = CustomOptionPriceModel()
31+
option.set_price_model(self._option_price_model)
32+
33+
def on_data(self, slice):
34+
if self.portfolio.invested:
35+
return
36+
37+
chain = slice.option_chains.get(self._option_symbol)
38+
if not chain:
39+
return
40+
41+
contracts = sorted(sorted(sorted(chain, \
42+
key = lambda x: abs(chain.underlying.price - x.strike)), \
43+
key = lambda x: x.expiry, reverse=True), \
44+
key = lambda x: x.right, reverse=True)
45+
46+
if len(contracts) == 0:
47+
return
48+
49+
if (contracts[0].theoretical_price > 0):
50+
self.market_order(contracts[0].symbol, 1)
51+
52+
def on_end_of_algorithm(self):
53+
if self._option_price_model.evaluation_count == 0:
54+
raise RegressionTestException("CustomOptionPriceModel.Evaluate() was never called")
55+
56+
class CustomOptionPriceModel():
57+
def __init__(self):
58+
self.evaluation_count = 0
59+
60+
def evaluate(self, parameters):
61+
self.evaluation_count += 1
62+
contract = parameters.contract
63+
underlying = contract.underlying_last_price
64+
strike = contract.strike
65+
66+
if contract.right == OptionRight.CALL:
67+
intrinsic = max(0, underlying - strike)
68+
else:
69+
intrinsic = max(0, strike - underlying)
70+
71+
theoretical_price = intrinsic + 1.0
72+
73+
return OptionPriceModelResult(theoretical_price, Greeks(0.5, 0.1, 0.2, -0.05, 0.1, 2.0))

Common/Data/Market/Greeks.cs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ namespace QuantConnect.Data.Market
2020
/// <summary>
2121
/// Defines the greeks
2222
/// </summary>
23-
public abstract class Greeks
23+
public class Greeks
2424
{
2525
/// <summary>
2626
/// Gets the delta.
@@ -29,7 +29,7 @@ public abstract class Greeks
2929
/// the underlying asset'sprice. (∂V/∂S)
3030
/// </para>
3131
/// </summary>
32-
public abstract decimal Delta { get; }
32+
public virtual decimal Delta { get; set; }
3333

3434
/// <summary>
3535
/// Gets the gamma.
@@ -38,7 +38,7 @@ public abstract class Greeks
3838
/// the underlying asset'sprice. (∂²V/∂S²)
3939
/// </para>
4040
/// </summary>
41-
public abstract decimal Gamma { get; }
41+
public virtual decimal Gamma { get; set; }
4242

4343
/// <summary>
4444
/// Gets the vega.
@@ -47,7 +47,7 @@ public abstract class Greeks
4747
/// the underlying's volatility. (∂V/∂σ)
4848
/// </para>
4949
/// </summary>
50-
public abstract decimal Vega { get; }
50+
public virtual decimal Vega { get; set; }
5151

5252
/// <summary>
5353
/// Gets the theta.
@@ -56,7 +56,7 @@ public abstract class Greeks
5656
/// time. This is commonly known as the 'time decay.' (∂V/∂τ)
5757
/// </para>
5858
/// </summary>
59-
public abstract decimal Theta { get; }
59+
public virtual decimal Theta { get; set; }
6060

6161
/// <summary>
6262
/// Gets the rho.
@@ -65,7 +65,7 @@ public abstract class Greeks
6565
/// the risk free interest rate. (∂V/∂r)
6666
/// </para>
6767
/// </summary>
68-
public abstract decimal Rho { get; }
68+
public virtual decimal Rho { get; set; }
6969

7070
/// <summary>
7171
/// Gets the lambda.
@@ -76,7 +76,7 @@ public abstract class Greeks
7676
/// </para>
7777
/// </summary>
7878
[PandasIgnore]
79-
public abstract decimal Lambda { get; }
79+
public virtual decimal Lambda { get; set; }
8080

8181
/// <summary>
8282
/// Gets the lambda.
@@ -91,7 +91,11 @@ public abstract class Greeks
9191
/// PEP8 API is used (lambda is a reserved keyword in Python).
9292
/// </remarks>
9393
[PandasIgnore]
94-
public virtual decimal Lambda_ => Lambda;
94+
public virtual decimal Lambda_
95+
{
96+
get { return Lambda; }
97+
set { Lambda = value; }
98+
}
9599

96100
/// <summary>
97101
/// Gets the theta per day.
@@ -101,6 +105,30 @@ public abstract class Greeks
101105
/// </para>
102106
/// </summary>
103107
[PandasIgnore]
104-
public virtual decimal ThetaPerDay => Theta / 365m;
108+
public virtual decimal ThetaPerDay
109+
{
110+
get { return Theta / 365m; }
111+
set { Theta = value * 365m; }
112+
}
113+
114+
/// <summary>
115+
/// Initializes a new instance of the <see cref="Greeks"/> class.
116+
/// </summary>
117+
public Greeks()
118+
{
119+
}
120+
121+
/// <summary>
122+
/// Initializes a new instance of the <see cref="Greeks"/> class with specified values.
123+
/// </summary>
124+
public Greeks(decimal delta, decimal gamma, decimal vega, decimal theta, decimal rho, decimal lambda)
125+
{
126+
Delta = delta;
127+
Gamma = gamma;
128+
Vega = vega;
129+
Theta = theta;
130+
Rho = rho;
131+
Lambda = lambda;
132+
}
105133
}
106134
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*
15+
*/
16+
17+
using Python.Runtime;
18+
using QuantConnect.Python;
19+
using System;
20+
21+
namespace QuantConnect.Securities.Option
22+
{
23+
/// <summary>
24+
/// Provides an implementation of <see cref="IOptionPriceModel"/> that wraps a <see cref="PyObject"/> object
25+
/// </summary>
26+
public class OptionPriceModelPythonWrapper : BasePythonWrapper<IOptionPriceModel>, IOptionPriceModel
27+
{
28+
/// <summary>
29+
/// Creates a new instance
30+
/// </summary>
31+
/// <param name="model">The python model to wrap</param>
32+
public OptionPriceModelPythonWrapper(PyObject model)
33+
: base(model)
34+
{
35+
}
36+
37+
/// <summary>
38+
/// Evaluates the specified option contract to compute a theoretical price, IV and greeks
39+
/// </summary>
40+
/// <param name="parameters">A <see cref="OptionPriceModelParameters"/> object
41+
/// containing the security, slice and contract</param>
42+
/// <returns>An instance of <see cref="OptionPriceModelResult"/> containing the theoretical
43+
/// price of the specified option contract</returns>
44+
public OptionPriceModelResult Evaluate(OptionPriceModelParameters parameters)
45+
{
46+
return InvokeMethod<OptionPriceModelResult>(nameof(Evaluate), parameters);
47+
}
48+
}
49+
}

0 commit comments

Comments
 (0)