chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* 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.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Crisis events utility class
|
||||
/// </summary>
|
||||
public class Crisis
|
||||
{
|
||||
/// <summary>
|
||||
/// Crisis events and pre-defined values
|
||||
/// </summary>
|
||||
public static readonly Dictionary<CrisisEvent, Crisis> Events = new Dictionary<CrisisEvent, Crisis>
|
||||
{
|
||||
{CrisisEvent.DotCom, new Crisis("DotCom Bubble 2000", new DateTime(2000, 2, 26), new DateTime(2000, 9, 10)) },
|
||||
{CrisisEvent.SeptemberEleventh, new Crisis("September 11, 2001", new DateTime(2001, 9, 5), new DateTime(2001, 10, 10)) },
|
||||
{CrisisEvent.USHousingBubble2003, new Crisis("U.S. Housing Bubble 2003", new DateTime(2003, 1, 1), new DateTime(2003, 2, 20)) },
|
||||
{CrisisEvent.GlobalFinancialCrisis, new Crisis("Global Financial Crisis 2007", new DateTime(2007, 10, 1), new DateTime(2011, 12, 1))},
|
||||
{CrisisEvent.FlashCrash, new Crisis("Flash Crash 2010", new DateTime(2010, 5, 1), new DateTime(2010, 5, 22))},
|
||||
{CrisisEvent.FukushimaMeltdown, new Crisis("Fukushima Meltdown 2011", new DateTime(2011, 3, 1), new DateTime(2011, 4, 22)) },
|
||||
{CrisisEvent.USDowngradeEuropeanDebt, new Crisis("U.S. Credit Downgrade 2011", new DateTime(2011, 8, 5), new DateTime(2011, 9, 1))},
|
||||
{CrisisEvent.EurozoneSeptember2012, new Crisis("ECB IR Event 2012", new DateTime(2012, 9, 5), new DateTime(2012, 10, 12))},
|
||||
{CrisisEvent.EurozoneOctober2014, new Crisis("European Debt Crisis 2014", new DateTime(2014, 10, 1), new DateTime(2014, 10, 29))},
|
||||
{CrisisEvent.MarketSellOff2015, new Crisis("Market Sell-Off 2015", new DateTime(2015, 8, 10), new DateTime(2015, 10, 10))},
|
||||
{CrisisEvent.Recovery, new Crisis("Recovery 2010-2012", new DateTime(2010, 1, 1), new DateTime(2012, 10, 1))},
|
||||
{CrisisEvent.NewNormal, new Crisis("New Normal 2014-2019", new DateTime(2014, 1, 1), new DateTime(2019, 1, 1))},
|
||||
{CrisisEvent.COVID19, new Crisis("COVID-19 Pandemic 2020", new DateTime(2020, 2, 10), new DateTime(2020, 9, 20))},
|
||||
{CrisisEvent.PostCOVIDRunUp, new Crisis("Post-COVID Run-up 2020-2021", new DateTime(2020, 4, 1), new DateTime(2022, 1, 1))},
|
||||
{CrisisEvent.MemeSeason, new Crisis("Meme Season 2021", new DateTime(2021, 1, 1), new DateTime(2021, 5, 15))},
|
||||
{CrisisEvent.RussiaInvadesUkraine, new Crisis("Russia Invades Ukraine 2022-2023", new DateTime(2022, 2, 1), new DateTime(2024, 1, 1))},
|
||||
{CrisisEvent.AIBoom, new Crisis("AI Boom 2022-Present", new DateTime(2022, 11, 30), DateTime.Now)},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Start of the crisis event
|
||||
/// </summary>
|
||||
public DateTime Start { get; }
|
||||
|
||||
/// <summary>
|
||||
/// End of the crisis event
|
||||
/// </summary>
|
||||
public DateTime End { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the crisis
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new crisis instance with the given name and start/end date.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the crisis</param>
|
||||
/// <param name="start">Start date of the crisis</param>
|
||||
/// <param name="end">End date of the crisis</param>
|
||||
public Crisis(string name, DateTime start, DateTime end)
|
||||
{
|
||||
Name = name;
|
||||
Start = start;
|
||||
End = end;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a pre-defined crisis event
|
||||
/// </summary>
|
||||
/// <param name="crisisEvent">Crisis Event</param>
|
||||
/// <returns>Pre-defined crisis event</returns>
|
||||
public static Crisis FromCrisis(CrisisEvent crisisEvent)
|
||||
{
|
||||
return Events[crisisEvent];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts instance to string using the dates in the instance as start/end dates
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(Start, End);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts instance to string using the provided dates
|
||||
/// </summary>
|
||||
/// <param name="start">Start date</param>
|
||||
/// <param name="end">End date</param>
|
||||
/// <returns></returns>
|
||||
public string ToString(DateTime start, DateTime end)
|
||||
{
|
||||
return $"{Name}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* 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.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Crisis Events
|
||||
/// </summary>
|
||||
public enum CrisisEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// DotCom bubble - https://en.wikipedia.org/wiki/Dot-com_bubble (0)
|
||||
/// </summary>
|
||||
DotCom,
|
||||
|
||||
/// <summary>
|
||||
/// September 11, 2001 attacks - https://en.wikipedia.org/wiki/September_11_attacks (1)
|
||||
/// </summary>
|
||||
SeptemberEleventh,
|
||||
|
||||
/// <summary>
|
||||
/// United States housing bubble - https://en.wikipedia.org/wiki/United_States_housing_bubble (2)
|
||||
/// </summary>
|
||||
USHousingBubble2003,
|
||||
|
||||
/// <summary>
|
||||
/// https://en.wikipedia.org/wiki/Financial_crisis_of_2007%E2%80%9308 (3)
|
||||
/// </summary>
|
||||
GlobalFinancialCrisis,
|
||||
|
||||
/// <summary>
|
||||
/// The flash crash of 2010 - https://en.wikipedia.org/wiki/2010_Flash_Crash (4)
|
||||
/// </summary>
|
||||
FlashCrash,
|
||||
|
||||
/// <summary>
|
||||
/// Fukushima nuclear power plant meltdown - https://en.wikipedia.org/wiki/Fukushima_Daiichi_nuclear_disaster (5)
|
||||
/// </summary>
|
||||
FukushimaMeltdown,
|
||||
|
||||
/// <summary>
|
||||
/// United States credit rating downgrade - https://en.wikipedia.org/wiki/United_States_federal_government_credit-rating_downgrades
|
||||
/// European debt crisis - https://en.wikipedia.org/wiki/European_debt_crisis (6)
|
||||
/// </summary>
|
||||
USDowngradeEuropeanDebt,
|
||||
|
||||
/// <summary>
|
||||
/// European debt crisis - https://en.wikipedia.org/wiki/European_debt_crisis (7)
|
||||
/// </summary>
|
||||
EurozoneSeptember2012,
|
||||
|
||||
/// <summary>
|
||||
/// European debt crisis - https://en.wikipedia.org/wiki/European_debt_crisis (8)
|
||||
/// </summary>
|
||||
EurozoneOctober2014,
|
||||
|
||||
/// <summary>
|
||||
/// 2015-2016 market sell off https://en.wikipedia.org/wiki/2015%E2%80%9316_stock_market_selloff (9)
|
||||
/// </summary>
|
||||
MarketSellOff2015,
|
||||
|
||||
/// <summary>
|
||||
/// Crisis recovery (2010 - 2012) (10)
|
||||
/// </summary>
|
||||
Recovery,
|
||||
|
||||
/// <summary>
|
||||
/// 2014 - 2019 market performance (11)
|
||||
/// </summary>
|
||||
NewNormal,
|
||||
|
||||
/// <summary>
|
||||
/// COVID-19 pandemic market crash (12)
|
||||
/// </summary>
|
||||
COVID19,
|
||||
|
||||
/// <summary>
|
||||
/// Post COVID-19 recovery (13)
|
||||
/// </summary>
|
||||
PostCOVIDRunUp,
|
||||
|
||||
/// <summary>
|
||||
/// Meme-craze era like GME, AMC, and DOGE (14)
|
||||
/// </summary>
|
||||
MemeSeason,
|
||||
|
||||
/// <summary>
|
||||
/// Russia invased Ukraine (15)
|
||||
/// </summary>
|
||||
RussiaInvadesUkraine,
|
||||
|
||||
/// <summary>
|
||||
/// Artificial intelligence boom (16)
|
||||
/// </summary>
|
||||
AIBoom
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility extension methods for Deedle series/frames
|
||||
/// </summary>
|
||||
public static class DeedleUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the cumulative sum for the given series
|
||||
/// </summary>
|
||||
/// <param name="input">Series to calculate cumulative sum for</param>
|
||||
/// <returns>Cumulative sum in series form</returns>
|
||||
public static Series<DateTime, double> CumulativeSum(this Series<DateTime, double> input)
|
||||
{
|
||||
if (input.IsEmpty)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
var prev = 0.0;
|
||||
|
||||
return input.SelectValues(current =>
|
||||
{
|
||||
var sum = prev + current;
|
||||
prev = sum;
|
||||
|
||||
return sum;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the cumulative product of the series. This is equal to the python pandas method: `df.cumprod()`
|
||||
/// </summary>
|
||||
/// <param name="input">Input series</param>
|
||||
/// <returns>Cumulative product</returns>
|
||||
public static Series<DateTime, double> CumulativeProduct(this Series<DateTime, double> input)
|
||||
{
|
||||
if (input.IsEmpty)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
var prev = 1.0;
|
||||
|
||||
return input.SelectValues(current =>
|
||||
{
|
||||
var product = prev * current;
|
||||
prev = product;
|
||||
|
||||
return product;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the cumulative max of the series. This is equal to the python pandas method: `df.cummax()`.
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
public static Series<DateTime, double> CumulativeMax(this Series<DateTime, double> input)
|
||||
{
|
||||
if (input.IsEmpty)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
var prevMax = double.NegativeInfinity;
|
||||
var values = new List<double>();
|
||||
|
||||
foreach (var point in input.Values)
|
||||
{
|
||||
if (point > prevMax)
|
||||
{
|
||||
prevMax = point;
|
||||
}
|
||||
|
||||
values.Add(prevMax);
|
||||
}
|
||||
|
||||
return new Series<DateTime, double>(input.Keys, values);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the percentage change from the previous value to the current
|
||||
/// </summary>
|
||||
/// <param name="input">Series to calculate percentage change for</param>
|
||||
/// <returns>Percentage change in series form</returns>
|
||||
/// <remarks>Equivalent to `df.pct_change()`</remarks>
|
||||
public static Series<DateTime, double> PercentChange(this Series<DateTime, double> input)
|
||||
{
|
||||
if (input.IsEmpty)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
var inputShifted = input.Shift(1);
|
||||
|
||||
return (input - inputShifted) / inputShifted;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the cumulative returns series of the given input equity curve
|
||||
/// </summary>
|
||||
/// <param name="input">Equity curve series</param>
|
||||
/// <returns>Cumulative returns over time</returns>
|
||||
public static Series<DateTime, double> CumulativeReturns(this Series<DateTime, double> input)
|
||||
{
|
||||
if (input.IsEmpty)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
|
||||
return (input.PercentChange()
|
||||
.Where(kvp => !double.IsInfinity(kvp.Value)) + 1)
|
||||
.CumulativeProduct() - 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the total returns over a period of time for the given input
|
||||
/// </summary>
|
||||
/// <param name="input">Equity curve series</param>
|
||||
/// <returns>Total returns over time</returns>
|
||||
public static double TotalReturns(this Series<DateTime, double> input)
|
||||
{
|
||||
var returns = input.CumulativeReturns();
|
||||
|
||||
if (returns.IsEmpty)
|
||||
{
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
return returns.LastValue();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops sparse columns only if every value is `missing` in the column
|
||||
/// </summary>
|
||||
/// <typeparam name="TRowKey">Frame row key</typeparam>
|
||||
/// <typeparam name="TColumnKey">Frame column key</typeparam>
|
||||
/// <param name="frame">Data Frame</param>
|
||||
/// <returns>new Frame with sparse columns dropped</returns>
|
||||
/// <remarks>Equivalent to `df.dropna(axis=1, how='all')`</remarks>
|
||||
public static Frame<TRowKey, TColumnKey> DropSparseColumnsAll<TRowKey, TColumnKey>(this Frame<TRowKey, TColumnKey> frame)
|
||||
{
|
||||
var newFrame = frame.Clone();
|
||||
|
||||
foreach (var key in frame.ColumnKeys)
|
||||
{
|
||||
if (newFrame[key].DropMissing().ValueCount == 0)
|
||||
{
|
||||
newFrame.DropColumn(key);
|
||||
}
|
||||
}
|
||||
|
||||
return newFrame;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops sparse rows if and only if every value is `missing` in the Frame
|
||||
/// </summary>
|
||||
/// <typeparam name="TRowKey">Frame row key</typeparam>
|
||||
/// <typeparam name="TColumnKey">Frame column key</typeparam>
|
||||
/// <param name="frame">Data Frame</param>
|
||||
/// <returns>new Frame with sparse rows dropped</returns>
|
||||
/// <remarks>Equivalent to `df.dropna(how='all')`</remarks>
|
||||
public static Frame<TRowKey, TColumnKey> DropSparseRowsAll<TRowKey, TColumnKey>(this Frame<TRowKey, TColumnKey> frame)
|
||||
{
|
||||
if (frame.ColumnKeys.Count() == 0)
|
||||
{
|
||||
return Frame.CreateEmpty<TRowKey, TColumnKey>();
|
||||
}
|
||||
|
||||
var newFrame = frame.Clone().Transpose();
|
||||
|
||||
foreach (var key in frame.RowKeys)
|
||||
{
|
||||
if (newFrame[key].DropMissing().ValueCount == 0)
|
||||
{
|
||||
newFrame.DropColumn(key);
|
||||
}
|
||||
}
|
||||
|
||||
return newFrame.Transpose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
* 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.Packets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Collection of drawdowns for the given period marked by start and end date
|
||||
/// </summary>
|
||||
public class DrawdownCollection
|
||||
{
|
||||
/// <summary>
|
||||
/// Starting time of the drawdown collection
|
||||
/// </summary>
|
||||
public DateTime Start { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ending time of the drawdown collection
|
||||
/// </summary>
|
||||
public DateTime End { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of periods to take into consideration for the top N drawdown periods.
|
||||
/// This will be the number of items contained in the <see cref="Drawdowns"/> collection.
|
||||
/// </summary>
|
||||
public int Periods { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Worst drawdowns encountered
|
||||
/// </summary>
|
||||
public List<DrawdownPeriod> Drawdowns { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance with a default collection (no items) and the top N worst drawdowns
|
||||
/// </summary>
|
||||
/// <param name="periods"></param>
|
||||
public DrawdownCollection(int periods)
|
||||
{
|
||||
Drawdowns = new List<DrawdownPeriod>();
|
||||
Periods = periods;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance from the given drawdowns and the top N worst drawdowns
|
||||
/// </summary>
|
||||
/// <param name="strategySeries">Equity curve with both live and backtesting merged</param>
|
||||
/// <param name="periods">Periods this collection contains</param>
|
||||
public DrawdownCollection(Series<DateTime, double> strategySeries, int periods)
|
||||
{
|
||||
var drawdowns = GetDrawdownPeriods(strategySeries, periods).ToList();
|
||||
|
||||
Periods = periods;
|
||||
Start = strategySeries.IsEmpty ? DateTime.MinValue : strategySeries.FirstKey();
|
||||
End = strategySeries.IsEmpty ? DateTime.MaxValue : strategySeries.LastKey();
|
||||
Drawdowns = drawdowns.OrderByDescending(x => x.PeakToTrough)
|
||||
.Take(Periods)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate a new instance of DrawdownCollection from backtest and live <see cref="Result"/> derived instances
|
||||
/// </summary>
|
||||
/// <param name="backtestResult">Backtest result packet</param>
|
||||
/// <param name="liveResult">Live result packet</param>
|
||||
/// <param name="periods">Top N drawdown periods to get</param>
|
||||
/// <returns>DrawdownCollection instance</returns>
|
||||
public static DrawdownCollection FromResult(BacktestResult backtestResult = null, LiveResult liveResult = null, int periods = 5)
|
||||
{
|
||||
return new DrawdownCollection(NormalizeResults(backtestResult, liveResult), periods);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the Series used to calculate the drawdown plots and charts
|
||||
/// </summary>
|
||||
/// <param name="backtestResult">Backtest result packet</param>
|
||||
/// <param name="liveResult">Live result packet</param>
|
||||
/// <returns></returns>
|
||||
public static Series<DateTime, double> NormalizeResults(BacktestResult backtestResult, LiveResult liveResult)
|
||||
{
|
||||
var backtestPoints = ResultsUtil.EquityPoints(backtestResult);
|
||||
var livePoints = ResultsUtil.EquityPoints(liveResult);
|
||||
|
||||
if (backtestPoints.Count < 2 && livePoints.Count < 2)
|
||||
{
|
||||
return new Series<DateTime, double>(new DateTime[] { }, new double[] { });
|
||||
}
|
||||
|
||||
var startingEquity = backtestPoints.Count == 0 ? livePoints.First().Value : backtestPoints.First().Value;
|
||||
|
||||
// Note: these calculations are *incorrect* for getting the cumulative returns. However, since we're just
|
||||
// trying to normalize these two series with each other, it's a good candidate for it since the original
|
||||
// values can easily be recalculated from this point
|
||||
var backtestSeries = new Series<DateTime, double>(backtestPoints).PercentChange().Where(kvp => !double.IsInfinity(kvp.Value)).CumulativeSum();
|
||||
var liveSeries = new Series<DateTime, double>(livePoints).PercentChange().Where(kvp => !double.IsInfinity(kvp.Value)).CumulativeSum();
|
||||
|
||||
// Get the last key of the backtest series if our series is empty to avoid issues with empty frames
|
||||
var firstLiveKey = liveSeries.IsEmpty ? backtestSeries.LastKey().AddDays(1) : liveSeries.FirstKey();
|
||||
|
||||
// Add the final non-overlapping point of the backtest equity curve to the entire live series to keep continuity.
|
||||
if (!backtestSeries.IsEmpty)
|
||||
{
|
||||
var filtered = backtestSeries.Where(kvp => kvp.Key < firstLiveKey);
|
||||
liveSeries = filtered.IsEmpty ? liveSeries : liveSeries + filtered.LastValue();
|
||||
}
|
||||
|
||||
// Prefer the live values as we don't care about backtest once we've deployed into live.
|
||||
// All in all, this is a normalized equity curve, though it's been normalized
|
||||
// so that there are no discontinuous jumps in equity value if we only used equity cash
|
||||
// to add the last value of the backtest series to the live series.
|
||||
//
|
||||
// Pandas equivalent:
|
||||
//
|
||||
// ```
|
||||
// pd.concat([backtestSeries, liveSeries], axis=1).fillna(method='ffill').dropna().diff().add(1).cumprod().mul(startingEquity)
|
||||
// ```
|
||||
return backtestSeries.Merge(liveSeries, UnionBehavior.PreferRight)
|
||||
.FillMissing(Direction.Forward)
|
||||
.DropMissing()
|
||||
.Diff(1)
|
||||
.SelectValues(x => x + 1)
|
||||
.CumulativeProduct()
|
||||
.SelectValues(x => x * startingEquity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underwater plot for the provided curve.
|
||||
/// Data is expected to be the concatenated output of <see cref="ResultsUtil.EquityPoints"/>.
|
||||
/// </summary>
|
||||
/// <param name="curve">Equity curve</param>
|
||||
/// <returns></returns>
|
||||
public static Series<DateTime, double> GetUnderwater(Series<DateTime, double> curve)
|
||||
{
|
||||
if (curve.IsEmpty)
|
||||
{
|
||||
return curve;
|
||||
}
|
||||
|
||||
var returns = curve / curve.FirstValue();
|
||||
var cumulativeMax = returns.CumulativeMax();
|
||||
|
||||
return (1 - (returns / cumulativeMax)) * -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all the data associated with the underwater plot and everything used to generate it.
|
||||
/// Note that you should instead use <see cref="GetUnderwater(Series{DateTime, double})"/> if you
|
||||
/// want to just generate an underwater plot. This is internally used to get the top N worst drawdown periods.
|
||||
/// </summary>
|
||||
/// <param name="curve">Equity curve</param>
|
||||
/// <returns>Frame containing the following keys: "returns", "cumulativeMax", "drawdown"</returns>
|
||||
public static Frame<DateTime, string> GetUnderwaterFrame(Series<DateTime, double> curve)
|
||||
{
|
||||
var frame = Frame.CreateEmpty<DateTime, string>();
|
||||
if (curve.IsEmpty)
|
||||
{
|
||||
return frame;
|
||||
}
|
||||
|
||||
var returns = curve / curve.FirstValue();
|
||||
var cumulativeMax = returns.CumulativeMax();
|
||||
var drawdown = 1 - (returns / cumulativeMax);
|
||||
|
||||
frame.AddColumn("returns", returns);
|
||||
frame.AddColumn("cumulativeMax", cumulativeMax);
|
||||
frame.AddColumn("drawdown", drawdown);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the top N worst drawdowns and associated statistics.
|
||||
/// Returns a Frame with the following keys: "duration", "cumulativeMax", "drawdown"
|
||||
/// </summary>
|
||||
/// <param name="curve">Equity curve</param>
|
||||
/// <param name="periods">Top N worst periods. If this is greater than the results, we retrieve all the items instead</param>
|
||||
/// <returns>Frame with the following keys: "duration", "cumulativeMax", "drawdown"</returns>
|
||||
public static Frame<DateTime, string> GetTopWorstDrawdowns(Series<DateTime, double> curve, int periods)
|
||||
{
|
||||
var frame = Frame.CreateEmpty<DateTime, string>();
|
||||
if (curve.IsEmpty)
|
||||
{
|
||||
return frame;
|
||||
}
|
||||
|
||||
var returns = curve / curve.FirstValue();
|
||||
var cumulativeMax = returns.CumulativeMax();
|
||||
var drawdown = 1 - (returns / cumulativeMax);
|
||||
|
||||
var groups = cumulativeMax.GroupBy(kvp => kvp.Value);
|
||||
// In order, the items are: date, duration, cumulative max, max drawdown
|
||||
var drawdownGroups = new List<Tuple<DateTime, double, double, double>>();
|
||||
|
||||
foreach (var group in groups.Values)
|
||||
{
|
||||
var firstDate = group.SortByKey().FirstKey();
|
||||
var lastDate = group.SortByKey().LastKey();
|
||||
|
||||
var cumulativeMaxGroup = cumulativeMax.Between(firstDate, lastDate);
|
||||
var drawdownGroup = drawdown.Between(firstDate, lastDate);
|
||||
var drawdownGroupMax = drawdownGroup.Values.Max();
|
||||
|
||||
var drawdownMax = drawdownGroup.Where(kvp => kvp.Value == drawdownGroupMax);
|
||||
|
||||
drawdownGroups.Add(new Tuple<DateTime, double, double, double>(
|
||||
drawdownMax.FirstKey(),
|
||||
group.ValueCount,
|
||||
cumulativeMaxGroup.FirstValue(),
|
||||
drawdownMax.FirstValue()
|
||||
));
|
||||
}
|
||||
|
||||
var drawdowns = new Series<DateTime, double>(drawdownGroups.Select(x => x.Item1), drawdownGroups.Select(x => x.Item4));
|
||||
// Sort by negative drawdown value (in ascending order), which leaves it sorted in descending order 😮
|
||||
var sortedDrawdowns = drawdowns.SortBy(x => -x);
|
||||
// Only get the most we're allowed to take so that we don't overflow trying to get more drawdown items than exist
|
||||
var periodsToTake = periods < sortedDrawdowns.ValueCount ? periods : sortedDrawdowns.ValueCount;
|
||||
|
||||
// Again, in order, the items are: date (Item1), duration (Item2), cumulative max (Item3), max drawdown (Item4).
|
||||
var topDrawdowns = new Series<DateTime, double>(sortedDrawdowns.Keys.Take(periodsToTake), sortedDrawdowns.Values.Take(periodsToTake));
|
||||
var topDurations = new Series<DateTime, double>(topDrawdowns.Keys.OrderBy(x => x), drawdownGroups.Where(t => topDrawdowns.Keys.Contains(t.Item1)).OrderBy(x => x.Item1).Select(x => x.Item2));
|
||||
var topCumulativeMax = new Series<DateTime, double>(topDrawdowns.Keys.OrderBy(x => x), drawdownGroups.Where(t => topDrawdowns.Keys.Contains(t.Item1)).OrderBy(x => x.Item1).Select(x => x.Item3));
|
||||
|
||||
frame.AddColumn("duration", topDurations);
|
||||
frame.AddColumn("cumulativeMax", topCumulativeMax);
|
||||
frame.AddColumn("drawdown", topDrawdowns);
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the given drawdown periods from the equity curve and the set periods
|
||||
/// </summary>
|
||||
/// <param name="curve">Equity curve</param>
|
||||
/// <param name="periods">Top N drawdown periods to get</param>
|
||||
/// <returns>Enumerable of DrawdownPeriod</returns>
|
||||
public static IEnumerable<DrawdownPeriod> GetDrawdownPeriods(Series<DateTime, double> curve, int periods = 5)
|
||||
{
|
||||
var frame = GetUnderwaterFrame(curve);
|
||||
var topDrawdowns = GetTopWorstDrawdowns(curve, periods);
|
||||
|
||||
for (var i = 1; i <= topDrawdowns.RowCount; i++)
|
||||
{
|
||||
var data = DrawdownGroup(frame, topDrawdowns["cumulativeMax"].GetAt(i - 1));
|
||||
|
||||
// Tuple is as follows: Start (Item1: DateTime), End (Item2: DateTime), Max Drawdown (Item3: double)
|
||||
yield return new DrawdownPeriod(data.Item1, data.Item2, data.Item3);
|
||||
}
|
||||
}
|
||||
|
||||
private static Tuple<DateTime, DateTime, double> DrawdownGroup(Frame<DateTime, string> frame, double groupMax)
|
||||
{
|
||||
var drawdownAfter = frame["cumulativeMax"].Where(kvp => kvp.Value > groupMax);
|
||||
var drawdownGroup = frame["cumulativeMax"].Where(kvp => kvp.Value == groupMax);
|
||||
var groupDrawdown = frame["drawdown"].Realign(drawdownGroup.Keys).Max();
|
||||
|
||||
var groupStart = drawdownGroup.FirstKey();
|
||||
// Get the start of the next period if it exists. That is when the drawdown period has officially ended.
|
||||
// We do this to extend the drawdown period enough so that missing values don't stop it early.
|
||||
var groupEnd = drawdownAfter.IsEmpty ? drawdownGroup.LastKey() : drawdownAfter.FirstKey();
|
||||
|
||||
return new Tuple<DateTime, DateTime, double>(groupStart, groupEnd, groupDrawdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a period of time where the drawdown ranks amongst the top N drawdowns.
|
||||
/// </summary>
|
||||
public class DrawdownPeriod
|
||||
{
|
||||
/// <summary>
|
||||
/// Start of the drawdown period
|
||||
/// </summary>
|
||||
public DateTime Start { get; }
|
||||
|
||||
/// <summary>
|
||||
/// End of the drawdown period
|
||||
/// </summary>
|
||||
public DateTime End { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Loss in percent from peak to trough
|
||||
/// </summary>
|
||||
public double PeakToTrough { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Loss in percent from peak to trough - Alias for <see cref="PeakToTrough"/>
|
||||
/// </summary>
|
||||
public double Drawdown => PeakToTrough;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance with the given start, end, and drawdown
|
||||
/// </summary>
|
||||
/// <param name="start">Start of the drawdown period</param>
|
||||
/// <param name="end">End of the drawdown period</param>
|
||||
/// <param name="drawdown">Max drawdown of the period</param>
|
||||
public DrawdownPeriod(DateTime start, DateTime end, double drawdown)
|
||||
{
|
||||
Start = start;
|
||||
End = end;
|
||||
PeakToTrough = drawdown;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/*
|
||||
* 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
|
||||
{
|
||||
/// <summary>
|
||||
/// Strategy metrics collection such as usage of funds and asset allocations
|
||||
/// </summary>
|
||||
public static class Metrics
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the leverage used from trades. The series used to call this extension function should
|
||||
/// be the equity curve with the associated <see cref="Order"/> objects that go along with it.
|
||||
/// </summary>
|
||||
/// <param name="equityCurve">Equity curve series</param>
|
||||
/// <param name="orders">Orders associated with the equity curve</param>
|
||||
/// <returns>Leverage utilization over time</returns>
|
||||
public static Series<DateTime, double> LeverageUtilization(Series<DateTime, double> equityCurve, List<Order> orders)
|
||||
{
|
||||
if (equityCurve.IsEmpty || orders.Count == 0)
|
||||
{
|
||||
return new Series<DateTime, double>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the leverage utilization from a list of <see cref="PointInTimePortfolio"/>
|
||||
/// </summary>
|
||||
/// <param name="portfolios">Point in time portfolios</param>
|
||||
/// <returns>Series of leverage utilization</returns>
|
||||
public static Series<DateTime, double> LeverageUtilization(List<PointInTimePortfolio> portfolios)
|
||||
{
|
||||
var leverage = portfolios.GroupBy(portfolio => portfolio.Time)
|
||||
.Select(group => new KeyValuePair<DateTime, double>(group.Key, (double)group.Last().Leverage))
|
||||
.ToList();
|
||||
|
||||
// Drop missing because we don't care about the missing values
|
||||
return new Series<DateTime, double>(leverage).DropMissing();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see cref="Order"/> objects that go along with it.
|
||||
/// </summary>
|
||||
/// <param name="equityCurve">Equity curve series</param>
|
||||
/// <param name="orders">Orders associated with the equity curve</param>
|
||||
/// <returns></returns>
|
||||
public static Series<Symbol, double> AssetAllocations(Series<DateTime, double> equityCurve, List<Order> orders)
|
||||
{
|
||||
if (equityCurve.IsEmpty || orders.Count == 0)
|
||||
{
|
||||
return new Series<Symbol, double>(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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the asset allocation percentage over time.
|
||||
/// </summary>
|
||||
/// <param name="portfolios">Point in time portfolios</param>
|
||||
/// <returns>Series keyed by Symbol containing the percentage allocated to that asset over time</returns>
|
||||
public static Series<Symbol, double> AssetAllocations(List<PointInTimePortfolio> 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<Symbol, double>();
|
||||
|
||||
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<Symbol, double>(
|
||||
holdingsBySymbolOverTime.Keys,
|
||||
holdingsBySymbolOverTime.Values.Select(x => x / totalPortfolioValueOverTime).ToList()
|
||||
).DropMissing();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strategy long/short exposure by asset class
|
||||
/// </summary>
|
||||
/// <param name="equityCurve">Equity curve</param>
|
||||
/// <param name="orders">Orders of the strategy</param>
|
||||
/// <param name="direction">Long or short</param>
|
||||
/// <returns>
|
||||
/// Frame keyed by <see cref="SecurityType"/> and <see cref="OrderDirection"/>.
|
||||
/// Returns a Frame of exposure per asset per direction over time
|
||||
/// </returns>
|
||||
public static Frame<DateTime, Tuple<SecurityType, OrderDirection>> Exposure(Series<DateTime, double> equityCurve, List<Order> orders, OrderDirection direction)
|
||||
{
|
||||
if (equityCurve.IsEmpty || orders.Count == 0)
|
||||
{
|
||||
return Frame.CreateEmpty<DateTime, Tuple<SecurityType, OrderDirection>>();
|
||||
}
|
||||
|
||||
return Exposure(PortfolioLooper.FromOrders(equityCurve, orders).ToList(), direction);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Strategy long/short exposure by asset class
|
||||
/// </summary>
|
||||
/// <param name="portfolios">Point in time portfolios</param>
|
||||
/// <param name="direction">Long or short</param>
|
||||
/// <returns>
|
||||
/// Frame keyed by <see cref="SecurityType"/> and <see cref="OrderDirection"/>.
|
||||
/// Returns a Frame of exposure per asset per direction over time
|
||||
/// </returns>
|
||||
public static Frame<DateTime, Tuple<SecurityType, OrderDirection>> Exposure(List<PointInTimePortfolio> 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<SecurityType, List<KeyValuePair<DateTime, double>>>();
|
||||
var multiplier = direction == OrderDirection.Sell ? -1 : 1;
|
||||
|
||||
foreach (var portfolio in portfolios)
|
||||
{
|
||||
List<KeyValuePair<DateTime, double>> holdings;
|
||||
if (!holdingsByAssetClass.TryGetValue(portfolio.Order.SecurityType, out holdings))
|
||||
{
|
||||
holdings = new List<KeyValuePair<DateTime, double>>();
|
||||
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<DateTime, double>(portfolio.Time, sum / (double)portfolio.TotalPortfolioValue));
|
||||
}
|
||||
}
|
||||
|
||||
var frame = Frame.CreateEmpty<DateTime, Tuple<SecurityType, OrderDirection>>();
|
||||
|
||||
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<SecurityType, OrderDirection>(kvp.Key, direction),
|
||||
new Series<DateTime, double>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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 Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Removes null values in the <see cref="Result"/> object's x,y values so that
|
||||
/// deserialization can occur without exceptions.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Result type to deserialize into</typeparam>
|
||||
public class NullResultValueTypeJsonConverter<T> : JsonConverter
|
||||
where T : Result
|
||||
{
|
||||
private JsonSerializerSettings _settings;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize a new instance of <see cref="NullResultValueTypeJsonConverter{T}"/>
|
||||
/// </summary>
|
||||
public NullResultValueTypeJsonConverter()
|
||||
{
|
||||
_settings = new JsonSerializerSettings
|
||||
{
|
||||
Converters = new List<JsonConverter> { new OrderTypeNormalizingJsonConverter() },
|
||||
FloatParseHandling = FloatParseHandling.Decimal
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine if this converter can convert a given type
|
||||
/// </summary>
|
||||
/// <param name="objectType">Object type to convert</param>
|
||||
/// <returns>Always true</returns>
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return objectType.IsAssignableTo(typeof(T));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read Json for conversion
|
||||
/// </summary>
|
||||
/// <returns>Resulting object</returns>
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var token = JToken.ReadFrom(reader);
|
||||
if (token.Type == JTokenType.Null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (JProperty property in GetProperty(token, "Charts").Children())
|
||||
{
|
||||
foreach (JProperty seriesProperty in GetProperty(property.Value, "Series"))
|
||||
{
|
||||
var newValues = new List<JToken>();
|
||||
foreach (var entry in GetProperty(seriesProperty.Value, "Values"))
|
||||
{
|
||||
if (entry is JObject jobj &&
|
||||
(jobj["x"] == null || jobj["x"].Value<long?>() == null ||
|
||||
jobj["y"] == null || jobj["y"].Value<decimal?>() == null))
|
||||
{
|
||||
// null chart point
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry is JArray jArray && jArray.Any(jToken => jToken.Type == JTokenType.Null))
|
||||
{
|
||||
// null candlestick
|
||||
continue;
|
||||
}
|
||||
|
||||
newValues.Add(entry);
|
||||
}
|
||||
|
||||
var chart = GetProperty(token, "Charts")[property.Name];
|
||||
var series = GetProperty(chart, "Series")[seriesProperty.Name];
|
||||
if (series["Values"] != null)
|
||||
{
|
||||
series["Values"] = JArray.FromObject(newValues);
|
||||
}
|
||||
else if (series["values"] != null)
|
||||
{
|
||||
series["values"] = JArray.FromObject(newValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialize with OrderJsonConverter, otherwise it will fail. We convert the token back
|
||||
// to its JSON representation and use the `JsonConvert.DeserializeObject<T>(...)` method instead
|
||||
// of using `token.ToObject<T>()` since it can be provided a JsonConverter in its arguments.
|
||||
return JsonConvert.DeserializeObject<T>(token.ToString(), _settings);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write Json; Not implemented
|
||||
/// </summary>
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static JToken GetProperty(JToken jToken, string name)
|
||||
{
|
||||
return jToken[name] ?? jToken[name.ToLower()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 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.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using QuantConnect.Orders;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Normalizes the "Type" field to a value that will allow for
|
||||
/// successful deserialization in the <see cref="OrderJsonConverter"/> class.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// All of these values should result in the same object:
|
||||
/// <code>
|
||||
/// [
|
||||
/// { "Type": "marketOnOpen", ... },
|
||||
/// { "Type": "MarketOnOpen", ... },
|
||||
/// { "Type": 4, ... },
|
||||
/// ]
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class OrderTypeNormalizingJsonConverter : JsonConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Determine if this Converter can convert a given object type
|
||||
/// </summary>
|
||||
/// <param name="objectType">Object type to convert</param>
|
||||
/// <returns>True if assignable from <see cref="Order"/></returns>
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(Order).IsAssignableFrom(objectType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read Json and convert
|
||||
/// </summary>
|
||||
/// <returns>Resulting <see cref="Order"/></returns>
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
var token = JToken.ReadFrom(reader);
|
||||
var jtokenType = token["Type"] ?? token["type"];
|
||||
int orderType = GetOrderType(jtokenType);
|
||||
if (token["Type"] != null)
|
||||
{
|
||||
token["Type"] = orderType;
|
||||
}
|
||||
else if (token["type"] != null)
|
||||
{
|
||||
token["type"] = orderType;
|
||||
}
|
||||
|
||||
return OrderJsonConverter.CreateOrderFromJObject((JObject)token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write Json; Not implemented
|
||||
/// </summary>
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private int GetOrderType(JToken type)
|
||||
{
|
||||
var orderTypeValue = type.Value<string>();
|
||||
int orderTypeNumber;
|
||||
return Parse.TryParse(orderTypeValue, NumberStyles.Any, out orderTypeNumber) ?
|
||||
orderTypeNumber :
|
||||
(int)(OrderType)Enum.Parse(typeof(OrderType), orderTypeValue, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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 QuantConnect.Orders;
|
||||
using QuantConnect.Securities;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Lightweight portfolio at a point in time
|
||||
/// </summary>
|
||||
public class PointInTimePortfolio
|
||||
{
|
||||
/// <summary>
|
||||
/// Time that this point in time portfolio is for
|
||||
/// </summary>
|
||||
public DateTime Time { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The total value of the portfolio. This is cash + absolute value of holdings
|
||||
/// </summary>
|
||||
public decimal TotalPortfolioValue { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The cash the portfolio has
|
||||
/// </summary>
|
||||
public decimal Cash { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The order we just processed
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public Order Order { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// A list of holdings at the current moment in time
|
||||
/// </summary>
|
||||
public List<PointInTimeHolding> Holdings { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Portfolio leverage - provided for convenience
|
||||
/// </summary>
|
||||
public decimal Leverage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the PointInTimePortfolio object
|
||||
/// </summary>
|
||||
/// <param name="order">Order applied to the portfolio</param>
|
||||
/// <param name="portfolio">Algorithm portfolio at a point in time</param>
|
||||
public PointInTimePortfolio(Order order, SecurityPortfolioManager portfolio)
|
||||
{
|
||||
Time = order.Time;
|
||||
Order = order;
|
||||
TotalPortfolioValue = portfolio.TotalPortfolioValue;
|
||||
Cash = portfolio.Cash;
|
||||
Holdings = portfolio.Securities.Values.Select(x => new PointInTimeHolding(x.Symbol, x.Holdings.HoldingsValue, x.Holdings.Quantity)).ToList();
|
||||
Leverage = Holdings.Sum(x => x.AbsoluteHoldingsValue) / TotalPortfolioValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clones the provided portfolio
|
||||
/// </summary>
|
||||
/// <param name="portfolio">Portfolio</param>
|
||||
/// <param name="time">Time</param>
|
||||
public PointInTimePortfolio(PointInTimePortfolio portfolio, DateTime time)
|
||||
{
|
||||
Time = time;
|
||||
Order = portfolio.Order;
|
||||
TotalPortfolioValue = portfolio.TotalPortfolioValue;
|
||||
Cash = portfolio.Cash;
|
||||
Holdings = portfolio.Holdings.Select(x => new PointInTimeHolding(x.Symbol, x.HoldingsValue, x.Quantity)).ToList();
|
||||
Leverage = portfolio.Leverage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters out any empty holdings from the current <see cref="Holdings"/>
|
||||
/// </summary>
|
||||
/// <returns>Current object, but without empty holdings</returns>
|
||||
public PointInTimePortfolio NoEmptyHoldings()
|
||||
{
|
||||
Holdings = Holdings.Where(h => h.Quantity != 0).ToList();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Holding of an asset at a point in time
|
||||
/// </summary>
|
||||
public class PointInTimeHolding
|
||||
{
|
||||
/// <summary>
|
||||
/// Symbol of the holding
|
||||
/// </summary>
|
||||
public Symbol Symbol { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Value of the holdings of the asset. Can be negative if shorting an asset
|
||||
/// </summary>
|
||||
public decimal HoldingsValue { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Quantity of the asset. Can be negative if shorting an asset
|
||||
/// </summary>
|
||||
public decimal Quantity { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Absolute value of the holdings.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public decimal AbsoluteHoldingsValue => Math.Abs(HoldingsValue);
|
||||
|
||||
/// <summary>
|
||||
/// Absolute value of the quantity
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public decimal AbsoluteHoldingsQuantity => Math.Abs(Quantity);
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of PointInTimeHolding, representing a holding at a given point in time
|
||||
/// </summary>
|
||||
/// <param name="symbol">Symbol of the holding</param>
|
||||
/// <param name="holdingsValue">Value of the holding</param>
|
||||
/// <param name="holdingsQuantity">Quantity of the holding</param>
|
||||
public PointInTimeHolding(Symbol symbol, decimal holdingsValue, decimal holdingsQuantity)
|
||||
{
|
||||
Symbol = symbol;
|
||||
HoldingsValue = holdingsValue;
|
||||
Quantity = holdingsQuantity;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* 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 QuantConnect.Data.UniverseSelection;
|
||||
using QuantConnect.Interfaces;
|
||||
using QuantConnect.Lean.Engine.DataFeeds;
|
||||
using QuantConnect.Lean.Engine.Results;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Fake IDataFeed
|
||||
/// </summary>
|
||||
public class MockDataFeed : IDataFeed
|
||||
{
|
||||
/// <summary>
|
||||
/// Bool if the feed is active
|
||||
/// </summary>
|
||||
public bool IsActive { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the data feed
|
||||
/// This implementation does nothing
|
||||
/// </summary>
|
||||
public void Initialize(
|
||||
IAlgorithm algorithm,
|
||||
AlgorithmNodePacket job,
|
||||
IResultHandler resultHandler,
|
||||
IMapFileProvider mapFileProvider,
|
||||
IFactorFileProvider factorFileProvider,
|
||||
IDataProvider dataProvider,
|
||||
IDataFeedSubscriptionManager subscriptionManager,
|
||||
IDataFeedTimeProvider dataFeedTimeProvider,
|
||||
IDataChannelProvider dataChannelProvider
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create Subscription
|
||||
/// </summary>
|
||||
/// <param name="request">Subscription request to use</param>
|
||||
/// <returns>Always null</returns>
|
||||
public Subscription CreateSubscription(SubscriptionRequest request)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove Subscription; Not implemented
|
||||
/// </summary>
|
||||
/// <param name="subscription">Subscription to remove</param>
|
||||
public void RemoveSubscription(Subscription subscription)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataFeed Exit
|
||||
/// </summary>
|
||||
public void Exit()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
/*
|
||||
* 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 QuantConnect.Brokerages.Backtesting;
|
||||
using QuantConnect.Data;
|
||||
using QuantConnect.Data.Market;
|
||||
using QuantConnect.Interfaces;
|
||||
using QuantConnect.Lean.Engine.DataFeeds;
|
||||
using QuantConnect.Lean.Engine.Results;
|
||||
using QuantConnect.Lean.Engine.Setup;
|
||||
using QuantConnect.Lean.Engine.TransactionHandlers;
|
||||
using QuantConnect.Logging;
|
||||
using QuantConnect.Packets;
|
||||
using QuantConnect.Securities;
|
||||
using QuantConnect.Util;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using QuantConnect.Lean.Engine.HistoricalData;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs LEAN to calculate the portfolio at a given time from <see cref="Order"/> objects.
|
||||
/// Generates and returns <see cref="PointInTimePortfolio"/> objects that represents
|
||||
/// the holdings and other miscellaneous metrics at a point in time by reprocessing the orders
|
||||
/// as they were filled.
|
||||
/// </summary>
|
||||
public class PortfolioLooper : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Default resolution to read. This will affect the granularity of the results generated for FX and Crypto
|
||||
/// </summary>
|
||||
private const Resolution _resolution = Resolution.Hour;
|
||||
|
||||
private SecurityService _securityService;
|
||||
private DataManager _dataManager;
|
||||
private IResultHandler _resultHandler;
|
||||
private IDataCacheProvider _cacheProvider;
|
||||
private IEnumerable<Slice> _conversionSlices = new List<Slice>();
|
||||
|
||||
/// <summary>
|
||||
/// QCAlgorithm derived class that sets up internal data feeds for
|
||||
/// use with crypto and forex data, as well as managing the <see cref="SecurityPortfolioManager"/>
|
||||
/// </summary>
|
||||
public PortfolioLooperAlgorithm Algorithm { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of the PortfolioLooper class
|
||||
/// </summary>
|
||||
/// <param name="startingCash">Equity curve</param>
|
||||
/// <param name="orders">Order events</param>
|
||||
/// <param name="resolution">Optional parameter to override default resolution (Hourly)</param>
|
||||
/// <param name="algorithmConfiguration">Optional parameter to override default algorithm configuration</param>
|
||||
private PortfolioLooper(double startingCash, List<Order> orders, Resolution resolution = _resolution,
|
||||
AlgorithmConfiguration algorithmConfiguration = null)
|
||||
{
|
||||
// Initialize the providers that the HistoryProvider requires
|
||||
var factorFileProvider = Composer.Instance.GetExportedValueByTypeName<IFactorFileProvider>("LocalDiskFactorFileProvider");
|
||||
var mapFileProvider = Composer.Instance.GetExportedValueByTypeName<IMapFileProvider>("LocalDiskMapFileProvider");
|
||||
_cacheProvider = new ZipDataCacheProvider(new DefaultDataProvider(), false);
|
||||
var historyProvider = new SubscriptionDataReaderHistoryProvider();
|
||||
|
||||
Algorithm = new PortfolioLooperAlgorithm((decimal)startingCash, orders, algorithmConfiguration);
|
||||
var dataPermissionManager = new DataPermissionManager();
|
||||
historyProvider.Initialize(new HistoryProviderInitializeParameters(null, null, null, _cacheProvider, mapFileProvider, factorFileProvider, (_) => { }, false, dataPermissionManager, Algorithm.ObjectStore, Algorithm.Settings));
|
||||
Algorithm.SetHistoryProvider(historyProvider);
|
||||
|
||||
// Dummy LEAN datafeed classes and initializations that essentially do nothing
|
||||
var job = new BacktestNodePacket(1, 2, "3", null, 9m, $"");
|
||||
var feed = new MockDataFeed();
|
||||
|
||||
// Create MHDB and Symbol properties DB instances for the DataManager
|
||||
var marketHoursDatabase = MarketHoursDatabase.FromDataFolder();
|
||||
var symbolPropertiesDataBase = SymbolPropertiesDatabase.FromDataFolder();
|
||||
_dataManager = new DataManager(feed,
|
||||
new UniverseSelection(
|
||||
Algorithm,
|
||||
new SecurityService(Algorithm.Portfolio.CashBook,
|
||||
marketHoursDatabase,
|
||||
symbolPropertiesDataBase,
|
||||
Algorithm,
|
||||
RegisteredSecurityDataTypesProvider.Null,
|
||||
new SecurityCacheProvider(Algorithm.Portfolio),
|
||||
algorithm: Algorithm),
|
||||
dataPermissionManager,
|
||||
new DefaultDataProvider()),
|
||||
Algorithm,
|
||||
Algorithm.TimeKeeper,
|
||||
marketHoursDatabase,
|
||||
false,
|
||||
RegisteredSecurityDataTypesProvider.Null,
|
||||
dataPermissionManager);
|
||||
|
||||
_securityService = new SecurityService(Algorithm.Portfolio.CashBook,
|
||||
marketHoursDatabase,
|
||||
symbolPropertiesDataBase,
|
||||
Algorithm,
|
||||
RegisteredSecurityDataTypesProvider.Null,
|
||||
new SecurityCacheProvider(Algorithm.Portfolio),
|
||||
algorithm: Algorithm);
|
||||
|
||||
var transactions = new BacktestingTransactionHandler();
|
||||
_resultHandler = new BacktestingResultHandler();
|
||||
|
||||
// Initialize security services and other properties so that we
|
||||
// don't get null reference exceptions during our re-calculation
|
||||
Algorithm.Securities.SetSecurityService(_securityService);
|
||||
Algorithm.SubscriptionManager.SetDataManager(_dataManager);
|
||||
|
||||
// Initialize the algorithm before adding any securities
|
||||
Algorithm.Initialize();
|
||||
Algorithm.PostInitialize();
|
||||
|
||||
// Initializes all the proper Securities from the orders provided by the user
|
||||
Algorithm.FromOrders(orders);
|
||||
|
||||
// More initialization, this time with Algorithm and other misc. classes
|
||||
_resultHandler.Initialize(new (job, new Messaging.Messaging(), new Api.Api(), transactions, mapFileProvider));
|
||||
_resultHandler.SetAlgorithm(Algorithm, Algorithm.Portfolio.TotalPortfolioValue);
|
||||
|
||||
Algorithm.Transactions.SetOrderProcessor(transactions);
|
||||
|
||||
transactions.Initialize(Algorithm, new BacktestingBrokerage(Algorithm), _resultHandler);
|
||||
feed.Initialize(Algorithm, job, _resultHandler, null, null, null, _dataManager, null, null);
|
||||
|
||||
// Begin setting up the currency conversion feed if needed
|
||||
var coreSecurities = Algorithm.Securities.Values.ToList();
|
||||
|
||||
BaseSetupHandler.SetupCurrencyConversions(Algorithm, _dataManager.UniverseSelection);
|
||||
var conversionSecurities = Algorithm.Securities.Values.Where(s => !coreSecurities.Contains(s)).ToList();
|
||||
|
||||
// Skip the history request if we don't need to convert anything
|
||||
if (conversionSecurities.Any())
|
||||
{
|
||||
// Point-in-time Slices to convert FX and Crypto currencies to the portfolio currency
|
||||
_conversionSlices = GetHistory(Algorithm, conversionSecurities, resolution);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
_dataManager.RemoveAllSubscriptions();
|
||||
_cacheProvider.DisposeSafely();
|
||||
_resultHandler.Exit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal method to get the history for the given securities
|
||||
/// </summary>
|
||||
/// <param name="algorithm">Algorithm</param>
|
||||
/// <param name="securities">Securities to get history for</param>
|
||||
/// <param name="resolution">Resolution to retrieve data in</param>
|
||||
/// <returns>History of the given securities</returns>
|
||||
/// <remarks>Method is static because we want to use it from the constructor as well</remarks>
|
||||
private static IEnumerable<Slice> GetHistory(IAlgorithm algorithm, List<Security> securities, Resolution resolution)
|
||||
{
|
||||
var historyRequests = new List<Data.HistoryRequest>();
|
||||
var historyRequestFactory = new HistoryRequestFactory(algorithm);
|
||||
|
||||
// Create the history requests
|
||||
foreach (var security in securities)
|
||||
{
|
||||
var configs = algorithm.SubscriptionManager
|
||||
.SubscriptionDataConfigService
|
||||
.GetSubscriptionDataConfigs(security.Symbol, includeInternalConfigs: true);
|
||||
|
||||
// we need to order and select a specific configuration type
|
||||
// so the conversion rate is deterministic
|
||||
var configToUse = configs.OrderBy(x => x.TickType).First();
|
||||
|
||||
var startTime = historyRequestFactory.GetStartTimeAlgoTz(
|
||||
security.Symbol,
|
||||
1,
|
||||
resolution,
|
||||
security.Exchange.Hours,
|
||||
configToUse.DataTimeZone,
|
||||
configToUse.Type);
|
||||
var endTime = algorithm.EndDate;
|
||||
|
||||
historyRequests.Add(historyRequestFactory.CreateHistoryRequest(
|
||||
configToUse,
|
||||
startTime,
|
||||
endTime,
|
||||
security.Exchange.Hours,
|
||||
resolution
|
||||
));
|
||||
}
|
||||
|
||||
return algorithm.HistoryProvider.GetHistory(historyRequests, algorithm.TimeZone).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the history for the given symbols from the <paramref name="start"/> to the <paramref name="end"/>
|
||||
/// </summary>
|
||||
/// <param name="symbols">Symbols to request history for</param>
|
||||
/// <param name="start">Start date of history request</param>
|
||||
/// <param name="end">End date of history request</param>
|
||||
/// <param name="resolution">Resolution of history request</param>
|
||||
/// <returns>Enumerable of slices</returns>
|
||||
public static IEnumerable<Slice> GetHistory(List<Symbol> symbols, DateTime start, DateTime end, Resolution resolution)
|
||||
{
|
||||
// Handles the conversion of Symbol to Security for us.
|
||||
var looper = new PortfolioLooper(0, new List<Order>(), resolution);
|
||||
var securities = new List<Security>();
|
||||
|
||||
looper.Algorithm.SetStartDate(start);
|
||||
looper.Algorithm.SetEndDate(end);
|
||||
|
||||
foreach (var symbol in symbols)
|
||||
{
|
||||
var configs = looper.Algorithm.SubscriptionManager.SubscriptionDataConfigService.Add(symbol, resolution, false, false);
|
||||
securities.Add(looper.Algorithm.Securities.CreateSecurity(symbol, configs));
|
||||
}
|
||||
|
||||
return GetHistory(looper.Algorithm, securities, resolution);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the point in time portfolio over multiple deployments
|
||||
/// </summary>
|
||||
/// <param name="equityCurve">Equity curve series</param>
|
||||
/// <param name="orders">Orders</param>
|
||||
/// <param name="algorithmConfiguration">Optional parameter to override default algorithm configuration</param>
|
||||
/// <param name="liveSeries">Equity curve series originates from LiveResult</param>
|
||||
/// <returns>Enumerable of <see cref="PointInTimePortfolio"/></returns>
|
||||
public static IEnumerable<PointInTimePortfolio> FromOrders(Series<DateTime, double> equityCurve, IEnumerable<Order> orders,
|
||||
AlgorithmConfiguration algorithmConfiguration = null, bool liveSeries = false)
|
||||
{
|
||||
// Don't do anything if we have no orders or equity curve to process
|
||||
if (!orders.Any() || equityCurve.IsEmpty)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Chunk different deployments into separate Lists for separate processing
|
||||
var portfolioDeployments = new List<List<Order>>();
|
||||
|
||||
// Orders are guaranteed to start counting from 1. This ensures that we have
|
||||
// no collision at all with the start of a deployment
|
||||
var previousOrderId = 0;
|
||||
var currentDeployment = new List<Order>();
|
||||
|
||||
// Make use of reference semantics to add new deployments to the list
|
||||
portfolioDeployments.Add(currentDeployment);
|
||||
|
||||
foreach (var order in orders)
|
||||
{
|
||||
// In case we have two different deployments with only a single
|
||||
// order in the deployments, <= was chosen because it covers duplicate values
|
||||
if (order.Id <= previousOrderId)
|
||||
{
|
||||
currentDeployment = new List<Order>();
|
||||
portfolioDeployments.Add(currentDeployment);
|
||||
}
|
||||
|
||||
currentDeployment.Add(order);
|
||||
previousOrderId = order.Id;
|
||||
}
|
||||
|
||||
PortfolioLooper looper = null;
|
||||
PointInTimePortfolio prev = null;
|
||||
foreach (var deploymentOrders in portfolioDeployments)
|
||||
{
|
||||
if (deploymentOrders.Count == 0)
|
||||
{
|
||||
Log.Trace($"PortfolioLooper.FromOrders(): Deployment contains no orders");
|
||||
continue;
|
||||
}
|
||||
var startTime = deploymentOrders.First().Time;
|
||||
var deployment = equityCurve.Where(kvp => kvp.Key <= startTime);
|
||||
if (deployment.IsEmpty)
|
||||
{
|
||||
Log.Trace($"PortfolioLooper.FromOrders(): Equity series is empty after filtering with upper bound: {startTime}");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip any deployments that haven't been ran long enough to be generated in live mode
|
||||
if (liveSeries && deploymentOrders.First().Time.Date == deploymentOrders.Last().Time.Date)
|
||||
{
|
||||
Log.Trace("PortfolioLooper.FromOrders(): Filtering deployment because it has not been deployed for more than one day");
|
||||
continue;
|
||||
}
|
||||
|
||||
// For every deployment, we want to start fresh.
|
||||
looper = new PortfolioLooper(deployment.LastValue(), deploymentOrders, algorithmConfiguration: algorithmConfiguration);
|
||||
|
||||
foreach (var portfolio in looper.ProcessOrders(deploymentOrders))
|
||||
{
|
||||
prev = portfolio;
|
||||
yield return portfolio;
|
||||
}
|
||||
}
|
||||
|
||||
if (prev != null)
|
||||
{
|
||||
yield return new PointInTimePortfolio(prev, equityCurve.LastKey());
|
||||
}
|
||||
|
||||
looper.DisposeSafely();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process the orders
|
||||
/// </summary>
|
||||
/// <param name="orders">orders</param>
|
||||
/// <returns>PointInTimePortfolio</returns>
|
||||
private IEnumerable<PointInTimePortfolio> ProcessOrders(IEnumerable<Order> orders)
|
||||
{
|
||||
// Portfolio.ProcessFill(...) does not filter out invalid orders. We must do so ourselves
|
||||
foreach (var order in orders)
|
||||
{
|
||||
Algorithm.SetDateTime(order.Time);
|
||||
|
||||
var orderSecurity = Algorithm.Securities[order.Symbol];
|
||||
DateTime lastFillTime;
|
||||
|
||||
if ((order.Type == OrderType.MarketOnOpen || order.Type == OrderType.MarketOnClose) &&
|
||||
(order.Status == OrderStatus.Filled || order.Status == OrderStatus.PartiallyFilled) && order.LastFillTime == null)
|
||||
{
|
||||
lastFillTime = order.Time;
|
||||
}
|
||||
else if (order.LastFillTime == null)
|
||||
{
|
||||
Log.Trace($"Order with ID: {order.Id} has been skipped because of null LastFillTime");
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
lastFillTime = order.LastFillTime.Value;
|
||||
}
|
||||
|
||||
var tick = new Tick { Quantity = order.Quantity, AskPrice = order.Price, BidPrice = order.Price, Value = order.Price, EndTime = lastFillTime };
|
||||
var tradeBar = new TradeBar
|
||||
{
|
||||
Open = order.Price,
|
||||
High = order.Price,
|
||||
Low = order.Price,
|
||||
Close = order.Price,
|
||||
Volume = order.Quantity,
|
||||
|
||||
DataType = MarketDataType.TradeBar,
|
||||
Period = TimeSpan.Zero,
|
||||
Symbol = order.Symbol,
|
||||
Time = lastFillTime,
|
||||
};
|
||||
|
||||
// Required for crypto so that the Cache Price is updated accordingly,
|
||||
// since its `Security.Price` implementation explicitly requests TradeBars.
|
||||
// For most asset types this might be enough as well, but there is the
|
||||
// possibility that some trades might get filtered, so we cover that
|
||||
// case by setting the market price via Tick as well.
|
||||
orderSecurity.SetMarketPrice(tradeBar);
|
||||
orderSecurity.SetMarketPrice(tick);
|
||||
|
||||
// Check if we have a base currency (i.e. forex or crypto that requires currency conversion)
|
||||
// to ensure the proper conversion rate is set for them
|
||||
var baseCurrency = orderSecurity as IBaseCurrencySymbol;
|
||||
|
||||
if (baseCurrency != null)
|
||||
{
|
||||
// We want slices that apply to either this point in time, or the last most recent point in time
|
||||
var updateSlices = _conversionSlices.Where(x => x.Time <= order.Time).ToList();
|
||||
|
||||
// This is put here because there can potentially be no slices
|
||||
if (updateSlices.Count != 0)
|
||||
{
|
||||
var updateSlice = updateSlices.Last();
|
||||
|
||||
foreach (var quoteBar in updateSlice.QuoteBars.Values)
|
||||
{
|
||||
Algorithm.Securities[quoteBar.Symbol].SetMarketPrice(quoteBar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update our cash holdings before we invalidate the portfolio value
|
||||
// to calculate the proper cash value of other assets the algo owns
|
||||
foreach (var cash in Algorithm.Portfolio.CashBook.Values.Where(x => x.CurrencyConversion != null))
|
||||
{
|
||||
cash.Update();
|
||||
}
|
||||
|
||||
// Securities prices might have been updated, so we need to recalculate how much
|
||||
// money we have in our portfolio, otherwise we risk being out of date and
|
||||
// calculate on stale data.
|
||||
Algorithm.Portfolio.InvalidateTotalPortfolioValue();
|
||||
|
||||
var ticket = order.ToOrderTicket(Algorithm.Transactions);
|
||||
var orderEvent = new OrderEvent(order, order.Time, Orders.Fees.OrderFee.Zero) { FillPrice = order.Price, FillQuantity = order.Quantity, Ticket = ticket };
|
||||
|
||||
// Process the order
|
||||
Algorithm.Portfolio.ProcessFills(new List<OrderEvent> { orderEvent });
|
||||
|
||||
// Create portfolio statistics and return back to the user
|
||||
yield return new PointInTimePortfolio(order, Algorithm.Portfolio);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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 QuantConnect.Algorithm;
|
||||
using QuantConnect.Orders;
|
||||
using QuantConnect.Securities;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Fake algorithm that initializes portfolio and algorithm securities. Never ran.
|
||||
/// </summary>
|
||||
public class PortfolioLooperAlgorithm : QCAlgorithm
|
||||
{
|
||||
private decimal _startingCash;
|
||||
private List<Order> _orders;
|
||||
private AlgorithmConfiguration _algorithmConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize an instance of <see cref="PortfolioLooperAlgorithm"/>
|
||||
/// </summary>
|
||||
/// <param name="startingCash">Starting algorithm cash</param>
|
||||
/// <param name="orders">Orders to use</param>
|
||||
/// <param name="algorithmConfiguration">Optional parameter to override default algorithm configuration</param>
|
||||
public PortfolioLooperAlgorithm(decimal startingCash, IEnumerable<Order> orders, AlgorithmConfiguration algorithmConfiguration = null) : base()
|
||||
{
|
||||
_startingCash = startingCash;
|
||||
_orders = orders.ToList();
|
||||
_algorithmConfiguration = algorithmConfiguration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes all the proper Securities from the orders provided by the user
|
||||
/// </summary>
|
||||
/// <param name="orders">Orders to use</param>
|
||||
public void FromOrders(IEnumerable<Order> orders)
|
||||
{
|
||||
foreach (var symbol in orders.Select(x => x.Symbol).Distinct())
|
||||
{
|
||||
Resolution resolution;
|
||||
switch (symbol.SecurityType)
|
||||
{
|
||||
case SecurityType.Option:
|
||||
case SecurityType.Future:
|
||||
resolution = Resolution.Minute;
|
||||
break;
|
||||
default:
|
||||
resolution = Resolution.Daily;
|
||||
break;
|
||||
}
|
||||
|
||||
var configs = SubscriptionManager.SubscriptionDataConfigService.Add(symbol, resolution, false, false);
|
||||
var security = Securities.CreateSecurity(symbol, configs, 0m);
|
||||
if (symbol.SecurityType == SecurityType.Crypto)
|
||||
{
|
||||
security.BuyingPowerModel = new SecurityMarginModel();
|
||||
}
|
||||
|
||||
// Set leverage to 10000 to account for unknown leverage values in user algorithms
|
||||
security.SetLeverage(10000m);
|
||||
|
||||
var method = typeof(QCAlgorithm).GetMethod("AddToUserDefinedUniverse", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
method.Invoke(this, new object[] { security, configs });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize this algorithm
|
||||
/// </summary>
|
||||
public override void Initialize()
|
||||
{
|
||||
if (_algorithmConfiguration != null)
|
||||
{
|
||||
SetAccountCurrency(_algorithmConfiguration.AccountCurrency);
|
||||
SetBrokerageModel(_algorithmConfiguration.Brokerage, _algorithmConfiguration.AccountType);
|
||||
}
|
||||
|
||||
SetCash(_startingCash);
|
||||
|
||||
if (_orders.Count != 0)
|
||||
{
|
||||
SetStartDate(_orders.First().Time);
|
||||
SetEndDate(_orders.Last().Time);
|
||||
}
|
||||
|
||||
SetBenchmark(b => 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
/*
|
||||
* 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.IO;
|
||||
using Newtonsoft.Json;
|
||||
using QuantConnect.Util;
|
||||
using System.Diagnostics;
|
||||
using QuantConnect.Logging;
|
||||
using QuantConnect.Packets;
|
||||
using QuantConnect.Lean.Engine;
|
||||
using System.Collections.Generic;
|
||||
using QuantConnect.Configuration;
|
||||
using QuantConnect.Python;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Lean Report creates a PDF strategy summary from the backtest and live json objects.
|
||||
/// </summary>
|
||||
class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
// Parse report arguments and merge with config to use in report creator:
|
||||
if (args.Length > 0)
|
||||
{
|
||||
Config.MergeCommandLineArgumentsWithConfiguration(ReportArgumentParser.ParseArguments(args));
|
||||
}
|
||||
|
||||
// initialize required lean handlers
|
||||
LeanEngineAlgorithmHandlers.FromConfiguration(Composer.Instance);
|
||||
var name = Config.Get("strategy-name");
|
||||
var description = Config.Get("strategy-description");
|
||||
var version = Config.Get("strategy-version");
|
||||
var backtestDataFile = Config.Get("backtest-data-source-file");
|
||||
var liveDataFile = Config.Get("live-data-source-file");
|
||||
var destination = Config.Get("report-destination");
|
||||
var reportFormat = Config.Get("report-format");
|
||||
var cssOverrideFile = Config.Get("report-css-override-file", "css/report_override.css");
|
||||
var htmlCustomFile = Config.Get("report-html-custom-file", "template.html");
|
||||
var pythonVirtualEnvironment = Config.Get("python-venv");
|
||||
|
||||
// Activate virtual environment if defined
|
||||
PythonInitializer.ActivatePythonVirtualEnvironment(pythonVirtualEnvironment);
|
||||
|
||||
// Initialize and add our Paths
|
||||
PythonInitializer.Initialize();
|
||||
|
||||
// Parse content from source files into result objects
|
||||
Log.Trace($"QuantConnect.Report.Main(): Parsing source files...{backtestDataFile}, {liveDataFile}");
|
||||
var backtestSettings = new JsonSerializerSettings
|
||||
{
|
||||
Converters = new List<JsonConverter> { new NullResultValueTypeJsonConverter<BacktestResult>() },
|
||||
FloatParseHandling = FloatParseHandling.Decimal
|
||||
};
|
||||
|
||||
var backtest = JsonConvert.DeserializeObject<BacktestResult>(File.ReadAllText(backtestDataFile), backtestSettings);
|
||||
LiveResult live = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(liveDataFile))
|
||||
{
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
Converters = new List<JsonConverter> { new NullResultValueTypeJsonConverter<LiveResult>() }
|
||||
};
|
||||
|
||||
live = JsonConvert.DeserializeObject<LiveResult>(File.ReadAllText(liveDataFile), settings);
|
||||
}
|
||||
|
||||
string cssOverrideContent = null;
|
||||
if (!string.IsNullOrEmpty(cssOverrideFile))
|
||||
{
|
||||
if (File.Exists(cssOverrideFile))
|
||||
{
|
||||
cssOverrideContent = File.ReadAllText(cssOverrideFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Trace($"QuantConnect.Report.Main(): CSS override file {cssOverrideFile} was not found");
|
||||
}
|
||||
}
|
||||
|
||||
string htmlCustomContent = null;
|
||||
if (!string.IsNullOrEmpty(htmlCustomFile))
|
||||
{
|
||||
if (File.Exists(htmlCustomFile))
|
||||
{
|
||||
htmlCustomContent = File.ReadAllText(htmlCustomFile);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Trace($"QuantConnect.Report.Main(): HTML custom file {htmlCustomFile} was not found");
|
||||
}
|
||||
}
|
||||
|
||||
//Create a new report
|
||||
Log.Trace("QuantConnect.Report.Main(): Instantiating report...");
|
||||
var report = new Report(name, description, version, backtest, live, cssOverride: cssOverrideContent, htmlCustom: htmlCustomContent);
|
||||
|
||||
// Generate the html content
|
||||
Log.Trace("QuantConnect.Report.Main(): Starting content compile...");
|
||||
string html;
|
||||
string _;
|
||||
|
||||
report.Compile(out html, out _);
|
||||
|
||||
//Write it to target destination.
|
||||
if (!string.IsNullOrEmpty(destination))
|
||||
{
|
||||
Log.Trace($"QuantConnect.Report.Main(): Writing content to file {destination}");
|
||||
File.WriteAllText(destination, html);
|
||||
|
||||
if (!String.IsNullOrEmpty(reportFormat) && reportFormat.ToUpperInvariant() == "PDF")
|
||||
{
|
||||
try
|
||||
{
|
||||
Log.Trace("QuantConnect.Report.Main(): Starting conversion to PDF");
|
||||
// Ensure wkhtmltopdf and xvfb are installed and accessible from the $PATH
|
||||
var pdfDestination = destination.Replace(".html", ".pdf");
|
||||
Process process = new();
|
||||
process.StartInfo.FileName = "xvfb-run";
|
||||
process.StartInfo.Arguments = $"--server-args=\"-screen 0, 1600x1200x24+32\" wkhtmltopdf {destination} {pdfDestination}";
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
process.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
|
||||
|
||||
process.OutputDataReceived += (sender, e) => Log.Trace($"QuantConnect.Report.Main(): {e.Data}");
|
||||
process.ErrorDataReceived += (sender, e) => Log.Error($"QuantConnect.Report.Main(): {e.Data}");
|
||||
|
||||
process.Start();
|
||||
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
|
||||
var processExited = process.WaitForExit(1*60*1000); // wait for up to 1 minutes
|
||||
|
||||
if (processExited)
|
||||
{
|
||||
Log.Trace("QuantConnect.Report.Main(): Convert to PDF process exited with code " + process.ExitCode);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("QuantConnect.Report.Main(): Process did not exit within the timeout period.");
|
||||
process.Kill(); // kill the process if it's still running
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error($"QuantConnect.Report.Main(): {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Write(html);
|
||||
}
|
||||
|
||||
Log.Trace("QuantConnect.Report.Main(): Completed.");
|
||||
|
||||
if (!Console.IsInputRedirected && !Config.GetBool("close-automatically"))
|
||||
{
|
||||
Console.ReadKey();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("QuantConnect.Report")]
|
||||
[assembly: AssemblyProduct("QuantConnect.Report")]
|
||||
[assembly: AssemblyCulture("")]
|
||||
|
||||
// Setting ComVisible to false makes the types in this assembly not visible
|
||||
// to COM components. If you need to access a type in this assembly from
|
||||
// COM, set the ComVisible attribute to true on that type.
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||
[assembly: Guid("2431419f-8bc6-4f59-944e-9a1cd28982df")]
|
||||
@@ -0,0 +1,88 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>QuantConnect.Report</RootNamespace>
|
||||
<AssemblyName>QuantConnect.Report</AssemblyName>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||
<DocumentationFile>bin\$(Configuration)\QuantConnect.Report.xml</DocumentationFile>
|
||||
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<Description>QuantConnect LEAN Engine: Report Project - Generates live and backtesting reports</Description>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>$(SelectedOptimization)</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<StartupObject>QuantConnect.Report.Program</StartupObject>
|
||||
</PropertyGroup>
|
||||
<Target Name="PrintRID" BeforeTargets="Build">
|
||||
<Message Text="SelectedOptimization $(SelectedOptimization)" Importance="high" />
|
||||
</Target>
|
||||
<PropertyGroup>
|
||||
<Features>flow-analysis</Features>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="QuantConnect.pythonnet" Version="2.0.60" />
|
||||
<PackageReference Include="Deedle" Version="2.1.0" />
|
||||
<PackageReference Include="MathNet.Numerics" Version="5.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageReference Include="NodaTime" Version="3.0.5" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Common\Properties\SharedAssemblyInfo.cs" Link="Properties\SharedAssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="css\report.css">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="css\report_override.css">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="ReportCharts.py">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
<Content Include="config.example.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Algorithm\QuantConnect.Algorithm.csproj" />
|
||||
<ProjectReference Include="..\Api\QuantConnect.Api.csproj" />
|
||||
<ProjectReference Include="..\Brokerages\QuantConnect.Brokerages.csproj" />
|
||||
<ProjectReference Include="..\Common\QuantConnect.csproj" />
|
||||
<ProjectReference Include="..\Configuration\QuantConnect.Configuration.csproj" />
|
||||
<ProjectReference Include="..\Engine\QuantConnect.Lean.Engine.csproj" />
|
||||
<ProjectReference Include="..\Logging\QuantConnect.Logging.csproj" />
|
||||
<ProjectReference Include="..\Messaging\QuantConnect.Messaging.csproj" />
|
||||
<ProjectReference Include="..\ToolBox\QuantConnect.ToolBox.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="ReportChartTests.py" />
|
||||
<Content Include="template.html">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\LICENSE">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath></PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* 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.IO;
|
||||
using System.Linq;
|
||||
using Deedle;
|
||||
using Newtonsoft.Json;
|
||||
using QuantConnect.Configuration;
|
||||
using QuantConnect.Logging;
|
||||
using QuantConnect.Packets;
|
||||
using QuantConnect.Report.ReportElements;
|
||||
using QuantConnect.Orders;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Report class
|
||||
/// </summary>
|
||||
public class Report
|
||||
{
|
||||
private string _template;
|
||||
private readonly List<IReportElement> _elements;
|
||||
|
||||
/// <summary>
|
||||
/// File name for statistics
|
||||
/// </summary>
|
||||
public const string StatisticsFileName = "report-statistics.json";
|
||||
|
||||
/// <summary>
|
||||
/// Create beautiful HTML and PDF Reports based on backtest and live data.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the strategy</param>
|
||||
/// <param name="description">Description of the strategy</param>
|
||||
/// <param name="version">Version number of the strategy</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
/// <param name="pointInTimePortfolioDestination">Point in time portfolio json output base filename</param>
|
||||
/// <param name="cssOverride">CSS file that overrides some of the default rules defined in report.css</param>
|
||||
/// <param name="htmlCustom">Custom HTML file to replace the default template</param>
|
||||
public Report(string name, string description, string version, BacktestResult backtest, LiveResult live, string pointInTimePortfolioDestination = null, string cssOverride = null, string htmlCustom = null)
|
||||
{
|
||||
_template = htmlCustom ?? File.ReadAllText("template.html");
|
||||
var crisisHtmlContent = GetRegexInInput(@"<!--crisis(\r|\n)*((\r|\n|.)*?)crisis-->", _template);
|
||||
var parametersHtmlContent = GetRegexInInput(@"<!--parameters(\r|\n)*((\r|\n|.)*?)parameters-->", _template);
|
||||
|
||||
var backtestCurve = new Series<DateTime, double>(ResultsUtil.EquityPoints(backtest));
|
||||
var liveCurve = new Series<DateTime, double>(ResultsUtil.EquityPoints(live));
|
||||
|
||||
var backtestOrders = backtest?.Orders?.Values.ToList() ?? new List<Order>();
|
||||
var liveOrders = live?.Orders?.Values.ToList() ?? new List<Order>();
|
||||
|
||||
var backtestConfiguration = backtest?.AlgorithmConfiguration;
|
||||
var liveConfiguration = live?.AlgorithmConfiguration;
|
||||
|
||||
// Earlier we use constant's value tradingDaysPerYear = 252
|
||||
// backtestConfiguration?.TradingDaysPerYear equal liveConfiguration?.TradingDaysPerYear
|
||||
var tradingDayPerYear = backtestConfiguration?.TradingDaysPerYear ?? 252;
|
||||
|
||||
Log.Trace($"QuantConnect.Report.Report(): Processing backtesting orders");
|
||||
var backtestPortfolioInTime = PortfolioLooper.FromOrders(backtestCurve, backtestOrders, backtestConfiguration).ToList();
|
||||
Log.Trace($"QuantConnect.Report.Report(): Processing live orders");
|
||||
var livePortfolioInTime = PortfolioLooper.FromOrders(liveCurve, liveOrders, liveConfiguration, liveSeries: true).ToList();
|
||||
|
||||
var destination = pointInTimePortfolioDestination ?? Config.Get("report-destination");
|
||||
if (!string.IsNullOrWhiteSpace(destination))
|
||||
{
|
||||
if (backtestPortfolioInTime.Count != 0)
|
||||
{
|
||||
var dailyBacktestPortfolioInTime = backtestPortfolioInTime
|
||||
.Select(x => new PointInTimePortfolio(x, x.Time.Date).NoEmptyHoldings())
|
||||
.GroupBy(x => x.Time.Date)
|
||||
.Select(kvp => kvp.Last())
|
||||
.OrderBy(x => x.Time)
|
||||
.ToList();
|
||||
|
||||
var outputFile = destination.Replace(".html", string.Empty) + "-backtesting-portfolio.json";
|
||||
Log.Trace($"Report.Report(): Writing backtest point-in-time portfolios to JSON file: {outputFile}");
|
||||
var backtestPortfolioOutput = JsonConvert.SerializeObject(dailyBacktestPortfolioInTime);
|
||||
File.WriteAllText(outputFile, backtestPortfolioOutput);
|
||||
}
|
||||
if (livePortfolioInTime.Count != 0)
|
||||
{
|
||||
var dailyLivePortfolioInTime = livePortfolioInTime
|
||||
.Select(x => new PointInTimePortfolio(x, x.Time.Date).NoEmptyHoldings())
|
||||
.GroupBy(x => x.Time.Date)
|
||||
.Select(kvp => kvp.Last())
|
||||
.OrderBy(x => x.Time)
|
||||
.ToList();
|
||||
|
||||
var outputFile = destination.Replace(".html", string.Empty) + "-live-portfolio.json";
|
||||
Log.Trace($"Report.Report(): Writing live point-in-time portfolios to JSON file: {outputFile}");
|
||||
var livePortfolioOutput = JsonConvert.SerializeObject(dailyLivePortfolioInTime);
|
||||
File.WriteAllText(outputFile, livePortfolioOutput);
|
||||
}
|
||||
}
|
||||
|
||||
_elements = new List<IReportElement>
|
||||
{
|
||||
//Basics
|
||||
new TextReportElement("strategy name", ReportKey.StrategyName, name),
|
||||
new TextReportElement("description", ReportKey.StrategyDescription, description),
|
||||
new TextReportElement("version", ReportKey.StrategyVersion, version),
|
||||
new TextReportElement("stylesheet", ReportKey.Stylesheet, File.ReadAllText("css/report.css") + (cssOverride)),
|
||||
new TextReportElement("live marker key", ReportKey.LiveMarker, live == null ? string.Empty : "Live "),
|
||||
|
||||
//KPI's Backtest:
|
||||
new RuntimeDaysReportElement("runtime days kpi", ReportKey.BacktestDays, backtest, live),
|
||||
new CAGRReportElement("cagr kpi", ReportKey.CAGR, backtest, live),
|
||||
new TurnoverReportElement("turnover kpi", ReportKey.Turnover, backtest, live),
|
||||
new MaxDrawdownReportElement("max drawdown kpi", ReportKey.MaxDrawdown, backtest, live),
|
||||
new MaxDrawdownRecoveryReportElement("max drawdown recovery kpi", ReportKey.MaxDrawdownRecovery, backtest, live),
|
||||
new SharpeRatioReportElement("sharpe kpi", ReportKey.SharpeRatio, backtest, live, tradingDayPerYear),
|
||||
new SortinoRatioReportElement("sortino kpi", ReportKey.SortinoRatio, backtest, live, tradingDayPerYear),
|
||||
new PSRReportElement("psr kpi", ReportKey.PSR, backtest, live, tradingDayPerYear),
|
||||
new InformationRatioReportElement("ir kpi", ReportKey.InformationRatio, backtest, live),
|
||||
new MarketsReportElement("markets kpi", ReportKey.Markets, backtest, live),
|
||||
new TradesPerDayReportElement("trades per day kpi", ReportKey.TradesPerDay, backtest, live),
|
||||
new EstimatedCapacityReportElement("estimated algorithm capacity", ReportKey.StrategyCapacity, backtest, live),
|
||||
|
||||
// Generate and insert plots MonthlyReturnsReportElement
|
||||
new MonthlyReturnsReportElement("monthly return plot", ReportKey.MonthlyReturns, backtest, live),
|
||||
new CumulativeReturnsReportElement("cumulative returns", ReportKey.CumulativeReturns, backtest, live),
|
||||
new AnnualReturnsReportElement("annual returns", ReportKey.AnnualReturns, backtest, live),
|
||||
new ReturnsPerTradeReportElement("returns per trade", ReportKey.ReturnsPerTrade, backtest, live),
|
||||
new AssetAllocationReportElement("asset allocation over time pie chart", ReportKey.AssetAllocation, backtest, live, backtestPortfolioInTime, livePortfolioInTime),
|
||||
new DrawdownReportElement("drawdown plot", ReportKey.Drawdown, backtest, live),
|
||||
new DailyReturnsReportElement("daily returns plot", ReportKey.DailyReturns, backtest, live),
|
||||
new RollingPortfolioBetaReportElement("rolling beta to equities plot", ReportKey.RollingBeta, backtest, live, tradingDayPerYear),
|
||||
new RollingSharpeReportElement("rolling sharpe ratio plot", ReportKey.RollingSharpe, backtest, live, tradingDayPerYear),
|
||||
new LeverageUtilizationReportElement("leverage plot", ReportKey.LeverageUtilization, backtest, live, backtestPortfolioInTime, livePortfolioInTime),
|
||||
new ExposureReportElement("exposure plot", ReportKey.Exposure, backtest, live, backtestPortfolioInTime, livePortfolioInTime)
|
||||
};
|
||||
|
||||
// Include Algorithm Parameters
|
||||
if (parametersHtmlContent != null)
|
||||
{
|
||||
_elements.Add(new ParametersReportElement("parameters page", ReportKey.ParametersPageStyle, backtestConfiguration, liveConfiguration, parametersHtmlContent));
|
||||
_elements.Add(new ParametersReportElement("parameters", ReportKey.Parameters, backtestConfiguration, liveConfiguration, parametersHtmlContent));
|
||||
}
|
||||
|
||||
// Array of Crisis Plots:
|
||||
if (crisisHtmlContent != null)
|
||||
{
|
||||
_elements.Add(new CrisisReportElement("crisis page", ReportKey.CrisisPageStyle, backtest, live, crisisHtmlContent));
|
||||
_elements.Add(new CrisisReportElement("crisis plots", ReportKey.CrisisPlots, backtest, live, crisisHtmlContent));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compile the backtest data into a report
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public void Compile(out string html, out string reportStatistics)
|
||||
{
|
||||
html = _template;
|
||||
var statistics = new Dictionary<string, object>();
|
||||
|
||||
// Render the output and replace the report section
|
||||
foreach (var element in _elements)
|
||||
{
|
||||
Log.Trace($"QuantConnect.Report.Compile(): Rendering {element.Name}...");
|
||||
html = html.Replace(element.Key, element.Render());
|
||||
|
||||
if (element is TextReportElement || element is CrisisReportElement || element is ParametersReportElement || (element as ReportElement) == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var reportElement = element as ReportElement;
|
||||
statistics[reportElement.JsonKey] = reportElement.Result;
|
||||
}
|
||||
|
||||
reportStatistics = JsonConvert.SerializeObject(statistics, Formatting.None);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the regex pattern in the given input string
|
||||
/// </summary>
|
||||
/// <param name="pattern">Regex pattern to be find the input string</param>
|
||||
/// <param name="input">Input string that may contain the regex pattern</param>
|
||||
/// <returns>The regex pattern in the input string if found. Otherwise, null</returns>
|
||||
public static string GetRegexInInput(string pattern, string input)
|
||||
{
|
||||
var regex = new Regex(pattern);
|
||||
var match = regex.Match(input);
|
||||
var regexWithinInput = match.Success ? match.Groups[2].Value : null;
|
||||
return regexWithinInput;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
# 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.
|
||||
|
||||
# You can run this test by first running `nPython.exe` (with mono or otherwise):
|
||||
# $ ./nPython.exe ReportChartTests.py
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from datetime import datetime
|
||||
from ReportCharts import ReportCharts
|
||||
|
||||
charts = ReportCharts()
|
||||
|
||||
## Test GetReturnsPerTrade
|
||||
backtest = list(np.random.normal(0, 1, 1000))
|
||||
live = list(np.random.normal(0.5, 1, 400))
|
||||
result = charts.GetReturnsPerTrade([], [])
|
||||
result = charts.GetReturnsPerTrade(backtest, [])
|
||||
result = charts.GetReturnsPerTrade(backtest, live)
|
||||
|
||||
## Test GetCumulativeReturnsPlot
|
||||
time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01T00:00:00', periods=365)]
|
||||
strategy = np.linspace(1, 25, 365)
|
||||
benchmark = np.linspace(2, 26, 365)
|
||||
backtest = [time, strategy, time, benchmark]
|
||||
|
||||
time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2013-10-01T00:00:00', periods=50)]
|
||||
strategy = np.linspace(25, 29, 50)
|
||||
benchmark = np.linspace(26, 30, 50)
|
||||
live = [time, strategy, time, benchmark]
|
||||
|
||||
result = charts.GetCumulativeReturns()
|
||||
result = charts.GetCumulativeReturns(backtest)
|
||||
result = charts.GetCumulativeReturns(backtest, live)
|
||||
|
||||
## Test GetDailyReturnsPlot
|
||||
time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01T00:00:00', periods=365)]
|
||||
data = list(np.random.normal(0, 1, 365))
|
||||
backtest = [time, data]
|
||||
|
||||
time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2013-10-01T00:00:00', periods=120)]
|
||||
data = list(np.random.normal(0.5, 1.5, 120))
|
||||
live = [time, data]
|
||||
|
||||
empty = [[], []]
|
||||
result = charts.GetDailyReturns(empty, empty)
|
||||
result = charts.GetDailyReturns(backtest, empty)
|
||||
result = charts.GetDailyReturns(backtest, live)
|
||||
|
||||
## Test GetMonthlyReturnsPlot
|
||||
backtest = {'2016': [0.5, 0.7, 0.2, 0.23, 1.3, 1.45, 1.67, -2.3, -0.5, 1.23, 1.23, -3.5],
|
||||
'2017': [0.5, 0.7, 0.2, 0.23, 1.3, 1.45, 1.67, -2.3, -0.5, 1.23, 1.23, -3.5][::-1]}
|
||||
|
||||
live = {'2018': [0.5, 0.7, 0.2, 0.23, 1.3, 1.45, 1.67, -2.3, -0.5, 1.23, 1.23, -3.5],
|
||||
'2019': [1.5, 2.7, -3.2, -0.23, 4.3, -2.45, -1.67, 2.3, np.nan, np.nan, np.nan, np.nan]}
|
||||
|
||||
result = charts.GetMonthlyReturns({}, {})
|
||||
result = charts.GetMonthlyReturns(backtest, pd.DataFrame())
|
||||
result = charts.GetMonthlyReturns(backtest, live)
|
||||
|
||||
## Test GetAnnualReturnsPlot
|
||||
time = ['2012', '2013', '2014', '2015', '2016']
|
||||
strategy = list(np.random.normal(0, 1, 5))
|
||||
backtest = [time, strategy]
|
||||
|
||||
time = ['2017', '2018']
|
||||
strategy = list(np.random.normal(0.5, 1.5, 2))
|
||||
live = [time, strategy]
|
||||
|
||||
empty = [[], []]
|
||||
result = charts.GetAnnualReturns()
|
||||
result = charts.GetAnnualReturns(backtest)
|
||||
result = charts.GetAnnualReturns(backtest, live)
|
||||
|
||||
## Test GetDrawdownPlot
|
||||
time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01', periods=365)]
|
||||
data = list(np.random.uniform(-5, 0, 365))
|
||||
backtest = [time, data]
|
||||
time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2013-10-01', periods=100)]
|
||||
data = list(np.random.uniform(-5, 0, 100))
|
||||
live = [time, data]
|
||||
worst = [{'Begin': datetime(2012, 10, 1), 'End': datetime(2012, 10, 11)},
|
||||
{'Begin': datetime(2012, 12, 1), 'End': datetime(2012, 12, 11)},
|
||||
{'Begin': datetime(2013, 3, 1), 'End': datetime(2013, 3, 11)},
|
||||
{'Begin': datetime(2013, 4, 1), 'End': datetime(2013, 4, 1)},
|
||||
{'Begin': datetime(2013, 6, 1), 'End': datetime(2013, 6, 11)}]
|
||||
empty = [[], []]
|
||||
result = charts.GetDrawdown(empty, empty, {})
|
||||
result = charts.GetDrawdown(backtest, empty, worst)
|
||||
result = charts.GetDrawdown(backtest, live, worst)
|
||||
|
||||
## Test GetCrisisPlots (backtest only)
|
||||
equity = list(np.linspace(1, 25, 365))
|
||||
benchmark = list(np.linspace(2, 26, 365))
|
||||
time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01 00:00:00', periods=365)]
|
||||
backtest = [time, equity, benchmark]
|
||||
|
||||
empty = [[], [], []]
|
||||
result = charts.GetCrisisEventsPlots(empty, 'empty_crisis')
|
||||
result = charts.GetCrisisEventsPlots(backtest, 'dummy_crisis')
|
||||
|
||||
## Test GetRollingBetaPlot
|
||||
empty = [[], [], [], []]
|
||||
twelve = [np.nan for x in range(180)] + list(np.random.uniform(-1, 1, 185))
|
||||
six = list(np.random.uniform(-1, 1, 365))
|
||||
time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01 00:00:00', periods=365)]
|
||||
backtest = [time, six, twelve]
|
||||
|
||||
result = charts.GetRollingBeta([time, six, time, twelve], empty)
|
||||
result = charts.GetRollingBeta([time, six, [], []], empty)
|
||||
result = charts.GetRollingBeta(empty, empty)
|
||||
|
||||
twelve = [np.nan for x in range(180)] + list(np.random.uniform(-1, 1, 185))
|
||||
six = list(np.random.uniform(-1, 1, 365))
|
||||
time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2013-10-01 00:00:00', periods=365)]
|
||||
live = [time, six, time, twelve]
|
||||
|
||||
result = charts.GetRollingBeta(live)
|
||||
|
||||
## Test GetRollingSharpeRatioPlot
|
||||
six = list(np.random.uniform(1, 3, 365 * 2))
|
||||
twelve = [np.nan for x in range(365)] + list(np.random.uniform(1, 3, 365))
|
||||
time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2012-10-01 00:00:00', periods=365 * 2)]
|
||||
six_live = list(np.random.uniform(1, 3, 365 + 180))
|
||||
twelve_live = [np.nan for x in range(180)] + list(np.random.uniform(1, 3, 365))
|
||||
time_live = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2014-10-01 00:00:00', periods=365 + 180)]
|
||||
|
||||
empty = [[], [], [], []]
|
||||
result = charts.GetRollingSharpeRatio([time, six, time, twelve], empty)
|
||||
result = charts.GetRollingSharpeRatio([time, six, [], []], empty)
|
||||
result = charts.GetRollingSharpeRatio([time, six, time, twelve], [time_live, six_live, time_live, twelve_live])
|
||||
result = charts.GetRollingSharpeRatio([time, six, [], []], [time_live, six_live, time_live, twelve_live])
|
||||
result = charts.GetRollingSharpeRatio([time, six, time, twelve], [time_live, six_live, [], []])
|
||||
result = charts.GetRollingSharpeRatio([time, six, [], []], [time_live, six_live, [], []])
|
||||
|
||||
## Test GetAssetAllocationPlot
|
||||
backtest = [['SPY', 'IBM', 'NFLX', 'AAPL'], [0.50, 0.25, 0.125, 0.125]]
|
||||
live = [['SPY', 'IBM', 'AAPL'], [0.4, 0.4, 0.2]]
|
||||
empty = [[], []]
|
||||
result = charts.GetAssetAllocation(empty, empty)
|
||||
result = charts.GetAssetAllocation(backtest, empty)
|
||||
result = charts.GetAssetAllocation(backtest, live)
|
||||
|
||||
## Test GetLeveragePlot
|
||||
backtest = [[pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2014-10-01', periods=365)],
|
||||
list(np.random.uniform(0.5, 1.5, 365))]
|
||||
live = [[pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2015-10-01', periods=100)],
|
||||
list(np.random.uniform(0.5, 2, 100))]
|
||||
empty = [[], []]
|
||||
result = charts.GetLeverage(empty, empty)
|
||||
result = charts.GetLeverage(backtest, empty)
|
||||
result = charts.GetLeverage(backtest, live)
|
||||
|
||||
## Test GetExposurePlot
|
||||
time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2014-10-01', periods=365)]
|
||||
long_securities = list(ReportCharts.color_map.keys())
|
||||
short_securities = long_securities
|
||||
long = [np.random.uniform(0, 0.5, 365) for x in long_securities]
|
||||
short = [np.random.uniform(-0.5, 0, 365) for x in short_securities]
|
||||
|
||||
live_time = [pd.Timestamp(x).to_pydatetime() for x in pd.date_range('2015-10-01', periods=100)]
|
||||
live_long_securities = long_securities
|
||||
live_short_securities = long_securities
|
||||
live_long = [np.random.uniform(0, 0.5, 100) for x in live_long_securities]
|
||||
live_short = [np.random.uniform(-0.5, -0, 100) for x in live_short_securities]
|
||||
|
||||
result = charts.GetExposure()
|
||||
result = charts.GetExposure(time, long_securities = long_securities, long_data=long, short_securities=[], short_data=[list(np.zeros(len(long[0])))])
|
||||
result = charts.GetExposure(time, long_securities=[], long_data=[list(np.zeros(len(short[0])))], short_securities = short_securities, short_data=short)
|
||||
result = charts.GetExposure(time, long_securities, short_securities, long, short)
|
||||
result = charts.GetExposure(time, long_securities, short_securities, long, short,
|
||||
live_time, live_long_securities, live_short_securities,
|
||||
live_long, live_short)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
using System.Linq;
|
||||
using Deedle;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class AnnualReturnsReportElement : ChartReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new plot of annual returns
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public AnnualReturnsReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the annual returns plot using the python libraries.
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var backtestReturns = ResultsUtil.EquityPoints(_backtest);
|
||||
var liveReturns = ResultsUtil.EquityPoints(_live);
|
||||
|
||||
var backtestTime = backtestReturns.Keys.ToList();
|
||||
var backtestStrategy = backtestReturns.Values.ToList();
|
||||
|
||||
var liveTime = liveReturns.Keys.ToList();
|
||||
var liveStrategy = liveReturns.Values.ToList();
|
||||
|
||||
var base64 = "";
|
||||
using (Py.GIL())
|
||||
{
|
||||
var backtestList = new PyList();
|
||||
var liveList = new PyList();
|
||||
|
||||
// We need to set the datetime index first before we resample
|
||||
//var backtestSeries = Pandas.Series(backtestStrategy.ToPython());
|
||||
var backtestSeries = new Series<DateTime, double>(backtestTime, backtestStrategy);
|
||||
|
||||
// Get the annual returns for the strategy
|
||||
// ResampleEquivalence works similarly to Pandas' DataFrame.resample(...) method
|
||||
// Here we transform the series to resample to the year's start, then we get the aggregate return from the year.
|
||||
// Pandas equivalent:
|
||||
//
|
||||
// df.pct_change().resample('AS').sum().mul(100)
|
||||
var backtestAnnualReturns = backtestSeries.ResampleEquivalence(date => new DateTime(date.Year, 1, 1), agg => agg.TotalReturns() * 100).DropMissing();
|
||||
|
||||
// We need to set the datetime index first before we resample
|
||||
var liveSeries = new Series<DateTime, double>(liveTime, liveStrategy);
|
||||
|
||||
// Get the annual returns for the live strategy.
|
||||
// Same as above, this is equivalent to:
|
||||
//
|
||||
// df.pct_change().resample('AS').sum().mul(100)
|
||||
var liveAnnualReturns = liveSeries.ResampleEquivalence(date => new DateTime(date.Year, 1, 1), agg => agg.TotalReturns() * 100).DropMissing();
|
||||
|
||||
// Select only the year number and pass it to the plotting library
|
||||
backtestList.Append(backtestAnnualReturns.Keys.Select(x => x.Year).ToList().ToPython());
|
||||
backtestList.Append(backtestAnnualReturns.Values.ToList().ToPython());
|
||||
liveList.Append(liveAnnualReturns.Keys.Select(x => x.Year).ToList().ToPython());
|
||||
liveList.Append(liveAnnualReturns.Values.ToList().ToPython());
|
||||
|
||||
base64 = Charting.GetAnnualReturns(backtestList, liveList);
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* 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 Deedle;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Orders;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class AssetAllocationReportElement : ChartReportElement
|
||||
{
|
||||
private BacktestResult _backtest;
|
||||
private List<PointInTimePortfolio> _backtestPortfolios;
|
||||
private LiveResult _live;
|
||||
private List<PointInTimePortfolio> _livePortfolios;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new plot of the asset allocation over time
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
/// <param name="backtestPortfolios">Backtest point in time portfolios</param>
|
||||
/// <param name="livePortfolios">Live point in time portfolios</param>
|
||||
public AssetAllocationReportElement(
|
||||
string name,
|
||||
string key,
|
||||
BacktestResult backtest,
|
||||
LiveResult live,
|
||||
List<PointInTimePortfolio> backtestPortfolios,
|
||||
List<PointInTimePortfolio> livePortfolios)
|
||||
{
|
||||
_backtest = backtest;
|
||||
_backtestPortfolios = backtestPortfolios;
|
||||
_live = live;
|
||||
_livePortfolios = livePortfolios;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the asset allocation pie chart using the python libraries.
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var backtestSeries = Metrics.AssetAllocations(_backtestPortfolios);
|
||||
var liveSeries = Metrics.AssetAllocations(_livePortfolios);
|
||||
|
||||
PyObject result;
|
||||
|
||||
using (Py.GIL())
|
||||
{
|
||||
var data = new PyList();
|
||||
var liveData = new PyList();
|
||||
|
||||
data.Append(backtestSeries.SortBy(x => -x).Where(x => x.Value != 0).Keys.Select(x => x.Value).ToList().ToPython());
|
||||
data.Append(backtestSeries.SortBy(x => -x).Where(x => x.Value != 0).Values.ToList().ToPython());
|
||||
|
||||
liveData.Append(liveSeries.SortBy(x => -x).Where(x => x.Value != 0).Keys.Select(x => x.Value).ToList().ToPython());
|
||||
liveData.Append(liveSeries.SortBy(x => -x).Where(x => x.Value != 0).Values.ToList().ToPython());
|
||||
|
||||
result = Charting.GetAssetAllocation(data, liveData);
|
||||
}
|
||||
|
||||
var base64 = result.ConvertToDictionary<string, string>();
|
||||
if (base64.ContainsKey("Live Asset Allocation"))
|
||||
{
|
||||
return base64["Live Asset Allocation"];
|
||||
}
|
||||
|
||||
return base64["Backtest Asset Allocation"];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 Deedle;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class CAGRReportElement : ReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the CAGR of the strategy.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public CAGRReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The generated output string to be injected
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var equityCurve = _live == null
|
||||
? new Series<DateTime, double>(ResultsUtil.EquityPoints(_backtest))
|
||||
: DrawdownCollection.NormalizeResults(_backtest, _live);
|
||||
|
||||
if (equityCurve.IsEmpty)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
var years = (decimal)(equityCurve.LastKey() - equityCurve.FirstKey()).TotalDays / 365m;
|
||||
|
||||
Result = Statistics.Statistics.CompoundingAnnualPerformance(
|
||||
equityCurve.FirstValue().SafeDecimalCast(),
|
||||
equityCurve.LastValue().SafeDecimalCast(),
|
||||
years);
|
||||
|
||||
return ((decimal?)Result)?.ToString("P1") ?? "-";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 Python.Runtime;
|
||||
using QuantConnect.Python;
|
||||
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal abstract class ChartReportElement : ReportElement
|
||||
{
|
||||
internal static dynamic Charting;
|
||||
|
||||
/// <summary>
|
||||
/// Charting base class report element
|
||||
/// </summary>
|
||||
protected ChartReportElement()
|
||||
{
|
||||
PythonInitializer.Initialize();
|
||||
|
||||
using (Py.GIL())
|
||||
{
|
||||
dynamic module = Py.Import("ReportCharts");
|
||||
var classObj = module.ReportCharts;
|
||||
|
||||
Charting = classObj.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* 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 System.Linq;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Packets;
|
||||
using System;
|
||||
using QuantConnect.Util;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class CumulativeReturnsReportElement : ChartReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new array of cumulative percentage return of strategy and benchmark
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public CumulativeReturnsReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the cumulative return of the backtest, benchmark, and live
|
||||
/// strategy using the ReportCharts.py python library
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var backtestReturns = ResultsUtil.EquityPoints(_backtest);
|
||||
var benchmark = ResultsUtil.BenchmarkPoints(_backtest);
|
||||
var liveReturns = ResultsUtil.EquityPoints(_live);
|
||||
var liveBenchmark = ResultsUtil.BenchmarkPoints(_live);
|
||||
|
||||
var backtestTime = backtestReturns.Keys.ToList();
|
||||
var backtestStrategy = backtestReturns.Values.ToList();
|
||||
var benchmarkTime = benchmark.Keys.ToList();
|
||||
var benchmarkPoints = benchmark.Values.ToList();
|
||||
|
||||
var liveTime = liveReturns.Keys.ToList();
|
||||
var liveStrategy = liveReturns.Values.ToList();
|
||||
var liveBenchmarkTime = liveBenchmark.Keys.ToList();
|
||||
var liveBenchmarkStrategy = liveBenchmark.Values.ToList();
|
||||
|
||||
var base64 = "";
|
||||
using (Py.GIL())
|
||||
{
|
||||
var backtestList = new PyList();
|
||||
var liveList = new PyList();
|
||||
|
||||
var backtestSeries = new Series<DateTime, double>(backtestTime, backtestStrategy);
|
||||
var liveSeries = new Series<DateTime, double>(liveTime, liveStrategy);
|
||||
var backtestBenchmarkSeries = new Series<DateTime, double>(benchmarkTime, benchmarkPoints);
|
||||
var liveBenchmarkSeries = new Series<DateTime, double>(liveBenchmarkTime, liveBenchmarkStrategy);
|
||||
|
||||
// Equivalent in python using pandas for the following operations is:
|
||||
// --------------------------------------------------
|
||||
// >>> # note: [...] denotes the data we're passing in
|
||||
// >>> df = pd.Series([...], index=time)
|
||||
// >>> df_live = pd.Series([...], index=live_time)
|
||||
// >>> df_live = df_live.mul(df.iloc[-1] / df_live.iloc[0]).fillna(method='ffill').dropna()
|
||||
// >>> df_final = pd.concat([df, df_live], axis=0)
|
||||
// >>> df_cumulative_returns = ((df_final.pct_change().dropna() + 1).cumprod() - 1)
|
||||
// --------------------------------------------------
|
||||
//
|
||||
// We multiply the final value of the backtest and benchmark to have a continuous graph showing the performance out of sample
|
||||
// as a continuation of the cumulative returns graph. Otherwise, we start plotting from 0% and not the last value of the backtest data
|
||||
|
||||
var backtestLastValue = backtestSeries.ValueCount == 0 ? 0 : backtestSeries.LastValue();
|
||||
var backtestBenchmarkLastValue = backtestBenchmarkSeries.ValueCount == 0 ? 0 : backtestBenchmarkSeries.LastValue();
|
||||
|
||||
var liveContinuousEquity = liveSeries;
|
||||
var liveBenchContinuousEquity = liveBenchmarkSeries;
|
||||
|
||||
if (liveSeries.ValueCount != 0)
|
||||
{
|
||||
liveContinuousEquity = (liveSeries * (backtestLastValue / liveSeries.FirstValue()))
|
||||
.FillMissing(Direction.Forward)
|
||||
.DropMissing();
|
||||
}
|
||||
if (liveBenchmarkSeries.ValueCount != 0)
|
||||
{
|
||||
liveBenchContinuousEquity = (liveBenchmarkSeries * (backtestBenchmarkLastValue / liveBenchmarkSeries.FirstValue()))
|
||||
.FillMissing(Direction.Forward)
|
||||
.DropMissing();
|
||||
}
|
||||
|
||||
var liveStart = liveContinuousEquity.ValueCount == 0 ? DateTime.MaxValue : liveContinuousEquity.DropMissing().FirstKey();
|
||||
var liveBenchStart = liveBenchContinuousEquity.ValueCount == 0 ? DateTime.MaxValue : liveBenchContinuousEquity.DropMissing().FirstKey();
|
||||
|
||||
var finalEquity = backtestSeries.Where(kvp => kvp.Key < liveStart).Observations.ToList();
|
||||
var finalBenchEquity = backtestBenchmarkSeries.Where(kvp => kvp.Key < liveBenchStart).Observations.ToList();
|
||||
|
||||
finalEquity.AddRange(liveContinuousEquity.Observations);
|
||||
finalBenchEquity.AddRange(liveBenchContinuousEquity.Observations);
|
||||
|
||||
var finalSeries = (new Series<DateTime, double>(finalEquity).CumulativeReturns() * 100)
|
||||
.FillMissing(Direction.Forward)
|
||||
.DropMissing();
|
||||
|
||||
var finalBenchSeries = (new Series<DateTime, double>(finalBenchEquity).CumulativeReturns() * 100)
|
||||
.FillMissing(Direction.Forward)
|
||||
.DropMissing();
|
||||
|
||||
var backtestCumulativePercent = finalSeries.Where(kvp => kvp.Key < liveStart);
|
||||
var backtestBenchmarkCumulativePercent = finalBenchSeries.Where(kvp => kvp.Key < liveBenchStart);
|
||||
|
||||
var liveCumulativePercent = finalSeries.Where(kvp => kvp.Key >= liveStart);
|
||||
var liveBenchmarkCumulativePercent = finalBenchSeries.Where(kvp => kvp.Key >= liveBenchStart);
|
||||
|
||||
backtestList.Append(backtestCumulativePercent.Keys.ToList().ToPython());
|
||||
backtestList.Append(backtestCumulativePercent.Values.ToList().ToPython());
|
||||
backtestList.Append(backtestBenchmarkCumulativePercent.Keys.ToList().ToPython());
|
||||
backtestList.Append(backtestBenchmarkCumulativePercent.Values.ToList().ToPython());
|
||||
|
||||
liveList.Append(liveCumulativePercent.Keys.ToList().ToPython());
|
||||
liveList.Append(liveCumulativePercent.Values.ToList().ToPython());
|
||||
liveList.Append(liveBenchmarkCumulativePercent.Keys.ToList().ToPython());
|
||||
liveList.Append(liveBenchmarkCumulativePercent.Values.ToList().ToPython());
|
||||
|
||||
base64 = Charting.GetCumulativeReturns(backtestList, liveList);
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 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.Linq;
|
||||
using Deedle;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class DailyReturnsReportElement : ChartReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new plot of the daily returns in bar chart format
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public DailyReturnsReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the daily returns plot using the python libraries.
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var backtestReturns = ResultsUtil.EquityPoints(_backtest);
|
||||
var liveReturns = ResultsUtil.EquityPoints(_live);
|
||||
|
||||
var backtestSeries = new Series<DateTime, double>(backtestReturns.Keys, backtestReturns.Values);
|
||||
var liveSeries = new Series<DateTime, double>(liveReturns.Keys, liveReturns.Values);
|
||||
|
||||
// The following two operations are equivalent to the Pandas `DataFrame.resample(...)` method
|
||||
var backtestResampled = backtestSeries.ResampleEquivalence(date => date.Date, s => s.LastValue()).PercentChange().DropMissing() * 100;
|
||||
var liveResampled = liveSeries.ResampleEquivalence(date => date.Date, s => s.LastValue()).PercentChange().DropMissing() * 100;
|
||||
|
||||
var base64 = "";
|
||||
using (Py.GIL())
|
||||
{
|
||||
var backtestList = new PyList();
|
||||
backtestList.Append(backtestResampled.Keys.ToList().ToPython());
|
||||
backtestList.Append(backtestResampled.Values.ToList().ToPython());
|
||||
|
||||
var liveList = new PyList();
|
||||
liveList.Append(liveResampled.Keys.ToList().ToPython());
|
||||
liveList.Append(liveResampled.Values.ToList().ToPython());
|
||||
|
||||
base64 = Charting.GetDailyReturns(backtestList, liveList);
|
||||
|
||||
backtestList.Dispose();
|
||||
liveList.Dispose();
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* 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 Deedle;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Packets;
|
||||
using QuantConnect.Util;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class DrawdownReportElement : ChartReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new plot of the top N worst drawdown durations
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public DrawdownReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the top N drawdown plot using the python libraries.
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var backtestPoints = ResultsUtil.EquityPoints(_backtest);
|
||||
var livePoints = ResultsUtil.EquityPoints(_live);
|
||||
|
||||
var liveSeries = new Series<DateTime, double>(livePoints.Keys, livePoints.Values);
|
||||
var strategySeries = DrawdownCollection.NormalizeResults(_backtest, _live);
|
||||
|
||||
var seriesUnderwaterPlot = DrawdownCollection.GetUnderwater(strategySeries).DropMissing();
|
||||
var liveUnderwaterPlot = backtestPoints.Count == 0 ? seriesUnderwaterPlot : seriesUnderwaterPlot.After(backtestPoints.Last().Key);
|
||||
var drawdownCollection = DrawdownCollection.FromResult(_backtest, _live, periods: 5);
|
||||
|
||||
var base64 = "";
|
||||
using (Py.GIL())
|
||||
{
|
||||
var backtestList = new PyList();
|
||||
|
||||
if (liveUnderwaterPlot.IsEmpty)
|
||||
{
|
||||
backtestList.Append(seriesUnderwaterPlot.Keys.ToList().ToPython());
|
||||
backtestList.Append(seriesUnderwaterPlot.Values.ToList().ToPython());
|
||||
}
|
||||
else
|
||||
{
|
||||
backtestList.Append(seriesUnderwaterPlot.Before(liveUnderwaterPlot.FirstKey()).Keys.ToList().ToPython());
|
||||
backtestList.Append(seriesUnderwaterPlot.Before(liveUnderwaterPlot.FirstKey()).Values.ToList().ToPython());
|
||||
}
|
||||
|
||||
var liveList = new PyList();
|
||||
liveList.Append(liveUnderwaterPlot.Keys.ToList().ToPython());
|
||||
liveList.Append(liveUnderwaterPlot.Values.ToList().ToPython());
|
||||
|
||||
var worstList = new PyList();
|
||||
var previousDrawdownPeriods = new List<KeyValuePair<DateTime, DateTime>>();
|
||||
|
||||
foreach (var group in drawdownCollection.Drawdowns)
|
||||
{
|
||||
// Skip drawdown periods that are overlapping
|
||||
if (previousDrawdownPeriods.Where(kvp => (group.Start >= kvp.Key && group.Start <= kvp.Value) || (group.End >= kvp.Key && group.End <= kvp.Value)).Any())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var worst = new PyDict();
|
||||
worst.SetItem("Begin", group.Start.ToPython());
|
||||
worst.SetItem("End", group.End.ToPython());
|
||||
worst.SetItem("Total", group.PeakToTrough.ToPython());
|
||||
|
||||
worstList.Append(worst);
|
||||
previousDrawdownPeriods.Add(new KeyValuePair<DateTime, DateTime>(group.Start, group.End));
|
||||
}
|
||||
|
||||
base64 = Charting.GetDrawdown(backtestList, liveList, worstList);
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.Globalization;
|
||||
using System.Linq;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
/// <summary>
|
||||
/// Capacity Estimation Report Element
|
||||
/// </summary>
|
||||
public sealed class EstimatedCapacityReportElement : ReportElement
|
||||
{
|
||||
private readonly BacktestResult _backtest;
|
||||
private readonly LiveResult _live;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new capacity estimate
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public EstimatedCapacityReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render element
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var statistics = _backtest?.Statistics;
|
||||
string capacityWithCurrency;
|
||||
if (statistics == null || !statistics.TryGetValue("Estimated Strategy Capacity", out capacityWithCurrency))
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
var capacity = Currencies.Parse(capacityWithCurrency).RoundToSignificantDigits(2);
|
||||
|
||||
Result = capacity;
|
||||
|
||||
if (capacity == 0m)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
return capacity.ToFinancialFigures();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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 Deedle;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Orders;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class ExposureReportElement : ChartReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
private List<PointInTimePortfolio> _backtestPortfolios;
|
||||
private List<PointInTimePortfolio> _livePortfolios;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new plot of the exposure
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
/// <param name="backtestPortfolios">Backtest point in time portfolios</param>
|
||||
/// <param name="livePortfolios">Live point in time portfolios</param>
|
||||
public ExposureReportElement(
|
||||
string name,
|
||||
string key,
|
||||
BacktestResult backtest,
|
||||
LiveResult live,
|
||||
List<PointInTimePortfolio> backtestPortfolios,
|
||||
List<PointInTimePortfolio> livePortfolios)
|
||||
{
|
||||
_backtest = backtest;
|
||||
_backtestPortfolios = backtestPortfolios;
|
||||
_live = live;
|
||||
_livePortfolios = livePortfolios;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the exposure plot using the python libraries.
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var longBacktestFrame = Metrics.Exposure(_backtestPortfolios, OrderDirection.Buy);
|
||||
var shortBacktestFrame = Metrics.Exposure(_backtestPortfolios, OrderDirection.Sell);
|
||||
var longLiveFrame = Metrics.Exposure(_livePortfolios, OrderDirection.Buy);
|
||||
var shortLiveFrame = Metrics.Exposure(_livePortfolios, OrderDirection.Sell);
|
||||
|
||||
var backtestFrame = longBacktestFrame.Join(shortBacktestFrame)
|
||||
.FillMissing(Direction.Forward)
|
||||
.FillMissing(0.0);
|
||||
|
||||
var liveFrame = longLiveFrame.Join(shortLiveFrame)
|
||||
.FillMissing(Direction.Forward)
|
||||
.FillMissing(0.0);
|
||||
|
||||
longBacktestFrame = Frame.CreateEmpty<DateTime, Tuple<SecurityType, OrderDirection>>();
|
||||
shortBacktestFrame = Frame.CreateEmpty<DateTime, Tuple<SecurityType, OrderDirection>>();
|
||||
longLiveFrame = Frame.CreateEmpty<DateTime, Tuple<SecurityType, OrderDirection>>();
|
||||
shortLiveFrame = Frame.CreateEmpty<DateTime, Tuple<SecurityType, OrderDirection>>();
|
||||
|
||||
foreach (var key in backtestFrame.ColumnKeys)
|
||||
{
|
||||
longBacktestFrame[key] = backtestFrame[key].SelectValues(x => x < 0 ? 0 : x);
|
||||
shortBacktestFrame[key] = backtestFrame[key].SelectValues(x => x > 0 ? 0 : x);
|
||||
}
|
||||
|
||||
foreach (var key in liveFrame.ColumnKeys)
|
||||
{
|
||||
longLiveFrame[key] = liveFrame[key].SelectValues(x => x < 0 ? 0 : x);
|
||||
shortLiveFrame[key] = liveFrame[key].SelectValues(x => x > 0 ? 0 : x);
|
||||
}
|
||||
|
||||
longBacktestFrame = longBacktestFrame.DropSparseColumnsAll();
|
||||
shortBacktestFrame = shortBacktestFrame.DropSparseColumnsAll();
|
||||
longLiveFrame = longLiveFrame.DropSparseColumnsAll();
|
||||
shortLiveFrame = shortLiveFrame.DropSparseColumnsAll();
|
||||
|
||||
var base64 = "";
|
||||
using (Py.GIL())
|
||||
{
|
||||
var time = backtestFrame.RowKeys.ToList().ToPython();
|
||||
var longSecurities = longBacktestFrame.ColumnKeys.Select(x => x.Item1.ToStringInvariant()).ToList().ToPython();
|
||||
var shortSecurities = shortBacktestFrame.ColumnKeys.Select(x => x.Item1.ToStringInvariant()).ToList().ToPython();
|
||||
var longData = longBacktestFrame.ColumnKeys.Select(x => longBacktestFrame[x].Values.ToList().ToPython()).ToPython();
|
||||
var shortData = shortBacktestFrame.ColumnKeys.Select(x => shortBacktestFrame[x].Values.ToList().ToPython()).ToPython();
|
||||
var liveTime = liveFrame.RowKeys.ToList().ToPython();
|
||||
var liveLongSecurities = longLiveFrame.ColumnKeys.Select(x => x.Item1.ToStringInvariant()).ToList().ToPython();
|
||||
var liveShortSecurities = shortLiveFrame.ColumnKeys.Select(x => x.Item1.ToStringInvariant()).ToList().ToPython();
|
||||
var liveLongData = longLiveFrame.ColumnKeys.Select(x => longLiveFrame[x].Values.ToList().ToPython()).ToPython();
|
||||
var liveShortData = shortLiveFrame.ColumnKeys.Select(x => shortLiveFrame[x].Values.ToList().ToPython()).ToPython();
|
||||
|
||||
base64 = Charting.GetExposure(
|
||||
time,
|
||||
longSecurities,
|
||||
shortSecurities,
|
||||
longData,
|
||||
shortData,
|
||||
liveTime,
|
||||
liveLongSecurities,
|
||||
liveShortSecurities,
|
||||
liveLongData,
|
||||
liveShortData
|
||||
);
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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.Report.ReportElements
|
||||
{
|
||||
/// <summary>
|
||||
/// Common interface for template elements of the report
|
||||
/// </summary>
|
||||
internal interface IReportElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of this report element
|
||||
/// </summary>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Template key code.
|
||||
/// </summary>
|
||||
string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The generated output string to be injected
|
||||
/// </summary>
|
||||
string Render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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 QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class InformationRatioReportElement : ReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the information ratio of the strategy.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public InformationRatioReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The generated output string to be injected
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var informationRatio = _backtest?.TotalPerformance?.PortfolioStatistics?.InformationRatio;
|
||||
Result = informationRatio;
|
||||
return informationRatio?.ToString("F1") ?? "-";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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 Deedle;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Orders;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class LeverageUtilizationReportElement : ChartReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
private List<PointInTimePortfolio> _backtestPortfolios;
|
||||
private List<PointInTimePortfolio> _livePortfolios;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new plot of the leverage utilization
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
/// <param name="backtestPortfolios">Backtest point in time portfolios</param>
|
||||
/// <param name="livePortfolios">Live point in time portfolios</param>
|
||||
public LeverageUtilizationReportElement(
|
||||
string name,
|
||||
string key,
|
||||
BacktestResult backtest,
|
||||
LiveResult live,
|
||||
List<PointInTimePortfolio> backtestPortfolios,
|
||||
List<PointInTimePortfolio> livePortfolios)
|
||||
{
|
||||
_backtest = backtest;
|
||||
_backtestPortfolios = backtestPortfolios;
|
||||
_live = live;
|
||||
_livePortfolios = livePortfolios;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the leverage utilization plot using the python libraries.
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var backtestSeries = Metrics.LeverageUtilization(_backtestPortfolios).FillMissing(Direction.Forward);
|
||||
var liveSeries = Metrics.LeverageUtilization(_livePortfolios).FillMissing(Direction.Forward);
|
||||
|
||||
var base64 = "";
|
||||
using (Py.GIL())
|
||||
{
|
||||
var backtestList = new PyList();
|
||||
var liveList = new PyList();
|
||||
|
||||
backtestList.Append(backtestSeries.Keys.ToList().ToPython());
|
||||
backtestList.Append(backtestSeries.Values.ToList().ToPython());
|
||||
|
||||
liveList.Append(liveSeries.Keys.ToList().ToPython());
|
||||
liveList.Append(liveSeries.Values.ToList().ToPython());
|
||||
|
||||
base64 = Charting.GetLeverage(backtestList, liveList);
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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.Linq;
|
||||
using QuantConnect.Orders;
|
||||
using QuantConnect.Packets;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class MarketsReportElement : ReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Get the markets of the strategy.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public MarketsReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The generated output string to be injected
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var liveOrders = _live?.Orders?.Values.ToList();
|
||||
if (liveOrders == null)
|
||||
{
|
||||
liveOrders = new List<Order>();
|
||||
}
|
||||
|
||||
var orders = new List<Order>();
|
||||
var backtestOrders = _backtest?.Orders?.Values;
|
||||
if (backtestOrders != null)
|
||||
{
|
||||
orders = backtestOrders.ToList();
|
||||
}
|
||||
|
||||
orders = orders.Union(liveOrders).ToList();
|
||||
|
||||
var securityTypes = orders.DistinctBy(o => o.SecurityType).Select(s => s.SecurityType.ToString()).ToList();
|
||||
Result = securityTypes;
|
||||
|
||||
return string.Join(",", securityTypes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class MaxDrawdownRecoveryReportElement : ReportElement
|
||||
{
|
||||
private LiveResult _liveResult;
|
||||
private BacktestResult _backtestResult;
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the max drawdown of the strategy.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtestResult">Backtest result object</param>
|
||||
/// <param name="liveResult">Live result object</param>
|
||||
public MaxDrawdownRecoveryReportElement(string name, string key, BacktestResult backtestResult, LiveResult liveResult)
|
||||
{
|
||||
_liveResult = liveResult;
|
||||
_backtestResult = backtestResult;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The generated output string to be injected
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
if (_liveResult == null)
|
||||
{
|
||||
var backtestDrawdownRecovery = _backtestResult?.TotalPerformance?.PortfolioStatistics?.DrawdownRecovery;
|
||||
Result = backtestDrawdownRecovery;
|
||||
return backtestDrawdownRecovery?.ToStringInvariant() ?? "-";
|
||||
}
|
||||
var equityCurve = new SortedDictionary<DateTime, decimal>(DrawdownCollection.NormalizeResults(_backtestResult, _liveResult)
|
||||
.Observations
|
||||
.ToDictionary(kvp => kvp.Key, kvp => (decimal)kvp.Value));
|
||||
|
||||
var maxDrawdownRecovery = Statistics.Statistics.CalculateDrawdownMetrics(equityCurve).DrawdownRecovery;
|
||||
Result = maxDrawdownRecovery;
|
||||
|
||||
return $"{maxDrawdownRecovery}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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 Deedle;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class MaxDrawdownReportElement : ReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the max drawdown of the strategy.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public MaxDrawdownReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The generated output string to be injected
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
if (_live == null)
|
||||
{
|
||||
var backtestDrawdown = _backtest?.TotalPerformance?.PortfolioStatistics?.Drawdown;
|
||||
Result = backtestDrawdown;
|
||||
return backtestDrawdown?.ToString("P1") ?? "-";
|
||||
}
|
||||
|
||||
var equityCurve = new SortedDictionary<DateTime, decimal>(DrawdownCollection.NormalizeResults(_backtest, _live)
|
||||
.Observations
|
||||
.ToDictionary(kvp => kvp.Key, kvp => (decimal)kvp.Value));
|
||||
|
||||
var maxDrawdown = Statistics.Statistics.CalculateDrawdownMetrics(equityCurve).Drawdown;
|
||||
Result = maxDrawdown;
|
||||
|
||||
return $"{maxDrawdown:P1}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 Deedle;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class MonthlyReturnsReportElement : ChartReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Create a monthly returns plot
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public MonthlyReturnsReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the monthly returns plot using the python libraries.
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var backtestPoints = ResultsUtil.EquityPoints(_backtest);
|
||||
var livePoints = ResultsUtil.EquityPoints(_live);
|
||||
|
||||
var backtestSeries = new Series<DateTime, double>(backtestPoints.Keys, backtestPoints.Values);
|
||||
var liveSeries = new Series<DateTime, double>(livePoints.Keys, livePoints.Values);
|
||||
|
||||
// Equivalent to python pandas line: `backtestSeries.resample('M').apply(lambda x: x.pct_change().sum())`
|
||||
var backtestMonthlyReturns = backtestSeries.ResampleEquivalence(date => new DateTime(date.Year, date.Month, 1).AddMonths(1).AddDays(-1))
|
||||
.Select(kvp => kvp.Value.TotalReturns());
|
||||
|
||||
var liveMonthlyReturns = liveSeries.ResampleEquivalence(date => new DateTime(date.Year, date.Month, 1).AddMonths(1).AddDays(-1))
|
||||
.Select(kvp => kvp.Value.TotalReturns());
|
||||
|
||||
var base64 = "";
|
||||
using (Py.GIL())
|
||||
{
|
||||
var backtestResults = new PyDict();
|
||||
foreach (var kvp in backtestMonthlyReturns.GroupBy(kvp => kvp.Key.Year).GetObservations())
|
||||
{
|
||||
var key = kvp.Key.ToStringInvariant();
|
||||
var monthlyReturns = kvp.Value * 100;
|
||||
|
||||
var values = new List<double>();
|
||||
for (var i = 1; i <= 12; i++)
|
||||
{
|
||||
var returns = monthlyReturns.Where(row => row.Key.Month == i);
|
||||
if (!returns.IsEmpty)
|
||||
{
|
||||
values.Add(returns.FirstValue());
|
||||
continue;
|
||||
}
|
||||
|
||||
values.Add(double.NaN);
|
||||
}
|
||||
|
||||
backtestResults.SetItem(key.ToPython(), values.ToPython());
|
||||
}
|
||||
|
||||
var liveResults = new PyDict();
|
||||
foreach (var kvp in liveMonthlyReturns.GroupBy(kvp => kvp.Key.Year).GetObservations())
|
||||
{
|
||||
var key = kvp.Key.ToStringInvariant();
|
||||
var monthlyReturns = kvp.Value * 100;
|
||||
|
||||
var values = new List<double>();
|
||||
for (var i = 1; i <= 12; i++)
|
||||
{
|
||||
var returns = monthlyReturns.Where(row => row.Key.Month == i);
|
||||
if (!returns.IsEmpty)
|
||||
{
|
||||
values.Add(returns.FirstValue());
|
||||
continue;
|
||||
}
|
||||
|
||||
values.Add(double.NaN);
|
||||
}
|
||||
|
||||
liveResults.SetItem(key.ToPython(), values.ToPython());
|
||||
}
|
||||
|
||||
base64 = Charting.GetMonthlyReturns(backtestResults, liveResults);
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 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.Linq;
|
||||
using Deedle;
|
||||
using QuantConnect.Data;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class PSRReportElement : ReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// The number of trading days per year to get better result of statistics
|
||||
/// </summary>
|
||||
private int _tradingDaysPerYear;
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the PSR of the strategy.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
/// <param name="tradingDaysPerYear">The number of trading days per year to get better result of statistics</param>
|
||||
public PSRReportElement(string name, string key, BacktestResult backtest, LiveResult live, int tradingDaysPerYear)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
_tradingDaysPerYear = tradingDaysPerYear;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The generated output string to be injected
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
decimal? psr;
|
||||
if (_live == null)
|
||||
{
|
||||
psr = _backtest?.TotalPerformance?.PortfolioStatistics?.ProbabilisticSharpeRatio;
|
||||
Result = psr;
|
||||
if (psr == null)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
return $"{psr:P0}";
|
||||
}
|
||||
|
||||
var equityCurvePerformance = DrawdownCollection.NormalizeResults(_backtest, _live)
|
||||
.ResampleEquivalence(date => date.Date, s => s.LastValue())
|
||||
.PercentChange();
|
||||
|
||||
if (equityCurvePerformance.IsEmpty || equityCurvePerformance.KeyCount < 180)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
var sixMonthsBefore = equityCurvePerformance.LastKey() - TimeSpan.FromDays(180);
|
||||
// Skip weekends so we stay on a trading-day basis. The risk-free rate below is deannualized by
|
||||
// tradingDaysPerYear, so leaving calendar days in would deduct it over ~365 days a year and over-deduct the rate
|
||||
var lastSixMonthsPerformance = equityCurvePerformance.Where(kvp => kvp.Key >= sixMonthsBefore
|
||||
&& kvp.Key.DayOfWeek != DayOfWeek.Saturday
|
||||
&& kvp.Key.DayOfWeek != DayOfWeek.Sunday);
|
||||
|
||||
var benchmarkSharpeRatio = 1.0d / Math.Sqrt(_tradingDaysPerYear);
|
||||
// Use the same excess-return basis as the reported PSR by subtracting the deannualized risk-free rate
|
||||
var riskFreeRate = new InterestRateProvider().GetAverageRiskFreeRate(lastSixMonthsPerformance.Keys);
|
||||
psr = Statistics.Statistics.ProbabilisticSharpeRatio(
|
||||
lastSixMonthsPerformance
|
||||
.Values
|
||||
.ToList(),
|
||||
benchmarkSharpeRatio,
|
||||
(double)riskFreeRate / _tradingDaysPerYear)
|
||||
.SafeDecimalCast();
|
||||
|
||||
Result = psr;
|
||||
return $"{psr:P0}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for creating a two column table for the Algorithm's Parameters in a report
|
||||
/// </summary>
|
||||
public class ParametersReportElement : ReportElement
|
||||
{
|
||||
private IReadOnlyDictionary<string, string> _parameters;
|
||||
private readonly string _template;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a two column table for the Algorithm's Parameters
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtestConfiguration">The configuration of the backtest algorithm</param>
|
||||
/// <param name="liveConfiguration">The configuration of the live algorithm</param>
|
||||
/// <param name="template">HTML template to use</param>
|
||||
public ParametersReportElement(string name, string key, AlgorithmConfiguration backtestConfiguration, AlgorithmConfiguration liveConfiguration, string template)
|
||||
{
|
||||
Name = name;
|
||||
Key = key;
|
||||
_template = template;
|
||||
|
||||
if (liveConfiguration != null)
|
||||
{
|
||||
_parameters = liveConfiguration.Parameters;
|
||||
}
|
||||
else if (backtestConfiguration != null)
|
||||
{
|
||||
_parameters = backtestConfiguration.Parameters;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This case only happens for the unit tests, then we just create an empty dictionary
|
||||
_parameters = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a HTML two column table for the Algorithm's Parameters
|
||||
/// </summary>
|
||||
/// <returns>Returns a string representing a HTML two column table</returns>
|
||||
public override string Render()
|
||||
{
|
||||
var items = new List<string>();
|
||||
int parameterIndex;
|
||||
var columns = (new Regex(@"{{\$KEY(\d+?)}}")).Matches(_template).Count;
|
||||
|
||||
for (parameterIndex = 0; parameterIndex < _parameters.Count;)
|
||||
{
|
||||
var template = _template;
|
||||
int column;
|
||||
for (column = 0; column < columns; column++)
|
||||
{
|
||||
var currTemplateKey = "{{$KEY" + column.ToString() + "}}";
|
||||
var currTemplateValue = "{{$VALUE" + column.ToString() + "}}";
|
||||
|
||||
if (parameterIndex < _parameters.Count)
|
||||
{
|
||||
var parameter = _parameters.ElementAt(parameterIndex);
|
||||
template = template.Replace(currTemplateKey, parameter.Key);
|
||||
template = template.Replace(currTemplateValue, parameter.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
template = template.Replace(currTemplateKey, string.Empty);
|
||||
template = template.Replace(currTemplateValue, string.Empty);
|
||||
}
|
||||
|
||||
parameterIndex++;
|
||||
}
|
||||
|
||||
if (column == 0)
|
||||
{
|
||||
parameterIndex++;
|
||||
}
|
||||
|
||||
items.Add(template);
|
||||
}
|
||||
|
||||
if (Key == ReportKey.ParametersPageStyle)
|
||||
{
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return "display: none;";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var parameters= string.Join("\n", items);
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.Report.ReportElements
|
||||
{
|
||||
/// <summary>
|
||||
/// Common interface for template elements of the report
|
||||
/// </summary>
|
||||
public abstract class ReportElement : IReportElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Name of this report element
|
||||
/// </summary>
|
||||
public virtual string Name { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Template key code.
|
||||
/// </summary>
|
||||
public virtual string Key { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes the key into a JSON-friendly key
|
||||
/// </summary>
|
||||
public string JsonKey => Key.Replace("KPI-", "").Replace("$", "").Replace("{", "").Replace("}", "") .ToLowerInvariant();
|
||||
|
||||
/// <summary>
|
||||
/// Result of the render as an object for serialization to JSON
|
||||
/// </summary>
|
||||
public virtual object Result { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// The generated output string to be injected
|
||||
/// </summary>
|
||||
public abstract string Render();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* 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 Python.Runtime;
|
||||
using QuantConnect.Logging;
|
||||
using QuantConnect.Packets;
|
||||
using QuantConnect.Statistics;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class ReturnsPerTradeReportElement : ChartReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new distribution plot of returns per trade
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public ReturnsPerTradeReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the returns per trade plot using the python libraries.
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var backtestPercentagePerTrade = new List<double>();
|
||||
if (_backtest?.TotalPerformance?.ClosedTrades != null)
|
||||
{
|
||||
foreach (var trade in _backtest.TotalPerformance.ClosedTrades)
|
||||
{
|
||||
if (trade.EntryPrice == 0m)
|
||||
{
|
||||
Log.Error($"ReturnsPerTradeReportElement.Render(): Encountered entry price of 0 in trade with entry time: {trade.EntryTime:yyyy-MM-dd HH:mm:ss} - Exit time: {trade.ExitTime:yyyy-MM-dd HH::mm:ss}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var sideMultiplier = trade.Direction == TradeDirection.Long ? 1 : -1;
|
||||
backtestPercentagePerTrade.Add(sideMultiplier * (Convert.ToDouble(trade.ExitPrice) - Convert.ToDouble(trade.EntryPrice)) / Convert.ToDouble(trade.EntryPrice));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: LiveResult does not contain a TotalPerformance field, so skip live mode for now
|
||||
|
||||
var base64 = "";
|
||||
using (Py.GIL())
|
||||
{
|
||||
// Charting library does not expect values to be in whole percentage values (i.e. not 1% == 1.0, but rather 1% == 0.01),
|
||||
base64 = Charting.GetReturnsPerTrade(backtestPercentagePerTrade.ToPython());
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* 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.Linq;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Packets;
|
||||
using System.Collections.Generic;
|
||||
using QuantConnect.Lean.Engine.Results;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class RollingPortfolioBetaReportElement : ChartReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// The number of trading days per year to get better result of statistics
|
||||
/// </summary>
|
||||
private int _tradingDaysPerYear;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new plot of the rolling portfolio beta to equities
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
/// <param name="tradingDaysPerYear">The number of trading days per year to get better result of statistics</param>
|
||||
public RollingPortfolioBetaReportElement(string name, string key, BacktestResult backtest, LiveResult live, int tradingDaysPerYear)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
_tradingDaysPerYear = tradingDaysPerYear;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the rolling portfolio beta to equities plot using the python libraries.
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var backtestPoints = GetReturnSeries(_backtest);
|
||||
var backtestBenchmarkPoints = ResultsUtil.BenchmarkPoints(_backtest);
|
||||
var livePoints = GetReturnSeries(_live);
|
||||
var liveBenchmarkPoints = ResultsUtil.BenchmarkPoints(_live);
|
||||
|
||||
var base64 = "";
|
||||
using (Py.GIL())
|
||||
{
|
||||
var backtestList = new PyList();
|
||||
var liveList = new PyList();
|
||||
|
||||
var backtestRollingBetaSixMonths = Rolling.Beta(backtestPoints, backtestBenchmarkPoints, windowSize: 22 * 6);
|
||||
var backtestRollingBetaTwelveMonths = Rolling.Beta(backtestPoints, backtestBenchmarkPoints, windowSize: _tradingDaysPerYear);
|
||||
|
||||
backtestList.Append(backtestRollingBetaSixMonths.Keys.ToList().ToPython());
|
||||
backtestList.Append(backtestRollingBetaSixMonths.Values.ToList().ToPython());
|
||||
backtestList.Append(backtestRollingBetaTwelveMonths.Keys.ToList().ToPython());
|
||||
backtestList.Append(backtestRollingBetaTwelveMonths.Values.ToList().ToPython());
|
||||
|
||||
var liveRollingBetaSixMonths = Rolling.Beta(livePoints, liveBenchmarkPoints, windowSize: 22 * 6);
|
||||
var liveRollingBetaTwelveMonths = Rolling.Beta(livePoints, liveBenchmarkPoints, windowSize: _tradingDaysPerYear);
|
||||
|
||||
liveList.Append(liveRollingBetaSixMonths.Keys.ToList().ToPython());
|
||||
liveList.Append(liveRollingBetaSixMonths.Values.ToList().ToPython());
|
||||
liveList.Append(liveRollingBetaTwelveMonths.Keys.ToList().ToPython());
|
||||
liveList.Append(liveRollingBetaTwelveMonths.Values.ToList().ToPython());
|
||||
|
||||
base64 = Charting.GetRollingBeta(backtestList, liveList);
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
|
||||
private static SortedList<DateTime, double> GetReturnSeries(Result leanResult)
|
||||
{
|
||||
var returnSeries = ResultsUtil.EquityPoints(leanResult, BaseResultsHandler.ReturnKey);
|
||||
if (returnSeries == null || returnSeries.Count == 0)
|
||||
{
|
||||
// for backwards compatibility
|
||||
returnSeries = ResultsUtil.EquityPoints(leanResult, "Daily Performance");
|
||||
}
|
||||
return returnSeries;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* 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.Linq;
|
||||
using Deedle;
|
||||
using Python.Runtime;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class RollingSharpeReportElement : ChartReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// The number of trading days per year to get better result of statistics
|
||||
/// </summary>
|
||||
private int _tradingDaysPerYear;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new plot of the rolling sharpe ratio
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
/// <param name="tradingDaysPerYear">The number of trading days per year to get better result of statistics</param>
|
||||
public RollingSharpeReportElement(string name, string key, BacktestResult backtest, LiveResult live, int tradingDaysPerYear)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
_tradingDaysPerYear = tradingDaysPerYear;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the rolling sharpe using the python libraries.
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var backtestPoints = ResultsUtil.EquityPoints(_backtest);
|
||||
var livePoints = ResultsUtil.EquityPoints(_live);
|
||||
|
||||
var backtestSeries = new Series<DateTime, double>(backtestPoints);
|
||||
var liveSeries = new Series<DateTime, double>(livePoints);
|
||||
|
||||
var backtestRollingSharpeSixMonths = Rolling.Sharpe(backtestSeries, 6, _tradingDaysPerYear).DropMissing();
|
||||
var backtestRollingSharpeTwelveMonths = Rolling.Sharpe(backtestSeries, 12, _tradingDaysPerYear).DropMissing();
|
||||
var liveRollingSharpeSixMonths = Rolling.Sharpe(liveSeries, 6, _tradingDaysPerYear).DropMissing();
|
||||
var liveRollingSharpeTwelveMonths = Rolling.Sharpe(liveSeries, 12, _tradingDaysPerYear).DropMissing();
|
||||
|
||||
var base64 = "";
|
||||
using (Py.GIL())
|
||||
{
|
||||
var backtestList = new PyList();
|
||||
var liveList = new PyList();
|
||||
|
||||
backtestList.Append(backtestRollingSharpeSixMonths.Keys.ToList().ToPython());
|
||||
backtestList.Append(backtestRollingSharpeSixMonths.Values.ToList().ToPython());
|
||||
backtestList.Append(backtestRollingSharpeTwelveMonths.Keys.ToList().ToPython());
|
||||
backtestList.Append(backtestRollingSharpeTwelveMonths.Values.ToList().ToPython());
|
||||
|
||||
liveList.Append(liveRollingSharpeSixMonths.Keys.ToList().ToPython());
|
||||
liveList.Append(liveRollingSharpeSixMonths.Values.ToList().ToPython());
|
||||
liveList.Append(liveRollingSharpeTwelveMonths.Keys.ToList().ToPython());
|
||||
liveList.Append(liveRollingSharpeTwelveMonths.Values.ToList().ToPython());
|
||||
|
||||
base64 = Charting.GetRollingSharpeRatio(backtestList, liveList);
|
||||
}
|
||||
|
||||
return base64;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.Linq;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal class RuntimeDaysReportElement : ReportElement
|
||||
{
|
||||
private BacktestResult _backtest;
|
||||
private LiveResult _live;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new metric describing the number of days an algorithm ran for.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public RuntimeDaysReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_backtest = backtest;
|
||||
_live = live;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The generated output string to be injected
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var result = (Result) _live ?? _backtest;
|
||||
if (result == null)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
var equityPoints = ResultsUtil.EquityPoints(result);
|
||||
if (equityPoints.Count == 0)
|
||||
{
|
||||
Result = 0;
|
||||
return "0";
|
||||
}
|
||||
|
||||
var days = (equityPoints.Last().Key - equityPoints.First().Key).Days;
|
||||
Result = days;
|
||||
|
||||
return days.ToStringInvariant();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* 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.Data;
|
||||
using QuantConnect.Packets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
/// <summary>
|
||||
/// Class for render the Sharpe Ratio statistic for a report
|
||||
/// </summary>
|
||||
public class SharpeRatioReportElement : ReportElement
|
||||
{
|
||||
/// <summary>
|
||||
/// The number of trading days per year to get better result of statistics
|
||||
/// </summary>
|
||||
private double _tradingDaysPerYear;
|
||||
|
||||
/// <summary>
|
||||
/// Live result object
|
||||
/// </summary>
|
||||
protected LiveResult LiveResult { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Backtest result object
|
||||
/// </summary>
|
||||
protected BacktestResult BacktestResult { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Sharpe Ratio from a backtest
|
||||
/// </summary>
|
||||
public virtual decimal? BacktestResultValue => BacktestResult?.TotalPerformance?.PortfolioStatistics?.SharpeRatio;
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the sharpe ratio of the strategy.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
/// <param name="tradingDaysPerYear">The number of trading days per year to get better result of statistics</param>
|
||||
public SharpeRatioReportElement(string name, string key, BacktestResult backtest, LiveResult live, int tradingDaysPerYear)
|
||||
{
|
||||
LiveResult = live;
|
||||
BacktestResult = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
_tradingDaysPerYear = Convert.ToDouble(tradingDaysPerYear, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The generated output string to be injected
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
if (LiveResult == null)
|
||||
{
|
||||
Result = BacktestResultValue;
|
||||
return BacktestResultValue?.ToString("F1") ?? "-";
|
||||
}
|
||||
|
||||
var equityPoints = ResultsUtil.EquityPoints(LiveResult);
|
||||
var performance = DeedleUtil.PercentChange(new Series<DateTime, double>(equityPoints).ResampleEquivalence(date => date.Date, s => s.LastValue()));
|
||||
if (performance.ValueCount == 0)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
var sixMonthsAgo = performance.LastKey().AddDays(-180);
|
||||
var trailingSeries = performance.Where(series => series.Key >= sixMonthsAgo && series.Key.DayOfWeek != DayOfWeek.Saturday && series.Key.DayOfWeek != DayOfWeek.Sunday);
|
||||
var trailingPerformance = trailingSeries
|
||||
.Values
|
||||
.ToList();
|
||||
|
||||
var annualStandardDeviation = trailingPerformance.Count < 7 ? 0 : GetAnnualStandardDeviation(trailingPerformance, _tradingDaysPerYear);
|
||||
if (annualStandardDeviation <= 0)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
// Use excess returns to stay consistent with the reported PSR and backtest Sharpe ratio
|
||||
var riskFreeRate = new InterestRateProvider().GetAverageRiskFreeRate(trailingSeries.Keys);
|
||||
var annualPerformance = Statistics.Statistics.AnnualPerformance(trailingPerformance, _tradingDaysPerYear);
|
||||
var liveResultValue = Statistics.Statistics.SharpeRatio(annualPerformance, annualStandardDeviation, (double)riskFreeRate);
|
||||
Result = liveResultValue;
|
||||
return liveResultValue.ToString("F2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get annual standard deviation
|
||||
/// </summary>
|
||||
/// <param name="trailingPerformance">The performance for the last period</param>
|
||||
/// <param name="tradingDaysPerYear">The number of trading days per year to get better result of statistics</param>
|
||||
/// <returns>Annual standard deviation.</returns>
|
||||
public virtual double GetAnnualStandardDeviation(List<double> trailingPerformance, double tradingDaysPerYear)
|
||||
{
|
||||
return Statistics.Statistics.AnnualStandardDeviation(trailingPerformance, tradingDaysPerYear);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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.Globalization;
|
||||
using QuantConnect.Packets;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class SortinoRatioReportElement : SharpeRatioReportElement
|
||||
{
|
||||
/// <summary>
|
||||
/// Sortino ratio from a backtest
|
||||
/// </summary>
|
||||
public override decimal? BacktestResultValue => BacktestResult?.TotalPerformance?.PortfolioStatistics?.SortinoRatio;
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the Sortino ratio of the strategy.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
/// <param name="tradingDaysPerYear">The number of trading days per year to get better result of statistics</param>
|
||||
public SortinoRatioReportElement(string name, string key, BacktestResult backtest, LiveResult live, int tradingDaysPerYear)
|
||||
: base(name, key, backtest, live, tradingDaysPerYear)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get annual standard deviation
|
||||
/// </summary>
|
||||
/// <param name="trailingPerformance">The performance for the last period</param>
|
||||
/// <param name="tradingDaysPerYear">The number of trading days per year to get better result of statistics</param>
|
||||
/// <returns>Annual downside standard deviation.</returns>
|
||||
public override double GetAnnualStandardDeviation(List<double> trailingPerformance, double tradingDaysPerYear)
|
||||
{
|
||||
return Statistics.Statistics.AnnualDownsideStandardDeviation(trailingPerformance, tradingDaysPerYear);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* 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.Report.ReportElements
|
||||
{
|
||||
internal sealed class TextReportElement : ReportElement
|
||||
{
|
||||
private readonly string _content;
|
||||
|
||||
/// <summary>
|
||||
/// Text place holder report element
|
||||
/// </summary>
|
||||
/// <param name="name">Name of this text field</param>
|
||||
/// <param name="key">Report injection point</param>
|
||||
/// <param name="content">Content for injection</param>
|
||||
public TextReportElement(string name, string key, string content)
|
||||
{
|
||||
Name = name;
|
||||
Key = key;
|
||||
_content = content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Render the element contents
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override string Render()
|
||||
{
|
||||
return _content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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;
|
||||
using System.Linq;
|
||||
using QuantConnect.Orders;
|
||||
using QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class TradesPerDayReportElement : ReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the trades per day of the strategy.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public TradesPerDayReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate trades per day
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var liveOrders = _live?.Orders?.Values.ToList();
|
||||
if (liveOrders == null)
|
||||
{
|
||||
liveOrders = new List<Order>();
|
||||
}
|
||||
|
||||
var orders = _backtest?.Orders?.Values.Concat(liveOrders).OrderBy(x => x.Time);
|
||||
if (orders == null)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
if (!orders.Any())
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
var days = orders.Last().Time
|
||||
.Subtract(orders.First().Time)
|
||||
.TotalDays;
|
||||
|
||||
if (days == 0)
|
||||
{
|
||||
days = 1;
|
||||
}
|
||||
|
||||
var tradesPerDay = orders.Count() / days;
|
||||
Result = tradesPerDay;
|
||||
|
||||
if (tradesPerDay > 9)
|
||||
{
|
||||
return $"{tradesPerDay:F0}";
|
||||
}
|
||||
|
||||
return $"{tradesPerDay:F1}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 QuantConnect.Packets;
|
||||
|
||||
namespace QuantConnect.Report.ReportElements
|
||||
{
|
||||
internal sealed class TurnoverReportElement : ReportElement
|
||||
{
|
||||
private LiveResult _live;
|
||||
private BacktestResult _backtest;
|
||||
|
||||
/// <summary>
|
||||
/// Estimate the turnover of the strategy.
|
||||
/// </summary>
|
||||
/// <param name="name">Name of the widget</param>
|
||||
/// <param name="key">Location of injection</param>
|
||||
/// <param name="backtest">Backtest result object</param>
|
||||
/// <param name="live">Live result object</param>
|
||||
public TurnoverReportElement(string name, string key, BacktestResult backtest, LiveResult live)
|
||||
{
|
||||
_live = live;
|
||||
_backtest = backtest;
|
||||
Name = name;
|
||||
Key = key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The generated output string to be injected
|
||||
/// </summary>
|
||||
public override string Render()
|
||||
{
|
||||
var turnover = _backtest?.TotalPerformance?.PortfolioStatistics?.PortfolioTurnover;
|
||||
Result = turnover;
|
||||
if (turnover == null)
|
||||
{
|
||||
return "-";
|
||||
}
|
||||
|
||||
return $"{turnover:P0}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper shortcuts for report injection points.
|
||||
/// </summary>
|
||||
internal static class ReportKey
|
||||
{
|
||||
public const string Stylesheet = @"{{$REPORT-STYLESHEET}}";
|
||||
public const string StrategyName = @"{{$TEXT-STRATEGY-NAME}}";
|
||||
public const string StrategyDescription = @"{{$TEXT-STRATEGY-DESCRIPTION}}";
|
||||
public const string StrategyVersion = @"{{$TEXT-STRATEGY-VERSION}}";
|
||||
public const string LiveMarker = @"{{$LIVE-MARKER}}";
|
||||
public const string ParametersPageStyle = @"{{$CSS-PARAMETERS-PAGE-STYLE}}";
|
||||
public const string Parameters = @"{{$PARAMETERS}}";
|
||||
|
||||
public const string CAGR = @"{{$KPI-CAGR}}";
|
||||
public const string Turnover = @"{{$KPI-TURNOVER}}";
|
||||
public const string MaxDrawdown = @"{{$KPI-DRAWDOWN}}";
|
||||
public const string MaxDrawdownRecovery = @"{{$KPI-DRAWDOWN-RECOVERY}}";
|
||||
public const string KellyEstimate = @"{{$KPI-KELLY-ESTIMATE}}";
|
||||
public const string SharpeRatio = @"{{$KPI-SHARPE}}";
|
||||
public const string SortinoRatio = @"{{$KPI-SORTINO}}";
|
||||
public const string BacktestDays = @"{{$KPI-BACKTEST-DAYS}}";
|
||||
public const string DaysLive = @"{{$KPI-DAYS-LIVE}}";
|
||||
public const string InformationRatio = @"{{$KPI-INFORMATION-RATIO}}";
|
||||
public const string TradesPerDay = @"{{$KPI-TRADES-PER-DAY}}";
|
||||
public const string Markets = @"{{$KPI-MARKETS}}";
|
||||
public const string PSR = @"{{$KPI-PSR}}";
|
||||
public const string StrategyCapacity = @"{{$KPI-STRATEGY-CAPACITY}}";
|
||||
|
||||
public const string MonthlyReturns = @"{{$PLOT-MONTHLY-RETURNS}}";
|
||||
public const string CumulativeReturns = @"{{$PLOT-CUMULATIVE-RETURNS}}";
|
||||
public const string AnnualReturns = @"{{$PLOT-ANNUAL-RETURNS}}";
|
||||
public const string ReturnsPerTrade = @"{{$PLOT-RETURNS-PER-TRADE}}";
|
||||
public const string AssetAllocation = @"{{$PLOT-ASSET-ALLOCATION}}";
|
||||
public const string Drawdown = @"{{$PLOT-DRAWDOWN}}";
|
||||
public const string DailyReturns = @"{{$PLOT-DAILY-RETURNS}}";
|
||||
public const string RollingBeta = @"{{$PLOT-BETA}}";
|
||||
public const string RollingSharpe = @"{{$PLOT-SHARPE}}";
|
||||
public const string LeverageUtilization = @"{{$PLOT-LEVERAGE}}";
|
||||
public const string Exposure = @"{{$PLOT-EXPOSURE}}";
|
||||
public const string CrisisPageStyle = @"{{$CSS-CRISIS-PAGE-STYLE}}";
|
||||
public const string CrisisPlots = @"{{$HTML-CRISIS-PLOTS}}";
|
||||
public const string CrisisTitle = @"{{$TEXT-CRISIS-TITLE}}";
|
||||
public const string CrisisContents = @"{{$PLOT-CRISIS-CONTENT}}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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 QuantConnect.Lean.Engine.Results;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility methods for dealing with the <see cref="Result"/> objects
|
||||
/// </summary>
|
||||
public static class ResultsUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Get the points, from the Series name given, in Strategy Equity chart
|
||||
/// </summary>
|
||||
/// <param name="result">Result object to extract the chart points</param>
|
||||
/// <param name="seriesName">Series name from which the points will be extracted. By default is Equity series</param>
|
||||
/// <returns></returns>
|
||||
public static SortedList<DateTime, double> EquityPoints(Result result, string seriesName = null)
|
||||
{
|
||||
var points = new SortedList<DateTime, double>();
|
||||
|
||||
seriesName ??= BaseResultsHandler.EquityKey;
|
||||
if (result == null || result.Charts == null ||
|
||||
!result.Charts.ContainsKey(BaseResultsHandler.StrategyEquityKey) ||
|
||||
result.Charts[BaseResultsHandler.StrategyEquityKey].Series == null ||
|
||||
!result.Charts[BaseResultsHandler.StrategyEquityKey].Series.ContainsKey(seriesName))
|
||||
{
|
||||
return points;
|
||||
}
|
||||
|
||||
var series = result.Charts[BaseResultsHandler.StrategyEquityKey].Series[seriesName];
|
||||
switch (series)
|
||||
{
|
||||
case Series s:
|
||||
foreach (ChartPoint point in s.Values)
|
||||
{
|
||||
points[point.Time] = Convert.ToDouble(point.y);
|
||||
}
|
||||
break;
|
||||
|
||||
case CandlestickSeries candlestickSeries:
|
||||
foreach (Candlestick candlestick in candlestickSeries.Values)
|
||||
{
|
||||
points[candlestick.Time] = Convert.ToDouble(candlestick.Close);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the points of the benchmark
|
||||
/// </summary>
|
||||
/// <param name="result">Backtesting or live results</param>
|
||||
/// <returns>Sorted list keyed by date and value</returns>
|
||||
public static SortedList<DateTime, double> BenchmarkPoints(Result result)
|
||||
{
|
||||
var points = new SortedList<DateTime, double>();
|
||||
|
||||
if (result == null || result.Charts == null ||
|
||||
!result.Charts.ContainsKey(BaseResultsHandler.BenchmarkKey) ||
|
||||
result.Charts[BaseResultsHandler.BenchmarkKey].Series == null ||
|
||||
!result.Charts[BaseResultsHandler.BenchmarkKey].Series.ContainsKey(BaseResultsHandler.BenchmarkKey))
|
||||
{
|
||||
return points;
|
||||
}
|
||||
|
||||
if (!result.Charts.ContainsKey(BaseResultsHandler.BenchmarkKey))
|
||||
{
|
||||
return new SortedList<DateTime, double>();
|
||||
}
|
||||
if (!result.Charts[BaseResultsHandler.BenchmarkKey].Series.ContainsKey(BaseResultsHandler.BenchmarkKey))
|
||||
{
|
||||
return new SortedList<DateTime, double>();
|
||||
}
|
||||
|
||||
// Benchmark should be a Series, so we cast the points directly to ChartPoint
|
||||
foreach (ChartPoint point in result.Charts[BaseResultsHandler.BenchmarkKey].Series[BaseResultsHandler.BenchmarkKey].Values)
|
||||
{
|
||||
points[Time.UnixTimeStampToDateTime(point.x)] = Convert.ToDouble(point.y);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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 MathNet.Numerics.Statistics;
|
||||
using System;
|
||||
using QuantConnect.Statistics;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using QuantConnect.Data;
|
||||
|
||||
namespace QuantConnect.Report
|
||||
{
|
||||
/// <summary>
|
||||
/// Rolling window functions
|
||||
/// </summary>
|
||||
public static class Rolling
|
||||
{
|
||||
private static readonly IRiskFreeInterestRateModel _interestRateProvider = new InterestRateProvider();
|
||||
|
||||
/// <summary>
|
||||
/// Calculate the rolling beta with the given window size (in days)
|
||||
/// </summary>
|
||||
/// <param name="performancePoints">The performance points you want to measure beta for</param>
|
||||
/// <param name="benchmarkPoints">The benchmark/points you want to calculate beta with</param>
|
||||
/// <param name="windowSize">Days/window to lookback</param>
|
||||
/// <returns>Rolling beta</returns>
|
||||
public static Series<DateTime, double> Beta(SortedList<DateTime, double> performancePoints, SortedList<DateTime, double> benchmarkPoints, int windowSize = 132)
|
||||
{
|
||||
var dailyDictionary = StatisticsBuilder.PreprocessPerformanceValues(performancePoints.Select(x => new KeyValuePair<DateTime, decimal>(x.Key, (decimal)x.Value)));
|
||||
var dailyReturnsSeries = new Series<DateTime, double>(dailyDictionary);
|
||||
|
||||
Series<DateTime, double> benchmarkReturns;
|
||||
if (benchmarkPoints.Count != 0)
|
||||
{
|
||||
var benchmarkReturnsDictionary = StatisticsBuilder.CreateBenchmarkDifferences(benchmarkPoints.Select(x => new KeyValuePair<DateTime, decimal>(x.Key, (decimal)x.Value)), benchmarkPoints.Keys.First(), benchmarkPoints.Keys.Last());
|
||||
benchmarkReturns = new Series<DateTime, double>(benchmarkReturnsDictionary);
|
||||
}
|
||||
else
|
||||
{
|
||||
benchmarkReturns = new Series<DateTime, double>(benchmarkPoints);
|
||||
}
|
||||
|
||||
var returns = Frame.CreateEmpty<DateTime, string>();
|
||||
returns["strategy"] = dailyReturnsSeries;
|
||||
returns = returns.Join("benchmark", benchmarkReturns)
|
||||
.FillMissing(Direction.Forward)
|
||||
.DropSparseRows();
|
||||
|
||||
var correlation = returns
|
||||
.Window(windowSize)
|
||||
.SelectValues(x => Correlation.Pearson(x["strategy"].Values, x["benchmark"].Values));
|
||||
|
||||
var portfolioStandardDeviation = dailyReturnsSeries.Window(windowSize).SelectValues(s => s.StdDev());
|
||||
var benchmarkStandardDeviation = benchmarkReturns.Window(windowSize).SelectValues(s => s.StdDev());
|
||||
|
||||
return (correlation * (portfolioStandardDeviation / benchmarkStandardDeviation))
|
||||
.FillMissing(Direction.Forward)
|
||||
.DropMissing();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the rolling sharpe of the given series with a lookback of <paramref name="months"/>. The risk free rate is adjustable
|
||||
/// </summary>
|
||||
/// <param name="equityCurve">Equity curve to calculate rolling sharpe for</param>
|
||||
/// <param name="months">Number of months to calculate the rolling period for</param>
|
||||
/// <param name="tradingDayPerYear">The number of trading days per year to increase result of Annual statistics</param>
|
||||
/// <returns>Rolling sharpe ratio</returns>
|
||||
public static Series<DateTime, double> Sharpe(Series<DateTime, double> equityCurve, int months, int tradingDayPerYear)
|
||||
{
|
||||
var riskFreeRate = (double)_interestRateProvider.GetAverageRiskFreeRate(equityCurve.Keys);
|
||||
if (equityCurve.IsEmpty)
|
||||
{
|
||||
return equityCurve;
|
||||
}
|
||||
|
||||
var dailyReturns = equityCurve.ResampleEquivalence(date => date.Date, s => s.LastValue())
|
||||
.PercentChange();
|
||||
|
||||
var rollingSharpeData = new List<KeyValuePair<DateTime, double>>();
|
||||
var firstDate = equityCurve.FirstKey();
|
||||
|
||||
foreach (var date in equityCurve.Keys)
|
||||
{
|
||||
var nMonthsAgo = date.AddMonths(-months);
|
||||
if (nMonthsAgo < firstDate)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var algoPerformanceLookback = dailyReturns.Between(nMonthsAgo, date);
|
||||
rollingSharpeData.Add(
|
||||
new KeyValuePair<DateTime, double>(
|
||||
date,
|
||||
Statistics.Statistics.SharpeRatio(algoPerformanceLookback.Values.ToList(), riskFreeRate, tradingDayPerYear)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return new Series<DateTime, double>(rollingSharpeData.Select(kvp => kvp.Key), rollingSharpeData.Select(kvp => kvp.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"data-folder": "../../../Data",
|
||||
"strategy-name": "Foobar",
|
||||
"strategy-version": "v1.0.0",
|
||||
"strategy-description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum",
|
||||
"live-data-source-file": "Foobar-live.json",
|
||||
"backtest-data-source-file": "Foobar.json",
|
||||
"report-destination": "Foobar.html",
|
||||
|
||||
"environment": "report",
|
||||
|
||||
// handlers
|
||||
"log-handler": "QuantConnect.Logging.CompositeLogHandler",
|
||||
"messaging-handler": "QuantConnect.Messaging.Messaging",
|
||||
"job-queue-handler": "QuantConnect.Queues.JobQueue",
|
||||
"api-handler": "QuantConnect.Api.Api",
|
||||
"map-file-provider": "QuantConnect.Data.Auxiliary.LocalDiskMapFileProvider",
|
||||
"factor-file-provider": "QuantConnect.Data.Auxiliary.LocalDiskFactorFileProvider",
|
||||
"data-provider": "QuantConnect.Lean.Engine.DataFeeds.DefaultDataProvider",
|
||||
"alpha-handler": "QuantConnect.Lean.Engine.Alphas.DefaultAlphaHandler",
|
||||
"data-channel-provider": "DataChannelProvider",
|
||||
|
||||
"environments": {
|
||||
|
||||
// defines the 'backtesting' environment
|
||||
"report": {
|
||||
"live-mode": false,
|
||||
|
||||
"setup-handler": "QuantConnect.Lean.Engine.Setup.ConsoleSetupHandler",
|
||||
"result-handler": "QuantConnect.Lean.Engine.Results.BacktestingResultHandler",
|
||||
"data-feed-handler": "QuantConnect.Lean.Engine.DataFeeds.FileSystemDataFeed",
|
||||
"real-time-handler": "QuantConnect.Lean.Engine.RealTime.BacktestingRealTimeHandler",
|
||||
"history-provider": "QuantConnect.Lean.Engine.HistoricalData.SubscriptionDataReaderHistoryProvider",
|
||||
"transaction-handler": "QuantConnect.Lean.Engine.TransactionHandlers.BacktestingTransactionHandler"
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,540 @@
|
||||
body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
background-color: #666666;
|
||||
box-shadow: #0A0A0A;
|
||||
font-family: "Roboto";
|
||||
}
|
||||
|
||||
.table.qc-table.compact thead > tr {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.page .content .text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page .content .text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.page .content .text-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 993px;
|
||||
height: 1405px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin: 10px auto;
|
||||
background-color: #fff;
|
||||
page-break-after: always; /* Fixes wkhtmltopdf header bug - https://github.com/wkhtmltopdf/wkhtmltopdf/issues/1524*/
|
||||
}
|
||||
|
||||
|
||||
.text-h1,
|
||||
h1 {
|
||||
font-size: 26px;
|
||||
width: 100%;
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.footer {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
color: #222;
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
text-align: center;
|
||||
line-height: 50px;
|
||||
}
|
||||
|
||||
.footer .footer-page {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
line-height: 50px;
|
||||
margin-right: 40px;
|
||||
}
|
||||
|
||||
.footer .footer-id {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
line-height: 50px;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.header {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 120px;
|
||||
padding-left: 40px;
|
||||
padding-right: 40px;
|
||||
color: #222;
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.header .header-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.header .header-left img {
|
||||
height: 58px;
|
||||
padding-top: 25px;
|
||||
}
|
||||
|
||||
.header .header-right {
|
||||
margin-top: 40px;
|
||||
line-height: 30px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.page .content {
|
||||
position: absolute;
|
||||
left: 40px;
|
||||
right: 40px;
|
||||
top: 80px;
|
||||
bottom: 80px;
|
||||
border-top: 1px solid #222;
|
||||
border-bottom: 1px solid #222;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
table.table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.table.qc-table {
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.table.qc-table thead th {
|
||||
padding: 5px;
|
||||
border-top: solid 1px #677080;
|
||||
border-left: solid 1px #677080;
|
||||
border-bottom: solid 1px #677080;
|
||||
color: #ffffff;
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table.qc-table thead tr {
|
||||
background-color: #677080;
|
||||
}
|
||||
|
||||
.table.qc-table thead th:last-child {
|
||||
border-right: solid 1px #677080;
|
||||
}
|
||||
|
||||
.table.qc-table tbody tr td {
|
||||
border-top: none;
|
||||
border-bottom: solid 1px #e9edf1;
|
||||
border-left: solid 1px #e9edf1;
|
||||
padding: 15px;
|
||||
margin: 0;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.table.qc-table.compact tbody tr td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.table.qc-table tbody tr {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.table.qc-table tbody tr:nth-child(odd) {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.table.qc-table tbody tr td:last-child {
|
||||
border-right: solid 1px #e9edf1;
|
||||
}
|
||||
|
||||
.table.qc-table a,
|
||||
.table.qc-table span,
|
||||
.table.qc-table div,
|
||||
.table.qc-table p {
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table.qc-table.table-itemized tbody tr td:first-child,
|
||||
.table.qc-table.table-itemized tbody tr td:first-child a,
|
||||
.table.qc-table.table-itemized tbody tr td:first-child span,
|
||||
.table.qc-table.table-itemized tbody tr td:first-child div,
|
||||
.table.qc-table.table-itemized tbody tr td:first-child p {
|
||||
font-family: 'Inter', Helvetica, Arial, sans-serif;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.table.qc-table a,
|
||||
.table.qc-table a span,
|
||||
.table.qc-table a div,
|
||||
.table.qc-table a p {
|
||||
color: #f5ae29;
|
||||
}
|
||||
|
||||
.table.v-top tr td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.table.col-2 td {
|
||||
width: 50%
|
||||
}
|
||||
|
||||
.table.col-3 td {
|
||||
width: 33.3%
|
||||
}
|
||||
|
||||
.table.col-4 td {
|
||||
width: 25%
|
||||
}
|
||||
|
||||
.table.col-5 td {
|
||||
width: 20%
|
||||
}
|
||||
|
||||
.no-padding {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.no-margin {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.split-text {
|
||||
-webkit-column-count: 2;
|
||||
-moz-column-count: 2;
|
||||
column-count: 2;
|
||||
}
|
||||
|
||||
.profile img {
|
||||
max-height: 150px;
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
display: block;
|
||||
margin: auto auto 12px;
|
||||
}
|
||||
|
||||
.page .content .text-justify,
|
||||
.page .content p {
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.hide-print {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page {
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.container-row {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.container-row > div:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.container-row > div:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
#table-summary .fa-times {
|
||||
color: #d9534f;
|
||||
}
|
||||
|
||||
#author-metadata tr > td:first-child,
|
||||
#project-metadata tr > td:first-child {
|
||||
width: 66%;
|
||||
}
|
||||
|
||||
#project-metadata tr > td:last-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#key-statistics tr > td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#key-statistics tr > td,
|
||||
#key-characteristics tr > td,
|
||||
table.table tr > td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table.table.align-top tr > td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table.table {
|
||||
height: 230px;
|
||||
}
|
||||
|
||||
table img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 225px;
|
||||
}
|
||||
|
||||
.col-xs-12 table > tbody > tr > td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table.table > thead > tr > th {
|
||||
border-bottom: none;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
padding-top: 15px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
table.table > tbody > tr > td {
|
||||
border-top: none;
|
||||
font-size: 15px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
table#key-characteristics {
|
||||
width: calc(100% - 30px);
|
||||
}
|
||||
|
||||
table#key-characteristics > thead > tr > th {
|
||||
font-size: 14px;
|
||||
width: 12.5%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table#key-characteristics > thead > tr > th.title {
|
||||
font-size: 18px;
|
||||
width: 25%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table#key-characteristics > tbody > tr {
|
||||
border-bottom: 1px solid #cbd1d4;
|
||||
}
|
||||
|
||||
table#key-characteristics > tbody > tr > td {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
table#key-characteristics > tbody > tr > td.title {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table#key-characteristics > tbody > tr:first-child {
|
||||
border-top: 1px solid #9c9c9c;
|
||||
}
|
||||
|
||||
table#key-characteristics > tbody > tr > td > span.markets {
|
||||
background: #8f9ca3;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
padding: 8px 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.col-xs-4:nth-child(2) table#key-characteristics > tbody > tr > td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
table#key-characteristics > tbody > tr > td:first-child {
|
||||
border-top: #c3cace;
|
||||
}
|
||||
|
||||
table#description-box {
|
||||
word-wrap: break-word;
|
||||
min-height: 225px;
|
||||
}
|
||||
|
||||
table#description-box > thead > tr > th > p {
|
||||
color: #f5ae29;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
table#description-box > thead > tr > th > p > span {
|
||||
font-weight: 100;
|
||||
}
|
||||
|
||||
table#description-box > thead > tr > th > p > span {
|
||||
margin-right: 10px;
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: #f5ae29;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 1200px;
|
||||
height: 1698px;
|
||||
}
|
||||
|
||||
.page .content {
|
||||
top: 80px;
|
||||
left: 110px;
|
||||
right: 110px;
|
||||
border-top: 1px solid #888888;
|
||||
border-bottom: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.page .header {
|
||||
height: 80px;
|
||||
left: 110px;
|
||||
right: 110px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.header .header-left img {
|
||||
width: 230px;
|
||||
height: auto;
|
||||
padding-top: 0;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
.header .header-right {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: bold;
|
||||
margin-top: 40px;
|
||||
line-height: 23px;
|
||||
float: right;
|
||||
font-size: 18px;
|
||||
max-width: 70%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.container-row {
|
||||
height: auto;
|
||||
overflow: auto;
|
||||
border-bottom: 1px solid #b8b8b8;
|
||||
}
|
||||
|
||||
.container-row.first-row .col-xs-4:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.container-row.first-row .col-xs-4:last-child p {
|
||||
font-family: 'Inter', sans-serif;
|
||||
position: absolute;
|
||||
transform: rotate(-90deg);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.container-row.first-row .col-xs-4:last-child p:first-child {
|
||||
left: -13px;
|
||||
top: 128px;
|
||||
}
|
||||
|
||||
.container-row.first-row .col-xs-4:last-child p:nth-child(2) {
|
||||
bottom: 36px;
|
||||
}
|
||||
|
||||
.container-row.first-row .col-xs-4:last-child table img {
|
||||
width: calc(100% - 25px);
|
||||
}
|
||||
|
||||
.container-row:empty {
|
||||
border: none;
|
||||
}
|
||||
|
||||
span.checkmark, span.exmark {
|
||||
border-radius: 50%;
|
||||
position: absolute;
|
||||
border: none;
|
||||
top: 10px;
|
||||
right: 36%;
|
||||
}
|
||||
|
||||
span.checkmark.half, span.exmark.half {
|
||||
right: 44%;
|
||||
}
|
||||
|
||||
span.checkmark {
|
||||
background-color: #46bd6a;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
span.checkmark:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 3px;
|
||||
width: 5px;
|
||||
height: 10px;
|
||||
border: solid #fff;
|
||||
border-width: 0 1px 1px 0;
|
||||
-webkit-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
span.exmark {
|
||||
background-color: #bc4143;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
padding: 4px 5px;
|
||||
padding-top: 4px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
p#strategy-description {
|
||||
overflow: hidden;
|
||||
max-height: 130px;
|
||||
margin-bottom: 0;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.kpi-live {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
body {
|
||||
margin: 0;
|
||||
box-shadow: 0;
|
||||
padding: 0;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.page {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#monthly-returns-plot {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.page .container-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
Please define here the CSS changes you would like to apply to the HTML file, namely, (template.html).
|
||||
To apply the changes, please pass the content of this file to Report.cs constructor through its arguments .
|
||||
It's worth to be mentioned, that if you need to modify certain property, write it here with the same
|
||||
name since CSS applies always the last rule defined in the stylesheet and this file will be appended
|
||||
at the bottom of the default CSS template, namely, (report.css).
|
||||
*/
|
||||
@@ -0,0 +1,358 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<meta property="og:title" content="QuantConnect Backtest Report: "/>
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:site_name" content="QuantConnect.com"/>
|
||||
<meta property="og:description" content=""/>
|
||||
<meta property="og:url" content="https://www.quantconnect.com/terminal/reports/"/>
|
||||
<meta property="og:image" content="{{$PLOT-CUMULATIVE-RETURNS}}"/>
|
||||
<meta property="og:image" content="{{$PLOT-ANNUAL-RETURNS}}"/>
|
||||
<meta property="og:image" content="{{$PNG-AUTHOR-PHOTO}}"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter&family=Roboto&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
{{$REPORT-STYLESHEET}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<img src="https://cdn.quantconnect.com/web/i/logo.png">
|
||||
</div>
|
||||
<div class="header-right">Strategy Report: {{$TEXT-STRATEGY-NAME}} {{$TEXT-STRATEGY-VERSION}}</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="container-row">
|
||||
<div class="col-xs-12">
|
||||
<table id="description-box" class="table compact no-margin align-top">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<p>
|
||||
<span>|</span> Strategy Description
|
||||
</p>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<p id="strategy-description" class="text-justify">
|
||||
{{$TEXT-STRATEGY-DESCRIPTION}}
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-row first-row">
|
||||
<div class="col-xs-8">
|
||||
<table id="key-characteristics" class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="title">Key Statistics</th><th class="kpi-live">Backtest</th><th class="kpi-live">Live</th><th class="title"></th><th class="kpi-live">Backtest</th><th class="kpi-live">Live</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="title">Runtime Days</td><td>{{$KPI-BACKTEST-DAYS}}</td><td class="kpi-live">{{$KPI-DAYS-LIVE}}</td>
|
||||
<td class="title">Drawdown</td><td>{{$KPI-DRAWDOWN}}</td><td class="kpi-live">{{$KPI-LIVE-DRAWDOWN}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">Turnover</td><td>{{$KPI-TURNOVER}}</td><td class="kpi-live">{{$KPI-LIVE-TURNOVER}}</td>
|
||||
<td class="title">Probabilistic SR</td><td>{{$KPI-PSR}}</td><td class="kpi-live">{{$KPI-LIVE-PSR}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">CAGR</td><td>{{$KPI-CAGR}}</td><td class="kpi-live">{{$KPI-LIVE-CAGR}}</td>
|
||||
<td class="title">{{$LIVE-MARKER}}Sharpe Ratio</td><td>{{$KPI-SHARPE}}</td><td class="kpi-live">{{$KPI-LIVE-SHARPE}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">Capacity (USD)</td><td>{{$KPI-STRATEGY-CAPACITY}}</td>
|
||||
<td class="title">Sortino Ratio</td><td>{{$KPI-SORTINO}}</td><td class="kpi-live">{{$KPI-LIVE-SORTINO}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">Trades per Day</td><td>{{$KPI-TRADES-PER-DAY}}</td><td class="kpi-live">{{$KPI-LIVE-TRADES-PER-DAY}}</td>
|
||||
<td class="title">Information Ratio</td><td>{{$KPI-INFORMATION-RATIO}}</td><td class="kpi-live">{{$KPI-LIVE-INFORMATION-RATIO}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="title">Drawdown Recovery</td><td>{{$KPI-DRAWDOWN-RECOVERY}}</td><td class="kpi-live">{{$KPI-LIVE-DRAWDOWN-RECOVERY}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-xs-4">
|
||||
<table class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Monthly Returns</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 0;">
|
||||
<img id="monthly-returns-plot" src="{{$PLOT-MONTHLY-RETURNS}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-row">
|
||||
<div class="col-xs-12">
|
||||
<table class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cumulative Returns</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 0;">
|
||||
<img src="{{$PLOT-CUMULATIVE-RETURNS}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-row">
|
||||
<div class="col-xs-4">
|
||||
<table class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Annual Returns</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:0;">
|
||||
<img src="{{$PLOT-ANNUAL-RETURNS}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-xs-4">
|
||||
<table class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Returns Per Trade</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:0;">
|
||||
<img src="{{$PLOT-RETURNS-PER-TRADE}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-xs-4">
|
||||
<table class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset Allocation</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:0;">
|
||||
<img src="{{$PLOT-ASSET-ALLOCATION}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-row">
|
||||
<div class="col-xs-12">
|
||||
<table class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Drawdown</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 0;">
|
||||
<img src="{{$PLOT-DRAWDOWN}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<img src="https://cdn.quantconnect.com/web/i/logo.png">
|
||||
</div>
|
||||
<div class="header-right">Strategy Report Summary: {{$TEXT-STRATEGY-NAME}} {{$TEXT-STRATEGY-VERSION}}</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="container-row">
|
||||
<div class="col-xs-12">
|
||||
<table class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Daily Returns</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 0;">
|
||||
<img src="{{$PLOT-DAILY-RETURNS}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-row">
|
||||
<div class="col-xs-12">
|
||||
<table class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rolling Portfolio Beta</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 0;">
|
||||
<img src="{{$PLOT-BETA}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-row">
|
||||
<div class="col-xs-12">
|
||||
<table class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rolling Sharpe Ratio</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 0;">
|
||||
<img src="{{$PLOT-SHARPE}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-row">
|
||||
<div class="col-xs-12">
|
||||
<table class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Leverage</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 0;">
|
||||
<img src="{{$PLOT-LEVERAGE}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-row">
|
||||
<div class="col-xs-12">
|
||||
<table class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Long-Short Exposure</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 0;">
|
||||
<img src="{{$PLOT-EXPOSURE}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page" style="{{$CSS-CRISIS-PAGE-STYLE}}">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<img src="https://cdn.quantconnect.com/web/i/logo.png">
|
||||
</div>
|
||||
<div class="header-right">Strategy Report Summary: {{$TEXT-STRATEGY-NAME}} {{$TEXT-STRATEGY-VERSION}}</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="container-row">
|
||||
{{$HTML-CRISIS-PLOTS}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page" id="parameters" style="{{$CSS-PARAMETERS-PAGE-STYLE}}">
|
||||
<div class="header">
|
||||
<div class="header-left">
|
||||
<img src="https://cdn.quantconnect.com/web/i/logo.png">
|
||||
</div>
|
||||
<div class="header-right">Strategy Report Summary: {{$TEXT-STRATEGY-NAME}} {{$TEXT-STRATEGY-VERSION}}</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="container-row">
|
||||
<div class="col-xs-12">
|
||||
<table id="key-characteristics" class="table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="title">Parameters</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{$PARAMETERS}}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<!--crisis
|
||||
<div class="col-xs-4">
|
||||
<table class="crisis-chart table compact">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="display: block; height: 75px;">{{$TEXT-CRISIS-TITLE}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:0;">
|
||||
<img src="{{$PLOT-CRISIS-CONTENT}}">
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
crisis-->
|
||||
|
||||
<!--parameters
|
||||
<tr>
|
||||
<td class = "title"> {{$KEY0}} </td><td> {{$VALUE0}} </td>
|
||||
<td class = "title"> {{$KEY1}} </td><td> {{$VALUE1}} </td>
|
||||
</tr>
|
||||
parameters-->
|
||||
Reference in New Issue
Block a user