public static async Task <string> GenerateQuickTesterResponse(GeneralStrategyParameters p_generalParams, string p_strategyName, string p_params) { Stopwatch stopWatchTotalResponse = Stopwatch.StartNew(); if (p_strategyName != "LETFDiscrepancy1" && p_strategyName != "LETFDiscrepancy2" && p_strategyName != "LETFDiscrepancy3") { return(null); } string strategyParams = p_params; int ind = -1; string etfPairs = null; if (strategyParams.StartsWith("ETFPairs=", StringComparison.InvariantCultureIgnoreCase)) { strategyParams = strategyParams.Substring("ETFPairs=".Length); ind = strategyParams.IndexOf('&'); if (ind == -1) { return(@"{ ""errorMessage"": ""Error: uriQuery.IndexOf('&') 2. Uri: " + strategyParams + @""" }"); } etfPairs = strategyParams.Substring(0, ind); strategyParams = strategyParams.Substring(ind + 1); } string rebalancingFrequency = null; if (strategyParams.StartsWith("rebalancingFrequency=", StringComparison.InvariantCultureIgnoreCase)) { strategyParams = strategyParams.Substring("rebalancingFrequency=".Length); ind = strategyParams.IndexOf('&'); if (ind == -1) { ind = strategyParams.Length; } rebalancingFrequency = strategyParams.Substring(0, ind); if (ind < strategyParams.Length) { strategyParams = strategyParams.Substring(ind + 1); } else { strategyParams = ""; } } ind = etfPairs.IndexOf('-'); if (ind == -1) { return(@"{ ""errorMessage"": ""Error: cannot find tickers in : " + etfPairs + @""" }"); } string bullishTicker = etfPairs.Substring(0, ind); string bearishTicker = etfPairs.Substring(ind + 1); // startDates // URE: Feb 2, 2007 // SRS: Feb 1, 2007 // XIV: Nov 30, 2010 // VXX: Jan 30, 2009 // FAS: Nov 19, 2008 // FAZ: Nov 19, 2008 Stopwatch stopWatch = Stopwatch.StartNew(); var getAllQuotesTask = StrategiesCommon.GetHistoricalAndRealtimesQuotesAsync(p_generalParams, (new string[] { bullishTicker, bearishTicker }).ToList()); // Control returns here before GetHistoricalQuotesAsync() returns. // ... Prompt the user. Console.WriteLine("Please wait patiently while I do SQL and realtime price queries."); var getAllQuotesData = await getAllQuotesTask; stopWatch.Stop(); string htmlNoteFromStrategy = "", noteToUserCheckData = "", noteToUserBacktest = "", debugMessage = "", errorMessage = ""; List <DailyData> pv = null; if (String.Equals(p_strategyName, "LETFDiscrepancy1", StringComparison.InvariantCultureIgnoreCase)) { pv = DoBacktestExample(getAllQuotesData.Item1, bullishTicker, bearishTicker, rebalancingFrequency); } else { pv = DoBacktestBasic(getAllQuotesData.Item1, bullishTicker, bearishTicker, p_strategyName, rebalancingFrequency, ref noteToUserCheckData, ref htmlNoteFromStrategy); } stopWatchTotalResponse.Stop(); StrategyResult strategyResult = StrategiesCommon.CreateStrategyResultFromPV(pv, htmlNoteFromStrategy + ". " + noteToUserCheckData + "***" + noteToUserBacktest, errorMessage, debugMessage + String.Format("SQL query time: {0:000}ms", getAllQuotesData.Item2.TotalMilliseconds) + String.Format(", RT query time: {0:000}ms", getAllQuotesData.Item3.TotalMilliseconds) + String.Format(", All query time: {0:000}ms", stopWatch.Elapsed.TotalMilliseconds) + String.Format(", TotalC#Response: {0:000}ms", stopWatchTotalResponse.Elapsed.TotalMilliseconds)); string jsonReturn = JsonConvert.SerializeObject(strategyResult); return(jsonReturn); //{ // "Name": "Apple", // "Expiry": "2008-12-28T00:00:00", // "Sizes": [ // "Small" // ] //} //returnStr = "[" + String.Join(Environment.NewLine, // (await Tools.GetHistoricalQuotesAsync(new[] { // new QuoteRequest { Ticker = "VXX", nQuotes = 2, StartDate = new DateTime(2011,1,1), NonAdjusted = true }, // new QuoteRequest { Ticker = "SPY", nQuotes = 3 } // }, HQCommon.AssetType.Stock)) // .Select(row => String.Join(",", row))) + "]"; //returnStr = returnStr.Replace(" 00:00:00", ""); //returnStr = returnStr.Replace("\n", ","); //return @"[{""Symbol"":""VXX""},{""Symbol"":""^VIX"",""LastUtc"":""2015-01-08T19:25:48"",""Last"":17.45,""UtcTimeType"":""LastChangedTime""}]"; }
public static async Task<string> GenerateQuickTesterResponse(GeneralStrategyParameters p_generalParams, string p_strategyName, string p_params) { Stopwatch stopWatchTotalResponse = Stopwatch.StartNew(); if (p_strategyName != "VXX_SPY_Controversial") return null; string strategyParams = p_params; int ind = -1; string spyMinPctMoveStr = null; if (strategyParams.StartsWith("SpyMinPctMove=", StringComparison.InvariantCultureIgnoreCase)) { strategyParams = strategyParams.Substring("SpyMinPctMove=".Length); ind = strategyParams.IndexOf('&'); if (ind == -1) { ind = strategyParams.Length; } spyMinPctMoveStr = strategyParams.Substring(0, ind); if (ind < strategyParams.Length) strategyParams = strategyParams.Substring(ind + 1); else strategyParams = ""; } string vxxMinPctMoveStr = null; if (strategyParams.StartsWith("VxxMinPctMove=", StringComparison.InvariantCultureIgnoreCase)) { strategyParams = strategyParams.Substring("VxxMinPctMove=".Length); ind = strategyParams.IndexOf('&'); if (ind == -1) { ind = strategyParams.Length; } vxxMinPctMoveStr = strategyParams.Substring(0, ind); if (ind < strategyParams.Length) strategyParams = strategyParams.Substring(ind + 1); else strategyParams = ""; } string longOrShortTrade = null; if (strategyParams.StartsWith("LongOrShortTrade=", StringComparison.InvariantCultureIgnoreCase)) { strategyParams = strategyParams.Substring("LongOrShortTrade=".Length); ind = strategyParams.IndexOf('&'); if (ind == -1) { ind = strategyParams.Length; } longOrShortTrade = strategyParams.Substring(0, ind); if (ind < strategyParams.Length) strategyParams = strategyParams.Substring(ind + 1); else strategyParams = ""; } double spyMinPctMove; bool isParseSuccess = Double.TryParse(spyMinPctMoveStr, out spyMinPctMove); if (!isParseSuccess) { throw new Exception("Error: spyMinPctMoveStr as " + spyMinPctMoveStr + " cannot be converted to number."); } double vxxMinPctMove; isParseSuccess = Double.TryParse(vxxMinPctMoveStr, out vxxMinPctMove); if (!isParseSuccess) { throw new Exception("Error: vxxMinPctMoveStr as " + vxxMinPctMoveStr + " cannot be converted to number."); } Stopwatch stopWatch = Stopwatch.StartNew(); var getAllQuotesTask = StrategiesCommon.GetHistoricalAndRealtimesQuotesAsync(p_generalParams, (new string[] { "VXX", "SPY" }).ToList()); // Control returns here before GetHistoricalQuotesAsync() returns. // ... Prompt the user. Console.WriteLine("Please wait patiently while I do SQL and realtime price queries."); var getAllQuotesData = await getAllQuotesTask; stopWatch.Stop(); var vxxQoutes = getAllQuotesData.Item1[0]; var spyQoutes = getAllQuotesData.Item1[1]; string noteToUserCheckData = "", noteToUserBacktest = "", debugMessage = "", errorMessage = ""; List<DailyData> pv = StrategiesCommon.DetermineBacktestPeriodCheckDataCorrectness(vxxQoutes, spyQoutes, ref noteToUserCheckData); if (String.Equals(p_strategyName, "VXX_SPY_Controversial", StringComparison.InvariantCultureIgnoreCase)) { DoBacktestInTheTimeInterval_VXX_SPY_Controversial(vxxQoutes, spyQoutes, spyMinPctMove, vxxMinPctMove, longOrShortTrade, pv, ref noteToUserBacktest); } //else if (String.Equals(p_strategyName, "LETFDiscrepancy3", StringComparison.InvariantCultureIgnoreCase)) //{ // //DoBacktestInTheTimeInterval_AddToTheWinningSideWithLeverage(bullishQoutes, bearishQoutes, p_rebalancingFrequency, pv, ref noteToUserBacktest); //} else { } stopWatchTotalResponse.Stop(); StrategyResult strategyResult = StrategiesCommon.CreateStrategyResultFromPV(pv, noteToUserCheckData + "***" + noteToUserBacktest, errorMessage, debugMessage + String.Format("SQL query time: {0:000}ms", getAllQuotesData.Item2.TotalMilliseconds) + String.Format(", RT query time: {0:000}ms", getAllQuotesData.Item3.TotalMilliseconds) + String.Format(", All query time: {0:000}ms", stopWatch.Elapsed.TotalMilliseconds) + String.Format(", TotalC#Response: {0:000}ms", stopWatchTotalResponse.Elapsed.TotalMilliseconds)); string jsonReturn = JsonConvert.SerializeObject(strategyResult); return jsonReturn; }
// Sortino calculation from here: http://www.redrockcapital.com/Sortino__A__Sharper__Ratio_Red_Rock_Capital.pdf //The main reason we wrote this article is because in both literature and trading software packages, //we have seen the Sortino ratio, and in particular the target downside deviation, calculated //incorrectly more often than not. Most often, we see the target downside deviation calculated //by “throwing away all the positive returns and take the standard deviation of negative returns”. //We hope that by reading this article, you can see how this is incorrect // George: a little problem to me, but left it like this: underPerfFromTarget is distance from Zero, while in StDev, it was distance from Avg. public static StrategyResult CreateStrategyResultFromPV(List<DailyData> p_pv, string p_htmlNoteFromStrategy, string p_errorMessage, string p_debugMessage) { //IEnumerable<string> chartDataToSend = pv.Select(row => row.Date.Year + "-" + row.Date.Month + "-" + row.Date.Day + "-" + String.Format("{0:0.00}", row.ClosePrice)); IEnumerable<string> chartDataToSend = p_pv.Select(row => row.Date.Year + "-" + row.Date.Month + "-" + row.Date.Day + "," + String.Format("{0:0.00}", row.ClosePrice >= 0 ? row.ClosePrice : 0.0)); // postprocess: TradingViewChart cannot accept negative numbers DateTime startDate = p_pv[0].Date; DateTime endDate = p_pv[p_pv.Count() - 1].Date; int nTradingDays = p_pv.Count(); double nYears = nTradingDays / 252.0; //https://www.google.co.uk/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=how%20many%20trading%20days%20in%20a%20year double pvStart = p_pv[0].ClosePrice; double pvEnd = p_pv[p_pv.Count() - 1].ClosePrice; double totalGainPct = pvEnd/pvStart - 1.0; double cagr = Math.Pow(totalGainPct + 1, 1.0 / nYears) - 1.0; var dailyReturns = new double[p_pv.Count() - 1]; for (int i = 0; i < p_pv.Count() - 1; i++) { dailyReturns[i] = p_pv[i + 1].ClosePrice / p_pv[i].ClosePrice - 1.0; } double avgReturn = dailyReturns.Average(); double dailyStdDev = Math.Sqrt(dailyReturns.Sum(r => (r - avgReturn) * (r - avgReturn)) / ((double)dailyReturns.Count() - 1.0)); //http://www.styleadvisor.com/content/standard-deviation, "Morningstar uses the sample standard deviation method: divide by n-1 double annualizedStDev = dailyStdDev * Math.Sqrt(252.0); //http://en.wikipedia.org/wiki/Trading_day, http://www.styleadvisor.com/content/annualized-standard-deviation double annualizedSharpeRatio = cagr / annualizedStDev; double sortinoDailyTargetReturn = 0.0; // assume investor is happy with any positive return double sortinoAnnualizedTargetReturn = Math.Pow(sortinoDailyTargetReturn, 252.0); // maybe around annual 3-6% is expected by investor double dailyTargetDownsideDeviation = 0.0; for (int i = 0; i < dailyReturns.Length; i++) { double underPerfFromTarget = dailyReturns[i] - sortinoDailyTargetReturn; if (underPerfFromTarget < 0.0) dailyTargetDownsideDeviation += underPerfFromTarget * underPerfFromTarget; } dailyTargetDownsideDeviation = Math.Sqrt(dailyTargetDownsideDeviation / (double)dailyReturns.Length); // see Sortino PDF for explanation why we use the 0 samples too for the Average double annualizedDailyTargetDownsideDeviation = dailyTargetDownsideDeviation * Math.Sqrt(252.0); //http://en.wikipedia.org/wiki/Trading_day, http://www.styleadvisor.com/content/annualized-standard-deviation //double dailySortinoRatio = (avgReturn - sortinoDailyTargetReturn) / dailyTargetDownsideDeviation; // daily gave too small values double annualizedSortinoRatio = (cagr - sortinoAnnualizedTargetReturn) / annualizedDailyTargetDownsideDeviation; // var drawdowns = new double[p_pv.Count()]; double maxPv = Double.NegativeInfinity; double maxDD = Double.PositiveInfinity; double quadraticMeanDD = 0.0; for (int i = 0; i < p_pv.Count(); i++) { if (p_pv[i].ClosePrice > maxPv) maxPv = p_pv[i].ClosePrice; double dd = p_pv[i].ClosePrice / maxPv - 1.0; drawdowns[i] = dd; quadraticMeanDD += dd * dd; if (dd < maxDD) maxDD = drawdowns[i]; } double ulcerInd = Math.Sqrt(quadraticMeanDD / (double)nTradingDays); int maxTradingDaysInDD = 0; int daysInDD = 0; for (int i = 0; i < drawdowns.Count(); i++) { if (drawdowns[i] < 0.0) daysInDD++; else { if (daysInDD > maxTradingDaysInDD) maxTradingDaysInDD = daysInDD; daysInDD = 0; } } if (daysInDD > maxTradingDaysInDD) // if the current DD is the longest one, then we have to check at the end maxTradingDaysInDD = daysInDD; int winnersCount = dailyReturns.Count(r => r > 0.0); int losersCount = dailyReturns.Count(r => r < 0.0); //double profitDaysPerAllDays = (double)dailyReturns.Count(r => r > 0.0) / dailyReturns.Count(); //double losingDaysPerAllDays = (double)dailyReturns.Count(r => r < 0.0) / dailyReturns.Count(); StrategyResult strategyResult = new StrategyResult() { startDateStr = startDate.ToString("yyyy-MM-dd"), rebalanceFrequencyStr = "Daily", benchmarkStr = "SPX", endDateStr = endDate.ToString("yyyy-MM-dd"), pvStartValue = pvStart, pvEndValue = pvEnd, totalGainPct = totalGainPct, cagr = cagr, annualizedStDev = annualizedStDev, sharpeRatio = annualizedSharpeRatio, sortinoRatio = annualizedSortinoRatio, maxDD = maxDD, ulcerInd = ulcerInd, maxTradingDaysInDD = maxTradingDaysInDD, winnersStr = String.Format("({0}/{1}) {2:0.00}%", winnersCount, dailyReturns.Count(), 100.0 * (double)winnersCount / dailyReturns.Count()), losersStr = String.Format("({0}/{1}) {2:0.00}%", losersCount, dailyReturns.Count(), 100.0 * (double)losersCount / dailyReturns.Count()), benchmarkCagr = 0, benchmarkMaxDD = 0, benchmarkCorrelation = 0, pvCash = 0.0, nPositions = 0, holdingsListStr = "NotApplicable", chartData = chartDataToSend.ToList(), htmlNoteFromStrategy = p_htmlNoteFromStrategy, errorMessage = p_errorMessage, debugMessage = p_debugMessage }; return strategyResult; }
// Sortino calculation from here: http://www.redrockcapital.com/Sortino__A__Sharper__Ratio_Red_Rock_Capital.pdf //The main reason we wrote this article is because in both literature and trading software packages, //we have seen the Sortino ratio, and in particular the target downside deviation, calculated //incorrectly more often than not. Most often, we see the target downside deviation calculated //by “throwing away all the positive returns and take the standard deviation of negative returns”. //We hope that by reading this article, you can see how this is incorrect // George: a little problem to me, but left it like this: underPerfFromTarget is distance from Zero, while in StDev, it was distance from Avg. public static StrategyResult CreateStrategyResultFromPV(List <DailyData> p_pv, string p_htmlNoteFromStrategy, string p_errorMessage, string p_debugMessage) { //IEnumerable<string> chartDataToSend = pv.Select(row => row.Date.Year + "-" + row.Date.Month + "-" + row.Date.Day + "-" + String.Format("{0:0.00}", row.ClosePrice)); IEnumerable <string> chartDataToSend = p_pv.Select(row => row.Date.Year + "-" + row.Date.Month + "-" + row.Date.Day + "," + String.Format("{0:0.00}", row.ClosePrice >= 0 ? row.ClosePrice : 0.0)); // postprocess: TradingViewChart cannot accept negative numbers DateTime startDate = p_pv[0].Date; DateTime endDate = p_pv[p_pv.Count() - 1].Date; int nTradingDays = p_pv.Count(); double nYears = nTradingDays / 252.0; //https://www.google.co.uk/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8#q=how%20many%20trading%20days%20in%20a%20year double pvStart = p_pv[0].ClosePrice; double pvEnd = p_pv[p_pv.Count() - 1].ClosePrice; double totalGainPct = pvEnd / pvStart - 1.0; double cagr = Math.Pow(totalGainPct + 1, 1.0 / nYears) - 1.0; var dailyReturns = new double[p_pv.Count() - 1]; for (int i = 0; i < p_pv.Count() - 1; i++) { dailyReturns[i] = p_pv[i + 1].ClosePrice / p_pv[i].ClosePrice - 1.0; } double avgReturn = dailyReturns.Average(); double dailyStdDev = Math.Sqrt(dailyReturns.Sum(r => (r - avgReturn) * (r - avgReturn)) / ((double)dailyReturns.Count() - 1.0)); //http://www.styleadvisor.com/content/standard-deviation, "Morningstar uses the sample standard deviation method: divide by n-1 double annualizedStDev = dailyStdDev * Math.Sqrt(252.0); //http://en.wikipedia.org/wiki/Trading_day, http://www.styleadvisor.com/content/annualized-standard-deviation double annualizedSharpeRatio = cagr / annualizedStDev; double sortinoDailyTargetReturn = 0.0; // assume investor is happy with any positive return double sortinoAnnualizedTargetReturn = Math.Pow(sortinoDailyTargetReturn, 252.0); // maybe around annual 3-6% is expected by investor double dailyTargetDownsideDeviation = 0.0; for (int i = 0; i < dailyReturns.Length; i++) { double underPerfFromTarget = dailyReturns[i] - sortinoDailyTargetReturn; if (underPerfFromTarget < 0.0) { dailyTargetDownsideDeviation += underPerfFromTarget * underPerfFromTarget; } } dailyTargetDownsideDeviation = Math.Sqrt(dailyTargetDownsideDeviation / (double)dailyReturns.Length); // see Sortino PDF for explanation why we use the 0 samples too for the Average double annualizedDailyTargetDownsideDeviation = dailyTargetDownsideDeviation * Math.Sqrt(252.0); //http://en.wikipedia.org/wiki/Trading_day, http://www.styleadvisor.com/content/annualized-standard-deviation //double dailySortinoRatio = (avgReturn - sortinoDailyTargetReturn) / dailyTargetDownsideDeviation; // daily gave too small values double annualizedSortinoRatio = (cagr - sortinoAnnualizedTargetReturn) / annualizedDailyTargetDownsideDeviation; // var drawdowns = new double[p_pv.Count()]; double maxPv = Double.NegativeInfinity; double maxDD = Double.PositiveInfinity; double quadraticMeanDD = 0.0; for (int i = 0; i < p_pv.Count(); i++) { if (p_pv[i].ClosePrice > maxPv) { maxPv = p_pv[i].ClosePrice; } double dd = p_pv[i].ClosePrice / maxPv - 1.0; drawdowns[i] = dd; quadraticMeanDD += dd * dd; if (dd < maxDD) { maxDD = drawdowns[i]; } } double ulcerInd = Math.Sqrt(quadraticMeanDD / (double)nTradingDays); int maxTradingDaysInDD = 0; int daysInDD = 0; for (int i = 0; i < drawdowns.Count(); i++) { if (drawdowns[i] < 0.0) { daysInDD++; } else { if (daysInDD > maxTradingDaysInDD) { maxTradingDaysInDD = daysInDD; } daysInDD = 0; } } if (daysInDD > maxTradingDaysInDD) // if the current DD is the longest one, then we have to check at the end { maxTradingDaysInDD = daysInDD; } int winnersCount = dailyReturns.Count(r => r > 0.0); int losersCount = dailyReturns.Count(r => r < 0.0); //double profitDaysPerAllDays = (double)dailyReturns.Count(r => r > 0.0) / dailyReturns.Count(); //double losingDaysPerAllDays = (double)dailyReturns.Count(r => r < 0.0) / dailyReturns.Count(); StrategyResult strategyResult = new StrategyResult() { startDateStr = startDate.ToString("yyyy-MM-dd"), rebalanceFrequencyStr = "Daily", benchmarkStr = "SPX", endDateStr = endDate.ToString("yyyy-MM-dd"), pvStartValue = pvStart, pvEndValue = pvEnd, totalGainPct = totalGainPct, cagr = cagr, annualizedStDev = annualizedStDev, sharpeRatio = annualizedSharpeRatio, sortinoRatio = annualizedSortinoRatio, maxDD = maxDD, ulcerInd = ulcerInd, maxTradingDaysInDD = maxTradingDaysInDD, winnersStr = String.Format("({0}/{1}) {2:0.00}%", winnersCount, dailyReturns.Count(), 100.0 * (double)winnersCount / dailyReturns.Count()), losersStr = String.Format("({0}/{1}) {2:0.00}%", losersCount, dailyReturns.Count(), 100.0 * (double)losersCount / dailyReturns.Count()), benchmarkCagr = 0, benchmarkMaxDD = 0, benchmarkCorrelation = 0, pvCash = 0.0, nPositions = 0, holdingsListStr = "NotApplicable", chartData = chartDataToSend.ToList(), htmlNoteFromStrategy = p_htmlNoteFromStrategy, errorMessage = p_errorMessage, debugMessage = p_debugMessage }; return(strategyResult); }