/*
* 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);
}
}
}
}