fe64fada3d
- 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
199 lines
8.5 KiB
Python
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 |