Lean  $LEAN_TAG$
BacktestingResultHandler.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.Generic;
19 using System.IO;
20 using System.Linq;
24 using QuantConnect.Logging;
25 using QuantConnect.Orders;
26 using QuantConnect.Packets;
29 using QuantConnect.Util;
30 
32 {
33  /// <summary>
34  /// Backtesting result handler passes messages back from the Lean to the User.
35  /// </summary>
37  {
38  private const double Samples = 4000;
39  private const double MinimumSamplePeriod = 4;
40 
41  private BacktestNodePacket _job;
42  private DateTime _nextUpdate;
43  private DateTime _nextS3Update;
44  private string _errorMessage;
45  private int _daysProcessedFrontier;
46  private readonly HashSet<string> _chartSeriesExceededDataPoints;
47  private readonly HashSet<string> _chartSeriesCount;
48  private bool _chartSeriesCountExceededError;
49 
50  private BacktestProgressMonitor _progressMonitor;
51 
52  /// <summary>
53  /// Calculates the capacity of a strategy per Symbol in real-time
54  /// </summary>
55  private CapacityEstimate _capacityEstimate;
56 
57  //Processing Time:
58  private DateTime _nextSample;
59  private string _algorithmId;
60  private int _projectId;
61 
62  /// <summary>
63  /// A dictionary containing summary statistics
64  /// </summary>
65  public Dictionary<string, string> FinalStatistics { get; private set; }
66 
67  /// <summary>
68  /// Creates a new instance
69  /// </summary>
71  {
72  ResamplePeriod = TimeSpan.FromMinutes(4);
73  NotificationPeriod = TimeSpan.FromSeconds(2);
74 
75  _chartSeriesExceededDataPoints = new();
76  _chartSeriesCount = new();
77 
78  // Delay uploading first packet
79  _nextS3Update = StartTime.AddSeconds(5);
80  }
81 
82  /// <summary>
83  /// Initialize the result handler with this result packet.
84  /// </summary>
85  public override void Initialize(ResultHandlerInitializeParameters parameters)
86  {
87  _job = (BacktestNodePacket)parameters.Job;
88  State["Name"] = _job.Name;
89  _algorithmId = _job.AlgorithmId;
90  _projectId = _job.ProjectId;
91  if (_job == null) throw new Exception("BacktestingResultHandler.Constructor(): Submitted Job type invalid.");
92  base.Initialize(parameters);
93  if (!string.IsNullOrEmpty(_job.OptimizationId))
94  {
95  State["OptimizationId"] = _job.OptimizationId;
96  }
97  }
98 
99  /// <summary>
100  /// The main processing method steps through the messaging queue and processes the messages one by one.
101  /// </summary>
102  protected override void Run()
103  {
104  try
105  {
106  while (!(ExitTriggered && Messages.IsEmpty))
107  {
108  //While there's no work to do, go back to the algorithm:
109  if (Messages.IsEmpty)
110  {
111  ExitEvent.WaitOne(50);
112  }
113  else
114  {
115  //1. Process Simple Messages in Queue
116  Packet packet;
117  if (Messages.TryDequeue(out packet))
118  {
119  MessagingHandler.Send(packet);
120  }
121  }
122 
123  //2. Update the packet scanner:
124  Update();
125 
126  } // While !End.
127  }
128  catch (Exception err)
129  {
130  // unexpected error, we need to close down shop
131  Algorithm.SetRuntimeError(err, "ResultHandler");
132  }
133 
134  Log.Trace("BacktestingResultHandler.Run(): Ending Thread...");
135  } // End Run();
136 
137  /// <summary>
138  /// Send a backtest update to the browser taking a latest snapshot of the charting data.
139  /// </summary>
140  private void Update()
141  {
142  try
143  {
144  //Sometimes don't run the update, if not ready or we're ending.
145  if (Algorithm?.Transactions == null || ExitTriggered || !Algorithm.GetLocked())
146  {
147  return;
148  }
149 
150  var utcNow = DateTime.UtcNow;
151  if (utcNow <= _nextUpdate || _progressMonitor.ProcessedDays < _daysProcessedFrontier) return;
152 
153  var deltaOrders = GetDeltaOrders(LastDeltaOrderPosition, shouldStop: orderCount => orderCount >= 50);
154  // Deliberately skip to the end of order event collection to prevent overloading backtesting UX
155  LastDeltaOrderPosition = TransactionHandler.OrderEvents.Count();
156 
157  //Reset loop variables:
158  try
159  {
160  _daysProcessedFrontier = _progressMonitor.ProcessedDays + 1;
161  _nextUpdate = utcNow.AddSeconds(3);
162  }
163  catch (Exception err)
164  {
165  Log.Error(err, "Can't update variables");
166  }
167 
168  var deltaCharts = new Dictionary<string, Chart>();
169  var serverStatistics = GetServerStatistics(utcNow);
170  var performanceCharts = new Dictionary<string, Chart>();
171 
172  // Process our charts updates
173  lock (ChartLock)
174  {
175  foreach (var kvp in Charts)
176  {
177  var chart = kvp.Value;
178 
179  // Get a copy of this chart with updates only since last request
180  var updates = chart.GetUpdates();
181  if (!updates.IsEmpty())
182  {
183  deltaCharts.Add(chart.Name, updates);
184  }
185 
186  // Update our algorithm performance charts
187  if (AlgorithmPerformanceCharts.Contains(kvp.Key))
188  {
189  performanceCharts[kvp.Key] = chart.Clone();
190  }
191 
192  if (updates.Name == PortfolioMarginKey)
193  {
195  }
196  }
197  }
198 
199  //Get the runtime statistics from the user algorithm:
200  var summary = GenerateStatisticsResults(performanceCharts, estimatedStrategyCapacity: _capacityEstimate).Summary;
201  var runtimeStatistics = GetAlgorithmRuntimeStatistics(summary, _capacityEstimate);
202 
203  var progress = _progressMonitor.Progress;
204 
205  //1. Cloud Upload -> Upload the whole packet to S3 Immediately:
206  if (utcNow > _nextS3Update)
207  {
208  // For intermediate backtesting results, we truncate the order list to include only the last 100 orders
209  // The final packet will contain the full list of orders.
210  const int maxOrders = 100;
211  var orderCount = TransactionHandler.Orders.Count;
212 
213  var completeResult = new BacktestResult(new BacktestResultParameters(
214  Charts,
215  orderCount > maxOrders ? TransactionHandler.Orders.Skip(orderCount - maxOrders).ToDictionary() : TransactionHandler.Orders.ToDictionary(),
216  Algorithm.Transactions.TransactionRecord,
217  new Dictionary<string, string>(),
218  runtimeStatistics,
219  new Dictionary<string, AlgorithmPerformance>(),
220  // we store the last 100 order events, the final packet will contain the full list
221  TransactionHandler.OrderEvents.Reverse().Take(100).ToList(), state: GetAlgorithmState()));
222 
223  StoreResult(new BacktestResultPacket(_job, completeResult, Algorithm.EndDate, Algorithm.StartDate, progress));
224 
225  _nextS3Update = DateTime.UtcNow.AddSeconds(30);
226  }
227 
228  //2. Backtest Update -> Send the truncated packet to the backtester:
229  var splitPackets = SplitPackets(deltaCharts, deltaOrders, runtimeStatistics, progress, serverStatistics);
230 
231  foreach (var backtestingPacket in splitPackets)
232  {
233  MessagingHandler.Send(backtestingPacket);
234  }
235 
236  // let's re update this value after we finish just in case, so we don't re enter in the next loop
237  _nextUpdate = DateTime.UtcNow.Add(MainUpdateInterval);
238  }
239  catch (Exception err)
240  {
241  Log.Error(err);
242  }
243  }
244 
245  /// <summary>
246  /// Run over all the data and break it into smaller packets to ensure they all arrive at the terminal
247  /// </summary>
248  public virtual IEnumerable<BacktestResultPacket> SplitPackets(Dictionary<string, Chart> deltaCharts, Dictionary<int, Order> deltaOrders, SortedDictionary<string, string> runtimeStatistics, decimal progress, Dictionary<string, string> serverStatistics)
249  {
250  // break the charts into groups
251  var splitPackets = new List<BacktestResultPacket>();
252  foreach (var chart in deltaCharts.Values)
253  {
254  splitPackets.Add(new BacktestResultPacket(_job, new BacktestResult
255  {
256  Charts = new Dictionary<string, Chart>
257  {
258  {chart.Name, chart}
259  }
260  }, Algorithm.EndDate, Algorithm.StartDate, progress));
261  }
262 
263  // only send orders if there is actually any update
264  if (deltaOrders.Count > 0)
265  {
266  // Add the orders into the charting packet:
267  splitPackets.Add(new BacktestResultPacket(_job, new BacktestResult { Orders = deltaOrders }, Algorithm.EndDate, Algorithm.StartDate, progress));
268  }
269 
270  //Add any user runtime statistics into the backtest.
271  splitPackets.Add(new BacktestResultPacket(_job, new BacktestResult { ServerStatistics = serverStatistics, RuntimeStatistics = runtimeStatistics }, Algorithm.EndDate, Algorithm.StartDate, progress));
272 
273 
274  return splitPackets;
275  }
276 
277  /// <summary>
278  /// Save the snapshot of the total results to storage.
279  /// </summary>
280  /// <param name="packet">Packet to store.</param>
281  protected override void StoreResult(Packet packet)
282  {
283  try
284  {
285  // Make sure this is the right type of packet:
286  if (packet.Type != PacketType.BacktestResult) return;
287 
288  // Port to packet format:
289  var result = packet as BacktestResultPacket;
290 
291  if (result != null)
292  {
293  // Get Storage Location:
294  var key = $"{AlgorithmId}.json";
295 
296  BacktestResult results;
297  lock (ChartLock)
298  {
299  results = new BacktestResult(new BacktestResultParameters(
300  result.Results.Charts.ToDictionary(x => x.Key, x => x.Value.Clone()),
301  result.Results.Orders,
302  result.Results.ProfitLoss,
303  result.Results.Statistics,
304  result.Results.RuntimeStatistics,
305  result.Results.RollingWindow,
306  null, // null order events, we store them separately
307  result.Results.TotalPerformance,
308  result.Results.AlgorithmConfiguration,
309  result.Results.State));
310 
311  if (result.Results.Charts.TryGetValue(PortfolioMarginKey, out var marginChart))
312  {
314  }
315  }
316  // Save results
317  SaveResults(key, results);
318 
319  // Store Order Events in a separate file
320  StoreOrderEvents(Algorithm?.UtcTime ?? DateTime.UtcNow, result.Results.OrderEvents);
321  }
322  else
323  {
324  Log.Error("BacktestingResultHandler.StoreResult(): Result Null.");
325  }
326  }
327  catch (Exception err)
328  {
329  Log.Error(err);
330  }
331  }
332 
333  /// <summary>
334  /// Send a final analysis result back to the IDE.
335  /// </summary>
336  protected void SendFinalResult()
337  {
338  try
339  {
340  var endTime = DateTime.UtcNow;
341  BacktestResultPacket result;
342  // could happen if algorithm failed to init
343  if (Algorithm != null)
344  {
345  //Convert local dictionary:
346  var charts = new Dictionary<string, Chart>(Charts);
347  var orders = new Dictionary<int, Order>(TransactionHandler.Orders);
348  var profitLoss = new SortedDictionary<DateTime, decimal>(Algorithm.Transactions.TransactionRecord);
349  var statisticsResults = GenerateStatisticsResults(charts, profitLoss, _capacityEstimate);
350  var runtime = GetAlgorithmRuntimeStatistics(statisticsResults.Summary, capacityEstimate: _capacityEstimate);
351 
352  FinalStatistics = statisticsResults.Summary;
353 
354  // clear the trades collection before placing inside the backtest result
355  foreach (var ap in statisticsResults.RollingPerformances.Values)
356  {
357  ap.ClosedTrades.Clear();
358  }
359  var orderEvents = TransactionHandler.OrderEvents.ToList();
360  //Create a result packet to send to the browser.
361  result = new BacktestResultPacket(_job,
362  new BacktestResult(new BacktestResultParameters(charts, orders, profitLoss, statisticsResults.Summary, runtime,
363  statisticsResults.RollingPerformances, orderEvents, statisticsResults.TotalPerformance,
366  }
367  else
368  {
369  result = BacktestResultPacket.CreateEmpty(_job);
370  result.Results.State = GetAlgorithmState(endTime);
371  }
372 
373  result.ProcessingTime = (endTime - StartTime).TotalSeconds;
374  result.DateFinished = DateTime.Now;
375  result.Progress = 1;
376 
377  StoreInsights();
378 
379  // Save summary results
380  SaveResults($"{AlgorithmId}-summary.json", CreateResultSummary(result));
381 
382  //Place result into storage.
383  StoreResult(result);
384 
385  result.Results.ServerStatistics = GetServerStatistics(endTime);
386  //Second, send the truncated packet:
387  MessagingHandler.Send(result);
388 
389  Log.Trace("BacktestingResultHandler.SendAnalysisResult(): Processed final packet");
390  }
391  catch (Exception err)
392  {
393  Log.Error(err);
394  }
395  }
396 
397  /// <summary>
398  /// Set the Algorithm instance for ths result.
399  /// </summary>
400  /// <param name="algorithm">Algorithm we're working on.</param>
401  /// <param name="startingPortfolioValue">Algorithm starting capital for statistics calculations</param>
402  /// <remarks>While setting the algorithm the backtest result handler.</remarks>
403  public virtual void SetAlgorithm(IAlgorithm algorithm, decimal startingPortfolioValue)
404  {
405  Algorithm = algorithm;
407  State["Name"] = Algorithm.Name;
408  StartingPortfolioValue = startingPortfolioValue;
412  _capacityEstimate = new CapacityEstimate(Algorithm);
414 
415  //Get the resample period:
416  var totalMinutes = (algorithm.EndDate - algorithm.StartDate).TotalMinutes;
417  var resampleMinutes = totalMinutes < MinimumSamplePeriod * Samples ? MinimumSamplePeriod : totalMinutes / Samples; // Space out the sampling every
418  ResamplePeriod = TimeSpan.FromMinutes(resampleMinutes);
419  Log.Trace("BacktestingResultHandler(): Sample Period Set: " + resampleMinutes.ToStringInvariant("00.00"));
420 
421  //Set the security / market types.
422  var types = new List<SecurityType>();
423  foreach (var kvp in Algorithm.Securities)
424  {
425  var security = kvp.Value;
426 
427  if (!types.Contains(security.Type)) types.Add(security.Type);
428  }
429  SecurityType(types);
430 
431  ConfigureConsoleTextWriter(algorithm);
432 
433  // Wire algorithm name and tags updates
434  algorithm.NameUpdated += (sender, name) => AlgorithmNameUpdated(name);
435  algorithm.TagsUpdated += (sender, tags) => AlgorithmTagsUpdated(tags);
436  }
437 
438  /// <summary>
439  /// Handles updates to the algorithm's name
440  /// </summary>
441  /// <param name="name">The new name</param>
442  public virtual void AlgorithmNameUpdated(string name)
443  {
444  Messages.Enqueue(new AlgorithmNameUpdatePacket(AlgorithmId, name));
445  }
446 
447  /// <summary>
448  /// Sends a packet communicating an update to the algorithm's tags
449  /// </summary>
450  /// <param name="tags">The new tags</param>
451  public virtual void AlgorithmTagsUpdated(HashSet<string> tags)
452  {
453  Messages.Enqueue(new AlgorithmTagsUpdatePacket(AlgorithmId, tags));
454  }
455 
456  /// <summary>
457  /// Send a debug message back to the browser console.
458  /// </summary>
459  /// <param name="message">Message we'd like shown in console.</param>
460  public virtual void DebugMessage(string message)
461  {
462  Messages.Enqueue(new DebugPacket(_projectId, AlgorithmId, CompileId, message));
463  AddToLogStore(message);
464  }
465 
466  /// <summary>
467  /// Send a system debug message back to the browser console.
468  /// </summary>
469  /// <param name="message">Message we'd like shown in console.</param>
470  public virtual void SystemDebugMessage(string message)
471  {
472  Messages.Enqueue(new SystemDebugPacket(_projectId, AlgorithmId, CompileId, message));
473  AddToLogStore(message);
474  }
475 
476  /// <summary>
477  /// Send a logging message to the log list for storage.
478  /// </summary>
479  /// <param name="message">Message we'd in the log.</param>
480  public virtual void LogMessage(string message)
481  {
482  Messages.Enqueue(new LogPacket(AlgorithmId, message));
483  AddToLogStore(message);
484  }
485 
486  /// <summary>
487  /// Add message to LogStore
488  /// </summary>
489  /// <param name="message">Message to add</param>
490  protected override void AddToLogStore(string message)
491  {
492  var messageToLog = Algorithm != null
493  ? Algorithm.Time.ToStringInvariant(DateFormat.UI) + " " + message
494  : "Algorithm Initialization: " + message;
495 
496  base.AddToLogStore(messageToLog);
497  }
498 
499  /// <summary>
500  /// Send list of security asset types the algorithm uses to browser.
501  /// </summary>
502  public virtual void SecurityType(List<SecurityType> types)
503  {
504  var packet = new SecurityTypesPacket
505  {
506  Types = types
507  };
508  Messages.Enqueue(packet);
509  }
510 
511  /// <summary>
512  /// Send an error message back to the browser highlighted in red with a stacktrace.
513  /// </summary>
514  /// <param name="message">Error message we'd like shown in console.</param>
515  /// <param name="stacktrace">Stacktrace information string</param>
516  public virtual void ErrorMessage(string message, string stacktrace = "")
517  {
518  if (message == _errorMessage) return;
519  if (Messages.Count > 500) return;
520  Messages.Enqueue(new HandledErrorPacket(AlgorithmId, message, stacktrace));
521  _errorMessage = message;
522  }
523 
524  /// <summary>
525  /// Send a runtime error message back to the browser highlighted with in red
526  /// </summary>
527  /// <param name="message">Error message.</param>
528  /// <param name="stacktrace">Stacktrace information string</param>
529  public virtual void RuntimeError(string message, string stacktrace = "")
530  {
531  PurgeQueue();
532  Messages.Enqueue(new RuntimeErrorPacket(_job.UserId, AlgorithmId, message, stacktrace));
533  _errorMessage = message;
534  SetAlgorithmState(message, stacktrace);
535  }
536 
537  /// <summary>
538  /// Process brokerage message events
539  /// </summary>
540  /// <param name="brokerageMessageEvent">The brokerage message event</param>
541  public virtual void BrokerageMessage(BrokerageMessageEvent brokerageMessageEvent)
542  {
543  // NOP
544  }
545 
546  /// <summary>
547  /// Add a sample to the chart specified by the chartName, and seriesName.
548  /// </summary>
549  /// <param name="chartName">String chart name to place the sample.</param>
550  /// <param name="seriesIndex">Type of chart we should create if it doesn't already exist.</param>
551  /// <param name="seriesName">Series name for the chart.</param>
552  /// <param name="seriesType">Series type for the chart.</param>
553  /// <param name="value">Value for the chart sample.</param>
554  /// <param name="unit">Unit of the sample</param>
555  protected override void Sample(string chartName, string seriesName, int seriesIndex, SeriesType seriesType, ISeriesPoint value,
556  string unit = "$")
557  {
558  // Sampling during warming up period skews statistics
560  {
561  return;
562  }
563 
564  lock (ChartLock)
565  {
566  //Add a copy locally:
567  Chart chart;
568  if (!Charts.TryGetValue(chartName, out chart))
569  {
570  chart = new Chart(chartName);
571  Charts.AddOrUpdate(chartName, chart);
572  }
573 
574  //Add the sample to our chart:
575  BaseSeries series;
576  if (!chart.Series.TryGetValue(seriesName, out series))
577  {
578  series = BaseSeries.Create(seriesType, seriesName, seriesIndex, unit);
579  chart.Series.Add(seriesName, series);
580  }
581 
582  //Add our value:
583  if (series.Values.Count == 0 || value.Time > series.Values[series.Values.Count - 1].Time
584  // always sample portfolio turnover and use latest value
585  || chartName == PortfolioTurnoverKey)
586  {
587  series.AddPoint(value);
588  }
589  }
590  }
591 
592  /// <summary>
593  /// Sample estimated strategy capacity
594  /// </summary>
595  /// <param name="time">Time of the sample</param>
596  protected override void SampleCapacity(DateTime time)
597  {
598  // Sample strategy capacity, round to 1k
599  var roundedCapacity = _capacityEstimate.Capacity;
600  Sample("Capacity", "Strategy Capacity", 0, SeriesType.Line, new ChartPoint(time, roundedCapacity), AlgorithmCurrencySymbol);
601  }
602 
603  /// <summary>
604  /// Add a range of samples from the users algorithms to the end of our current list.
605  /// </summary>
606  /// <param name="updates">Chart updates since the last request.</param>
607  protected void SampleRange(IEnumerable<Chart> updates)
608  {
609  lock (ChartLock)
610  {
611  foreach (var update in updates)
612  {
613  //Create the chart if it doesn't exist already:
614  Chart chart;
615  if (!Charts.TryGetValue(update.Name, out chart))
616  {
617  chart = new Chart(update.Name);
618  Charts.AddOrUpdate(update.Name, chart);
619  }
620 
621  //Add these samples to this chart.
622  foreach (var series in update.Series.Values)
623  {
624  // let's assert we are within series count limit
625  if (_chartSeriesCount.Count < _job.Controls.MaximumChartSeries)
626  {
627  _chartSeriesCount.Add(series.Name);
628  }
629  else if (!_chartSeriesCount.Contains(series.Name))
630  {
631  // above the limit and this is a new series
632  if(!_chartSeriesCountExceededError)
633  {
634  _chartSeriesCountExceededError = true;
635  DebugMessage($"Exceeded maximum chart series count for organization tier, new series will be ignored. Limit is currently set at {_job.Controls.MaximumChartSeries}. https://qnt.co/docs-charting-quotas");
636  }
637  continue;
638  }
639 
640  if (series.Values.Count > 0)
641  {
642  var thisSeries = chart.TryAddAndGetSeries(series.Name, series, forceAddNew: false);
643  if (series.SeriesType == SeriesType.Pie)
644  {
645  var dataPoint = series.ConsolidateChartPoints();
646  if (dataPoint != null)
647  {
648  thisSeries.AddPoint(dataPoint);
649  }
650  }
651  else
652  {
653  var values = thisSeries.Values;
654  if ((values.Count + series.Values.Count) <= _job.Controls.MaximumDataPointsPerChartSeries) // check chart data point limit first
655  {
656  //We already have this record, so just the new samples to the end:
657  values.AddRange(series.Values);
658  }
659  else if (!_chartSeriesExceededDataPoints.Contains(chart.Name + series.Name))
660  {
661  _chartSeriesExceededDataPoints.Add(chart.Name + series.Name);
662  DebugMessage($"Exceeded maximum data points per series for organization tier, chart update skipped. Chart Name {update.Name}. Series name {series.Name}. https://qnt.co/docs-charting-quotas" +
663  $"Limit is currently set at {_job.Controls.MaximumDataPointsPerChartSeries}");
664  }
665  }
666  }
667  }
668  }
669  }
670  }
671 
672  /// <summary>
673  /// Terminate the result thread and apply any required exit procedures like sending final results.
674  /// </summary>
675  public override void Exit()
676  {
677  // Only process the logs once
678  if (!ExitTriggered)
679  {
680  Log.Trace("BacktestingResultHandler.Exit(): starting...");
681  List<LogEntry> copy;
682  lock (LogStore)
683  {
684  copy = LogStore.ToList();
685  }
687  Log.Trace("BacktestingResultHandler.Exit(): Saving logs...");
688  var logLocation = SaveLogs(_algorithmId, copy);
689  SystemDebugMessage("Your log was successfully created and can be retrieved from: " + logLocation);
690 
691  // Set exit flag, update task will send any message before stopping
692  ExitTriggered = true;
693  ExitEvent.Set();
694 
696 
697  SendFinalResult();
698 
699  base.Exit();
700  }
701  }
702 
703  /// <summary>
704  /// Send an algorithm status update to the browser.
705  /// </summary>
706  /// <param name="status">Status enum value.</param>
707  /// <param name="message">Additional optional status message.</param>
708  public virtual void SendStatusUpdate(AlgorithmStatus status, string message = "")
709  {
710  var statusPacket = new AlgorithmStatusPacket(_algorithmId, _projectId, status, message) { OptimizationId = _job.OptimizationId };
711  MessagingHandler.Send(statusPacket);
712  }
713 
714  /// <summary>
715  /// Set the current runtime statistics of the algorithm.
716  /// These are banner/title statistics which show at the top of the live trading results.
717  /// </summary>
718  /// <param name="key">Runtime headline statistic name</param>
719  /// <param name="value">Runtime headline statistic value</param>
720  public virtual void RuntimeStatistic(string key, string value)
721  {
722  lock (RuntimeStatistics)
723  {
724  RuntimeStatistics[key] = value;
725  }
726  }
727 
728  /// <summary>
729  /// Handle order event
730  /// </summary>
731  /// <param name="newEvent">Event to process</param>
732  public override void OrderEvent(OrderEvent newEvent)
733  {
734  _capacityEstimate?.OnOrderEvent(newEvent);
735  }
736 
737  /// <summary>
738  /// Process the synchronous result events, sampling and message reading.
739  /// This method is triggered from the algorithm manager thread.
740  /// </summary>
741  /// <remarks>Prime candidate for putting into a base class. Is identical across all result handlers.</remarks>
742  public virtual void ProcessSynchronousEvents(bool forceProcess = false)
743  {
744  if (Algorithm == null) return;
745 
746  _capacityEstimate.UpdateMarketCapacity(forceProcess);
747 
748  // Invalidate the processed days count so it gets recalculated
749  _progressMonitor.InvalidateProcessedDays();
750 
751  // Update the equity bar
753 
754  var time = Algorithm.UtcTime;
755  if (time > _nextSample || forceProcess)
756  {
757  //Set next sample time: 4000 samples per backtest
758  _nextSample = time.Add(ResamplePeriod);
759 
760  //Sample the portfolio value over time for chart.
761  SampleEquity(time);
762 
763  //Also add the user samples / plots to the result handler tracking:
765  }
766 
768 
769  //Set the running statistics:
770  foreach (var pair in Algorithm.RuntimeStatistics)
771  {
772  RuntimeStatistic(pair.Key, pair.Value);
773  }
774  }
775 
776  /// <summary>
777  /// Configures the <see cref="Console.Out"/> and <see cref="Console.Error"/> <see cref="TextWriter"/>
778  /// instances. By default, we forward <see cref="Console.WriteLine(string)"/> to <see cref="IAlgorithm.Debug"/>.
779  /// This is perfect for running in the cloud, but since they're processed asynchronously, the ordering of these
780  /// messages with respect to <see cref="Log"/> messages is broken. This can lead to differences in regression
781  /// test logs based solely on the ordering of messages. To disable this forwarding, set <code>"forward-console-messages"</code>
782  /// to <code>false</code> in the configuration.
783  /// </summary>
784  protected virtual void ConfigureConsoleTextWriter(IAlgorithm algorithm)
785  {
786  if (Config.GetBool("forward-console-messages", true))
787  {
788  // we need to forward Console.Write messages to the algorithm's Debug function
789  Console.SetOut(new FuncTextWriter(algorithm.Debug));
790  Console.SetError(new FuncTextWriter(algorithm.Error));
791  }
792  else
793  {
794  // we need to forward Console.Write messages to the standard Log functions
795  Console.SetOut(new FuncTextWriter(msg => Log.Trace(msg)));
796  Console.SetError(new FuncTextWriter(msg => Log.Error(msg)));
797  }
798  }
799 
800  /// <summary>
801  /// Calculates and gets the current statistics for the algorithm
802  /// </summary>
803  /// <returns>The current statistics</returns>
805  {
806  return GenerateStatisticsResults(_capacityEstimate);
807  }
808 
809  /// <summary>
810  /// Sets or updates a custom summary statistic
811  /// </summary>
812  /// <param name="name">The statistic name</param>
813  /// <param name="value">The statistic value</param>
814  public void SetSummaryStatistic(string name, string value)
815  {
816  SummaryStatistic(name, value);
817  }
818 
819  private static BacktestResult CreateResultSummary(BacktestResultPacket result)
820  {
821  // Save summary results
822  var summary = new BacktestResult
823  {
824  Charts = new Dictionary<string, Chart>(),
825  State = result.Results.State,
826  Statistics = result.Results.Statistics,
827  TotalPerformance = new()
828  {
831  },
832  ServerStatistics = result.Results.ServerStatistics,
835  };
836  CandlestickSeries equity = null;
837  if (result.Results.Charts != null && result.Results.Charts.TryGetValue(StrategyEquityKey, out var chart) && chart.Series.TryGetValue(EquityKey, out var series))
838  {
839  equity = (CandlestickSeries)series;
840  var samplePeriod = Math.Min(7, series.Values.Count / 100);
841  if (samplePeriod > 1)
842  {
843  var sampler = new SeriesSampler(TimeSpan.FromDays(samplePeriod));
844  equity = (CandlestickSeries)sampler.Sample(series, Time.BeginningOfTime, Time.EndOfTime, truncateValues: true);
845  }
846  var chartClone = chart.CloneEmpty();
847  chartClone.AddSeries(equity);
848  summary.Charts[StrategyEquityKey] = chartClone;
849  }
850  else
851  {
852  Log.Trace($"BacktestingResultHandler.CreateResultSummary(): '{StrategyEquityKey}' chart not found");
853  }
854  return summary;
855  }
856  }
857 }