/*
* 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.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using QuantConnect.Configuration;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Indicators;
using QuantConnect.Interfaces;
using QuantConnect.Lean.Engine.TransactionHandlers;
using QuantConnect.Logging;
using QuantConnect.Orders;
using QuantConnect.Orders.Serialization;
using QuantConnect.Packets;
using QuantConnect.Securities.Positions;
using QuantConnect.Statistics;
namespace QuantConnect.Lean.Engine.Results
{
///
/// Provides base functionality to the implementations of
///
public abstract class BaseResultsHandler
{
private RollingWindow _previousSalesVolume;
private DateTime _previousPortfolioTurnoverSample;
private bool _packetDroppedWarning;
private int _logCount;
private ConcurrentDictionary _customSummaryStatistics;
// used for resetting out/error upon completion
private static readonly TextWriter StandardOut = Console.Out;
private static readonly TextWriter StandardError = Console.Error;
private string _hostName;
private Bar _currentAlgorithmEquity;
///
/// String message saying: Strategy Equity
///
public const string StrategyEquityKey = "Strategy Equity";
///
/// String message saying: Equity
///
public const string EquityKey = "Equity";
///
/// String message saying: Return
///
public const string ReturnKey = "Return";
///
/// String message saying: Benchmark
///
public const string BenchmarkKey = "Benchmark";
///
/// String message saying: Drawdown
///
public const string DrawdownKey = "Drawdown";
///
/// String message saying: PortfolioTurnover
///
public const string PortfolioTurnoverKey = "Portfolio Turnover";
///
/// String message saying: Portfolio Margin
///
public const string PortfolioMarginKey = "Portfolio Margin";
///
/// String message saying: Portfolio Margin
///
public const string AssetsSalesVolumeKey = "Assets Sales Volume";
///
/// The main loop update interval
///
protected virtual TimeSpan MainUpdateInterval { get; } = TimeSpan.FromSeconds(3);
///
/// The chart update interval
///
protected TimeSpan ChartUpdateInterval { get; set; } = TimeSpan.FromMinutes(1);
///
/// The last position consumed from the by
///
protected int LastDeltaOrderPosition { get; set; }
///
/// The last position consumed from the while determining delta order events
///
protected int LastDeltaOrderEventsPosition { get; set; }
///
/// Serializer settings to use
///
protected JsonSerializerSettings SerializerSettings { get; set; } = new ()
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy
{
ProcessDictionaryKeys = false,
OverrideSpecifiedNames = true
}
}
};
///
/// The current aggregated equity bar for sampling.
/// It will be aggregated with values from the
///
protected Bar CurrentAlgorithmEquity
{
get
{
if (_currentAlgorithmEquity == null)
{
_currentAlgorithmEquity = new Bar();
UpdateAlgorithmEquity(_currentAlgorithmEquity);
}
return _currentAlgorithmEquity;
}
set
{
_currentAlgorithmEquity = value;
}
}
///
/// The task in charge of running the update method
///
private Thread _updateRunner;
///
/// Boolean flag indicating the thread is still active.
///
public bool IsActive => _updateRunner != null && _updateRunner.IsAlive;
///
/// Live packet messaging queue. Queue the messages here and send when the result queue is ready.
///
public ConcurrentQueue Messages { get; set; }
///
/// Storage for the price and equity charts of the live results.
///
public ConcurrentDictionary Charts { get; set; }
///
/// True if the exit has been triggered
///
protected volatile bool ExitTriggered;
///
/// Event set when exit is triggered
///
protected ManualResetEvent ExitEvent { get; }
///
/// The log store instance
///
protected List LogStore { get; }
///
/// Algorithms performance related chart names
///
/// Used to calculate the probabilistic sharpe ratio
protected List AlgorithmPerformanceCharts { get; } = new List { StrategyEquityKey, BenchmarkKey };
///
/// Lock to be used when accessing the chart collection
///
protected object ChartLock { get; }
///
/// The algorithm project id
///
protected int ProjectId { get; set; }
///
/// The maximum amount of RAM (in MB) this algorithm is allowed to utilize
///
protected string RamAllocation { get; set; }
///
/// The algorithm unique compilation id
///
protected string CompileId { get; set; }
///
/// The algorithm job id.
/// This is the deploy id for live, backtesting id for backtesting
///
protected string AlgorithmId { get; set; }
///
/// The result handler start time
///
protected DateTime StartTime { get; }
///
/// Customizable dynamic statistics
///
protected Dictionary RuntimeStatistics { get; }
///
/// State of the algorithm
///
protected Dictionary State { get; set; }
///
/// The handler responsible for communicating messages to listeners
///
protected IMessagingHandler MessagingHandler { get; set; }
///
/// The transaction handler used to get the algorithms Orders information
///
protected ITransactionHandler TransactionHandler { get; set; }
///
/// The algorithms starting portfolio value.
/// Used to calculate the portfolio return
///
protected decimal StartingPortfolioValue { get; set; }
///
/// The algorithm instance
///
protected virtual IAlgorithm Algorithm { get; set; }
///
/// Algorithm currency symbol, used in charting
///
protected string AlgorithmCurrencySymbol { get; set; }
///
/// Closing portfolio value. Used to calculate daily performance.
///
protected decimal DailyPortfolioValue { get; set; }
///
/// Cumulative max portfolio value. Used to calculate drawdown underwater.
///
protected decimal CumulativeMaxPortfolioValue { get; set; }
///
/// Sampling period for timespans between resamples of the charting equity.
///
/// Specifically critical for backtesting since with such long timeframes the sampled data can get extreme.
protected TimeSpan ResamplePeriod { get; set; }
///
/// How frequently the backtests push messages to the browser.
///
/// Update frequency of notification packets
protected TimeSpan NotificationPeriod { get; set; }
///
/// Directory location to store results
///
protected string ResultsDestinationFolder { get; set; }
///
/// The map file provider instance to use
///
protected IMapFileProvider MapFileProvider { get; set; }
///
/// Creates a new instance
///
protected BaseResultsHandler()
{
ExitEvent = new ManualResetEvent(false);
Charts = new ConcurrentDictionary();
//Default charts:
var equityChart = Charts[StrategyEquityKey] = new Chart(StrategyEquityKey);
equityChart.Series.Add(EquityKey, new CandlestickSeries(EquityKey, 0, "$"));
equityChart.Series.Add(ReturnKey, new Series(ReturnKey, SeriesType.Bar, 1, "%"));
Messages = new ConcurrentQueue();
RuntimeStatistics = new Dictionary();
StartTime = DateTime.UtcNow;
CompileId = "";
AlgorithmId = "";
ChartLock = new object();
LogStore = new List();
ResultsDestinationFolder = Globals.ResultsDestinationFolder;
State = new Dictionary
{
["StartTime"] = StartTime.ToStringInvariant(DateFormat.UI),
["EndTime"] = string.Empty,
["RuntimeError"] = string.Empty,
["StackTrace"] = string.Empty,
["LogCount"] = "0",
["OrderCount"] = "0",
["InsightCount"] = "0"
};
_previousSalesVolume = new(2);
_previousSalesVolume.Add(0);
_customSummaryStatistics = new();
}
///
/// New order event for the algorithm
///
/// New event details
public virtual void OrderEvent(OrderEvent newEvent)
{
}
///
/// Terminate the result thread and apply any required exit procedures like sending final results
///
public virtual void Exit()
{
// reset standard out/error
Console.SetOut(StandardOut);
Console.SetError(StandardError);
}
///
/// Gets the current Server statistics
///
protected virtual Dictionary GetServerStatistics(DateTime utcNow)
{
var serverStatistics = OS.GetServerStatistics();
serverStatistics["Hostname"] = _hostName;
var upTime = utcNow - StartTime;
serverStatistics["Up Time"] = $"{upTime.Days}d {upTime:hh\\:mm\\:ss}";
serverStatistics["Total RAM (MB)"] = RamAllocation;
return serverStatistics;
}
///
/// Stores the order events
///
/// The utc date associated with these order events
/// The order events to store
protected virtual void StoreOrderEvents(DateTime utcTime, List orderEvents)
{
if (orderEvents.Count <= 0)
{
return;
}
var filename = $"{AlgorithmId}-order-events.json";
var path = GetResultsPath(filename);
var data = JsonConvert.SerializeObject(orderEvents, Formatting.None, SerializerSettings);
File.WriteAllText(path, data);
}
///
/// Save insight results to persistent storage
///
/// Method called by the storing timer and on exit
protected virtual void StoreInsights()
{
if (Algorithm?.Insights == null)
{
// could be null if we are not initialized and exit is called
return;
}
// default save all results to disk and don't remove any from memory
// this will result in one file with all of the insights/results in it
var allInsights = Algorithm.Insights.GetInsights();
if (allInsights.Count > 0)
{
var alphaResultsPath = GetResultsPath(Path.Combine(AlgorithmId, "alpha-results.json"));
var directory = Directory.GetParent(alphaResultsPath);
if (!directory.Exists)
{
directory.Create();
}
var orderedInsights = allInsights.OrderBy(insight => insight.GeneratedTimeUtc);
File.WriteAllText(alphaResultsPath, JsonConvert.SerializeObject(orderedInsights, Formatting.Indented, SerializerSettings));
}
}
///
/// Gets the orders generated starting from the provided position
///
/// The delta orders
protected virtual Dictionary GetDeltaOrders(int orderEventsStartPosition, Func shouldStop)
{
var deltaOrders = new Dictionary();
foreach (var orderId in TransactionHandler.OrderEvents.Skip(orderEventsStartPosition).Select(orderEvent => orderEvent.OrderId))
{
LastDeltaOrderPosition++;
if (deltaOrders.ContainsKey(orderId))
{
// we can have more than 1 order event per order id
continue;
}
var order = Algorithm.Transactions.GetOrderById(orderId);
if (order == null)
{
// this shouldn't happen but just in case
continue;
}
// for charting
order.Price = order.Price.SmartRounding();
deltaOrders[orderId] = order;
if (shouldStop(deltaOrders.Count))
{
break;
}
}
return deltaOrders;
}
///
/// Initialize the result handler with this result packet.
///
/// DTO parameters class to initialize a result handler
public virtual void Initialize(ResultHandlerInitializeParameters parameters)
{
_hostName = parameters.Job.HostName ?? Environment.MachineName;
MessagingHandler = parameters.MessagingHandler;
TransactionHandler = parameters.TransactionHandler;
CompileId = parameters.Job.CompileId;
AlgorithmId = parameters.Job.AlgorithmId;
ProjectId = parameters.Job.ProjectId;
RamAllocation = parameters.Job.RamAllocation.ToStringInvariant();
_updateRunner = new Thread(Run, 0) { IsBackground = true, Name = "Result Thread" };
_updateRunner.Start();
State["Hostname"] = _hostName;
MapFileProvider = parameters.MapFileProvider;
SerializerSettings = new()
{
Converters = new [] { new OrderEventJsonConverter(AlgorithmId) },
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy
{
ProcessDictionaryKeys = false,
OverrideSpecifiedNames = true
}
}
};
}
///
/// Result handler update method
///
protected abstract void Run();
///
/// Gets the full path for a results file
///
/// The filename to add to the path
/// The full path, including the filename
protected string GetResultsPath(string filename)
{
return Path.Combine(ResultsDestinationFolder, filename);
}
///
/// Event fired each time that we add/remove securities from the data feed
///
public virtual void OnSecuritiesChanged(SecurityChanges changes)
{
}
///
/// Returns the location of the logs
///
/// Id that will be incorporated into the algorithm log name
/// The logs to save
/// The path to the logs
public virtual string SaveLogs(string id, List logs)
{
var filename = $"{id}-log.txt";
var path = GetResultsPath(filename);
var logLines = logs.Select(x => x.Message);
File.WriteAllLines(path, logLines);
return path;
}
///
/// Save the results to disk
///
/// The name of the results
/// The results to save
public virtual void SaveResults(string name, Result result)
{
File.WriteAllText(GetResultsPath(name), JsonConvert.SerializeObject(result, Formatting.Indented, SerializerSettings));
}
///
/// Purge/clear any outstanding messages in message queue.
///
protected void PurgeQueue()
{
Messages.Clear();
}
///
/// Stops the update runner task
///
protected void StopUpdateRunner()
{
_updateRunner.StopSafely(TimeSpan.FromMinutes(10));
_updateRunner = null;
}
///
/// Gets the algorithm net return
///
protected decimal GetNetReturn()
{
//Some users have $0 in their brokerage account / starting cash of $0. Prevent divide by zero errors
return StartingPortfolioValue > 0 ?
(Algorithm.Portfolio.TotalPortfolioValue - StartingPortfolioValue) / StartingPortfolioValue
: 0;
}
///
/// Save the snapshot of the total results to storage.
///
/// Packet to store.
protected abstract void StoreResult(Packet packet);
///
/// Gets the current portfolio value
///
/// Useful so that live trading implementation can freeze the returned value if there is no user exchange open
/// so we ignore extended market hours updates
protected virtual decimal GetPortfolioValue()
{
return Algorithm.Portfolio.TotalPortfolioValue;
}
///
/// Gets the current benchmark value
///
/// Useful so that live trading implementation can freeze the returned value if there is no user exchange open
/// so we ignore extended market hours updates
/// Time to resolve benchmark value at
protected virtual decimal GetBenchmarkValue(DateTime time)
{
if (Algorithm == null || Algorithm.Benchmark == null)
{
// this could happen if the algorithm exploded mid initialization
return 0;
}
return Algorithm.Benchmark.Evaluate(time).SmartRounding();
}
///
/// Samples portfolio equity, benchmark, and daily performance
/// Called by scheduled event every night at midnight algorithm time
///
/// Current UTC time in the AlgorithmManager loop
public virtual void Sample(DateTime time)
{
var currentPortfolioValue = GetPortfolioValue();
var portfolioPerformance = DailyPortfolioValue == 0 ? 0 : Math.Round((currentPortfolioValue - DailyPortfolioValue) * 100 / DailyPortfolioValue, 10);
// Update our max portfolio value
CumulativeMaxPortfolioValue = Math.Max(currentPortfolioValue, CumulativeMaxPortfolioValue);
// Sample all our default charts
UpdateAlgorithmEquity();
SampleEquity(time);
SampleBenchmark(time, GetBenchmarkValue(time));
SamplePerformance(time, portfolioPerformance);
SampleDrawdown(time, currentPortfolioValue);
SampleSalesVolume(time);
SampleExposure(time, currentPortfolioValue);
SampleCapacity(time);
SamplePortfolioTurnover(time, currentPortfolioValue);
SamplePortfolioMargin(time, currentPortfolioValue);
// Update daily portfolio value; works because we only call sample once a day
DailyPortfolioValue = currentPortfolioValue;
}
private void SamplePortfolioMargin(DateTime algorithmUtcTime, decimal currentPortfolioValue)
{
var state = PortfolioState.Create(Algorithm.Portfolio, algorithmUtcTime, currentPortfolioValue);
lock (ChartLock)
{
if (!Charts.TryGetValue(PortfolioMarginKey, out var chart))
{
chart = new Chart(PortfolioMarginKey);
Charts.AddOrUpdate(PortfolioMarginKey, chart);
}
PortfolioMarginChart.AddSample(chart, state, MapFileProvider, DateTime.UtcNow.Date);
}
}
///
/// Sample the current equity of the strategy directly with time and using
/// the current algorithm equity value in
///
/// Equity candlestick end time
protected virtual void SampleEquity(DateTime time)
{
Sample(StrategyEquityKey, EquityKey, 0, SeriesType.Candle, new Candlestick(time, CurrentAlgorithmEquity), AlgorithmCurrencySymbol);
// Reset the current algorithm equity object so another bar is create on the next sample
CurrentAlgorithmEquity = null;
}
///
/// Sample the current daily performance directly with a time-value pair.
///
/// Time of the sample.
/// Current daily performance value.
protected virtual void SamplePerformance(DateTime time, decimal value)
{
if (Log.DebuggingEnabled)
{
Log.Debug("BaseResultsHandler.SamplePerformance(): " + time.ToShortTimeString() + " >" + value);
}
Sample(StrategyEquityKey, ReturnKey, 1, SeriesType.Bar, new ChartPoint(time, value), "%");
}
///
/// Sample the current benchmark performance directly with a time-value pair.
///
/// Time of the sample.
/// Current benchmark value.
///
protected virtual void SampleBenchmark(DateTime time, decimal value)
{
Sample(BenchmarkKey, BenchmarkKey, 0, SeriesType.Line, new ChartPoint(time, value));
}
///
/// Sample drawdown of equity of the strategy
///
/// Time of the sample
/// Current equity value
protected virtual void SampleDrawdown(DateTime time, decimal currentPortfolioValue)
{
// This will throw otherwise, in this case just don't sample
if (CumulativeMaxPortfolioValue != 0)
{
// Calculate our drawdown and sample it
var drawdown = Statistics.Statistics.DrawdownPercent(currentPortfolioValue, CumulativeMaxPortfolioValue);
Sample(DrawdownKey, "Equity Drawdown", 0, SeriesType.Line, new ChartPoint(time, drawdown), "%");
}
}
///
/// Sample portfolio turn over of the strategy
///
/// Time of the sample
/// Current equity value
protected virtual void SamplePortfolioTurnover(DateTime time, decimal currentPortfolioValue)
{
if (currentPortfolioValue != 0)
{
if (Algorithm.StartDate == time.ConvertFromUtc(Algorithm.TimeZone))
{
// the first sample in backtesting is at start, we only want to sample after a full algorithm execution date
return;
}
var currentTotalSaleVolume = Algorithm.Portfolio.TotalSaleVolume;
decimal todayPortfolioTurnOver;
if (_previousPortfolioTurnoverSample == time)
{
// we are sampling the same time twice, this can happen if we sample at the start of the portfolio loop
// and the algorithm happen to end at the same time and we trigger the final sample to take into account that last loop
// this new sample will overwrite the previous, so we resample using T-2 sales volume
todayPortfolioTurnOver = (currentTotalSaleVolume - _previousSalesVolume[1]) / currentPortfolioValue;
}
else
{
todayPortfolioTurnOver = (currentTotalSaleVolume - _previousSalesVolume[0]) / currentPortfolioValue;
}
_previousSalesVolume.Add(currentTotalSaleVolume);
_previousPortfolioTurnoverSample = time;
Sample(PortfolioTurnoverKey, PortfolioTurnoverKey, 0, SeriesType.Line, new ChartPoint(time, todayPortfolioTurnOver), "%");
}
}
///
/// Sample assets sales volume
///
/// Time of the sample
protected virtual void SampleSalesVolume(DateTime time)
{
// Sample top 30 holdings by sales volume
foreach (var holding in Algorithm.Portfolio.Values.Where(y => y.TotalSaleVolume != 0)
.OrderByDescending(x => x.TotalSaleVolume).Take(30))
{
Sample(AssetsSalesVolumeKey, $"{holding.Symbol.Value}", 0, SeriesType.Treemap, new ChartPoint(time, holding.TotalSaleVolume),
AlgorithmCurrencySymbol);
}
}
///
/// Sample portfolio exposure long/short ratios by security type
///
/// Time of the sample
/// Current value of the portfolio
protected virtual void SampleExposure(DateTime time, decimal currentPortfolioValue)
{
// Will throw in this case, just return without sampling
if (currentPortfolioValue == 0)
{
return;
}
// Split up our holdings in one enumeration into long and shorts holding values
// only process those that we hold stock in.
var shortHoldings = new Dictionary();
var longHoldings = new Dictionary();
foreach (var holding in Algorithm.Portfolio.Values)
{
// Ensure we have a value for this security type in both our dictionaries
if (!longHoldings.ContainsKey(holding.Symbol.SecurityType))
{
longHoldings.Add(holding.Symbol.SecurityType, 0);
shortHoldings.Add(holding.Symbol.SecurityType, 0);
}
var holdingsValue = holding.HoldingsValue;
if (holdingsValue == 0)
{
continue;
}
// Long Position
if (holdingsValue > 0)
{
longHoldings[holding.Symbol.SecurityType] += holdingsValue;
}
// Short Position
else
{
shortHoldings[holding.Symbol.SecurityType] += holdingsValue;
}
}
// Sample our long and short positions
SampleExposureHelper(PositionSide.Long, time, currentPortfolioValue, longHoldings);
SampleExposureHelper(PositionSide.Short, time, currentPortfolioValue, shortHoldings);
}
///
/// Helper method for SampleExposure, samples our holdings value to
/// our exposure chart by their position side and security type
///
/// Side to sample from portfolio
/// Time of the sample
/// Current value of the portfolio
/// Enumerable of holdings to sample
private void SampleExposureHelper(PositionSide type, DateTime time, decimal currentPortfolioValue, Dictionary holdings)
{
foreach (var kvp in holdings)
{
var ratio = Math.Round(kvp.Value / currentPortfolioValue, 4);
Sample("Exposure", $"{kvp.Key} - {type} Ratio", 0, SeriesType.Line, new ChartPoint(time, ratio),
"");
}
}
///
/// Sample estimated strategy capacity
///
/// Time of the sample
protected virtual void SampleCapacity(DateTime time)
{
// NOP; Used only by BacktestingResultHandler because he owns a CapacityEstimate
}
///
/// Add a sample to the chart specified by the chartName, and seriesName.
///
/// String chart name to place the sample.
/// Series name for the chart.
/// Series chart index - which chart should this series belong
/// Series type for the chart.
/// Value for the chart sample.
/// Unit for the chart axis
/// Sample can be used to create new charts or sample equity - daily performance.
protected abstract void Sample(string chartName,
string seriesName,
int seriesIndex,
SeriesType seriesType,
ISeriesPoint value,
string unit = "$");
///
/// Gets the algorithm runtime statistics
///
protected SortedDictionary GetAlgorithmRuntimeStatistics(Dictionary summary, CapacityEstimate capacityEstimate = null)
{
var runtimeStatistics = new SortedDictionary();
lock (RuntimeStatistics)
{
foreach (var pair in RuntimeStatistics)
{
runtimeStatistics.Add(pair.Key, pair.Value);
}
}
if (summary.ContainsKey("Probabilistic Sharpe Ratio"))
{
runtimeStatistics["Probabilistic Sharpe Ratio"] = summary["Probabilistic Sharpe Ratio"];
}
else
{
runtimeStatistics["Probabilistic Sharpe Ratio"] = "0%";
}
runtimeStatistics["Unrealized"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalUnrealizedProfit.ToStringInvariant("N2");
runtimeStatistics["Fees"] = $"-{AlgorithmCurrencySymbol}{Algorithm.Portfolio.TotalFees.ToStringInvariant("N2")}";
runtimeStatistics["Net Profit"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalNetProfit.ToStringInvariant("N2");
runtimeStatistics["Return"] = GetNetReturn().ToStringInvariant("P");
runtimeStatistics["Equity"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalPortfolioValue.ToStringInvariant("N2");
runtimeStatistics["Holdings"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalHoldingsValue.ToStringInvariant("N2");
runtimeStatistics["Volume"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalSaleVolume.ToStringInvariant("N2");
return runtimeStatistics;
}
///
/// Sets the algorithm state data
///
protected void SetAlgorithmState(string error, string stack)
{
State["RuntimeError"] = error;
State["StackTrace"] = stack;
}
///
/// Gets the algorithm state data
///
protected Dictionary GetAlgorithmState(DateTime? endTime = null)
{
if (Algorithm == null || !string.IsNullOrEmpty(State["RuntimeError"]))
{
State["Status"] = AlgorithmStatus.RuntimeError.ToStringInvariant();
}
else
{
State["Status"] = Algorithm.Status.ToStringInvariant();
}
State["EndTime"] = endTime != null ? endTime.ToStringInvariant(DateFormat.UI) : string.Empty;
lock (LogStore)
{
State["LogCount"] = _logCount.ToStringInvariant();
}
State["OrderCount"] = Algorithm?.Transactions?.OrdersCount.ToStringInvariant() ?? "0";
State["InsightCount"] = Algorithm?.Insights.TotalCount.ToStringInvariant() ?? "0";
return State;
}
///
/// Will generate the statistics results and update the provided runtime statistics
///
protected StatisticsResults GenerateStatisticsResults(Dictionary charts,
SortedDictionary profitLoss = null, CapacityEstimate estimatedStrategyCapacity = null)
{
var statisticsResults = new StatisticsResults();
if (profitLoss == null)
{
profitLoss = new SortedDictionary();
}
try
{
//Generates error when things don't exist (no charting logged, runtime errors in main algo execution)
// make sure we've taken samples for these series before just blindly requesting them
if (charts.TryGetValue(StrategyEquityKey, out var strategyEquity) &&
strategyEquity.Series.TryGetValue(EquityKey, out var equity) &&
strategyEquity.Series.TryGetValue(ReturnKey, out var performance) &&
charts.TryGetValue(BenchmarkKey, out var benchmarkChart) &&
benchmarkChart.Series.TryGetValue(BenchmarkKey, out var benchmark))
{
var trades = Algorithm.TradeBuilder.ClosedTrades;
BaseSeries portfolioTurnover;
if (charts.TryGetValue(PortfolioTurnoverKey, out var portfolioTurnoverChart))
{
portfolioTurnoverChart.Series.TryGetValue(PortfolioTurnoverKey, out portfolioTurnover);
}
else
{
portfolioTurnover = new Series();
}
statisticsResults = StatisticsBuilder.Generate(trades, profitLoss, equity.Values, performance.Values, benchmark.Values,
portfolioTurnover.Values, StartingPortfolioValue, Algorithm.Portfolio.TotalFees, TotalTradesCount(),
estimatedStrategyCapacity, AlgorithmCurrencySymbol, Algorithm.Transactions, Algorithm.RiskFreeInterestRateModel,
Algorithm.Settings.TradingDaysPerYear.Value // already set in Brokerage|Backtesting-SetupHandler classes
);
}
statisticsResults.AddCustomSummaryStatistics(_customSummaryStatistics);
}
catch (Exception err)
{
Log.Error(err, "BaseResultsHandler.GenerateStatisticsResults(): Error generating statistics packet");
}
return statisticsResults;
}
///
/// Helper method to get the total trade count statistic
///
protected int TotalTradesCount()
{
return TransactionHandler?.OrdersCount ?? 0;
}
///
/// Calculates and gets the current statistics for the algorithm.
/// It will use the current and profit loss information calculated from the current transaction record
/// to generate the results.
///
/// The current statistics
protected StatisticsResults GenerateStatisticsResults(CapacityEstimate estimatedStrategyCapacity = null)
{
// could happen if algorithm failed to init
if (Algorithm == null)
{
return new StatisticsResults();
}
Dictionary charts;
lock (ChartLock)
{
charts = new(Charts);
}
var profitLoss = new SortedDictionary(Algorithm.Transactions.TransactionRecord);
return GenerateStatisticsResults(charts, profitLoss, estimatedStrategyCapacity);
}
///
/// Save an algorithm message to the log store. Uses a different timestamped method of adding messaging to interweve debug and logging messages.
///
/// String message to store
protected virtual void AddToLogStore(string message)
{
lock (LogStore)
{
LogStore.Add(new LogEntry(message));
_logCount++;
}
}
///
/// Processes algorithm logs.
/// Logs of the same type are batched together one per line and are sent out
///
protected void ProcessAlgorithmLogs(int? messageQueueLimit = null)
{
ProcessAlgorithmLogsImpl(Algorithm.DebugMessages, PacketType.Debug, messageQueueLimit);
ProcessAlgorithmLogsImpl(Algorithm.ErrorMessages, PacketType.HandledError, messageQueueLimit);
ProcessAlgorithmLogsImpl(Algorithm.LogMessages, PacketType.Log, messageQueueLimit);
}
private void ProcessAlgorithmLogsImpl(ConcurrentQueue concurrentQueue, PacketType packetType, int? messageQueueLimit = null)
{
if (concurrentQueue.IsEmpty)
{
return;
}
var endTime = DateTime.UtcNow.AddMilliseconds(250).Ticks;
var currentMessageCount = -1;
while (DateTime.UtcNow.Ticks < endTime && concurrentQueue.TryDequeue(out var message))
{
if (messageQueueLimit.HasValue)
{
if (currentMessageCount == -1)
{
// this is expensive, so let's get it once
currentMessageCount = Messages.Count;
}
if (currentMessageCount > messageQueueLimit)
{
if (!_packetDroppedWarning)
{
_packetDroppedWarning = true;
// this shouldn't happen in most cases, queue limit is high and consumed often but just in case let's not silently drop packets without a warning
Messages.Enqueue(new HandledErrorPacket(AlgorithmId, "Your algorithm messaging has been rate limited to prevent browser flooding."));
}
//if too many in the queue already skip the logging and drop the messages
continue;
}
}
if (packetType == PacketType.Debug)
{
Messages.Enqueue(new DebugPacket(ProjectId, AlgorithmId, CompileId, message));
}
else if (packetType == PacketType.Log)
{
Messages.Enqueue(new LogPacket(AlgorithmId, message));
}
else if (packetType == PacketType.HandledError)
{
Messages.Enqueue(new HandledErrorPacket(AlgorithmId, message));
}
AddToLogStore(message);
// increase count after we add
currentMessageCount++;
}
}
///
/// Sets or updates a custom summary statistic
///
/// The statistic name
/// The statistic value
protected void SummaryStatistic(string name, string value)
{
_customSummaryStatistics.AddOrUpdate(name, value);
}
///
/// Updates the current equity bar with the current equity value from
///
///
/// This is required in order to update the bar without using the getter,
/// which would cause the bar to be created if it doesn't exist.
///
private void UpdateAlgorithmEquity(Bar equity)
{
equity.Update(Math.Round(GetPortfolioValue(), 4));
}
///
/// Updates the current equity bar with the current equity value from
///
protected void UpdateAlgorithmEquity()
{
UpdateAlgorithmEquity(CurrentAlgorithmEquity);
}
}
}