Meta Analysis

Live Analysis

Introduction

Load your live trading results into the Research Environment to compare live trading performance against simulated backtest results.

Read Live Results

To get the results of a live algorithm, call the ReadLiveAlgorithmread_live_algorithm method with the project Id and deployment ID.

#load "../Initialize.csx"
#load "../QuantConnect.csx"
using QuantConnect;
using QuantConnect.Api;

var liveAlgorithm = api.ReadLiveAlgorithm(projectId, deployId);
live_algorithm = api.read_live_algorithm(project_id, deploy_id)

The following table provides links to documentation that explains how to get the project Id and deployment Id, depending on the platform you use:

PlatformProject IdDeployment Id
Cloud PlatformGet Project IdGet Deployment Id
Local PlatformGet Project IdGet Deployment Id
CLIGet Project Id

The ReadLiveAlgorithmread_live_algorithm method returns a LiveAlgorithmResults object, which have the following attributes:

Plot Order Fills

Follow these steps to plot the daily order fills of a live algorithm:

  1. Get the live trading orders.
  2. orders = api.read_live_orders(project_id)

    The following table provides links to documentation that explains how to get the project Id, depending on the platform you use:

    PlatformProject Id
    Cloud PlatformGet Project Id
    Local PlatformGet Project Id
    CLIGet Project Id

    By default, the orders with an ID between 0 and 100. To get orders with an ID greater than 100, pass start and end arguments to the ReadLiveOrdersread_live_orders method. Note that end - start must be less than 100.

    orders = api.read_live_orders(project_id, 100, 150)

    The ReadLiveOrdersread_live_orders method returns a list of Order objects, which have the following properties:

  3. 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)
  4. 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)
  5. 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()
  6. Plot of AAPL price with buy/sell markers Plot of SPY price with buy/sell markers

    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 Charts

Follow these steps to plot the equity curve, benchmark, and drawdown of a live algorithm:

  1. Define the project Id and read the "Strategy Equity", "Drawdown", and "Benchmark" charts.
  2. from time import sleep, time
    
    project_id = 23034953
    
    def read_chart(project_id, chart_name, start=0, end=int(time()), count=500):
        # Retry up to 10 times if the chart data is still loading
        for attempt in range(10):
            result = api.read_live_chart(project_id, chart_name, start, end, count)
            if result.status == 'loading':
                print(f"Chart data is loading... (attempt {attempt + 1}/10)")
                sleep(10)
                continue
            break
        return result.chart
    
    strategy_equity = read_chart(project_id, 'Strategy Equity')
    drawdown_chart = read_chart(project_id, 'Drawdown')
    benchmark_chart = read_chart(project_id, 'Benchmark')

    The process to get your project Id depends on if you use the Cloud Platform, Local Platform, or CLI.

  3. Extract the series and create a pandas.DataFrame.
  4. 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)
  5. Plot the performance chart.
  6. # 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("%");
    api-equity-curve

The following table shows all the chart series you can plot:

ChartSeriesDescription
Strategy EquityEquityTime series of the equity curve. This series may not update daily.
ReturnTime series of daily returns. Use this series for daily updates.
Daily PerformanceTime series of daily percentage change. This series has as many data points as "Equity".
CapacityStrategy CapacityTime series of strategy capacity snapshots
DrawdownEquity DrawdownTime series of equity peak-to-trough value
BenchmarkBenchmarkTime series of the benchmark closing price (SPY, by default)
ExposureSecurityType - Long RatioTime series of the overall ratio of SecurityType long positions of the whole portfolio if any SecurityType is ever in the universe
SecurityType - Short RatioTime series of the overall ratio of SecurityType short position of the whole portfolio if any SecurityType is ever in the universe
Assets Sales VolumeEach ticker is one seriesA chart showing the proportion of total volume for each traded security.
Portfolio TurnoverPortfolio TurnoverA time series of the portfolio turnover rate.
Portfolio MarginEach ticker is one seriesA stacked area chart of the portfolio margin usage. For more information about this chart, see Portfolio Margin Plots.
Asset PlotPrice and AnnotationsA time series of an asset's price with order event annotations. For more information about these charts, see Asset Plots.
Custom ChartCustom SeriesTime series of a Series in a custom chart

Reconstruct Trades

When you stop a live algorithm, it loses its in-memory state. After you redeploy the project, the trade builder starts empty, so its ClosedTradesclosed_trades property only contains the trades of the current deployment. Follow these steps to reconstruct the complete trade list across all the live deployments of a project in the Research Environment:

#load "../Initialize.csx"
#load "../QuantConnect.csx"
  1. Define the project Id and get the deployments of the project.
  2. using QuantConnect;
    using QuantConnect.Api;
    using QuantConnect.Orders;
    using QuantConnect.Statistics;
    
    var projectId = 12345678;
    
    // Request each status separately. Requests without a status filter can time out on accounts with many deployments.
    var statuses = new[]
    {
        AlgorithmStatus.Running, AlgorithmStatus.Stopped, AlgorithmStatus.Liquidated, AlgorithmStatus.RuntimeError
    };
    var deployments = statuses
        .SelectMany(status => api.ListLiveAlgorithms(status).Algorithms)
        .Where(x => x.ProjectId == projectId)
        .OrderBy(x => x.Launched)
        .ToList();
    foreach (var deployment in deployments)
    {
        Console.WriteLine($"{deployment.DeployId}: {deployment.Launched} - {deployment.Stopped}");
    }
    project_id = 12345678
    
    # Request each status separately. Requests without a status filter can time out on accounts with many deployments.
    statuses = [
        AlgorithmStatus.RUNNING, AlgorithmStatus.STOPPED, AlgorithmStatus.LIQUIDATED, AlgorithmStatus.RUNTIME_ERROR
    ]
    deployments = sorted(
        [x for status in statuses for x in api.list_live_algorithms(status).algorithms if x.project_id == project_id],
        key=lambda x: x.launched
    )
    for deployment in deployments:
        print(f'{deployment.deploy_id}: {deployment.launched} - {deployment.stopped}')

    The following table provides links to documentation that explains how to get the project Id, depending on the platform you use:

    PlatformProject Id
    Cloud PlatformGet Project Id
    Local PlatformGet Project Id
    CLIGet Project Id

    The ListLiveAlgorithmslist_live_algorithms method returns a list of deployment summaries with the DeployIddeploy_id, Launchedlaunched, and Stoppedstopped times of each deployment. The reconstruction in the following steps doesn't need them, but each fill event carries its deployment Id, so you can use these summaries to relate the reconstructed trades to a specific deployment.

  3. Get all the orders of the project.
  4. List<ApiOrderResponse> ReadAllOrders(Func<int, int, List<ApiOrderResponse>> fetchWindow)
    {
        var all = new List<ApiOrderResponse>();
        // Retry the first window while the response is empty (may be loading).
        List<ApiOrderResponse> first = null;
        for (var attempt = 0; attempt < 10; attempt++)
        {
            first = fetchWindow(0, 100);
            if (first.Any()) break;
            Console.WriteLine($"Orders loading... (attempt {attempt + 1}/10)");
            Thread.Sleep(10000);
        }
        if (first == null || !first.Any()) return all;
        all.AddRange(first);
        // Paginate in 100-index windows until the endpoint returns an empty window.
        var start = 100;
        while (true)
        {
            var window = fetchWindow(start, start + 100);
            if (!window.Any()) break;
            all.AddRange(window);
            start += 100;
        }
        return all;
    }
    
    var orders = ReadAllOrders((s, e) => api.ReadLiveOrders(projectId, s, e));
    Console.WriteLine($"Orders: {orders.Count}");
    from time import sleep
    
    def read_all_orders(fetch_window):
        orders = []
        # Retry the first window while the response is empty (may be loading).
        first = []
        for attempt in range(10):
            first = fetch_window(0, 100)
            if first:
                break
            print(f"Orders loading... (attempt {attempt + 1}/10)")
            sleep(10)
        if not first:
            return orders
        orders.extend(first)
        # Paginate in 100-index windows until the endpoint returns an empty window.
        start = 100
        while True:
            window = fetch_window(start, start + 100)
            if not window:
                break
            orders.extend(window)
            start += 100
        return orders
    
    orders = read_all_orders(lambda s, e: api.read_live_orders(project_id, s, e))
    print(f'Orders: {len(orders)}')

    The ReadLiveOrdersread_live_orders method returns the orders of every deployment of the project, concatenated in the order the deployments launched, so a single project-level loop retrieves the complete order history. Each call returns at most 100 orders, so paginate in 100-index windows until the endpoint returns an empty window, like the Live Reconciliation tutorial does. Each element of the result is an ApiOrderResponse object that contains the order and its fill events.

  5. Replay the fill events through a new trade builder.
  6. // Use the same grouping and matching methods as the trade builder in your algorithm.
    var tradeBuilder = new TradeBuilder(FillGroupingMethod.FillToFill, FillMatchingMethod.FIFO);
    
    // The orders are already in deployment-launch order, so replay them in list order.
    foreach (var response in orders)
    {
        foreach (var serializedEvent in response.Events.Where(x => x.FillQuantity != 0))
        {
            var fill = OrderEvent.FromSerialized(serializedEvent);
            tradeBuilder.ProcessFill(fill, 1m, serializedEvent.OrderFeeAmount ?? 0m);
        }
    }
    # Use the same grouping and matching methods as the trade builder in your algorithm.
    trade_builder = TradeBuilder(FillGroupingMethod.FILL_TO_FILL, FillMatchingMethod.FIFO)
    
    # The orders are already in deployment-launch order, so replay them in list order.
    for response in orders:
        for event in [x for x in response.events if x.fill_quantity != 0]:
            fill = OrderEvent.from_serialized(event)
            trade_builder.process_fill(fill, 1, event.order_fee_amount if event.order_fee_amount else 0)

    Each fill event carries the Id of the deployment that produced it in its AlgorithmIdalgorithm_id property, so you can group the fills by deployment.

  7. Get the closed trades of the trade builder.
  8. foreach (var trade in tradeBuilder.ClosedTrades)
    {
        Console.WriteLine($"{trade.Symbols[0].Value} {trade.Direction} quantity {trade.Quantity} " +
            $"entry {trade.EntryTime:u} @ {trade.EntryPrice} exit {trade.ExitTime:u} @ {trade.ExitPrice} " +
            $"profit {trade.ProfitLoss} fees {trade.TotalFees}");
    }
    trades = trade_builder.closed_trades
    data = pd.DataFrame([{
        'symbol': trade.symbols[0].value,
        'direction': str(trade.direction),
        'quantity': trade.quantity,
        'entry_time': trade.entry_time,
        'entry_price': trade.entry_price,
        'exit_time': trade.exit_time,
        'exit_price': trade.exit_price,
        'profit_loss': trade.profit_loss,
        'total_fees': trade.total_fees
    } for trade in trades])
    data

    The ClosedTradesclosed_trades property returns a list of Trade objects, which have the following attributes:

The preceding replay passes a conversion rate of 1 and the raw fee amount of each event, so it assumes the security and its fees are denominated in your account currency and the contract multiplier is 1. Adjust these arguments if these assumptions don't hold for the products you trade. Order Ids restart at 1 for each deployment and the trade builder assigns an order fee only to the first fill of each order Id, so the total fees exclude the fees of reused order Ids in later deployments. Positions that stay open across a redeployment form complete trades only because the replay is continuous across all the deployments.

Example

Example 1: Read Live Algorithm Statistics

The following example reads the current running live algorithm's 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 current running live algorithm list in the current project.
var liveAlgorithms = api.ListLiveAlgorithms(AlgorithmStatus.Running)
var deployId = liveAlgorithms.Algorithms
    .Single(x => x.ProjectId == qb.ProjectId)
    .DeployId;
var liveAlgorithm = api.ReadLiveAlgorithm(qb.ProjectId, deployId);

// Obtain the live algorithm statistics.
Console.WriteLine(backtest.RuntimeStatistics.ToString());
# Instantiate QuantBook instance for researching.
qb = QuantBook()

# Get current running live algorithm list in the current project.
live_algorithms = api.list_live_algorithms(AlgorithmStatus.RUNNING)
deploy_id = next(
    [x for x in live_algorithms.algorithms if x.project_id == qb.project_id]
).deploy_id
live_algorithm = api.read_live_algorithm(project_id, deploy_id)

# Obtain the live algorithm statistics.
print(live_algorithm.runtime_statistics)

Example 2: Reconstruct Trades Across Deployments

The following example reconstructs the complete trade list across all the live deployments of the current project 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.Orders;
using QuantConnect.Research;
using QuantConnect.Statistics;

// Instantiate a QuantBook instance to get the project Id.
var qb = new QuantBook();
var projectId = qb.ProjectId;

// Get the deployments of the project, sorted by launch time.
// Request each status separately, since unfiltered requests can time out on large accounts.
var statuses = new[]
{
    AlgorithmStatus.Running, AlgorithmStatus.Stopped, AlgorithmStatus.Liquidated, AlgorithmStatus.RuntimeError
};
var deployments = statuses
    .SelectMany(status => api.ListLiveAlgorithms(status).Algorithms)
    .Where(x => x.ProjectId == projectId)
    .OrderBy(x => x.Launched)
    .ToList();
Console.WriteLine($"Deployments: {deployments.Count}");

// Get the first window of orders. Retry while it is empty, since the orders may still be loading.
var orders = new List<ApiOrderResponse>();
for (var attempt = 0; attempt < 10; attempt++)
{
    orders = api.ReadLiveOrders(projectId, 0, 100);
    if (orders.Any()) break;
    Thread.Sleep(10000);
}

// Get the remaining orders. A single project-level loop spans every deployment.
var start = 100;
while (true)
{
    var window = api.ReadLiveOrders(projectId, start, start + 100);
    if (!window.Any()) break;
    orders.AddRange(window);
    start += 100;
}

// Replay the fill events through a trade builder to reconstruct the trades.
var tradeBuilder = new TradeBuilder(FillGroupingMethod.FillToFill, FillMatchingMethod.FIFO);
foreach (var response in orders)
{
    foreach (var serializedEvent in response.Events.Where(x => x.FillQuantity != 0))
    {
        tradeBuilder.ProcessFill(OrderEvent.FromSerialized(serializedEvent), 1m, serializedEvent.OrderFeeAmount ?? 0m);
    }
}

// Print the closed trades of every deployment.
foreach (var trade in tradeBuilder.ClosedTrades)
{
    Console.WriteLine($"{trade.Symbols[0].Value} {trade.Direction} quantity {trade.Quantity} " +
        $"entry {trade.EntryTime:u} @ {trade.EntryPrice} exit {trade.ExitTime:u} @ {trade.ExitPrice} " +
        $"profit {trade.ProfitLoss} fees {trade.TotalFees}");
}
from time import sleep

# Instantiate a QuantBook instance to get the project Id.
qb = QuantBook()
project_id = qb.project_id

# Get the deployments of the project, sorted by launch time.
# Request each status separately, since unfiltered requests can time out on large accounts.
statuses = [
    AlgorithmStatus.RUNNING, AlgorithmStatus.STOPPED, AlgorithmStatus.LIQUIDATED, AlgorithmStatus.RUNTIME_ERROR
]
deployments = sorted(
    [x for status in statuses for x in api.list_live_algorithms(status).algorithms if x.project_id == project_id],
    key=lambda x: x.launched
)
print(f'Deployments: {len(deployments)}')

# Get the first window of orders. Retry while it is empty, since the orders may still be loading.
orders = []
for attempt in range(10):
    orders = api.read_live_orders(project_id, 0, 100)
    if orders:
        break
    sleep(10)

# Get the remaining orders. A single project-level loop spans every deployment.
start = 100
while True:
    window = api.read_live_orders(project_id, start, start + 100)
    if not window:
        break
    orders.extend(window)
    start += 100

# Replay the fill events through a trade builder to reconstruct the trades.
trade_builder = TradeBuilder(FillGroupingMethod.FILL_TO_FILL, FillMatchingMethod.FIFO)
for response in orders:
    for event in [x for x in response.events if x.fill_quantity != 0]:
        fee = event.order_fee_amount if event.order_fee_amount else 0
        trade_builder.process_fill(OrderEvent.from_serialized(event), 1, fee)

# Organize the closed trades of every deployment into a DataFrame.
trades = trade_builder.closed_trades
pd.DataFrame([{
    'symbol': trade.symbols[0].value,
    'direction': str(trade.direction),
    'quantity': trade.quantity,
    'entry_time': trade.entry_time,
    'entry_price': trade.entry_price,
    'exit_time': trade.exit_time,
    'exit_price': trade.exit_price,
    'profit_loss': trade.profit_loss,
    'total_fees': trade.total_fees
} for trade in trades])

You can also see our Videos. You can also get in touch with us via Discord.

Did you find this page helpful?

Contribute to the documentation: