/*
* 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 Deedle;
using QuantConnect.Orders;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Report
{
///
/// Strategy metrics collection such as usage of funds and asset allocations
///
public static class Metrics
{
///
/// Calculates the leverage used from trades. The series used to call this extension function should
/// be the equity curve with the associated objects that go along with it.
///
/// Equity curve series
/// Orders associated with the equity curve
/// Leverage utilization over time
public static Series LeverageUtilization(Series equityCurve, List orders)
{
if (equityCurve.IsEmpty || orders.Count == 0)
{
return new Series(new DateTime[] { }, new double[] { });
}
var pointInTimePortfolios = PortfolioLooper.FromOrders(equityCurve, orders)
.ToList(); // Required because for some reason our AbsoluteHoldingsValue is multiplied by two whenever we GroupBy on the raw IEnumerable
return LeverageUtilization(pointInTimePortfolios);
}
///
/// Gets the leverage utilization from a list of
///
/// Point in time portfolios
/// Series of leverage utilization
public static Series LeverageUtilization(List portfolios)
{
var leverage = portfolios.GroupBy(portfolio => portfolio.Time)
.Select(group => new KeyValuePair(group.Key, (double)group.Last().Leverage))
.ToList();
// Drop missing because we don't care about the missing values
return new Series(leverage).DropMissing();
}
///
/// Calculates the portfolio's asset allocation percentage over time. The series used to call this extension function should
/// be the equity curve with the associated objects that go along with it.
///
/// Equity curve series
/// Orders associated with the equity curve
///
public static Series AssetAllocations(Series equityCurve, List orders)
{
if (equityCurve.IsEmpty || orders.Count == 0)
{
return new Series(new Symbol[] { }, new double[] { });
}
// Convert PointInTimePortfolios to List because for some reason our AbsoluteHoldingsValue is multiplied by two whenever we GroupBy on the raw IEnumerable
return AssetAllocations(PortfolioLooper.FromOrders(equityCurve, orders).ToList());
}
///
/// Calculates the asset allocation percentage over time.
///
/// Point in time portfolios
/// Series keyed by Symbol containing the percentage allocated to that asset over time
public static Series AssetAllocations(List portfolios)
{
var portfolioHoldings = portfolios.GroupBy(x => x.Time)
.Select(kvp => kvp.Last())
.ToList();
var totalPortfolioValueOverTime = (double)portfolioHoldings.Sum(x => x.Holdings.Sum(y => y.AbsoluteHoldingsValue));
var holdingsBySymbolOverTime = new Dictionary();
foreach (var portfolio in portfolioHoldings)
{
foreach (var holding in portfolio.Holdings)
{
if (!holdingsBySymbolOverTime.ContainsKey(holding.Symbol))
{
holdingsBySymbolOverTime[holding.Symbol] = (double)holding.AbsoluteHoldingsValue;
continue;
}
holdingsBySymbolOverTime[holding.Symbol] = holdingsBySymbolOverTime[holding.Symbol] + (double)holding.AbsoluteHoldingsValue;
}
}
return new Series(
holdingsBySymbolOverTime.Keys,
holdingsBySymbolOverTime.Values.Select(x => x / totalPortfolioValueOverTime).ToList()
).DropMissing();
}
///
/// Strategy long/short exposure by asset class
///
/// Equity curve
/// Orders of the strategy
/// Long or short
///
/// Frame keyed by and .
/// Returns a Frame of exposure per asset per direction over time
///
public static Frame> Exposure(Series equityCurve, List orders, OrderDirection direction)
{
if (equityCurve.IsEmpty || orders.Count == 0)
{
return Frame.CreateEmpty>();
}
return Exposure(PortfolioLooper.FromOrders(equityCurve, orders).ToList(), direction);
}
///
/// Strategy long/short exposure by asset class
///
/// Point in time portfolios
/// Long or short
///
/// Frame keyed by and .
/// Returns a Frame of exposure per asset per direction over time
///
public static Frame> Exposure(List portfolios, OrderDirection direction)
{
// We want to add all of the holdings by asset class to a mock dataframe that is column keyed by SecurityType with
// rows being DateTime and values being the exposure at that given time (as double)
var holdingsByAssetClass = new Dictionary>>();
var multiplier = direction == OrderDirection.Sell ? -1 : 1;
foreach (var portfolio in portfolios)
{
List> holdings;
if (!holdingsByAssetClass.TryGetValue(portfolio.Order.SecurityType, out holdings))
{
holdings = new List>();
holdingsByAssetClass[portfolio.Order.SecurityType] = holdings;
}
var assets = portfolio.Holdings
.Where(pointInTimeHoldings => pointInTimeHoldings.Symbol.SecurityType == portfolio.Order.SecurityType)
.ToList();
if (assets.Count > 0)
{
// Use the multiplier to flip the holdings value around
var sum = (double)assets.Where(pointInTimeHoldings => multiplier * pointInTimeHoldings.HoldingsValue > 0)
.Select(pointInTimeHoldings => pointInTimeHoldings.AbsoluteHoldingsValue)
.Sum();
holdings.Add(new KeyValuePair(portfolio.Time, sum / (double)portfolio.TotalPortfolioValue));
}
}
var frame = Frame.CreateEmpty>();
foreach (var kvp in holdingsByAssetClass)
{
// Skip Base asset class since we need it as a special value
// (and it can't be traded on either way)
if (kvp.Key == SecurityType.Base)
{
continue;
}
// Select the last entry of a given time to get accurate results of the portfolio's actual value.
// Then, select only the long or short holdings.
frame = frame.Join(
new Tuple(kvp.Key, direction),
new Series(kvp.Value.GroupBy(x => x.Key).Select(x => x.Last())) * multiplier
);
}
// Equivalent to `pd.fillna(method='ffill').dropna(axis=1, how='all').dropna(how='all')`
// First drops any missing SecurityTypes, then drops the rows with missing values
// to get rid of any empty data prior to the first value.
return frame.FillMissing(Direction.Forward)
.DropSparseColumnsAll()
.DropSparseRowsAll();
}
}
}