/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading; using QuantConnect.Interfaces; using QuantConnect.Lean.Engine.DataFeeds; using QuantConnect.Lean.Engine.Setup; using QuantConnect.Lean.Engine.TransactionHandlers; using QuantConnect.Logging; using QuantConnect.Orders; using QuantConnect.Packets; using QuantConnect.Statistics; using System.Diagnostics; using QuantConnect.Securities; namespace QuantConnect.Lean.Engine.Results { /// /// Desktop Result Handler - Desktop GUI Result Handler for Piping Results to WinForms: /// public class DesktopResultHandler : BaseResultsHandler, IResultHandler { private bool _exitTriggered; private readonly object _chartLock = new object(); private AlgorithmNodePacket _job; //Sampling Periods: private DateTime _nextSample; /// /// A dictionary containing summary statistics /// public Dictionary FinalStatistics { get; private set; } = new Dictionary(); /// /// Messaging to store notification messages for processing. /// public ConcurrentQueue Messages { get; set; } = new ConcurrentQueue(); /// /// Local object access to the algorithm for the underlying Debug and Error messaging. /// public IAlgorithm Algorithm { get; set; } /// /// Charts collection for storing the master copy of user charting data. /// public ConcurrentDictionary Charts { get; set; } = new ConcurrentDictionary(); /// /// Boolean flag indicating the result hander thread is busy. /// False means it has completely finished and ready to dispose. /// public bool IsActive { get; private set; } = true; /// /// Sampling period for timespans between resamples of the charting equity. /// /// Specifically critical for backtesting since with such long timeframes the sampled data can get extreme. public TimeSpan ResamplePeriod { get; } = TimeSpan.FromSeconds(2); /// /// How frequently the backtests push messages to the browser. /// /// Update frequency of notification packets public TimeSpan NotificationPeriod { get; } = TimeSpan.FromSeconds(2); /// /// Initialize the result handler with this result packet. /// /// Algorithm job packet for this result handler /// /// /// /// public void Initialize(AlgorithmNodePacket job, IMessagingHandler messagingHandler, IApi api, ISetupHandler setupHandler, ITransactionHandler transactionHandler) { //Redirect the log messages here: _job = job; var desktopLogging = new FunctionalLogHandler(DebugMessage, DebugMessage, ErrorMessage); Log.LogHandler = new CompositeLogHandler(desktopLogging, Log.LogHandler); } /// /// Entry point for console result handler thread. /// public void Run() { while ( !_exitTriggered || Messages.Count > 0 ) { Thread.Sleep(100); } DebugMessage("DesktopResultHandler: Ending Thread..."); IsActive = false; } /// /// Send a debug message back to the browser console. /// /// Message we'd like shown in console. public void DebugMessage(string message) { Messages.Enqueue(new DebugPacket(0, "", "", message)); } /// /// Send a system debug message back to the browser console. /// /// Message we'd like shown in console. public void SystemDebugMessage(string message) { Messages.Enqueue(new SystemDebugPacket(0, "", "", message)); } /// /// Send a logging message to the log list for storage. /// /// Message we'd in the log. public void LogMessage(string message) { Messages.Enqueue(new LogPacket("", message)); } /// /// Send a runtime error message back to the browser highlighted with in red /// /// Error message. /// Stacktrace information string public void RuntimeError(string message, string stacktrace = "") { Messages.Enqueue(new RuntimeErrorPacket(_job.UserId, "", message, stacktrace)); } /// /// Send an error message back to the console highlighted in red with a stacktrace. /// /// Error message we'd like shown in console. public void ErrorMessage(string message) { Messages.Enqueue(new HandledErrorPacket("", message)); } /// /// Send an error message back to the console highlighted in red with a stacktrace. /// /// Error message we'd like shown in console. /// Stacktrace information string public void ErrorMessage(string message, string stacktrace = "") { Messages.Enqueue(new HandledErrorPacket("", message, stacktrace)); } /// /// Add a sample to the chart specified by the chartName, and seriesName. /// /// String chart name to place the sample. /// Type of chart we should create if it doesn't already exist. /// Series name for the chart. /// Series type for the chart. /// Time for the sample /// Value for the chart sample. /// Unit for the sample axis /// Sample can be used to create new charts or sample equity - daily performance. public void Sample(string chartName, string seriesName, int seriesIndex, SeriesType seriesType, DateTime time, decimal value, string unit = "$") { // Sampling during warming up period skews statistics if (Algorithm.IsWarmingUp) { return; } lock (_chartLock) { //Add a copy locally: if (!Charts.ContainsKey(chartName)) { Charts.AddOrUpdate(chartName, new Chart(chartName)); } //Add the sample to our chart: if (!Charts[chartName].Series.ContainsKey(seriesName)) { Charts[chartName].Series.Add(seriesName, new Series(seriesName, seriesType, seriesIndex, unit)); } //Add our value: Charts[chartName].Series[seriesName].Values.Add(new ChartPoint(time, value)); } } /// /// Sample the strategy equity at this moment in time. /// /// Current time /// Current equity value public void SampleEquity(DateTime time, decimal value) { Sample("Strategy Equity", "Equity", 0, SeriesType.Candle, time, value); } /// /// Sample today's algorithm daily performance value. /// /// Current time. /// Value of the daily performance. public void SamplePerformance(DateTime time, decimal value) { Sample("Strategy Equity", "Daily Performance", 0, SeriesType.Line, time, value, "%"); } /// /// Sample the current benchmark performance directly with a time-value pair. /// /// Current backtest date. /// Current benchmark value. /// public void SampleBenchmark(DateTime time, decimal value) { Sample("Benchmark", "Benchmark", 0, SeriesType.Line, time, value); } /// /// Analyse the algorithm and determine its security types. /// /// List of security types in the algorithm public void SecurityType(List types) { //NOP } /// /// Send an algorithm status update to the browser. /// /// Status enum value. /// Additional optional status message. /// In backtesting we do not send the algorithm status updates. public void SendStatusUpdate(AlgorithmStatus status, string message = "") { DebugMessage("DesktopResultHandler.SendStatusUpdate(): Algorithm Status: " + status + " : " + message); } /// /// Sample the asset prices to generate plots. /// /// Symbol we're sampling. /// Time of sample /// Value of the asset price public void SampleAssetPrices(Symbol symbol, DateTime time, decimal value) { //NOP. Don't sample asset prices in console. } /// /// Add a range of samples to the store. /// /// Charting updates since the last sample request. public void SampleRange(List updates) { lock (_chartLock) { foreach (var update in updates) { //Create the chart if it doesn't exist already: if (!Charts.ContainsKey(update.Name)) { Charts.AddOrUpdate(update.Name, new Chart(update.Name, update.ChartType)); } //Add these samples to this chart. foreach (var series in update.Series.Values) { //If we don't already have this record, its the first packet if (!Charts[update.Name].Series.ContainsKey(series.Name)) { Charts[update.Name].Series.Add(series.Name, new Series(series.Name, series.SeriesType)); } //We already have this record, so just the new samples to the end: Charts[update.Name].Series[series.Name].Values.AddRange(series.Values); } } } } /// /// Algorithm final analysis results dumped to the console. /// /// Lean AlgorithmJob task /// Collection of orders from the algorithm /// Collection of time-profit values for the algorithm /// Current holdings state for the algorithm /// Cashbook for the holdings. /// Statistics information for the algorithm (empty if not finished) /// Runtime statistics banner information public void SendFinalResult(AlgorithmNodePacket job, Dictionary orders, Dictionary profitLoss, Dictionary holdings, CashBook cashbook, StatisticsResults statisticsResults, Dictionary banner) { // uncomment these code traces to help write regression tests //Log.Trace("var statistics = new Dictionary();"); // Bleh. Nicely format statistical analysis on your algorithm results. Save to file etc. foreach (var pair in statisticsResults.Summary) { DebugMessage("STATISTICS:: " + pair.Key + " " + pair.Value); } FinalStatistics = statisticsResults.Summary; } /// /// Set the Algorithm instance for ths result. /// /// Algorithm we're working on. /// While setting the algorithm the backtest result handler. public void SetAlgorithm(IAlgorithm algorithm) { Algorithm = algorithm; } /// /// Terminate the result thread and apply any required exit proceedures. /// public void Exit() { _exitTriggered = true; } /// /// Send a new order event to the browser. /// /// In backtesting the order events are not sent because it would generate a high load of messaging. /// New order event details public void OrderEvent(OrderEvent newEvent) { DebugMessage("DesktopResultHandler.OrderEvent(): id:" + newEvent.OrderId + " >> Status:" + newEvent.Status + " >> Fill Price: " + newEvent.FillPrice.ToString("C") + " >> Fill Quantity: " + newEvent.FillQuantity); } /// /// Set the current runtime statistics of the algorithm /// /// Runtime headline statistic name /// Runtime headline statistic value public void RuntimeStatistic(string key, string value) { DebugMessage("DesktopResultHandler.RuntimeStatistic(): " + key + " : " + value); } /// /// Clear the outstanding message queue to exit the thread. /// public void PurgeQueue() { Messages.Clear(); } /// /// Store result on desktop. /// /// Packet of data to store. /// Store the packet asyncronously to speed up the thread. /// Async creates crashes in Mono 3.10 if the thread disappears before the upload is complete so it is disabled for now. public void StoreResult(Packet packet, bool async = false) { // Do nothing. } /// /// Not used /// public void SetChartSubscription(string symbol) { // } /// /// Process the synchronous result events, sampling and message reading. /// This method is triggered from the algorithm manager thread. /// /// Prime candidate for putting into a base class. Is identical across all result handlers. public void ProcessSynchronousEvents(bool forceProcess = false) { var time = Algorithm.Time; if (time > _nextSample || forceProcess) { //Set next sample time: 4000 samples per backtest _nextSample = time.Add(ResamplePeriod); //Sample the portfolio value over time for chart. SampleEquity(time, Math.Round(Algorithm.Portfolio.TotalPortfolioValue, 4)); //Also add the user samples / plots to the result handler tracking: SampleRange(Algorithm.GetChartUpdates()); //Sample the asset pricing: foreach (var kvp in Algorithm.Securities) { var security = kvp.Value; SampleAssetPrices(security.Symbol, time, security.Price); } } //Send out the debug messages: var debugStopWatch = Stopwatch.StartNew(); while (Algorithm.DebugMessages.Count > 0 && debugStopWatch.ElapsedMilliseconds < 250) { string message; if (Algorithm.DebugMessages.TryDequeue(out message)) { DebugMessage(message); } } //Send out the error messages: var errorStopWatch = Stopwatch.StartNew(); while (Algorithm.ErrorMessages.Count > 0 && errorStopWatch.ElapsedMilliseconds < 250) { string message; if (Algorithm.ErrorMessages.TryDequeue(out message)) { ErrorMessage(message); } } //Send out the log messages: var logStopWatch = Stopwatch.StartNew(); while (Algorithm.LogMessages.Count > 0 && logStopWatch.ElapsedMilliseconds < 250) { string message; if (Algorithm.LogMessages.TryDequeue(out message)) { LogMessage(message); } } //Set the running statistics: foreach (var pair in Algorithm.RuntimeStatistics) { RuntimeStatistic(pair.Key, pair.Value); } } } }