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  //Place result into storage.
380  StoreResult(result);
381 
382  result.Results.ServerStatistics = GetServerStatistics(endTime);
383  //Second, send the truncated packet:
384  MessagingHandler.Send(result);
385 
386  Log.Trace("BacktestingResultHandler.SendAnalysisResult(): Processed final packet");
387  }
388  catch (Exception err)
389  {
390  Log.Error(err);
391  }
392  }
393 
394  /// <summary>
395  /// Set the Algorithm instance for ths result.
396  /// </summary>
397  /// <param name="algorithm">Algorithm we're working on.</param>
398  /// <param name="startingPortfolioValue">Algorithm starting capital for statistics calculations</param>
399  /// <remarks>While setting the algorithm the backtest result handler.</remarks>
400  public virtual void SetAlgorithm(IAlgorithm algorithm, decimal startingPortfolioValue)
401  {
402  Algorithm = algorithm;
404  State["Name"] = Algorithm.Name;
405  StartingPortfolioValue = startingPortfolioValue;
409  _capacityEstimate = new CapacityEstimate(Algorithm);
411 
412  //Get the resample period:
413  var totalMinutes = (algorithm.EndDate - algorithm.StartDate).TotalMinutes;
414  var resampleMinutes = totalMinutes < MinimumSamplePeriod * Samples ? MinimumSamplePeriod : totalMinutes / Samples; // Space out the sampling every
415  ResamplePeriod = TimeSpan.FromMinutes(resampleMinutes);
416  Log.Trace("BacktestingResultHandler(): Sample Period Set: " + resampleMinutes.ToStringInvariant("00.00"));
417 
418  //Set the security / market types.
419  var types = new List<SecurityType>();
420  foreach (var kvp in Algorithm.Securities)
421  {
422  var security = kvp.Value;
423 
424  if (!types.Contains(security.Type)) types.Add(security.Type);
425  }
426  SecurityType(types);
427 
428  ConfigureConsoleTextWriter(algorithm);
429 
430  // Wire algorithm name and tags updates
431  algorithm.NameUpdated += (sender, name) => AlgorithmNameUpdated(name);
432  algorithm.TagsUpdated += (sender, tags) => AlgorithmTagsUpdated(tags);
433  }
434 
435  /// <summary>
436  /// Handles updates to the algorithm's name
437  /// </summary>
438  /// <param name="name">The new name</param>
439  public virtual void AlgorithmNameUpdated(string name)
440  {
441  Messages.Enqueue(new AlgorithmNameUpdatePacket(AlgorithmId, name));
442  }
443 
444  /// <summary>
445  /// Sends a packet communicating an update to the algorithm's tags
446  /// </summary>
447  /// <param name="tags">The new tags</param>
448  public virtual void AlgorithmTagsUpdated(HashSet<string> tags)
449  {
450  Messages.Enqueue(new AlgorithmTagsUpdatePacket(AlgorithmId, tags));
451  }
452 
453  /// <summary>
454  /// Send a debug message back to the browser console.
455  /// </summary>
456  /// <param name="message">Message we'd like shown in console.</param>
457  public virtual void DebugMessage(string message)
458  {
459  Messages.Enqueue(new DebugPacket(_projectId, AlgorithmId, CompileId, message));
460  AddToLogStore(message);
461  }
462 
463  /// <summary>
464  /// Send a system debug message back to the browser console.
465  /// </summary>
466  /// <param name="message">Message we'd like shown in console.</param>
467  public virtual void SystemDebugMessage(string message)
468  {
469  Messages.Enqueue(new SystemDebugPacket(_projectId, AlgorithmId, CompileId, message));
470  AddToLogStore(message);
471  }
472 
473  /// <summary>
474  /// Send a logging message to the log list for storage.
475  /// </summary>
476  /// <param name="message">Message we'd in the log.</param>
477  public virtual void LogMessage(string message)
478  {
479  Messages.Enqueue(new LogPacket(AlgorithmId, message));
480  AddToLogStore(message);
481  }
482 
483  /// <summary>
484  /// Add message to LogStore
485  /// </summary>
486  /// <param name="message">Message to add</param>
487  protected override void AddToLogStore(string message)
488  {
489  var messageToLog = Algorithm != null
490  ? Algorithm.Time.ToStringInvariant(DateFormat.UI) + " " + message
491  : "Algorithm Initialization: " + message;
492 
493  base.AddToLogStore(messageToLog);
494  }
495 
496  /// <summary>
497  /// Send list of security asset types the algorithm uses to browser.
498  /// </summary>
499  public virtual void SecurityType(List<SecurityType> types)
500  {
501  var packet = new SecurityTypesPacket
502  {
503  Types = types
504  };
505  Messages.Enqueue(packet);
506  }
507 
508  /// <summary>
509  /// Send an error message back to the browser highlighted in red with a stacktrace.
510  /// </summary>
511  /// <param name="message">Error message we'd like shown in console.</param>
512  /// <param name="stacktrace">Stacktrace information string</param>
513  public virtual void ErrorMessage(string message, string stacktrace = "")
514  {
515  if (message == _errorMessage) return;
516  if (Messages.Count > 500) return;
517  Messages.Enqueue(new HandledErrorPacket(AlgorithmId, message, stacktrace));
518  _errorMessage = message;
519  }
520 
521  /// <summary>
522  /// Send a runtime error message back to the browser highlighted with in red
523  /// </summary>
524  /// <param name="message">Error message.</param>
525  /// <param name="stacktrace">Stacktrace information string</param>
526  public virtual void RuntimeError(string message, string stacktrace = "")
527  {
528  PurgeQueue();
529  Messages.Enqueue(new RuntimeErrorPacket(_job.UserId, AlgorithmId, message, stacktrace));
530  _errorMessage = message;
531  SetAlgorithmState(message, stacktrace);
532  }
533 
534  /// <summary>
535  /// Process brokerage message events
536  /// </summary>
537  /// <param name="brokerageMessageEvent">The brokerage message event</param>
538  public virtual void BrokerageMessage(BrokerageMessageEvent brokerageMessageEvent)
539  {
540  // NOP
541  }
542 
543  /// <summary>
544  /// Add a sample to the chart specified by the chartName, and seriesName.
545  /// </summary>
546  /// <param name="chartName">String chart name to place the sample.</param>
547  /// <param name="seriesIndex">Type of chart we should create if it doesn't already exist.</param>
548  /// <param name="seriesName">Series name for the chart.</param>
549  /// <param name="seriesType">Series type for the chart.</param>
550  /// <param name="value">Value for the chart sample.</param>
551  /// <param name="unit">Unit of the sample</param>
552  protected override void Sample(string chartName, string seriesName, int seriesIndex, SeriesType seriesType, ISeriesPoint value,
553  string unit = "$")
554  {
555  // Sampling during warming up period skews statistics
557  {
558  return;
559  }
560 
561  lock (ChartLock)
562  {
563  //Add a copy locally:
564  Chart chart;
565  if (!Charts.TryGetValue(chartName, out chart))
566  {
567  chart = new Chart(chartName);
568  Charts.AddOrUpdate(chartName, chart);
569  }
570 
571  //Add the sample to our chart:
572  BaseSeries series;
573  if (!chart.Series.TryGetValue(seriesName, out series))
574  {
575  series = BaseSeries.Create(seriesType, seriesName, seriesIndex, unit);
576  chart.Series.Add(seriesName, series);
577  }
578 
579  //Add our value:
580  if (series.Values.Count == 0 || value.Time > series.Values[series.Values.Count - 1].Time
581  // always sample portfolio turnover and use latest value
582  || chartName == PortfolioTurnoverKey)
583  {
584  series.AddPoint(value);
585  }
586  }
587  }
588 
589  /// <summary>
590  /// Sample estimated strategy capacity
591  /// </summary>
592  /// <param name="time">Time of the sample</param>
593  protected override void SampleCapacity(DateTime time)
594  {
595  // Sample strategy capacity, round to 1k
596  var roundedCapacity = _capacityEstimate.Capacity;
597  Sample("Capacity", "Strategy Capacity", 0, SeriesType.Line, new ChartPoint(time, roundedCapacity), AlgorithmCurrencySymbol);
598  }
599 
600  /// <summary>
601  /// Add a range of samples from the users algorithms to the end of our current list.
602  /// </summary>
603  /// <param name="updates">Chart updates since the last request.</param>
604  protected void SampleRange(IEnumerable<Chart> updates)
605  {
606  lock (ChartLock)
607  {
608  foreach (var update in updates)
609  {
610  //Create the chart if it doesn't exist already:
611  Chart chart;
612  if (!Charts.TryGetValue(update.Name, out chart))
613  {
614  chart = new Chart(update.Name);
615  Charts.AddOrUpdate(update.Name, chart);
616  }
617 
618  //Add these samples to this chart.
619  foreach (var series in update.Series.Values)
620  {
621  // let's assert we are within series count limit
622  if (_chartSeriesCount.Count < _job.Controls.MaximumChartSeries)
623  {
624  _chartSeriesCount.Add(series.Name);
625  }
626  else if (!_chartSeriesCount.Contains(series.Name))
627  {
628  // above the limit and this is a new series
629  if(!_chartSeriesCountExceededError)
630  {
631  _chartSeriesCountExceededError = true;
632  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");
633  }
634  continue;
635  }
636 
637  if (series.Values.Count > 0)
638  {
639  var thisSeries = chart.TryAddAndGetSeries(series.Name, series, forceAddNew: false);
640  if (series.SeriesType == SeriesType.Pie)
641  {
642  var dataPoint = series.ConsolidateChartPoints();
643  if (dataPoint != null)
644  {
645  thisSeries.AddPoint(dataPoint);
646  }
647  }
648  else
649  {
650  var values = thisSeries.Values;
651  if ((values.Count + series.Values.Count) <= _job.Controls.MaximumDataPointsPerChartSeries) // check chart data point limit first
652  {
653  //We already have this record, so just the new samples to the end:
654  values.AddRange(series.Values);
655  }
656  else if (!_chartSeriesExceededDataPoints.Contains(chart.Name + series.Name))
657  {
658  _chartSeriesExceededDataPoints.Add(chart.Name + series.Name);
659  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" +
660  $"Limit is currently set at {_job.Controls.MaximumDataPointsPerChartSeries}");
661  }
662  }
663  }
664  }
665  }
666  }
667  }
668 
669  /// <summary>
670  /// Terminate the result thread and apply any required exit procedures like sending final results.
671  /// </summary>
672  public override void Exit()
673  {
674  // Only process the logs once
675  if (!ExitTriggered)
676  {
677  Log.Trace("BacktestingResultHandler.Exit(): starting...");
678  List<LogEntry> copy;
679  lock (LogStore)
680  {
681  copy = LogStore.ToList();
682  }
684  Log.Trace("BacktestingResultHandler.Exit(): Saving logs...");
685  var logLocation = SaveLogs(_algorithmId, copy);
686  SystemDebugMessage("Your log was successfully created and can be retrieved from: " + logLocation);
687 
688  // Set exit flag, update task will send any message before stopping
689  ExitTriggered = true;
690  ExitEvent.Set();
691 
693 
694  SendFinalResult();
695 
696  base.Exit();
697  }
698  }
699 
700  /// <summary>
701  /// Send an algorithm status update to the browser.
702  /// </summary>
703  /// <param name="status">Status enum value.</param>
704  /// <param name="message">Additional optional status message.</param>
705  public virtual void SendStatusUpdate(AlgorithmStatus status, string message = "")
706  {
707  var statusPacket = new AlgorithmStatusPacket(_algorithmId, _projectId, status, message) { OptimizationId = _job.OptimizationId };
708  MessagingHandler.Send(statusPacket);
709  }
710 
711  /// <summary>
712  /// Set the current runtime statistics of the algorithm.
713  /// These are banner/title statistics which show at the top of the live trading results.
714  /// </summary>
715  /// <param name="key">Runtime headline statistic name</param>
716  /// <param name="value">Runtime headline statistic value</param>
717  public virtual void RuntimeStatistic(string key, string value)
718  {
719  lock (RuntimeStatistics)
720  {
721  RuntimeStatistics[key] = value;
722  }
723  }
724 
725  /// <summary>
726  /// Handle order event
727  /// </summary>
728  /// <param name="newEvent">Event to process</param>
729  public override void OrderEvent(OrderEvent newEvent)
730  {
731  _capacityEstimate?.OnOrderEvent(newEvent);
732  }
733 
734  /// <summary>
735  /// Process the synchronous result events, sampling and message reading.
736  /// This method is triggered from the algorithm manager thread.
737  /// </summary>
738  /// <remarks>Prime candidate for putting into a base class. Is identical across all result handlers.</remarks>
739  public virtual void ProcessSynchronousEvents(bool forceProcess = false)
740  {
741  if (Algorithm == null) return;
742 
743  _capacityEstimate.UpdateMarketCapacity(forceProcess);
744 
745  // Invalidate the processed days count so it gets recalculated
746  _progressMonitor.InvalidateProcessedDays();
747 
748  // Update the equity bar
750 
751  var time = Algorithm.UtcTime;
752  if (time > _nextSample || forceProcess)
753  {
754  //Set next sample time: 4000 samples per backtest
755  _nextSample = time.Add(ResamplePeriod);
756 
757  //Sample the portfolio value over time for chart.
758  SampleEquity(time);
759 
760  //Also add the user samples / plots to the result handler tracking:
762  }
763 
765 
766  //Set the running statistics:
767  foreach (var pair in Algorithm.RuntimeStatistics)
768  {
769  RuntimeStatistic(pair.Key, pair.Value);
770  }
771  }
772 
773  /// <summary>
774  /// Configures the <see cref="Console.Out"/> and <see cref="Console.Error"/> <see cref="TextWriter"/>
775  /// instances. By default, we forward <see cref="Console.WriteLine(string)"/> to <see cref="IAlgorithm.Debug"/>.
776  /// This is perfect for running in the cloud, but since they're processed asynchronously, the ordering of these
777  /// messages with respect to <see cref="Log"/> messages is broken. This can lead to differences in regression
778  /// test logs based solely on the ordering of messages. To disable this forwarding, set <code>"forward-console-messages"</code>
779  /// to <code>false</code> in the configuration.
780  /// </summary>
781  protected virtual void ConfigureConsoleTextWriter(IAlgorithm algorithm)
782  {
783  if (Config.GetBool("forward-console-messages", true))
784  {
785  // we need to forward Console.Write messages to the algorithm's Debug function
786  Console.SetOut(new FuncTextWriter(algorithm.Debug));
787  Console.SetError(new FuncTextWriter(algorithm.Error));
788  }
789  else
790  {
791  // we need to forward Console.Write messages to the standard Log functions
792  Console.SetOut(new FuncTextWriter(msg => Log.Trace(msg)));
793  Console.SetError(new FuncTextWriter(msg => Log.Error(msg)));
794  }
795  }
796 
797  /// <summary>
798  /// Calculates and gets the current statistics for the algorithm
799  /// </summary>
800  /// <returns>The current statistics</returns>
802  {
803  return GenerateStatisticsResults(_capacityEstimate);
804  }
805 
806  /// <summary>
807  /// Sets or updates a custom summary statistic
808  /// </summary>
809  /// <param name="name">The statistic name</param>
810  /// <param name="value">The statistic value</param>
811  public void SetSummaryStatistic(string name, string value)
812  {
813  SummaryStatistic(name, value);
814  }
815  }
816 }