/* * 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); } } }