Lean  $LEAN_TAG$
BaseResultsHandler.cs
1 /*
2  * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3  * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4  *
5  * Licensed under the Apache License, Version 2.0 (the "License");
6  * you may not use this file except in compliance with the License.
7  * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  *
15 */
16 
17 using System;
18 using System.Collections.Concurrent;
19 using System.Collections.Generic;
20 using System.IO;
21 using System.Linq;
22 using System.Threading;
23 using Newtonsoft.Json;
24 using Newtonsoft.Json.Serialization;
31 using QuantConnect.Logging;
32 using QuantConnect.Orders;
34 using QuantConnect.Packets;
37 
39 {
40  /// <summary>
41  /// Provides base functionality to the implementations of <see cref="IResultHandler"/>
42  /// </summary>
43  public abstract class BaseResultsHandler
44  {
45  private RollingWindow<decimal> _previousSalesVolume;
46  private DateTime _previousPortfolioTurnoverSample;
47  private bool _packetDroppedWarning;
48  private int _logCount;
49  private ConcurrentDictionary<string, string> _customSummaryStatistics;
50  // used for resetting out/error upon completion
51  private static readonly TextWriter StandardOut = Console.Out;
52  private static readonly TextWriter StandardError = Console.Error;
53 
54  private string _hostName;
55 
56  private Bar _currentAlgorithmEquity;
57 
58  /// <summary>
59  /// String message saying: Strategy Equity
60  /// </summary>
61  public const string StrategyEquityKey = "Strategy Equity";
62 
63  /// <summary>
64  /// String message saying: Equity
65  /// </summary>
66  public const string EquityKey = "Equity";
67 
68  /// <summary>
69  /// String message saying: Return
70  /// </summary>
71  public const string ReturnKey = "Return";
72 
73  /// <summary>
74  /// String message saying: Benchmark
75  /// </summary>
76  public const string BenchmarkKey = "Benchmark";
77 
78  /// <summary>
79  /// String message saying: Drawdown
80  /// </summary>
81  public const string DrawdownKey = "Drawdown";
82 
83  /// <summary>
84  /// String message saying: PortfolioTurnover
85  /// </summary>
86  public const string PortfolioTurnoverKey = "Portfolio Turnover";
87 
88  /// <summary>
89  /// String message saying: Portfolio Margin
90  /// </summary>
91  public const string PortfolioMarginKey = "Portfolio Margin";
92 
93  /// <summary>
94  /// String message saying: Portfolio Margin
95  /// </summary>
96  public const string AssetsSalesVolumeKey = "Assets Sales Volume";
97 
98  /// <summary>
99  /// The main loop update interval
100  /// </summary>
101  protected virtual TimeSpan MainUpdateInterval { get; } = TimeSpan.FromSeconds(3);
102 
103  /// <summary>
104  /// The chart update interval
105  /// </summary>
106  protected TimeSpan ChartUpdateInterval { get; set; } = TimeSpan.FromMinutes(1);
107 
108  /// <summary>
109  /// The last position consumed from the <see cref="ITransactionHandler.OrderEvents"/> by <see cref="GetDeltaOrders"/>
110  /// </summary>
111  protected int LastDeltaOrderPosition { get; set; }
112 
113  /// <summary>
114  /// The last position consumed from the <see cref="ITransactionHandler.OrderEvents"/> while determining delta order events
115  /// </summary>
116  protected int LastDeltaOrderEventsPosition { get; set; }
117 
118  /// <summary>
119  /// Serializer settings to use
120  /// </summary>
121  protected JsonSerializerSettings SerializerSettings { get; set; } = new ()
122  {
123  ContractResolver = new DefaultContractResolver
124  {
125  NamingStrategy = new CamelCaseNamingStrategy
126  {
127  ProcessDictionaryKeys = false,
128  OverrideSpecifiedNames = true
129  }
130  }
131  };
132 
133  /// <summary>
134  /// The current aggregated equity bar for sampling.
135  /// It will be aggregated with values from the <see cref="GetPortfolioValue"/>
136  /// </summary>
137  protected Bar CurrentAlgorithmEquity
138  {
139  get
140  {
141  if (_currentAlgorithmEquity == null)
142  {
143  _currentAlgorithmEquity = new Bar();
144  UpdateAlgorithmEquity(_currentAlgorithmEquity);
145  }
146  return _currentAlgorithmEquity;
147  }
148  set
149  {
150  _currentAlgorithmEquity = value;
151  }
152  }
153 
154  /// <summary>
155  /// The task in charge of running the <see cref="Run"/> update method
156  /// </summary>
157  private Thread _updateRunner;
158 
159  /// <summary>
160  /// Boolean flag indicating the thread is still active.
161  /// </summary>
162  public bool IsActive => _updateRunner != null && _updateRunner.IsAlive;
163 
164  /// <summary>
165  /// Live packet messaging queue. Queue the messages here and send when the result queue is ready.
166  /// </summary>
167  public ConcurrentQueue<Packet> Messages { get; set; }
168 
169  /// <summary>
170  /// Storage for the price and equity charts of the live results.
171  /// </summary>
172  public ConcurrentDictionary<string, Chart> Charts { get; set; }
173 
174  /// <summary>
175  /// True if the exit has been triggered
176  /// </summary>
177  protected volatile bool ExitTriggered;
178 
179  /// <summary>
180  /// Event set when exit is triggered
181  /// </summary>
182  protected ManualResetEvent ExitEvent { get; }
183 
184  /// <summary>
185  /// The log store instance
186  /// </summary>
187  protected List<LogEntry> LogStore { get; }
188 
189  /// <summary>
190  /// Algorithms performance related chart names
191  /// </summary>
192  /// <remarks>Used to calculate the probabilistic sharpe ratio</remarks>
193  protected List<string> AlgorithmPerformanceCharts { get; } = new List<string> { StrategyEquityKey, BenchmarkKey };
194 
195  /// <summary>
196  /// Lock to be used when accessing the chart collection
197  /// </summary>
198  protected object ChartLock { get; }
199 
200  /// <summary>
201  /// The algorithm project id
202  /// </summary>
203  protected int ProjectId { get; set; }
204 
205  /// <summary>
206  /// The maximum amount of RAM (in MB) this algorithm is allowed to utilize
207  /// </summary>
208  protected string RamAllocation { get; set; }
209 
210  /// <summary>
211  /// The algorithm unique compilation id
212  /// </summary>
213  protected string CompileId { get; set; }
214 
215  /// <summary>
216  /// The algorithm job id.
217  /// This is the deploy id for live, backtesting id for backtesting
218  /// </summary>
219  protected string AlgorithmId { get; set; }
220 
221  /// <summary>
222  /// The result handler start time
223  /// </summary>
224  protected DateTime StartTime { get; }
225 
226  /// <summary>
227  /// Customizable dynamic statistics <see cref="IAlgorithm.RuntimeStatistics"/>
228  /// </summary>
229  protected Dictionary<string, string> RuntimeStatistics { get; }
230 
231  /// <summary>
232  /// State of the algorithm
233  /// </summary>
234  protected Dictionary<string, string> State { get; set; }
235 
236  /// <summary>
237  /// The handler responsible for communicating messages to listeners
238  /// </summary>
239  protected IMessagingHandler MessagingHandler { get; set; }
240 
241  /// <summary>
242  /// The transaction handler used to get the algorithms Orders information
243  /// </summary>
244  protected ITransactionHandler TransactionHandler { get; set; }
245 
246  /// <summary>
247  /// The algorithms starting portfolio value.
248  /// Used to calculate the portfolio return
249  /// </summary>
250  protected decimal StartingPortfolioValue { get; set; }
251 
252  /// <summary>
253  /// The algorithm instance
254  /// </summary>
255  protected virtual IAlgorithm Algorithm { get; set; }
256 
257  /// <summary>
258  /// Algorithm currency symbol, used in charting
259  /// </summary>
260  protected string AlgorithmCurrencySymbol { get; set; }
261 
262  /// <summary>
263  /// Closing portfolio value. Used to calculate daily performance.
264  /// </summary>
265  protected decimal DailyPortfolioValue { get; set; }
266 
267  /// <summary>
268  /// Cumulative max portfolio value. Used to calculate drawdown underwater.
269  /// </summary>
270  protected decimal CumulativeMaxPortfolioValue { get; set; }
271 
272  /// <summary>
273  /// Sampling period for timespans between resamples of the charting equity.
274  /// </summary>
275  /// <remarks>Specifically critical for backtesting since with such long timeframes the sampled data can get extreme.</remarks>
276  protected TimeSpan ResamplePeriod { get; set; }
277 
278  /// <summary>
279  /// How frequently the backtests push messages to the browser.
280  /// </summary>
281  /// <remarks>Update frequency of notification packets</remarks>
282  protected TimeSpan NotificationPeriod { get; set; }
283 
284  /// <summary>
285  /// Directory location to store results
286  /// </summary>
287  protected string ResultsDestinationFolder { get; set; }
288 
289  /// <summary>
290  /// The map file provider instance to use
291  /// </summary>
292  protected IMapFileProvider MapFileProvider { get; set; }
293 
294  /// <summary>
295  /// Creates a new instance
296  /// </summary>
297  protected BaseResultsHandler()
298  {
299  ExitEvent = new ManualResetEvent(false);
300  Charts = new ConcurrentDictionary<string, Chart>();
301  //Default charts:
302  var equityChart = Charts[StrategyEquityKey] = new Chart(StrategyEquityKey);
303  equityChart.Series.Add(EquityKey, new CandlestickSeries(EquityKey, 0, "$"));
304  equityChart.Series.Add(ReturnKey, new Series(ReturnKey, SeriesType.Bar, 1, "%"));
305 
306  Messages = new ConcurrentQueue<Packet>();
307  RuntimeStatistics = new Dictionary<string, string>();
308  StartTime = DateTime.UtcNow;
309  CompileId = "";
310  AlgorithmId = "";
311  ChartLock = new object();
312  LogStore = new List<LogEntry>();
314  State = new Dictionary<string, string>
315  {
316  ["StartTime"] = StartTime.ToStringInvariant(DateFormat.UI),
317  ["EndTime"] = string.Empty,
318  ["RuntimeError"] = string.Empty,
319  ["StackTrace"] = string.Empty,
320  ["LogCount"] = "0",
321  ["OrderCount"] = "0",
322  ["InsightCount"] = "0"
323  };
324  _previousSalesVolume = new(2);
325  _previousSalesVolume.Add(0);
326  _customSummaryStatistics = new();
327  }
328 
329  /// <summary>
330  /// New order event for the algorithm
331  /// </summary>
332  /// <param name="newEvent">New event details</param>
333  public virtual void OrderEvent(OrderEvent newEvent)
334  {
335  }
336 
337  /// <summary>
338  /// Terminate the result thread and apply any required exit procedures like sending final results
339  /// </summary>
340  public virtual void Exit()
341  {
342  // reset standard out/error
343  Console.SetOut(StandardOut);
344  Console.SetError(StandardError);
345  }
346 
347  /// <summary>
348  /// Gets the current Server statistics
349  /// </summary>
350  protected virtual Dictionary<string, string> GetServerStatistics(DateTime utcNow)
351  {
352  var serverStatistics = OS.GetServerStatistics();
353  serverStatistics["Hostname"] = _hostName;
354  var upTime = utcNow - StartTime;
355  serverStatistics["Up Time"] = $"{upTime.Days}d {upTime:hh\\:mm\\:ss}";
356  serverStatistics["Total RAM (MB)"] = RamAllocation;
357  return serverStatistics;
358  }
359 
360  /// <summary>
361  /// Stores the order events
362  /// </summary>
363  /// <param name="utcTime">The utc date associated with these order events</param>
364  /// <param name="orderEvents">The order events to store</param>
365  protected virtual void StoreOrderEvents(DateTime utcTime, List<OrderEvent> orderEvents)
366  {
367  if (orderEvents.Count <= 0)
368  {
369  return;
370  }
371 
372  var filename = $"{AlgorithmId}-order-events.json";
373  var path = GetResultsPath(filename);
374 
375  var data = JsonConvert.SerializeObject(orderEvents, Formatting.None, SerializerSettings);
376 
377  File.WriteAllText(path, data);
378  }
379 
380  /// <summary>
381  /// Save insight results to persistent storage
382  /// </summary>
383  /// <remarks>Method called by the storing timer and on exit</remarks>
384  protected virtual void StoreInsights()
385  {
386  if (Algorithm?.Insights == null)
387  {
388  // could be null if we are not initialized and exit is called
389  return;
390  }
391  // default save all results to disk and don't remove any from memory
392  // this will result in one file with all of the insights/results in it
393  var allInsights = Algorithm.Insights.GetInsights();
394  if (allInsights.Count > 0)
395  {
396  var alphaResultsPath = GetResultsPath(Path.Combine(AlgorithmId, "alpha-results.json"));
397  var directory = Directory.GetParent(alphaResultsPath);
398  if (!directory.Exists)
399  {
400  directory.Create();
401  }
402  var orderedInsights = allInsights.OrderBy(insight => insight.GeneratedTimeUtc);
403  File.WriteAllText(alphaResultsPath, JsonConvert.SerializeObject(orderedInsights, Formatting.Indented, SerializerSettings));
404  }
405  }
406 
407  /// <summary>
408  /// Gets the orders generated starting from the provided <see cref="ITransactionHandler.OrderEvents"/> position
409  /// </summary>
410  /// <returns>The delta orders</returns>
411  protected virtual Dictionary<int, Order> GetDeltaOrders(int orderEventsStartPosition, Func<int, bool> shouldStop)
412  {
413  var deltaOrders = new Dictionary<int, Order>();
414 
415  foreach (var orderId in TransactionHandler.OrderEvents.Skip(orderEventsStartPosition).Select(orderEvent => orderEvent.OrderId))
416  {
418  if (deltaOrders.ContainsKey(orderId))
419  {
420  // we can have more than 1 order event per order id
421  continue;
422  }
423 
424  var order = Algorithm.Transactions.GetOrderById(orderId);
425  if (order == null)
426  {
427  // this shouldn't happen but just in case
428  continue;
429  }
430 
431  // for charting
432  order.Price = order.Price.SmartRounding();
433 
434  deltaOrders[orderId] = order;
435 
436  if (shouldStop(deltaOrders.Count))
437  {
438  break;
439  }
440  }
441 
442  return deltaOrders;
443  }
444 
445  /// <summary>
446  /// Initialize the result handler with this result packet.
447  /// </summary>
448  /// <param name="parameters">DTO parameters class to initialize a result handler</param>
449  public virtual void Initialize(ResultHandlerInitializeParameters parameters)
450  {
451  _hostName = parameters.Job.HostName ?? Environment.MachineName;
452  MessagingHandler = parameters.MessagingHandler;
454  CompileId = parameters.Job.CompileId;
455  AlgorithmId = parameters.Job.AlgorithmId;
456  ProjectId = parameters.Job.ProjectId;
457  RamAllocation = parameters.Job.RamAllocation.ToStringInvariant();
458  _updateRunner = new Thread(Run, 0) { IsBackground = true, Name = "Result Thread" };
459  _updateRunner.Start();
460  State["Hostname"] = _hostName;
461  MapFileProvider = parameters.MapFileProvider;
462 
463  SerializerSettings = new()
464  {
465  Converters = new [] { new OrderEventJsonConverter(AlgorithmId) },
466  ContractResolver = new DefaultContractResolver
467  {
468  NamingStrategy = new CamelCaseNamingStrategy
469  {
470  ProcessDictionaryKeys = false,
471  OverrideSpecifiedNames = true
472  }
473  }
474  };
475  }
476 
477  /// <summary>
478  /// Result handler update method
479  /// </summary>
480  protected abstract void Run();
481 
482  /// <summary>
483  /// Gets the full path for a results file
484  /// </summary>
485  /// <param name="filename">The filename to add to the path</param>
486  /// <returns>The full path, including the filename</returns>
487  protected string GetResultsPath(string filename)
488  {
489  return Path.Combine(ResultsDestinationFolder, filename);
490  }
491 
492  /// <summary>
493  /// Event fired each time that we add/remove securities from the data feed
494  /// </summary>
495  public virtual void OnSecuritiesChanged(SecurityChanges changes)
496  {
497  }
498 
499  /// <summary>
500  /// Returns the location of the logs
501  /// </summary>
502  /// <param name="id">Id that will be incorporated into the algorithm log name</param>
503  /// <param name="logs">The logs to save</param>
504  /// <returns>The path to the logs</returns>
505  public virtual string SaveLogs(string id, List<LogEntry> logs)
506  {
507  var filename = $"{id}-log.txt";
508  var path = GetResultsPath(filename);
509  var logLines = logs.Select(x => x.Message);
510  File.WriteAllLines(path, logLines);
511  return path;
512  }
513 
514  /// <summary>
515  /// Save the results to disk
516  /// </summary>
517  /// <param name="name">The name of the results</param>
518  /// <param name="result">The results to save</param>
519  public virtual void SaveResults(string name, Result result)
520  {
521  File.WriteAllText(GetResultsPath(name), JsonConvert.SerializeObject(result, Formatting.Indented, SerializerSettings));
522  }
523 
524  /// <summary>
525  /// Purge/clear any outstanding messages in message queue.
526  /// </summary>
527  protected void PurgeQueue()
528  {
529  Messages.Clear();
530  }
531 
532  /// <summary>
533  /// Stops the update runner task
534  /// </summary>
535  protected void StopUpdateRunner()
536  {
537  _updateRunner.StopSafely(TimeSpan.FromMinutes(10));
538  _updateRunner = null;
539  }
540 
541  /// <summary>
542  /// Gets the algorithm net return
543  /// </summary>
544  protected decimal GetNetReturn()
545  {
546  //Some users have $0 in their brokerage account / starting cash of $0. Prevent divide by zero errors
547  return StartingPortfolioValue > 0 ?
549  : 0;
550  }
551 
552  /// <summary>
553  /// Save the snapshot of the total results to storage.
554  /// </summary>
555  /// <param name="packet">Packet to store.</param>
556  protected abstract void StoreResult(Packet packet);
557 
558  /// <summary>
559  /// Gets the current portfolio value
560  /// </summary>
561  /// <remarks>Useful so that live trading implementation can freeze the returned value if there is no user exchange open
562  /// so we ignore extended market hours updates</remarks>
563  protected virtual decimal GetPortfolioValue()
564  {
565  return Algorithm.Portfolio.TotalPortfolioValue;
566  }
567 
568  /// <summary>
569  /// Gets the current benchmark value
570  /// </summary>
571  /// <remarks>Useful so that live trading implementation can freeze the returned value if there is no user exchange open
572  /// so we ignore extended market hours updates</remarks>
573  /// <param name="time">Time to resolve benchmark value at</param>
574  protected virtual decimal GetBenchmarkValue(DateTime time)
575  {
576  if (Algorithm == null || Algorithm.Benchmark == null)
577  {
578  // this could happen if the algorithm exploded mid initialization
579  return 0;
580  }
581  return Algorithm.Benchmark.Evaluate(time).SmartRounding();
582  }
583 
584  /// <summary>
585  /// Samples portfolio equity, benchmark, and daily performance
586  /// Called by scheduled event every night at midnight algorithm time
587  /// </summary>
588  /// <param name="time">Current UTC time in the AlgorithmManager loop</param>
589  public virtual void Sample(DateTime time)
590  {
591  var currentPortfolioValue = GetPortfolioValue();
592  var portfolioPerformance = DailyPortfolioValue == 0 ? 0 : Math.Round((currentPortfolioValue - DailyPortfolioValue) * 100 / DailyPortfolioValue, 10);
593 
594  // Update our max portfolio value
595  CumulativeMaxPortfolioValue = Math.Max(currentPortfolioValue, CumulativeMaxPortfolioValue);
596 
597  // Sample all our default charts
599  SampleEquity(time);
600  SampleBenchmark(time, GetBenchmarkValue(time));
601  SamplePerformance(time, portfolioPerformance);
602  SampleDrawdown(time, currentPortfolioValue);
603  SampleSalesVolume(time);
604  SampleExposure(time, currentPortfolioValue);
605  SampleCapacity(time);
606  SamplePortfolioTurnover(time, currentPortfolioValue);
607  SamplePortfolioMargin(time, currentPortfolioValue);
608 
609  // Update daily portfolio value; works because we only call sample once a day
610  DailyPortfolioValue = currentPortfolioValue;
611  }
612 
613  private void SamplePortfolioMargin(DateTime algorithmUtcTime, decimal currentPortfolioValue)
614  {
615  var state = PortfolioState.Create(Algorithm.Portfolio, algorithmUtcTime, currentPortfolioValue);
616 
617  lock (ChartLock)
618  {
619  if (!Charts.TryGetValue(PortfolioMarginKey, out var chart))
620  {
621  chart = new Chart(PortfolioMarginKey);
622  Charts.AddOrUpdate(PortfolioMarginKey, chart);
623  }
624  PortfolioMarginChart.AddSample(chart, state, MapFileProvider, DateTime.UtcNow.Date);
625  }
626  }
627 
628  /// <summary>
629  /// Sample the current equity of the strategy directly with time and using
630  /// the current algorithm equity value in <see cref="CurrentAlgorithmEquity"/>
631  /// </summary>
632  /// <param name="time">Equity candlestick end time</param>
633  protected virtual void SampleEquity(DateTime time)
634  {
636 
637  // Reset the current algorithm equity object so another bar is create on the next sample
638  CurrentAlgorithmEquity = null;
639  }
640 
641  /// <summary>
642  /// Sample the current daily performance directly with a time-value pair.
643  /// </summary>
644  /// <param name="time">Time of the sample.</param>
645  /// <param name="value">Current daily performance value.</param>
646  protected virtual void SamplePerformance(DateTime time, decimal value)
647  {
648  if (Log.DebuggingEnabled)
649  {
650  Log.Debug("BaseResultsHandler.SamplePerformance(): " + time.ToShortTimeString() + " >" + value);
651  }
652  Sample(StrategyEquityKey, ReturnKey, 1, SeriesType.Bar, new ChartPoint(time, value), "%");
653  }
654 
655  /// <summary>
656  /// Sample the current benchmark performance directly with a time-value pair.
657  /// </summary>
658  /// <param name="time">Time of the sample.</param>
659  /// <param name="value">Current benchmark value.</param>
660  /// <seealso cref="IResultHandler.Sample"/>
661  protected virtual void SampleBenchmark(DateTime time, decimal value)
662  {
663  Sample(BenchmarkKey, BenchmarkKey, 0, SeriesType.Line, new ChartPoint(time, value));
664  }
665 
666  /// <summary>
667  /// Sample drawdown of equity of the strategy
668  /// </summary>
669  /// <param name="time">Time of the sample</param>
670  /// <param name="currentPortfolioValue">Current equity value</param>
671  protected virtual void SampleDrawdown(DateTime time, decimal currentPortfolioValue)
672  {
673  // This will throw otherwise, in this case just don't sample
675  {
676  // Calculate our drawdown and sample it
677  var drawdown = Statistics.Statistics.DrawdownPercent(currentPortfolioValue, CumulativeMaxPortfolioValue);
678  Sample(DrawdownKey, "Equity Drawdown", 0, SeriesType.Line, new ChartPoint(time, drawdown), "%");
679  }
680  }
681 
682  /// <summary>
683  /// Sample portfolio turn over of the strategy
684  /// </summary>
685  /// <param name="time">Time of the sample</param>
686  /// <param name="currentPortfolioValue">Current equity value</param>
687  protected virtual void SamplePortfolioTurnover(DateTime time, decimal currentPortfolioValue)
688  {
689  if (currentPortfolioValue != 0)
690  {
691  if (Algorithm.StartDate == time.ConvertFromUtc(Algorithm.TimeZone))
692  {
693  // the first sample in backtesting is at start, we only want to sample after a full algorithm execution date
694  return;
695  }
696  var currentTotalSaleVolume = Algorithm.Portfolio.TotalSaleVolume;
697 
698  decimal todayPortfolioTurnOver;
699  if (_previousPortfolioTurnoverSample == time)
700  {
701  // we are sampling the same time twice, this can happen if we sample at the start of the portfolio loop
702  // and the algorithm happen to end at the same time and we trigger the final sample to take into account that last loop
703  // this new sample will overwrite the previous, so we resample using T-2 sales volume
704  todayPortfolioTurnOver = (currentTotalSaleVolume - _previousSalesVolume[1]) / currentPortfolioValue;
705  }
706  else
707  {
708  todayPortfolioTurnOver = (currentTotalSaleVolume - _previousSalesVolume[0]) / currentPortfolioValue;
709  }
710 
711  _previousSalesVolume.Add(currentTotalSaleVolume);
712  _previousPortfolioTurnoverSample = time;
713 
714  Sample(PortfolioTurnoverKey, PortfolioTurnoverKey, 0, SeriesType.Line, new ChartPoint(time, todayPortfolioTurnOver), "%");
715  }
716  }
717 
718  /// <summary>
719  /// Sample assets sales volume
720  /// </summary>
721  /// <param name="time">Time of the sample</param>
722  protected virtual void SampleSalesVolume(DateTime time)
723  {
724  // Sample top 30 holdings by sales volume
725  foreach (var holding in Algorithm.Portfolio.Values.Where(y => y.TotalSaleVolume != 0)
726  .OrderByDescending(x => x.TotalSaleVolume).Take(30))
727  {
728  Sample(AssetsSalesVolumeKey, $"{holding.Symbol.Value}", 0, SeriesType.Treemap, new ChartPoint(time, holding.TotalSaleVolume),
730  }
731  }
732 
733  /// <summary>
734  /// Sample portfolio exposure long/short ratios by security type
735  /// </summary>
736  /// <param name="time">Time of the sample</param>
737  /// <param name="currentPortfolioValue">Current value of the portfolio</param>
738  protected virtual void SampleExposure(DateTime time, decimal currentPortfolioValue)
739  {
740  // Will throw in this case, just return without sampling
741  if (currentPortfolioValue == 0)
742  {
743  return;
744  }
745 
746  // Split up our holdings in one enumeration into long and shorts holding values
747  // only process those that we hold stock in.
748  var shortHoldings = new Dictionary<SecurityType, decimal>();
749  var longHoldings = new Dictionary<SecurityType, decimal>();
750  foreach (var holding in Algorithm.Portfolio.Values)
751  {
752  // Ensure we have a value for this security type in both our dictionaries
753  if (!longHoldings.ContainsKey(holding.Symbol.SecurityType))
754  {
755  longHoldings.Add(holding.Symbol.SecurityType, 0);
756  shortHoldings.Add(holding.Symbol.SecurityType, 0);
757  }
758 
759  var holdingsValue = holding.HoldingsValue;
760  if (holdingsValue == 0)
761  {
762  continue;
763  }
764 
765  // Long Position
766  if (holdingsValue > 0)
767  {
768  longHoldings[holding.Symbol.SecurityType] += holdingsValue;
769  }
770  // Short Position
771  else
772  {
773  shortHoldings[holding.Symbol.SecurityType] += holdingsValue;
774  }
775  }
776 
777  // Sample our long and short positions
778  SampleExposureHelper(PositionSide.Long, time, currentPortfolioValue, longHoldings);
779  SampleExposureHelper(PositionSide.Short, time, currentPortfolioValue, shortHoldings);
780  }
781 
782  /// <summary>
783  /// Helper method for SampleExposure, samples our holdings value to
784  /// our exposure chart by their position side and security type
785  /// </summary>
786  /// <param name="type">Side to sample from portfolio</param>
787  /// <param name="time">Time of the sample</param>
788  /// <param name="currentPortfolioValue">Current value of the portfolio</param>
789  /// <param name="holdings">Enumerable of holdings to sample</param>
790  private void SampleExposureHelper(PositionSide type, DateTime time, decimal currentPortfolioValue, Dictionary<SecurityType, decimal> holdings)
791  {
792  foreach (var kvp in holdings)
793  {
794  var ratio = Math.Round(kvp.Value / currentPortfolioValue, 4);
795  Sample("Exposure", $"{kvp.Key} - {type} Ratio", 0, SeriesType.Line, new ChartPoint(time, ratio),
796  "");
797  }
798  }
799 
800  /// <summary>
801  /// Sample estimated strategy capacity
802  /// </summary>
803  /// <param name="time">Time of the sample</param>
804  protected virtual void SampleCapacity(DateTime time)
805  {
806  // NOP; Used only by BacktestingResultHandler because he owns a CapacityEstimate
807  }
808 
809  /// <summary>
810  /// Add a sample to the chart specified by the chartName, and seriesName.
811  /// </summary>
812  /// <param name="chartName">String chart name to place the sample.</param>
813  /// <param name="seriesName">Series name for the chart.</param>
814  /// <param name="seriesIndex">Series chart index - which chart should this series belong</param>
815  /// <param name="seriesType">Series type for the chart.</param>
816  /// <param name="value">Value for the chart sample.</param>
817  /// <param name="unit">Unit for the chart axis</param>
818  /// <remarks>Sample can be used to create new charts or sample equity - daily performance.</remarks>
819  protected abstract void Sample(string chartName,
820  string seriesName,
821  int seriesIndex,
822  SeriesType seriesType,
823  ISeriesPoint value,
824  string unit = "$");
825 
826  /// <summary>
827  /// Gets the algorithm runtime statistics
828  /// </summary>
829  protected SortedDictionary<string, string> GetAlgorithmRuntimeStatistics(Dictionary<string, string> summary, CapacityEstimate capacityEstimate = null)
830  {
831  var runtimeStatistics = new SortedDictionary<string, string>();
832  lock (RuntimeStatistics)
833  {
834  foreach (var pair in RuntimeStatistics)
835  {
836  runtimeStatistics.Add(pair.Key, pair.Value);
837  }
838  }
839 
840  if (summary.ContainsKey("Probabilistic Sharpe Ratio"))
841  {
842  runtimeStatistics["Probabilistic Sharpe Ratio"] = summary["Probabilistic Sharpe Ratio"];
843  }
844  else
845  {
846  runtimeStatistics["Probabilistic Sharpe Ratio"] = "0%";
847  }
848 
849  runtimeStatistics["Unrealized"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalUnrealizedProfit.ToStringInvariant("N2");
850  runtimeStatistics["Fees"] = $"-{AlgorithmCurrencySymbol}{Algorithm.Portfolio.TotalFees.ToStringInvariant("N2")}";
851  runtimeStatistics["Net Profit"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalNetProfit.ToStringInvariant("N2");
852  runtimeStatistics["Return"] = GetNetReturn().ToStringInvariant("P");
853  runtimeStatistics["Equity"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalPortfolioValue.ToStringInvariant("N2");
854  runtimeStatistics["Holdings"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalHoldingsValue.ToStringInvariant("N2");
855  runtimeStatistics["Volume"] = AlgorithmCurrencySymbol + Algorithm.Portfolio.TotalSaleVolume.ToStringInvariant("N2");
856 
857  return runtimeStatistics;
858  }
859 
860  /// <summary>
861  /// Sets the algorithm state data
862  /// </summary>
863  protected void SetAlgorithmState(string error, string stack)
864  {
865  State["RuntimeError"] = error;
866  State["StackTrace"] = stack;
867  }
868 
869  /// <summary>
870  /// Gets the algorithm state data
871  /// </summary>
872  protected Dictionary<string, string> GetAlgorithmState(DateTime? endTime = null)
873  {
874  if (Algorithm == null || !string.IsNullOrEmpty(State["RuntimeError"]))
875  {
876  State["Status"] = AlgorithmStatus.RuntimeError.ToStringInvariant();
877  }
878  else
879  {
880  State["Status"] = Algorithm.Status.ToStringInvariant();
881  }
882  State["EndTime"] = endTime != null ? endTime.ToStringInvariant(DateFormat.UI) : string.Empty;
883 
884  lock (LogStore)
885  {
886  State["LogCount"] = _logCount.ToStringInvariant();
887  }
888  State["OrderCount"] = Algorithm?.Transactions?.OrdersCount.ToStringInvariant() ?? "0";
889  State["InsightCount"] = Algorithm?.Insights.TotalCount.ToStringInvariant() ?? "0";
890 
891  return State;
892  }
893 
894  /// <summary>
895  /// Will generate the statistics results and update the provided runtime statistics
896  /// </summary>
897  protected StatisticsResults GenerateStatisticsResults(Dictionary<string, Chart> charts,
898  SortedDictionary<DateTime, decimal> profitLoss = null, CapacityEstimate estimatedStrategyCapacity = null)
899  {
900  var statisticsResults = new StatisticsResults();
901  if (profitLoss == null)
902  {
903  profitLoss = new SortedDictionary<DateTime, decimal>();
904  }
905 
906  try
907  {
908  //Generates error when things don't exist (no charting logged, runtime errors in main algo execution)
909 
910  // make sure we've taken samples for these series before just blindly requesting them
911  if (charts.TryGetValue(StrategyEquityKey, out var strategyEquity) &&
912  strategyEquity.Series.TryGetValue(EquityKey, out var equity) &&
913  strategyEquity.Series.TryGetValue(ReturnKey, out var performance) &&
914  charts.TryGetValue(BenchmarkKey, out var benchmarkChart) &&
915  benchmarkChart.Series.TryGetValue(BenchmarkKey, out var benchmark))
916  {
917  var trades = Algorithm.TradeBuilder.ClosedTrades;
918 
919  BaseSeries portfolioTurnover;
920  if (charts.TryGetValue(PortfolioTurnoverKey, out var portfolioTurnoverChart))
921  {
922  portfolioTurnoverChart.Series.TryGetValue(PortfolioTurnoverKey, out portfolioTurnover);
923  }
924  else
925  {
926  portfolioTurnover = new Series();
927  }
928 
929  statisticsResults = StatisticsBuilder.Generate(trades, profitLoss, equity.Values, performance.Values, benchmark.Values,
930  portfolioTurnover.Values, StartingPortfolioValue, Algorithm.Portfolio.TotalFees, TotalTradesCount(),
932  Algorithm.Settings.TradingDaysPerYear.Value // already set in Brokerage|Backtesting-SetupHandler classes
933  );
934  }
935 
936  statisticsResults.AddCustomSummaryStatistics(_customSummaryStatistics);
937  }
938  catch (Exception err)
939  {
940  Log.Error(err, "BaseResultsHandler.GenerateStatisticsResults(): Error generating statistics packet");
941  }
942 
943  return statisticsResults;
944  }
945 
946  /// <summary>
947  /// Helper method to get the total trade count statistic
948  /// </summary>
949  protected int TotalTradesCount()
950  {
951  return TransactionHandler?.OrdersCount ?? 0;
952  }
953 
954  /// <summary>
955  /// Calculates and gets the current statistics for the algorithm.
956  /// It will use the current <see cref="Charts"/> and profit loss information calculated from the current transaction record
957  /// to generate the results.
958  /// </summary>
959  /// <returns>The current statistics</returns>
960  protected StatisticsResults GenerateStatisticsResults(CapacityEstimate estimatedStrategyCapacity = null)
961  {
962  // could happen if algorithm failed to init
963  if (Algorithm == null)
964  {
965  return new StatisticsResults();
966  }
967 
968  Dictionary<string, Chart> charts;
969  lock (ChartLock)
970  {
971  charts = new(Charts);
972  }
973  var profitLoss = new SortedDictionary<DateTime, decimal>(Algorithm.Transactions.TransactionRecord);
974 
975  return GenerateStatisticsResults(charts, profitLoss, estimatedStrategyCapacity);
976  }
977 
978  /// <summary>
979  /// Save an algorithm message to the log store. Uses a different timestamped method of adding messaging to interweve debug and logging messages.
980  /// </summary>
981  /// <param name="message">String message to store</param>
982  protected virtual void AddToLogStore(string message)
983  {
984  lock (LogStore)
985  {
986  LogStore.Add(new LogEntry(message));
987  _logCount++;
988  }
989  }
990 
991  /// <summary>
992  /// Processes algorithm logs.
993  /// Logs of the same type are batched together one per line and are sent out
994  /// </summary>
995  protected void ProcessAlgorithmLogs(int? messageQueueLimit = null)
996  {
997  ProcessAlgorithmLogsImpl(Algorithm.DebugMessages, PacketType.Debug, messageQueueLimit);
998  ProcessAlgorithmLogsImpl(Algorithm.ErrorMessages, PacketType.HandledError, messageQueueLimit);
999  ProcessAlgorithmLogsImpl(Algorithm.LogMessages, PacketType.Log, messageQueueLimit);
1000  }
1001 
1002  private void ProcessAlgorithmLogsImpl(ConcurrentQueue<string> concurrentQueue, PacketType packetType, int? messageQueueLimit = null)
1003  {
1004  if (concurrentQueue.IsEmpty)
1005  {
1006  return;
1007  }
1008 
1009  var endTime = DateTime.UtcNow.AddMilliseconds(250).Ticks;
1010  var currentMessageCount = -1;
1011  while (DateTime.UtcNow.Ticks < endTime && concurrentQueue.TryDequeue(out var message))
1012  {
1013  if (messageQueueLimit.HasValue)
1014  {
1015  if (currentMessageCount == -1)
1016  {
1017  // this is expensive, so let's get it once
1018  currentMessageCount = Messages.Count;
1019  }
1020  if (currentMessageCount > messageQueueLimit)
1021  {
1022  if (!_packetDroppedWarning)
1023  {
1024  _packetDroppedWarning = true;
1025  // 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
1026  Messages.Enqueue(new HandledErrorPacket(AlgorithmId, "Your algorithm messaging has been rate limited to prevent browser flooding."));
1027  }
1028  //if too many in the queue already skip the logging and drop the messages
1029  continue;
1030  }
1031  }
1032 
1033  if (packetType == PacketType.Debug)
1034  {
1035  Messages.Enqueue(new DebugPacket(ProjectId, AlgorithmId, CompileId, message));
1036  }
1037  else if (packetType == PacketType.Log)
1038  {
1039  Messages.Enqueue(new LogPacket(AlgorithmId, message));
1040  }
1041  else if (packetType == PacketType.HandledError)
1042  {
1043  Messages.Enqueue(new HandledErrorPacket(AlgorithmId, message));
1044  }
1045  AddToLogStore(message);
1046 
1047  // increase count after we add
1048  currentMessageCount++;
1049  }
1050  }
1051 
1052  /// <summary>
1053  /// Sets or updates a custom summary statistic
1054  /// </summary>
1055  /// <param name="name">The statistic name</param>
1056  /// <param name="value">The statistic value</param>
1057  protected void SummaryStatistic(string name, string value)
1058  {
1059  _customSummaryStatistics.AddOrUpdate(name, value);
1060  }
1061 
1062  /// <summary>
1063  /// Updates the current equity bar with the current equity value from <see cref="GetPortfolioValue"/>
1064  /// </summary>
1065  /// <remarks>
1066  /// This is required in order to update the <see cref="CurrentAlgorithmEquity"/> bar without using the getter,
1067  /// which would cause the bar to be created if it doesn't exist.
1068  /// </remarks>
1069  private void UpdateAlgorithmEquity(Bar equity)
1070  {
1071  equity.Update(Math.Round(GetPortfolioValue(), 4));
1072  }
1073 
1074  /// <summary>
1075  /// Updates the current equity bar with the current equity value from <see cref="GetPortfolioValue"/>
1076  /// </summary>
1077  protected void UpdateAlgorithmEquity()
1078  {
1080  }
1081  }
1082 }