New Statistics implementation

Added Trade and TradeBuilder classes
Added UtcTime and OrderFee property to OrderEvent class
Added AlgorithmPerformance class with a few metrics + tests
Added portfolio statistics + rolling statistics

Closes #30 via PR #164

Thanks @SteffanoRaggi!
This commit is contained in:
Stefano Raggi
2015-08-18 23:23:58 +02:00
committed by snugs
parent 788b598b6d
commit fac6f46a9a
32 changed files with 4138 additions and 75 deletions
+10
View File
@@ -28,6 +28,7 @@ using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Statistics;
namespace QuantConnect.Algorithm
{
@@ -108,6 +109,9 @@ namespace QuantConnect.Algorithm
// initialize our scheduler, this acts as a liason to the real time handler
Schedule = new ScheduleManager(Securities, TimeZone);
// initialize the trade builder
TradeBuilder = new TradeBuilder(FillGroupingMethod.FillToFill, FillMatchingMethod.FIFO);
}
/// <summary>
@@ -178,6 +182,11 @@ namespace QuantConnect.Algorithm
set;
}
/// <summary>
/// Gets the Trade Builder to generate trades from executions
/// </summary>
public TradeBuilder TradeBuilder { get; private set; }
/// <summary>
/// Gets the date rules helper object to make specifying dates for events easier
/// </summary>
@@ -936,6 +945,7 @@ namespace QuantConnect.Algorithm
{
_liveMode = live;
Notify = new NotificationManager(live);
TradeBuilder.SetLiveMode(live);
}
}
@@ -119,7 +119,8 @@ namespace QuantConnect.Brokerages.Backtesting
if (!order.BrokerId.Contains(order.Id)) order.BrokerId.Add(order.Id);
// fire off the event that says this order has been submitted
var submitted = new OrderEvent(order) {Status = OrderStatus.Submitted};
const int orderFee = 0;
var submitted = new OrderEvent(order, _algorithm.UtcTime, orderFee) { Status = OrderStatus.Submitted };
OnOrderEvent(submitted);
return true;
@@ -152,7 +153,8 @@ namespace QuantConnect.Brokerages.Backtesting
if (!order.BrokerId.Contains(order.Id)) order.BrokerId.Add(order.Id);
// fire off the event that says this order has been updated
var updated = new OrderEvent(order) {Status = OrderStatus.Submitted};
const int orderFee = 0;
var updated = new OrderEvent(order, _algorithm.UtcTime, orderFee) { Status = OrderStatus.Submitted };
OnOrderEvent(updated);
return true;
@@ -176,7 +178,8 @@ namespace QuantConnect.Brokerages.Backtesting
if (!order.BrokerId.Contains(order.Id)) order.BrokerId.Add(order.Id);
// fire off the event that says this order has been canceled
var canceled = new OrderEvent(order) {Status = OrderStatus.Canceled};
const int orderFee = 0;
var canceled = new OrderEvent(order, _algorithm.UtcTime, orderFee) { Status = OrderStatus.Canceled };
OnOrderEvent(canceled);
return true;
@@ -223,7 +226,8 @@ namespace QuantConnect.Brokerages.Backtesting
continue;
}
var fill = new OrderEvent(order);
var orderFee = security.TransactionModel.GetOrderFee(security, order);
var fill = new OrderEvent(order, _algorithm.UtcTime, orderFee);
// verify sure we have enough cash to perform the fill
bool sufficientBuyingPower;
@@ -237,7 +241,7 @@ namespace QuantConnect.Brokerages.Backtesting
Order pending;
_pending.TryRemove(order.Id, out pending);
order.Status = OrderStatus.Invalid;
OnOrderEvent(new OrderEvent(order, "Error in GetSufficientCapitalForOrder"));
OnOrderEvent(new OrderEvent(order, _algorithm.UtcTime, orderFee, "Error in GetSufficientCapitalForOrder"));
Log.Error(err);
_algorithm.Error(string.Format("Order Error: id: {0}, Error executing margin models: {1}", order.Id, err.Message));
@@ -684,7 +684,8 @@ namespace QuantConnect.Brokerages.InteractiveBrokers
// invalidate the order
var order = _orderProvider.GetOrderByBrokerageId(e.TickerId);
var orderEvent = new OrderEvent(order) {Status = OrderStatus.Invalid};
const int orderFee = 0;
var orderEvent = new OrderEvent(order, DateTime.UtcNow, orderFee) { Status = OrderStatus.Invalid };
OnOrderEvent(orderEvent);
}
@@ -777,7 +778,8 @@ namespace QuantConnect.Brokerages.InteractiveBrokers
// mark sells as negative quantities
var fillQuantity = order.Direction == OrderDirection.Buy ? update.Filled : -update.Filled;
var orderEvent = new OrderEvent(order, "Interactive Brokers Fill Event")
const int orderFee = 0;
var orderEvent = new OrderEvent(order, DateTime.UtcNow, orderFee, "Interactive Brokers Fill Event")
{
Status = status,
FillPrice = update.AverageFillPrice,
+10 -6
View File
@@ -1139,7 +1139,8 @@ namespace QuantConnect.Brokerages.Tradier
{
TradierOrder tradierOrder;
_cachedOpenOrdersByTradierOrderID.TryRemove(orderID, out tradierOrder);
OnOrderEvent(new OrderEvent(order, "Tradier Fill Event"){Status = OrderStatus.Canceled});
const int orderFee = 0;
OnOrderEvent(new OrderEvent(order, DateTime.UtcNow, orderFee, "Tradier Fill Event") { Status = OrderStatus.Canceled });
}
}
@@ -1211,7 +1212,8 @@ namespace QuantConnect.Brokerages.Tradier
if (response != null && response.Errors.Errors.IsNullOrEmpty())
{
// send the submitted event
OnOrderEvent(new OrderEvent(order.QCOrder){Status = OrderStatus.Submitted});
const int orderFee = 0;
OnOrderEvent(new OrderEvent(order.QCOrder, DateTime.UtcNow, orderFee) { Status = OrderStatus.Submitted });
// mark this in our open orders before we submit so it's gauranteed to be there when we poll for updates
_cachedOpenOrdersByTradierOrderID.AddOrUpdate(response.Order.Id, new TradierOrderDetailed
@@ -1240,7 +1242,8 @@ namespace QuantConnect.Brokerages.Tradier
else
{
// invalidate the order, bad request
OnOrderEvent(new OrderEvent(order.QCOrder) {Status = OrderStatus.Invalid});
const int orderFee = 0;
OnOrderEvent(new OrderEvent(order.QCOrder, DateTime.UtcNow, orderFee) { Status = OrderStatus.Invalid });
string message = _previousResponseRaw;
if (response != null && response.Errors != null && !response.Errors.Errors.IsNullOrEmpty())
@@ -1436,7 +1439,8 @@ namespace QuantConnect.Brokerages.Tradier
|| ConvertStatus(updatedOrder.Status) != ConvertStatus(cachedOrder.Status))
{
var qcOrder = _orderProvider.GetOrderByBrokerageId((int)updatedOrder.Id);
var fill = new OrderEvent(qcOrder, "Tradier Fill Event")
const int orderFee = 0;
var fill = new OrderEvent(qcOrder, DateTime.UtcNow, orderFee, "Tradier Fill Event")
{
Status = ConvertStatus(updatedOrder.Status),
// this is guaranteed to be wrong in the event we have multiple fills within our polling interval,
@@ -1492,14 +1496,14 @@ namespace QuantConnect.Brokerages.Tradier
Log.Error("TradierBrokerage.SubmitContingentOrder(): Failed to submit contingent order.");
var message = string.Format("{0} Failed submitting contingent order for QC id: {1} Filled Tradier Order id: {2}", qcOrder.Symbol, qcOrder.Id, updatedOrder.Id);
OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "ContingentOrderFailed", message));
OnOrderEvent(new OrderEvent(qcOrder) {Status = OrderStatus.Canceled});
OnOrderEvent(new OrderEvent(qcOrder, DateTime.UtcNow, orderFee) { Status = OrderStatus.Canceled });
}
}
catch (Exception err)
{
Log.Error(err);
OnMessage(new BrokerageMessageEvent(BrokerageMessageType.Warning, "ContingentOrderError", "An error ocurred while trying to submit an Tradier contingent order: " + err.Message));
OnOrderEvent(new OrderEvent(qcOrder) {Status = OrderStatus.Canceled});
OnOrderEvent(new OrderEvent(qcOrder, DateTime.UtcNow, orderFee) { Status = OrderStatus.Canceled });
}
finally
{
+9
View File
@@ -229,6 +229,15 @@ namespace QuantConnect
return value;
}
/// <summary>
/// Check if a number is NaN or equal to zero
/// </summary>
/// <param name="value">The double value to check</param>
public static bool IsNaNOrZero(this double value)
{
return double.IsNaN(value) || Math.Abs(value) < double.Epsilon;
}
/// <summary>
/// Gets the smallest positive number that can be added to a decimal instance and return
/// a new value that does not == the old value
+39 -11
View File
@@ -32,11 +32,21 @@ namespace QuantConnect.Orders
/// </summary>
public Symbol Symbol;
/// <summary>
/// The date and time of this event (UTC).
/// </summary>
public DateTime UtcTime;
/// <summary>
/// Status message of the order.
/// </summary>
public OrderStatus Status;
/// <summary>
/// The fee associated with the order (always positive value).
/// </summary>
public decimal OrderFee;
/// <summary>
/// Fill price information about the order
/// </summary>
@@ -72,42 +82,51 @@ namespace QuantConnect.Orders
public string Message;
/// <summary>
/// Order Constructor.
/// Order Event Constructor.
/// </summary>
/// <param name="id">Id of the parent order</param>
/// <param name="orderId">Id of the parent order</param>
/// <param name="symbol">Asset Symbol</param>
/// <param name="utcTime">Date/time of this event</param>
/// <param name="status">Status of the order</param>
/// <param name="direction">The direction of the order this event belongs to</param>
/// <param name="fillPrice">Fill price information if applicable.</param>
/// <param name="fillQuantity">Fill quantity</param>
/// <param name="orderFee">The order fee</param>
/// <param name="message">Message from the exchange</param>
public OrderEvent(int id, Symbol symbol, OrderStatus status, OrderDirection direction, decimal fillPrice, int fillQuantity, string message = "")
public OrderEvent(int orderId, Symbol symbol, DateTime utcTime, OrderStatus status, OrderDirection direction, decimal fillPrice, int fillQuantity, decimal orderFee, string message = "")
{
OrderId = id;
Status = status;
FillPrice = fillPrice;
Message = message;
FillQuantity = fillQuantity;
OrderId = orderId;
Symbol = symbol;
UtcTime = utcTime;
Status = status;
Direction = direction;
FillPrice = fillPrice;
FillQuantity = fillQuantity;
OrderFee = orderFee;
Message = message;
}
/// <summary>
/// Helper Constructor using Order to Initialize.
/// </summary>
/// <param name="order">Order for this order status</param>
/// <param name="utcTime">Date/time of this event</param>
/// <param name="orderFee">The order fee</param>
/// <param name="message">Message from exchange or QC.</param>
public OrderEvent(Order order, string message = "")
public OrderEvent(Order order, DateTime utcTime, decimal orderFee, string message = "")
{
OrderId = order.Id;
Status = order.Status;
Message = message;
Symbol = order.Symbol;
Status = order.Status;
Direction = order.Direction;
//Initialize to zero, manually set fill quantity
FillQuantity = 0;
FillPrice = 0;
UtcTime = utcTime;
OrderFee = orderFee;
Message = message;
}
/// <summary>
@@ -123,6 +142,15 @@ namespace QuantConnect.Orders
? string.Format("OrderID: {0} Symbol: {1} Status: {2}", OrderId, Symbol, Status)
: string.Format("OrderID: {0} Symbol: {1} Status: {2} Quantity: {3} FillPrice: {4}", OrderId, Symbol, Status, FillQuantity, FillPrice);
}
/// <summary>
/// Returns a clone of the current object.
/// </summary>
/// <returns>The new clone object</returns>
public OrderEvent Clone()
{
return (OrderEvent)MemberwiseClone();
}
}
} // End QC Namespace:
+8
View File
@@ -267,9 +267,17 @@
<Compile Include="Securities\SecurityPortfolioManager.cs" />
<Compile Include="Securities\SecurityTransactionManager.cs" />
<Compile Include="OS.cs" />
<Compile Include="Statistics\AlgorithmPerformance.cs" />
<Compile Include="Statistics\PortfolioStatistics.cs" />
<Compile Include="Statistics\StatisticsBuilder.cs" />
<Compile Include="Statistics\StatisticsResults.cs" />
<Compile Include="Statistics\TradeStatistics.cs" />
<Compile Include="Statistics\Statistics.cs" />
<Compile Include="RealTimeSynchronizedTimer.cs" />
<Compile Include="Symbol.cs" />
<Compile Include="Statistics\Trade.cs" />
<Compile Include="Statistics\TradeBuilder.cs" />
<Compile Include="Statistics\TradeEnums.cs" />
<Compile Include="Time.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="TimeKeeper.cs" />
@@ -62,7 +62,7 @@ namespace QuantConnect.Securities.Interfaces
/// </summary>
/// <param name="asset">Asset we're trading with this order</param>
/// <param name="order">Order to be filled</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
OrderEvent MarketOnOpenFill(Security asset, MarketOnOpenOrder order);
/// <summary>
@@ -70,7 +70,7 @@ namespace QuantConnect.Securities.Interfaces
/// </summary>
/// <param name="asset">Asset we're trading with this order</param>
/// <param name="order">Order to be filled</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
OrderEvent MarketOnCloseFill(Security asset, MarketOnCloseOrder order);
/// <summary>
+31 -17
View File
@@ -37,13 +37,15 @@ namespace QuantConnect.Securities
/// </summary>
/// <param name="asset">Security asset we're filling</param>
/// <param name="order">Order packet to model</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
/// <seealso cref="StopMarketFill(Security, StopMarketOrder)"/>
/// <seealso cref="LimitFill(Security, LimitOrder)"/>
public virtual OrderEvent MarketFill(Security asset, MarketOrder order)
{
//Default order event to return.
var fill = new OrderEvent(order);
var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
var orderFee = GetOrderFee(asset, order);
var fill = new OrderEvent(order, utcTime, orderFee);
if (order.Status == OrderStatus.Canceled) return fill;
@@ -88,13 +90,15 @@ namespace QuantConnect.Securities
/// </summary>
/// <param name="asset">Security asset we're filling</param>
/// <param name="order">Order packet to model</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
/// <seealso cref="MarketFill(Security, MarketOrder)"/>
/// <seealso cref="LimitFill(Security, LimitOrder)"/>
public virtual OrderEvent StopMarketFill(Security asset, StopMarketOrder order)
{
//Default order event to return.
var fill = new OrderEvent(order);
var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
var orderFee = GetOrderFee(asset, order);
var fill = new OrderEvent(order, utcTime, orderFee);
// make sure the exchange is open before filling
if (!IsExchangeOpen(asset)) return fill;
@@ -156,7 +160,7 @@ namespace QuantConnect.Securities
/// </summary>
/// <param name="asset">Security asset we're filling</param>
/// <param name="order">Order packet to model</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
/// <seealso cref="StopMarketFill(Security, StopMarketOrder)"/>
/// <seealso cref="LimitFill(Security, LimitOrder)"/>
/// <remarks>
@@ -169,7 +173,9 @@ namespace QuantConnect.Securities
public virtual OrderEvent StopLimitFill(Security asset, StopLimitOrder order)
{
//Default order event to return.
var fill = new OrderEvent(order);
var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
var orderFee = GetOrderFee(asset, order);
var fill = new OrderEvent(order, utcTime, orderFee);
try
{
@@ -237,13 +243,15 @@ namespace QuantConnect.Securities
/// </summary>
/// <param name="asset">Security asset we're filling</param>
/// <param name="order">Order packet to model</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
/// <seealso cref="StopMarketFill(Security, StopMarketOrder)"/>
/// <seealso cref="MarketFill(Security, MarketOrder)"/>
public virtual OrderEvent LimitFill(Security asset, LimitOrder order)
{
//Initialise;
var fill = new OrderEvent(order);
var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
var orderFee = GetOrderFee(asset, order);
var fill = new OrderEvent(order, utcTime, orderFee);
try
{
@@ -301,10 +309,12 @@ namespace QuantConnect.Securities
/// </summary>
/// <param name="asset">Asset we're trading with this order</param>
/// <param name="order">Order to be filled</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
public OrderEvent MarketOnOpenFill(Security asset, MarketOnOpenOrder order)
{
var fill = new OrderEvent(order);
var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
var orderFee = GetOrderFee(asset, order);
var fill = new OrderEvent(order, utcTime, orderFee);
if (order.Status == OrderStatus.Canceled) return fill;
@@ -365,10 +375,12 @@ namespace QuantConnect.Securities
/// </summary>
/// <param name="asset">Asset we're trading with this order</param>
/// <param name="order">Order to be filled</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
public OrderEvent MarketOnCloseFill(Security asset, MarketOnCloseOrder order)
{
var fill = new OrderEvent(order);
var utcTime = asset.LocalTime.ConvertToUtc(asset.Exchange.TimeZone);
var orderFee = GetOrderFee(asset, order);
var fill = new OrderEvent(order, utcTime, orderFee);
if (order.Status == OrderStatus.Canceled) return fill;
@@ -494,11 +506,13 @@ namespace QuantConnect.Securities
/// </summary>
/// <param name="vehicle">Asset we're working with</param>
/// <param name="order">Order class to check if filled.</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
[Obsolete("Fill method has been made obsolete, use order type fill methods directly.")]
public virtual OrderEvent Fill(Security vehicle, Order order)
{
return new OrderEvent(order);
var utcTime = vehicle.LocalTime.ConvertToUtc(vehicle.Exchange.TimeZone);
var orderFee = GetOrderFee(vehicle, order);
return new OrderEvent(order, utcTime, orderFee);
}
/// <summary>
@@ -506,7 +520,7 @@ namespace QuantConnect.Securities
/// </summary>
/// <param name="security">Security asset we're filling</param>
/// <param name="order">Order packet to model</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
/// <seealso cref="StopMarketFill(Security, StopMarketOrder)"/>
/// <seealso cref="LimitFill(Security, LimitOrder)"/>
[Obsolete("MarketFill(Security, Order) method has been made obsolete, use MarketFill(Security, MarketOrder) method instead.")]
@@ -520,7 +534,7 @@ namespace QuantConnect.Securities
/// </summary>
/// <param name="security">Security asset we're filling</param>
/// <param name="order">Order packet to model</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
/// <seealso cref="LimitFill(Security, LimitOrder)"/>
/// <seealso cref="MarketFill(Security, MarketOrder)"/>
[Obsolete("StopFill(Security, Order) method has been made obsolete, use StopMarketFill(Security, StopMarketOrder) method instead.")]
@@ -534,7 +548,7 @@ namespace QuantConnect.Securities
/// </summary>
/// <param name="security">Security asset we're filling</param>
/// <param name="order">Order packet to model</param>
/// <returns>Order fill informaton detailing the average price and quantity filled.</returns>
/// <returns>Order fill information detailing the average price and quantity filled.</returns>
/// <seealso cref="StopMarketFill(Security, StopMarketOrder)"/>
/// <seealso cref="MarketFill(Security, MarketOrder)"/>
[Obsolete("LimitFill(Security, Order) method has been made obsolete, use LimitFill(Security, LimitOrder) method instead.")]
+74
View File
@@ -0,0 +1,74 @@
/*
* 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.Generic;
namespace QuantConnect.Statistics
{
/// <summary>
/// The <see cref="AlgorithmPerformance"/> class is a wrapper for <see cref="TradeStatistics"/> and <see cref="PortfolioStatistics"/>
/// </summary>
public class AlgorithmPerformance
{
/// <summary>
/// The algorithm statistics on closed trades
/// </summary>
public TradeStatistics TradeStatistics { get; private set; }
/// <summary>
/// The algorithm statistics on portfolio
/// </summary>
public PortfolioStatistics PortfolioStatistics { get; private set; }
/// <summary>
/// The list of closed trades
/// </summary>
public List<Trade> ClosedTrades { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="AlgorithmPerformance"/> class
/// </summary>
/// <param name="trades">The list of closed trades</param>
/// <param name="profitLoss">Trade record of profits and losses</param>
/// <param name="equity">The list of daily equity values</param>
/// <param name="listPerformance">The list of algorithm performance values</param>
/// <param name="listBenchmark">The list of benchmark values</param>
/// <param name="startingCapital">The algorithm starting capital</param>
public AlgorithmPerformance(
List<Trade> trades,
SortedDictionary<DateTime, decimal> profitLoss,
SortedDictionary<DateTime, decimal> equity,
List<double> listPerformance,
List<double> listBenchmark,
decimal startingCapital)
{
TradeStatistics = new TradeStatistics(trades);
PortfolioStatistics = new PortfolioStatistics(profitLoss, equity, listPerformance, listBenchmark, startingCapital);
ClosedTrades = trades;
}
/// <summary>
/// Initializes a new instance of the <see cref="AlgorithmPerformance"/> class
/// </summary>
public AlgorithmPerformance()
{
TradeStatistics = new TradeStatistics();
PortfolioStatistics = new PortfolioStatistics();
ClosedTrades = new List<Trade>();
}
}
}
+271
View File
@@ -0,0 +1,271 @@
/*
* 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.Generic;
using System.Linq;
using MathNet.Numerics.Statistics;
namespace QuantConnect.Statistics
{
/// <summary>
/// The <see cref="PortfolioStatistics"/> class represents a set of statistics calculated from equity and benchmark samples
/// </summary>
public class PortfolioStatistics
{
private const decimal RiskFreeRate = 0;
/// <summary>
/// The average rate of return for winning trades
/// </summary>
public decimal AverageWinRate { get; private set; }
/// <summary>
/// The average rate of return for losing trades
/// </summary>
public decimal AverageLossRate { get; private set; }
/// <summary>
/// The ratio of the average win rate to the average loss rate
/// </summary>
/// <remarks>If the average loss rate is zero, ProfitLossRatio is set to 0</remarks>
public decimal ProfitLossRatio { get; private set; }
/// <summary>
/// The ratio of the number of winning trades to the total number of trades
/// </summary>
/// <remarks>If the total number of trades is zero, WinRate is set to zero</remarks>
public decimal WinRate { get; private set; }
/// <summary>
/// The ratio of the number of losing trades to the total number of trades
/// </summary>
/// <remarks>If the total number of trades is zero, LossRate is set to zero</remarks>
public decimal LossRate { get; private set; }
/// <summary>
/// The expected value of the rate of return
/// </summary>
public decimal Expectancy { get; private set; }
/// <summary>
/// Annual compounded returns statistic based on the final-starting capital and years.
/// </summary>
/// <remarks>Also known as Compound Annual Growth Rate (CAGR)</remarks>
public decimal CompoundingAnnualReturn { get; private set; }
/// <summary>
/// Drawdown maximum percentage.
/// </summary>
public decimal Drawdown { get; private set; }
/// <summary>
/// The total net profit percentage.
/// </summary>
public decimal TotalNetProfit { get; private set; }
/// <summary>
/// Sharpe ratio with respect to risk free rate: measures excess of return per unit of risk.
/// </summary>
/// <remarks>With risk defined as the algorithm's volatility</remarks>
public decimal SharpeRatio { get; private set; }
/// <summary>
/// Algorithm "Alpha" statistic - abnormal returns over the risk free rate and the relationshio (beta) with the benchmark returns.
/// </summary>
public decimal Alpha { get; private set; }
/// <summary>
/// Algorithm "beta" statistic - the covariance between the algorithm and benchmark performance, divided by benchmark's variance
/// </summary>
public decimal Beta { get; private set; }
/// <summary>
/// Annualized standard deviation
/// </summary>
public decimal AnnualStandardDeviation { get; private set; }
/// <summary>
/// Annualized variance statistic calculation using the daily performance variance and trading days per year.
/// </summary>
public decimal AnnualVariance { get; private set; }
/// <summary>
/// Information ratio - risk adjusted return
/// </summary>
/// <remarks>(risk = tracking error volatility, a volatility measures that considers the volatility of both algo and benchmark)</remarks>
public decimal InformationRatio { get; private set; }
/// <summary>
/// Tracking error volatility (TEV) statistic - a measure of how closely a portfolio follows the index to which it is benchmarked
/// </summary>
/// <remarks>If algo = benchmark, TEV = 0</remarks>
public decimal TrackingError { get; private set; }
/// <summary>
/// Treynor ratio statistic is a measurement of the returns earned in excess of that which could have been earned on an investment that has no diversifiable risk
/// </summary>
public decimal TreynorRatio { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="PortfolioStatistics"/> class
/// </summary>
/// <param name="profitLoss">Trade record of profits and losses</param>
/// <param name="equity">The list of daily equity values</param>
/// <param name="listPerformance">The list of algorithm performance values</param>
/// <param name="listBenchmark">The list of benchmark values</param>
/// <param name="startingCapital">The algorithm starting capital</param>
/// <param name="tradingDaysPerYear">The number of trading days per year</param>
public PortfolioStatistics(
SortedDictionary<DateTime, decimal> profitLoss,
SortedDictionary<DateTime, decimal> equity,
List<double> listPerformance,
List<double> listBenchmark,
decimal startingCapital,
int tradingDaysPerYear = 252)
{
if (startingCapital == 0) return;
var runningCapital = startingCapital;
var totalProfit = 0m;
var totalLoss = 0m;
var totalWins = 0;
var totalLosses = 0;
foreach (var pair in profitLoss)
{
var tradeProfitLoss = pair.Value;
if (tradeProfitLoss > 0)
{
totalProfit += tradeProfitLoss / runningCapital;
totalWins++;
}
else
{
totalLoss += tradeProfitLoss / runningCapital;
totalLosses++;
}
runningCapital += tradeProfitLoss;
}
AverageWinRate = totalWins == 0 ? 0 : totalProfit / totalWins;
AverageLossRate = totalLosses == 0 ? 0 : totalLoss / totalLosses;
ProfitLossRatio = AverageLossRate == 0 ? 0 : AverageWinRate / Math.Abs(AverageLossRate);
WinRate = profitLoss.Count == 0 ? 0 : (decimal)totalWins / profitLoss.Count;
LossRate = profitLoss.Count == 0 ? 0 : (decimal)totalLosses / profitLoss.Count;
Expectancy = WinRate * ProfitLossRatio - LossRate;
if (profitLoss.Count > 0)
{
TotalNetProfit = (equity.Values.LastOrDefault() / startingCapital) - 1;
}
var fractionOfYears = (decimal)(equity.Keys.LastOrDefault() - equity.Keys.FirstOrDefault()).TotalDays / 365;
CompoundingAnnualReturn = CompoundingAnnualPerformance(startingCapital, equity.Values.LastOrDefault(), fractionOfYears);
Drawdown = DrawdownPercent(equity, 3);
AnnualVariance = GetAnnualVariance(listPerformance, tradingDaysPerYear);
AnnualStandardDeviation = (decimal)Math.Sqrt((double)AnnualVariance);
var annualPerformance = GetAnnualPerformance(listPerformance, tradingDaysPerYear);
SharpeRatio = AnnualStandardDeviation == 0 ? 0 : (annualPerformance - RiskFreeRate) / AnnualStandardDeviation;
var benchmarkVariance = listBenchmark.Variance();
Beta = benchmarkVariance.IsNaNOrZero() ? 0 : (decimal)(listPerformance.Covariance(listBenchmark) / benchmarkVariance);
Alpha = Beta == 0 ? 0 : annualPerformance - (RiskFreeRate + Beta * (GetAnnualPerformance(listBenchmark, tradingDaysPerYear) - RiskFreeRate));
var correlation = Correlation.Pearson(listPerformance, listBenchmark);
var benchmarkAnnualVariance = benchmarkVariance * tradingDaysPerYear;
TrackingError = correlation.IsNaNOrZero() || benchmarkAnnualVariance.IsNaNOrZero() ? 0 :
(decimal)Math.Sqrt((double)AnnualVariance - 2 * correlation * (double)AnnualStandardDeviation * Math.Sqrt(benchmarkAnnualVariance) + benchmarkAnnualVariance);
InformationRatio = TrackingError == 0 ? 0 : (annualPerformance - GetAnnualPerformance(listBenchmark, tradingDaysPerYear)) / TrackingError;
TreynorRatio = Beta == 0 ? 0 : (annualPerformance - RiskFreeRate) / Beta;
}
/// <summary>
/// Initializes a new instance of the <see cref="PortfolioStatistics"/> class
/// </summary>
public PortfolioStatistics()
{
}
/// <summary>
/// Annual compounded returns statistic based on the final-starting capital and years.
/// </summary>
/// <param name="startingCapital">Algorithm starting capital</param>
/// <param name="finalCapital">Algorithm final capital</param>
/// <param name="years">Years trading</param>
/// <returns>Decimal fraction for annual compounding performance</returns>
private static decimal CompoundingAnnualPerformance(decimal startingCapital, decimal finalCapital, decimal years)
{
return (decimal)Math.Pow((double)finalCapital / (double)startingCapital, (1 / (double)years)) - 1;
}
/// <summary>
/// Drawdown maximum percentage.
/// </summary>
/// <param name="equityOverTime">The list of daily equity values</param>
/// <param name="rounding">The number of decimal places to round the result</param>
/// <returns>The drawdown percentage</returns>
private static decimal DrawdownPercent(SortedDictionary<DateTime, decimal> equityOverTime, int rounding = 2)
{
var prices = equityOverTime.Values.ToList();
if (prices.Count == 0) return 0;
var drawdowns = new List<decimal>();
var high = prices[0];
foreach (var price in prices)
{
if (price > high) high = price;
if (high > 0) drawdowns.Add(price / high - 1);
}
return Math.Round(Math.Abs(drawdowns.Min()), rounding);
}
/// <summary>
/// Annualized return statistic calculated as an average of daily trading performance multiplied by the number of trading days per year.
/// </summary>
/// <param name="performance">Dictionary collection of double performance values</param>
/// <param name="tradingDaysPerYear">Trading days per year for the assets in portfolio</param>
/// <remarks>May be unaccurate for forex algorithms with more trading days in a year</remarks>
/// <returns>Double annual performance percentage</returns>
private static decimal GetAnnualPerformance(List<double> performance, int tradingDaysPerYear = 252)
{
return (decimal)performance.Average() * tradingDaysPerYear;
}
/// <summary>
/// Annualized variance statistic calculation using the daily performance variance and trading days per year.
/// </summary>
/// <param name="performance"></param>
/// <param name="tradingDaysPerYear"></param>
/// <remarks>Invokes the variance extension in the MathNet Statistics class</remarks>
/// <returns>Annual variance value</returns>
private static decimal GetAnnualVariance(List<double> performance, int tradingDaysPerYear = 252)
{
var variance = performance.Variance();
return variance.IsNaNOrZero() ? 0 : (decimal)variance * tradingDaysPerYear;
}
}
}
+300
View File
@@ -0,0 +1,300 @@
/*
* 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.Generic;
using System.Globalization;
using System.Linq;
using QuantConnect.Logging;
namespace QuantConnect.Statistics
{
/// <summary>
/// The <see cref="StatisticsBuilder"/> class creates summary and rolling statistics from trades, equity and benchmark points
/// </summary>
public static class StatisticsBuilder
{
/// <summary>
/// Generates the statistics and returns the results
/// </summary>
/// <param name="trades">The list of closed trades</param>
/// <param name="profitLoss">Trade record of profits and losses</param>
/// <param name="pointsEquity">The list of daily equity values</param>
/// <param name="pointsPerformance">The list of algorithm performance values</param>
/// <param name="pointsBenchmark">The list of benchmark values</param>
/// <param name="startingCapital">The algorithm starting capital</param>
/// <param name="totalFees">The total fees</param>
/// <param name="totalTransactions">The total number of transactions</param>
/// <returns>Returns a <see cref="StatisticsResults"/> object</returns>
public static StatisticsResults Generate(
List<Trade> trades,
SortedDictionary<DateTime, decimal> profitLoss,
List<ChartPoint> pointsEquity,
List<ChartPoint> pointsPerformance,
List<ChartPoint> pointsBenchmark,
decimal startingCapital,
decimal totalFees,
int totalTransactions)
{
var equity = ChartPointToDictionary(pointsEquity);
var firstDate = equity.Keys.FirstOrDefault().Date;
var lastDate = equity.Keys.LastOrDefault().Date;
var totalPerformance = GetAlgorithmPerformance(firstDate, lastDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark, startingCapital);
var rollingPerformances = GetRollingPerformances(firstDate, lastDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark, startingCapital);
var summary = GetSummary(totalPerformance, totalFees, totalTransactions);
return new StatisticsResults(totalPerformance, rollingPerformances, summary);
}
/// <summary>
/// Returns the performance of the algorithm in the specified date range
/// </summary>
/// <param name="fromDate">The initial date of the range</param>
/// <param name="toDate">The final date of the range</param>
/// <param name="trades">The list of closed trades</param>
/// <param name="profitLoss">Trade record of profits and losses</param>
/// <param name="equity">The list of daily equity values</param>
/// <param name="pointsPerformance">The list of algorithm performance values</param>
/// <param name="pointsBenchmark">The list of benchmark values</param>
/// <param name="startingCapital">The algorithm starting capital</param>
/// <returns>The algorithm performance</returns>
private static AlgorithmPerformance GetAlgorithmPerformance(
DateTime fromDate,
DateTime toDate,
List<Trade> trades,
SortedDictionary<DateTime, decimal> profitLoss,
SortedDictionary<DateTime, decimal> equity,
List<ChartPoint> pointsPerformance,
List<ChartPoint> pointsBenchmark,
decimal startingCapital)
{
var periodTrades = trades.Where(x => x.ExitTime.Date >= fromDate && x.ExitTime < toDate.AddDays(1)).ToList();
var periodProfitLoss = new SortedDictionary<DateTime, decimal>(profitLoss.Where(x => x.Key >= fromDate && x.Key.Date < toDate.AddDays(1)).ToDictionary(x => x.Key, y => y.Value));
var periodEquity = new SortedDictionary<DateTime, decimal>(equity.Where(x => x.Key.Date >= fromDate && x.Key.Date < toDate.AddDays(1)).ToDictionary(x => x.Key, y => y.Value));
var listPerformance = new List<double>();
var performance = ChartPointToDictionary(pointsPerformance, fromDate, toDate);
performance.Values.ToList().ForEach(i => listPerformance.Add((double)(i / 100)));
var benchmark = ChartPointToDictionary(pointsBenchmark, fromDate, toDate);
var listBenchmark = CreateBenchmarkDifferences(benchmark, periodEquity);
EnsureSameLength(listPerformance, listBenchmark);
var runningCapital = equity.Count == periodEquity.Count ? startingCapital : periodEquity.Values.FirstOrDefault();
return new AlgorithmPerformance(periodTrades, periodProfitLoss, periodEquity, listPerformance, listBenchmark, runningCapital);
}
/// <summary>
/// Returns the rolling performances of the algorithm
/// </summary>
/// <param name="firstDate">The first date of the total period</param>
/// <param name="lastDate">The last date of the total period</param>
/// <param name="trades">The list of closed trades</param>
/// <param name="profitLoss">Trade record of profits and losses</param>
/// <param name="equity">The list of daily equity values</param>
/// <param name="pointsPerformance">The list of algorithm performance values</param>
/// <param name="pointsBenchmark">The list of benchmark values</param>
/// <param name="startingCapital">The algorithm starting capital</param>
/// <returns>A dictionary with the rolling performances</returns>
private static Dictionary<string, AlgorithmPerformance> GetRollingPerformances(
DateTime firstDate,
DateTime lastDate,
List<Trade> trades,
SortedDictionary<DateTime, decimal> profitLoss,
SortedDictionary<DateTime, decimal> equity,
List<ChartPoint> pointsPerformance,
List<ChartPoint> pointsBenchmark,
decimal startingCapital)
{
var rollingPerformances = new Dictionary<string, AlgorithmPerformance>();
var monthPeriods = new[] { 1, 3, 6, 12 };
foreach (var monthPeriod in monthPeriods)
{
var ranges = GetPeriodRanges(monthPeriod, firstDate, lastDate);
foreach (var period in ranges)
{
var key = "M" + monthPeriod + "_" + period.EndDate.ToString("yyyyMMdd");
var periodPerformance = GetAlgorithmPerformance(period.StartDate, period.EndDate, trades, profitLoss, equity, pointsPerformance, pointsBenchmark, startingCapital);
rollingPerformances[key] = periodPerformance;
}
}
return rollingPerformances;
}
/// <summary>
/// Returns a summary of the algorithm performance as a dictionary
/// </summary>
private static Dictionary<string, string> GetSummary(AlgorithmPerformance totalPerformance, decimal totalFees, int totalTransactions)
{
return new Dictionary<string, string>
{
{ "Total Trades", totalTransactions.ToString(CultureInfo.InvariantCulture) },
{ "Average Win", Math.Round(totalPerformance.PortfolioStatistics.AverageWinRate * 100, 2) + "%" },
{ "Average Loss", Math.Round(totalPerformance.PortfolioStatistics.AverageLossRate * 100, 2) + "%" },
{ "Compounding Annual Return", Math.Round(totalPerformance.PortfolioStatistics.CompoundingAnnualReturn * 100, 3) + "%" },
{ "Drawdown", (Math.Round(totalPerformance.PortfolioStatistics.Drawdown * 100, 3)) + "%" },
{ "Expectancy", Math.Round(totalPerformance.PortfolioStatistics.Expectancy, 3).ToString(CultureInfo.InvariantCulture) },
{ "Net Profit", Math.Round(totalPerformance.PortfolioStatistics.TotalNetProfit * 100, 3) + "%"},
{ "Sharpe Ratio", Math.Round((double)totalPerformance.PortfolioStatistics.SharpeRatio, 3).ToString(CultureInfo.InvariantCulture) },
{ "Loss Rate", Math.Round(totalPerformance.PortfolioStatistics.LossRate * 100) + "%" },
{ "Win Rate", Math.Round(totalPerformance.PortfolioStatistics.WinRate * 100) + "%" },
{ "Profit-Loss Ratio", Math.Round(totalPerformance.PortfolioStatistics.ProfitLossRatio, 2).ToString(CultureInfo.InvariantCulture) },
{ "Alpha", Math.Round((double)totalPerformance.PortfolioStatistics.Alpha, 3).ToString(CultureInfo.InvariantCulture) },
{ "Beta", Math.Round((double)totalPerformance.PortfolioStatistics.Beta, 3).ToString(CultureInfo.InvariantCulture) },
{ "Annual Standard Deviation", Math.Round((double)totalPerformance.PortfolioStatistics.AnnualStandardDeviation, 3).ToString(CultureInfo.InvariantCulture) },
{ "Annual Variance", Math.Round((double)totalPerformance.PortfolioStatistics.AnnualVariance, 3).ToString(CultureInfo.InvariantCulture) },
{ "Information Ratio", Math.Round((double)totalPerformance.PortfolioStatistics.InformationRatio, 3).ToString(CultureInfo.InvariantCulture) },
{ "Tracking Error", Math.Round((double)totalPerformance.PortfolioStatistics.TrackingError, 3).ToString(CultureInfo.InvariantCulture) },
{ "Treynor Ratio", Math.Round((double)totalPerformance.PortfolioStatistics.TreynorRatio, 3).ToString(CultureInfo.InvariantCulture) },
{ "Total Fees", "$" + totalFees.ToString("0.00") }
};
}
/// <summary>
/// Helper class for rolling statistics
/// </summary>
private class PeriodRange
{
internal DateTime StartDate { get; set; }
internal DateTime EndDate { get; set; }
}
//
/// <summary>
/// Gets a list of date ranges for the requested monthly period
/// </summary>
/// <remarks>The first and last ranges created are partial periods</remarks>
/// <param name="periodMonths">The number of months in the period (valid inputs are [1, 3, 6, 12])</param>
/// <param name="firstDate">The first date of the total period</param>
/// <param name="lastDate">The last date of the total period</param>
/// <returns>The list of date ranges</returns>
private static IEnumerable<PeriodRange> GetPeriodRanges(int periodMonths, DateTime firstDate, DateTime lastDate)
{
// get end dates
var date = lastDate.Date;
var endDates = new List<DateTime>();
do
{
endDates.Add(date);
date = new DateTime(date.Year, date.Month, 1).AddDays(-1);
} while (date >= firstDate);
// build period ranges
var ranges = new List<PeriodRange> { new PeriodRange { StartDate = firstDate, EndDate = endDates[endDates.Count - 1] } };
for (var i = endDates.Count - 2; i >= 0; i--)
{
var startDate = ranges[ranges.Count - 1].EndDate.AddDays(1).AddMonths(1 - periodMonths);
if (startDate < firstDate) startDate = firstDate;
ranges.Add(new PeriodRange
{
StartDate = startDate,
EndDate = endDates[i]
});
}
return ranges;
}
/// <summary>
/// Convert the charting data into an equity array.
/// </summary>
/// <remarks>This is required to convert the equity plot into a usable form for the statistics calculation</remarks>
/// <param name="points">ChartPoints Array</param>
/// <param name="fromDate">An optional starting date</param>
/// <param name="toDate">An optional ending date</param>
/// <returns>SortedDictionary of the equity decimal values ordered in time</returns>
private static SortedDictionary<DateTime, decimal> ChartPointToDictionary(IEnumerable<ChartPoint> points, DateTime? fromDate = null, DateTime? toDate = null)
{
var dictionary = new SortedDictionary<DateTime, decimal>();
foreach (var point in points)
{
var x = Time.UnixTimeStampToDateTime(point.x);
if (fromDate != null && x.Date < fromDate) continue;
if (toDate != null && x.Date >= ((DateTime)toDate).AddDays(1)) break;
dictionary[x] = point.y;
}
return dictionary;
}
/// <summary>
/// Creates a list of benchmark differences for the period
/// </summary>
/// <param name="benchmark">The benchmark values</param>
/// <param name="equity">The equity values</param>
/// <returns>The list of benchmark differences</returns>
private static List<double> CreateBenchmarkDifferences(SortedDictionary<DateTime, decimal> benchmark, SortedDictionary<DateTime, decimal> equity)
{
// to find the delta in benchmark for first day, we need to know the price at the opening
// moment of the day, but since we cannot find this, we cannot find the first benchmark's delta,
// so we pad it with Zero. If running a short backtest this will skew results, longer backtests
// will not be affected much
var listBenchmark = new List<double> { 0 };
// Get benchmark performance array for same period:
var dtPrevious = new DateTime();
benchmark.Keys.ToList().ForEach(dt =>
{
if (dt >= equity.Keys.FirstOrDefault().AddDays(-1) && dt < equity.Keys.LastOrDefault())
{
decimal previous;
if (benchmark.TryGetValue(dtPrevious, out previous) && previous != 0)
{
var deltaBenchmark = (benchmark[dt] - previous) / previous;
listBenchmark.Add((double)deltaBenchmark);
}
else
{
listBenchmark.Add(0);
}
dtPrevious = dt;
}
});
return listBenchmark;
}
/// <summary>
/// Ensures the performance list and benchmark list have the same length, padding with trailing zeros
/// </summary>
/// <param name="listPerformance">The performance list</param>
/// <param name="listBenchmark">The benchmark list</param>
private static void EnsureSameLength(List<double> listPerformance, List<double> listBenchmark)
{
// THIS SHOULD NEVER HAPPEN --> But if it does, log it and fail silently.
while (listPerformance.Count < listBenchmark.Count)
{
listPerformance.Add(0);
Log.Error("StatisticsBuilder.EnsureSameLength(): Padded Performance");
}
while (listPerformance.Count > listBenchmark.Count)
{
listBenchmark.Add(0);
Log.Error("StatisticsBuilder.EnsureSameLength(): Padded Benchmark");
}
}
}
}
+63
View File
@@ -0,0 +1,63 @@
/*
* 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.Collections.Generic;
namespace QuantConnect.Statistics
{
/// <summary>
/// The <see cref="StatisticsResults"/> class represents total and rolling statistics for an algorithm
/// </summary>
public class StatisticsResults
{
/// <summary>
/// The performance of the algorithm over the whole period
/// </summary>
public AlgorithmPerformance TotalPerformance { get; private set; }
/// <summary>
/// The rolling performance of the algorithm over 1, 3, 6, 12 month periods
/// </summary>
public Dictionary<string, AlgorithmPerformance> RollingPerformances { get; private set; }
/// <summary>
/// Returns a summary of the algorithm performance as a dictionary
/// </summary>
public Dictionary<string, string> Summary { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="StatisticsResults"/> class
/// </summary>
/// <param name="totalPerformance">The algorithm total performance</param>
/// <param name="rollingPerformances">The algorithm rolling performances</param>
/// <param name="summary">The summary performance dictionary</param>
public StatisticsResults(AlgorithmPerformance totalPerformance, Dictionary<string, AlgorithmPerformance> rollingPerformances, Dictionary<string, string> summary)
{
TotalPerformance = totalPerformance;
RollingPerformances = rollingPerformances;
Summary = summary;
}
/// <summary>
/// Initializes a new instance of the <see cref="StatisticsResults"/> class
/// </summary>
public StatisticsResults()
{
TotalPerformance = new AlgorithmPerformance();
RollingPerformances = new Dictionary<string, AlgorithmPerformance>();
Summary = new Dictionary<string, string>();
}
}
}
+97
View File
@@ -0,0 +1,97 @@
/*
* 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;
namespace QuantConnect.Statistics
{
/// <summary>
/// Represents a closed trade
/// </summary>
public class Trade
{
/// <summary>
/// The symbol of the traded instrument
/// </summary>
public string Symbol { get; set; }
/// <summary>
/// The date and time the trade was opened
/// </summary>
public DateTime EntryTime { get; set; }
/// <summary>
/// The price at which the trade was opened (or the average price if multiple entries)
/// </summary>
public decimal EntryPrice { get; set; }
/// <summary>
/// The direction of the trade (Long or Short)
/// </summary>
public TradeDirection Direction { get; set; }
/// <summary>
/// The total unsigned quantity of the trade
/// </summary>
public int Quantity { get; set; }
/// <summary>
/// The date and time the trade was closed
/// </summary>
public DateTime ExitTime { get; set; }
/// <summary>
/// The price at which the trade was closed (or the average price if multiple exits)
/// </summary>
public decimal ExitPrice { get; set; }
/// <summary>
/// The gross profit/loss of the trade (as symbol currency)
/// </summary>
public decimal ProfitLoss { get; set; }
/// <summary>
/// The total fees associated with the trade (always positive value) (as symbol currency)
/// </summary>
public decimal TotalFees { get; set; }
/// <summary>
/// The Maximum Adverse Excursion (as symbol currency)
/// </summary>
public decimal MAE { get; set; }
/// <summary>
/// The Maximum Favorable Excursion (as symbol currency)
/// </summary>
public decimal MFE { get; set; }
/// <summary>
/// Returns the duration of the trade
/// </summary>
public TimeSpan Duration
{
get { return ExitTime - EntryTime; }
}
/// <summary>
/// Returns the amount of profit given back before the trade was closed
/// </summary>
public decimal EndTradeDrawdown
{
get { return ProfitLoss - MFE; }
}
}
}
+488
View File
@@ -0,0 +1,488 @@
/*
* 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.Generic;
using System.Linq;
using QuantConnect.Orders;
using QuantConnect.Util;
namespace QuantConnect.Statistics
{
/// <summary>
/// The <see cref="TradeBuilder"/> class generates trades from executions and market price updates
/// </summary>
public class TradeBuilder
{
/// <summary>
/// Helper class to manage pending trades and market price updates for a symbol
/// </summary>
private class Position
{
internal List<Trade> PendingTrades { get; set; }
internal List<OrderEvent> PendingFills { get; set; }
internal decimal TotalFees { get; set; }
internal decimal MaxPrice { get; set; }
internal decimal MinPrice { get; set; }
public Position()
{
PendingTrades = new List<Trade>();
PendingFills = new List<OrderEvent>();
}
}
private const int LiveModeMaxTradeCount = 10000;
private const int LiveModeMaxTradeAgeMonths = 12;
private const int MaxOrderIdCacheSize = 1000;
private readonly List<Trade> _closedTrades = new List<Trade>();
private readonly Dictionary<Symbol, Position> _positions = new Dictionary<Symbol, Position>();
private readonly FixedSizeHashQueue<int> _ordersWithFeesAssigned = new FixedSizeHashQueue<int>(MaxOrderIdCacheSize);
private readonly FillGroupingMethod _groupingMethod;
private readonly FillMatchingMethod _matchingMethod;
private bool _liveMode;
/// <summary>
/// Initializes a new instance of the <see cref="TradeBuilder"/> class
/// </summary>
public TradeBuilder(FillGroupingMethod groupingMethod, FillMatchingMethod matchingMethod)
{
_groupingMethod = groupingMethod;
_matchingMethod = matchingMethod;
}
/// <summary>
/// Sets the live mode flag
/// </summary>
/// <param name="live">The live mode flag</param>
public void SetLiveMode(bool live)
{
_liveMode = live;
}
/// <summary>
/// The list of closed trades
/// </summary>
public List<Trade> ClosedTrades
{
get { return _closedTrades; }
}
/// <summary>
/// Returns true if there is an open position for the symbol
/// </summary>
/// <param name="symbol">The symbol</param>
/// <returns>true if there is an open position for the symbol</returns>
public bool HasOpenPosition(Symbol symbol)
{
Position position;
if (!_positions.TryGetValue(symbol, out position)) return false;
if (_groupingMethod == FillGroupingMethod.FillToFill)
return position.PendingTrades.Count > 0;
return position.PendingFills.Count > 0;
}
/// <summary>
/// Sets the current market price for the symbol
/// </summary>
/// <param name="symbol"></param>
/// <param name="price"></param>
public void SetMarketPrice(Symbol symbol, decimal price)
{
Position position;
if (!_positions.TryGetValue(symbol, out position)) return;
if (price > position.MaxPrice)
position.MaxPrice = price;
else if (price < position.MinPrice)
position.MinPrice = price;
}
/// <summary>
/// Processes a new fill, eventually creating new trades
/// </summary>
/// <param name="fill">The new fill order event</param>
/// <param name="conversionRate">The current market conversion rate into the account currency</param>
public void ProcessFill(OrderEvent fill, decimal conversionRate)
{
// If we have multiple fills per order, we assign the order fee only to its first fill
// to avoid counting the same order fee multiple times.
var orderFee = 0m;
if (!_ordersWithFeesAssigned.Contains(fill.OrderId))
{
orderFee = fill.OrderFee;
_ordersWithFeesAssigned.Add(fill.OrderId);
}
switch (_groupingMethod)
{
case FillGroupingMethod.FillToFill:
ProcessFillUsingFillToFill(fill.Clone(), orderFee, conversionRate);
break;
case FillGroupingMethod.FlatToFlat:
ProcessFillUsingFlatToFlat(fill.Clone(), orderFee, conversionRate);
break;
case FillGroupingMethod.FlatToReduced:
ProcessFillUsingFlatToReduced(fill.Clone(), orderFee, conversionRate);
break;
}
}
private void ProcessFillUsingFillToFill(OrderEvent fill, decimal orderFee, decimal conversionRate)
{
Position position;
if (!_positions.TryGetValue(fill.Symbol, out position) || position.PendingTrades.Count == 0)
{
// no pending trades for symbol
_positions[fill.Symbol] = new Position
{
PendingTrades = new List<Trade>
{
new Trade
{
Symbol = fill.Symbol,
EntryTime = fill.UtcTime,
EntryPrice = fill.FillPrice,
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
Quantity = fill.AbsoluteFillQuantity,
TotalFees = orderFee
}
},
MinPrice = fill.FillPrice,
MaxPrice = fill.FillPrice
};
return;
}
SetMarketPrice(fill.Symbol, fill.FillPrice);
var index = _matchingMethod == FillMatchingMethod.FIFO ? 0 : position.PendingTrades.Count - 1;
if (Math.Sign(fill.FillQuantity) == (position.PendingTrades[index].Direction == TradeDirection.Long ? +1 : -1))
{
// execution has same direction of trade
position.PendingTrades.Add(new Trade
{
Symbol = fill.Symbol,
EntryTime = fill.UtcTime,
EntryPrice = fill.FillPrice,
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
Quantity = fill.AbsoluteFillQuantity,
TotalFees = orderFee
});
}
else
{
// execution has opposite direction of trade
var totalExecutedQuantity = 0;
var orderFeeAssigned = false;
while (position.PendingTrades.Count > 0 && Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
{
var trade = position.PendingTrades[index];
if (fill.AbsoluteFillQuantity >= trade.Quantity)
{
totalExecutedQuantity -= trade.Quantity * (trade.Direction == TradeDirection.Long ? +1 : -1);
position.PendingTrades.RemoveAt(index);
if (index > 0 && _matchingMethod == FillMatchingMethod.LIFO) index--;
trade.ExitTime = fill.UtcTime;
trade.ExitPrice = fill.FillPrice;
trade.ProfitLoss = Math.Round((trade.ExitPrice - trade.EntryPrice) * trade.Quantity * (trade.Direction == TradeDirection.Long ? +1 : -1) * conversionRate, 2);
// if closing multiple trades with the same order, assign order fee only once
trade.TotalFees += orderFeeAssigned ? 0 : orderFee;
trade.MAE = Math.Round((trade.Direction == TradeDirection.Long ? position.MinPrice - trade.EntryPrice : trade.EntryPrice - position.MaxPrice) * trade.Quantity * conversionRate, 2);
trade.MFE = Math.Round((trade.Direction == TradeDirection.Long ? position.MaxPrice - trade.EntryPrice : trade.EntryPrice - position.MinPrice) * trade.Quantity * conversionRate, 2);
AddNewTrade(trade);
}
else
{
totalExecutedQuantity += fill.FillQuantity;
trade.Quantity -= fill.AbsoluteFillQuantity;
AddNewTrade(new Trade
{
Symbol = trade.Symbol,
EntryTime = trade.EntryTime,
EntryPrice = trade.EntryPrice,
Direction = trade.Direction,
Quantity = fill.AbsoluteFillQuantity,
ExitTime = fill.UtcTime,
ExitPrice = fill.FillPrice,
ProfitLoss = Math.Round((fill.FillPrice - trade.EntryPrice) * fill.AbsoluteFillQuantity * (trade.Direction == TradeDirection.Long ? +1 : -1) * conversionRate, 2),
TotalFees = trade.TotalFees + (orderFeeAssigned ? 0 : orderFee),
MAE = Math.Round((trade.Direction == TradeDirection.Long ? position.MinPrice - trade.EntryPrice : trade.EntryPrice - position.MaxPrice) * fill.AbsoluteFillQuantity * conversionRate, 2),
MFE = Math.Round((trade.Direction == TradeDirection.Long ? position.MaxPrice - trade.EntryPrice : trade.EntryPrice - position.MinPrice) * fill.AbsoluteFillQuantity * conversionRate, 2)
});
trade.TotalFees = 0;
}
orderFeeAssigned = true;
}
if (Math.Abs(totalExecutedQuantity) == fill.AbsoluteFillQuantity && position.PendingTrades.Count == 0)
{
_positions.Remove(fill.Symbol);
}
else if (Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
{
// direction reversal
fill.FillQuantity -= totalExecutedQuantity;
position.PendingTrades = new List<Trade>
{
new Trade
{
Symbol = fill.Symbol,
EntryTime = fill.UtcTime,
EntryPrice = fill.FillPrice,
Direction = fill.FillQuantity > 0 ? TradeDirection.Long : TradeDirection.Short,
Quantity = fill.AbsoluteFillQuantity,
TotalFees = 0
}
};
position.MinPrice = fill.FillPrice;
position.MaxPrice = fill.FillPrice;
}
}
}
private void ProcessFillUsingFlatToFlat(OrderEvent fill, decimal orderFee, decimal conversionRate)
{
Position position;
if (!_positions.TryGetValue(fill.Symbol, out position) || position.PendingFills.Count == 0)
{
// no pending executions for symbol
_positions[fill.Symbol] = new Position
{
PendingFills = new List<OrderEvent> { fill },
TotalFees = orderFee,
MinPrice = fill.FillPrice,
MaxPrice = fill.FillPrice
};
return;
}
SetMarketPrice(fill.Symbol, fill.FillPrice);
if (Math.Sign(position.PendingFills[0].FillQuantity) == Math.Sign(fill.FillQuantity))
{
// execution has same direction of trade
position.PendingFills.Add(fill);
position.TotalFees += orderFee;
}
else
{
// execution has opposite direction of trade
if (position.PendingFills.Sum(x => x.FillQuantity) + fill.FillQuantity == 0 || fill.AbsoluteFillQuantity > Math.Abs(position.PendingFills.Sum(x => x.FillQuantity)))
{
// trade closed
position.PendingFills.Add(fill);
position.TotalFees += orderFee;
var reverseQuantity = position.PendingFills.Sum(x => x.FillQuantity);
var index = _matchingMethod == FillMatchingMethod.FIFO ? 0 : position.PendingFills.Count - 1;
var entryTime = position.PendingFills[0].UtcTime;
var totalEntryQuantity = 0;
var totalExitQuantity = 0;
var entryAveragePrice = 0m;
var exitAveragePrice = 0m;
while (position.PendingFills.Count > 0)
{
if (Math.Sign(position.PendingFills[index].FillQuantity) != Math.Sign(fill.FillQuantity))
{
// entry
totalEntryQuantity += position.PendingFills[index].FillQuantity;
entryAveragePrice += (position.PendingFills[index].FillPrice - entryAveragePrice) * position.PendingFills[index].FillQuantity / totalEntryQuantity;
}
else
{
// exit
totalExitQuantity += position.PendingFills[index].FillQuantity;
exitAveragePrice += (position.PendingFills[index].FillPrice - exitAveragePrice) * position.PendingFills[index].FillQuantity / totalExitQuantity;
}
position.PendingFills.RemoveAt(index);
if (_matchingMethod == FillMatchingMethod.LIFO && index > 0) index--;
}
var direction = Math.Sign(fill.FillQuantity) < 0 ? TradeDirection.Long : TradeDirection.Short;
AddNewTrade(new Trade
{
Symbol = fill.Symbol,
EntryTime = entryTime,
EntryPrice = entryAveragePrice,
Direction = direction,
Quantity = Math.Abs(totalEntryQuantity),
ExitTime = fill.UtcTime,
ExitPrice = exitAveragePrice,
ProfitLoss = Math.Round((exitAveragePrice - entryAveragePrice) * Math.Abs(totalEntryQuantity) * Math.Sign(totalEntryQuantity) * conversionRate, 2),
TotalFees = position.TotalFees,
MAE = Math.Round((direction == TradeDirection.Long ? position.MinPrice - entryAveragePrice : entryAveragePrice - position.MaxPrice) * Math.Abs(totalEntryQuantity) * conversionRate, 2),
MFE = Math.Round((direction == TradeDirection.Long ? position.MaxPrice - entryAveragePrice : entryAveragePrice - position.MinPrice) * Math.Abs(totalEntryQuantity) * conversionRate, 2)
});
_positions.Remove(fill.Symbol);
if (reverseQuantity != 0)
{
// direction reversal
fill.FillQuantity = reverseQuantity;
_positions[fill.Symbol] = new Position
{
PendingFills = new List<OrderEvent> { fill },
TotalFees = 0,
MinPrice = fill.FillPrice,
MaxPrice = fill.FillPrice
};
}
}
else
{
// trade open
position.PendingFills.Add(fill);
position.TotalFees += orderFee;
}
}
}
private void ProcessFillUsingFlatToReduced(OrderEvent fill, decimal orderFee, decimal conversionRate)
{
Position position;
if (!_positions.TryGetValue(fill.Symbol, out position) || position.PendingFills.Count == 0)
{
// no pending executions for symbol
_positions[fill.Symbol] = new Position
{
PendingFills = new List<OrderEvent> { fill },
TotalFees = orderFee,
MinPrice = fill.FillPrice,
MaxPrice = fill.FillPrice
};
return;
}
SetMarketPrice(fill.Symbol, fill.FillPrice);
var index = _matchingMethod == FillMatchingMethod.FIFO ? 0 : position.PendingFills.Count - 1;
if (Math.Sign(fill.FillQuantity) == Math.Sign(position.PendingFills[index].FillQuantity))
{
// execution has same direction of trade
position.PendingFills.Add(fill);
position.TotalFees += orderFee;
}
else
{
// execution has opposite direction of trade
var entryTime = position.PendingFills[index].UtcTime;
var totalExecutedQuantity = 0;
var entryPrice = 0m;
position.TotalFees += orderFee;
while (position.PendingFills.Count > 0 && Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
{
if (fill.AbsoluteFillQuantity >= Math.Abs(position.PendingFills[index].FillQuantity))
{
if (_matchingMethod == FillMatchingMethod.LIFO)
entryTime = position.PendingFills[index].UtcTime;
totalExecutedQuantity -= position.PendingFills[index].FillQuantity;
entryPrice -= (position.PendingFills[index].FillPrice - entryPrice) * position.PendingFills[index].FillQuantity / totalExecutedQuantity;
position.PendingFills.RemoveAt(index);
if (_matchingMethod == FillMatchingMethod.LIFO && index > 0) index--;
}
else
{
totalExecutedQuantity += fill.FillQuantity;
entryPrice += (position.PendingFills[index].FillPrice - entryPrice) * fill.FillQuantity / totalExecutedQuantity;
position.PendingFills[index].FillQuantity += fill.FillQuantity;
}
}
var direction = totalExecutedQuantity < 0 ? TradeDirection.Long : TradeDirection.Short;
AddNewTrade(new Trade
{
Symbol = fill.Symbol,
EntryTime = entryTime,
EntryPrice = entryPrice,
Direction = direction,
Quantity = Math.Abs(totalExecutedQuantity),
ExitTime = fill.UtcTime,
ExitPrice = fill.FillPrice,
ProfitLoss = Math.Round((fill.FillPrice - entryPrice) * Math.Abs(totalExecutedQuantity) * Math.Sign(-totalExecutedQuantity) * conversionRate, 2),
TotalFees = position.TotalFees,
MAE = Math.Round((direction == TradeDirection.Long ? position.MinPrice - entryPrice : entryPrice - position.MaxPrice) * Math.Abs(totalExecutedQuantity) * conversionRate, 2),
MFE = Math.Round((direction == TradeDirection.Long ? position.MaxPrice - entryPrice : entryPrice - position.MinPrice) * Math.Abs(totalExecutedQuantity) * conversionRate, 2)
});
if (Math.Abs(totalExecutedQuantity) < fill.AbsoluteFillQuantity)
{
// direction reversal
fill.FillQuantity -= totalExecutedQuantity;
position.PendingFills = new List<OrderEvent> { fill };
position.TotalFees = 0;
position.MinPrice = fill.FillPrice;
position.MaxPrice = fill.FillPrice;
}
else if (Math.Abs(totalExecutedQuantity) == fill.AbsoluteFillQuantity)
{
if (position.PendingFills.Count == 0)
_positions.Remove(fill.Symbol);
else
position.TotalFees = 0;
}
}
}
/// <summary>
/// Adds a trade to the list of closed trades, capping the total number only in live mode
/// </summary>
private void AddNewTrade(Trade trade)
{
_closedTrades.Add(trade);
// Due to memory constraints in live mode, we cap the number of trades
if (!_liveMode)
return;
// maximum number of trades
if (_closedTrades.Count > LiveModeMaxTradeCount)
{
_closedTrades.RemoveRange(0, _closedTrades.Count - LiveModeMaxTradeCount);
}
// maximum age of trades
while (_closedTrades.Count > 0 && _closedTrades[0].ExitTime.Date.AddMonths(LiveModeMaxTradeAgeMonths) < DateTime.Today)
{
_closedTrades.RemoveAt(0);
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
/*
* 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.
*/
namespace QuantConnect.Statistics
{
/// <summary>
/// Direction of a trade
/// </summary>
public enum TradeDirection
{
/// <summary>
/// Long direction
/// </summary>
Long,
/// <summary>
/// Short direction
/// </summary>
Short
}
/// <summary>
/// The method used to group order fills into trades
/// </summary>
public enum FillGroupingMethod
{
/// <summary>
/// A Trade is defined by a fill that establishes or increases a position and an offsetting fill that reduces the position size.
/// </summary>
FillToFill,
/// <summary>
/// A Trade is defined by a sequence of fills, from a flat position to a non-zero position which may increase or decrease in quantity, and back to a flat position.
/// </summary>
FlatToFlat,
/// <summary>
/// A Trade is defined by a sequence of fills, from a flat position to a non-zero position and an offsetting fill that reduces the position size.
/// </summary>
FlatToReduced,
}
/// <summary>
/// The method used to match offsetting order fills
/// </summary>
public enum FillMatchingMethod
{
/// <summary>
/// First In First Out fill matching method
/// </summary>
FIFO,
/// <summary>
/// Last In Last Out fill matching method
/// </summary>
LIFO
}
}
+365
View File
@@ -0,0 +1,365 @@
/*
* 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.Generic;
namespace QuantConnect.Statistics
{
/// <summary>
/// The <see cref="TradeStatistics"/> class represents a set of statistics calculated from a list of closed trades
/// </summary>
public class TradeStatistics
{
/// <summary>
/// The entry date/time of the first trade
/// </summary>
public DateTime? StartDateTime { get; private set; }
/// <summary>
/// The exit date/time of the last trade
/// </summary>
public DateTime? EndDateTime { get; private set; }
/// <summary>
/// The total number of trades
/// </summary>
public int TotalNumberOfTrades { get; private set; }
/// <summary>
/// The total number of winning trades
/// </summary>
public int NumberOfWinningTrades { get; private set; }
/// <summary>
/// The total number of losing trades
/// </summary>
public int NumberOfLosingTrades { get; private set; }
/// <summary>
/// The total profit/loss for all trades (as symbol currency)
/// </summary>
public decimal TotalProfitLoss { get; private set; }
/// <summary>
/// The total profit for all winning trades (as symbol currency)
/// </summary>
public decimal TotalProfit { get; private set; }
/// <summary>
/// The total loss for all losing trades (as symbol currency)
/// </summary>
public decimal TotalLoss { get; private set; }
/// <summary>
/// The largest profit in a single trade (as symbol currency)
/// </summary>
public decimal LargestProfit { get; private set; }
/// <summary>
/// The largest loss in a single trade (as symbol currency)
/// </summary>
public decimal LargestLoss { get; private set; }
/// <summary>
/// The average profit/loss (a.k.a. Expectancy or Average Trade) for all trades (as symbol currency)
/// </summary>
public decimal AverageProfitLoss { get; private set; }
/// <summary>
/// The average profit for all winning trades (as symbol currency)
/// </summary>
public decimal AverageProfit { get; private set; }
/// <summary>
/// The average loss for all winning trades (as symbol currency)
/// </summary>
public decimal AverageLoss { get; private set; }
/// <summary>
/// The average duration for all trades
/// </summary>
public TimeSpan AverageTradeDuration { get; private set; }
/// <summary>
/// The average duration for all winning trades
/// </summary>
public TimeSpan AverageWinningTradeDuration { get; private set; }
/// <summary>
/// The average duration for all losing trades
/// </summary>
public TimeSpan AverageLosingTradeDuration { get; private set; }
/// <summary>
/// The maximum number of consecutive winning trades
/// </summary>
public int MaxConsecutiveWinningTrades { get; private set; }
/// <summary>
/// The maximum number of consecutive losing trades
/// </summary>
public int MaxConsecutiveLosingTrades { get; private set; }
/// <summary>
/// The ratio of the average profit per trade to the average loss per trade
/// </summary>
/// <remarks>If the average loss is zero, ProfitLossRatio is set to 0</remarks>
public decimal ProfitLossRatio { get; private set; }
/// <summary>
/// The ratio of the number of winning trades to the number of losing trades
/// </summary>
/// <remarks>If the total number of trades is zero, WinLossRatio is set to zero</remarks>
/// <remarks>If the number of losing trades is zero and the number of winning trades is nonzero, WinLossRatio is set to 10</remarks>
public decimal WinLossRatio { get; private set; }
/// <summary>
/// The ratio of the number of winning trades to the total number of trades
/// </summary>
/// <remarks>If the total number of trades is zero, WinRate is set to zero</remarks>
public decimal WinRate { get; private set; }
/// <summary>
/// The ratio of the number of losing trades to the total number of trades
/// </summary>
/// <remarks>If the total number of trades is zero, LossRate is set to zero</remarks>
public decimal LossRate { get; private set; }
/// <summary>
/// The average Maximum Adverse Excursion for all trades
/// </summary>
public decimal AverageMAE { get; private set; }
/// <summary>
/// The average Maximum Favorable Excursion for all trades
/// </summary>
public decimal AverageMFE { get; private set; }
/// <summary>
/// The largest Maximum Adverse Excursion in a single trade (as symbol currency)
/// </summary>
public decimal LargestMAE { get; private set; }
/// <summary>
/// The largest Maximum Favorable Excursion in a single trade (as symbol currency)
/// </summary>
public decimal LargestMFE { get; private set; }
/// <summary>
/// The maximum closed-trade drawdown for all trades (as symbol currency)
/// </summary>
/// <remarks>The calculation only takes into account the profit/loss of each trade</remarks>
public decimal MaximumClosedTradeDrawdown { get; private set; }
/// <summary>
/// The maximum intra-trade drawdown for all trades (as symbol currency)
/// </summary>
/// <remarks>The calculation takes into account MAE and MFE of each trade</remarks>
public decimal MaximumIntraTradeDrawdown { get; private set; }
/// <summary>
/// The standard deviation of the profits/losses for all trades (as symbol currency)
/// </summary>
public decimal ProfitLossStandardDeviation { get; private set; }
/// <summary>
/// The downside deviation of the profits/losses for all trades (as symbol currency)
/// </summary>
/// <remarks>This metric only considers deviations of losing trades</remarks>
public decimal ProfitLossDownsideDeviation { get; private set; }
/// <summary>
/// The ratio of the total profit to the total loss
/// </summary>
/// <remarks>If the total profit is zero, ProfitFactor is set to zero</remarks>
/// <remarks>if the total loss is zero and the total profit is nonzero, ProfitFactor is set to 10</remarks>
public decimal ProfitFactor { get; private set; }
/// <summary>
/// The ratio of the average profit/loss to the standard deviation
/// </summary>
public decimal SharpeRatio { get; private set; }
/// <summary>
/// The ratio of the average profit/loss to the downside deviation
/// </summary>
public decimal SortinoRatio { get; private set; }
/// <summary>
/// The ratio of the total profit/loss to the maximum closed trade drawdown
/// </summary>
/// <remarks>If the total profit/loss is zero, ProfitToMaxDrawdownRatio is set to zero</remarks>
/// <remarks>if the drawdown is zero and the total profit is nonzero, ProfitToMaxDrawdownRatio is set to 10</remarks>
public decimal ProfitToMaxDrawdownRatio { get; private set; }
/// <summary>
/// The maximum amount of profit given back by a single trade before exit (as symbol currency)
/// </summary>
public decimal MaximumEndTradeDrawdown { get; private set; }
/// <summary>
/// The average amount of profit given back by all trades before exit (as symbol currency)
/// </summary>
public decimal AverageEndTradeDrawdown { get; private set; }
/// <summary>
/// The maximum amount of time to recover from a drawdown (longest time between new equity highs or peaks)
/// </summary>
public TimeSpan MaximumDrawdownDuration { get; private set; }
/// <summary>
/// The sum of fees for all trades
/// </summary>
public decimal TotalFees { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="TradeStatistics"/> class
/// </summary>
/// <param name="trades">The list of closed trades</param>
public TradeStatistics(IEnumerable<Trade> trades)
{
var maxConsecutiveWinners = 0;
var maxConsecutiveLosers = 0;
var maxTotalProfitLoss = 0m;
var maxTotalProfitLossWithMfe = 0m;
var sumForVariance = 0m;
var sumForDownsideVariance = 0m;
var lastPeakTime = DateTime.MinValue;
var isInDrawdown = false;
foreach (var trade in trades)
{
if (lastPeakTime == DateTime.MinValue) lastPeakTime = trade.EntryTime;
if (StartDateTime == null || trade.EntryTime < StartDateTime)
StartDateTime = trade.EntryTime;
if (EndDateTime == null || trade.ExitTime > EndDateTime)
EndDateTime = trade.ExitTime;
TotalNumberOfTrades++;
if (TotalProfitLoss + trade.MFE > maxTotalProfitLossWithMfe)
maxTotalProfitLossWithMfe = TotalProfitLoss + trade.MFE;
if (TotalProfitLoss + trade.MAE - maxTotalProfitLossWithMfe < MaximumIntraTradeDrawdown)
MaximumIntraTradeDrawdown = TotalProfitLoss + trade.MAE - maxTotalProfitLossWithMfe;
if (trade.ProfitLoss > 0)
{
// winning trade
NumberOfWinningTrades++;
TotalProfitLoss += trade.ProfitLoss;
TotalProfit += trade.ProfitLoss;
AverageProfit += (trade.ProfitLoss - AverageProfit) / NumberOfWinningTrades;
AverageWinningTradeDuration += TimeSpan.FromSeconds((trade.Duration.TotalSeconds - AverageWinningTradeDuration.TotalSeconds) / NumberOfWinningTrades);
if (trade.ProfitLoss > LargestProfit)
LargestProfit = trade.ProfitLoss;
maxConsecutiveWinners++;
maxConsecutiveLosers = 0;
if (maxConsecutiveWinners > MaxConsecutiveWinningTrades)
MaxConsecutiveWinningTrades = maxConsecutiveWinners;
if (TotalProfitLoss > maxTotalProfitLoss)
{
// new equity high
maxTotalProfitLoss = TotalProfitLoss;
if (isInDrawdown && trade.ExitTime - lastPeakTime > MaximumDrawdownDuration)
MaximumDrawdownDuration = trade.ExitTime - lastPeakTime;
lastPeakTime = trade.ExitTime;
isInDrawdown = false;
}
}
else
{
// losing trade
NumberOfLosingTrades++;
TotalProfitLoss += trade.ProfitLoss;
TotalLoss += trade.ProfitLoss;
var prevAverageLoss = AverageLoss;
AverageLoss += (trade.ProfitLoss - AverageLoss) / NumberOfLosingTrades;
sumForDownsideVariance += (trade.ProfitLoss - prevAverageLoss) * (trade.ProfitLoss - AverageLoss);
var downsideVariance = NumberOfLosingTrades > 1 ? sumForDownsideVariance / (NumberOfLosingTrades - 1) : 0;
ProfitLossDownsideDeviation = (decimal)Math.Sqrt((double)downsideVariance);
AverageLosingTradeDuration += TimeSpan.FromSeconds((trade.Duration.TotalSeconds - AverageLosingTradeDuration.TotalSeconds) / NumberOfLosingTrades);
if (trade.ProfitLoss < LargestLoss)
LargestLoss = trade.ProfitLoss;
maxConsecutiveWinners = 0;
maxConsecutiveLosers++;
if (maxConsecutiveLosers > MaxConsecutiveLosingTrades)
MaxConsecutiveLosingTrades = maxConsecutiveLosers;
if (TotalProfitLoss - maxTotalProfitLoss < MaximumClosedTradeDrawdown)
MaximumClosedTradeDrawdown = TotalProfitLoss - maxTotalProfitLoss;
isInDrawdown = true;
}
var prevAverageProfitLoss = AverageProfitLoss;
AverageProfitLoss += (trade.ProfitLoss - AverageProfitLoss) / TotalNumberOfTrades;
sumForVariance += (trade.ProfitLoss - prevAverageProfitLoss) * (trade.ProfitLoss - AverageProfitLoss);
var variance = TotalNumberOfTrades > 1 ? sumForVariance / (TotalNumberOfTrades - 1) : 0;
ProfitLossStandardDeviation = (decimal)Math.Sqrt((double)variance);
AverageTradeDuration += TimeSpan.FromSeconds((trade.Duration.TotalSeconds - AverageTradeDuration.TotalSeconds) / TotalNumberOfTrades);
AverageMAE += (trade.MAE - AverageMAE) / TotalNumberOfTrades;
AverageMFE += (trade.MFE - AverageMFE) / TotalNumberOfTrades;
if (trade.MAE < LargestMAE)
LargestMAE = trade.MAE;
if (trade.MFE > LargestMFE)
LargestMFE = trade.MFE;
if (trade.EndTradeDrawdown < MaximumEndTradeDrawdown)
MaximumEndTradeDrawdown = trade.EndTradeDrawdown;
TotalFees += trade.TotalFees;
}
ProfitLossRatio = AverageLoss == 0 ? 0 : AverageProfit / Math.Abs(AverageLoss);
WinLossRatio = TotalNumberOfTrades == 0 ? 0 : (NumberOfLosingTrades > 0 ? (decimal)NumberOfWinningTrades / NumberOfLosingTrades : 10);
WinRate = TotalNumberOfTrades > 0 ? (decimal)NumberOfWinningTrades / TotalNumberOfTrades : 0;
LossRate = TotalNumberOfTrades > 0 ? 1 - WinRate : 0;
ProfitFactor = TotalProfit == 0 ? 0 : (TotalLoss < 0 ? TotalProfit / Math.Abs(TotalLoss) : 10);
SharpeRatio = ProfitLossStandardDeviation > 0 ? AverageProfitLoss / ProfitLossStandardDeviation : 0;
SortinoRatio = ProfitLossDownsideDeviation > 0 ? AverageProfitLoss / ProfitLossDownsideDeviation : 0;
ProfitToMaxDrawdownRatio = TotalProfitLoss == 0 ? 0 : (MaximumClosedTradeDrawdown < 0 ? TotalProfitLoss / Math.Abs(MaximumClosedTradeDrawdown) : 10);
AverageEndTradeDrawdown = AverageProfitLoss - AverageMFE;
}
/// <summary>
/// Initializes a new instance of the <see cref="TradeStatistics"/> class
/// </summary>
public TradeStatistics()
{
}
}
}
+6
View File
@@ -290,6 +290,12 @@ namespace QuantConnect.Lean.Engine
foreach (var kvp in timeSlice.SecuritiesUpdateData)
{
kvp.Key.SetMarketPrice(kvp.Value);
// Send market price updates to the TradeBuilder
if (kvp.Value != null)
{
algorithm.TradeBuilder.SetMarketPrice(kvp.Key.Symbol, kvp.Value.Price);
}
}
// fire real time events after we've updated based on the new data
+10 -8
View File
@@ -25,6 +25,7 @@ using QuantConnect.Interfaces;
using QuantConnect.Logging;
using QuantConnect.Orders;
using QuantConnect.Packets;
using QuantConnect.Statistics;
using QuantConnect.Util;
namespace QuantConnect.Lean.Engine
@@ -333,11 +334,12 @@ namespace QuantConnect.Lean.Engine
try
{
var trades = algorithm.TradeBuilder.ClosedTrades;
var charts = new Dictionary<string, Chart>(_algorithmHandlers.Results.Charts);
var orders = new Dictionary<int, Order>(_algorithmHandlers.Transactions.Orders);
var holdings = new Dictionary<string, Holding>();
var statistics = new Dictionary<string, string>();
var banner = new Dictionary<string, string>();
var statisticsResults = new StatisticsResults();
try
{
@@ -354,12 +356,12 @@ namespace QuantConnect.Lean.Engine
{
var equity = charts[strategyEquityKey].Series[equityKey].Values;
var performance = charts[strategyEquityKey].Series[dailyPerformanceKey].Values;
var profitLoss =
new SortedDictionary<DateTime, decimal>(algorithm.Transactions.TransactionRecord);
var numberOfTrades = algorithm.Transactions.GetOrders(x => x.Status.IsFill()).Count();
var benchmark = charts[benchmarkKey].Series[benchmarkKey].Values.ToDictionary(chartPoint => Time.UnixTimeStampToDateTime(chartPoint.x), chartPoint => chartPoint.y);
statistics = Statistics.Statistics.Generate(equity, profitLoss, performance, benchmark,
_algorithmHandlers.Setup.StartingPortfolioValue, algorithm.Portfolio.TotalFees, numberOfTrades, 252);
var profitLoss = new SortedDictionary<DateTime, decimal>(algorithm.Transactions.TransactionRecord);
var totalTransactions = algorithm.Transactions.GetOrders(x => x.Status.IsFill()).Count();
var benchmark = charts[benchmarkKey].Series[benchmarkKey].Values;
statisticsResults = StatisticsBuilder.Generate(trades, profitLoss, equity, performance, benchmark,
_algorithmHandlers.Setup.StartingPortfolioValue, algorithm.Portfolio.TotalFees, totalTransactions);
}
}
catch (Exception err)
@@ -375,7 +377,7 @@ namespace QuantConnect.Lean.Engine
job.AlgorithmId, totalSeconds.ToString("F2"), ((dataPoints/(double) 1000)/totalSeconds).ToString("F0"),
dataPoints.ToString("N0")));
_algorithmHandlers.Results.SendFinalResult(job, orders, algorithm.Transactions.TransactionRecord, holdings, statistics, banner);
_algorithmHandlers.Results.SendFinalResult(job, orders, algorithm.Transactions.TransactionRecord, holdings, statisticsResults, banner);
}
catch (Exception err)
{
@@ -31,6 +31,7 @@ using QuantConnect.Lean.Engine.TransactionHandlers;
using QuantConnect.Orders;
using QuantConnect.Packets;
using QuantConnect.Securities;
using QuantConnect.Statistics;
using HistoryRequest = QuantConnect.Data.HistoryRequest;
namespace QuantConnect.Lean.Engine.HistoricalData
@@ -211,7 +212,7 @@ namespace QuantConnect.Lean.Engine.HistoricalData
public void SampleRange(List<Chart> samples) { }
public void SetAlgorithm(IAlgorithm algorithm) { }
public void StoreResult(Packet packet, bool async = false) { }
public void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, Dictionary<string, string> statistics, Dictionary<string, string> banner) { }
public void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, StatisticsResults statisticsResults, Dictionary<string, string> banner) { }
public void SendStatusUpdate(string algorithmId, AlgorithmStatus status, string message = "") { }
public void SetChartSubscription(string symbol) { }
public void RuntimeStatistic(string key, string value) { }
+4 -3
View File
@@ -27,6 +27,7 @@ using QuantConnect.Lean.Engine.TransactionHandlers;
using QuantConnect.Logging;
using QuantConnect.Orders;
using QuantConnect.Packets;
using QuantConnect.Statistics;
using QuantConnect.Util;
namespace QuantConnect.Lean.Engine.Results
@@ -452,9 +453,9 @@ namespace QuantConnect.Lean.Engine.Results
/// <param name="orders">Collection of orders from the algorithm</param>
/// <param name="profitLoss">Collection of time-profit values for the algorithm</param>
/// <param name="holdings">Current holdings state for the algorithm</param>
/// <param name="statistics">Statistics information for the algorithm (empty if not finished)</param>
/// <param name="statisticsResults">Statistics information for the algorithm (empty if not finished)</param>
/// <param name="banner">Runtime statistics banner information</param>
public void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, Dictionary<string, string> statistics, Dictionary<string, string> banner)
public void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, StatisticsResults statisticsResults, Dictionary<string, string> banner)
{
try
{
@@ -464,7 +465,7 @@ namespace QuantConnect.Lean.Engine.Results
//Create a result packet to send to the browser.
BacktestResultPacket result = new BacktestResultPacket((BacktestNodePacket) job,
new BacktestResult(charts, orders, profitLoss, statistics), 1m)
new BacktestResult(charts, orders, profitLoss, statisticsResults.Summary), 1m)
{
ProcessingTime = (DateTime.Now - _startTime).TotalSeconds,
DateFinished = DateTime.Now,
+13 -4
View File
@@ -27,6 +27,7 @@ using QuantConnect.Lean.Engine.TransactionHandlers;
using QuantConnect.Logging;
using QuantConnect.Orders;
using QuantConnect.Packets;
using QuantConnect.Statistics;
namespace QuantConnect.Lean.Engine.Results
{
@@ -50,6 +51,9 @@ namespace QuantConnect.Lean.Engine.Results
private string _chartDirectory;
private readonly Dictionary<string, List<string>> _equityResults;
/// <summary>
/// A dictionary containing summary statistics
/// </summary>
public Dictionary<string, string> FinalStatistics { get; private set; }
/// <summary>
@@ -396,21 +400,26 @@ namespace QuantConnect.Lean.Engine.Results
/// <param name="orders">Collection of orders from the algorithm</param>
/// <param name="profitLoss">Collection of time-profit values for the algorithm</param>
/// <param name="holdings">Current holdings state for the algorithm</param>
/// <param name="statistics">Statistics information for the algorithm (empty if not finished)</param>
/// <param name="statisticsResults">Statistics information for the algorithm (empty if not finished)</param>
/// <param name="banner">Runtime statistics banner information</param>
public void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, Dictionary<string, string> statistics, Dictionary<string, string> banner)
public void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, StatisticsResults statisticsResults, Dictionary<string, string> banner)
{
// uncomment these code traces to help write regression tests
//Console.WriteLine("var statistics = new Dictionary<string, string>();");
// Bleh. Nicely format statistical analysis on your algorithm results. Save to file etc.
foreach (var pair in statistics)
foreach (var pair in statisticsResults.Summary)
{
Log.Trace("STATISTICS:: " + pair.Key + " " + pair.Value);
//Console.WriteLine(string.Format("statistics.Add(\"{0}\",\"{1}\");", pair.Key, pair.Value));
}
FinalStatistics = statistics;
//foreach (var pair in statisticsResults.RollingPerformances)
//{
// Log.Trace("ROLLINGSTATS:: " + pair.Key + " SharpeRatio: " + Math.Round(pair.Value.PortfolioStatistics.SharpeRatio, 3));
//}
FinalStatistics = statisticsResults.Summary;
}
/// <summary>
+9 -4
View File
@@ -25,6 +25,7 @@ using QuantConnect.Lean.Engine.TransactionHandlers;
using QuantConnect.Logging;
using QuantConnect.Orders;
using QuantConnect.Packets;
using QuantConnect.Statistics;
namespace QuantConnect.Lean.Engine.Results
{
@@ -43,6 +44,10 @@ namespace QuantConnect.Lean.Engine.Results
private DateTime _nextSample;
private readonly TimeSpan _resamplePeriod;
private readonly TimeSpan _notificationPeriod;
/// <summary>
/// A dictionary containing summary statistics
/// </summary>
public Dictionary<string, string> FinalStatistics { get; private set; }
/// <summary>
@@ -342,20 +347,20 @@ namespace QuantConnect.Lean.Engine.Results
/// <param name="orders">Collection of orders from the algorithm</param>
/// <param name="profitLoss">Collection of time-profit values for the algorithm</param>
/// <param name="holdings">Current holdings state for the algorithm</param>
/// <param name="statistics">Statistics information for the algorithm (empty if not finished)</param>
/// <param name="statisticsResults">Statistics information for the algorithm (empty if not finished)</param>
/// <param name="banner">Runtime statistics banner information</param>
public void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, Dictionary<string, string> statistics, Dictionary<string, string> banner)
public void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, StatisticsResults statisticsResults, Dictionary<string, string> banner)
{
// uncomment these code traces to help write regression tests
//Log.Trace("var statistics = new Dictionary<string, string>();");
// Bleh. Nicely format statistical analysis on your algorithm results. Save to file etc.
foreach (var pair in statistics)
foreach (var pair in statisticsResults.Summary)
{
DebugMessage("STATISTICS:: " + pair.Key + " " + pair.Value);
}
FinalStatistics = statistics;
FinalStatistics = statisticsResults.Summary;
}
/// <summary>
+8 -1
View File
@@ -24,6 +24,7 @@ using QuantConnect.Lean.Engine.Setup;
using QuantConnect.Lean.Engine.TransactionHandlers;
using QuantConnect.Orders;
using QuantConnect.Packets;
using QuantConnect.Statistics;
namespace QuantConnect.Lean.Engine.Results
{
@@ -197,7 +198,13 @@ namespace QuantConnect.Lean.Engine.Results
/// <summary>
/// Post the final result back to the controller worker if backtesting, or to console if local.
/// </summary>
void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, Dictionary<string, string> statistics, Dictionary<string, string> banner);
/// <param name="job">Lean AlgorithmJob task</param>
/// <param name="orders">Collection of orders from the algorithm</param>
/// <param name="profitLoss">Collection of time-profit values for the algorithm</param>
/// <param name="holdings">Current holdings state for the algorithm</param>
/// <param name="statisticsResults">Statistics information for the algorithm (empty if not finished)</param>
/// <param name="banner">Runtime statistics banner information</param>
void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, StatisticsResults statisticsResults, Dictionary<string, string> banner);
/// <summary>
/// Send a algorithm status update to the user of the algorithms running state.
+4 -3
View File
@@ -31,6 +31,7 @@ using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Packets;
using QuantConnect.Securities;
using QuantConnect.Statistics;
using QuantConnect.Util;
namespace QuantConnect.Lean.Engine.Results
@@ -790,9 +791,9 @@ namespace QuantConnect.Lean.Engine.Results
/// <param name="orders">Collection of orders from the algorithm</param>
/// <param name="profitLoss">Collection of time-profit values for the algorithm</param>
/// <param name="holdings">Current holdings state for the algorithm</param>
/// <param name="statistics">Statistics information for the algorithm (empty if not finished)</param>
/// <param name="statisticsResults">Statistics information for the algorithm (empty if not finished)</param>
/// <param name="runtime">Runtime statistics banner information</param>
public void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, Dictionary<string, string> statistics, Dictionary<string, string> runtime)
public void SendFinalResult(AlgorithmNodePacket job, Dictionary<int, Order> orders, Dictionary<DateTime, decimal> profitLoss, Dictionary<string, Holding> holdings, StatisticsResults statisticsResults, Dictionary<string, string> runtime)
{
try
{
@@ -800,7 +801,7 @@ namespace QuantConnect.Lean.Engine.Results
var charts = new Dictionary<string, Chart>(Charts);
//Create a packet:
var result = new LiveResultPacket((LiveNodePacket)job, new LiveResult(charts, orders, profitLoss, holdings, statistics, runtime));
var result = new LiveResultPacket((LiveNodePacket)job, new LiveResult(charts, orders, profitLoss, holdings, statisticsResults.Summary, runtime));
//Save the processing time:
result.ProcessingTime = (DateTime.Now - _startTime).TotalSeconds;
@@ -25,6 +25,7 @@ using QuantConnect.Lean.Engine.Results;
using QuantConnect.Logging;
using QuantConnect.Orders;
using QuantConnect.Securities;
using QuantConnect.Securities.Forex;
namespace QuantConnect.Lean.Engine.TransactionHandlers
{
@@ -783,6 +784,16 @@ namespace QuantConnect.Lean.Engine.TransactionHandlers
try
{
_algorithm.Portfolio.ProcessFill(fill);
var conversionRate = 1m;
if (order.SecurityType == SecurityType.Forex)
{
string baseCurrency, quoteCurrency;
Forex.DecomposeCurrencyPair(fill.Symbol, out baseCurrency, out quoteCurrency);
conversionRate = _algorithm.Portfolio.CashBook[quoteCurrency].ConversionRate;
}
_algorithm.TradeBuilder.ProcessFill(fill, conversionRate);
}
catch (Exception err)
{
+6
View File
@@ -23,6 +23,7 @@ using QuantConnect.Notifications;
using QuantConnect.Orders;
using QuantConnect.Scheduling;
using QuantConnect.Securities;
using QuantConnect.Statistics;
namespace QuantConnect.Interfaces
{
@@ -234,6 +235,11 @@ namespace QuantConnect.Interfaces
get;
}
/// <summary>
/// Gets the Trade Builder to generate trades from executions
/// </summary>
TradeBuilder TradeBuilder { get; }
/// <summary>
/// Initialise the Algorithm and Prepare Required Data:
/// </summary>
@@ -261,7 +261,7 @@ namespace QuantConnect.Tests.Common.Securities.Forex
private Security CreateSecurity()
{
var config = CreateTradeBarDataConfig(SecurityType.Forex, Symbol);
var security = new Security(SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork), config, 1);
var security = new QuantConnect.Securities.Forex.Forex(SecurityExchangeHours.AlwaysOpen(TimeZones.NewYork), new Cash("abc", 0, 0), config, 1);
return security;
}
}
@@ -45,12 +45,14 @@ namespace QuantConnect.Tests.Common.Securities
var fills = XDocument.Load(fillsFile).Descendants("OrderEvent").Select(x => new OrderEvent(
x.Get<int>("OrderId"),
x.Get<string>("Symbol"),
DateTime.MinValue,
x.Get<OrderStatus>("Status"),
x.Get<int>("FillQuantity") < 0 ? OrderDirection.Sell
: x.Get<int>("FillQuantity") > 0 ? OrderDirection.Buy
: OrderDirection.Hold,
x.Get<decimal>("FillPrice"),
x.Get<int>("FillQuantity"))
x.Get<int>("FillQuantity"),
0m)
).ToList();
var equity = XDocument.Load(equityFile).Descendants("decimal")
@@ -97,12 +99,14 @@ namespace QuantConnect.Tests.Common.Securities
var fills = XDocument.Load(fillsFile).Descendants("OrderEvent").Select(x => new OrderEvent(
x.Get<int>("OrderId"),
x.Get<string>("Symbol"),
DateTime.MinValue,
x.Get<OrderStatus>("Status"),
x.Get<int>("FillQuantity") < 0 ? OrderDirection.Sell
: x.Get<int>("FillQuantity") > 0 ? OrderDirection.Buy
: OrderDirection.Hold,
x.Get<decimal>("FillPrice"),
x.Get<int>("FillQuantity"))
x.Get<int>("FillQuantity"),
0)
).ToList();
var equity = XDocument.Load(equityFile).Descendants("decimal")
@@ -218,7 +222,7 @@ namespace QuantConnect.Tests.Common.Securities
security.SetMarketPrice(new TradeBar(time, "AAPL", buyPrice, buyPrice, buyPrice, buyPrice, 1));
var order = new MarketOrder("AAPL", quantity, time) {Price = buyPrice};
var fill = new OrderEvent(order){FillPrice = buyPrice, FillQuantity = quantity};
var fill = new OrderEvent(order, DateTime.UtcNow, 0) { FillPrice = buyPrice, FillQuantity = quantity };
Assert.AreEqual(portfolio.CashBook["USD"].Quantity, fill.FillPrice*fill.FillQuantity);
@@ -268,7 +272,7 @@ namespace QuantConnect.Tests.Common.Securities
security.SetLeverage(leverage * 2);
order = new MarketOrder("AAPL", quantity, time) { Price = buyPrice };
fill = new OrderEvent(order) { FillPrice = buyPrice, FillQuantity = quantity };
fill = new OrderEvent(order, DateTime.UtcNow, 0) { FillPrice = buyPrice, FillQuantity = quantity };
portfolio.ProcessFill(fill);
@@ -353,7 +357,7 @@ namespace QuantConnect.Tests.Common.Securities
securities.Add("AAPL", new Security(SecurityExchangeHours, CreateTradeBarDataConfig(SecurityType.Equity, "AAPL"), 1));
var fill = new OrderEvent(1, "AAPL", OrderStatus.Filled, OrderDirection.Sell, 100, -100);
var fill = new OrderEvent(1, "AAPL", DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 100, -100, 0);
portfolio.ProcessFill(fill);
Assert.AreEqual(100 * 100, portfolio.Cash);
@@ -371,7 +375,7 @@ namespace QuantConnect.Tests.Common.Securities
securities.Add("AAPL", new Security(SecurityExchangeHours, CreateTradeBarDataConfig(SecurityType.Equity, "AAPL"), 1));
securities["AAPL"].Holdings.SetHoldings(100, 100);
var fill = new OrderEvent(1, "AAPL", OrderStatus.Filled, OrderDirection.Sell, 100, -100);
var fill = new OrderEvent(1, "AAPL", DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 100, -100, 0);
portfolio.ProcessFill(fill);
Assert.AreEqual(100 * 100, portfolio.Cash);
@@ -389,7 +393,7 @@ namespace QuantConnect.Tests.Common.Securities
securities.Add("AAPL", new Security(SecurityExchangeHours, CreateTradeBarDataConfig(SecurityType.Equity, "AAPL"), 1));
securities["AAPL"].Holdings.SetHoldings(100, -100);
var fill = new OrderEvent(1, "AAPL", OrderStatus.Filled, OrderDirection.Sell, 100, -100);
var fill = new OrderEvent(1, "AAPL", DateTime.MinValue, OrderStatus.Filled, OrderDirection.Sell, 100, -100, 0);
Assert.AreEqual(-100, securities["AAPL"].Holdings.Quantity);
portfolio.ProcessFill(fill);
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,557 @@
/*
* 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.Generic;
using NUnit.Framework;
using QuantConnect.Statistics;
namespace QuantConnect.Tests.Common.Statistics
{
[TestFixture]
class TradeStatisticsTests
{
private const string Symbol = "EURUSD";
private const decimal TradeFee = 2;
private readonly DateTime _startTime = new DateTime(2015, 08, 06, 15, 30, 0);
[Test]
public void NoTrades()
{
var statistics = new TradeStatistics(new List<Trade>());
Assert.AreEqual(null, statistics.StartDateTime);
Assert.AreEqual(null, statistics.EndDateTime);
Assert.AreEqual(0, statistics.TotalNumberOfTrades);
Assert.AreEqual(0, statistics.NumberOfWinningTrades);
Assert.AreEqual(0, statistics.NumberOfLosingTrades);
Assert.AreEqual(0, statistics.TotalProfitLoss);
Assert.AreEqual(0, statistics.TotalProfit);
Assert.AreEqual(0, statistics.TotalLoss);
Assert.AreEqual(0, statistics.LargestProfit);
Assert.AreEqual(0, statistics.LargestLoss);
Assert.AreEqual(0, statistics.AverageProfitLoss);
Assert.AreEqual(0, statistics.AverageProfit);
Assert.AreEqual(0, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.Zero, statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.Zero, statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.Zero, statistics.AverageLosingTradeDuration);
Assert.AreEqual(0, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(0, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0, statistics.ProfitLossRatio);
Assert.AreEqual(0, statistics.WinLossRatio);
Assert.AreEqual(0, statistics.WinRate);
Assert.AreEqual(0, statistics.LossRate);
Assert.AreEqual(0, statistics.AverageMAE);
Assert.AreEqual(0, statistics.AverageMFE);
Assert.AreEqual(0, statistics.LargestMAE);
Assert.AreEqual(0, statistics.LargestMFE);
Assert.AreEqual(0, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(0, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(0, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(0, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(0, statistics.ProfitFactor);
Assert.AreEqual(0, statistics.SharpeRatio);
Assert.AreEqual(0, statistics.SortinoRatio);
Assert.AreEqual(0, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(0, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(0, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration);
Assert.AreEqual(0, statistics.TotalFees);
}
[Test]
public void ThreeWinners()
{
var statistics = new TradeStatistics(CreateThreeWinners());
Assert.AreEqual(_startTime, statistics.StartDateTime);
Assert.AreEqual(_startTime.AddMinutes(40), statistics.EndDateTime);
Assert.AreEqual(3, statistics.TotalNumberOfTrades);
Assert.AreEqual(3, statistics.NumberOfWinningTrades);
Assert.AreEqual(0, statistics.NumberOfLosingTrades);
Assert.AreEqual(50, statistics.TotalProfitLoss);
Assert.AreEqual(50, statistics.TotalProfit);
Assert.AreEqual(0, statistics.TotalLoss);
Assert.AreEqual(20, statistics.LargestProfit);
Assert.AreEqual(0, statistics.LargestLoss);
Assert.AreEqual(16.666666666666666666666666667m, statistics.AverageProfitLoss);
Assert.AreEqual(16.666666666666666666666666667m, statistics.AverageProfit);
Assert.AreEqual(0, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.FromMinutes(20), statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(20), statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.Zero, statistics.AverageLosingTradeDuration);
Assert.AreEqual(3, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(0, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0, statistics.ProfitLossRatio);
Assert.AreEqual(10, statistics.WinLossRatio);
Assert.AreEqual(1, statistics.WinRate);
Assert.AreEqual(0, statistics.LossRate);
Assert.AreEqual(-16.666666666666666666666666667m, statistics.AverageMAE);
Assert.AreEqual(33.333333333333333333333333333m, statistics.AverageMFE);
Assert.AreEqual(-30, statistics.LargestMAE);
Assert.AreEqual(40, statistics.LargestMFE);
Assert.AreEqual(0, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(-70, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(5.77350269189626m, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(0, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(10, statistics.ProfitFactor);
Assert.AreEqual(2.8867513459481276450914878051m, statistics.SharpeRatio);
Assert.AreEqual(0, statistics.SortinoRatio);
Assert.AreEqual(10, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(-20, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(-16.666666666666666666666666666m, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration);
Assert.AreEqual(6, statistics.TotalFees);
}
private IEnumerable<Trade> CreateThreeWinners()
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbol = Symbol,
EntryTime = time,
EntryPrice = 1.07m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = 20,
TotalFees = TradeFee,
MAE = -5,
MFE = 30
},
new Trade
{
Symbol = Symbol,
EntryTime = time.AddMinutes(10),
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 2000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = 20,
TotalFees = TradeFee,
MAE = -30,
MFE = 40
},
new Trade
{
Symbol = Symbol,
EntryTime = time.AddMinutes(30),
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30
}
};
}
[Test]
public void ThreeLosers()
{
var statistics = new TradeStatistics(CreateThreeLosers());
Assert.AreEqual(_startTime, statistics.StartDateTime);
Assert.AreEqual(_startTime.AddMinutes(40), statistics.EndDateTime);
Assert.AreEqual(3, statistics.TotalNumberOfTrades);
Assert.AreEqual(0, statistics.NumberOfWinningTrades);
Assert.AreEqual(3, statistics.NumberOfLosingTrades);
Assert.AreEqual(-50, statistics.TotalProfitLoss);
Assert.AreEqual(0, statistics.TotalProfit);
Assert.AreEqual(-50, statistics.TotalLoss);
Assert.AreEqual(0, statistics.LargestProfit);
Assert.AreEqual(-20, statistics.LargestLoss);
Assert.AreEqual(-16.666666666666666666666666667m, statistics.AverageProfitLoss);
Assert.AreEqual(0, statistics.AverageProfit);
Assert.AreEqual(-16.666666666666666666666666667m, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.FromMinutes(20), statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.Zero, statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(20), statistics.AverageLosingTradeDuration);
Assert.AreEqual(0, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(3, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0, statistics.ProfitLossRatio);
Assert.AreEqual(0, statistics.WinLossRatio);
Assert.AreEqual(0, statistics.WinRate);
Assert.AreEqual(1, statistics.LossRate);
Assert.AreEqual(-33.333333333333333333333333333m, statistics.AverageMAE);
Assert.AreEqual(16.666666666666666666666666667m, statistics.AverageMFE);
Assert.AreEqual(-40, statistics.LargestMAE);
Assert.AreEqual(30, statistics.LargestMFE);
Assert.AreEqual(-50, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(-80, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(5.77350269189626m, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(5.77350269189626m, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(0, statistics.ProfitFactor);
Assert.AreEqual(-2.8867513459481276450914878051m, statistics.SharpeRatio);
Assert.AreEqual(-2.8867513459481276450914878051m, statistics.SortinoRatio);
Assert.AreEqual(-1, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(-50, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(-33.333333333333333333333333334m, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration);
Assert.AreEqual(6, statistics.TotalFees);
}
private IEnumerable<Trade> CreateThreeLosers()
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbol = Symbol,
EntryTime = time,
EntryPrice = 1.07m,
Direction = TradeDirection.Short,
Quantity = 1000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5
},
new Trade
{
Symbol = Symbol,
EntryTime = time.AddMinutes(10),
EntryPrice = 1.08m,
Direction = TradeDirection.Short,
Quantity = 2000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30
},
new Trade
{
Symbol = Symbol,
EntryTime = time.AddMinutes(30),
EntryPrice = 1.08m,
Direction = TradeDirection.Short,
Quantity = 1000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = -10,
TotalFees = TradeFee,
MAE = -30,
MFE = 15
}
};
}
[Test]
public void TwoLosersOneWinner()
{
var statistics = new TradeStatistics(CreateTwoLosersOneWinner());
Assert.AreEqual(_startTime, statistics.StartDateTime);
Assert.AreEqual(_startTime.AddMinutes(40), statistics.EndDateTime);
Assert.AreEqual(3, statistics.TotalNumberOfTrades);
Assert.AreEqual(1, statistics.NumberOfWinningTrades);
Assert.AreEqual(2, statistics.NumberOfLosingTrades);
Assert.AreEqual(-30, statistics.TotalProfitLoss);
Assert.AreEqual(10, statistics.TotalProfit);
Assert.AreEqual(-40, statistics.TotalLoss);
Assert.AreEqual(10, statistics.LargestProfit);
Assert.AreEqual(-20, statistics.LargestLoss);
Assert.AreEqual(-10, statistics.AverageProfitLoss);
Assert.AreEqual(10, statistics.AverageProfit);
Assert.AreEqual(-20, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.FromSeconds(800), statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(10), statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(15), statistics.AverageLosingTradeDuration);
Assert.AreEqual(1, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(2, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0.5m, statistics.ProfitLossRatio);
Assert.AreEqual(0.5m, statistics.WinLossRatio);
Assert.AreEqual(0.3333333333333333333333333333m, statistics.WinRate);
Assert.AreEqual(0.6666666666666666666666666667m, statistics.LossRate);
Assert.AreEqual(-28.333333333333333333333333333333m, statistics.AverageMAE);
Assert.AreEqual(21.666666666666666666666666666667m, statistics.AverageMFE);
Assert.AreEqual(-40, statistics.LargestMAE);
Assert.AreEqual(30, statistics.LargestMFE);
Assert.AreEqual(-40, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(-70, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(17.3205080756888m, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(0, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(0.25m, statistics.ProfitFactor);
Assert.AreEqual(-0.5773502691896248623516308943m, statistics.SharpeRatio);
Assert.AreEqual(0, statistics.SortinoRatio);
Assert.AreEqual(-0.75m, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(-50, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(-31.666666666666666666666666666667m, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration);
Assert.AreEqual(6, statistics.TotalFees);
}
private IEnumerable<Trade> CreateTwoLosersOneWinner()
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbol = Symbol,
EntryTime = time,
EntryPrice = 1.07m,
Direction = TradeDirection.Short,
Quantity = 1000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5
},
new Trade
{
Symbol = Symbol,
EntryTime = time.AddMinutes(10),
EntryPrice = 1.08m,
Direction = TradeDirection.Short,
Quantity = 2000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30
},
new Trade
{
Symbol = Symbol,
EntryTime = time.AddMinutes(30),
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30
}
};
}
[Test]
public void OneWinnerTwoLosers()
{
var statistics = new TradeStatistics(CreateOneWinnerTwoLosers());
Assert.AreEqual(_startTime, statistics.StartDateTime);
Assert.AreEqual(_startTime.AddMinutes(40), statistics.EndDateTime);
Assert.AreEqual(3, statistics.TotalNumberOfTrades);
Assert.AreEqual(1, statistics.NumberOfWinningTrades);
Assert.AreEqual(2, statistics.NumberOfLosingTrades);
Assert.AreEqual(-30, statistics.TotalProfitLoss);
Assert.AreEqual(10, statistics.TotalProfit);
Assert.AreEqual(-40, statistics.TotalLoss);
Assert.AreEqual(10, statistics.LargestProfit);
Assert.AreEqual(-20, statistics.LargestLoss);
Assert.AreEqual(-10, statistics.AverageProfitLoss);
Assert.AreEqual(10, statistics.AverageProfit);
Assert.AreEqual(-20, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.FromSeconds(800), statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(10), statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(15), statistics.AverageLosingTradeDuration);
Assert.AreEqual(1, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(2, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0.5m, statistics.ProfitLossRatio);
Assert.AreEqual(0.5m, statistics.WinLossRatio);
Assert.AreEqual(0.3333333333333333333333333333m, statistics.WinRate);
Assert.AreEqual(0.6666666666666666666666666667m, statistics.LossRate);
Assert.AreEqual(-28.333333333333333333333333333333m, statistics.AverageMAE);
Assert.AreEqual(21.666666666666666666666666666667m, statistics.AverageMFE);
Assert.AreEqual(-40, statistics.LargestMAE);
Assert.AreEqual(30, statistics.LargestMFE);
Assert.AreEqual(-40, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(-80, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(17.3205080756888m, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(0, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(0.25m, statistics.ProfitFactor);
Assert.AreEqual(-0.5773502691896248623516308943m, statistics.SharpeRatio);
Assert.AreEqual(0, statistics.SortinoRatio);
Assert.AreEqual(-0.75m, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(-50, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(-31.666666666666666666666666666667m, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.Zero, statistics.MaximumDrawdownDuration);
Assert.AreEqual(6, statistics.TotalFees);
}
private IEnumerable<Trade> CreateOneWinnerTwoLosers()
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbol = Symbol,
EntryTime = time,
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(10),
ExitPrice = 1.09m,
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30
},
new Trade
{
Symbol = Symbol,
EntryTime = time.AddMinutes(20),
EntryPrice = 1.07m,
Direction = TradeDirection.Short,
Quantity = 1000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5
},
new Trade
{
Symbol = Symbol,
EntryTime = time.AddMinutes(30),
EntryPrice = 1.08m,
Direction = TradeDirection.Short,
Quantity = 2000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30
}
};
}
[Test]
public void OneLoserTwoWinners()
{
var statistics = new TradeStatistics(CreateOneLoserTwoWinners());
Assert.AreEqual(_startTime, statistics.StartDateTime);
Assert.AreEqual(_startTime.AddMinutes(40), statistics.EndDateTime);
Assert.AreEqual(3, statistics.TotalNumberOfTrades);
Assert.AreEqual(2, statistics.NumberOfWinningTrades);
Assert.AreEqual(1, statistics.NumberOfLosingTrades);
Assert.AreEqual(10, statistics.TotalProfitLoss);
Assert.AreEqual(30, statistics.TotalProfit);
Assert.AreEqual(-20, statistics.TotalLoss);
Assert.AreEqual(20, statistics.LargestProfit);
Assert.AreEqual(-20, statistics.LargestLoss);
Assert.AreEqual(3.3333333333333333333333333333m, statistics.AverageProfitLoss);
Assert.AreEqual(15, statistics.AverageProfit);
Assert.AreEqual(-20, statistics.AverageLoss);
Assert.AreEqual(TimeSpan.FromSeconds(800), statistics.AverageTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(10), statistics.AverageWinningTradeDuration);
Assert.AreEqual(TimeSpan.FromMinutes(20), statistics.AverageLosingTradeDuration);
Assert.AreEqual(2, statistics.MaxConsecutiveWinningTrades);
Assert.AreEqual(1, statistics.MaxConsecutiveLosingTrades);
Assert.AreEqual(0.75m, statistics.ProfitLossRatio);
Assert.AreEqual(2, statistics.WinLossRatio);
Assert.AreEqual(0.6666666666666666666666666667m, statistics.WinRate);
Assert.AreEqual(0.3333333333333333333333333333m, statistics.LossRate);
Assert.AreEqual(-28.333333333333333333333333333333m, statistics.AverageMAE);
Assert.AreEqual(21.666666666666666666666666666667m, statistics.AverageMFE);
Assert.AreEqual(-40, statistics.LargestMAE);
Assert.AreEqual(30, statistics.LargestMFE);
Assert.AreEqual(-20, statistics.MaximumClosedTradeDrawdown);
Assert.AreEqual(-70, statistics.MaximumIntraTradeDrawdown);
Assert.AreEqual(20.8166599946613m, statistics.ProfitLossStandardDeviation);
Assert.AreEqual(0, statistics.ProfitLossDownsideDeviation);
Assert.AreEqual(1.5m, statistics.ProfitFactor);
Assert.AreEqual(0.1601281538050873438895842626m, statistics.SharpeRatio);
Assert.AreEqual(0, statistics.SortinoRatio);
Assert.AreEqual(0.5m, statistics.ProfitToMaxDrawdownRatio);
Assert.AreEqual(-25, statistics.MaximumEndTradeDrawdown);
Assert.AreEqual(-18.333333333333333333333333334m, statistics.AverageEndTradeDrawdown);
Assert.AreEqual(TimeSpan.FromMinutes(40), statistics.MaximumDrawdownDuration);
Assert.AreEqual(6, statistics.TotalFees);
}
private IEnumerable<Trade> CreateOneLoserTwoWinners()
{
var time = _startTime;
return new List<Trade>
{
new Trade
{
Symbol = Symbol,
EntryTime = time,
EntryPrice = 1.07m,
Direction = TradeDirection.Short,
Quantity = 1000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = -20,
TotalFees = TradeFee,
MAE = -30,
MFE = 5
},
new Trade
{
Symbol = Symbol,
EntryTime = time.AddMinutes(10),
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 2000,
ExitTime = time.AddMinutes(20),
ExitPrice = 1.09m,
ProfitLoss = 20,
TotalFees = TradeFee,
MAE = -40,
MFE = 30
},
new Trade
{
Symbol = Symbol,
EntryTime = time.AddMinutes(30),
EntryPrice = 1.08m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(40),
ExitPrice = 1.09m,
ProfitLoss = 10,
TotalFees = TradeFee,
MAE = -15,
MFE = 30
}
};
}
}
}
+2
View File
@@ -96,6 +96,8 @@
<Compile Include="Common\Securities\SymbolTests.cs" />
<Compile Include="Common\SeriesTests.cs" />
<Compile Include="Common\SymbolTests.cs" />
<Compile Include="Common\Statistics\TradeStatisticsTests.cs" />
<Compile Include="Common\Statistics\TradeBuilderTests.cs" />
<Compile Include="Common\TimeKeeperTests.cs" />
<Compile Include="Common\TimeTests.cs" />
<Compile Include="Common\TimeZoneOffsetProviderTests.cs" />