diff --git a/Algorithm/QCAlgorithm.Indicators.cs b/Algorithm/QCAlgorithm.Indicators.cs index ab6470781479..01b2aaef857e 100644 --- a/Algorithm/QCAlgorithm.Indicators.cs +++ b/Algorithm/QCAlgorithm.Indicators.cs @@ -1500,19 +1500,26 @@ public SuperTrend STR(Symbol symbol, int period, decimal multiplier, MovingAvera } /// - /// Creates a new RollingSharpeRatio indicator. + /// Creates a new SharpeRatio indicator. /// /// The symbol whose RSR we want /// Period of historical observation for sharpe ratio calculation - /// Risk-free rate for sharpe ratio calculation + /// + /// Risk-free rate for sharpe ratio calculation. If not specified, it will use the algorithms' + /// /// The resolution /// Selects a value from the BaseData to send into the indicator, if null defaults to the Value property of BaseData (x => x.Value) - /// The RollingSharpeRatio indicator for the requested symbol over the specified period + /// The SharpeRatio indicator for the requested symbol over the specified period [DocumentationAttribute(Indicators)] - public SharpeRatio SR(Symbol symbol, int sharpePeriod, decimal riskFreeRate = 0.0m, Resolution ? resolution = null, Func selector = null) + public SharpeRatio SR(Symbol symbol, int sharpePeriod, decimal? riskFreeRate = null, Resolution? resolution = null, Func selector = null) { - var name = CreateIndicatorName(symbol, $"SR({sharpePeriod},{riskFreeRate})", resolution); - var sharpeRatio = new SharpeRatio(name, sharpePeriod, riskFreeRate); + var baseBame = riskFreeRate.HasValue ? $"SR({sharpePeriod},{riskFreeRate})" : $"SR({sharpePeriod})"; + var name = CreateIndicatorName(symbol, baseBame, resolution); + IRiskFreeInterestRateModel riskFreeRateModel = riskFreeRate.HasValue + ? new ConstantRiskFreeRateInterestRateModel(riskFreeRate.Value) + // Make it a function so it's lazily evaluated: SetRiskFreeInterestRateModel can be called after this method + : new FuncRiskFreeRateInterestRateModel((datetime) => RiskFreeInterestRateModel.GetInterestRate(datetime)); + var sharpeRatio = new SharpeRatio(name, sharpePeriod, riskFreeRateModel); InitializeIndicator(symbol, sharpeRatio, resolution, selector); return sharpeRatio; diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index 31b40bf73024..accb7fbb6f3c 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -1148,6 +1148,16 @@ public void SetBrokerageModel(PyObject model) SetBrokerageModel(brokerageModel); } + /// + /// Sets the risk free interest rate model to be used in the algorithm + /// + /// The risk free interest rate model to use + [DocumentationAttribute(Modeling)] + public void SetRiskFreeInterestRateModel(PyObject model) + { + SetRiskFreeInterestRateModel(RiskFreeInterestRateModelPythonWrapper.FromPyObject(model)); + } + /// /// Sets the security initializer function, used to initialize/configure securities after creation /// diff --git a/Algorithm/QCAlgorithm.cs b/Algorithm/QCAlgorithm.cs index aaee6ec40b0d..8d67056119ce 100644 --- a/Algorithm/QCAlgorithm.cs +++ b/Algorithm/QCAlgorithm.cs @@ -174,6 +174,7 @@ public QCAlgorithm() SignalExport = new SignalExportManager(this); BrokerageModel = new DefaultBrokerageModel(); + RiskFreeInterestRateModel = new InterestRateProvider(); Notify = new NotificationManager(false); // Notification manager defaults to disabled. //Initialise to unlocked: @@ -337,6 +338,16 @@ public IBrokerageMessageHandler BrokerageMessageHandler set; } + /// + /// Gets the risk free interest rate model used to get the interest rates + /// + [DocumentationAttribute(Modeling)] + public IRiskFreeInterestRateModel RiskFreeInterestRateModel + { + get; + private set; + } + /// /// Notification Manager for Sending Live Runtime Notifications to users about important events. /// @@ -1278,6 +1289,16 @@ public void SetBrokerageMessageHandler(IBrokerageMessageHandler handler) BrokerageMessageHandler = handler ?? throw new ArgumentNullException(nameof(handler)); } + /// + /// Sets the risk free interest rate model to be used in the algorithm + /// + /// The risk free interest rate model to use + [DocumentationAttribute(Modeling)] + public void SetRiskFreeInterestRateModel(IRiskFreeInterestRateModel model) + { + RiskFreeInterestRateModel = model ?? throw new ArgumentNullException(nameof(model)); + } + /// /// Sets the benchmark used for computing statistics of the algorithm to the specified symbol /// diff --git a/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs b/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs index 34032b3742eb..7707000ccd6f 100644 --- a/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs +++ b/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs @@ -187,6 +187,11 @@ public IBrokerageMessageHandler BrokerageMessageHandler /// public BrokerageName BrokerageName => _baseAlgorithm.BrokerageName; + /// + /// Gets the risk free interest rate model used to get the interest rates + /// + public IRiskFreeInterestRateModel RiskFreeInterestRateModel => _baseAlgorithm.RiskFreeInterestRateModel; + /// /// Debug messages from the strategy: /// diff --git a/Common/Data/ConstantRiskFreeRateInterestRateModel.cs b/Common/Data/ConstantRiskFreeRateInterestRateModel.cs new file mode 100644 index 000000000000..feca8491c966 --- /dev/null +++ b/Common/Data/ConstantRiskFreeRateInterestRateModel.cs @@ -0,0 +1,45 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; + +namespace QuantConnect.Data +{ + /// + /// Constant risk free rate interest rate model + /// + public class ConstantRiskFreeRateInterestRateModel : IRiskFreeInterestRateModel + { + private readonly decimal _riskFreeRate; + + /// + /// Instantiates a with the specified risk free rate + /// + public ConstantRiskFreeRateInterestRateModel(decimal riskFreeRate) + { + _riskFreeRate = riskFreeRate; + } + + /// + /// Get interest rate by a given date + /// + /// The date + /// Interest rate on the given date + public decimal GetInterestRate(DateTime date) + { + return _riskFreeRate; + } + } +} diff --git a/Common/Data/FuncRiskFreeRateInterestRateModel.cs b/Common/Data/FuncRiskFreeRateInterestRateModel.cs new file mode 100644 index 000000000000..c662efad6918 --- /dev/null +++ b/Common/Data/FuncRiskFreeRateInterestRateModel.cs @@ -0,0 +1,45 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; + +namespace QuantConnect.Data +{ + /// + /// Constant risk free rate interest rate model + /// + public class FuncRiskFreeRateInterestRateModel : IRiskFreeInterestRateModel + { + private readonly Func _getInterestRateFunc; + + /// + /// Create class instance of interest rate provider + /// + public FuncRiskFreeRateInterestRateModel(Func getInterestRateFunc) + { + _getInterestRateFunc = getInterestRateFunc; + } + + /// + /// Get interest rate by a given date + /// + /// The date + /// Interest rate on the given date + public decimal GetInterestRate(DateTime date) + { + return _getInterestRateFunc(date); + } + } +} diff --git a/Common/Data/IRiskFreeInterestRateModel.cs b/Common/Data/IRiskFreeInterestRateModel.cs new file mode 100644 index 000000000000..04a4b1f90743 --- /dev/null +++ b/Common/Data/IRiskFreeInterestRateModel.cs @@ -0,0 +1,64 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace QuantConnect.Data +{ + /// + /// Represents a model that provides risk free interest rate data + /// + public interface IRiskFreeInterestRateModel + { + /// + /// Get interest rate by a given date + /// + /// The date + /// Interest rate on the given date + decimal GetInterestRate(DateTime date); + } + + /// + /// Provide extension and static methods for + /// + public static class RiskFreeInterestRateModelExtensions + { + /// + /// Gets the average risk free annual return rate + /// + /// The interest rate model + /// Start date to calculate the average + /// End date to calculate the average + public static decimal GetRiskFreeRate(this IRiskFreeInterestRateModel model, DateTime startDate, DateTime endDate) + { + return model.GetAverageRiskFreeRate(Time.EachDay(startDate, endDate)); + } + + /// + /// Gets the average Risk Free Rate from the interest rate of the given dates + /// + /// The interest rate model + /// + /// Collection of dates from which the interest rates will be computed and then the average of them + /// + public static decimal GetAverageRiskFreeRate(this IRiskFreeInterestRateModel model, IEnumerable dates) + { + var interestRates = dates.Select(x => model.GetInterestRate(x)).DefaultIfEmpty(0); + return interestRates.Average(); + } + } +} diff --git a/Common/Data/InterestRateProvider.cs b/Common/Data/InterestRateProvider.cs index 1803b7686d27..b7dadd202969 100644 --- a/Common/Data/InterestRateProvider.cs +++ b/Common/Data/InterestRateProvider.cs @@ -28,38 +28,54 @@ namespace QuantConnect.Data /// /// Fed US Primary Credit Rate at given date /// - public class InterestRateProvider + public class InterestRateProvider : IRiskFreeInterestRateModel { - private static readonly DateTime FirstInterestRateDate = new DateTime(1998, 1, 1); + private static readonly DateTime _firstInterestRateDate = new DateTime(1998, 1, 1); + private static DateTime _lastInterestRateDate; + private static Dictionary _riskFreeRateProvider; + private static readonly object _lock = new(); /// /// Default Risk Free Rate of 1% /// - public static decimal DefaultRiskFreeRate { get; } = 0.01m; - - private DateTime _lastInterestRateDate; - private Dictionary _riskFreeRateProvider; + public static readonly decimal DefaultRiskFreeRate = 0.01m; /// - /// Create class instance of interest rate provider + /// Lazily loads the interest rate provider from disk and returns it /// - public InterestRateProvider() + private IReadOnlyDictionary RiskFreeRateProvider { - LoadInterestRateProvider(); + get + { + // let's not lock if the provider is already loaded + if (_riskFreeRateProvider != null) + { + return _riskFreeRateProvider; + } + + lock (_lock) + { + if (_riskFreeRateProvider == null) + { + LoadInterestRateProvider(); + } + return _riskFreeRateProvider; + } + } } /// - /// Get interest rate by a given datetime + /// Get interest rate by a given date /// - /// - /// interest rate of the given date - public decimal GetInterestRate(DateTime dateTime) + /// The date + /// Interest rate on the given date + public decimal GetInterestRate(DateTime date) { - if (!_riskFreeRateProvider.TryGetValue(dateTime.Date, out var interestRate)) + if (!RiskFreeRateProvider.TryGetValue(date.Date, out var interestRate)) { - return dateTime < FirstInterestRateDate - ? _riskFreeRateProvider[FirstInterestRateDate] - : _riskFreeRateProvider[_lastInterestRateDate]; + return date < _firstInterestRateDate + ? RiskFreeRateProvider[_firstInterestRateDate] + : RiskFreeRateProvider[_lastInterestRateDate]; } return interestRate; @@ -77,7 +93,7 @@ protected void LoadInterestRateProvider() _lastInterestRateDate = DateTime.UtcNow.Date; // Sparse the discrete data points into continuous credit rate data for every day - for (var date = FirstInterestRateDate; date <= _lastInterestRateDate; date = date.AddDays(1)) + for (var date = _firstInterestRateDate; date <= _lastInterestRateDate; date = date.AddDays(1)) { if (!_riskFreeRateProvider.TryGetValue(date, out var currentRate)) { @@ -151,7 +167,7 @@ public static bool TryParse(string csvLine, out DateTime date, out decimal inter return false; } - // Unit conversion from % + // Unit conversion from % interestRate /= 100; return true; } diff --git a/Common/Interfaces/IAlgorithm.cs b/Common/Interfaces/IAlgorithm.cs index 3092effbc5c4..48788585a697 100644 --- a/Common/Interfaces/IAlgorithm.cs +++ b/Common/Interfaces/IAlgorithm.cs @@ -132,6 +132,14 @@ BrokerageName BrokerageName get; } + /// + /// Gets the risk free interest rate model used to get the interest rates + /// + IRiskFreeInterestRateModel RiskFreeInterestRateModel + { + get; + } + /// /// Gets the brokerage message handler used to decide what to do /// with each message sent from the brokerage diff --git a/Common/Python/RiskFreeInterestRateModelPythonWrapper.cs b/Common/Python/RiskFreeInterestRateModelPythonWrapper.cs new file mode 100644 index 000000000000..d8c31a7c8944 --- /dev/null +++ b/Common/Python/RiskFreeInterestRateModelPythonWrapper.cs @@ -0,0 +1,64 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using Python.Runtime; +using QuantConnect.Data; + +namespace QuantConnect.Python +{ + /// + /// Wraps a object that represents a risk-free interest rate model + /// + public class RiskFreeInterestRateModelPythonWrapper : IRiskFreeInterestRateModel + { + private readonly dynamic _model; + + /// + /// Constructor for initializing the class with wrapped object + /// + /// Represents a security's model of buying power + public RiskFreeInterestRateModelPythonWrapper(PyObject model) + { + _model = model.ValidateImplementationOf(); + } + + /// + /// Get interest rate by a given date + /// + /// The date + /// Interest rate on the given date + public decimal GetInterestRate(DateTime date) + { + using var _ = Py.GIL(); + return (_model.GetInterestRate(date) as PyObject).GetAndDispose(); + } + + /// + /// Converts a object into a object, wrapping it if necessary + /// + /// The Python model + /// The converted instance + public static IRiskFreeInterestRateModel FromPyObject(PyObject model) + { + if (!model.TryConvert(out IRiskFreeInterestRateModel riskFreeInterestRateModel)) + { + riskFreeInterestRateModel = new RiskFreeInterestRateModelPythonWrapper(model); + } + + return riskFreeInterestRateModel; + } + } +} diff --git a/Common/Statistics/AlgorithmPerformance.cs b/Common/Statistics/AlgorithmPerformance.cs index ebcb5cf4a443..f32ca9e07fcb 100644 --- a/Common/Statistics/AlgorithmPerformance.cs +++ b/Common/Statistics/AlgorithmPerformance.cs @@ -13,7 +13,7 @@ * limitations under the License. */ -using QuantConnect.Securities; +using QuantConnect.Data; using System; using System.Collections.Generic; @@ -51,6 +51,7 @@ public class AlgorithmPerformance /// The algorithm starting capital /// Number of winning transactions /// Number of losing transactions + /// The risk free interest rate model to use public AlgorithmPerformance( List trades, SortedDictionary profitLoss, @@ -60,12 +61,13 @@ public AlgorithmPerformance( List listBenchmark, decimal startingCapital, int winningTransactions, - int losingTransactions) + int losingTransactions, + IRiskFreeInterestRateModel riskFreeInterestRateModel) { TradeStatistics = new TradeStatistics(trades); PortfolioStatistics = new PortfolioStatistics(profitLoss, equity, portfolioTurnover, listPerformance, listBenchmark, startingCapital, - winCount: winningTransactions, lossCount: losingTransactions); + riskFreeInterestRateModel, winCount: winningTransactions, lossCount: losingTransactions); ClosedTrades = trades; } diff --git a/Common/Statistics/PortfolioStatistics.cs b/Common/Statistics/PortfolioStatistics.cs index 0f023dccc3f9..dd151b8b084d 100644 --- a/Common/Statistics/PortfolioStatistics.cs +++ b/Common/Statistics/PortfolioStatistics.cs @@ -28,8 +28,6 @@ namespace QuantConnect.Statistics /// public class PortfolioStatistics { - private static Lazy _interestRateProvider = new Lazy(); - /// /// The average rate of return for winning trades /// @@ -162,6 +160,7 @@ public class PortfolioStatistics /// The list of algorithm performance values /// The list of benchmark values /// The algorithm starting capital + /// The risk free interest rate model to use /// The number of trading days per year /// /// The number of wins, including ITM options with profitLoss less than 0. @@ -175,6 +174,7 @@ public PortfolioStatistics( List listPerformance, List listBenchmark, decimal startingCapital, + IRiskFreeInterestRateModel riskFreeInterestRateModel, int tradingDaysPerYear = 252, int? winCount = null, int? lossCount = null) @@ -248,7 +248,7 @@ public PortfolioStatistics( var benchmarkAnnualPerformance = GetAnnualPerformance(listBenchmark, tradingDaysPerYear); var annualPerformance = GetAnnualPerformance(listPerformance, tradingDaysPerYear); - var riskFreeRate = GetAverageRiskFreeRate(equity.Select(x => x.Key)); + var riskFreeRate = riskFreeInterestRateModel.GetAverageRiskFreeRate(equity.Select(x => x.Key)); SharpeRatio = AnnualStandardDeviation == 0 ? 0 : (annualPerformance - riskFreeRate) / AnnualStandardDeviation; var benchmarkVariance = listBenchmark.Variance(); @@ -274,27 +274,6 @@ public PortfolioStatistics() { } - /// - /// Gets the average risk free annual return rate - /// - /// Start date to calculate the average - /// End date to calculate the average - public static decimal GetRiskFreeRate(DateTime startDate, DateTime endDate) - { - return GetAverageRiskFreeRate(Time.EachDay(startDate, endDate)); - } - - /// - /// Gets the average Risk Free Rate from the interest rate of the given dates - /// - /// Collection of dates from which the interest rates will be computed - /// and then the average of them - public static decimal GetAverageRiskFreeRate(IEnumerable dates) - { - var interestRates = dates.Select(x => _interestRateProvider.Value.GetInterestRate(x)).DefaultIfEmpty(0); - return interestRates.Average(); - } - /// /// Drawdown maximum percentage. /// diff --git a/Common/Statistics/StatisticsBuilder.cs b/Common/Statistics/StatisticsBuilder.cs index 18b42c7695c2..1e919ab15dce 100644 --- a/Common/Statistics/StatisticsBuilder.cs +++ b/Common/Statistics/StatisticsBuilder.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using QuantConnect.Data; using QuantConnect.Securities; using QuantConnect.Util; @@ -44,6 +45,7 @@ public static class StatisticsBuilder /// /// The transaction manager to get number of winning and losing transactions /// + /// The risk free interest rate model to use /// Returns a object public static StatisticsResults Generate( List trades, @@ -57,7 +59,8 @@ public static StatisticsResults Generate( int totalTransactions, CapacityEstimate estimatedStrategyCapacity, string accountCurrencySymbol, - SecurityTransactionManager transactions) + SecurityTransactionManager transactions, + IRiskFreeInterestRateModel riskFreeInterestRateModel) { var equity = ChartPointToDictionary(pointsEquity); @@ -65,9 +68,9 @@ public static StatisticsResults Generate( var lastDate = equity.Keys.LastOrDefault().Date; var totalPerformance = GetAlgorithmPerformance(firstDate, lastDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark, - pointsPortfolioTurnover, startingCapital, transactions); + pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel); var rollingPerformances = GetRollingPerformances(firstDate, lastDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark, - pointsPortfolioTurnover, startingCapital, transactions); + pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel); var summary = GetSummary(totalPerformance, estimatedStrategyCapacity, totalFees, totalTransactions, accountCurrencySymbol); return new StatisticsResults(totalPerformance, rollingPerformances, summary); @@ -88,6 +91,7 @@ public static StatisticsResults Generate( /// /// The transaction manager to get number of winning and losing transactions /// + /// The risk free interest rate model to use /// The algorithm performance private static AlgorithmPerformance GetAlgorithmPerformance( DateTime fromDate, @@ -99,7 +103,8 @@ private static AlgorithmPerformance GetAlgorithmPerformance( List pointsBenchmark, List pointsPortfolioTurnover, decimal startingCapital, - SecurityTransactionManager transactions) + SecurityTransactionManager transactions, + IRiskFreeInterestRateModel riskFreeInterestRateModel) { var periodEquity = new SortedDictionary(equity.Where(x => x.Key.Date >= fromDate && x.Key.Date < toDate.AddDays(1)).ToDictionary(x => x.Key, y => y.Value)); @@ -136,7 +141,7 @@ private static AlgorithmPerformance GetAlgorithmPerformance( var runningCapital = equity.Count == periodEquity.Count ? startingCapital : periodEquity.Values.FirstOrDefault(); return new AlgorithmPerformance(periodTrades, periodProfitLoss, periodEquity, portfolioTurnover, listPerformance, listBenchmark, - runningCapital, periodWinCount, periodLossCount); + runningCapital, periodWinCount, periodLossCount, riskFreeInterestRateModel); } /// @@ -154,6 +159,7 @@ private static AlgorithmPerformance GetAlgorithmPerformance( /// /// The transaction manager to get number of winning and losing transactions /// + /// The risk free interest rate model to use /// A dictionary with the rolling performances private static Dictionary GetRollingPerformances( DateTime firstDate, @@ -165,7 +171,8 @@ private static Dictionary GetRollingPerformances( List pointsBenchmark, List pointsPortfolioTurnover, decimal startingCapital, - SecurityTransactionManager transactions) + SecurityTransactionManager transactions, + IRiskFreeInterestRateModel riskFreeInterestRateModel) { var rollingPerformances = new Dictionary(); @@ -178,7 +185,7 @@ private static Dictionary GetRollingPerformances( { var key = $"M{monthPeriod}_{period.EndDate.ToStringInvariant("yyyyMMdd")}"; var periodPerformance = GetAlgorithmPerformance(period.StartDate, period.EndDate, trades, profitLoss, equity, pointsPerformance, - pointsBenchmark, pointsPortfolioTurnover, startingCapital, transactions); + pointsBenchmark, pointsPortfolioTurnover, startingCapital, transactions, riskFreeInterestRateModel); rollingPerformances[key] = periodPerformance; } } diff --git a/Engine/Results/BaseResultsHandler.cs b/Engine/Results/BaseResultsHandler.cs index 3c4e99a903c5..b1afa93a761c 100644 --- a/Engine/Results/BaseResultsHandler.cs +++ b/Engine/Results/BaseResultsHandler.cs @@ -848,7 +848,7 @@ protected StatisticsResults GenerateStatisticsResults(Dictionary statisticsResults = StatisticsBuilder.Generate(trades, profitLoss, equity.Values, performance.Values, benchmark.Values, portfolioTurnover.Values, StartingPortfolioValue, Algorithm.Portfolio.TotalFees, totalTransactions, - estimatedStrategyCapacity, AlgorithmCurrencySymbol, Algorithm.Transactions); + estimatedStrategyCapacity, AlgorithmCurrencySymbol, Algorithm.Transactions, Algorithm.RiskFreeInterestRateModel); } statisticsResults.AddCustomSummaryStatistics(_customSummaryStatistics); diff --git a/Indicators/SharpeRatio.cs b/Indicators/SharpeRatio.cs index 99961f02bf70..262870c21fbc 100644 --- a/Indicators/SharpeRatio.cs +++ b/Indicators/SharpeRatio.cs @@ -13,6 +13,10 @@ * limitations under the License. */ +using Python.Runtime; +using QuantConnect.Data; +using QuantConnect.Python; + namespace QuantConnect.Indicators { /// @@ -32,11 +36,21 @@ public class SharpeRatio : IndicatorBase, IIndicatorWarmUpPe /// private readonly int _period; + /// + /// Risk-free rate model + /// + private readonly IRiskFreeInterestRateModel _riskFreeInterestRateModel; + /// /// RateOfChange indicator for calculating the sharpe ratio /// protected RateOfChange RateOfChange { get; } + /// + /// RiskFreeRate indicator for calculating the sharpe ratio + /// + protected Identity RiskFreeRate { get; } + /// /// Indicator to store the calculation of the sharpe ratio /// @@ -62,23 +76,66 @@ public class SharpeRatio : IndicatorBase, IIndicatorWarmUpPe /// /// The name of this indicator /// Period of historical observation for sharpe ratio calculation - /// Risk-free rate for sharpe ratio calculation - public SharpeRatio(string name, int period, decimal riskFreeRate = 0.0m) + /// Risk-free rate model + public SharpeRatio(string name, int period, IRiskFreeInterestRateModel riskFreeRateModel) : base(name) { _period = period; + _riskFreeInterestRateModel = riskFreeRateModel; // calculate sharpe ratio using indicators RateOfChange = new RateOfChange(1); - Numerator = RateOfChange.SMA(period).Minus(riskFreeRate); + RiskFreeRate = new Identity(name + "_RiskFreeRate"); + Numerator = RateOfChange.SMA(period).Minus(RiskFreeRate); var denominator = new StandardDeviation(period).Of(RateOfChange); Ratio = Numerator.Over(denominator); - // define warmup value; + // define warmup value; // _roc is the base of our indicator chain + period of STD and SMA WarmUpPeriod = RateOfChange.WarmUpPeriod + period; } + /// + /// Creates a new Sharpe Ratio indicator using the specified periods + /// + /// Period of historical observation for sharpe ratio calculation + /// Risk-free rate model + public SharpeRatio(int period, IRiskFreeInterestRateModel riskFreeRateModel) + : this($"SR({period})", period, riskFreeRateModel) + { + } + + /// + /// Creates a new Sharpe Ratio indicator using the specified period using a Python risk free rate model + /// + /// Period of historical observation for sharpe ratio calculation + /// Risk-free rate model + public SharpeRatio(string name, int period, PyObject riskFreeRateModel) + : this(name, period, RiskFreeInterestRateModelPythonWrapper.FromPyObject(riskFreeRateModel)) + { + } + + /// + /// Creates a new Sharpe Ratio indicator using the specified period using a Python risk free rate model + /// + /// Period of historical observation for sharpe ratio calculation + /// Risk-free rate model + public SharpeRatio(int period, PyObject riskFreeRateModel) + : this(period, RiskFreeInterestRateModelPythonWrapper.FromPyObject(riskFreeRateModel)) + { + } + + /// + /// Creates a new Sharpe Ratio indicator using the specified periods + /// + /// The name of this indicator + /// Period of historical observation for sharpe ratio calculation + /// Risk-free rate for sharpe ratio calculation + public SharpeRatio(string name, int period, decimal riskFreeRate = 0.0m) + : this(name, period, new ConstantRiskFreeRateInterestRateModel(riskFreeRate)) + { + } + /// /// Creates a new SharpeRatio indicator using the specified periods /// @@ -96,6 +153,7 @@ public SharpeRatio(int period, decimal riskFreeRate = 0.0m) /// A new value for this indicator protected override decimal ComputeNextValue(IndicatorDataPoint input) { + RiskFreeRate.Update(input.Time, _riskFreeInterestRateModel.GetInterestRate(input.Time)); RateOfChange.Update(input); return Ratio; } diff --git a/Report/Rolling.cs b/Report/Rolling.cs index 469f427de296..ce5aecda976c 100644 --- a/Report/Rolling.cs +++ b/Report/Rolling.cs @@ -19,6 +19,7 @@ using QuantConnect.Statistics; using System.Collections.Generic; using System.Linq; +using QuantConnect.Data; namespace QuantConnect.Report { @@ -27,6 +28,8 @@ namespace QuantConnect.Report /// public static class Rolling { + private static readonly IRiskFreeInterestRateModel _interestRateProvider = new InterestRateProvider(); + /// /// Calculate the rolling beta with the given window size (in days) /// @@ -76,7 +79,7 @@ public static Series Beta(SortedList perform /// Rolling sharpe ratio public static Series Sharpe(Series equityCurve, int months) { - var riskFreeRate = (double)PortfolioStatistics.GetAverageRiskFreeRate(equityCurve.Keys); + var riskFreeRate = (double)_interestRateProvider.GetAverageRiskFreeRate(equityCurve.Keys); if (equityCurve.IsEmpty) { return equityCurve; diff --git a/Research/QuantBook.cs b/Research/QuantBook.cs index 1af482be15f1..6d0f1e3b8646 100644 --- a/Research/QuantBook.cs +++ b/Research/QuantBook.cs @@ -769,7 +769,7 @@ public PyDict GetPortfolioStatistics(PyObject dataFrame) var startingCapital = Convert.ToDecimal(dictEquity.FirstOrDefault().Value); // Compute portfolio statistics - var stats = new PortfolioStatistics(profitLoss, equity, new(), listPerformance, listBenchmark, startingCapital); + var stats = new PortfolioStatistics(profitLoss, equity, new(), listPerformance, listBenchmark, startingCapital, RiskFreeInterestRateModel); result.SetItem("Average Win (%)", Convert.ToDouble(stats.AverageWinRate * 100).ToPython()); result.SetItem("Average Loss (%)", Convert.ToDouble(stats.AverageLossRate * 100).ToPython()); diff --git a/Tests/Algorithm/AlgorithmIndicatorsTests.cs b/Tests/Algorithm/AlgorithmIndicatorsTests.cs index 19de0764c286..1ef047f3b732 100644 --- a/Tests/Algorithm/AlgorithmIndicatorsTests.cs +++ b/Tests/Algorithm/AlgorithmIndicatorsTests.cs @@ -66,5 +66,32 @@ public void IndicatorsPassSelectorToWarmUp() Assert.IsTrue(indicator.IsReady); mockSelector.Verify(_ => _(It.IsAny()), Times.Exactly(indicator.WarmUpPeriod)); } + + [Test] + public void SharpeRatioIndicatorUsesAlgorithmsRiskFreeRateModelSetAfterIndicatorRegistration() + { + // Register indicator + var sharpeRatio = _algorithm.SR(Symbols.SPY, 10); + + // Setup risk free rate model + var interestRateProviderMock = new Mock(); + var reference = new DateTime(2023, 11, 21, 10, 0, 0); + interestRateProviderMock.Setup(x => x.GetInterestRate(reference)).Verifiable(); + + // Update indicator + sharpeRatio.Update(new IndicatorDataPoint(Symbols.SPY, reference, 300m)); + + // Our interest rate provider shouldn't have been called yet since it's hasn't been set to the algorithm + interestRateProviderMock.Verify(x => x.GetInterestRate(reference), Times.Never); + + // Set the interest rate provider to the algorithm + _algorithm.SetRiskFreeInterestRateModel(interestRateProviderMock.Object); + + // Update indicator + sharpeRatio.Update(new IndicatorDataPoint(Symbols.SPY, reference, 300m)); + + // Our interest rate provider should have been called once + interestRateProviderMock.Verify(x => x.GetInterestRate(reference), Times.Once); + } } } diff --git a/Tests/Common/Statistics/PortfolioStatisticsTests.cs b/Tests/Common/Statistics/PortfolioStatisticsTests.cs index bd7dff926da5..2662a2a0aacc 100644 --- a/Tests/Common/Statistics/PortfolioStatisticsTests.cs +++ b/Tests/Common/Statistics/PortfolioStatisticsTests.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using QuantConnect.Data; using QuantConnect.Statistics; namespace QuantConnect.Tests.Common.Statistics @@ -36,7 +37,7 @@ public void ITMOptionAssignment([Values] bool win) var lossCount = trades.Count - winCount; var statistics = new PortfolioStatistics(profitLoss, new SortedDictionary(), new SortedDictionary(), new List { 0, 0 }, new List { 0, 0 }, 100000, - winCount: winCount, lossCount: lossCount); + new InterestRateProvider(), winCount: winCount, lossCount: lossCount); if (win) { diff --git a/Tests/Common/Statistics/StatisticsBuilderTests.cs b/Tests/Common/Statistics/StatisticsBuilderTests.cs index fab46de6b2b0..8903e13281eb 100644 --- a/Tests/Common/Statistics/StatisticsBuilderTests.cs +++ b/Tests/Common/Statistics/StatisticsBuilderTests.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using QuantConnect.Data; using QuantConnect.Statistics; namespace QuantConnect.Tests.Common.Statistics @@ -75,7 +76,8 @@ public void MisalignedValues_ShouldThrow_DuringGeneration() "$", new QuantConnect.Securities.SecurityTransactionManager( null, - new QuantConnect.Securities.SecurityManager(new TimeKeeper(DateTime.UtcNow)))); + new QuantConnect.Securities.SecurityManager(new TimeKeeper(DateTime.UtcNow))), + new InterestRateProvider()); }, "Misaligned values provided, but we still generate statistics"); } } diff --git a/Tests/Indicators/SharpeRatioTests.cs b/Tests/Indicators/SharpeRatioTests.cs index 985c4960994e..42b6742ca7c1 100644 --- a/Tests/Indicators/SharpeRatioTests.cs +++ b/Tests/Indicators/SharpeRatioTests.cs @@ -13,16 +13,19 @@ * limitations under the License. */ +using Moq; using NUnit.Framework; -using QuantConnect.Data.Market; +using Python.Runtime; +using QuantConnect.Data; using QuantConnect.Indicators; using System; +using System.Linq; -namespace QuantConnect.Tests.Indicators +namespace QuantConnect.Tests.Indicators { [TestFixture] public class SharpeRatioTests : CommonIndicatorTests - { + { protected override IndicatorBase CreateIndicator() { return new SharpeRatio("SR", 10); @@ -33,48 +36,131 @@ protected override IndicatorBase CreateIndicator() protected override string TestColumnName => "SR_10"; [Test] - public void TestTradeBarsWithSameValue() + public void TestTradeBarsWithSameValue() { // With the value not changing, the indicator should return default value 0m. var sr = new SharpeRatio("SR", 10); - + // push the value 100000 into the indicator 20 times (sharpeRatioPeriod + movingAveragePeriod) for(int i = 0; i < 20; i++) { IndicatorDataPoint point = new IndicatorDataPoint(new DateTime(), 100000m); sr.Update(point); } - + Assert.AreEqual(sr.Current.Value, 0m); } - + [Test] - public void TestTradeBarsWithDifferingValue() + public void TestTradeBarsWithDifferingValue() { // With the value changing, the indicator should return a value that is not the default 0m. var sr = new SharpeRatio("SR", 10); - + // push the value 100000 into the indicator 20 times (sharpeRatioPeriod + movingAveragePeriod) for(int i = 0; i < 20; i++) { IndicatorDataPoint point = new IndicatorDataPoint(new DateTime(), 100000m + i); sr.Update(point); } - + Assert.AreNotEqual(sr.Current.Value, 0m); } - + [Test] public void TestDivByZero() { // With the value changing, the indicator should return a value that is not the default 0m. var sr = new SharpeRatio("SR", 10); - + // push the value 100000 into the indicator 20 times (sharpeRatioPeriod + movingAveragePeriod) - for(int i = 0; i < 20; i++) { + for(int i = 0; i < 20; i++) + { IndicatorDataPoint point = new IndicatorDataPoint(new DateTime(), 0); sr.Update(point); } - + Assert.AreEqual(sr.Current.Value, 0m); } + + [Test] + public void UsesRiskFreeInterestRateModel() + { + const int count = 20; + var dates = Enumerable.Range(0, count).Select(i => new DateTime(2023, 11, 21, 10, 0, 0) + TimeSpan.FromMinutes(i)).ToList(); + var interestRateValues = Enumerable.Range(0, count).Select(i => 0m + (10 - 0m) * (i / (count - 1m))).ToList(); + + var interestRateProviderMock = new Mock(); + + // Set up + for (int i = 0; i < count; i++) + { + interestRateProviderMock.Setup(x => x.GetInterestRate(dates[i])).Returns(interestRateValues[i]).Verifiable(); + } + + var sr = new TestableSharpeRatio("SR", 10, interestRateProviderMock.Object); + + // Push the value 100000 into the indicator 20 times (sharpeRatioPeriod + movingAveragePeriod) + for (int i = 0; i < count; i++) + { + sr.Update(new IndicatorDataPoint(dates[i], 100000m + i)); + Assert.AreEqual(interestRateValues[i], sr.RiskFreeRatePublic.Current.Value); + } + + // Assert + Assert.IsTrue(sr.IsReady); + interestRateProviderMock.Verify(x => x.GetInterestRate(It.IsAny()), Times.Exactly(dates.Count)); + for (int i = 0; i < count; i++) + { + interestRateProviderMock.Verify(x => x.GetInterestRate(dates[i]), Times.Once); + } + } + + [Test] + public void UsesPythonDefinedRiskFreeInterestRateModel() + { + using var _ = Py.GIL(); + + var module = PyModule.FromString(Guid.NewGuid().ToString(), @" +from AlgorithmImports import * + +class TestRiskFreeInterestRateModel: + CallCount = 0 + + def GetInterestRate(self, date: datetime) -> float: + TestRiskFreeInterestRateModel.CallCount += 1 + return 0.5 + +def getSharpeRatioIndicator() -> SharpeRatio: + return SharpeRatio(""SR"", 10, TestRiskFreeInterestRateModel()) + "); + + var sr = module.GetAttr("getSharpeRatioIndicator").Invoke().GetAndDispose(); + var modelClass = module.GetAttr("TestRiskFreeInterestRateModel"); + + var reference = new DateTime(2023, 11, 21, 10, 0, 0); + for (int i = 0; i < 20; i++) + { + sr.Update(new IndicatorDataPoint(reference + TimeSpan.FromMinutes(i), 100000m + i)); + Assert.AreEqual(i + 1, modelClass.GetAttr("CallCount").GetAndDispose()); + } + } + + private class TestableSharpeRatio : SharpeRatio + { + public Identity RiskFreeRatePublic => RiskFreeRate; + + public TestableSharpeRatio(string name, int period, IRiskFreeInterestRateModel riskFreeRateModel) + : base(name, period, riskFreeRateModel) + { + } + public TestableSharpeRatio(int period, decimal riskFreeRate = 0.0m) + : base(period, riskFreeRate) + { + } + + public TestableSharpeRatio(string name, int period, decimal riskFreeRate = 0.0m) + : base(name, period, riskFreeRate) + { + } + } } -} \ No newline at end of file +} diff --git a/Tests/ToolBox/RandomDataGenerator/RandomDataGeneratorTests.cs b/Tests/ToolBox/RandomDataGenerator/RandomDataGeneratorTests.cs index c238272f1eed..e5312c805520 100644 --- a/Tests/ToolBox/RandomDataGenerator/RandomDataGeneratorTests.cs +++ b/Tests/ToolBox/RandomDataGenerator/RandomDataGeneratorTests.cs @@ -117,6 +117,8 @@ public void RandomGeneratorProducesValuesBoundedForEquitiesWhenSplit(string star } } + private static readonly IRiskFreeInterestRateModel _interestRateProvider = new InterestRateProvider(); + private static SecurityService GetSecurityService(RandomDataGeneratorSettings settings, SecurityManager securityManager) { var securityService = new SecurityService( @@ -135,7 +137,8 @@ private static SecurityService GetSecurityService(RandomDataGeneratorSettings se // from settings if (security is Option option) { - option.PriceModel = OptionPriceModels.Create(settings.OptionPriceEngineName, Statistics.PortfolioStatistics.GetRiskFreeRate(settings.Start, settings.End)); + option.PriceModel = OptionPriceModels.Create(settings.OptionPriceEngineName, + _interestRateProvider.GetRiskFreeRate(settings.Start, settings.End)); } })), RegisteredSecurityDataTypesProvider.Null, diff --git a/ToolBox/RandomDataGenerator/RandomDataGeneratorProgram.cs b/ToolBox/RandomDataGenerator/RandomDataGeneratorProgram.cs index bad490a7f238..91f2937f01b6 100644 --- a/ToolBox/RandomDataGenerator/RandomDataGeneratorProgram.cs +++ b/ToolBox/RandomDataGenerator/RandomDataGeneratorProgram.cs @@ -24,6 +24,7 @@ using System; using System.Collections.Generic; using QuantConnect.Logging; +using QuantConnect.Data; namespace QuantConnect.ToolBox.RandomDataGenerator { @@ -32,6 +33,8 @@ namespace QuantConnect.ToolBox.RandomDataGenerator /// public static class RandomDataGeneratorProgram { + private static readonly IRiskFreeInterestRateModel _interestRateProvider = new InterestRateProvider(); + public static void RandomDataGenerator( string startDateString, string endDateString, @@ -81,7 +84,7 @@ List tickers Log.Error($"RandomDataGeneratorProgram(): Required parameter --start must be at least 19980101"); Environment.Exit(1); } - + var securityManager = new SecurityManager(new TimeKeeper(settings.Start, new[] { TimeZones.Utc })); var securityService = new SecurityService( new CashBook(), @@ -99,7 +102,8 @@ List tickers // from settings if (security is Option option) { - option.PriceModel = OptionPriceModels.Create(settings.OptionPriceEngineName, Statistics.PortfolioStatistics.GetRiskFreeRate(settings.Start, settings.End)); + option.PriceModel = OptionPriceModels.Create(settings.OptionPriceEngineName, + _interestRateProvider.GetRiskFreeRate(settings.Start, settings.End)); } })), RegisteredSecurityDataTypesProvider.Null,