pyalgotrading 2023.7.2__py3-none-any.whl → 2023.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyalgotrading/algobulls/api.py +13 -20
- pyalgotrading/algobulls/connection.py +284 -126
- pyalgotrading/constants.py +36 -0
- pyalgotrading/strategy/__init__.py +2 -0
- pyalgotrading/strategy/strategy_options_base_v2.py +90 -0
- pyalgotrading/utils/func.py +64 -3
- {pyalgotrading-2023.7.2.dist-info → pyalgotrading-2023.10.1.dist-info}/METADATA +4 -3
- {pyalgotrading-2023.7.2.dist-info → pyalgotrading-2023.10.1.dist-info}/RECORD +11 -10
- {pyalgotrading-2023.7.2.dist-info → pyalgotrading-2023.10.1.dist-info}/WHEEL +1 -1
- {pyalgotrading-2023.7.2.dist-info → pyalgotrading-2023.10.1.dist-info}/LICENSE +0 -0
- {pyalgotrading-2023.7.2.dist-info → pyalgotrading-2023.10.1.dist-info}/top_level.txt +0 -0
pyalgotrading/algobulls/api.py
CHANGED
|
@@ -26,6 +26,7 @@ class AlgoBullsAPI:
|
|
|
26
26
|
"""
|
|
27
27
|
self.connection = connection
|
|
28
28
|
self.headers = None
|
|
29
|
+
self.page_size = 1000
|
|
29
30
|
self.__key_backtesting = {} # strategy-cstc_id mapping
|
|
30
31
|
self.__key_papertrading = {} # strategy-cstc_id mapping
|
|
31
32
|
self.__key_realtrading = {} # strategy-cstc_id mapping
|
|
@@ -335,7 +336,7 @@ class AlgoBullsAPI:
|
|
|
335
336
|
TradingType.BACKTESTING: 'backDataDate'
|
|
336
337
|
}
|
|
337
338
|
execute_config = {
|
|
338
|
-
map_trading_type_to_date_key[trading_type]: [start_timestamp.astimezone(timezone.utc).isoformat(), end_timestamp.astimezone(timezone.utc).isoformat()],
|
|
339
|
+
map_trading_type_to_date_key[trading_type]: [start_timestamp.astimezone(timezone.utc).replace(tzinfo=None).isoformat(), end_timestamp.astimezone(timezone.utc).replace(tzinfo=None).isoformat()],
|
|
339
340
|
'isLiveDataTestMode': trading_type in [TradingType.PAPERTRADING, TradingType.REALTRADING],
|
|
340
341
|
'customizationsQuantity': lots,
|
|
341
342
|
'brokingDetails': broker_details,
|
|
@@ -408,14 +409,13 @@ class AlgoBullsAPI:
|
|
|
408
409
|
|
|
409
410
|
return response
|
|
410
411
|
|
|
411
|
-
def get_logs(self, strategy_code: str, trading_type: TradingType,
|
|
412
|
+
def get_logs(self, strategy_code: str, trading_type: TradingType, initial_next_token: str = None) -> dict:
|
|
412
413
|
"""
|
|
413
414
|
Fetch logs for a strategy
|
|
414
415
|
|
|
415
416
|
Args:
|
|
416
417
|
strategy_code: Strategy code
|
|
417
418
|
trading_type: Trading type
|
|
418
|
-
log_type: type of logs, 'partial' or 'full' requests
|
|
419
419
|
initial_next_token: Token of next logs for v4 logs
|
|
420
420
|
|
|
421
421
|
Returns:
|
|
@@ -424,23 +424,17 @@ class AlgoBullsAPI:
|
|
|
424
424
|
Info: ENDPOINT
|
|
425
425
|
`POST`: v2/user/strategy/logs
|
|
426
426
|
"""
|
|
427
|
-
key = self.__get_key(strategy_code=strategy_code, trading_type=trading_type)
|
|
428
|
-
params = None
|
|
429
|
-
|
|
430
|
-
if log_type == 'partial':
|
|
431
|
-
endpoint = 'v4/user/strategy/logs'
|
|
432
|
-
json_data = {'key': key, 'nextToken': initial_next_token, 'limit': 1000, 'direction': 'forward', 'reverse': False, 'type': 'userLogs'}
|
|
433
|
-
params = {'isPythonBuild': True, 'isLive': trading_type == TradingType.REALTRADING}
|
|
434
427
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
428
|
+
key = self.__get_key(strategy_code=strategy_code, trading_type=trading_type)
|
|
429
|
+
endpoint = 'v4/user/strategy/logs'
|
|
430
|
+
json_data = {'key': key, 'nextForwardToken': initial_next_token, 'limit': self.page_size, 'direction': 'forward', 'type': 'userLogs'}
|
|
431
|
+
params = {'isPythonBuild': True, 'isLive': trading_type == TradingType.REALTRADING}
|
|
438
432
|
|
|
439
433
|
response = self._send_request(method='post', endpoint=endpoint, json_data=json_data, params=params)
|
|
440
434
|
|
|
441
435
|
return response
|
|
442
436
|
|
|
443
|
-
def get_reports(self, strategy_code: str, trading_type: TradingType, report_type: TradingReportType, country: str) -> dict:
|
|
437
|
+
def get_reports(self, strategy_code: str, trading_type: TradingType, report_type: TradingReportType, country: str, current_page: int) -> dict:
|
|
444
438
|
"""
|
|
445
439
|
Fetch report for a strategy
|
|
446
440
|
|
|
@@ -449,14 +443,13 @@ class AlgoBullsAPI:
|
|
|
449
443
|
trading_type: Value of TradingType Enum
|
|
450
444
|
report_type: Value of TradingReportType Enum
|
|
451
445
|
country: Country of the exchange
|
|
452
|
-
|
|
446
|
+
current_page: current page of data being retrieved (for order history)
|
|
453
447
|
Returns:
|
|
454
448
|
Report data
|
|
455
449
|
|
|
456
450
|
Info: ENDPOINT
|
|
457
|
-
`GET`
|
|
458
|
-
`GET`
|
|
459
|
-
`GET` v2/user/strategy/orderhistory Order History
|
|
451
|
+
`GET` v4/book/pl/data for P&L Table
|
|
452
|
+
`GET` v5/build/python/user/order/charts Order History
|
|
460
453
|
"""
|
|
461
454
|
|
|
462
455
|
key = self.__get_key(strategy_code=strategy_code, trading_type=trading_type)
|
|
@@ -465,8 +458,8 @@ class AlgoBullsAPI:
|
|
|
465
458
|
endpoint = f'v4/book/pl/data'
|
|
466
459
|
params = {'pageSize': 0, 'isPythonBuild': "true", 'strategyId': strategy_code, 'isLive': trading_type is TradingType.REALTRADING, 'country': country, 'filters': _filter}
|
|
467
460
|
elif report_type is TradingReportType.ORDER_HISTORY:
|
|
468
|
-
endpoint = '
|
|
469
|
-
params = {'
|
|
461
|
+
endpoint = 'v5/build/python/user/order/charts'
|
|
462
|
+
params = {'strategyId': strategy_code, 'country': country, 'currentPage': current_page, 'pageSize': self.page_size, 'isLive': trading_type is TradingType.REALTRADING}
|
|
470
463
|
else:
|
|
471
464
|
raise NotImplementedError
|
|
472
465
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Module for AlgoBulls connection
|
|
3
3
|
"""
|
|
4
4
|
import inspect
|
|
5
|
+
import os
|
|
5
6
|
import pprint
|
|
6
7
|
import re
|
|
7
8
|
import time
|
|
@@ -14,10 +15,10 @@ from tabulate import tabulate
|
|
|
14
15
|
from tqdm.auto import tqdm
|
|
15
16
|
|
|
16
17
|
from .api import AlgoBullsAPI
|
|
17
|
-
from .exceptions import AlgoBullsAPIBadRequestException, AlgoBullsAPIGatewayTimeoutErrorException
|
|
18
|
-
from ..constants import StrategyMode, TradingType, TradingReportType, CandleInterval, AlgoBullsEngineVersion, Country, ExecutionStatus, EXCHANGE_LOCALE_MAP, Locale
|
|
18
|
+
from .exceptions import AlgoBullsAPIBadRequestException, AlgoBullsAPIGatewayTimeoutErrorException, AlgoBullsAPIUnauthorizedErrorException
|
|
19
|
+
from ..constants import StrategyMode, TradingType, TradingReportType, CandleInterval, AlgoBullsEngineVersion, Country, ExecutionStatus, EXCHANGE_LOCALE_MAP, Locale, CandleIntervalSecondsMap
|
|
19
20
|
from ..strategy.strategy_base import StrategyBase
|
|
20
|
-
from ..utils.func import get_valid_enum_names, get_datetime_with_tz
|
|
21
|
+
from ..utils.func import get_valid_enum_names, get_datetime_with_tz, calculate_brokerage, calculate_slippage
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class AlgoBullsConnection:
|
|
@@ -68,19 +69,27 @@ class AlgoBullsConnection:
|
|
|
68
69
|
url = 'https://app.algobulls.com/settings?section=developerOptions'
|
|
69
70
|
print(f'Please login to this URL to get your unique token: {url}')
|
|
70
71
|
|
|
71
|
-
def set_access_token(self, access_token):
|
|
72
|
+
def set_access_token(self, access_token, validate_token=True):
|
|
72
73
|
"""
|
|
73
74
|
Set the access token
|
|
74
75
|
|
|
75
76
|
Args:
|
|
76
77
|
access_token: access token
|
|
77
|
-
|
|
78
|
+
validate_token: whether you want to validate the access-token
|
|
78
79
|
Returns:
|
|
79
80
|
None
|
|
80
81
|
"""
|
|
81
82
|
assert isinstance(access_token, str), f'Argument "access_token" should be a string'
|
|
82
83
|
self.api.set_access_token(access_token)
|
|
83
84
|
|
|
85
|
+
if validate_token:
|
|
86
|
+
try:
|
|
87
|
+
_ = self.api.get_all_strategies()
|
|
88
|
+
print("Access token is valid.")
|
|
89
|
+
except AlgoBullsAPIUnauthorizedErrorException:
|
|
90
|
+
print(f"Access token is invalid. ", end='')
|
|
91
|
+
self.get_token_url()
|
|
92
|
+
|
|
84
93
|
def create_strategy(self, strategy, overwrite=False, strategy_code=None, abc_version=None):
|
|
85
94
|
"""
|
|
86
95
|
Method to upload new strategy.
|
|
@@ -270,89 +279,121 @@ class AlgoBullsConnection:
|
|
|
270
279
|
assert isinstance(strategy_code, str), f'Argument "strategy_code" should be a string'
|
|
271
280
|
assert isinstance(trading_type, TradingType), f'Argument "trading_type" should be an enum of type {TradingType.__name__}'
|
|
272
281
|
|
|
273
|
-
|
|
282
|
+
self.api.stop_strategy_algotrading(strategy_code=strategy_code, trading_type=trading_type)
|
|
274
283
|
|
|
275
|
-
def get_logs(self, strategy_code, trading_type,
|
|
284
|
+
def get_logs(self, strategy_code, trading_type, display_progress_bar=True, print_live_logs=False):
|
|
276
285
|
"""
|
|
277
286
|
Fetch logs for a strategy
|
|
278
287
|
|
|
279
288
|
Args:
|
|
280
289
|
strategy_code: strategy code
|
|
281
290
|
trading_type: trading type
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
291
|
+
display_progress_bar: to track the execution progress bar as your strategy is executed
|
|
292
|
+
print_live_logs: to print the logs as they are fetched
|
|
285
293
|
Returns:
|
|
286
294
|
Execution logs
|
|
287
295
|
"""
|
|
288
296
|
|
|
289
297
|
assert isinstance(strategy_code, str), f'Argument "strategy_code" should be a string'
|
|
290
298
|
assert isinstance(trading_type, TradingType), f'Argument "trading_type" should be an enum of type {TradingType.__name__}'
|
|
291
|
-
assert isinstance(auto_update, bool), f'Argument "show_progress_bar" should be a boolean'
|
|
292
299
|
|
|
293
300
|
# TODO: to extract timestamp from a different source which will be independent of whether save parameters are present in the object
|
|
294
301
|
start_timestamp_map = self.saved_parameters.get('start_timestamp_map')
|
|
295
302
|
end_timestamp_map = self.saved_parameters.get('end_timestamp_map')
|
|
296
|
-
|
|
303
|
+
sleep_time = 1
|
|
304
|
+
total_seconds = 1
|
|
297
305
|
|
|
298
|
-
#
|
|
299
|
-
if
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
306
|
+
# calculate the sleep time for RT and PT
|
|
307
|
+
if trading_type is not TradingType.BACKTESTING:
|
|
308
|
+
try:
|
|
309
|
+
sleep_time = CandleIntervalSecondsMap[self.saved_parameters.get('candle_interval').value]
|
|
310
|
+
print(f"Your candle interval is {self.saved_parameters.get('candle_interval').value}, therefore logs will be fetched every {sleep_time} seconds.")
|
|
311
|
+
except AttributeError:
|
|
312
|
+
print("WARNING: Could not fetch the candle interval from saved parameters. Logs will be fetched every 60 seconds.")
|
|
313
|
+
sleep_time = 60
|
|
314
|
+
|
|
315
|
+
# initialize the parameters required for displaying progress bar
|
|
316
|
+
if display_progress_bar:
|
|
317
|
+
if start_timestamp_map.get(trading_type) and end_timestamp_map.get(trading_type):
|
|
318
|
+
start_timestamp = start_timestamp_map.get(trading_type).replace(tzinfo=None)
|
|
319
|
+
end_timestamp = end_timestamp_map.get(trading_type).replace(tzinfo=None)
|
|
320
|
+
total_seconds = (end_timestamp - start_timestamp).total_seconds()
|
|
321
|
+
else:
|
|
322
|
+
display_progress_bar = False
|
|
308
323
|
|
|
309
|
-
|
|
310
|
-
|
|
324
|
+
# initialize all the variables
|
|
325
|
+
all_logs = ''
|
|
326
|
+
tqdm_progress_bar = None
|
|
327
|
+
initial_next_token = None
|
|
328
|
+
error_counter = 0
|
|
329
|
+
status = None
|
|
330
|
+
count_starting_status = 0
|
|
331
|
+
response = {}
|
|
332
|
+
logs = []
|
|
333
|
+
|
|
334
|
+
while True:
|
|
335
|
+
# if logs are in starting phase, we wait until it changes
|
|
336
|
+
if status is None or status == ExecutionStatus.STARTING.value:
|
|
337
|
+
status = self.get_job_status(strategy_code, trading_type)["message"]
|
|
338
|
+
time.sleep(5)
|
|
339
|
+
|
|
340
|
+
# log the counting for "STARTING" phase of execution
|
|
341
|
+
if status == ExecutionStatus.STARTING.value:
|
|
342
|
+
count_starting_status += 1
|
|
343
|
+
print('\r', end=f'Looking for a dedicated virtual server to execute your strategy... ({count_starting_status})')
|
|
344
|
+
continue
|
|
345
|
+
|
|
346
|
+
# if logs get in started phase, we initialize the tqdm object for progress tracking
|
|
347
|
+
if display_progress_bar:
|
|
348
|
+
if tqdm_progress_bar is None and status == ExecutionStatus.STARTED.value:
|
|
349
|
+
tqdm_progress_bar = tqdm(desc='Execution Progress', total=total_seconds, position=0, leave=True, bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]')
|
|
311
350
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
351
|
+
# try the logs API 5 times or until it succeeds
|
|
352
|
+
for i in range(5):
|
|
353
|
+
try:
|
|
354
|
+
response = self.api.get_logs(strategy_code=strategy_code, trading_type=trading_type, initial_next_token=initial_next_token)
|
|
355
|
+
logs = response.get('data')
|
|
356
|
+
if logs:
|
|
357
|
+
break
|
|
358
|
+
except AlgoBullsAPIGatewayTimeoutErrorException:
|
|
359
|
+
tqdm.write(f"\n{'----' * 10}\nFaced an error while fetching logs.\n{'----' * 10}\n")
|
|
315
360
|
time.sleep(5)
|
|
316
|
-
if status == ExecutionStatus.STARTING.value:
|
|
317
|
-
count_starting_status += 1
|
|
318
|
-
print('\r', end=f'Looking for a dedicated virtual server to execute your strategy... ({count_starting_status})')
|
|
319
|
-
continue
|
|
320
361
|
|
|
321
|
-
|
|
322
|
-
if tqdm_progress_bar is None and status == ExecutionStatus.STARTED.value:
|
|
323
|
-
tqdm_progress_bar = tqdm(desc='Execution Progress', total=total_seconds, position=0, leave=True, bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]')
|
|
362
|
+
initial_next_token = response.get('nextForwardToken')
|
|
324
363
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
all_logs += '\n'.join(logs) + '\n'
|
|
364
|
+
# if logs are empty we validate the status of execution
|
|
365
|
+
if not logs:
|
|
366
|
+
status = self.get_job_status(strategy_code, trading_type)["message"]
|
|
329
367
|
|
|
330
|
-
# if
|
|
331
|
-
if
|
|
332
|
-
status = self.get_job_status(strategy_code, trading_type)["message"]
|
|
368
|
+
# if status is stopped we break the while loop
|
|
369
|
+
if status in [ExecutionStatus.STOPPED.value, ExecutionStatus.STOPPING.value]:
|
|
333
370
|
|
|
334
|
-
#
|
|
335
|
-
if
|
|
336
|
-
# tqdm.write(f'INFO: Got status as {status}, strategy execution completed.') # for debug
|
|
371
|
+
# tqdm.write(f"INFO: Got status as {status}, strategy execution completed.") # for debug
|
|
372
|
+
if display_progress_bar:
|
|
337
373
|
if tqdm_progress_bar is not None:
|
|
338
374
|
tqdm_progress_bar.close()
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
# continue if logs are not fetched
|
|
342
|
-
else:
|
|
343
|
-
# tqdm.write(f'WARNING: got no data, current status is {status}...') # for debug
|
|
344
|
-
time.sleep(5)
|
|
345
|
-
continue
|
|
375
|
+
break
|
|
346
376
|
|
|
377
|
+
# continue if logs are not fetched
|
|
347
378
|
else:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
379
|
+
time.sleep(5)
|
|
380
|
+
|
|
381
|
+
# tqdm.write(f"WARNING: got no data, current status is {status}...") # for debug
|
|
382
|
+
continue
|
|
383
|
+
|
|
384
|
+
else:
|
|
385
|
+
# incoming logs are in list, merge them to string
|
|
386
|
+
if type(logs) is list and initial_next_token:
|
|
387
|
+
all_logs += ''.join(logs)
|
|
351
388
|
|
|
352
|
-
|
|
389
|
+
# print the logs below progressbar
|
|
390
|
+
if print_live_logs:
|
|
391
|
+
tqdm.write(''.join(logs))
|
|
392
|
+
|
|
393
|
+
# iterate in reverse order
|
|
394
|
+
if display_progress_bar:
|
|
353
395
|
for log in logs[::-1]:
|
|
354
396
|
try:
|
|
355
|
-
|
|
356
397
|
# extract log terms inside square brackets
|
|
357
398
|
_ = re.findall(r'\[(.*?)\]', log)
|
|
358
399
|
|
|
@@ -367,23 +408,25 @@ class AlgoBullsConnection:
|
|
|
367
408
|
tqdm.write(f'WARNING: faced an error while updating logs process. Error: {ex}')
|
|
368
409
|
error_counter += 1
|
|
369
410
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
411
|
+
# avoid infinite loop in case of error
|
|
412
|
+
if error_counter > 5:
|
|
413
|
+
break
|
|
414
|
+
|
|
415
|
+
if len(logs) >= self.api.page_size:
|
|
416
|
+
# tqdm.write(f"\n{'-----' * 5}\nWaiting {sleep_time} seconds for fetching next logs ...\n{'-----' * 5}\n") # for debug
|
|
417
|
+
time.sleep(sleep_time)
|
|
418
|
+
else:
|
|
419
|
+
time.sleep(1)
|
|
373
420
|
|
|
374
|
-
initial_next_token = response.get('initialNextToken')
|
|
375
|
-
else:
|
|
376
|
-
all_logs = self.api.get_logs(strategy_code=strategy_code, trading_type=trading_type, log_type='full').get('data')
|
|
377
421
|
return all_logs
|
|
378
422
|
|
|
379
|
-
def
|
|
423
|
+
def get_report_order_history(self, strategy_code, trading_type, render_as_dataframe=False, show_all_rows=True, country=None):
|
|
380
424
|
"""
|
|
381
425
|
Fetch report for a strategy
|
|
382
426
|
|
|
383
427
|
Args:
|
|
384
428
|
strategy_code: Strategy code
|
|
385
429
|
trading_type: Value of TradingType Enum
|
|
386
|
-
report_type: Value of TradingReportType Enum
|
|
387
430
|
render_as_dataframe: True or False
|
|
388
431
|
show_all_rows: True or False
|
|
389
432
|
country: country of the Exchange
|
|
@@ -394,23 +437,75 @@ class AlgoBullsConnection:
|
|
|
394
437
|
|
|
395
438
|
assert isinstance(strategy_code, str), f'Argument "strategy_code" should be a string'
|
|
396
439
|
assert isinstance(trading_type, TradingType), f'Argument "trading_type" should be an enum of type {TradingType.__name__}'
|
|
397
|
-
assert isinstance(report_type, TradingReportType), f'Argument "report_type" should be an enum of type {TradingReportType.__name__}'
|
|
398
440
|
assert isinstance(render_as_dataframe, bool), f'Argument "render_as_dataframe" should be a bool'
|
|
399
441
|
assert isinstance(show_all_rows, bool), f'Argument "show_all_rows" should be a bool'
|
|
400
442
|
# assert (broker is None or isinstance(broker, AlgoBullsSupportedBrokers) is True), f'Argument broker should be None or an enum of type {AlgoBullsSupportedBrokers.__name__}'
|
|
401
|
-
|
|
402
|
-
|
|
443
|
+
|
|
444
|
+
main_data = []
|
|
445
|
+
total_data = 0
|
|
446
|
+
_total = 0
|
|
447
|
+
i = 1
|
|
448
|
+
|
|
449
|
+
if country is None:
|
|
450
|
+
country = self.strategy_country_map[trading_type].get(strategy_code, Country.DEFAULT.value)
|
|
451
|
+
|
|
452
|
+
while True:
|
|
453
|
+
for _ in range(5):
|
|
454
|
+
try:
|
|
455
|
+
response = self.api.get_reports(strategy_code=strategy_code, trading_type=trading_type, report_type=TradingReportType.ORDER_HISTORY, country=country, current_page=i)
|
|
456
|
+
_total = response.get("totalTrades")
|
|
457
|
+
_data = response.get("data")
|
|
458
|
+
|
|
459
|
+
# if data is retrieved as a list then update the main data
|
|
460
|
+
if _data and isinstance(_data, list):
|
|
461
|
+
main_data.extend(_data)
|
|
462
|
+
total_data += self.api.page_size
|
|
463
|
+
i += 1
|
|
464
|
+
break
|
|
465
|
+
|
|
466
|
+
except AlgoBullsAPIGatewayTimeoutErrorException:
|
|
467
|
+
time.sleep(5)
|
|
468
|
+
|
|
469
|
+
# break if total requested data is more than total data
|
|
470
|
+
if total_data > _total:
|
|
471
|
+
break
|
|
472
|
+
|
|
473
|
+
if main_data:
|
|
474
|
+
|
|
475
|
+
# for rendering as dataframe
|
|
403
476
|
if render_as_dataframe:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
477
|
+
pandas_dataframe_all_rows()
|
|
478
|
+
|
|
479
|
+
# explode the "customer_tradebook_states" column to get separate rows for every state
|
|
480
|
+
_response = pd.DataFrame(main_data)
|
|
481
|
+
df = _response.explode('customer_tradebook_states').reset_index(drop=True)
|
|
482
|
+
df = df.join(pd.json_normalize(df.pop('customer_tradebook_states')))
|
|
483
|
+
_response = df.set_index("orderId").sort_values("timestamp_created")[["timestamp_created", "transaction_type", "state", "instrument", "quantity", "currency", "price"]]
|
|
484
|
+
|
|
485
|
+
# for rendering as string or json
|
|
407
486
|
else:
|
|
408
|
-
_response =
|
|
487
|
+
_response = main_data
|
|
488
|
+
main_order_string = ""
|
|
489
|
+
|
|
490
|
+
for i in range(len(main_data)):
|
|
491
|
+
# tables for displaying order details and order stats
|
|
492
|
+
order_detail = [
|
|
493
|
+
["Order ID", main_data[i]["orderId"]],
|
|
494
|
+
["Transaction Type", main_data[i]["transaction_type"]],
|
|
495
|
+
["Instrument", main_data[i]["instrument"]],
|
|
496
|
+
["Quantity", main_data[i]["quantity"]],
|
|
497
|
+
["Price", str(main_data[i]["currency"]) + str(main_data[i]["price"])]
|
|
498
|
+
]
|
|
499
|
+
main_order_string += tabulate(order_detail, tablefmt="psql") + "\n"
|
|
500
|
+
main_order_string += tabulate(main_data[i]["customer_tradebook_states"], headers="keys", tablefmt="psql") + "\n"
|
|
501
|
+
main_order_string += '\n' + '======' * 15 + '\n'
|
|
502
|
+
|
|
503
|
+
_response = main_order_string
|
|
409
504
|
return _response
|
|
410
505
|
else:
|
|
411
|
-
print(
|
|
506
|
+
print("Report not available yet. Please retry in sometime")
|
|
412
507
|
|
|
413
|
-
def
|
|
508
|
+
def get_report_pnl_table(self, strategy_code, trading_type, country, brokerage_percentage=None, brokerage_flat_price=None, slippage_percent=None, show_all_rows=True):
|
|
414
509
|
"""
|
|
415
510
|
Fetch BT/PT/RT Profit & Loss details
|
|
416
511
|
|
|
@@ -418,6 +513,10 @@ class AlgoBullsConnection:
|
|
|
418
513
|
strategy_code: strategy code
|
|
419
514
|
trading_type: type of trades : Backtesting, Papertrading, Realtrading
|
|
420
515
|
country: country of the exchange
|
|
516
|
+
brokerage_percentage: Percentage of broker commission per trade
|
|
517
|
+
brokerage_flat_price: Broker fee per trade
|
|
518
|
+
slippage_percent: percentage of slippage per order
|
|
519
|
+
show_all_rows: show all rows of the dataframe returned
|
|
421
520
|
|
|
422
521
|
Returns:
|
|
423
522
|
Report details
|
|
@@ -425,11 +524,16 @@ class AlgoBullsConnection:
|
|
|
425
524
|
|
|
426
525
|
assert isinstance(strategy_code, str), f'Argument "strategy_code" should be a string'
|
|
427
526
|
|
|
428
|
-
#
|
|
527
|
+
# Set the country code
|
|
429
528
|
if country is None:
|
|
430
529
|
country = self.strategy_country_map[trading_type].get(strategy_code, Country.DEFAULT.value)
|
|
431
530
|
|
|
432
|
-
|
|
531
|
+
# Fetch the data
|
|
532
|
+
response = self.api.get_reports(strategy_code=strategy_code, trading_type=trading_type, report_type=TradingReportType.PNL_TABLE, country=country, current_page=1)
|
|
533
|
+
data = response.get("data")
|
|
534
|
+
|
|
535
|
+
if show_all_rows:
|
|
536
|
+
pandas_dataframe_all_rows()
|
|
433
537
|
|
|
434
538
|
# Post-processing: Cleanup & converting data to dataframe
|
|
435
539
|
column_rename_map = OrderedDict([
|
|
@@ -447,6 +551,7 @@ class AlgoBullsConnection:
|
|
|
447
551
|
('exit.price', 'exit_price'),
|
|
448
552
|
('pnlAbsolute.value', 'pnl_absolute')
|
|
449
553
|
])
|
|
554
|
+
|
|
450
555
|
if data:
|
|
451
556
|
# Generate df from json data & perform cleanups
|
|
452
557
|
_df = pd.json_normalize(data[::-1])[list(column_rename_map.keys())].rename(columns=column_rename_map)
|
|
@@ -455,13 +560,19 @@ class AlgoBullsConnection:
|
|
|
455
560
|
_df['exit_transaction_type'] = _df['exit_transaction_type'].apply(lambda _: 'BUY' if _ else 'SELL')
|
|
456
561
|
_df["pnl_cumulative_absolute"] = _df["pnl_absolute"].cumsum(axis=0, skipna=True)
|
|
457
562
|
|
|
563
|
+
# generate slippage data
|
|
564
|
+
if slippage_percent:
|
|
565
|
+
_df = calculate_slippage(pnl_df=_df, slippage_percent=slippage_percent)
|
|
566
|
+
|
|
567
|
+
_df = calculate_brokerage(pnl_df=_df, brokerage_percentage=brokerage_percentage, brokerage_flat_price=brokerage_flat_price)
|
|
458
568
|
else:
|
|
459
569
|
# No data available, send back an empty dataframe
|
|
460
570
|
_df = pd.DataFrame(columns=list(column_rename_map.values()))
|
|
571
|
+
_df['net_pnl'] = None
|
|
461
572
|
|
|
462
573
|
return _df
|
|
463
574
|
|
|
464
|
-
def get_report_statistics(self, strategy_code, initial_funds, report, html_dump, pnl_df):
|
|
575
|
+
def get_report_statistics(self, strategy_code=None, initial_funds=None, report="full", html_dump=True, pnl_df=None, file_path="None", date_time_format="%Y-%m-%d %H:%M:%S%z"):
|
|
465
576
|
"""
|
|
466
577
|
Fetch BT/PT/RT report statistics
|
|
467
578
|
|
|
@@ -469,19 +580,45 @@ class AlgoBullsConnection:
|
|
|
469
580
|
strategy_code: strategy code
|
|
470
581
|
report: format and content of the report
|
|
471
582
|
html_dump: save it as a html file
|
|
472
|
-
pnl_df: dataframe containing pnl reports
|
|
583
|
+
pnl_df: dataframe containing pnl reports; this parameter will be ignored if file_path is provided
|
|
473
584
|
initial_funds: initial funds to before starting the job
|
|
585
|
+
file_path: file path of the csv or xlsx containing pnl data for statistics; if provided, pnl_df would be ignored
|
|
586
|
+
date_time_format: datetime format of the column inside the "entry_timestamp" column in the csv or xlxs file
|
|
474
587
|
Returns:
|
|
475
588
|
Report details
|
|
476
589
|
"""
|
|
477
590
|
|
|
591
|
+
# read and validate the csv given in path file
|
|
592
|
+
if os.path.isfile(file_path):
|
|
593
|
+
# only accept csv or xlxs files
|
|
594
|
+
_, _ext = os.path.splitext(file_path)
|
|
595
|
+
if _ext == '.csv':
|
|
596
|
+
pnl_df = pd.read_csv(file_path)
|
|
597
|
+
elif _ext == '.xlxs':
|
|
598
|
+
pnl_df = pd.read_excel(file_path)
|
|
599
|
+
else:
|
|
600
|
+
raise Exception(f'ERROR: File with extension {_ext} is not supported.\n Please provide path to files with extension as ".csv" or ".xlxs"')
|
|
601
|
+
|
|
602
|
+
# handle the exceptions gracefully, check the validity of the input file
|
|
603
|
+
if "entry_timestamp" not in pnl_df.columns or "net_pnl" not in pnl_df.columns:
|
|
604
|
+
raise Exception(f"ERROR: Given {_ext} file does not have the required columns 'entry_timestamp' and 'net_pnl'.")
|
|
605
|
+
try:
|
|
606
|
+
dt.strptime(pnl_df.iloc[0]["entry_timestamp"], date_time_format)
|
|
607
|
+
except ValueError:
|
|
608
|
+
raise ValueError(f"ERROR: Datetime strings inside 'entry_timestamp' column should be of the format {date_time_format}.")
|
|
609
|
+
|
|
610
|
+
# cleanup dataframe generated from read files
|
|
611
|
+
pnl_df[['entry_timestamp']] = pnl_df[['entry_timestamp']].apply(pd.to_datetime, format=date_time_format, errors="coerce")
|
|
612
|
+
|
|
478
613
|
order_report = None
|
|
614
|
+
if initial_funds is None:
|
|
615
|
+
initial_funds = self.saved_parameters.get("initial_funds_virtual") or 1e9
|
|
479
616
|
|
|
480
617
|
# get pnl data and cleanup as per quantstats format
|
|
481
|
-
_returns_df = pnl_df[['entry_timestamp', '
|
|
618
|
+
_returns_df = pnl_df[['entry_timestamp', 'net_pnl']]
|
|
482
619
|
_returns_df['entry_timestamp'] = _returns_df['entry_timestamp'].dt.tz_localize(None) # Note: Quantstats has a bug. It doesn't accept the df index, which is set below, with timezone. Hence, we have to drop the timezone info
|
|
483
620
|
_returns_df = _returns_df.set_index('entry_timestamp')
|
|
484
|
-
_returns_df["total_funds"] = _returns_df.
|
|
621
|
+
_returns_df["total_funds"] = _returns_df.net_pnl.cumsum() + initial_funds
|
|
485
622
|
_returns_df = _returns_df.dropna()
|
|
486
623
|
|
|
487
624
|
# Note: Quantstats has a potential bug. It cannot work with multiple entries having the same timestamp. For now, we are dropping multiple entries with the same entry_timestamp (else the quantstats code below would throw an error)
|
|
@@ -492,15 +629,22 @@ class AlgoBullsConnection:
|
|
|
492
629
|
total_funds_series = _returns_df.total_funds
|
|
493
630
|
|
|
494
631
|
# select report type
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
632
|
+
try:
|
|
633
|
+
if report == "metrics":
|
|
634
|
+
order_report = qs.reports.metrics(total_funds_series)
|
|
635
|
+
elif report == "full":
|
|
636
|
+
order_report = qs.reports.full(total_funds_series)
|
|
637
|
+
except ZeroDivisionError:
|
|
638
|
+
raise Exception("ERROR: PnL data generated is too less to perform statistical analysis")
|
|
499
639
|
|
|
500
640
|
# save as html file
|
|
501
641
|
if html_dump:
|
|
502
|
-
|
|
503
|
-
|
|
642
|
+
# if there is an error in calling the API, give a default name to html file.
|
|
643
|
+
try:
|
|
644
|
+
all_strategies = self.get_all_strategies()
|
|
645
|
+
strategy_name = all_strategies.loc[all_strategies['strategyCode'] == strategy_code]['strategyName'].iloc[0]
|
|
646
|
+
except Exception:
|
|
647
|
+
strategy_name = 'strategy_results'
|
|
504
648
|
qs.reports.html(total_funds_series, title=strategy_name, output='', download_filename=f'report_{strategy_name}_{time.time():.0f}.html')
|
|
505
649
|
|
|
506
650
|
return order_report
|
|
@@ -602,6 +746,8 @@ class AlgoBullsConnection:
|
|
|
602
746
|
candle_interval = CandleInterval[_]
|
|
603
747
|
if isinstance(instruments, str):
|
|
604
748
|
instruments = [instruments]
|
|
749
|
+
if strategy_parameters == {} or strategy_parameters is None:
|
|
750
|
+
strategy_parameters = dict()
|
|
605
751
|
|
|
606
752
|
# Sanity checks - Validate config parameters
|
|
607
753
|
assert isinstance(strategy_code, str), f'Argument "strategy" should be a valid string'
|
|
@@ -730,7 +876,7 @@ class AlgoBullsConnection:
|
|
|
730
876
|
"""
|
|
731
877
|
|
|
732
878
|
# start backtesting job
|
|
733
|
-
|
|
879
|
+
_ = self.start_job(
|
|
734
880
|
strategy_code=strategy, start_timestamp=start, end_timestamp=end, instruments=instruments, lots=lots, strategy_parameters=parameters, candle_interval=candle, strategy_mode=mode,
|
|
735
881
|
initial_funds_virtual=initial_funds_virtual, delete_previous_trades=delete_previous_trades, trading_type=TradingType.BACKTESTING, broking_details=vendor_details, **kwargs
|
|
736
882
|
)
|
|
@@ -768,50 +914,53 @@ class AlgoBullsConnection:
|
|
|
768
914
|
|
|
769
915
|
return self.stop_job(strategy_code=strategy_code, trading_type=TradingType.BACKTESTING)
|
|
770
916
|
|
|
771
|
-
def get_backtesting_logs(self, strategy_code,
|
|
917
|
+
def get_backtesting_logs(self, strategy_code, display_progress_bar=True, print_live_logs=False):
|
|
772
918
|
"""
|
|
773
919
|
Fetch Back Testing logs
|
|
774
920
|
|
|
775
921
|
Args:
|
|
776
922
|
strategy_code: Strategy code
|
|
777
|
-
|
|
778
|
-
|
|
923
|
+
display_progress_bar: to track the execution on progress bar as your strategy is executed
|
|
924
|
+
print_live_logs: to print the live logs as your strategy is executed
|
|
779
925
|
|
|
780
926
|
Returns:
|
|
781
927
|
Report details
|
|
782
928
|
"""
|
|
783
929
|
|
|
784
930
|
assert isinstance(strategy_code, str), f'Argument "strategy_code" should be a string'
|
|
931
|
+
assert isinstance(display_progress_bar, bool), f'Argument "display_progress_bar" should be a boolean'
|
|
932
|
+
assert isinstance(print_live_logs, bool), f'Argument "print_live_logs" should be a boolean'
|
|
785
933
|
|
|
786
|
-
return self.get_logs(strategy_code, trading_type=TradingType.BACKTESTING,
|
|
934
|
+
return self.get_logs(strategy_code, trading_type=TradingType.BACKTESTING, display_progress_bar=display_progress_bar, print_live_logs=print_live_logs)
|
|
787
935
|
|
|
788
|
-
def get_backtesting_report_pnl_table(self, strategy_code, country=None,
|
|
936
|
+
def get_backtesting_report_pnl_table(self, strategy_code, country=None, force_fetch=False, broker_commission_percentage=None, broker_commission_price=None, slippage_percent=None):
|
|
789
937
|
"""
|
|
790
938
|
Fetch Back Testing Profit & Loss details
|
|
791
939
|
|
|
792
940
|
Args:
|
|
793
941
|
strategy_code: strategy code
|
|
794
942
|
country: country of Exchange
|
|
795
|
-
show_all_rows: True or False
|
|
796
943
|
force_fetch: Forcefully fetch PnL data
|
|
944
|
+
broker_commission_percentage: Percentage of broker commission per trade
|
|
945
|
+
broker_commission_price: Broker fee per trade
|
|
946
|
+
slippage_percent: Slippage percentage value
|
|
797
947
|
|
|
798
948
|
Returns:
|
|
799
949
|
Report details
|
|
800
950
|
"""
|
|
801
951
|
|
|
802
952
|
if self.backtesting_pnl_data is None or country is not None or force_fetch:
|
|
803
|
-
self.backtesting_pnl_data = self.
|
|
953
|
+
self.backtesting_pnl_data = self.get_report_pnl_table(strategy_code, TradingType.BACKTESTING, country, broker_commission_percentage, broker_commission_price, slippage_percent)
|
|
804
954
|
|
|
805
955
|
return self.backtesting_pnl_data
|
|
806
956
|
|
|
807
|
-
def get_backtesting_report_statistics(self, strategy_code, initial_funds=
|
|
957
|
+
def get_backtesting_report_statistics(self, strategy_code, initial_funds=None, report='metrics', html_dump=False):
|
|
808
958
|
"""
|
|
809
959
|
Fetch Back Testing report statistics
|
|
810
960
|
|
|
811
961
|
Args:
|
|
812
962
|
strategy_code: strategy code
|
|
813
963
|
initial_funds: initial funds that were set before backtesting
|
|
814
|
-
mode: extension used to generate statistics
|
|
815
964
|
report: format and content of the report
|
|
816
965
|
html_dump: save it as a html file
|
|
817
966
|
|
|
@@ -830,20 +979,21 @@ class AlgoBullsConnection:
|
|
|
830
979
|
|
|
831
980
|
return order_report
|
|
832
981
|
|
|
833
|
-
def get_backtesting_report_order_history(self, strategy_code):
|
|
982
|
+
def get_backtesting_report_order_history(self, strategy_code, country=None, render_as_dataframe=False):
|
|
834
983
|
"""
|
|
835
984
|
Fetch Back Testing order history
|
|
836
985
|
|
|
837
986
|
Args:
|
|
838
987
|
strategy_code: strategy code
|
|
839
|
-
|
|
988
|
+
country: country of the segment
|
|
989
|
+
render_as_dataframe: return order history as dataframe or pretty string
|
|
840
990
|
Returns:
|
|
841
991
|
Report details
|
|
842
992
|
"""
|
|
843
993
|
|
|
844
994
|
assert isinstance(strategy_code, str), f'Argument "strategy_code" should be a string'
|
|
845
995
|
|
|
846
|
-
return self.
|
|
996
|
+
return self.get_report_order_history(strategy_code=strategy_code, trading_type=TradingType.BACKTESTING, render_as_dataframe=render_as_dataframe, country=country)
|
|
847
997
|
|
|
848
998
|
def papertrade(self, strategy=None, start=None, end=None, instruments=None, lots=None, parameters=None, candle=None, mode=None, delete_previous_trades=True, initial_funds_virtual=None, vendor_details=None, **kwargs):
|
|
849
999
|
"""
|
|
@@ -876,7 +1026,7 @@ class AlgoBullsConnection:
|
|
|
876
1026
|
"""
|
|
877
1027
|
|
|
878
1028
|
# start papertrading job
|
|
879
|
-
|
|
1029
|
+
_ = self.start_job(
|
|
880
1030
|
strategy_code=strategy, start_timestamp=start, end_timestamp=end, instruments=instruments, lots=lots, strategy_parameters=parameters, candle_interval=candle, strategy_mode=mode,
|
|
881
1031
|
initial_funds_virtual=initial_funds_virtual, delete_previous_trades=delete_previous_trades, trading_type=TradingType.PAPERTRADING, broking_details=vendor_details, **kwargs
|
|
882
1032
|
)
|
|
@@ -914,50 +1064,53 @@ class AlgoBullsConnection:
|
|
|
914
1064
|
|
|
915
1065
|
return self.stop_job(strategy_code=strategy_code, trading_type=TradingType.PAPERTRADING)
|
|
916
1066
|
|
|
917
|
-
def get_papertrading_logs(self, strategy_code,
|
|
1067
|
+
def get_papertrading_logs(self, strategy_code, display_progress_bar=True, print_live_logs=True):
|
|
918
1068
|
"""
|
|
919
1069
|
Fetch Paper Trading logs
|
|
920
1070
|
|
|
921
1071
|
Args:
|
|
922
1072
|
strategy_code: Strategy code
|
|
923
|
-
|
|
924
|
-
|
|
1073
|
+
display_progress_bar: to track the execution on progress bar as your strategy is executed
|
|
1074
|
+
print_live_logs: to print the live logs as your strategy is executed
|
|
925
1075
|
|
|
926
1076
|
Returns:
|
|
927
1077
|
Report details
|
|
928
1078
|
"""
|
|
929
1079
|
|
|
930
1080
|
assert isinstance(strategy_code, str), f'Argument "strategy_code" should be a string'
|
|
1081
|
+
assert isinstance(display_progress_bar, bool), f'Argument "display_progress_bar" should be a boolean'
|
|
1082
|
+
assert isinstance(print_live_logs, bool), f'Argument "print_live_logs" should be a boolean'
|
|
931
1083
|
|
|
932
|
-
return self.get_logs(strategy_code
|
|
1084
|
+
return self.get_logs(strategy_code, trading_type=TradingType.PAPERTRADING, display_progress_bar=display_progress_bar, print_live_logs=print_live_logs)
|
|
933
1085
|
|
|
934
|
-
def get_papertrading_report_pnl_table(self, strategy_code, country=None,
|
|
1086
|
+
def get_papertrading_report_pnl_table(self, strategy_code, country=None, force_fetch=False, broker_commission_percentage=None, broker_commission_price=None, slippage_percent=None):
|
|
935
1087
|
"""
|
|
936
1088
|
Fetch Paper Trading Profit & Loss details
|
|
937
1089
|
|
|
938
1090
|
Args:
|
|
939
1091
|
strategy_code: strategy code
|
|
940
1092
|
country: country of the exchange
|
|
941
|
-
show_all_rows: True or False
|
|
942
1093
|
force_fetch: Forcefully fetch PnL data
|
|
1094
|
+
broker_commission_percentage: Percentage of broker commission per trade
|
|
1095
|
+
broker_commission_price: Broker fee per trade
|
|
1096
|
+
slippage_percent: Slippage percentage value
|
|
943
1097
|
|
|
944
1098
|
Returns:
|
|
945
1099
|
Report details
|
|
946
1100
|
"""
|
|
947
1101
|
|
|
948
1102
|
if self.papertrade_pnl_data is None or country is not None or force_fetch:
|
|
949
|
-
self.papertrade_pnl_data = self.
|
|
1103
|
+
self.papertrade_pnl_data = self.get_report_pnl_table(strategy_code, TradingType.PAPERTRADING, country, broker_commission_percentage, broker_commission_price, slippage_percent)
|
|
950
1104
|
|
|
951
1105
|
return self.papertrade_pnl_data
|
|
952
1106
|
|
|
953
|
-
def get_papertrading_report_statistics(self, strategy_code, initial_funds=
|
|
1107
|
+
def get_papertrading_report_statistics(self, strategy_code, initial_funds=None, report='metrics', html_dump=False):
|
|
954
1108
|
"""
|
|
955
1109
|
Fetch Paper Trading report statistics
|
|
956
1110
|
|
|
957
1111
|
Args:
|
|
958
1112
|
strategy_code: strategy code
|
|
959
1113
|
initial_funds: initial funds allotted before papertrading
|
|
960
|
-
mode: extension used to generate statistics
|
|
961
1114
|
report: format and content of the report
|
|
962
1115
|
html_dump: save it as a html file
|
|
963
1116
|
|
|
@@ -976,12 +1129,14 @@ class AlgoBullsConnection:
|
|
|
976
1129
|
|
|
977
1130
|
return order_report
|
|
978
1131
|
|
|
979
|
-
def get_papertrading_report_order_history(self, strategy_code):
|
|
1132
|
+
def get_papertrading_report_order_history(self, strategy_code, country=None, render_as_dataframe=False):
|
|
980
1133
|
"""
|
|
981
1134
|
Fetch Paper Trading order history
|
|
982
1135
|
|
|
983
1136
|
Args:
|
|
984
|
-
strategy_code:
|
|
1137
|
+
strategy_code: strategy code
|
|
1138
|
+
country: country of the segment
|
|
1139
|
+
render_as_dataframe: return order history as dataframe or pretty string
|
|
985
1140
|
|
|
986
1141
|
Returns:
|
|
987
1142
|
Report details
|
|
@@ -989,7 +1144,7 @@ class AlgoBullsConnection:
|
|
|
989
1144
|
|
|
990
1145
|
assert isinstance(strategy_code, str), f'Argument "strategy_code" should be a string'
|
|
991
1146
|
|
|
992
|
-
return self.
|
|
1147
|
+
return self.get_report_order_history(strategy_code=strategy_code, trading_type=TradingType.PAPERTRADING, render_as_dataframe=render_as_dataframe, country=country)
|
|
993
1148
|
|
|
994
1149
|
def realtrade(self, strategy=None, start=None, end=None, instruments=None, lots=None, parameters=None, candle=None, mode=None, broking_details=None, **kwargs):
|
|
995
1150
|
"""
|
|
@@ -1023,9 +1178,8 @@ class AlgoBullsConnection:
|
|
|
1023
1178
|
"""
|
|
1024
1179
|
|
|
1025
1180
|
# start realtrading job
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
**kwargs)
|
|
1181
|
+
_ = self.start_job(strategy_code=strategy, start_timestamp=start, end_timestamp=end, instruments=instruments, lots=lots, strategy_parameters=parameters, candle_interval=candle, strategy_mode=mode, trading_type=TradingType.REALTRADING,
|
|
1182
|
+
broking_details=broking_details, **kwargs)
|
|
1029
1183
|
|
|
1030
1184
|
# Update previously saved pnl data and exchange location
|
|
1031
1185
|
self.realtrade_pnl_data = None
|
|
@@ -1063,50 +1217,52 @@ class AlgoBullsConnection:
|
|
|
1063
1217
|
|
|
1064
1218
|
return self.stop_job(strategy_code=strategy_code, trading_type=TradingType.REALTRADING)
|
|
1065
1219
|
|
|
1066
|
-
def get_realtrading_logs(self, strategy_code,
|
|
1220
|
+
def get_realtrading_logs(self, strategy_code, display_progress_bar=True, print_live_logs=True):
|
|
1067
1221
|
"""
|
|
1068
1222
|
Fetch Real Trading logs
|
|
1069
1223
|
|
|
1070
1224
|
Args:
|
|
1071
1225
|
strategy_code: Strategy code
|
|
1072
|
-
|
|
1073
|
-
|
|
1226
|
+
display_progress_bar: to track the execution on progress bar as your strategy is executed
|
|
1227
|
+
print_live_logs: to print the live logs as your strategy is executed
|
|
1074
1228
|
|
|
1075
1229
|
Returns:
|
|
1076
1230
|
Report details
|
|
1077
1231
|
"""
|
|
1078
1232
|
|
|
1079
1233
|
assert isinstance(strategy_code, str), f'Argument "strategy_code" should be a string'
|
|
1234
|
+
assert isinstance(display_progress_bar, bool), f'Argument "display_progress_bar" should be a boolean'
|
|
1235
|
+
assert isinstance(print_live_logs, bool), f'Argument "print_live_logs" should be a boolean'
|
|
1080
1236
|
|
|
1081
|
-
return self.get_logs(strategy_code
|
|
1237
|
+
return self.get_logs(strategy_code, trading_type=TradingType.REALTRADING, display_progress_bar=display_progress_bar, print_live_logs=print_live_logs)
|
|
1082
1238
|
|
|
1083
|
-
def get_realtrading_report_pnl_table(self, strategy_code, country=None,
|
|
1239
|
+
def get_realtrading_report_pnl_table(self, strategy_code, country=None, force_fetch=False, broker_commission_percentage=None, broker_commission_price=None):
|
|
1084
1240
|
"""
|
|
1085
1241
|
Fetch Real Trading Profit & Loss details
|
|
1086
1242
|
|
|
1087
1243
|
Args:
|
|
1088
1244
|
strategy_code: strategy code
|
|
1089
1245
|
country: country of the Exchange
|
|
1090
|
-
show_all_rows: True or False
|
|
1091
1246
|
force_fetch: Forcefully fetch PnL data
|
|
1247
|
+
broker_commission_percentage: Percentage of broker commission per trade
|
|
1248
|
+
broker_commission_price: Broker fee per trade
|
|
1092
1249
|
|
|
1093
1250
|
Returns:
|
|
1094
1251
|
Report details
|
|
1095
1252
|
"""
|
|
1096
1253
|
|
|
1097
1254
|
if self.realtrade_pnl_data is None or country is not None or force_fetch:
|
|
1098
|
-
self.realtrade_pnl_data = self.
|
|
1255
|
+
self.realtrade_pnl_data = self.get_report_pnl_table(strategy_code, TradingType.REALTRADING, country, broker_commission_percentage, broker_commission_price)
|
|
1099
1256
|
|
|
1100
1257
|
return self.realtrade_pnl_data
|
|
1101
1258
|
|
|
1102
|
-
def get_realtrading_report_statistics(self, strategy_code, initial_funds=
|
|
1259
|
+
def get_realtrading_report_statistics(self, strategy_code, initial_funds=None, report='metrics', html_dump=False):
|
|
1103
1260
|
"""
|
|
1104
1261
|
Fetch Real Trading report statistics
|
|
1105
1262
|
|
|
1106
1263
|
Args:
|
|
1107
1264
|
strategy_code: strategy code
|
|
1108
1265
|
initial_funds: initial funds allotted before realtrading
|
|
1109
|
-
mode: extension used to generate statistics
|
|
1110
1266
|
report: format and content of the report
|
|
1111
1267
|
html_dump: save it as a html file
|
|
1112
1268
|
|
|
@@ -1125,11 +1281,13 @@ class AlgoBullsConnection:
|
|
|
1125
1281
|
|
|
1126
1282
|
return order_report
|
|
1127
1283
|
|
|
1128
|
-
def get_realtrading_report_order_history(self, strategy_code):
|
|
1284
|
+
def get_realtrading_report_order_history(self, strategy_code, country=None, render_as_dataframe=False):
|
|
1129
1285
|
"""
|
|
1130
1286
|
Fetch Real Trading order history
|
|
1131
1287
|
Args:
|
|
1132
|
-
strategy_code:
|
|
1288
|
+
strategy_code: strategy code
|
|
1289
|
+
country: country of the segment
|
|
1290
|
+
render_as_dataframe: return order history as dataframe or pretty string
|
|
1133
1291
|
|
|
1134
1292
|
Returns:
|
|
1135
1293
|
Report details
|
|
@@ -1137,7 +1295,7 @@ class AlgoBullsConnection:
|
|
|
1137
1295
|
# assert (isinstance(broker, AlgoBullsSupportedBrokers) is True), f'Argument broker should be an enum of type {AlgoBullsSupportedBrokers.__name__}'
|
|
1138
1296
|
assert isinstance(strategy_code, str), f'Argument "strategy_code" should be a string'
|
|
1139
1297
|
|
|
1140
|
-
return self.
|
|
1298
|
+
return self.get_report_order_history(strategy_code=strategy_code, trading_type=TradingType.REALTRADING, render_as_dataframe=render_as_dataframe, country=country)
|
|
1141
1299
|
|
|
1142
1300
|
|
|
1143
1301
|
def pandas_dataframe_all_rows():
|
pyalgotrading/constants.py
CHANGED
|
@@ -274,6 +274,41 @@ class ExecutionStatus(Enum):
|
|
|
274
274
|
STOPPED = 'STOPPED'
|
|
275
275
|
|
|
276
276
|
|
|
277
|
+
class OptionsStrikeDirection(Enum):
|
|
278
|
+
ITM = 'ITM'
|
|
279
|
+
ATM = 'ATM'
|
|
280
|
+
OTM = 'OTM'
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class OptionsTradingsymbolSuffix(Enum):
|
|
284
|
+
CE = 'CE'
|
|
285
|
+
PE = 'PE'
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class OptionsExpiryKey(Enum):
|
|
289
|
+
WEEKLY_CURRENT = 'WEEKLY_CURRENT'
|
|
290
|
+
WEEKLY_NEXT = 'WEEKLY_NEXT'
|
|
291
|
+
MONTHLY_CURRENT = 'MONTHLY_CURRENT'
|
|
292
|
+
MONTHLY_NEXT = 'MONTHLY_NEXT'
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class OptionsInstrumentDirection(Enum):
|
|
296
|
+
EXACT = 'EXACT'
|
|
297
|
+
ROUNDUP = 'ROUNDUP'
|
|
298
|
+
ROUNDDOWN = 'ROUNDDOWN'
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
CandleIntervalSecondsMap = {
|
|
302
|
+
'minute': 60,
|
|
303
|
+
'3minutes': 180,
|
|
304
|
+
'5minutes': 300,
|
|
305
|
+
'10minutes': 600,
|
|
306
|
+
'15minutes': 900,
|
|
307
|
+
'30minutes': 1800,
|
|
308
|
+
'60minutes': 3600,
|
|
309
|
+
'day': 86400
|
|
310
|
+
}
|
|
311
|
+
|
|
277
312
|
KEY_DT_FORMAT_WITH_TIMEZONE = 0
|
|
278
313
|
KEY_DT_FORMAT_WITHOUT_TIMEZONE = 1
|
|
279
314
|
|
|
@@ -289,3 +324,4 @@ EXCHANGE_LOCALE_MAP = {
|
|
|
289
324
|
'NASDAQ': Locale.USA.value,
|
|
290
325
|
'NYSE': Locale.USA.value,
|
|
291
326
|
}
|
|
327
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from pyalgotrading.strategy.strategy_base import StrategyBase
|
|
2
|
+
from pyalgotrading.constants import *
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class StrategyOptionsBaseV2(StrategyBase):
|
|
6
|
+
"""
|
|
7
|
+
Dummy placeholder class. Here to ensure all required methods are implemented and as per requirements.
|
|
8
|
+
|
|
9
|
+
Once uploaded, this strategy will be replaced with the real base class strategy
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def get_options_ref_key(instrument, expiry_date):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def initialize_instrument(self, instrument):
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
def get_allowed_expiry_dates(self):
|
|
20
|
+
"""
|
|
21
|
+
Gives the allowed expiry date, depending on the selection of monthly expiry or weekly expiry.
|
|
22
|
+
Checkout the documentation to understand in more detail
|
|
23
|
+
"""
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
def options_instruments_set_up(self, base_instrument, instrument_direction, expiry_date, tradingsymbol_suffix, ltp=None, apply_modulo=False, modulo_value=100):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
def get_options_instruments(self, base_instrument, expiry_date, tradingsymbol_suffix, instrument_direction, ltp, apply_modulo=False, modulo_value=100):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def get_options_instrument_with_strike_direction(self, base_instrument, expiry_date, tradingsymbol_suffix, strike_direction, no_of_strikes):
|
|
33
|
+
instrument = None
|
|
34
|
+
return instrument
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class IntrumentsMappingManager:
|
|
38
|
+
def __init__(self):
|
|
39
|
+
self.base_instrument_to_instrument_map_list = {}
|
|
40
|
+
self.instrument_to_base_instrument_map = {}
|
|
41
|
+
|
|
42
|
+
def add_mappings(self, base_instrument, child_instruments):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def is_base_instrument(self, instrument):
|
|
46
|
+
return
|
|
47
|
+
|
|
48
|
+
def is_child_instrument(self, instrument):
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
def get_base_instrument(self, instrument):
|
|
52
|
+
return
|
|
53
|
+
|
|
54
|
+
def get_child_instruments_list(self, instrument):
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class OrderTagManager:
|
|
59
|
+
|
|
60
|
+
def add_order(self, order, tags=None):
|
|
61
|
+
"""
|
|
62
|
+
Adds an order to tags extracted from the order object as well as given tags (if any)
|
|
63
|
+
"""
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
# take 1 or more tags; return 1 or more items if many=True; return only 1 item if many=False;
|
|
67
|
+
def get_orders(self, tags, many=False, ignore_errors=False):
|
|
68
|
+
"""
|
|
69
|
+
Takes 1 or more tags; returns 1 or more orders if many=True; returns only 1 order if many=False
|
|
70
|
+
"""
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
def remove_tags(self, tags):
|
|
74
|
+
"""
|
|
75
|
+
Removes 1 or more tags from the data structure
|
|
76
|
+
"""
|
|
77
|
+
return
|
|
78
|
+
|
|
79
|
+
def remove_order(self, order):
|
|
80
|
+
"""
|
|
81
|
+
Removes an order from the data structure
|
|
82
|
+
"""
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
def get_internals(self):
|
|
86
|
+
"""
|
|
87
|
+
Returns the complete data structure
|
|
88
|
+
The developer should use this for debugging, but remove its usages before finalizing
|
|
89
|
+
"""
|
|
90
|
+
return
|
pyalgotrading/utils/func.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
A module for plotting candlesticks
|
|
3
3
|
"""
|
|
4
4
|
from datetime import datetime as dt, timezone
|
|
5
|
-
|
|
5
|
+
import random
|
|
6
6
|
import pandas as pd
|
|
7
7
|
|
|
8
8
|
from pyalgotrading.constants import PlotType, TRADING_TYPE_DT_FORMAT_MAP, KEY_DT_FORMAT_WITHOUT_TIMEZONE, KEY_DT_FORMAT_WITH_TIMEZONE
|
|
@@ -156,8 +156,69 @@ def get_datetime_with_tz(timestamp_str, trading_type, label=''):
|
|
|
156
156
|
timestamp_str = dt.strptime(timestamp_str, TRADING_TYPE_DT_FORMAT_MAP[trading_type][KEY_DT_FORMAT_WITHOUT_TIMEZONE])
|
|
157
157
|
timestamp_str = timestamp_str.replace(tzinfo=timezone.utc)
|
|
158
158
|
print(f'Warning: Timezone info not provided. Expected timestamp format is "{TRADING_TYPE_DT_FORMAT_MAP[trading_type][KEY_DT_FORMAT_WITH_TIMEZONE]}", received time "{timestamp_str}". Assuming timezone as UTC(+0000)...')
|
|
159
|
-
except ValueError
|
|
160
|
-
raise ValueError(f'Error: Invalid string timestamp format for argument "{label}".\nExpected timestamp format for {trading_type.name} is "{TRADING_TYPE_DT_FORMAT_MAP[trading_type][KEY_DT_FORMAT_WITH_TIMEZONE]}".
|
|
159
|
+
except ValueError:
|
|
160
|
+
raise ValueError(f'Error: Invalid string timestamp format for argument "{label}".\nExpected timestamp format for {trading_type.name} is "{TRADING_TYPE_DT_FORMAT_MAP[trading_type][KEY_DT_FORMAT_WITH_TIMEZONE]}". '
|
|
161
|
+
f'Received "{timestamp_str}" instead.')
|
|
161
162
|
|
|
162
163
|
return timestamp_str
|
|
163
164
|
|
|
165
|
+
|
|
166
|
+
def calculate_slippage(pnl_df, slippage_percent):
|
|
167
|
+
if 'exit_variety' not in pnl_df.columns or 'entry_variety' not in pnl_df.columns:
|
|
168
|
+
pnl_df['exit_variety'] = 'MARKET'
|
|
169
|
+
pnl_df['entry_variety'] = 'MARKET'
|
|
170
|
+
print('WARNING: Column for Order Variety not found. Assuming all trades are Market Orders.')
|
|
171
|
+
|
|
172
|
+
pnl_df[['entry_price', 'exit_price']] = pnl_df.apply(
|
|
173
|
+
lambda row: (slippage(row.entry_price, row.entry_variety, row.entry_transaction_type, slippage_percent), slippage(row.exit_price, row.exit_variety, row.exit_transaction_type, slippage_percent)), axis=1, result_type='expand')
|
|
174
|
+
|
|
175
|
+
pnl_df['pnl_absolute'] = pnl_df['exit_price'] - pnl_df['entry_price']
|
|
176
|
+
return pnl_df
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def calculate_brokerage(pnl_df, brokerage_percentage, brokerage_flat_price):
|
|
180
|
+
# generate brokerage data
|
|
181
|
+
brokerage_generated = False
|
|
182
|
+
|
|
183
|
+
# brokerage commission based on only percentage
|
|
184
|
+
if brokerage_percentage is not None:
|
|
185
|
+
pnl_df['brokerage'] = ((pnl_df['entry_price'] * pnl_df['entry_quantity']) + (pnl_df['exit_price'] * pnl_df['exit_quantity'])) * (brokerage_percentage / 100)
|
|
186
|
+
brokerage_generated = True
|
|
187
|
+
|
|
188
|
+
# brokerage commission based on only flat price
|
|
189
|
+
elif brokerage_flat_price is not None:
|
|
190
|
+
pnl_df['brokerage'] = brokerage_flat_price
|
|
191
|
+
brokerage_generated = True
|
|
192
|
+
|
|
193
|
+
# brokerage 0
|
|
194
|
+
else:
|
|
195
|
+
pnl_df['brokerage'] = 0
|
|
196
|
+
|
|
197
|
+
# brokerage commission based on minimum of percentage and flat price
|
|
198
|
+
if brokerage_percentage is not None and brokerage_flat_price is not None:
|
|
199
|
+
pnl_df["brokerage"].loc[pnl_df["brokerage"] > brokerage_flat_price] = brokerage_flat_price
|
|
200
|
+
brokerage_generated = True
|
|
201
|
+
|
|
202
|
+
if brokerage_generated:
|
|
203
|
+
pnl_df['net_pnl'] = pnl_df['pnl_absolute'] - pnl_df['brokerage']
|
|
204
|
+
else:
|
|
205
|
+
pnl_df['net_pnl'] = pnl_df['pnl_absolute']
|
|
206
|
+
|
|
207
|
+
return pnl_df
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def slippage(price, variety, transaction_type, slip_percent=1):
|
|
211
|
+
|
|
212
|
+
# convert slippage percentage to decimal
|
|
213
|
+
slip_percent = abs(slip_percent) / 100
|
|
214
|
+
|
|
215
|
+
# if market orders, we consider negative as well as positive slippage
|
|
216
|
+
if variety in ['MARKET', 'STOPLOSS_MARKET']:
|
|
217
|
+
return price*(1 + random.choice([1, 0, -1]) * slip_percent)
|
|
218
|
+
|
|
219
|
+
# if limit orders, we consider only positive slippage
|
|
220
|
+
else:
|
|
221
|
+
if transaction_type == 'BUY':
|
|
222
|
+
return price*(1 + random.choice([0, -1]) * slip_percent)
|
|
223
|
+
else:
|
|
224
|
+
return price*(1 + random.choice([1, 0]) * slip_percent)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pyalgotrading
|
|
3
|
-
Version: 2023.
|
|
3
|
+
Version: 2023.10.1
|
|
4
4
|
Summary: Official Python Package for Algorithmic Trading APIs powered by AlgoBulls
|
|
5
5
|
Home-page: https://github.com/algobulls/pyalgotrading
|
|
6
6
|
Author: Pushpak Dagade
|
|
@@ -25,8 +25,9 @@ Classifier: Programming Language :: Python :: 3.7
|
|
|
25
25
|
Classifier: Programming Language :: Python :: 3.8
|
|
26
26
|
Requires-Python: >=3.6
|
|
27
27
|
Description-Content-Type: text/markdown
|
|
28
|
-
|
|
29
|
-
Requires-Dist: pandas
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: pandas >=0.25.3
|
|
30
|
+
Requires-Dist: requests >=2.24.0
|
|
30
31
|
|
|
31
32
|
# pyalgotrading
|
|
32
33
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
pyalgotrading/__init__.py,sha256=e0lmS8IOW1JBf_jfyDAO-9cCEyNsNoSCjwetSCWPeIg,181
|
|
2
|
-
pyalgotrading/constants.py,sha256=
|
|
2
|
+
pyalgotrading/constants.py,sha256=gAzDcdmOqYGFC2GYC9w1i_aSCUF03eFhCLNZNOYCwLM,7309
|
|
3
3
|
pyalgotrading/algobulls/__init__.py,sha256=IRkbWtJfgQnPH7gikjqpa6QsYnmk_NQ1lwtx7LPIC6c,133
|
|
4
|
-
pyalgotrading/algobulls/api.py,sha256=
|
|
5
|
-
pyalgotrading/algobulls/connection.py,sha256=
|
|
4
|
+
pyalgotrading/algobulls/api.py,sha256=fhW0Ms73FqLGRSzz5HDB95ViNsnstXFig8aqGECODCY,19594
|
|
5
|
+
pyalgotrading/algobulls/connection.py,sha256=DKCzloCaZ8sQCr5YXqapQKxpdE_B1svKYkgvETHrzYY,59399
|
|
6
6
|
pyalgotrading/algobulls/exceptions.py,sha256=B96On8cN8tgtX7i4shKOlYfvjSjvspIRPbOpyF-jn0I,2187
|
|
7
7
|
pyalgotrading/broker/__init__.py,sha256=jXVWBVRlqzevKxNDqrPANJz9WubROBes8gaKcxcYz58,66
|
|
8
8
|
pyalgotrading/broker/broker_connection_base.py,sha256=vAJN5EAQuVfL8Ngf03wsaI5TTGdziCHKCb0bczGgSJ0,7594
|
|
@@ -14,17 +14,18 @@ pyalgotrading/order/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
|
|
|
14
14
|
pyalgotrading/order/order_base.py,sha256=trC_bvazLNvvuGqZEdacxzlacc3g73ZvqpedYYwuKJA,1742
|
|
15
15
|
pyalgotrading/order/order_bracket_base.py,sha256=7Sj0nlCWE20wZiZ12NciptPYKKGE9KsnKLCjdO7sKV4,3576
|
|
16
16
|
pyalgotrading/order/order_regular_base.py,sha256=wOFmv7QnxJvNKtqdoZn0a-cbTotik4cap5Z5mz0Qcs0,2313
|
|
17
|
-
pyalgotrading/strategy/__init__.py,sha256=
|
|
17
|
+
pyalgotrading/strategy/__init__.py,sha256=V58WuYIWpsmbs5pat5Z1FLxOR3nHwjaIhnJTQ8tzAUA,126
|
|
18
18
|
pyalgotrading/strategy/strategy_base.py,sha256=9I6ZdZT1IimO9g03QURrFo2Pxvs4Dsk8Ybdtij8ZIUQ,4717
|
|
19
|
+
pyalgotrading/strategy/strategy_options_base_v2.py,sha256=3SfCvWjz8eGXtlB2hIXREAODV0PbrtXIYgXRFB08UjQ,2765
|
|
19
20
|
pyalgotrading/strategy/validate_strategy.py,sha256=Ot2kvROtG-tpcbK_Fv-OdapG8oNdgvSNV2hTWtfTCQI,482
|
|
20
21
|
pyalgotrading/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
-
pyalgotrading/utils/func.py,sha256=
|
|
22
|
+
pyalgotrading/utils/func.py,sha256=Vadc2kbJox0vmBBUWD6DvuWB08rvYGDfvDi-pyOefUA,11155
|
|
22
23
|
pyalgotrading/utils/candlesticks/__init__.py,sha256=maIn__tvTvJDjldPhU9agBcNNuROt_lpNTV4CZ1Yl6I,83
|
|
23
24
|
pyalgotrading/utils/candlesticks/heikinashi.py,sha256=SpcuK7AYV0sgzcw-DMfLNtTDn2RfWqGvWBt4no7yKD4,2003
|
|
24
25
|
pyalgotrading/utils/candlesticks/linebreak.py,sha256=cYwoETMrenWOa06d03xASZoiou-qRz7n2mZYCi5ilEs,1434
|
|
25
26
|
pyalgotrading/utils/candlesticks/renko.py,sha256=zovQ6D658pBLas86FuTu9fU3-Kkv2hM-4h7OQJjdxng,2089
|
|
26
|
-
pyalgotrading-2023.
|
|
27
|
-
pyalgotrading-2023.
|
|
28
|
-
pyalgotrading-2023.
|
|
29
|
-
pyalgotrading-2023.
|
|
30
|
-
pyalgotrading-2023.
|
|
27
|
+
pyalgotrading-2023.10.1.dist-info/LICENSE,sha256=-LLEprvixKS-LwHef99YQSOFon_tWeTwJRAWuUwwr1g,1066
|
|
28
|
+
pyalgotrading-2023.10.1.dist-info/METADATA,sha256=bEA0ZdUGea69WTYNxyEdqPKTmO9SbRtt3pJom3I-aI0,5142
|
|
29
|
+
pyalgotrading-2023.10.1.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
|
|
30
|
+
pyalgotrading-2023.10.1.dist-info/top_level.txt,sha256=A12PTnbXqO3gsZ0D0Gkyzf_OYRQxjJvtg3MqN-Ur2zY,14
|
|
31
|
+
pyalgotrading-2023.10.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|