Files
AlexCatarino fe64fada3d Refactors Python Version of the API
- Refactor to simplify `quantconnect.api.Api` class. Adds `Execute` method that accepts a bolean to distinguish a `POST` from a `GET` request.

In the previous version, some methods were using the `params` argument instead of `data` in the `POST` case which threw an exception for big json objects.

- Adds option to save logs and backtest report to disk.
- Adds `Result` class to optionally convert the json objects into `pandas.DataFrame` when getting backtest or live results

- Subversion bump
2019-12-11 16:10:12 +00:00

199 lines
8.5 KiB
Python

# 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.
import pandas as pd
from math import isnan
from datetime import datetime
class Result:
'''Result represents the live or backtest result of a successfully executed algorithm'''
def __init__(self, json):
'''Creates a new instance of Result'''
tag = 'result'
# LiveResults special case:
self.LiveMode = 'LiveResults' in json
if self.LiveMode:
tag += 's'
json = json.pop('LiveResults', json)
result = json.pop(tag, json)
self.Statistics = Information(result.pop('Statistics', {}))
self.AlphaRuntimeStatistics = Information(result.pop('AlphaRuntimeStatistics', {}))
self.RuntimeStatistics = Information(result.pop('RuntimeStatistics', {}))
self.ClosedTrades = self.__create_closed_trades_table(result)
self.Charts = self.__create_charts_table(result)
self.ProfitLoss = self.__create_profit_loss_table(result)
self.Orders = self.__create_order_table(result)
self.RollingWindow = self.__create_rolling_window_table(result)
self.Information = Information(json)
def __create_order_table(self, json):
'''Creates a dataframe with the orders information'''
orders = json.pop('Orders', None)
if orders is None: return None
# In Live results, orders is a list, so convert to dict keyed by Id.
if isinstance(orders, list):
orders = {x['Id']: x for x in orders}
def __status_int_to_str(value):
if value is None: return None
values = [ 'New', 'Submitted', 'PartiallyFilled', 'Filled', 'Canceled', 'None', 'Invalid', 'CancelPending' ]
return str(values) if value >= len(values) else values[value]
def __security_type_int_to_str(value):
if value is None: return None
values = [ 'Base', 'Equity', 'Option', 'Commodity', 'Forex', 'Future', 'Cfd', 'Crypto' ]
return str(values) if value >= len(values) else values[value]
def __type_int_to_str(value):
if value is None: return None
values = [ 'Market', 'Limit', 'StopMarket', 'StopLimit', 'MarketOnOpen', 'MarketOnClose', 'OptionExercise' ]
return str(values) if value >= len(values) else values[value]
columns = [
'Id', 'Time', 'SecurityType', 'Symbol', 'PriceCurrency',
'Quantity', 'Direction', 'Price', 'Type', 'Status', 'Tag',
'LastFillTime', 'LastUpdateTime', 'CanceledTime' ]
if self.LiveMode:
columns += ['DeployId']
drop_columns = [
'BrokerId', 'ContingentId', 'CreatedTime', 'IsMarketable', 'Value',
'AbsoluteQuantity', 'OrderSubmissionData', 'Properties', 'TimeInForce']
df = pd.DataFrame([v for k, v in orders.items()], columns = columns + drop_columns)
df = df.set_index('Id').drop(drop_columns, axis=1)
df['Time'] = df['Time'].apply(self.__str_to_datetime)
df['CanceledTime'] = df['CanceledTime'].apply(self.__str_to_datetime)
df['LastFillTime'] = df['LastFillTime'].apply(self.__str_to_datetime)
df['LastUpdateTime'] = df['LastUpdateTime'].apply(self.__str_to_datetime)
df['Symbol'] = df['Symbol'].apply(lambda x: x['ID'])
df['Type'] = df['Type'].apply(__type_int_to_str)
df['Direction'] = df['Direction'].apply(self.__direction_int_to_str)
df['Status'] = df['Status'].apply(__status_int_to_str)
df['SecurityType'] = df['SecurityType'].apply(__security_type_int_to_str)
return df.dropna(how='all', axis=1)
def __create_profit_loss_table(self, json):
'''Creates a dataframe with the algorithm P&L'''
profitLoss = json.pop('ProfitLoss', None)
if profitLoss is None: return None
df = pd.DataFrame({'profit_loss' : profitLoss})
df.index.name = 'time'
df.index = df.index.map(self.__str_to_datetime)
return df
def __create_closed_trades_table(self, json):
'''Creates a dataframe with the closed trades information'''
total = json.get('TotalPerformance', None)
if total is None: return None
trades = total.get('ClosedTrades', None)
if trades is None: return None
df = pd.DataFrame(trades, columns = [
'Symbol', 'Quantity', 'Direction', 'EntryTime', 'EntryPrice',
'ExitPrice', 'ExitTime', 'Duration', 'EndTradeDrawdown',
'MAE', 'MFE', 'ProfitLoss', 'TotalFees'
])
df['Symbol'] = df['Symbol'].apply(lambda x: x['ID'])
df['Direction'] = df['Direction'].apply(self.__direction_int_to_str)
df['EntryTime'] = df['EntryTime'].apply(self.__str_to_datetime)
df['ExitTime'] = df['ExitTime'].apply(self.__str_to_datetime)
df['Duration'] = df['ExitTime'] - df['EntryTime']
return df.set_index('EntryTime')
def __create_charts_table(self, json):
'''Creates a dataframe with the charts information.
By converting the json into a dataframe, it makes data visualization easier'''
charts = json.pop('Charts', None)
if charts is None: return None
df_charts = dict()
for name, chart in charts.items():
# Skip Meta data
if name == 'Meta': continue
columns = list()
for column, series in chart['Series'].items():
df = pd.DataFrame(series['Values'])
df['x'] = pd.to_datetime(df['x'], unit='s')
df = df.rename(index=str, columns={"x": "time", "y": column})
columns.append(df.set_index('time'))
if len(columns) > 1:
df = pd.concat(columns, axis = 1, sort = True)
df = df.fillna(method = 'ffill')
df = df.fillna(method = 'bfill')
df_charts[name] = df
return df_charts
def __create_rolling_window_table(self, json):
'''Creates a dataframe with the rolling statistics information.
By converting the json into a dataframe, it makes data visualization easier'''
rollingWindow = json.pop('RollingWindow', None)
if rollingWindow is None: return None
series = dict()
if 'TotalPerformance' in json:
window = json['TotalPerformance']
if window is None: window = dict()
stats = window.get('PortfolioStatistics', dict())
stats.update(window.get('TradeStatistics', dict()))
series = {'TotalPerformance': pd.Series(stats)}
for row, window in rollingWindow.items():
stats = window.get('PortfolioStatistics', dict())
stats.update(window.get('TradeStatistics', dict()))
series.update({row: pd.Series(stats)})
return pd.DataFrame(series).transpose()
def __direction_int_to_str(self, value):
if value is None: return None
return [ 'Buy', 'Sell', 'Hold' ][value]
def __str_to_datetime(self, value):
if value is None: return None
if isinstance(value, float) and isnan(value): return None
fmt = '%Y-%m-%dT%H:%M:%SZ' if len(value) == 20 else '%Y-%m-%dT%H:%M:%S.%fZ'
return datetime.strptime(value, fmt)
class Information(dict):
def __init__(self, d):
d = d if d is not None else {}
super().__init__(d)
self.__repr = ''
for k, b in d.items():
a = k.replace(' ','').replace('-','')
if isinstance(b, (list, tuple)):
setattr(self, a, [Information(x) if isinstance(x, dict) else x for x in b])
elif isinstance(b, dict):
x = Information(b)
setattr(self, a, x)
s = '\n'.join([f' {l}' for l in repr(x).splitlines()])
self.__repr += f'{a}:\n{s}\n'
else:
setattr(self, a, b)
self.__repr += f'{a}: {b}\n'
def __repr__(self):
return self.__repr