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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.")]
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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) { }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user