Lean  $LEAN_TAG$
RegressionResultHandler.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.Diagnostics;
21 using System.IO;
22 using System.Linq;
23 using Newtonsoft.Json;
25 using QuantConnect.Data;
28 using QuantConnect.Logging;
29 using QuantConnect.Orders;
31 using QuantConnect.Util;
32 
34 {
35  /// <summary>
36  /// Provides a wrapper over the <see cref="BacktestingResultHandler"/> that logs all order events
37  /// to a separate file
38  /// </summary>
40  {
41  private Language Language => Config.GetValue<Language>("algorithm-language");
42 
43  private DateTime _testStartTime;
44  private DateTime _lastRuntimeStatisticsDate;
45  private DateTime _lastAlphaRuntimeStatisticsDate;
46 
47  private TextWriter _writer;
48  private readonly object _sync = new object();
49  private readonly ConcurrentQueue<string> _preInitializeLines;
50  private readonly Dictionary<string, string> _currentRuntimeStatistics;
51  private readonly Dictionary<string, string> _currentAlphaRuntimeStatistics;
52 
53  // this defaults to false since it can create massive files. a full regression run takes about 800MB
54  // for each folder (800MB for ./passed and 800MB for ./regression)
55  private static readonly bool HighFidelityLogging = Config.GetBool("regression-high-fidelity-logging", false);
56 
57  private static readonly bool IsTest = !Process.GetCurrentProcess().ProcessName.Contains("Lean.Launcher");
58 
59  /// <summary>
60  /// Gets the path used for logging all portfolio changing events, such as orders, TPV, daily holdings values
61  /// </summary>
62  public string LogFilePath => IsTest
63  ? $"./regression/{AlgorithmId}.{Language.ToLower()}.details.log"
64  : $"./{AlgorithmId}/{DateTime.Now:yyyy-MM-dd-hh-mm-ss}.{Language.ToLower()}.details.log";
65 
66  /// <summary>
67  /// True if there was a runtime error running the algorithm
68  /// </summary>
69  public bool HasRuntimeError { get; private set; }
70 
71  /// <summary>
72  /// Initializes a new instance of the <see cref="RegressionResultHandler"/> class
73  /// </summary>
75  {
76  _testStartTime = DateTime.UtcNow;
77  _preInitializeLines = new ConcurrentQueue<string>();
78  _currentRuntimeStatistics = new Dictionary<string, string>();
79  _currentAlphaRuntimeStatistics = new Dictionary<string, string>();
80  }
81 
82  /// <summary>
83  /// Initializes the stream writer using the algorithm's id (name) in the file path
84  /// </summary>
85  public override void SetAlgorithm(IAlgorithm algorithm, decimal startingPortfolioValue)
86  {
87  base.SetAlgorithm(algorithm, startingPortfolioValue);
88 
89  var fileInfo = new FileInfo(LogFilePath);
90  Directory.CreateDirectory(fileInfo.DirectoryName);
91  if (fileInfo.Exists)
92  {
93  fileInfo.Delete();
94  }
95 
96  lock (_sync)
97  {
98  _writer = new StreamWriter(LogFilePath);
99  WriteLine($"{_testStartTime}: Starting regression test");
100 
101  string line;
102  while (_preInitializeLines.TryDequeue(out line))
103  {
104  WriteLine(line);
105  }
106  }
107  }
108 
109  /// <summary>
110  /// Runs on date changes, use this to log TPV and holdings values each day
111  /// </summary>
112  protected override void SamplePerformance(DateTime time, decimal value)
113  {
114  lock (_sync)
115  {
116  WriteLine($"{Algorithm.UtcTime}: Total Portfolio Value: {Algorithm.Portfolio.TotalPortfolioValue}");
117 
118  // write the entire cashbook each day, includes current conversion rates and total value of cash holdings
119  WriteLine($"{Environment.NewLine}{Algorithm.Portfolio.CashBook}");
120 
121  foreach (var kvp in Algorithm.Securities)
122  {
123  var symbol = kvp.Key;
124  var security = kvp.Value;
125  if (!security.HoldStock)
126  {
127  continue;
128  }
129 
130  // detailed logging of security holdings
131  WriteLine(
132  $"{Algorithm.UtcTime}: " +
133  $"Holdings: {symbol.Value} ({symbol.ID}): " +
134  $"Price: {security.Price} " +
135  $"Quantity: {security.Holdings.Quantity} " +
136  $"Value: {security.Holdings.HoldingsValue} " +
137  $"LastData: {security.GetLastData()}"
138  );
139  }
140  }
141 
142  base.SamplePerformance(time, value);
143  }
144 
145  /// <summary>
146  /// Log the order and order event to the dedicated log file for this regression algorithm
147  /// </summary>
148  /// <remarks>In backtesting the order events are not sent because it would generate a high load of messaging.</remarks>
149  /// <param name="newEvent">New order event details</param>
150  public override void OrderEvent(OrderEvent newEvent)
151  {
152  // log order events to a separate file for easier diffing of regression runs
153  var order = Algorithm.Transactions.GetOrderById(newEvent.OrderId);
154 
155  lock (_sync)
156  {
157  WriteLine("==============================================================");
158  WriteLine($" Symbol: {order.Symbol}");
159  WriteLine($" Order: {order}");
160  WriteLine($" Event: {newEvent}");
161  WriteLine($" Position: {Algorithm.Portfolio[newEvent.Symbol].Quantity}");
162  SecurityHolding underlyingHolding;
163  if (newEvent.Symbol.HasUnderlying && Algorithm.Portfolio.TryGetValue(newEvent.Symbol.Underlying, out underlyingHolding))
164  {
165  WriteLine($"Underlying: {underlyingHolding.Quantity}");
166  }
167  WriteLine($" Cash: {Algorithm.Portfolio.Cash:0.00}");
168  WriteLine($" Portfolio: {Algorithm.Portfolio.TotalPortfolioValue:0.00}");
169  WriteLine("==============================================================");
170  }
171 
172  base.OrderEvent(newEvent);
173  }
174 
175  /// <summary>
176  /// Send list of security asset types the algortihm uses to browser.
177  /// </summary>
178  public override void SecurityType(List<SecurityType> types)
179  {
180  base.SecurityType(types);
181 
182  var sorted = types.Select(type => type.ToString()).OrderBy(type => type);
183  WriteLine($"SecurityTypes: {string.Join("|", sorted)}");
184  }
185 
186  /// <summary>
187  /// Send a debug message back to the browser console.
188  /// </summary>
189  /// <param name="message">Message we'd like shown in console.</param>
190  public override void DebugMessage(string message)
191  {
192  base.DebugMessage(message);
193 
194  WriteLine($"DebugMessage: {message}");
195  }
196 
197  /// <summary>
198  /// Send an error message back to the browser highlighted in red with a stacktrace.
199  /// </summary>
200  /// <param name="message">Error message we'd like shown in console.</param>
201  /// <param name="stacktrace">Stacktrace information string</param>
202  public override void ErrorMessage(string message, string stacktrace = "")
203  {
204  base.ErrorMessage(message, stacktrace);
205 
206  stacktrace = string.IsNullOrEmpty(stacktrace) ? null : Environment.NewLine + stacktrace;
207  WriteLine($"ErrorMessage: {message}{stacktrace}");
208  }
209 
210  /// <summary>
211  /// Send a logging message to the log list for storage.
212  /// </summary>
213  /// <param name="message">Message we'd in the log.</param>
214  public override void LogMessage(string message)
215  {
216  base.LogMessage(message);
217 
218  WriteLine($"LogMessage: {message}");
219  }
220 
221  /// <summary>
222  /// Send a runtime error message back to the browser highlighted with in red
223  /// </summary>
224  /// <param name="message">Error message.</param>
225  /// <param name="stacktrace">Stacktrace information string</param>
226  public override void RuntimeError(string message, string stacktrace = "")
227  {
228  HasRuntimeError = true;
229  base.RuntimeError(message, stacktrace);
230 
231  stacktrace = string.IsNullOrEmpty(stacktrace) ? null : Environment.NewLine + stacktrace;
232  WriteLine($"RuntimeError: {message}{stacktrace}");
233  }
234 
235  /// <summary>
236  /// Send a system debug message back to the browser console.
237  /// </summary>
238  /// <param name="message">Message we'd like shown in console.</param>
239  public override void SystemDebugMessage(string message)
240  {
241  base.SystemDebugMessage(message);
242 
243  WriteLine($"SystemDebugMessage: {message}");
244  }
245 
246  /// <summary>
247  /// Set the current runtime statistics of the algorithm.
248  /// These are banner/title statistics which show at the top of the live trading results.
249  /// </summary>
250  /// <param name="key">Runtime headline statistic name</param>
251  /// <param name="value">Runtime headline statistic value</param>
252  public override void RuntimeStatistic(string key, string value)
253  {
254  try
255  {
256  if (HighFidelityLogging || _lastRuntimeStatisticsDate != Algorithm.Time.Date)
257  {
258  _lastRuntimeStatisticsDate = Algorithm.Time.Date;
259 
260  string existingValue;
261  if (!_currentRuntimeStatistics.TryGetValue(key, out existingValue) || existingValue != value)
262  {
263  _currentRuntimeStatistics[key] = value;
264  WriteLine($"RuntimeStatistic: {key}: {value}");
265  }
266  }
267 
268  base.RuntimeStatistic(key, value);
269  }
270  catch (Exception exception)
271  {
272  Log.Error(exception);
273  }
274  }
275 
276  /// <summary>
277  /// Save an algorithm message to the log store. Uses a different timestamped method of adding messaging to interweve debug and logging messages.
278  /// </summary>
279  /// <param name="message">String message to store</param>
280  protected override void AddToLogStore(string message)
281  {
282  base.AddToLogStore(message);
283 
284  WriteLine($"AddToLogStore: {message}");
285  }
286 
287  /// <summary>
288  /// Event fired each time that we add/remove securities from the data feed
289  /// </summary>
290  public override void OnSecuritiesChanged(SecurityChanges changes)
291  {
292  base.OnSecuritiesChanged(changes);
293 
294  if (changes.AddedSecurities.Count > 0)
295  {
296  var added = changes.AddedSecurities
297  .Select(security => security.Symbol.ToString())
298  .OrderBy(symbol => symbol);
299 
300  WriteLine($"OnSecuritiesChanged:ADD: {string.Join("|", added)}");
301  }
302 
303  if (changes.RemovedSecurities.Count > 0)
304  {
305  var removed = changes.RemovedSecurities
306  .Select(security => security.Symbol.ToString())
307  .OrderBy(symbol => symbol);
308 
309  WriteLine($"OnSecuritiesChanged:REM: {string.Join("|", removed)}");
310  }
311  }
312 
313  /// <summary>
314  /// Runs at the end of each time loop. When HighFidelityLogging is enabled, we'll
315  /// log each piece of data to allow for faster determination of regression causes
316  /// </summary>
317  public override void ProcessSynchronousEvents(bool forceProcess = false)
318  {
319  if (HighFidelityLogging)
320  {
321  var slice = Algorithm.CurrentSlice;
322  if (slice != null)
323  {
324  lock (_sync)
325  {
326  // aggregate slice data
327  WriteLine($"Slice Time: {slice.Time:o} Slice Count: {slice.Count}");
328  var data = new Dictionary<Symbol, List<BaseData>>();
329  foreach (var kvp in slice.Bars)
330  {
331  data.Add(kvp.Key, (BaseData) kvp.Value);
332  }
333 
334  foreach (var kvp in slice.QuoteBars)
335  {
336  data.Add(kvp.Key, (BaseData)kvp.Value);
337  }
338 
339  foreach (var kvp in slice.Ticks)
340  {
341  foreach (var tick in kvp.Value)
342  {
343  data.Add(kvp.Key, (BaseData) tick);
344  }
345  }
346 
347  foreach (var kvp in slice.Delistings)
348  {
349  data.Add(kvp.Key, (BaseData) kvp.Value);
350  }
351 
352  foreach (var kvp in slice.Splits)
353  {
354  data.Add(kvp.Key, (BaseData) kvp.Value);
355  }
356 
357  foreach (var kvp in slice.SymbolChangedEvents)
358  {
359  data.Add(kvp.Key, (BaseData) kvp.Value);
360  }
361 
362  foreach (var kvp in slice.Dividends)
363  {
364  data.Add(kvp.Key, (BaseData) kvp.Value);
365  }
366 
367  foreach (var kvp in data.OrderBy(kvp => kvp.Key))
368  {
369  foreach (var item in kvp.Value)
370  {
371  WriteLine($"{Algorithm.UtcTime}: Slice: DataTime: {item.EndTime} {item}");
372  }
373  }
374  }
375  }
376  }
377 
378  base.ProcessSynchronousEvents(forceProcess);
379  }
380 
381  /// <summary>
382  /// Save the results to disk
383  /// </summary>
384  public override void SaveResults(string name, Result result)
385  {
386  File.WriteAllText(GetResultsPath(name), JsonConvert.SerializeObject(result));
387  }
388 
389  /// <summary>
390  /// Terminate the result thread and apply any required exit procedures.
391  /// Save orders log files to disk.
392  /// </summary>
393  public override void Exit()
394  {
395  if (!ExitTriggered && Algorithm != null)
396  {
397  var holdings = Algorithm.Portfolio.Values.Where(holding => holding.Invested).Select(holding => $"HOLDINGS:: {holding}").ToList();
398  if(holdings.Count > 0)
399  {
400  Log.Trace($"{Environment.NewLine}{string.Join(Environment.NewLine, holdings)}");
401  }
402  else
403  {
404  Log.Trace("HOLDINGS:: none");
405  }
406  Log.Trace($"{Environment.NewLine}{Algorithm.Portfolio.CashBook}");
407  }
408 
409  base.Exit();
410  lock (_sync)
411  {
412  if (_writer != null)
413  {
414  // only log final statistics and we want them to all be together
415  foreach (var kvp in RuntimeStatistics.OrderBy(kvp => kvp.Key))
416  {
417  WriteLine($"{kvp.Key,-15}\t{kvp.Value}");
418  }
419 
420  var end = DateTime.UtcNow;
421  var delta = end - _testStartTime;
422  WriteLine($"{end}: Completed regression test, took: {delta.TotalSeconds:0.0} seconds");
423  _writer.DisposeSafely();
424  _writer = null;
425  }
426  else
427  {
428  string line;
429  while (_preInitializeLines.TryDequeue(out line))
430  {
431  Console.WriteLine(line);
432  }
433  }
434  }
435  }
436 
437  /// <summary>
438  /// We want to make algorithm messages end up in both the standard regression log file {algorithm}.{language}.log
439  /// as well as the details log {algorithm}.{language}.details.log. The details log is focused on providing a log
440  /// dedicated solely to the algorithm's behavior, void of all <see cref="QuantConnect.Logging.Log"/> messages
441  /// </summary>
442  protected override void ConfigureConsoleTextWriter(IAlgorithm algorithm)
443  {
444  // configure Console.WriteLine and Console.Error.WriteLine to both logs, syslog and details.log
445  // when 'forward-console-messages' is set to false, it guarantees synchronous logging of these messages
446 
447  if (Config.GetBool("forward-console-messages", true))
448  {
449  // we need to forward Console.Write messages to the algorithm's Debug function
450  Console.SetOut(new FuncTextWriter(msg =>
451  {
452  algorithm.Debug(msg);
453  WriteLine($"DEBUG: {msg}");
454  }));
455  Console.SetError(new FuncTextWriter(msg =>
456  {
457  algorithm.Error(msg);
458  WriteLine($"ERROR: {msg}");
459  }));
460  }
461  else
462  {
463  // we need to forward Console.Write messages to the standard Log functions
464  Console.SetOut(new FuncTextWriter(msg =>
465  {
466  Log.Trace(msg);
467  WriteLine($"DEBUG: {msg}");
468  }));
469  Console.SetError(new FuncTextWriter(msg =>
470  {
471  Log.Error(msg);
472  WriteLine($"ERROR: {msg}");
473  }));
474  }
475  }
476 
477  private void WriteLine(string message)
478  {
479  if (!Log.DebuggingEnabled)
480  {
481  return;
482  }
483 
484  lock (_sync)
485  {
486  if (_writer == null)
487  {
488  _preInitializeLines.Enqueue(message);
489  }
490  else
491  {
492  _writer.WriteLine($"{Algorithm.Time:O}: {message}");
493  }
494  }
495  }
496  }
497 }