/*
* 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.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.Linq;
using NUnit.Framework;
using QuantConnect.Data;
using QuantConnect.Statistics;
using QuantConnect.Tests.Indicators;
using static Microsoft.FSharp.Core.ByRefKinds;
namespace QuantConnect.Tests.Common.Statistics
{
[TestFixture]
class PortfolioStatisticsTests
{
private const decimal TradeFee = 2;
private readonly DateTime _startTime = new DateTime(2015, 08, 06, 15, 30, 0);
///
/// TradingDaysPerYear: Use like backward compatibility
///
///
protected const int _tradingDaysPerYear = 252;
[Test]
public void ITMOptionAssignment([Values] bool win)
{
var statistics = GetPortfolioStatistics(win, _tradingDaysPerYear, new List { 0, 0 }, new List { 0, 0 });
if (win)
{
Assert.AreEqual(1m, statistics.WinRate);
Assert.AreEqual(0m, statistics.LossRate);
}
else
{
Assert.AreEqual(0.5m, statistics.WinRate);
Assert.AreEqual(0.5m, statistics.LossRate);
}
Assert.AreEqual(0.1173913043478260869565217391m, statistics.AverageWinRate);
Assert.AreEqual(-0.08m, statistics.AverageLossRate);
Assert.AreEqual(1.4673913043478260869565217388m, statistics.ProfitLossRatio);
}
public static IEnumerable StatisticsCases
{
get
{
yield return new TestCaseData(202, 0.00589787137120101M, 0.0767976000354244M, -3.0952570635188M, 0.167632655086644M, 0.252197874915608M);
yield return new TestCaseData(252, 0.00735774052248839M, 0.0857772727620108M, -3.3486737318423M, 0.187233350684845M, 0.257146306116665M);
yield return new TestCaseData(365, 0.0106570448043979M, 0.103232963748978M, -3.75507953923657M, 0.225335372429895M, 0.264390639112978M);
}
}
[TestCaseSource(nameof(StatisticsCases))]
public void ITMOptionAssignmentWithDifferentTradingDaysPerYearValue(
int tradingDaysPerYear, decimal expectedAnnualVariance, decimal expectedAnnualStandardDeviation,
decimal expectedSharpeRatio, decimal expectedTrackingError, decimal expectedProbabilisticSharpeRatio)
{
var listPerformance = new List { -0.009025132, 0.003653969, 0, 0 };
var listBenchmark = new List { -0.011587791300935783, 0.00054375782787618543, 0.022165997700413956, 0.006263266301918822 };
var statistics = GetPortfolioStatistics(true, tradingDaysPerYear, listPerformance, listBenchmark);
Assert.AreEqual(expectedAnnualVariance, statistics.AnnualVariance);
Assert.AreEqual(expectedAnnualStandardDeviation, statistics.AnnualStandardDeviation);
Assert.AreEqual(expectedSharpeRatio, statistics.SharpeRatio);
Assert.AreEqual(expectedTrackingError, statistics.TrackingError);
Assert.AreEqual(expectedProbabilisticSharpeRatio, statistics.ProbabilisticSharpeRatio);
}
[Test]
public void VaRMatchesExternalData()
{
var externalFileName = "spy_valueatrisk.csv";
var data = TestHelper.GetCsvFileStream(externalFileName);
var listPerformance = new List();
var iteration = 0;
foreach (var row in data)
{
if (iteration == 0)
{
iteration++;
continue;
}
Parse.TryParse(row["returns"], NumberStyles.Float, out double returns);
listPerformance.Add(returns);
Parse.TryParse(row["VaR_99"], NumberStyles.Float, out decimal expected99);
Parse.TryParse(row["VaR_95"], NumberStyles.Float, out decimal expected95);
var statistics = GetPortfolioStatistics(
true,
_tradingDaysPerYear,
listPerformance,
new List { 0, 0 });
Assert.AreEqual(Math.Round(expected99, 3), statistics.ValueAtRisk99);
Assert.AreEqual(Math.Round(expected95, 3), statistics.ValueAtRisk95);
}
}
[Test]
public void VaRIsZeroIfLessThan2Samples()
{
var listPerformance = new List { 0.006196177273682046 };
var statistics = GetPortfolioStatistics(
true,
_tradingDaysPerYear,
listPerformance,
new List { 0, 0 });
Assert.Zero(statistics.ValueAtRisk99);
Assert.Zero(statistics.ValueAtRisk95);
}
[Test]
public void PortfolioStatisticsDoesNotFailWhenAnnualPerformanceIsLarge()
{
var profitLoss = new SortedDictionary();
var equity = new SortedDictionary();
var portfolioTurnover = new SortedDictionary();
var listPerformance = new List() { 0.6281421, 2.3815, -0.620932, 0.2795571 };
var listBenchmark = new List() { -0.0015610669230773247, -0.024440492469623223, 0.008600225248460628, -0.020019532547249266 };
var startingCapital = 100000;
var riskFreeInterestRateModel = new InterestRateProvider();
var tradingDaysPerYear = 252;
Assert.DoesNotThrow(() => new PortfolioStatistics(profitLoss, equity, portfolioTurnover, listPerformance, listBenchmark, startingCapital, riskFreeInterestRateModel, tradingDaysPerYear));
}
///
/// Initialize and return Portfolio Statistics depends on input data
///
/// create profitable trade or not
/// amount days per year for brokerage (e.g. crypto exchange use 365 days)
/// The list of algorithm performance values
/// The list of benchmark values
/// The class represents a set of statistics calculated from equity and benchmark samples
private PortfolioStatistics GetPortfolioStatistics(bool win, int tradingDaysPerYear, List listPerformance, List listBenchmark)
{
var trades = CreateITMOptionAssignment(win);
var profitLoss = new SortedDictionary(trades.ToDictionary(x => x.ExitTime, x => x.ProfitLoss));
var winCount = trades.Count(x => x.IsWin);
var lossCount = trades.Count - winCount;
return new PortfolioStatistics(profitLoss, new SortedDictionary(),
new SortedDictionary(), listPerformance, listBenchmark, 100000,
new InterestRateProvider(), tradingDaysPerYear, winCount, lossCount);
}
private List CreateITMOptionAssignment(bool win)
{
var time = _startTime;
return new List
{
new Trade
{
Symbol = Symbols.SPY_C_192_Feb19_2016,
EntryTime = time,
EntryPrice = 80m,
Direction = TradeDirection.Long,
Quantity = 10,
ExitTime = time.AddMinutes(20),
ExitPrice = 0m,
ProfitLoss = -8000m,
TotalFees = TradeFee,
MAE = -8000m,
MFE = 0,
IsWin = win
},
new Trade
{
Symbol = Symbols.SPY,
EntryTime = time.AddMinutes(20),
EntryPrice = 192m,
Direction = TradeDirection.Long,
Quantity = 1000,
ExitTime = time.AddMinutes(30),
ExitPrice = 300m,
ProfitLoss = 10800m,
TotalFees = TradeFee,
MAE = 0,
MFE = 10800m,
IsWin = true
},
};
}
}
}