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