diff --git a/blankly/__init__.py b/blankly/__init__.py index fb68f1d2..a827cc81 100644 --- a/blankly/__init__.py +++ b/blankly/__init__.py @@ -44,10 +44,6 @@ from blankly.utils import time_builder from blankly.enums import Side, OrderType, OrderStatus, TimeInForce -from blankly.exchanges.interfaces.binance_futures.binance_futures import BinanceFutures -from blankly.exchanges.interfaces.ftx_futures.ftx_futures import FTXFutures -from blankly.frameworks.strategy import FuturesStrategy -from blankly.frameworks.strategy import FuturesStrategyState from blankly.deployment.reporter_headers import Reporter as __Reporter_Headers diff --git a/blankly/data/data_reader.py b/blankly/data/data_reader.py index c9d1e139..de8b616f 100644 --- a/blankly/data/data_reader.py +++ b/blankly/data/data_reader.py @@ -20,7 +20,6 @@ import pandas as pd from enum import Enum -from blankly.utils import convert_epochs from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface @@ -42,7 +41,6 @@ class DataTypes(Enum): class FileTypes(Enum): csv = 'csv' json = 'json' - df = 'df' class DataReader: @@ -85,12 +83,12 @@ def _convert_to_list(file_path, symbol): Primarily for CSV prices, this allows single or multi sets of symbols to be converted into matching arrays """ # Turn it into a list - if isinstance(file_path, str) or isinstance(file_path, pd.DataFrame): + if isinstance(file_path, str): file_paths = [file_path] else: file_paths = file_path - if isinstance(symbol, str) or isinstance(file_path, pd.DataFrame): + if isinstance(symbol, str): symbols = [symbol] else: # Could be None still @@ -98,24 +96,6 @@ def _convert_to_list(file_path, symbol): return file_paths, symbols - def _parse_df_prices(self, file_paths: list, symbols: list, columns: set) -> None: - - if symbols is None: - raise LookupError("Must pass one or more symbols to identify the DataFrame") - if len(file_paths) != len(symbols): - raise LookupError(f"Mismatching symbol & file path lengths, got {len(file_paths)} and {len(symbols)} for " - f"file paths and symbol lengths.") - - for index in range(len(file_paths)): - - self._check_length(file_paths[index], file_paths[index]) - - # Check if its contained - assert (columns.issubset(file_paths[index].columns)), f"{columns} not subset of {file_paths[index].columns}" - - # Now push it directly into the dataset and sort by time - self._internal_dataset[symbols[index]] = file_paths[index].sort_values('time') - def _parse_csv_prices(self, file_paths: list, symbols: list, columns: set) -> None: if symbols is None: raise LookupError("Must pass one or more symbols to identify the csv files") @@ -130,7 +110,7 @@ def _parse_csv_prices(self, file_paths: list, symbols: list, columns: set) -> No self._check_length(contents, file_paths[index]) # Check if its contained - assert (columns.issubset(contents.columns)), f"{columns} not subset of {contents.columns}" + assert (columns.issubset(contents.columns)) # Now push it directly into the dataset and sort by time self._internal_dataset[symbols[index]] = contents.sort_values('time') @@ -162,9 +142,7 @@ def complain_if_different(ending_: str, type_: str): raise LookupError("Cannot pass both csv files and json files into a single constructor.") for file_path in file_paths: - if isinstance(file_path, pd.DataFrame): - complain_if_different('df', FileTypes.df.value) - elif file_path[-3:] == 'csv': + if file_path[-3:] == 'csv': complain_if_different('csv', FileTypes.csv.value) elif file_path[-4:] == 'json': # In this instance the symbols should be None @@ -178,23 +156,12 @@ def _guess_resolutions(self): for symbol in self._internal_dataset: # Get the time diff time_series: pd.Series = self._internal_dataset[symbol]['time'] - - # Convert all epochs using the convert epoch function - time_series = time_series.apply(lambda x: convert_epochs(x)) - time_dif = time_series.diff() # Now find the most common difference and use that if symbol not in self.prices_info: self.prices_info[symbol] = {} - guessed_resolution = int(time_dif.value_counts().idxmax()) - - # If the resolution is 0, then we have a problem - if guessed_resolution == 0: - raise LookupError(f"Resolution is 0 for {symbol}, this is not allowed. Please check your data." - f" This commonly occurs when the data is in exponential format or too few datapoints") - # Store the resolution start time and end time of each dataset self.prices_info[symbol]['resolution'] = int(time_dif.value_counts().idxmax()) self.prices_info[symbol]['start_time'] = time_series.iloc[0] @@ -228,9 +195,7 @@ def __init__(self, file_path: [str, list], symbol: [str, list]): super().__init__(data_type) - if data_type == FileTypes.df.value: - self._parse_df_prices(file_paths, symbols, {'open', 'high', 'low', 'close', 'volume', 'time'}) - elif data_type == FileTypes.json.value: + if data_type == FileTypes.json.value: self._parse_json_prices(file_paths, ('open', 'high', 'low', 'close', 'volume', 'time')) elif data_type == FileTypes.csv.value: self._parse_csv_prices(file_paths, symbols, {'open', 'high', 'low', 'close', 'volume', 'time'}) @@ -243,9 +208,8 @@ def __init__(self, file_path: [str, list], symbol: [str, list]): class EventReader(DataReader): def __init__(self, event_type: str, events: dict): super().__init__(DataTypes.event_json) - if events: - time, data = zip(*events.items()) - self._write_dataset({'time': time, 'data': data}, event_type, ('time', 'data')) + time, data = zip(*events.items()) + self._write_dataset({'time': time, 'data': data}, event_type, ('time', 'data')) class JsonEventReader(DataReader): diff --git a/blankly/data/templates/keyless.py b/blankly/data/templates/keyless.py index b4fb5ac0..7db70aae 100644 --- a/blankly/data/templates/keyless.py +++ b/blankly/data/templates/keyless.py @@ -40,5 +40,5 @@ def init(symbol, state: blankly.StrategyState): strategy.add_price_event(price_event, symbol='BTC-USD', resolution='1d', init=init) # Backtest the strategy - results = strategy.backtest(start_date=1598377600, end_date=1650067200, initial_values={'USD': 10000}) + results = strategy.backtest(start_date=1588377600, end_date=1650067200, initial_values={'USD': 10000}) print(results) diff --git a/blankly/data/templates/none.py b/blankly/data/templates/none.py index 17668b1b..92954c96 100644 --- a/blankly/data/templates/none.py +++ b/blankly/data/templates/none.py @@ -7,4 +7,4 @@ if blankly.is_deployed: strategy.start() else: - strategy.backtest(to='1y', initial_values={'QUOTE_ASSET': 10000}) + strategy.backtest(to='1y', initial_values={'USD': 10000}) diff --git a/blankly/data/templates/rsi_bot.py b/blankly/data/templates/rsi_bot.py index 8f8a03e4..b51fd71b 100644 --- a/blankly/data/templates/rsi_bot.py +++ b/blankly/data/templates/rsi_bot.py @@ -44,4 +44,4 @@ def init(symbol, state: blankly.StrategyState): if blankly.is_deployed: strategy.start() else: - strategy.backtest(to='1y', initial_values={'QUOTE_ASSET': 10000}) + strategy.backtest(to='1y', initial_values={'USD': 10000}) diff --git a/blankly/deployment/exchange_data.py b/blankly/deployment/exchange_data.py index 324b1161..17f52ddf 100644 --- a/blankly/deployment/exchange_data.py +++ b/blankly/deployment/exchange_data.py @@ -67,12 +67,10 @@ def kucoin_test_func(auth, tld): base_url=(alpaca_api.live_url, alpaca_api.paper_url)[auth['sandbox']] ).get_account()), - Exchange('binance', ['BTC-USDT', 'ETH-USDT', 'SOL-USDT'], lambda auth, tld: BinanceClient(api_key=auth['API_KEY'], api_secret=auth['API_SECRET'], tld=tld, testnet=auth['sandbox']).get_account(), tlds=['com', 'us'], currency='USDT'), - Exchange('coinbase_pro', ['BTC-USD', 'ETH-USD', 'SOL-USD'], lambda auth, tld: CoinbaseProAPI(api_key=auth['API_KEY'], api_secret=auth['API_SECRET'], api_pass=auth['API_PASS'], @@ -83,12 +81,10 @@ def kucoin_test_func(auth, tld): Exchange('ftx', ['BTC-USD', 'ETH-USD', 'SOL-USD'], lambda auth, tld: FTXAPI(auth['API_KEY'], auth['API_SECRET'], tld).get_account_info(), tlds=['com', 'us'], python_class='FTX', display_name='FTX'), - Exchange('oanda', ['BTC-USD', 'ETH-USD', 'SOL-USD'], lambda auth, tld: OandaAPI(personal_access_token=auth['PERSONAL_ACCESS_TOKEN'], account_id=auth['ACCOUNT_ID'], sandbox=auth['sandbox']).get_account(), key_info=['ACCOUNT_ID', 'PERSONAL_ACCESS_TOKEN']), - Exchange('kucoin', ['BTC-USDT', 'ETH-USDT', 'SOL-USDT'], kucoin_test_func, key_info=['API_KEY', 'API_SECRET', 'API_PASS'], currency='USDT'), ] diff --git a/blankly/deployment/new_cli.py b/blankly/deployment/new_cli.py index 4dc3fb6f..00296161 100644 --- a/blankly/deployment/new_cli.py +++ b/blankly/deployment/new_cli.py @@ -29,6 +29,7 @@ from typing import Optional import pkgutil +import questionary from questionary import Choice from blankly.deployment.api import API @@ -39,12 +40,13 @@ from blankly.deployment.exchange_data import EXCHANGES, Exchange, EXCHANGE_CHOICES, exc_display_name, \ EXCHANGE_CHOICES_NO_KEYLESS from blankly.utils.utils import load_deployment_settings, load_user_preferences, load_backtest_preferences + TEMPLATES = {'strategy': {'none': 'none.py', 'rsi_bot': 'rsi_bot.py'}, 'screener': {'none': 'none_screener.py', 'rsi_screener': 'rsi_screener.py'}} -# AUTH_URL = 'https://app.blankly.finance/auth/signin?redirectUrl=/deploy' +AUTH_URL = 'https://app.blankly.finance/auth/signin?redirectUrl=/deploy' def validate_non_empty(text): @@ -56,11 +58,7 @@ def validate_non_empty(text): def create_model(api, name, description, model_type, project_id=None): with show_spinner('Creating model') as spinner: try: - # Expanded this logic because the other one was broken - if project_id is not None: - model = api.create_model(project_id, model_type, name, description) - else: - model = api.create_model(api.user_id, model_type, name, description) + model = api.create_model(project_id or api.user_id, model_type, name, description) except Exception: spinner.fail('Failed to create model') raise @@ -69,46 +67,46 @@ def create_model(api, name, description, model_type, project_id=None): return model -# def ensure_login() -> API: -# # TODO print selected team ? -# api = is_logged_in() -# if api: -# return api -# return launch_login_flow() - - -# def is_logged_in() -> Optional[API]: -# token = get_token() -# if not token: -# return -# -# # log into deployment api -# try: -# return API(token) -# except Exception: # TODO -# return - - -# def launch_login_flow() -> API: -# try: -# webbrowser.open_new(AUTH_URL) -# print_work(f'Your browser was opened to {AUTH_URL}. Open the window and login.') -# except Exception: -# print_work(f'Could not find a browser to open. Navigate to {AUTH_URL} and login') -# -# api = None -# with show_spinner(f'Waiting for login') as spinner: -# try: -# api = poll_login() -# except Exception: -# pass # we just check for api being valid, poll_login can return None -# -# if not api: -# spinner.fail('Failed to login') -# sys.exit(1) -# -# spinner.ok('Logged in') -# return api +def ensure_login() -> API: + # TODO print selected team ? + api = is_logged_in() + if api: + return api + return launch_login_flow() + + +def is_logged_in() -> Optional[API]: + token = get_token() + if not token: + return + + # log into deployment api + try: + return API(token) + except Exception: # TODO + return + + +def launch_login_flow() -> API: + try: + webbrowser.open_new(AUTH_URL) + print_work(f'Your browser was opened to {AUTH_URL}. Open the window and login.') + except Exception: + print_work(f'Could not find a browser to open. Navigate to {AUTH_URL} and login') + + api = None + with show_spinner(f'Waiting for login') as spinner: + try: + api = poll_login() + except Exception: + pass # we just check for api being valid, poll_login can return None + + if not api: + spinner.fail('Failed to login') + sys.exit(1) + + spinner.ok('Logged in') + return api def add_key_interactive(exchange: Exchange): @@ -159,9 +157,9 @@ def init_starter_model(model): if exchange and confirm(f'Would you like to add {exchange.display_name} keys to your model?').unsafe_ask(): add_key_interactive(exchange) - # if confirm('Would you like to connect this model to the Blankly Platform?').unsafe_ask(): - # api = ensure_login() - # ensure_model(api) + if confirm('Would you like to connect this model to the Blankly Platform?').unsafe_ask(): + api = ensure_login() + ensure_model(api) print_success('Done!') @@ -186,14 +184,13 @@ def blankly_init(args): api = None if args.model: - # api = ensure_login() - # starters = api.get_starter_models() or [] - # model = next((m for m in starters if m['shortName'] == args.model), None) - # if not model: - # print_failure('That starter model doesn\'t exist. Make sure you are typing the name properly.') - # return - print_failure("Starter models are currently disabled.") - ## init_starter_model(model) + api = ensure_login() + starters = api.get_starter_models() or [] + model = next((m for m in starters if m['shortName'] == args.model), None) + if not model: + print_failure('That starter model doesn\'t exist. Make sure you are typing the name properly.') + return + init_starter_model(model) return exchange = select('What exchange would you like to connect to?', EXCHANGE_CHOICES).unsafe_ask() @@ -215,10 +212,10 @@ def blankly_init(args): api = None model = None - # if args.prompt_login and confirm('Would you like to connect this model to the Blankly Platform?').unsafe_ask(): - # if not api: - # api = ensure_login() - # model = get_model_interactive(api, model_type) + if args.prompt_login and confirm('Would you like to connect this model to the Blankly Platform?').unsafe_ask(): + if not api: + api = ensure_login() + model = get_model_interactive(api, model_type) with show_spinner('Generating files') as spinner: files = [ @@ -341,19 +338,18 @@ def generate_bot_py(exchange: Optional[Exchange], template: str) -> str: ('EXCHANGE_NAME', exchange.display_name,), ('EXCHANGE_CLASS', exchange.python_class,), ('SYMBOL_LIST', "['" + "', '".join(exchange.symbols) + "']",), - ('SYMBOL', exchange.symbols[0],), - ('QUOTE_ASSET', exchange.currency) + ('SYMBOL', exchange.symbols[0],) ]: bot_py = bot_py.replace(pattern, replacement) return bot_py -# def blankly_login(args): -# if is_logged_in(): -# print_success('You are already logged in') -# return -# -# launch_login_flow() +def blankly_login(args): + if is_logged_in(): + print_success('You are already logged in') + return + + launch_login_flow() def blankly_logout(args): @@ -366,47 +362,47 @@ def blankly_logout(args): spinner.ok('Logged out') -# def ensure_model(api: API): -# # create model if it doesn't exist -# try: -# with open('blankly.json', 'r') as file: -# data = json.load(file) -# except FileNotFoundError: -# print_failure('There was no model detected in this directory. Try running `blankly init` to create one') -# sys.exit(1) -# -# if 'plan' not in data: -# data['plan'] = select('Select a plan:', [Choice(f'{name} - CPU: {info["cpu"]} RAM: {info["ram"]}', name) -# for name, info in api.get_plans('live').items()]).unsafe_ask() -# -# if 'model_id' not in data or 'project_id' not in data: -# model = get_model_interactive(api, data.get('type', 'strategy')) -# data['model_id'] = model['modelId'] -# data['project_id'] = model['projectId'] -# -# if 'api_key' not in data or 'api_pass' not in data: -# keys = api.generate_keys(data['project_id'], f'Local keys for {data["model_id"]}') -# data['api_key'] = keys['apiKey'] -# data['api_pass'] = keys['apiPass'] -# -# files = [f for f in os.listdir() if not f.startswith('.')] -# if 'main_script' not in data or data['main_script'].lstrip('./') not in files: -# if 'bot.py' in files: -# data['main_script'] = 'bot.py' -# else: -# data['main_script'] = path( -# 'What is the path to your main script/entry point? (Usually bot.py)').unsafe_ask() -# -# if data['main_script'].lstrip('./') not in files: -# print_failure( -# f'The file {data["main_script"]} could not be found. Please create it or set a different entry point.') -# sys.exit() -# -# # save model_id and plan back into blankly.json -# with open('blankly.json', 'w') as file: -# json.dump(data, file, indent=4) -# -# return data +def ensure_model(api: API): + # create model if it doesn't exist + try: + with open('blankly.json', 'r') as file: + data = json.load(file) + except FileNotFoundError: + print_failure('There was no model detected in this directory. Try running `blankly init` to create one') + sys.exit(1) + + if 'plan' not in data: + data['plan'] = select('Select a plan:', [Choice(f'{name} - CPU: {info["cpu"]} RAM: {info["ram"]}', name) + for name, info in api.get_plans('live').items()]).unsafe_ask() + + if 'model_id' not in data or 'project_id' not in data: + model = get_model_interactive(api, data.get('type', 'strategy')) + data['model_id'] = model['modelId'] + data['project_id'] = model['projectId'] + + if 'api_key' not in data or 'api_pass' not in data: + keys = api.generate_keys(data['project_id'], f'Local keys for {data["model_id"]}') + data['api_key'] = keys['apiKey'] + data['api_pass'] = keys['apiPass'] + + files = [f for f in os.listdir() if not f.startswith('.')] + if 'main_script' not in data or data['main_script'].lstrip('./') not in files: + if 'bot.py' in files: + data['main_script'] = 'bot.py' + else: + data['main_script'] = path( + 'What is the path to your main script/entry point? (Usually bot.py)').unsafe_ask() + + if data['main_script'].lstrip('./') not in files: + print_failure( + f'The file {data["main_script"]} could not be found. Please create it or set a different entry point.') + sys.exit() + + # save model_id and plan back into blankly.json + with open('blankly.json', 'w') as file: + json.dump(data, file, indent=4) + + return data def missing_deployment_files() -> list: @@ -414,39 +410,39 @@ def missing_deployment_files() -> list: return [path for path in paths if not Path(path).is_file()] -# def blankly_deploy(args): -# api = ensure_login() -# -# data = ensure_model(api) -# for path in missing_deployment_files(): -# if not confirm(f'{path} is missing. Are you sure you want to continue?', -# default=False).unsafe_ask(): -# print_failure('Deployment cancelled') -# print_failure(f'You can try `blankly init` to regenerate the {path} file.') -# return -# -# description = text('Enter a description for this version of the model:').unsafe_ask() -# -# with show_spinner('Uploading model') as spinner: -# model_path = zip_dir('.', data['ignore_files']) -# -# params = { -# 'file_path': model_path, -# 'project_id': data['project_id'], # set by ensure_model -# 'model_id': data['model_id'], # set by ensure_model -# 'version_description': description, -# 'python_version': get_python_version(), -# 'type_': data.get('type', 'strategy'), -# 'plan': data['plan'] # set by ensure_model -# } -# if data['type'] == 'screener': -# params['schedule'] = data['screener']['schedule'] -# -# response = api.deploy(**params) -# if response.get('status', None) == 'success': -# spinner.ok('Model uploaded') -# else: -# spinner.fail('Error: ' + response['error']) +def blankly_deploy(args): + api = ensure_login() + + data = ensure_model(api) + for path in missing_deployment_files(): + if not confirm(f'{path} is missing. Are you sure you want to continue?', + default=False).unsafe_ask(): + print_failure('Deployment cancelled') + print_failure(f'You can try `blankly init` to regenerate the {path} file.') + return + + description = text('Enter a description for this version of the model:').unsafe_ask() + + with show_spinner('Uploading model') as spinner: + model_path = zip_dir('.', data['ignore_files']) + + params = { + 'file_path': model_path, + 'project_id': data['project_id'], # set by ensure_model + 'model_id': data['model_id'], # set by ensure_model + 'version_description': description, + 'python_version': get_python_version(), + 'type_': data.get('type', 'strategy'), + 'plan': data['plan'] # set by ensure_model + } + if data['type'] == 'screener': + params['schedule'] = data['screener']['schedule'] + + response = api.deploy(**params) + if response.get('status', None) == 'success': + spinner.ok('Model uploaded') + else: + spinner.fail('Error: ' + response['error']) def blankly_add_key(args): @@ -499,14 +495,14 @@ def main(): help='don\'t prompt to connect to Blankly Platform') init_parser.set_defaults(func=blankly_init) - # login_parser = subparsers.add_parser('login', help='Login to the Blankly Platform') - # login_parser.set_defaults(func=blankly_login) + login_parser = subparsers.add_parser('login', help='Login to the Blankly Platform') + login_parser.set_defaults(func=blankly_login) - # logout_parser = subparsers.add_parser('logout', help='Logout of the Blankly Platform') - # logout_parser.set_defaults(func=blankly_logout) + logout_parser = subparsers.add_parser('logout', help='Logout of the Blankly Platform') + logout_parser.set_defaults(func=blankly_logout) - # deploy_parser = subparsers.add_parser('deploy', help='Upload this model to the Blankly Platform') - # deploy_parser.set_defaults(func=blankly_deploy) + deploy_parser = subparsers.add_parser('deploy', help='Upload this model to the Blankly Platform') + deploy_parser.set_defaults(func=blankly_deploy) key_parser = subparsers.add_parser('key', help='Manage model Exchange API keys') key_parser.set_defaults(func=blankly_key) diff --git a/blankly/deployment/reporter_headers.py b/blankly/deployment/reporter_headers.py index c47e529e..29e0627f 100644 --- a/blankly/deployment/reporter_headers.py +++ b/blankly/deployment/reporter_headers.py @@ -188,40 +188,3 @@ def email(self, email: str): email: The body of the email to send """ self.__send_email(email) - - def chat(self, message: str): - """ - Sends a text message to a Google Chat Space through a webhook_url - Create the webhook following these instructions: - https://developers.google.com/chat/how-tos/webhooks#register_the_incoming_webhook - Register the incoming webhook - 1. In a browser, open Chat. Webhooks aren't configurable from the Chat mobile app. - 2. Go to the space where you want to add a webhook. - 3. Next to the space title, click the expand more arrow, and then click Apps & integrations. - 4. Click Add webhooks. - 5. In the Name field, enter a name for your webhook (such as "Blankly Notifications"). - 6. In the Avatar URL field, enter a URL to an icon (such as https://blankly.finance/_nuxt/img/blankly-black.c8ffff6.svg). - 7. Click Save. - 8. To copy the webhook URL, click more_vert More, and then click Copy link. - Args: - message: The text of the message to send to the webhook_url specified in notify.json - """ - # https://github.com/googleworkspace/google-chat-samples/blob/main/python/webhook/quickstart.py - from json import dumps - from httplib2 import Http - - try: - notify_preferences = load_notify_preferences() - webhook = notify_preferences['chat']['webhook_url'] - - app_message = {"text": message} - message_headers = {"Content-Type": "application/json; charset=UTF-8"} - http_obj = Http() - response = http_obj.request( - uri=webhook, - method="POST", - headers=message_headers, - body=dumps(app_message), - ) - except KeyError: - raise KeyError("Google Chat webhook URL not found. Check the notify.json documentation") diff --git a/blankly/exchanges/exchange.py b/blankly/exchanges/exchange.py index 39797cb2..43fc61f6 100644 --- a/blankly/exchanges/exchange.py +++ b/blankly/exchanges/exchange.py @@ -98,13 +98,13 @@ def get_preferences(self): def get_interface(self) -> ABCExchangeInterface: """ - Get the authenticated interface for the object. This will provide authenticated API calls. + Get the the authenticated interface for the object. This will provide authenticated API calls. """ return self.interface def start_models(self, symbol=None): """ - Start all models or a specific one after appending it to the exchange. + Start all models or a specific one after appending it to to the exchange. This is used only for multiprocessed bots which are appended directly to the exchange. NOT bots that use the strategy class. diff --git a/blankly/exchanges/futures/futures_exchange.py b/blankly/exchanges/futures/futures_exchange.py index fcec3505..3bafa55d 100644 --- a/blankly/exchanges/futures/futures_exchange.py +++ b/blankly/exchanges/futures/futures_exchange.py @@ -16,7 +16,7 @@ along with this program. If not, see . """ import abc -from blankly.utils.utils import info_print +import time import blankly from blankly.exchanges.abc_base_exchange import ABCBaseExchange @@ -29,9 +29,6 @@ class FuturesExchange(ABCBaseExchange, abc.ABC): portfolio_name: str def __init__(self, exchange_type, portfolio_name, preferences_path): - info_print("Live futures trading is untested due to US regulations - this prevents blankly developers " - "from integrating with these exchanges. We are looking for someone who is interested in helping us " - "create & test our integrations.") self.exchange_type = exchange_type # binance, ftx self.portfolio_name = portfolio_name # my_cool_portfolio self.preferences = blankly.utils.load_user_preferences( diff --git a/blankly/exchanges/interfaces/binance/binance_interface.py b/blankly/exchanges/interfaces/binance/binance_interface.py index a0371233..a465f535 100644 --- a/blankly/exchanges/interfaces/binance/binance_interface.py +++ b/blankly/exchanges/interfaces/binance/binance_interface.py @@ -44,13 +44,12 @@ def init_exchange(self): import binance.exceptions try: self.calls.get_account() - except binance.exceptions.BinanceAPIException as e: - raise exceptions.APIException(f"Debugging info: {e} - {e.response} - {e.message}\n" - "Invalid API Key, IP, or permissions for action - are you trying " + except binance.exceptions.BinanceAPIException: + raise exceptions.APIException("Invalid API Key, IP, or permissions for action - are you trying " "to use your normal exchange keys while in sandbox mode? " "\nTry toggling the \'sandbox\' setting in your keys.json, check " "if the keys were input correctly into your keys.json or ensure you have set " - "the correct binance_tld in settings.json.\n") + "the correct binance_tld in settings.json.") symbols = self.calls.get_exchange_info()["symbols"] assets = [] @@ -800,9 +799,6 @@ def _binance_get_product_history(calls, symbol, epoch_start, epoch_stop, resolut def get_order_filter(self, symbol): """ Optimally we'll just remove the filter section and make the returns accurate - - - FOR THE US EXCHANGE: { "symbol": "BTCUSD", "status": "TRADING", @@ -861,141 +857,8 @@ def get_order_filter(self, symbol): {"filterType": "MAX_NUM_ALGO_ORDERS", "maxNumAlgoOrders": 5}, ], "permissions": ["SPOT"], - } - - - FOR THE INTERNATIONAL EXCHANGE: - { - "symbol": "ETHUSDT", - "status": "TRADING", - "baseAsset": "ETH", - "baseAssetPrecision": 8, - "quoteAsset": "USDT", - "quotePrecision": 8, - "quoteAssetPrecision": 8, - "baseCommissionPrecision": 8, - "quoteCommissionPrecision": 8, - "orderTypes": [ - "LIMIT", - "LIMIT_MAKER", - "MARKET", - "STOP_LOSS_LIMIT", - "TAKE_PROFIT_LIMIT" - ], - "icebergAllowed": true, - "ocoAllowed": true, - "quoteOrderQtyMarketAllowed": false, - "allowTrailingStop": true, - "cancelReplaceAllowed": true, - "isSpotTradingAllowed": true, - "isMarginTradingAllowed": true, - "filters": [ - { - "filterType": "PRICE_FILTER", - "minPrice": "0.01000000", - "maxPrice": "1000000.00000000", - "tickSize": "0.01000000" - }, - { - "filterType": "LOT_SIZE", - "minQty": "0.00010000", - "maxQty": "9000.00000000", - "stepSize": "0.00010000" - }, - { - "filterType": "ICEBERG_PARTS", - "limit": 10 - }, - { - "filterType": "MARKET_LOT_SIZE", - "minQty": "0.00000000", - "maxQty": "2575.21545313", - "stepSize": "0.00000000" - }, - { - "filterType": "TRAILING_DELTA", - "minTrailingAboveDelta": 10, - "maxTrailingAboveDelta": 2000, - "minTrailingBelowDelta": 10, - "maxTrailingBelowDelta": 2000 - }, - { - "filterType": "PERCENT_PRICE_BY_SIDE", - "bidMultiplierUp": "5", - "bidMultiplierDown": "0.2", - "askMultiplierUp": "5", - "askMultiplierDown": "0.2", - "avgPriceMins": 5 - }, - { - "filterType": "NOTIONAL", - "minNotional": "10.00000000", - "applyMinToMarket": true, - "maxNotional": "9000000.00000000", - "applyMaxToMarket": false, - "avgPriceMins": 5 - }, - { - "filterType": "MAX_NUM_ORDERS", - "maxNumOrders": 200 - }, - { - "filterType": "MAX_NUM_ALGO_ORDERS", - "maxNumAlgoOrders": 5 - } - ], - "permissions": [ - "SPOT", - "MARGIN", - "TRD_GRP_004", - "TRD_GRP_005", - "TRD_GRP_006", - "TRD_GRP_009", - "TRD_GRP_010", - "TRD_GRP_011", - "TRD_GRP_012", - "TRD_GRP_013" - ], - "defaultSelfTradePreventionMode": "NONE", - "allowedSelfTradePreventionModes": [ - "NONE", - "EXPIRE_TAKER", - "EXPIRE_MAKER", - "EXPIRE_BOTH" - ] }, """ - us_filter_mapping = { - 'PRICE_FILTER': 0, - 'PERCENT_PRICE': 1, - 'LOT_SIZE': 2, - 'MIN_NOTIONAL': 3, - 'ICEBERG_PARTS': 4, - 'MARKET_LOT_SIZE': 5, - 'TRAILING_DELTA': 6, - 'MAX_NUM_ORDERS': 7, - 'MAX_NUM_ALGO_ORDERS': 8 - } - - international_filter_mapping = { - 'PRICE_FILTER': 0, - 'LOT_SIZE': 1, - 'ICEBERG_PARTS': 2, - 'MARKET_LOT_SIZE': 3, - 'TRAILING_DELTA': 4, - 'PERCENT_PRICE_BY_SIDE': 5, - 'NOTIONAL': 6, - 'MAX_NUM_ORDERS': 7, - 'MAX_NUM_ALGO_ORDERS': 8 - } - - def is_international_exchange(asset_dict: dict) -> bool: - """ - Given the dictionary for the asset, tell if it is an international exchange - """ - filters_ = asset_dict['filters'] - - return filters_[1]['filterType'] == 'LOT_SIZE' converted_symbol = utils.to_exchange_symbol(symbol, 'binance') current_price = None @@ -1005,49 +868,30 @@ def is_international_exchange(asset_dict: dict) -> bool: symbol_data = i current_price = float(self.calls.get_avg_price(symbol=converted_symbol)['price']) break - - if current_price is None: raise LookupError("Specified market not found") - using_international_exchange = is_international_exchange(symbol_data) - if using_international_exchange: - filter_mapping = international_filter_mapping - else: - filter_mapping = us_filter_mapping - filters = symbol_data["filters"] - hard_min_price = float(filters[filter_mapping['PRICE_FILTER']]["minPrice"]) - hard_max_price = float(filters[filter_mapping['PRICE_FILTER']]["maxPrice"]) - quote_increment = float(filters[filter_mapping['PRICE_FILTER']]["tickSize"]) - - # This is the only one where the keys are different - if using_international_exchange: - # TODO technically this multiplier is different for buy and sell sides, this should be reflected in the - # backtesting engine - multiplier_up = float(filters[filter_mapping['PERCENT_PRICE_BY_SIDE']]["bidMultiplierUp"]) - multiplier_down = float(filters[filter_mapping['PERCENT_PRICE_BY_SIDE']]["bidMultiplierDown"]) - else: - multiplier_up = float(filters[filter_mapping['PERCENT_PRICE']]["multiplierUp"]) - multiplier_down = float(filters[filter_mapping['PERCENT_PRICE']]["multiplierDown"]) + hard_min_price = float(filters[0]["minPrice"]) + hard_max_price = float(filters[0]["maxPrice"]) + quote_increment = float(filters[0]["tickSize"]) - percent_min_price = multiplier_down * current_price - percent_max_price = multiplier_up * current_price + percent_min_price = float(filters[1]["multiplierDown"]) * current_price + percent_max_price = float(filters[1]["multiplierUp"]) * current_price - min_quantity = float(filters[filter_mapping['LOT_SIZE']]["minQty"]) - max_quantity = float(filters[filter_mapping['LOT_SIZE']]["maxQty"]) - base_increment = float(filters[filter_mapping['LOT_SIZE']]["stepSize"]) + min_quantity = float(filters[2]["minQty"]) + max_quantity = float(filters[2]["maxQty"]) + base_increment = float(filters[2]["stepSize"]) - if using_international_exchange: - min_market_notational = float(filters[filter_mapping['NOTIONAL']]['minNotional']) - max_market_notational = float(filters[filter_mapping['NOTIONAL']]['maxNotional']) - else: - min_market_notational = float(filters[filter_mapping['MIN_NOTIONAL']]['minNotional']) - max_market_notational = 92233720368.547752 # For some reason equal to the first *11 digits* of 2^63 then - # it gets weird past the decimal + min_market_notational = float(filters[3]['minNotional']) + max_market_notational = 92233720368.547752 # For some reason equal to the first *11 digits* of 2^63 then + # it gets weird past the decimal # Must test both nowadays - max_orders = int(filters[filter_mapping['MAX_NUM_ORDERS']]["maxNumOrders"]) + try: + max_orders = int(filters[7]["maxNumOrders"]) + except KeyError: + max_orders = int(filters[6]["maxNumOrders"]) if percent_min_price < hard_min_price: min_price = hard_min_price @@ -1092,8 +936,8 @@ def is_international_exchange(asset_dict: dict) -> bool: }, }, "exchange_specific": { - 'limit_multiplier_up': multiplier_up, - 'limit_multiplier_down': multiplier_down + 'limit_multiplier_up': float(filters[1]["multiplierUp"]), + 'limit_multiplier_down': float(filters[1]["multiplierDown"]) } } diff --git a/blankly/exchanges/interfaces/binance_futures/binance_futures_interface.py b/blankly/exchanges/interfaces/binance_futures/binance_futures_interface.py index 17139ff3..a4a94d1c 100644 --- a/blankly/exchanges/interfaces/binance_futures/binance_futures_interface.py +++ b/blankly/exchanges/interfaces/binance_futures/binance_futures_interface.py @@ -420,9 +420,10 @@ def get_funding_rate_history(self, symbol: str, epoch_start: int, response = True while response: + # TODO: Changed by UG on 220614 - That way it works to backtest up to ~1y back. Change to original if needed. response = self.calls.futures_funding_rate( - symbol=symbol, - startTime=window, + symbol=symbol.replace('-', ''), + # startTime=window, limit=1000) # very stinky ^^ diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py index dde19806..192ed400 100644 --- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py +++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py @@ -19,13 +19,10 @@ from blankly.exchanges.auth.auth_constructor import AuthConstructor from blankly.exchanges.exchange import Exchange from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_api import API as CoinbaseProAPI -from blankly.utils import info_print class CoinbasePro(Exchange): def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None): - info_print("Coinbase Pro is being deprecated by Coinbase. We are working on a Coinbase Advanced Trade " - "integration") Exchange.__init__(self, "coinbase_pro", portfolio_name, settings_path) # Load the auth from the keys file diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py index a8f2a421..b88db10e 100644 --- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py +++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py @@ -596,13 +596,9 @@ def get_order_filter(self, symbol: str): if products is None: raise LookupError("Specified market not found") - base_min_size = float(products.pop('base_increment')) - # Coinbase removed base_max_size - base_max_size = float(100000000000000000) - base_increment = float(base_min_size) - - min_market_funds = products['min_market_funds'] - max_market_funds = 1000000000 # big number approach + base_min_size = float(products.pop('base_min_size')) + base_max_size = float(products.pop('base_max_size')) + base_increment = float(products.pop('base_increment')) return { "symbol": products.pop('id'), @@ -629,12 +625,12 @@ def get_order_filter(self, symbol: str): "quote_increment": float(products.pop('quote_increment')), # Specifies the min order price as well # as the price increment. "buy": { - "min_funds": float(min_market_funds), - "max_funds": float(max_market_funds), + "min_funds": float(products['min_market_funds']), + "max_funds": float(products['max_market_funds']), }, "sell": { - "min_funds": float(min_market_funds), - "max_funds": float(max_market_funds), + "min_funds": float(products.pop('min_market_funds')), + "max_funds": float(products.pop('max_market_funds')), }, }, "exchange_specific": {**products} diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_utils.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_utils.py index 65f9850d..88d7e094 100644 --- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_utils.py +++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_utils.py @@ -49,6 +49,6 @@ def generate_limit_order(size, price, side, product_id): return order @staticmethod - def generate_uuid() -> str: + def generate_uuid(): # This should be a reasonable approximation for order UUIDs - return str(uuid.uuid4()) + return uuid.uuid4() diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py index 557f6e4e..3b328a82 100644 --- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py +++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py @@ -209,5 +209,5 @@ def restart_ticker(self): self.on_message, self.on_error, self.on_close, - self.run_forever + self.read_websocket ) diff --git a/blankly/exchanges/interfaces/ftx/ftx_interface.py b/blankly/exchanges/interfaces/ftx/ftx_interface.py index 485d78f1..69195137 100644 --- a/blankly/exchanges/interfaces/ftx/ftx_interface.py +++ b/blankly/exchanges/interfaces/ftx/ftx_interface.py @@ -477,13 +477,9 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution): Returns: Dataframe with *at least* 'time (epoch)', 'low', 'high', 'open', 'close', 'volume' as columns. """ - return self.ftx_product_history(self.get_calls(), symbol, epoch_start, epoch_stop, resolution) - @staticmethod - def ftx_product_history(api, symbol, epoch_start, epoch_stop, resolution): - # TODO this is probably the wrong place to put this code - # shared binance history code needs to be refactored as well - symbol = symbol.replace("-", "/") + if "-" in symbol: + symbol = symbol.replace("-", "/") # epoch_start, epoch_stop = super().get_product_history(symbol, epoch_start, epoch_stop, resolution) epoch_start = utils.convert_epochs(epoch_start) @@ -510,7 +506,7 @@ def ftx_product_history(api, symbol, epoch_start, epoch_stop, resolution): # Close is always 1500 points ahead window_close = window_open + 1500 * resolution - response = api.get_product_history(symbol, window_open, window_close, resolution) + response = self.get_calls().get_product_history(symbol, window_open, window_close, resolution) history = history + response @@ -521,7 +517,7 @@ def ftx_product_history(api, symbol, epoch_start, epoch_stop, resolution): # Fill the remainder window_close = epoch_stop - response = api.get_product_history(symbol, window_open, window_close, resolution) + response = self.get_calls().get_product_history(symbol, window_open, window_close, resolution) history_block = history + response # print(history_block) history_block.sort(key=lambda x: x["time"]) diff --git a/blankly/exchanges/interfaces/ftx_futures/ftx_futures.py b/blankly/exchanges/interfaces/ftx_futures/ftx_futures.py index 2b857eba..82e1f771 100644 --- a/blankly/exchanges/interfaces/ftx_futures/ftx_futures.py +++ b/blankly/exchanges/interfaces/ftx_futures/ftx_futures.py @@ -25,11 +25,10 @@ def __init__(self, if tld != 'com': raise Exception( f'FTX Futures exchange does not support .{tld} tld.') - + self.__calls = FTXAPI(auth.keys['API_KEY'], auth.keys['API_SECRET'], - tld=tld, - _subaccount_name=auth.keys.get('SUBACCOUNT', None)) + tld=tld) self.__interface = FTXFuturesInterface(self.exchange_type, self.calls) diff --git a/blankly/exchanges/interfaces/ftx_futures/ftx_futures_interface.py b/blankly/exchanges/interfaces/ftx_futures/ftx_futures_interface.py index cacaf082..2259dc2f 100644 --- a/blankly/exchanges/interfaces/ftx_futures/ftx_futures_interface.py +++ b/blankly/exchanges/interfaces/ftx_futures/ftx_futures_interface.py @@ -21,7 +21,6 @@ from blankly.enums import MarginType, HedgeMode, Side, PositionMode, TimeInForce, ContractType, OrderStatus, OrderType from blankly.exchanges.interfaces.ftx.ftx_api import FTXAPI -from blankly.exchanges.interfaces.ftx.ftx_interface import FTXInterface from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface from blankly.exchanges.orders.futures.futures_order import FuturesOrder from blankly.utils import utils, time_builder @@ -34,7 +33,7 @@ class FTXFuturesInterface(FuturesExchangeInterface): @staticmethod def to_exchange_symbol(symbol: str): - base_asset, quote_asset = symbol.split('-', 1) + base_asset, quote_asset = symbol.split('-') if quote_asset != 'USD': raise ValueError('invalid symbol') return base_asset + '-PERP' # only perpetual contracts right now @@ -327,7 +326,8 @@ def get_funding_rate_resolution(self) -> int: return time_builder.build_hour() def get_product_history(self, symbol, epoch_start, epoch_stop, resolution): - return FTXInterface.ftx_product_history(self.calls,self.to_exchange_symbol(symbol), epoch_start, epoch_stop, resolution) + raise NotImplementedError + def get_maker_fee(self) -> float: raise NotImplementedError diff --git a/blankly/exchanges/interfaces/kucoin/kucoin_interface.py b/blankly/exchanges/interfaces/kucoin/kucoin_interface.py index bea9a2da..ea486b65 100644 --- a/blankly/exchanges/interfaces/kucoin/kucoin_interface.py +++ b/blankly/exchanges/interfaces/kucoin/kucoin_interface.py @@ -617,8 +617,6 @@ def get_price(self, symbol) -> float: """ response = self._market.get_ticker(symbol) response = self.__correct_api_call(response) - if response is None or 'msg' in response: - if response is None: - raise APIException("Unknown API error") + if 'msg' in response: raise APIException("Error: " + response['msg']) return float(response['price']) diff --git a/blankly/exchanges/interfaces/paper_trade/backtest/format_platform_result.py b/blankly/exchanges/interfaces/paper_trade/backtest/format_platform_result.py index 1a585cfc..c252d729 100644 --- a/blankly/exchanges/interfaces/paper_trade/backtest/format_platform_result.py +++ b/blankly/exchanges/interfaces/paper_trade/backtest/format_platform_result.py @@ -64,8 +64,10 @@ def __compress_dict_series(values_column: dict, time_dictionary: dict): time_dictionary[time_keys[0]]: last_value } for i in range(len(values_column)): - # This used to compress by saving the last value, but now it does not - output_dict[time_dictionary[time_keys[i]]] = values_column[values_keys[i]] + if last_value != values_column[values_keys[i]]: + output_dict[time_dictionary[time_keys[i]]] = values_column[values_keys[i]] + + last_value = values_column[values_keys[i]] return output_dict @@ -91,12 +93,12 @@ def __parse_backtest_trades(trades: list, limit_executed: list, limit_canceled: # TODO this wastes a few CPU cycles at the moment so it could be cleaned up for j in limit_executed: if trades[i]['id'] == j['id']: - trades[i]['executed_time'] = j['executed_time'] + trades[i]['time'] = j['executed_time'] break for j in limit_canceled: if trades[i]['id'] == j['id']: - trades[i]['canceled_time'] = j['canceled_time'] + trades[i]['canceledTime'] = j['canceled_time'] break elif trades[i]['type'] == 'market': # This adds in the execution price for the market orders @@ -135,6 +137,8 @@ def format_platform_result(backtest_result): traded_symbols.append(i['symbol']) # Set the account values to None + raw_or_resampled_account_values = None + # Now grab the account value dictionary itself # Now just replicate the format of the resampled version # This was the annoying backtest glitch that almost cost us an investor meeting so its important diff --git a/blankly/exchanges/interfaces/paper_trade/backtest_controller.py b/blankly/exchanges/interfaces/paper_trade/backtest_controller.py index 7c280b06..73e95acb 100644 --- a/blankly/exchanges/interfaces/paper_trade/backtest_controller.py +++ b/blankly/exchanges/interfaces/paper_trade/backtest_controller.py @@ -438,8 +438,8 @@ def sort_identifier(identifier_: dict): f'{symbol},' f'{int(j[0])},' f'{int(j[1]) + resolution},' # This adds resolution - # back to the exported - # time series + # back to the exported + # time series f'{resolution}.csv'), index=False) @@ -526,8 +526,7 @@ def add_prices(self, end = (end_date - epoch).total_seconds() # If start/ends are specified unevenly - if (start_date is None and stop_date is not None) or (start_date is not None and stop_date is None) or \ - (start is None and end is None): + if (start_date is None and stop_date is not None) or (start_date is not None and stop_date is None): raise ValueError("Both start and end dates must be set or use the 'to' argument.") self.__add_prices(symbol, start, end, resolution) @@ -628,7 +627,7 @@ def format_account_data(self, interface: PaperTradeInterface, local_time) -> \ quote_value = true_account[self.quote_currency]['available'] + true_account[self.quote_currency]['hold'] except KeyError as e: raise KeyError(f"Failed looking up {e}. Try changing your quote_account_value_in in backtest.json to be " - f"{e}, or try a tether coin in backtest.json like USDT depending on exchange.") + f"{e}.") try: del true_account[self.quote_currency] except KeyError: @@ -652,20 +651,9 @@ def format_account_data(self, interface: PaperTradeInterface, local_time) -> \ price = interface.get_price(currency_pair) except KeyError: # Must be a currency we have no data for - # This used to return zero but I believe all the cases where no data was avaiable have been elimated. - raise KeyError(f"Failed to quote {currency_pair} because no downloaded data for that pair is available. " - f"Make sure to set \"quote_account_value_in\" in \"backtest.json\" to match the prices " - f"you are using. For example if you are trading \"USD-JPY\", set your quote value " - f"to \"JPY\". Currently it is set to {self.quote_currency}") - - # This is needed for futures apparently - if is_future: - value_total += price * abs(true_available[i]) - no_trade_value += price * abs(no_trade_available[i]) - else: - # For stocks make sure not to use an absolute value - value_total += price * true_available[i] - no_trade_value += price * no_trade_available[i] + price = 0 + value_total += price * abs(true_available[i]) + no_trade_value += price * abs(no_trade_available[i]) # Make sure to add the time key in true_available['time'] = local_time @@ -878,7 +866,7 @@ def check_if_any_column_has_prices(price_dict: dict) -> bool: if not check_if_any_column_has_prices(self.prices): raise IndexError('No cached or downloaded data available. Try adding arguments such as to="1y" ' 'in the backtest command. If there should be data downloaded, try deleting your' - f' ./price_caches folder. Also ensure that "{frame_symbol}" is spelled correctly.') + ' ./price_caches folder.') else: raise IndexError(f"Data for symbol {frame_symbol} is empty. Are you using a symbol that " f"is incompatible " @@ -1123,7 +1111,7 @@ def add_trace(self_, figure_, time__, data_, label): for column in cycle_status: if column != 'time' and self.__account_was_used(column): - p = figure(frame_width=900, frame_height=200, x_axis_type='datetime') + p = figure(plot_width=900, plot_height=200, x_axis_type='datetime') add_trace(self, p, time_, cycle_status[column], column) # Add the no-trade line to the backtest @@ -1169,52 +1157,49 @@ def add_trace(self_, figure_, time__, data_, label): figures.append(p) show(bokeh_columns(figures)) - # info_print(f'Make an account to take advantage of the platform backtest viewer: ' - # f'https://app.blankly.finance/RETIe0J8EPSQz7wizoJX0OAFb8y1/62iIMVRKV7zkcpJysYlP/' - # f'75a0c190-4d8a-44e2-9310-c47d4d72b070/backtest') + info_print(f'Make an account to take advantage of the platform backtest viewer: ' + f'https://app.blankly.finance/RETIe0J8EPSQz7wizoJX0OAFb8y1/EZkgTZMLJVaZK6kNy0mv/' + f'2b2ff92c-ee41-42b3-9afb-387de9e4f894/backtest') # This is where we end the backtesting time stop_clock = time.time() - internal_backtest_viewer() - # TODO this code does a good job uploading finished backtests to the platform. This should be fixed to - # allow configuration in the settings to reference any self hosted version of the platform - # try: - # json_file = json.loads(open('./blankly.json').read()) - # api_key = json_file['api_key'] - # api_pass = json_file['api_pass'] - # # Need this to generate the URL - # # Need this to know where to post to - # model_id = json_file['model_id'] - # - # requests.post(f'https://events.blankly.finance/v1/backtest/result', json=platform_result, headers={ - # 'api_key': api_key, - # 'api_pass': api_pass, - # 'model_id': model_id - # }) - # - # requests.post(f'https://events.blankly.finance/v1/backtest/status', json={ - # 'successful': True, - # 'status_summary': 'Completed', - # 'status_details': '', - # 'time_elapsed': stop_clock - start_clock, - # 'backtest_id': platform_result['backtest_id'] - # }, headers={ - # 'api_key': api_key, - # 'api_pass': api_pass, - # 'model_id': model_id - # }) - # - # import webbrowser - # - # link = f'https://app.blankly.finance/{api_key}/{model_id}/{platform_result["backtest_id"]}' \ - # f'/backtest' - # webbrowser.open( - # link - # ) - # info_print(f'View your backtest here: {link}') - # except (FileNotFoundError, KeyError): - # internal_backtest_viewer() + try: + json_file = json.loads(open('./blankly.json').read()) + api_key = json_file['api_key'] + api_pass = json_file['api_pass'] + # Need this to generate the URL + # Need this to know where to post to + model_id = json_file['model_id'] + + requests.post(f'https://events.blankly.finance/v1/backtest/result', json=platform_result, headers={ + 'api_key': api_key, + 'api_pass': api_pass, + 'model_id': model_id + }) + + requests.post(f'https://events.blankly.finance/v1/backtest/status', json={ + 'successful': True, + 'status_summary': 'Completed', + 'status_details': '', + 'time_elapsed': stop_clock - start_clock, + 'backtest_id': platform_result['backtest_id'] + }, headers={ + 'api_key': api_key, + 'api_pass': api_pass, + 'model_id': model_id + }) + + import webbrowser + + link = f'https://app.blankly.finance/{api_key}/{model_id}/{platform_result["backtest_id"]}' \ + f'/backtest' + webbrowser.open( + link + ) + info_print(f'View your backtest here: {link}') + except (FileNotFoundError, KeyError): + internal_backtest_viewer() # Finally, write the figures in result_object.figures = figures diff --git a/blankly/exchanges/interfaces/paper_trade/backtest_result.py b/blankly/exchanges/interfaces/paper_trade/backtest_result.py index 909ed20f..c875b12a 100644 --- a/blankly/exchanges/interfaces/paper_trade/backtest_result.py +++ b/blankly/exchanges/interfaces/paper_trade/backtest_result.py @@ -15,7 +15,7 @@ You should have received a copy of the GNU Lesser General Public License along with this program. If not, see . """ -import pandas as pd + from pandas import DataFrame, to_datetime, Timestamp from blankly.utils import time_interval_to_seconds as _time_interval_to_seconds, info_print @@ -174,26 +174,3 @@ def __str__(self): return_string += i + ": " + str(self.user_callbacks[i]) + "\n" return return_string - - def to_dict(self) -> dict: - history_copy: pd.DataFrame = self.history_and_returns['history'].copy(deep=True) - value_column = None - for column in history_copy: - if column[0:13] == "Account Value": - value_column = column - - # Renae the account value to just value - history_copy = history_copy.rename(columns={value_column: 'value'}) - - result = { - 'metrics': self.metrics, - 'exchange': self.exchange, - 'trades': self.trades, - 'quote_currency': self.quote_currency, - 'start_time': self.start_time, - 'stop_time': self.stop_time, - # "returns": self.history_and_returns['returns'].to_dict(), - "history": history_copy.to_dict('records') - } - - return result diff --git a/blankly/exchanges/interfaces/paper_trade/futures/futures_paper_trade_interface.py b/blankly/exchanges/interfaces/paper_trade/futures/futures_paper_trade_interface.py index fbacaf66..03d67c84 100644 --- a/blankly/exchanges/interfaces/paper_trade/futures/futures_paper_trade_interface.py +++ b/blankly/exchanges/interfaces/paper_trade/futures/futures_paper_trade_interface.py @@ -159,7 +159,7 @@ def get_hedge_mode(self): return HedgeMode.ONEWAY def set_leverage(self, leverage: float, symbol: str = None): - if len(self.paper_positions): + if (not symbol and len(self.paper_positions)) or symbol in list(self.paper_positions.keys()): raise BacktestingException('can\'t set leverage with open positions') if symbol: self.leverage[symbol] = leverage @@ -264,13 +264,16 @@ def do_funding(self, symbol: str, rate: float): m = 1 self.paper_positions[symbol]['size'] *= 1 + (abs(rate) * m) - def calculate_position_value(self, symbol): + def calculate_position_value(self, symbol, size): position = self.paper_positions.get(symbol, None) if not position: return 0 entry_price = position['exchange_specific']['entry_price'] - current_price = self.get_price(symbol) * abs(position['size']) - return entry_price + (current_price - entry_price) * self.get_leverage(symbol) + current_price = self.get_price(symbol) * size + if position['size'] > 0: + return entry_price + (current_price - entry_price) * self.get_leverage(symbol) + else: + return entry_price + (entry_price - current_price) * self.get_leverage(symbol) def evaluate_limits(self): self.check_margin_call() @@ -306,7 +309,7 @@ def execute_orders(self): entry_price += notional # we sold off our position if is_closing: - value = self.calculate_position_value(order.symbol) + value = self.calculate_position_value(order.symbol, order.size) self.paper_account[quote]['available'] += value else: self.paper_account[quote]['hold'] -= notional @@ -314,9 +317,11 @@ def execute_orders(self): # this overwrites but it's fine, we don't need to update new_position = position_size + size_diff - # minimum possible position size you can buy, times two - # TODO improve this to just take minimum position size from exchange - if abs(new_position) >= 2 * utils.precision_to_increment(product['size_precision']): + # minimum possible position size you can buy, times 1.2 + """ + Multiplier changed by UG on 220713 in order to allow smaller sized orders + """ + if abs(new_position) >= 1.2 * utils.precision_to_increment(product['size_precision']): self.paper_positions[order.symbol] = { 'symbol': order.symbol, 'base_asset': base, @@ -331,13 +336,26 @@ def execute_orders(self): } } else: - del self.paper_positions[order.symbol] + try: + del self.paper_positions[order.symbol] + except Exception as e: + # Passing on all caught exceptions + # TODO: Should add logging for caught exceptions + pass self.paper_account[base + '-PERP']['available'] = new_position + # Fees calculations + if is_closing: + calculated_fee = self.get_maker_fee() * order.size * asset_price * self.get_leverage(order.symbol) + else: + calculated_fee = self.get_taker_fee() * order.size * asset_price * self.get_leverage(order.symbol) + self.paper_account[quote]['available'] -= abs(calculated_fee) + + def check_margin_call(self): called = [] for symbol, position in self.paper_positions.items(): - value = self.calculate_position_value(symbol) + value = self.calculate_position_value(symbol, abs(position['size'])) if self.cash + value < 0: # placing orders in here fucks with the dictionary called.append((symbol, position)) @@ -348,7 +366,7 @@ def check_margin_call(self): def _place_order(self, type: OrderType, symbol: str, side: Side, size: float, limit_price: float = 0, position_mode: PositionMode = PositionMode.BOTH, reduce_only: bool = False, - time_in_force: TimeInForce = TimeInForce.GTC) -> FuturesOrder: + time_in_force: TimeInForce = TimeInForce.GTC, is_closing: bool = False) -> FuturesOrder: self.add_symbol(symbol) product = self.get_products(symbol) @@ -400,10 +418,11 @@ def _place_order(self, type: OrderType, symbol: str, side: Side, size: float, li acc = self.paper_account[quote] fee = self.get_taker_fee() + maker_fee = self.get_maker_fee() if is_closing: - funds = (price * size) * (1 - fee) + funds = (price * size) * (1 - fee - maker_fee) - (fee + maker_fee) * size * self.get_leverage() else: - funds = (price * size) * (1 + fee) + funds = (price * size) * (1 + fee + maker_fee) + (fee + maker_fee) * size * self.get_leverage() order_id = self.gen_order_id() if not is_closing: @@ -424,21 +443,6 @@ def _place_order(self, type: OrderType, symbol: str, side: Side, size: float, li return order - def backtesting_time(self): - """ - TODO this is duplicated from paper_trade_interface as a quick fix - Return the backtest time if we're backtesting, if not just return None. If it's None the caller - assumes no backtesting. The function being overridden always returns None - """ - # This is for the inits because it happens with both live calls and in the past - if self.initial_time is not None and not self.backtesting: - return self.initial_time - # This is for the actual price loops - if self.backtesting: - return self.time() - else: - return None - @staticmethod def is_closing_position(position, side): if not position or position['size'] == 0: diff --git a/blankly/exchanges/interfaces/paper_trade/local_account/trade_local.py b/blankly/exchanges/interfaces/paper_trade/local_account/trade_local.py index 31a07261..529220c1 100644 --- a/blankly/exchanges/interfaces/paper_trade/local_account/trade_local.py +++ b/blankly/exchanges/interfaces/paper_trade/local_account/trade_local.py @@ -72,14 +72,7 @@ def trade_local(self, symbol, side, base_delta, quote_delta, quote_resolution, b except KeyError: raise KeyError("Quote currency specified not found in local account") - def test_trade(self, currency_pair, - side, - qty, - quote_price, - quote_resolution, - base_resolution, - shortable, - calculate_margin=True) -> bool: + def test_trade(self, currency_pair, side, qty, quote_price, quote_resolution, base_resolution, shortable) -> bool: """ Test a paper trade to see if you have the funds @@ -92,9 +85,6 @@ def test_trade(self, currency_pair, """ # Nobody knows what's happening if it's shorting if shortable: - # If there are no issues with margin every trade is viable - if not calculate_margin: - return True base_asset = utils.get_base_asset(currency_pair) quote_asset = utils.get_quote_asset(currency_pair) base_account = self.local_account[base_asset] diff --git a/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py b/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py index 1575bfc5..cdf83ea9 100644 --- a/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py +++ b/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py @@ -19,7 +19,6 @@ import threading import time import traceback -import warnings import blankly.exchanges.interfaces.paper_trade.utils as paper_trade import blankly.utils.utils as utils @@ -66,16 +65,6 @@ def __init__(self, derived_interface: ABCExchangeInterface, initial_account_valu # self.evaluate_traded_account_assets() self.__enable_shorting = self.user_preferences['settings']['alpaca']['enable_shorting'] - self.__calculate_margin = self.user_preferences['settings']['simulate_margin'] - - # This logically overrides any __enable_shorting - self.__force_shorting = self.user_preferences['settings']['global_shorting'] - - if self.user_preferences['settings']['paper']['price_source'] == 'websocket': - from blankly.exchanges.managers.ticker_manager import TickerManager - warnings.warn("Experimental websocket prices enabled.") - self.__ticker_manager = TickerManager(self.get_exchange_type(), default_symbol='') - self._websocket_update = lambda *args: None @property def local_account(self): @@ -159,7 +148,7 @@ def start_paper_trade_watchdog(self): # TODO, this process could use variable update time/websocket usage, poll `time` and a variety of settings # to create a robust trading system # Create the watchdog for watching limit orders - self.__thread = threading.Thread(target=self.__paper_trade_watchdog, daemon=True) + self.__thread = threading.Thread(target=self.__paper_trade_watchdog(), daemon=True) self.__thread.start() self.__run_watchdog = True @@ -172,7 +161,6 @@ def __paper_trade_watchdog(self): """ Internal order watching system """ - utils.info_print('Evaluating paper limit orders every 10 seconds...') while True: time.sleep(10) if not self.__run_watchdog: @@ -352,10 +340,10 @@ def get_account(self, symbol=None) -> utils.AttributeDict: return self.local_account.get_account(symbol) except KeyError: if self.backtesting: - raise KeyError(f"Symbol {symbol} not found. This can be caused by an invalid quote currency " + raise KeyError("Symbol not found. This can be caused by an invalid quote currency " "in backtest.json.") else: - raise KeyError(f"Symbol {symbol} not found.") + raise KeyError("Symbol not found.") def take_profit_order(self, symbol: str, price: float, size: float) -> TakeProfitOrder: # we don't simulate partial fills, this is the same as take_profit @@ -414,9 +402,7 @@ def market_order(self, symbol, side, size) -> MarketOrder: # Test the purchase self.local_account.test_trade(symbol, side, qty, price, market_limits['market_order']["quote_increment"], - quantity_decimals, - (shortable and self.__enable_shorting) or self.__force_shorting, - calculate_margin=self.__calculate_margin) + quantity_decimals, (shortable and self.__enable_shorting)) # Create coinbase pro-like id coinbase_pro_id = paper_trade.generate_coinbase_pro_id() # TODO the force typing here isn't strictly necessary because its run int the isolate_specific anyway @@ -751,26 +737,7 @@ def get_price(self, symbol) -> float: if self.backtesting: return self.get_backtesting_price(symbol) else: - def idle(tick): - pass - # Only get crazy websocket prices if asked - if self.user_preferences['settings']['paper']['price_source'] == 'websocket': - tickers = self.__ticker_manager.get_all_tickers() - if self.get_exchange_type() in tickers and symbol in tickers[self.get_exchange_type()]: - most_recent_tick = tickers[self.get_exchange_type()][symbol].get_most_recent_tick() - - if most_recent_tick is None: - utils.info_print("No data found on ticker yet - using API...") - return self.calls.get_price(symbol) - else: - return most_recent_tick['price'] - else: - utils.info_print(f"Creating ticker on symbol {symbol} for exchange {self.get_exchange_type()}...") - self.__ticker_manager.create_ticker(callback=self._websocket_update, override_symbol=symbol, - override_exchange=self.get_exchange_type()) - return self.calls.get_price(symbol) - else: - return self.calls.get_price(symbol) + return self.calls.get_price(symbol) @staticmethod def __evaluate_binance_limits(price: (int, float), order_filter): diff --git a/blankly/exchanges/managers/general_stream_manager.py b/blankly/exchanges/managers/general_stream_manager.py index 2d6d50e6..147e708d 100644 --- a/blankly/exchanges/managers/general_stream_manager.py +++ b/blankly/exchanges/managers/general_stream_manager.py @@ -138,11 +138,11 @@ def create_general_connection(self, callback, channel, log=None, override_symbol asset_id_cache = blankly.utils.to_exchange_symbol(asset_id_cache, "alpaca") if use_sandbox: websocket = Alpaca_Websocket(asset_id_cache, channel, log, - websocket_url= - "wss://stream.data.sandbox.alpaca.markets/{}/".format(stream)) + WEBSOCKET_URL= + "wss://paper-api.alpaca.markets/stream/v2/{}/".format(stream)) else: websocket = Alpaca_Websocket(asset_id_cache, channel, log, - websocket_url="wss://stream.data.alpaca.markets/v2/{}/".format(stream)) + WEBSOCKET_URL="wss://stream.data.alpaca.markets/v2/{}/".format(stream)) websocket.append_callback(callback) self.__websockets[channel][exchange_cache][asset_id_cache] = websocket diff --git a/blankly/exchanges/managers/ticker_manager.py b/blankly/exchanges/managers/ticker_manager.py index d7a69d13..1e6532c7 100644 --- a/blankly/exchanges/managers/ticker_manager.py +++ b/blankly/exchanges/managers/ticker_manager.py @@ -72,12 +72,6 @@ def create_ticker(self, callback, log: str = None, override_symbol: str = None, Direct ticker object """ - # Delete the symbol arg because it shouldn't be in kwargs - try: - del(kwargs['symbol']) - except KeyError: - pass - sandbox_mode = self.preferences['settings']['use_sandbox_websockets'] exchange_name = self.__default_exchange @@ -144,7 +138,7 @@ def create_ticker(self, callback, log: str = None, override_symbol: str = None, f"{random.randint(1, 100000000) * 100000000}]", **kwargs) ticker.append_callback(callback) self.__tickers['kucoin'][override_symbol] = ticker - elif exchange_name == 'okx': + elif exchange_name == "okx": if override_symbol is None: override_symbol = self.__default_symbol diff --git a/blankly/frameworks/model/model.py b/blankly/frameworks/model/model.py index 0a335ce3..65855e03 100644 --- a/blankly/frameworks/model/model.py +++ b/blankly/frameworks/model/model.py @@ -22,7 +22,6 @@ from blankly.exchanges.abc_base_exchange import ABCBaseExchange from blankly.exchanges.futures.futures_exchange import FuturesExchange from blankly.exchanges.interfaces.paper_trade.backtest_controller import BackTestController, BacktestResult -from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface from blankly.exchanges.interfaces.paper_trade.abc_backtest_controller import ABCBacktestController from blankly.exchanges.interfaces.paper_trade.futures.futures_paper_trade import FuturesPaperTrade from blankly.exchanges.interfaces.paper_trade.paper_trade import PaperTrade @@ -37,9 +36,7 @@ def __init__(self, exchange: ABCBaseExchange): self.__exchange_cache = self.__exchange self.is_backtesting = False - # TODO every instance usage of this uses spot, this should be refactored to give linting for futures or spot - # depending on what people are running - self.interface: ABCExchangeInterface = exchange.get_interface() + self.interface = exchange.get_interface() self.has_data = True @@ -77,9 +74,6 @@ def backtest(self, args, initial_values: dict = None, settings_path: str = None, def run(self, args: typing.Any = None) -> threading.Thread: thread = threading.Thread(target=self.main, args=(args,)) thread.start() - # Don't force them to always enable limit order watch - if isinstance(self.__exchange, Exchange) and isinstance(self.__exchange, PaperTrade): - self.__exchange.start_limit_order_watch() return thread @abc.abstractmethod diff --git a/blankly/frameworks/screener/screener_runner.py b/blankly/frameworks/screener/screener_runner.py index 03f96335..0c2e3982 100644 --- a/blankly/frameworks/screener/screener_runner.py +++ b/blankly/frameworks/screener/screener_runner.py @@ -32,11 +32,7 @@ def __init__(self, cronjob: str): self.croniter = croniter except ImportError: raise ImportError("To run screeners locally, please \"pip install croniter\".") - try: - self.__main = __main__.__file__ - except AttributeError as e: - raise AttributeError(f"Please make a github issue for this error at " - f"https://github.com/blankly-finance/blankly/issues: \n{e}") + self.__main = __main__.__file__ self.croniter = self.croniter(cronjob, datetime.datetime.fromtimestamp(time.time()).astimezone(timezone.utc)) diff --git a/blankly/frameworks/strategy/strategy.py b/blankly/frameworks/strategy/strategy.py index 45581f3b..a74950af 100644 --- a/blankly/frameworks/strategy/strategy.py +++ b/blankly/frameworks/strategy/strategy.py @@ -53,7 +53,7 @@ def construct_strategy(self, schedulers, orderbook_websockets, self.orderbook_manager = orderbook_manager self.ticker_manager = ticker_manager - def rest_event(self, **event): + def rest_event(self, event): callback = event['callback'] # type: callable symbol = event['symbol'] # type: str resolution = event['resolution'] # type: int @@ -70,13 +70,10 @@ def rest_event(self, **event): while True: # Sometimes coinbase doesn't download recent data correctly try: - # A few seconds should pass before querying alpaca because any solution is acceptable + data = self.interface.history(symbol=symbol, to=1, resolution=resolution).iloc[-1].to_dict() if self.interface.get_exchange_type() == "alpaca": - time.sleep(2) - data = self.interface.history(symbol=symbol, to=1, resolution=resolution).iloc[-1].to_dict() break else: - data = self.interface.history(symbol=symbol, to=1, resolution=resolution).iloc[-1].to_dict() if data['time'] + resolution == bar_time: break except IndexError: @@ -85,11 +82,21 @@ def rest_event(self, **event): else: # If we are backtesting always just grab the last point and hope for the best of course try: - data = self.interface.history(symbol=symbol, to=1, resolution=resolution).iloc[-1].to_dict() + data = self.interface.history(symbol=symbol, to=2, resolution=resolution, start_date = self.backtester.time, end_date = self.backtester.time + resolution).iloc[-1].to_dict() # Use this line when working with BinanceFutures except IndexError: warnings.warn("No bar found for this time range") return + # don't send duplicate events + if data['time'] == event.get('prev_ev_time', None): + return + event['prev_ev_time'] = data['time'] + + # delay event once to sync up with historical data + was_delayed = event.get('was_delayed', False) + if not was_delayed and data['time'] < self.time: + return data['time'] + resolution + args = [data, symbol, state] elif type_ == EventType.price_event: data = self.interface.get_price(symbol) @@ -141,7 +148,7 @@ def run_price_events(self, events: list): self.sleep(event['next_run'] - self.time) # Run the event - next_run = self.rest_event(**event) + next_run = self.rest_event(event) if next_run: # if rest_event returns something, run this event again at that time # this implies the event did *not* run diff --git a/blankly/frameworks/strategy/strategy_base.py b/blankly/frameworks/strategy/strategy_base.py index 6425c644..1acd8e29 100644 --- a/blankly/frameworks/strategy/strategy_base.py +++ b/blankly/frameworks/strategy/strategy_base.py @@ -208,12 +208,8 @@ def __custom_price_event(self, blankly.reporter.export_used_symbol(symbol) @staticmethod - def __websocket_callback(tick, **kwargs): - user_callback = kwargs['user_callback'] - user_symbol = kwargs['user_symbol'] - user_state = kwargs['state'] - - user_callback(tick, user_symbol, user_state) + def __websocket_callback(tick, symbol, user_callback, state_object): + user_callback(tick, symbol, state_object) def add_tick_event(self, callback: callable, symbol: str, init: callable = None, teardown: callable = None, variables: dict = None): @@ -235,10 +231,8 @@ def add_tick_event(self, callback: callable, symbol: str, init: callable = None, state = StrategyState(self, AttributeDict(variables), symbol=symbol) self.ticker_manager.create_ticker(self.__websocket_callback, initially_stopped=True, - # This actually sets the symbol override_symbol=symbol, - # This is passed on to the user as info about the symbol (as just a kwarg) - user_symbol=symbol, + symbol=symbol, user_callback=callback, variables=variables, state=state) @@ -267,15 +261,13 @@ def add_orderbook_event(self, callback: callable, symbol: str, init: typing.Call # since it's less than 10 sec, we will just use the websocket feed - exchanges don't like fast calls self.orderbook_manager.create_orderbook(self.__websocket_callback, initially_stopped=True, - # This is the one that actually sets the symbol override_symbol=symbol, - # This is passed as a kwarg - user_symbol=symbol, + symbol=symbol, user_callback=callback, variables=variables, state=state) - self.orderbook_websockets.append([symbol, self.__exchange.get_type(), init, state, teardown]) + self.ticker_websockets.append([symbol, self.__exchange.get_type(), init, state, teardown]) def start(self): """ diff --git a/blankly/futures/utils.py b/blankly/futures/utils.py index 3e230782..eabb2fda 100644 --- a/blankly/futures/utils.py +++ b/blankly/futures/utils.py @@ -10,6 +10,32 @@ def close_position(symbol: str, state: FuturesStrategyState): state: the StrategyState symbol: the symbol to sell """ + position = state.interface.interface.get_position(symbol) + if not position: + position = state.interface.get_position(symbol) + if not position: + return + + if position['size'] < 0: + side = Side.BUY + elif position['size'] > 0: + side = Side.SELL + else: + # wtf? + return + + # TODO: CRITICAL! UNCOMMENT THE RIGHT LINE FOR YOUR USE CASE! SHOULD FIND A SOLID SOLUTION FOR BOTH CASES!!! + state.interface.market_order(symbol, side, abs(position['size']), reduce_only=True) # Use this line when backtesting + # state.interface.interface.market_order(symbol, side, abs(position['size']), reduce_only=True) # Use this line when live + + +def close_partialy_position(symbol: str, state: FuturesStrategyState, size): + """ + Exit a part of the position + Args: + state: the StrategyState + symbol: the symbol to sell + """ position = state.interface.get_position(symbol) if not position: return @@ -22,4 +48,7 @@ def close_position(symbol: str, state: FuturesStrategyState): # wtf? return - state.interface.market_order(symbol, side, abs(position['size']), reduce_only=True) + if abs(position['size']) < size: + return + + state.interface.market_order(symbol, side, size, reduce_only=True) diff --git a/blankly/indicators/indicators.py b/blankly/indicators/indicators.py index abcd1a25..ee932267 100644 --- a/blankly/indicators/indicators.py +++ b/blankly/indicators/indicators.py @@ -73,3 +73,26 @@ def average_true_range(high_data, low_data, close_data, period=50, use_series=Fa close_data = convert_to_numpy(close_data) atr = ti.atr(high_data, low_data, close_data, period=period) return pd.Series(atr) if use_series else atr + + +def adx(high_data, low_data, close_data, period=50, use_series=False): + if check_series(high_data) or check_series(low_data) or check_series(close_data): + use_series = True + high_data = convert_to_numpy(high_data) + low_data = convert_to_numpy(low_data) + close_data = convert_to_numpy(close_data) + adx = ti.adx(high_data, low_data, close_data, period=period) + return pd.Series(atr) if use_series else adx + + +def fibonacci_retracement(high_data, low_data): + high = max(high_data) + low = min(low_data) + + retracement_levels = [0.236, 0.382, 0.5, 0.618, 0.764] + calculated_retracement_levels = [] + + for retracement_level in retracement_levels: + calculated_retracement_levels.append(low + (high - low) * retracement_level) + + return calculated_retracement_levels diff --git a/blankly/indicators/moving_averages.py b/blankly/indicators/moving_averages.py index 1633eb54..83fa41d7 100644 --- a/blankly/indicators/moving_averages.py +++ b/blankly/indicators/moving_averages.py @@ -19,11 +19,32 @@ from typing import Any import pandas as pd +import tulipy import tulipy as ti +import pandas_ta from blankly.indicators.utils import check_series, convert_to_numpy +def supertrend(data: Any, period: int = 12, multiplier: int = 3, use_series=False) -> Any: + if check_series(data): + use_series = True + supertrend = pandas_ta.supertrend( + high=data['high'], + low=data['low'], + close=data['close'], + length=period, + multiplier=multiplier) + return pd.Series(supertrend) if use_series else supertrend + + +def dema(data: Any, period: int = 50, use_series=False) -> Any: + if check_series(data): + use_series = True + dema = pandas_ta.dema(data, period) + return pd.Series(dema) if use_series else dema + + def ema(data: Any, period: int = 50, use_series=False) -> Any: if check_series(data): use_series = True diff --git a/blankly/indicators/oscillators.py b/blankly/indicators/oscillators.py index 20083144..55fbbabc 100644 --- a/blankly/indicators/oscillators.py +++ b/blankly/indicators/oscillators.py @@ -21,6 +21,7 @@ import numpy as np import pandas as pd import tulipy as ti +import pandas_ta from blankly.indicators.utils import check_series, convert_to_numpy @@ -101,3 +102,24 @@ def stochastic_rsi(data, period=14, smooth_pct_k=3, smooth_pct_d=3): stochrsi_D = stochrsi_K.rolling(smooth_pct_d).mean() return round(rsi_values, 2), round(stochrsi_K * 100, 2), round(stochrsi_D * 100, 2) + + +def volume_oscillator(volume, short_len=5, long_len=10): + """ + Returns a percent as a result for a volume oscillator. + :param volume: + :param short_len: + :param long_len: + :return: + """ + short_len_ema = pandas_ta.ema( + volume, + short_len + ) + + long_len_ema = pandas_ta.ema( + volume, + long_len + ) + + return 100 * (short_len_ema - long_len_ema) / long_len_ema \ No newline at end of file diff --git a/blankly/utils/utils.py b/blankly/utils/utils.py index 9841e65d..e90bb5e6 100644 --- a/blankly/utils/utils.py +++ b/blankly/utils/utils.py @@ -35,8 +35,6 @@ "websocket_buffer_size": 10000, "test_connectivity_on_auth": True, "auto_truncate": False, - "global_shorting": False, - "simulate_margin": True, "coinbase_pro": { "cash": "USD" @@ -53,7 +51,7 @@ "websocket_stream": "iex", "cash": "USD", "enable_shorting": True, - "use_yfinance": False + "use_yfinance": False, }, "oanda": { "cash": "USD" @@ -74,9 +72,6 @@ "ftx_futures": { "cash": "USD", "ftx_tld": "com" - }, - "paper": { - "price_source": "api" } } } @@ -844,7 +839,7 @@ def aggregate_prices_by_resolution(price_dict, symbol_, resolution_, data_) -> d return price_dict -def extract_price_by_resolution(prices, symbol, epoch_start, epoch_stop, resolution): +def extract_price_by_resolution(prices, symbol, epoch_start, epoch_stop, resolution, ): if symbol in prices: if resolution in prices[symbol]: price_set = prices[symbol][resolution]