Meta Analysis
Backtest Analysis
Introduction
Load your backtest results into the Research Environment to analyze trades and easily compare them against the raw backtesting data. Compare backtests from different projects to find uncorrelated strategies to combine for better performance.
Loading your backtest trades allows you to plot fills against detailed data, or locate the source of profits. Similarly you can search for periods of high churn to reduce turnover and trading fees.
Read Backtest Results
To get the results of a backtest, call the ReadBacktestread_backtest method with the project Id and backtest ID.
#load "../Initialize.csx"
#load "../QuantConnect.csx"
using QuantConnect; using QuantConnect.Api; var backtest = api.ReadBacktest(projectId, backtestId);
backtest = api.read_backtest(project_id, backtest_id)
The following table provides links to documentation that explains how to get the project Id and backtest Id, depending on the platform you use:
| Platform | Project Id | Backtest Id |
|---|---|---|
| Cloud Platform | Get Project Id | Get Backtest Id |
| Local Platform | Get Project Id | Get Backtest Id |
| CLI | Get Project Id | Get Backtest Id |
Note that this method returns a snapshot of the backtest at the current moment. If the backtest is still executing, the result won't include all of the backtest data.
The ReadBacktestread_backtest method returns a Backtest object, which have the following attributes:
Plot Order Fills
Follow these steps to plot the daily order fills of a backtest:
- Get the backtest orders.
-
Organize the trade times and prices for each security into a dictionary.
class OrderData: def __init__(self): self.buy_fill_times = [] self.buy_fill_prices = [] self.sell_fill_times = [] self.sell_fill_prices = [] order_data_by_symbol = {} for order in [x.order for x in orders]: if order.symbol not in order_data_by_symbol: order_data_by_symbol[order.symbol] = OrderData() order_data = order_data_by_symbol[order.symbol] is_buy = order.quantity > 0 (order_data.buy_fill_times if is_buy else order_data.sell_fill_times).append(order.last_fill_time.date()) (order_data.buy_fill_prices if is_buy else order_data.sell_fill_prices).append(order.price) -
Get the price history of each security you traded.
qb = QuantBook() start_date = datetime.max.date() end_date = datetime.min.date() for symbol, order_data in order_data_by_symbol.items(): if order_data.buy_fill_times: start_date = min(start_date, min(order_data.buy_fill_times)) end_date = max(end_date, max(order_data.buy_fill_times)) if order_data.sell_fill_times: start_date = min(start_date, min(order_data.sell_fill_times)) end_date = max(end_date, max(order_data.sell_fill_times)) start_date -= timedelta(days=3) all_history = qb.history(list(order_data_by_symbol.keys()), start_date, end_date, Resolution.DAILY) -
Create a candlestick plot for each security and annotate each plot with buy and sell markers.
import plotly.express as px import plotly.graph_objects as go for symbol, order_data in order_data_by_symbol.items(): history = all_history.loc[symbol] # Plot security price candlesticks candlestick = go.Candlestick(x=history.index, open=history['open'], high=history['high'], low=history['low'], close=history['close'], name='Price') layout = go.Layout(title=go.layout.Title(text=f'{symbol.value} Trades'), xaxis_title='Date', yaxis_title='Price', xaxis_rangeslider_visible=False, height=600) fig = go.Figure(data=[candlestick], layout=layout) # Plot buys fig.add_trace(go.Scatter( x=order_data.buy_fill_times, y=order_data.buy_fill_prices, marker=go.scatter.Marker(color='aqua', symbol='triangle-up', size=10), mode='markers', name='Buys', )) # Plot sells fig.add_trace(go.Scatter( x=order_data.sell_fill_times, y=order_data.sell_fill_prices, marker=go.scatter.Marker(color='indigo', symbol='triangle-down', size=10), mode='markers', name='Sells', )) fig.show()
orders = api.read_backtest_orders(project_id, backtest_id)
The following table provides links to documentation that explains how to get the project Id and backtest Id, depending on the platform you use:
| Platform | Project Id | Backtest Id |
|---|---|---|
| Cloud Platform | Get Project Id | Get Backtest Id |
| Local Platform | Get Project Id | Get Backtest Id |
| CLI | Get Project Id | Get Backtest Id |
The ReadBacktestOrdersread_backtest_orders method returns a list of ApiOrderResponse objects, which have the following properties:
Note: The preceding plots only show the last fill of each trade. If your trade has partial fills, the plots only display the last fill.
Plot Metadata
Follow these steps to plot the equity curve, benchmark, and drawdown of a backtest:
- Define the project Id, backtest Id, and read the "Strategy Equity", "Drawdown", and "Benchmark" charts.
- Extract the series and create a
pandas.DataFrame. - Plot the performance chart.
from time import time
project_id = 23034953
backtest_id = 'ff616bb2cbccf70f61ea431278e57728'
def read_chart(project_id, backtest_id, chart_name, start=0, end=int(time()), count=500):
return api.read_backtest_chart(
project_id, chart_name, start, end, count, backtest_id
).chart
strategy_equity = read_chart(project_id, backtest_id, 'Strategy Equity')
drawdown_chart = read_chart(project_id, backtest_id, 'Drawdown')
benchmark_chart = read_chart(project_id, backtest_id, 'Benchmark')
The following table provides links to documentation that explains how to get the project Id and backtest Id, depending on the platform you use:
| Platform | Project Id | Backtest Id |
|---|---|---|
| Cloud Platform | Get Project Id | Get Backtest Id |
| Local Platform | Get Project Id | Get Backtest Id |
| CLI | Get Project Id | Get Backtest Id |
def to_series(chart, series_name, selector=lambda x: x.y):
return pd.Series({v.time: selector(v) for v in chart.series[series_name].values})
df = pd.DataFrame({
"Equity": to_series(strategy_equity, 'Equity', selector=lambda x: x.close),
"Return": to_series(strategy_equity, 'Return'),
"Drawdown": to_series(drawdown_chart, 'Equity Drawdown'),
"Benchmark": to_series(benchmark_chart, 'Benchmark')
}).ffill()
df.index = df.index.tz_localize('UTC').tz_convert('US/Eastern').tz_localize(None)
# Create subplots to plot series on same/different plots
fig, ax = plt.subplots(3, 1, figsize=(12, 16), sharex=True, gridspec_kw={'height_ratios': [2, 1, 1]})
# Plot the equity curve
ax[0].plot(df.index, df["Equity"])
ax[0].set_title("Strategy Equity Curve")
ax[0].set_ylabel("Portfolio Value ($)")
# Plot the benchmark on the same plot, scale by using another y-axis
ax2 = ax[0].twinx()
ax2.plot(df.index, df["Benchmark"], color="grey")
ax2.set_ylabel("Benchmark Price ($)", color="grey")
# Plot the daily returns
ax[1].plot(df.index, df["Return"], color="blue")
ax[1].set_title("Daily Return")
ax[1].set_ylabel("%")
# Plot the drawdown on another plot
ax[2].plot(df.index, df["Drawdown"], color="red")
ax[2].set_title("Drawdown")
ax[2].set_xlabel("Time")
ax[2].set_ylabel("%");
The following table shows all the chart series you can plot:
| Chart | Series | Description |
|---|---|---|
| Strategy Equity | Equity | Time series of the equity curve. This series may not update daily. |
| Return | Time series of daily returns. Use this series for daily updates. | |
| Daily Performance | Time series of daily percentage change. This series has as many data points as "Equity". | |
| Capacity | Strategy Capacity | Time series of strategy capacity snapshots |
| Drawdown | Equity Drawdown | Time series of equity peak-to-trough value |
| Benchmark | Benchmark | Time series of the benchmark closing price (SPY, by default) |
| Exposure | SecurityType - Long Ratio | Time series of the overall ratio of SecurityType long positions of the whole portfolio if any SecurityType is ever in the universe |
| SecurityType - Short Ratio | Time series of the overall ratio of SecurityType short position of the whole portfolio if any SecurityType is ever in the universe | |
| Assets Sales Volume | Each ticker is one series | A chart showing the proportion of total volume for each traded security. |
| Portfolio Turnover | Portfolio Turnover | A time series of the portfolio turnover rate. |
| Portfolio Margin | Each ticker is one series | A stacked area chart of the portfolio margin usage. For more information about this chart, see Portfolio Margin Plots. |
| Asset Plot | Price and Annotations | A time series of an asset's price with order event annotations. For more information about these charts, see Asset Plots. |
| Custom Chart | Custom Series | Time series of a Series in a custom chart |
Plot Insights
Follow these steps to display the insights of each asset in a backtest:
- Get the insights.
- Organize the insights into a DataFrame.
- Get the price history of each security that has an insight.
- Plot the price and insights of each asset.
insight_response = api.read_backtest_insights(project_id, backtest_id)
The following table provides links to documentation that explains how to get the project Id and backtest Id, depending on the platform you use:
| Platform | Project Id | Backtest Id |
|---|---|---|
| Cloud Platform | Get Project Id | Get Backtest Id |
| Local Platform | Get Project Id | Get Backtest Id |
| CLI | Get Project Id | Get Backtest Id |
The read_backtest_insights method returns an InsightResponse object, which have the following properties:
import pytz
def _eastern_time(unix_timestamp):
return unix_timestamp.replace(tzinfo=pytz.utc)\
.astimezone(pytz.timezone('US/Eastern')).replace(tzinfo=None)
insight_df = pd.DataFrame(
[
{
'Symbol': i.symbol,
'Direction': i.direction,
'Generated Time': _eastern_time(i.generated_time_utc),
'Close Time': _eastern_time(i.close_time_utc),
'Weight': i.weight
}
for i in insight_response.insights
]
)
symbols = list(insight_df['Symbol'].unique())
qb = QuantBook()
history = qb.history(
symbols, insight_df['Generated Time'].min()-timedelta(1),
insight_df['Close Time'].max(), Resolution.DAILY
)['close'].unstack(0)
colors = ['yellow', 'green', 'red']
fig, axs = plt.subplots(len(symbols), 1, sharex=True)
for i, symbol in enumerate(symbols):
ax = axs[i]
history[symbol].plot(ax=ax)
for _, insight in insight_df[insight_df['Symbol'] == symbol].iterrows():
ax.axvspan(
insight['Generated Time'], insight['Close Time'],
color=colors[insight['Direction']], alpha=0.3
)
ax.set_title(f'Insights for {symbol.value}')
ax.set_xlabel('Date')
ax.set_ylabel('Price')
plt.tight_layout()
plt.show()
Examples
Example 1: Read Backtest Statistics
The following example reads the last completed backtest statistics in a jupyter notebook.
// Load the necessary assemblies. #load "../Initialize.csx"
// Load the necessary assembly files. #load "../QuantConnect.csx"
using QuantConnect;
using QuantConnect.Api;
using QuantConnect.Research;
// Instantiate QuantBook instance for researching.
var qb = new QuantBook();
// Get backtest list in the current project.
var backtests = api.ListBacktests(qb.ProjectId)
// Get the last completed backtest to study.
var backtestId = backtests.Backtests
.Where(x => x.Progress == 1m)
.OrderByDescending(x => x.Created)
.First()
.BacktestId;
var backtest = api.ReadBacktest(qb.ProjectId, backtestId);
// Obtain the backtest statistics.
Console.WriteLine(backtest.Statistics.ToString()); # Instantiate QuantBook instance for researching.
qb = QuantBook()
# Get backtest list in the current project.
backtests = api.list_backtests(qb.project_id)
# Get the last completed backtest to study.
backtest_id = sorted(
[x for x in backtests.backtests if x.progress == 1],
key=lambda x: x.created,
reverse=True
)[0].backtest_id
backtest = api.read_backtest(project_id, backtest_id)
# Obtain the backtest statistics.
print(backtest.statistics)