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