Skip to content

Commit

Permalink
Add Kelly Criterion values
Browse files Browse the repository at this point in the history
- New `KellyCriterionManager` will be used by the
`StatisticsInsightManagerExtension` to calculate and update the
`AlphaRuntimeStatistics` with the new Kelly Criterion values, on a daily
basis.
  • Loading branch information
Martin-Molinero committed Jul 26, 2019
1 parent 6afe92f commit a79cc16
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 27 deletions.
2 changes: 2 additions & 0 deletions Algorithm.CSharp/BasicTemplateFrameworkAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ public override void OnOrderEvent(OrderEvent orderEvent)
{"Treynor Ratio", "0.011"},
{"Total Fees", "$9.77"},
{"Fitness Score", "0.747"},
{"Kelly Criterion Estimate", "0.26"},
{"Kelly Criterion Probability Value", "1"},
{"Total Insights Generated", "100"},
{"Total Insights Closed", "99"},
{"Total Insights Analysis Completed", "99"},
Expand Down
4 changes: 3 additions & 1 deletion Algorithm.CSharp/BasicTemplateFuturesAlgorithm.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ select futuresContract
{"Tracking Error", "0.414"},
{"Treynor Ratio", "10.413"},
{"Total Fees", "$15207.00"},
{"Fitness Score", "0.033"}
{"Fitness Score", "0.033"},
{"Kelly Criterion Estimate", "-0.022"},
{"Kelly Criterion Probability Value", "1"}
};
}
}
38 changes: 38 additions & 0 deletions Common/AlphaRuntimeStatistics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ public class AlphaRuntimeStatistics
private decimal _overrideEstimatedMonthlyAlphaValue;
private readonly IAccountCurrencyProvider _accountCurrencyProvider;
private decimal _fitnessScore;
private decimal _kellyCriterionEstimate;
private decimal _kellyCriterionProbabilityValue;
private decimal _portfolioTurnover;
private decimal _returnOverMaxDrawdown;
private decimal _sortinoRatio;
Expand Down Expand Up @@ -83,6 +85,40 @@ public AlphaRuntimeStatistics()
/// </summary>
public decimal TotalAccumulatedEstimatedAlphaValue { get; set; }

/// <summary>
/// Score of the strategy's insights predictive power
/// </summary>
/// <remarks>See https://www.quantconnect.com/forum/discussion/6194/insight-scoring-metric/p1.
/// For performance we only truncate when the value is gotten</remarks>
public decimal KellyCriterionEstimate
{
get
{
return _kellyCriterionEstimate.TruncateTo3DecimalPlaces();
}
set
{
_kellyCriterionEstimate = value;
}
}

/// <summary>
/// The p-value or probability value of the <see cref="KellyCriterionEstimate"/>
/// </summary>
/// <remarks>See https://www.quantconnect.com/forum/discussion/6194/insight-scoring-metric/p1.
/// For performance we only truncate when the value is gotten</remarks>
public decimal KellyCriterionProbabilityValue
{
get
{
return _kellyCriterionProbabilityValue.TruncateTo3DecimalPlaces();
}
set
{
_kellyCriterionProbabilityValue = value;
}
}

/// <summary>
/// Score of the strategy's performance, and suitability for the Alpha Stream Market
/// </summary>
Expand Down Expand Up @@ -198,6 +234,8 @@ public Dictionary<string, string> ToDictionary()
return new Dictionary<string, string>
{
{"Fitness Score", $"{FitnessScore}"},
{"Kelly Criterion Estimate", $"{KellyCriterionEstimate}"},
{"Kelly Criterion Probability Value", $"{KellyCriterionProbabilityValue}"},
{"Sortino Ratio", $"{SortinoRatio}"},
{"Return Over Maximum Drawdown", $"{ReturnOverMaxDrawdown}"},
{"Portfolio Turnover", $"{PortfolioTurnover}"},
Expand Down
1 change: 1 addition & 0 deletions Common/QuantConnect.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,7 @@
<Compile Include="Algorithm\Framework\Alphas\InsightType.cs" />
<Compile Include="Statistics\AlgorithmPerformance.cs" />
<Compile Include="Statistics\FitnessScoreManager.cs" />
<Compile Include="Statistics\KellyCriterionManager.cs" />
<Compile Include="Statistics\PortfolioStatistics.cs" />
<Compile Include="Statistics\StatisticsBuilder.cs" />
<Compile Include="Statistics\StatisticsResults.cs" />
Expand Down
123 changes: 123 additions & 0 deletions Common/Statistics/KellyCriterionManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* 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 MathNet.Numerics.Distributions;
using MathNet.Numerics.Statistics;
using QuantConnect.Logging;

namespace QuantConnect.Statistics
{
/// <summary>
/// Class in charge of calculating the Kelly Criterion values
/// </summary>
/// <remarks>See https://www.quantconnect.com/forum/discussion/6194/insight-scoring-metric/p1 </remarks>
public class KellyCriterionManager
{
private bool _requiresRecalculation;
private double _average;
private readonly Normal _normalDistribution = new Normal();
/// <summary>
/// We keep both the value and the corresponding time in separate collections for performance
/// this way we can directly calculate the Mean() and Variance() on the values collection
/// with no need to select or create another temporary collection
/// </summary>
private readonly List<double> _insightValues = new List<double>();
private readonly List<DateTime> _insightTime = new List<DateTime>();

/// <summary>
/// Score of the strategy's insights predictive power
/// </summary>
public decimal KellyCriterionEstimate { get; set; }

/// <summary>
/// The p-value or probability value of the <see cref="KellyCriterionEstimate"/>
/// </summary>
public decimal KellyCriterionProbabilityValue { get; set; }

/// <summary>
/// Adds a new value to the population.
/// Will remove values older than an year compared with the provided time.
/// For performance, will update the continuous average calculation
/// </summary>
/// <param name="newValue">The new value to add</param>
/// <param name="time">The new values time</param>
public void AddNewValue(decimal newValue, DateTime time)
{
_requiresRecalculation = true;

// calculate new average, adding new value
_average = (_insightValues.Count * _average + (double)newValue)
/ (_insightValues.Count + 1);

_insightValues.Add((double)newValue);
_insightTime.Add(time);

// clean up values older than a year
var firstTime = _insightTime[0];
while ((time - firstTime) >= Time.OneYear)
{
// calculate new average, removing a value
_average = (_insightValues.Count * _average - _insightValues[0])
/ (_insightValues.Count - 1);

_insightValues.RemoveAt(0);
_insightTime.RemoveAt(0);
// there will always be at least 1 item, the one we just added
firstTime = _insightTime[0];
}
}

/// <summary>
/// Updates the Kelly Criterion values
/// </summary>
public void UpdateScores()
{
try
{
// need at least 2 samples
if (_requiresRecalculation && _insightValues.Count > 1)
{
_requiresRecalculation = false;

var averagePowered = Math.Pow(_average, 2);

var variance = _insightValues.Variance();
var denominator = averagePowered + variance;
var kellyCriterionEstimate = denominator.IsNaNOrZero() ? 0 : _average / denominator;

KellyCriterionEstimate = kellyCriterionEstimate.SafeDecimalCast();

var variancePowered = Math.Pow(variance, 2);
var kellyCriterionStandardDeviation = Math.Sqrt(
(1 / variance + 2 * averagePowered / variancePowered)
/ _insightValues.Count - 1);

KellyCriterionProbabilityValue = 1 - _normalDistribution
.CumulativeDistribution(
kellyCriterionEstimate / kellyCriterionStandardDeviation)
.SafeDecimalCast();
}
}
catch (Exception exception)
{
// just in case...
Log.Error(exception);
}
}
}
}
6 changes: 6 additions & 0 deletions Common/Time.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ public static class Time
/// </summary>
public static readonly TimeSpan MaxTimeSpan = TimeSpan.FromDays(1000*365);

/// <summary>
/// One Year TimeSpan Period Constant
/// </summary>
/// <remarks>365 days</remarks>
public static readonly TimeSpan OneYear = TimeSpan.FromDays(365);

/// <summary>
/// One Day TimeSpan Period Constant
/// </summary>
Expand Down
46 changes: 24 additions & 22 deletions Engine/Alphas/DefaultAlphaHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,20 @@ public class DefaultAlphaHandler : IAlphaHandler
{
private DateTime _lastStepTime;
private List<Insight> _insights;
private ChartingInsightManagerExtension _charting;
private ISecurityValuesProvider _securityValuesProvider;
private CancellationTokenSource _cancellationTokenSource;
private FitnessScoreManager _fitnessScore;
private DateTime _lastFitnessScoreCalculation;
private readonly object _lock = new object();

/// <summary>
/// The cancellation token that will be cancelled when requested to exit
/// </summary>
protected CancellationTokenSource CancellationTokenSource { get; set; }

/// <summary>
/// Gets a flag indicating if this handler's thread is still running and processing messages
/// </summary>
public bool IsActive { get; private set; }
public virtual bool IsActive { get; private set; }

/// <summary>
/// Gets the current alpha runtime statistics
Expand Down Expand Up @@ -96,7 +99,7 @@ public class DefaultAlphaHandler : IAlphaHandler
/// <param name="api">Api instance</param>
public virtual void Initialize(AlgorithmNodePacket job, IAlgorithm algorithm, IMessagingHandler messagingHandler, IApi api)
{
// initializing these properties just in case, doens't hurt to have them populated
// initializing these properties just in case, doesn't hurt to have them populated
Job = job;
Algorithm = algorithm;
MessagingHandler = messagingHandler;
Expand All @@ -107,14 +110,11 @@ public virtual void Initialize(AlgorithmNodePacket job, IAlgorithm algorithm, IM

InsightManager = CreateInsightManager();

// send scored insights to messaging handler
InsightManager.AddExtension(CreateAlphaResultPacketSender());

var statistics = new StatisticsInsightManagerExtension(algorithm);
RuntimeStatistics = statistics.Statistics;
InsightManager.AddExtension(statistics);
_charting = new ChartingInsightManagerExtension(algorithm, statistics);
InsightManager.AddExtension(_charting);

AddInsightManagerCustomExtensions(statistics);

// when insight is generated, take snapshot of securities and place in queue for insight manager to process on alpha thread
algorithm.InsightsGenerated += (algo, collection) =>
Expand All @@ -126,6 +126,16 @@ public virtual void Initialize(AlgorithmNodePacket job, IAlgorithm algorithm, IM
};
}

/// <summary>
/// Allows each alpha handler implementation to add there own optional extensions
/// </summary>
protected virtual void AddInsightManagerCustomExtensions(StatisticsInsightManagerExtension statistics)
{
// send scored insights to messaging handler
InsightManager.AddExtension(new AlphaResultPacketSender(Job, MessagingHandler, TimeSpan.FromSeconds(1), 50));
InsightManager.AddExtension(new ChartingInsightManagerExtension(Algorithm, statistics));
}

/// <summary>
/// Invoked after the algorithm's Initialize method was called allowing the alpha handler to check
/// other things, such as sampling period for backtests
Expand All @@ -144,7 +154,7 @@ public void OnAfterAlgorithmInitialized(IAlgorithm algorithm)
/// </summary>
public virtual void ProcessSynchronousEvents()
{
// check the last snap shot time, we may have already produced a snapshot via OnInsightssGenerated
// check the last snap shot time, we may have already produced a snapshot via OnInsightsGenerated
if (_lastStepTime != Algorithm.UtcTime)
{
_lastStepTime = Algorithm.UtcTime;
Expand Down Expand Up @@ -173,15 +183,15 @@ public virtual void ProcessSynchronousEvents()
public virtual void Run()
{
IsActive = true;
_cancellationTokenSource = new CancellationTokenSource();
CancellationTokenSource = new CancellationTokenSource();

using (LiveMode ? new Timer(_ => StoreInsights(),
null,
TimeSpan.FromMinutes(10),
TimeSpan.FromMinutes(10)) : null)
{
// run main loop until canceled, will clean out work queues separately
while (!_cancellationTokenSource.IsCancellationRequested)
while (!CancellationTokenSource.IsCancellationRequested)
{
try
{
Expand Down Expand Up @@ -213,7 +223,7 @@ public void Exit()
{
Log.Trace("DefaultAlphaHandler.Exit(): Exiting Thread...");

_cancellationTokenSource.Cancel(false);
CancellationTokenSource?.Cancel(false);
}

/// <summary>
Expand All @@ -226,6 +236,7 @@ protected void ProcessAsynchronousEvents()
/// <summary>
/// Save insight results to persistent storage
/// </summary>
/// <remarks>Method called by <see cref="Run"/></remarks>
protected virtual void StoreInsights()
{
// avoid reentrancy
Expand Down Expand Up @@ -261,15 +272,6 @@ protected virtual InsightManager CreateInsightManager()
return new InsightManager(scoreFunctionProvider, 0);
}

/// <summary>
/// Creates the <see cref="AlphaResultPacketSender"/> to manage sending finalized insights via the messaging handler
/// </summary>
/// <returns>A new <see cref="CreateAlphaResultPacketSender"/> instance</returns>
protected virtual AlphaResultPacketSender CreateAlphaResultPacketSender()
{
return new AlphaResultPacketSender(Job, MessagingHandler, TimeSpan.FromSeconds(1), 50);
}

/// <summary>
/// Encapsulates routing finalized insights to the messaging handler
/// </summary>
Expand Down
Loading

0 comments on commit a79cc16

Please sign in to comment.