From 5dca183b7b275268227fe122c54c8dd305eb7954 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jan 2022 19:37:01 +0100 Subject: [PATCH 001/114] Combine order and Trade migrations to better facilitate migrations in advanced DB systems --- freqtrade/persistence/migrations.py | 44 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 1839c4130..de3179a3d 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -28,7 +28,10 @@ def get_backup_name(tabs, backup_prefix: str): return table_back_name -def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, cols: List): +def migrate_trades_and_orders_table( + decl_base, inspector, engine, + table_back_name: str, cols: List, + order_back_name: str): fee_open = get_column_def(cols, 'fee_open', 'fee') fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null') fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null') @@ -65,10 +68,14 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col # Schema migration necessary with engine.begin() as connection: connection.execute(text(f"alter table trades rename to {table_back_name}")) + with engine.begin() as connection: # drop indexes on backup table in new session for index in inspector.get_indexes(table_back_name): connection.execute(text(f"drop index {index['name']}")) + + drop_orders_table(inspector, engine, order_back_name) + # let SQLAlchemy create the schema as required decl_base.metadata.create_all(engine) @@ -103,6 +110,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col from {table_back_name} """)) + migrate_orders_table(decl_base, engine, order_back_name, cols) + def migrate_open_orders_to_trades(engine): with engine.begin() as connection: @@ -121,19 +130,18 @@ def migrate_open_orders_to_trades(engine): """)) -def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, cols: List): - # Schema migration necessary +def drop_orders_table(inspector, engine, table_back_name: str): + # Drop and recreate orders table as backup + # This drops foreign keys, too. with engine.begin() as connection: - connection.execute(text(f"alter table orders rename to {table_back_name}")) + connection.execute(text(f"create table {table_back_name} as select * from orders")) + connection.execute(text("drop table orders")) - with engine.begin() as connection: - # drop indexes on backup table in new session - for index in inspector.get_indexes(table_back_name): - connection.execute(text(f"drop index {index['name']}")) + +def migrate_orders_table(decl_base, engine, table_back_name: str, cols: List): # let SQLAlchemy create the schema as required - decl_base.metadata.create_all(engine) with engine.begin() as connection: connection.execute(text(f""" insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, @@ -155,11 +163,14 @@ def check_migrate(engine, decl_base, previous_tables) -> None: cols = inspector.get_columns('trades') tabs = get_table_names_for_table(inspector, 'trades') table_back_name = get_backup_name(tabs, 'trades_bak') + order_tabs = get_table_names_for_table(inspector, 'orders') + order_table_bak_name = get_backup_name(order_tabs, 'orders_bak') - # Check for latest column - if not has_column(cols, 'buy_tag'): + # Check if migration necessary + # Migrates both trades and orders table! logger.info(f'Running database migration for trades - backup: {table_back_name}') - migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) + migrate_trades_and_orders_table( + decl_base, inspector, engine, table_back_name, cols, order_table_bak_name) # Reread columns - the above recreated the table! inspector = inspect(engine) cols = inspector.get_columns('trades') @@ -167,12 +178,3 @@ def check_migrate(engine, decl_base, previous_tables) -> None: if 'orders' not in previous_tables and 'trades' in previous_tables: logger.info('Moving open orders to Orders table.') migrate_open_orders_to_trades(engine) - else: - cols_order = inspector.get_columns('orders') - - if not has_column(cols_order, 'average'): - tabs = get_table_names_for_table(inspector, 'orders') - # Empty for now - as there is only one iteration of the orders table so far. - table_back_name = get_backup_name(tabs, 'orders_bak') - - migrate_orders_table(decl_base, inspector, engine, table_back_name, cols) From 19948a6f89dde7ab3a9cc5433f0d0dfa50e96407 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jan 2022 06:51:04 +0100 Subject: [PATCH 002/114] Try fix sequence migrations --- freqtrade/persistence/migrations.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index de3179a3d..82f31d7a8 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -28,6 +28,27 @@ def get_backup_name(tabs, backup_prefix: str): return table_back_name +def get_last_sequence_ids(engine, inspector): + order_id: int = None + trade_id: int = None + + if engine.name == 'postgresql': + with engine.begin() as connection: + x = connection.execute( + text("select sequencename, last_value from pg_sequences")).fetchall() + ts = [s[1]for s in x if s[0].startswith('trades_id') and s[1] is not None] + os = [s[1] for s in x if s[0].startswith('orders_id') and s[1] is not None] + trade_id = max(ts) + order_id = max(os) + + return order_id, trade_id + + +def set_sequence_ids(engine, order_id, trade_id): + + if engine.name == 'postgresql': + pass + def migrate_trades_and_orders_table( decl_base, inspector, engine, table_back_name: str, cols: List, @@ -74,6 +95,8 @@ def migrate_trades_and_orders_table( for index in inspector.get_indexes(table_back_name): connection.execute(text(f"drop index {index['name']}")) + trade_id, order_id = get_last_sequence_ids(engine, inspector) + drop_orders_table(inspector, engine, order_back_name) # let SQLAlchemy create the schema as required @@ -111,6 +134,7 @@ def migrate_trades_and_orders_table( """)) migrate_orders_table(decl_base, engine, order_back_name, cols) + set_sequence_ids(engine, order_id, trade_id) def migrate_open_orders_to_trades(engine): From c265f393239bc0dc37338b12fac149b1dca1161c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jan 2022 12:55:35 +0100 Subject: [PATCH 003/114] Update sequences for postgres --- freqtrade/persistence/migrations.py | 34 +++++++++++++++++------------ 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 82f31d7a8..e96f83caa 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -28,30 +28,35 @@ def get_backup_name(tabs, backup_prefix: str): return table_back_name -def get_last_sequence_ids(engine, inspector): +def get_last_sequence_ids(engine, trade_back_name, order_back_name): order_id: int = None trade_id: int = None if engine.name == 'postgresql': with engine.begin() as connection: - x = connection.execute( - text("select sequencename, last_value from pg_sequences")).fetchall() - ts = [s[1]for s in x if s[0].startswith('trades_id') and s[1] is not None] - os = [s[1] for s in x if s[0].startswith('orders_id') and s[1] is not None] - trade_id = max(ts) - order_id = max(os) - + trade_id = connection.execute(text("select nextval('trades_id_seq')")).fetchone()[0] + order_id = connection.execute(text("select nextval('orders_id_seq')")).fetchone()[0] + with engine.begin() as connection: + connection.execute(text( + f"ALTER SEQUENCE orders_id_seq rename to {order_back_name}_id_seq_bak")) + connection.execute(text( + f"ALTER SEQUENCE trades_id_seq rename to {trade_back_name}_id_seq_bak")) return order_id, trade_id def set_sequence_ids(engine, order_id, trade_id): if engine.name == 'postgresql': - pass + with engine.begin() as connection: + if order_id: + connection.execute(text(f"ALTER SEQUENCE orders_id_seq RESTART WITH {order_id}")) + if trade_id: + connection.execute(text(f"ALTER SEQUENCE trades_id_seq RESTART WITH {trade_id}")) + def migrate_trades_and_orders_table( decl_base, inspector, engine, - table_back_name: str, cols: List, + trade_back_name: str, cols: List, order_back_name: str): fee_open = get_column_def(cols, 'fee_open', 'fee') fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null') @@ -88,14 +93,14 @@ def migrate_trades_and_orders_table( # Schema migration necessary with engine.begin() as connection: - connection.execute(text(f"alter table trades rename to {table_back_name}")) + connection.execute(text(f"alter table trades rename to {trade_back_name}")) with engine.begin() as connection: # drop indexes on backup table in new session - for index in inspector.get_indexes(table_back_name): + for index in inspector.get_indexes(trade_back_name): connection.execute(text(f"drop index {index['name']}")) - trade_id, order_id = get_last_sequence_ids(engine, inspector) + order_id, trade_id = get_last_sequence_ids(engine, trade_back_name, order_back_name) drop_orders_table(inspector, engine, order_back_name) @@ -130,7 +135,7 @@ def migrate_trades_and_orders_table( {sell_order_status} sell_order_status, {strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs - from {table_back_name} + from {trade_back_name} """)) migrate_orders_table(decl_base, engine, order_back_name, cols) @@ -192,6 +197,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: # Check if migration necessary # Migrates both trades and orders table! + if not has_column(cols, 'buy_tag'): logger.info(f'Running database migration for trades - backup: {table_back_name}') migrate_trades_and_orders_table( decl_base, inspector, engine, table_back_name, cols, order_table_bak_name) From 3d94d7df5c3c259eddcf99e3991668f2efe83193 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jan 2022 19:19:04 +0100 Subject: [PATCH 004/114] Update migrations for mariadb --- freqtrade/persistence/migrations.py | 8 ++++++-- tests/test_persistence.py | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index e96f83caa..9f7ab29cd 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -98,7 +98,10 @@ def migrate_trades_and_orders_table( with engine.begin() as connection: # drop indexes on backup table in new session for index in inspector.get_indexes(trade_back_name): - connection.execute(text(f"drop index {index['name']}")) + if engine.name == 'mysql': + connection.execute(text(f"drop index {index['name']} on {trade_back_name}")) + else: + connection.execute(text(f"drop index {index['name']}")) order_id, trade_id = get_last_sequence_ids(engine, trade_back_name, order_back_name) @@ -198,7 +201,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None: # Check if migration necessary # Migrates both trades and orders table! if not has_column(cols, 'buy_tag'): - logger.info(f'Running database migration for trades - backup: {table_back_name}') + logger.info(f"Running database migration for trades - " + f"backup: {table_back_name}, {order_table_bak_name}") migrate_trades_and_orders_table( decl_base, inspector, engine, table_back_name, cols, order_table_bak_name) # Reread columns - the above recreated the table! diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d98238f6f..b239f6d70 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -600,7 +600,8 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.stoploss_last_update is None assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak2", caplog) - assert log_has("Running database migration for trades - backup: trades_bak2", caplog) + assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0", + caplog) assert trade.open_trade_value == trade._calc_open_trade_value() assert trade.close_profit_abs is None @@ -733,7 +734,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): assert trade.initial_stop_loss == 0.0 assert trade.open_trade_value == trade._calc_open_trade_value() assert log_has("trying trades_bak0", caplog) - assert log_has("Running database migration for trades - backup: trades_bak0", caplog) + assert log_has("Running database migration for trades - backup: trades_bak0, orders_bak0", caplog) def test_adjust_stop_loss(fee): From 05046b9eef0ff92e74fb48e1e9e90f6b6551d52f Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sat, 22 Jan 2022 06:54:49 +0000 Subject: [PATCH 005/114] Add more info on status message --- freqtrade/persistence/models.py | 15 ++++++++ freqtrade/rpc/rpc.py | 4 ++- freqtrade/rpc/telegram.py | 63 +++++++++++++++++++++++++++++++-- tests/rpc/test_rpc.py | 6 ++++ tests/test_persistence.py | 2 ++ 5 files changed, 87 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 98a5329ba..275a2f949 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -282,6 +282,20 @@ class LocalTrade(): return self.close_date.replace(tzinfo=timezone.utc) def to_json(self) -> Dict[str, Any]: + fill_buy = self.select_filled_orders('buy') + buys_json = dict() + if len(fill_buy) > 0: + for x in range(len(fill_buy)): + buy = dict( + cost=fill_buy[x].cost if fill_buy[x].cost else 0.0, + amount=fill_buy[x].amount, + price=fill_buy[x].price, + average=round(fill_buy[x].average, 8) if fill_buy[x].average else 0.0, + order_filled_date=fill_buy[x].order_filled_date.strftime(DATETIME_PRINT_FORMAT) + if fill_buy[x].order_filled_date else None + ) + buys_json[str(x)] = buy + return { 'trade_id': self.id, 'pair': self.pair, @@ -345,6 +359,7 @@ class LocalTrade(): 'max_rate': self.max_rate, 'open_order_id': self.open_order_id, + 'filled_buys': buys_json, } @staticmethod diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 41fd37e51..bd6373cce 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -208,6 +208,8 @@ class RPC: order['type'], order['side'], order['remaining'] ) if order else None, )) + cp_cfg = self._config + trade_dict['position_adjustment_enable'] = cp_cfg['position_adjustment_enable'] results.append(trade_dict) return results @@ -242,7 +244,7 @@ class RPC: trade.id, trade.pair + ('*' if (trade.open_order_id is not None and trade.close_rate_requested is None) else '') - + ('**' if (trade.close_rate_requested is not None) else ''), + + ('**' if (trade.close_rate_requested is not None) else ''), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), profit_str ] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 716694a81..03eb836fa 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -369,6 +369,47 @@ class Telegram(RPCHandler): else: return "\N{CROSS MARK}" + def _prepare_buy_details(self, filled_trades, base_currency): + """ + Prepare details of trade with buy adjustment enabled + """ + lines = [] + for x in range(len(filled_trades)): + cur_buy_date = arrow.get(filled_trades[str(x)]["order_filled_date"]) + cur_buy_amount = filled_trades[str(x)]["amount"] + cur_buy_average = filled_trades[str(x)]["average"] + lines.append(" ") + if x == 0: + lines.append("*Buy #{}:*".format(x+1)) + lines.append("*Buy Amount:* {} ({:.8f} {})" + .format(cur_buy_amount, filled_trades[str(x)]["cost"], base_currency)) + lines.append("*Average Buy Price:* {}".format(cur_buy_average)) + else: + sumA = 0 + sumB = 0 + for y in range(x): + sumA += (filled_trades[str(y)]["amount"] * filled_trades[str(y)]["average"]) + sumB += filled_trades[str(y)]["amount"] + prev_avg_price = sumA/sumB + price_to_1st_buy = (cur_buy_average - filled_trades["0"]["average"]) \ + / filled_trades["0"]["average"] + minus_on_buy = (cur_buy_average - prev_avg_price)/prev_avg_price + dur_buys = cur_buy_date - arrow.get(filled_trades[str(x-1)]["order_filled_date"]) + days = dur_buys.days + hours, remainder = divmod(dur_buys.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + lines.append("*Buy #{}:* at {:.2%} avg profit".format(x+1, minus_on_buy)) + lines.append("({})".format(cur_buy_date + .humanize(granularity=["day", "hour", "minute"]))) + lines.append("*Buy Amount:* {} ({:.8f} {})" + .format(cur_buy_amount, filled_trades[str(x)]["cost"], base_currency)) + lines.append("*Average Buy Price:* {} ({:.2%} from 1st buy rate)" + .format(cur_buy_average, price_to_1st_buy)) + lines.append("*Filled at:* {}".format(filled_trades[str(x)]["order_filled_date"])) + lines.append("({}d {}h {}m {}s from previous buy)" + .format(days, hours, minutes, seconds)) + return lines + @authorized_only def _status(self, update: Update, context: CallbackContext) -> None: """ @@ -396,17 +437,31 @@ class Telegram(RPCHandler): messages = [] for r in results: r['open_date_hum'] = arrow.get(r['open_date']).humanize() + r['filled_buys'] = r.get('filled_buys', []) + r['num_buys'] = len(r['filled_buys']) + r['sell_reason'] = r.get('sell_reason', "") + r['position_adjustment_enable'] = r.get('position_adjustment_enable', False) lines = [ "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", + "*Sell Reason:* `{sell_reason}`" if r['sell_reason'] else "", + ] + + if r['position_adjustment_enable']: + lines.append("*Number of Buy(s):* `{num_buys}`") + + lines.extend([ "*Open Rate:* `{open_rate:.8f}`", - "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", + "*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "", + "*Open Date:* `{open_date}`", + "*Close Date:* `{close_date}`" if r['close_date'] else "", "*Current Rate:* `{current_rate:.8f}`", ("*Current Profit:* " if r['is_open'] else "*Close Profit: *") + "`{profit_ratio:.2%}`", - ] + ]) + if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_ratio'] is not None): # Adding initial stoploss only if it is different from stoploss @@ -424,6 +479,10 @@ class Telegram(RPCHandler): else: lines.append("*Open Order:* `{open_order}`") + if len(r['filled_buys']) > 1: + lines_detail = self._prepare_buy_details(r['filled_buys'], r['base_currency']) + lines.extend(lines_detail) + # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line]).format(**r)) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index e86022a91..0ea147c0a 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -108,6 +108,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', + 'position_adjustment_enable': False, + 'filled_buys': {'0': {'amount': 91.07468123, 'average': 1.098e-05, + 'cost': 0.0009999999999054, 'order_filled_date': ANY, 'price': 1.098e-05}}, } mocker.patch('freqtrade.exchange.Exchange.get_rate', @@ -175,6 +178,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', + 'position_adjustment_enable': False, + 'filled_buys': {'0': {'amount': 91.07468123, 'average': 1.098e-05, + 'cost': 0.0009999999999054, 'order_filled_date': ANY, 'price': 1.098e-05}}, } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d98238f6f..d6b12ea8c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -903,6 +903,7 @@ def test_to_json(default_conf, fee): 'buy_tag': None, 'timeframe': None, 'exchange': 'binance', + 'filled_buys': {} } # Simulate dry_run entries @@ -970,6 +971,7 @@ def test_to_json(default_conf, fee): 'buy_tag': 'buys_signal_001', 'timeframe': None, 'exchange': 'binance', + 'filled_buys': {} } From bd4014e1e67fb7e407797bffd855c9090fe6d36b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Jan 2022 19:42:31 +0100 Subject: [PATCH 006/114] Small cleanup --- freqtrade/persistence/migrations.py | 8 ++-- tests/test_persistence.py | 64 ++--------------------------- 2 files changed, 7 insertions(+), 65 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 9f7ab29cd..60c0eb5f9 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -105,7 +105,7 @@ def migrate_trades_and_orders_table( order_id, trade_id = get_last_sequence_ids(engine, trade_back_name, order_back_name) - drop_orders_table(inspector, engine, order_back_name) + drop_orders_table(engine, order_back_name) # let SQLAlchemy create the schema as required decl_base.metadata.create_all(engine) @@ -141,7 +141,7 @@ def migrate_trades_and_orders_table( from {trade_back_name} """)) - migrate_orders_table(decl_base, engine, order_back_name, cols) + migrate_orders_table(engine, order_back_name, cols) set_sequence_ids(engine, order_id, trade_id) @@ -162,7 +162,7 @@ def migrate_open_orders_to_trades(engine): """)) -def drop_orders_table(inspector, engine, table_back_name: str): +def drop_orders_table(engine, table_back_name: str): # Drop and recreate orders table as backup # This drops foreign keys, too. @@ -171,7 +171,7 @@ def drop_orders_table(inspector, engine, table_back_name: str): connection.execute(text("drop table orders")) -def migrate_orders_table(decl_base, engine, table_back_name: str, cols: List): +def migrate_orders_table(engine, table_back_name: str, cols: List): # let SQLAlchemy create the schema as required with engine.begin() as connection: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b239f6d70..8305fe0f5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -8,7 +8,7 @@ from unittest.mock import MagicMock import arrow import pytest -from sqlalchemy import create_engine, inspect, text +from sqlalchemy import create_engine, text from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException @@ -614,65 +614,6 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert orders[1].order_id == 'stop_order_id222' assert orders[1].ft_order_side == 'stoploss' - caplog.clear() - # Drop latest column - with engine.begin() as connection: - connection.execute(text("alter table orders rename to orders_bak")) - inspector = inspect(engine) - - with engine.begin() as connection: - for index in inspector.get_indexes('orders_bak'): - connection.execute(text(f"drop index {index['name']}")) - # Recreate table - connection.execute(text(""" - CREATE TABLE orders ( - id INTEGER NOT NULL, - ft_trade_id INTEGER, - ft_order_side VARCHAR NOT NULL, - ft_pair VARCHAR NOT NULL, - ft_is_open BOOLEAN NOT NULL, - order_id VARCHAR NOT NULL, - status VARCHAR, - symbol VARCHAR, - order_type VARCHAR, - side VARCHAR, - price FLOAT, - amount FLOAT, - filled FLOAT, - remaining FLOAT, - cost FLOAT, - order_date DATETIME, - order_filled_date DATETIME, - order_update_date DATETIME, - PRIMARY KEY (id), - CONSTRAINT _order_pair_order_id UNIQUE (ft_pair, order_id), - FOREIGN KEY(ft_trade_id) REFERENCES trades (id) - ) - """)) - - connection.execute(text(""" - insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, - symbol, order_type, side, price, amount, filled, remaining, cost, order_date, - order_filled_date, order_update_date) - select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, - symbol, order_type, side, price, amount, filled, remaining, cost, order_date, - order_filled_date, order_update_date - from orders_bak - """)) - - # Run init to test migration - init_db(default_conf['db_url'], default_conf['dry_run']) - - assert log_has("trying orders_bak1", caplog) - - orders = Order.query.all() - assert len(orders) == 2 - assert orders[0].order_id == 'buy_order' - assert orders[0].ft_order_side == 'buy' - - assert orders[1].order_id == 'stop_order_id222' - assert orders[1].ft_order_side == 'stoploss' - def test_migrate_mid_state(mocker, default_conf, fee, caplog): """ @@ -734,7 +675,8 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): assert trade.initial_stop_loss == 0.0 assert trade.open_trade_value == trade._calc_open_trade_value() assert log_has("trying trades_bak0", caplog) - assert log_has("Running database migration for trades - backup: trades_bak0, orders_bak0", caplog) + assert log_has("Running database migration for trades - backup: trades_bak0, orders_bak0", + caplog) def test_adjust_stop_loss(fee): From 480ed90a02b1b05e765096364af876569589bb17 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 23 Jan 2022 11:33:06 +0000 Subject: [PATCH 007/114] create to_json function for Order --- freqtrade/persistence/models.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 275a2f949..14ac65e61 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -165,6 +165,16 @@ class Order(_DECL_BASE): self.order_filled_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc) + def to_json(self) -> Dict[str, Any]: + return { + 'cost': self.cost if self.cost else 0, + 'amount': self.amount, + 'price': self.price, + 'average': round(self.average, 8) if self.average else 0, + 'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT) + if self.order_filled_date else None + } + @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): """ @@ -286,15 +296,7 @@ class LocalTrade(): buys_json = dict() if len(fill_buy) > 0: for x in range(len(fill_buy)): - buy = dict( - cost=fill_buy[x].cost if fill_buy[x].cost else 0.0, - amount=fill_buy[x].amount, - price=fill_buy[x].price, - average=round(fill_buy[x].average, 8) if fill_buy[x].average else 0.0, - order_filled_date=fill_buy[x].order_filled_date.strftime(DATETIME_PRINT_FORMAT) - if fill_buy[x].order_filled_date else None - ) - buys_json[str(x)] = buy + buys_json[str(x)] = fill_buy[x].to_json() return { 'trade_id': self.id, From bf62fc9b25523a1f119b30304ebce1a111397451 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Sun, 23 Jan 2022 21:58:46 +0200 Subject: [PATCH 008/114] Add /health endpoint that returns last_process timestamp, fix issue #6009 --- freqtrade/freqtradebot.py | 3 +++ freqtrade/rpc/api_server/api_schemas.py | 4 ++++ freqtrade/rpc/api_server/api_v1.py | 17 +++++++++++------ freqtrade/rpc/rpc.py | 5 +++++ freqtrade/rpc/telegram.py | 18 +++++++++++++++++- tests/rpc/test_rpc.py | 9 +++++++++ tests/rpc/test_rpc_apiserver.py | 10 ++++++++++ 7 files changed, 59 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e8f24864f..310f21b7b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -100,6 +100,8 @@ class FreqtradeBot(LoggingMixin): self._exit_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) + self.last_process = datetime.utcfromtimestamp(0.0) + def notify_status(self, msg: str) -> None: """ Public method for users of this class (worker, etc.) to send notifications @@ -187,6 +189,7 @@ class FreqtradeBot(LoggingMixin): self.enter_positions() Trade.commit() + self.last_process = datetime.utcnow() def process_stopped(self) -> None: """ diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index bbd858795..5709639b3 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -382,3 +382,7 @@ class BacktestResponse(BaseModel): class SysInfo(BaseModel): cpu_pct: List[float] ram_pct: float + + +class Health(BaseModel): + last_process: datetime diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 4c430dd46..506c886a3 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -14,12 +14,12 @@ from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count, Daily, DeleteLockRequest, DeleteTrade, ForceBuyPayload, - ForceBuyResponse, ForceSellPayload, Locks, Logs, - OpenTradeSchema, PairHistory, PerformanceEntry, - Ping, PlotConfig, Profit, ResultMsg, ShowConfig, - Stats, StatusMsg, StrategyListResponse, - StrategyResponse, SysInfo, Version, - WhitelistResponse) + ForceBuyResponse, ForceSellPayload, Health, + Locks, Logs, OpenTradeSchema, PairHistory, + PerformanceEntry, Ping, PlotConfig, Profit, + ResultMsg, ShowConfig, Stats, StatusMsg, + StrategyListResponse, StrategyResponse, SysInfo, + Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_exchange, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -290,3 +290,8 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option @router.get('/sysinfo', response_model=SysInfo, tags=['info']) def sysinfo(): return RPC._rpc_sysinfo() + + +@router.get('/health', response_model=Health, tags=['info']) +def health(rpc: RPC = Depends(get_rpc)): + return rpc._health() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c78ff1079..fa301ed60 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1030,3 +1030,8 @@ class RPC: "cpu_pct": psutil.cpu_percent(interval=1, percpu=True), "ram_pct": psutil.virtual_memory().percent } + + def _health(self) -> Dict[str, str]: + return { + 'last_process': str(self._freqtrade.last_process) + } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 716694a81..13ad98e0e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -113,7 +113,7 @@ class Telegram(RPCHandler): r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$', r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', - r'/forcebuy$', r'/edge$', r'/help$', r'/version$'] + r'/forcebuy$', r'/edge$', r'/health$', r'/help$', r'/version$'] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -173,6 +173,7 @@ class Telegram(RPCHandler): CommandHandler(['blacklist_delete', 'bl_delete'], self._blacklist_delete), CommandHandler('logs', self._logs), CommandHandler('edge', self._edge), + CommandHandler('health', self._health), CommandHandler('help', self._help), CommandHandler('version', self._version), ] @@ -1282,6 +1283,7 @@ class Telegram(RPCHandler): "*/logs [limit]:* `Show latest logs - defaults to 10` \n" "*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/edge:* `Shows validated pairs by Edge if it is enabled` \n" + "*/health* `Show latest process timestamp - defaults to 1970-01-01 00:00:00` \n" "_Statistics_\n" "------------\n" @@ -1309,6 +1311,20 @@ class Telegram(RPCHandler): self._send_msg(message, parse_mode=ParseMode.MARKDOWN) + @authorized_only + def _health(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /health + Shows the last process timestamp + """ + try: + health = self._rpc._health() + message = f"Last process: `{health['last_process']}`" + logger.debug(message) + self._send_msg(message) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _version(self, update: Update, context: CallbackContext) -> None: """ diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 27c509c94..c7616a295 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1272,3 +1272,12 @@ def test_rpc_edge_enabled(mocker, edge_conf) -> None: assert ret[0]['Winrate'] == 0.66 assert ret[0]['Expectancy'] == 1.71 assert ret[0]['Stoploss'] == -0.02 + + +def test_rpc_health(mocker, default_conf) -> None: + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + rpc = RPC(freqtradebot) + ret = rpc._health() + assert ret['last_process'] == '1970-01-01 00:00:00' diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 207d80cef..3e891baa8 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1442,3 +1442,13 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): assert result['status'] == 'reset' assert not result['running'] assert result['status_msg'] == 'Backtest reset' + + +def test_health(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/health") + + assert_response(rc) + ret = rc.json() + assert ret['last_process'] == '1970-01-01T00:00:00' From acf6e94591a639310387049e0d093597ec1c0bde Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 24 Jan 2022 13:56:52 +0200 Subject: [PATCH 009/114] Fix unittest. --- tests/rpc/test_rpc_telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1d638eed1..5e07e05e5 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -99,7 +99,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['count'], ['locks'], ['unlock', 'delete_locks'], " "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " "['stopbuy'], ['whitelist'], ['blacklist'], ['blacklist_delete', 'bl_delete'], " - "['logs'], ['edge'], ['help'], ['version']" + "['logs'], ['edge'], ['health'], ['help'], ['version']" "]") assert log_has(message_str, caplog) From 78986a0defb27be51dc644adfefe65653e6e2974 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 24 Jan 2022 14:09:23 +0200 Subject: [PATCH 010/114] I sort managed to fit it on another row. Impressive. --- freqtrade/rpc/api_server/api_v1.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 506c886a3..2dbf43f94 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -14,8 +14,8 @@ from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count, Daily, DeleteLockRequest, DeleteTrade, ForceBuyPayload, - ForceBuyResponse, ForceSellPayload, Health, - Locks, Logs, OpenTradeSchema, PairHistory, + ForceBuyResponse, ForceSellPayload, Health, Locks, + Logs, OpenTradeSchema, PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, ShowConfig, Stats, StatusMsg, StrategyListResponse, StrategyResponse, SysInfo, From e72c3ec19f110803f9be4981ef530b3cb3819838 Mon Sep 17 00:00:00 2001 From: Reigo Reinmets Date: Mon, 24 Jan 2022 15:27:03 +0200 Subject: [PATCH 011/114] Commit just to force tests to run again. --- tests/rpc/test_rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index c7616a295..9d8db06f2 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1279,5 +1279,5 @@ def test_rpc_health(mocker, default_conf) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) - ret = rpc._health() - assert ret['last_process'] == '1970-01-01 00:00:00' + result = rpc._health() + assert result['last_process'] == '1970-01-01 00:00:00' From 1f26709aca80df164e6952ceaa586d6ed98cb20c Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Wed, 26 Jan 2022 07:06:52 +0000 Subject: [PATCH 012/114] changes --- freqtrade/persistence/models.py | 42 +++++++++++++++++++++++---------- freqtrade/rpc/telegram.py | 25 ++++++++++---------- tests/rpc/test_rpc.py | 16 +++++++++---- tests/rpc/test_rpc_telegram.py | 3 ++- tests/test_persistence.py | 6 +++-- 5 files changed, 59 insertions(+), 33 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 14ac65e61..340aad331 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -167,12 +167,23 @@ class Order(_DECL_BASE): def to_json(self) -> Dict[str, Any]: return { - 'cost': self.cost if self.cost else 0, 'amount': self.amount, - 'price': self.price, 'average': round(self.average, 8) if self.average else 0, + 'cost': self.cost if self.cost else 0, + 'filled': self.filled, + 'ft_order_side': self.ft_order_side, + 'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT) + if self.order_date else None, + 'order_timestamp': int(self.order_date.replace( + tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None, 'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT) - if self.order_filled_date else None + if self.order_filled_date else None, + 'order_filled_timestamp': int(self.order_filled_date.replace( + tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None, + 'order_type': self.order_type, + 'price': self.price, + 'remaining': self.remaining, + 'status': self.status, } @staticmethod @@ -292,11 +303,15 @@ class LocalTrade(): return self.close_date.replace(tzinfo=timezone.utc) def to_json(self) -> Dict[str, Any]: - fill_buy = self.select_filled_orders('buy') - buys_json = dict() - if len(fill_buy) > 0: - for x in range(len(fill_buy)): - buys_json[str(x)] = fill_buy[x].to_json() + filled_orders = self.select_filled_orders() + filled_buys = [] + filled_sells = [] + if len(filled_orders) > 0: + for x in range(len(filled_orders)): + if filled_orders[x].ft_order_side == 'buy': + filled_buys.append(filled_orders[x].to_json()) + elif filled_orders[x].ft_order_side == 'sell': + filled_sells.append(filled_orders[x].to_json()) return { 'trade_id': self.id, @@ -361,7 +376,8 @@ class LocalTrade(): 'max_rate': self.max_rate, 'open_order_id': self.open_order_id, - 'filled_buys': buys_json, + 'filled_buys': filled_buys, + 'filled_sells': filled_sells, } @staticmethod @@ -631,14 +647,14 @@ class LocalTrade(): else: return None - def select_filled_orders(self, order_side: str) -> List['Order']: + def select_filled_orders(self, order_side: Optional[str] = None) -> List['Order']: """ Finds filled orders for this orderside. - :param order_side: Side of the order (either 'buy' or 'sell') + :param order_side: Side of the order (either 'buy', 'sell', or None) :return: array of Order objects """ - return [o for o in self.orders if o.ft_order_side == order_side and - o.ft_is_open is False and + return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None)) + and o.ft_is_open is False and (o.filled or 0) > 0 and o.status in NON_OPEN_EXCHANGE_STATES] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 03eb836fa..7f8c3fb1a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -375,37 +375,37 @@ class Telegram(RPCHandler): """ lines = [] for x in range(len(filled_trades)): - cur_buy_date = arrow.get(filled_trades[str(x)]["order_filled_date"]) - cur_buy_amount = filled_trades[str(x)]["amount"] - cur_buy_average = filled_trades[str(x)]["average"] + current_buy_datetime = arrow.get(filled_trades[x]["order_filled_date"]) + cur_buy_amount = filled_trades[x]["amount"] + cur_buy_average = filled_trades[x]["average"] lines.append(" ") if x == 0: lines.append("*Buy #{}:*".format(x+1)) lines.append("*Buy Amount:* {} ({:.8f} {})" - .format(cur_buy_amount, filled_trades[str(x)]["cost"], base_currency)) + .format(cur_buy_amount, filled_trades[x]["cost"], base_currency)) lines.append("*Average Buy Price:* {}".format(cur_buy_average)) else: sumA = 0 sumB = 0 for y in range(x): - sumA += (filled_trades[str(y)]["amount"] * filled_trades[str(y)]["average"]) - sumB += filled_trades[str(y)]["amount"] + sumA += (filled_trades[y]["amount"] * filled_trades[y]["average"]) + sumB += filled_trades[y]["amount"] prev_avg_price = sumA/sumB - price_to_1st_buy = (cur_buy_average - filled_trades["0"]["average"]) \ - / filled_trades["0"]["average"] + price_to_1st_buy = (cur_buy_average - filled_trades[0]["average"]) \ + / filled_trades[0]["average"] minus_on_buy = (cur_buy_average - prev_avg_price)/prev_avg_price - dur_buys = cur_buy_date - arrow.get(filled_trades[str(x-1)]["order_filled_date"]) + dur_buys = current_buy_datetime - arrow.get(filled_trades[x-1]["order_filled_date"]) days = dur_buys.days hours, remainder = divmod(dur_buys.seconds, 3600) minutes, seconds = divmod(remainder, 60) lines.append("*Buy #{}:* at {:.2%} avg profit".format(x+1, minus_on_buy)) - lines.append("({})".format(cur_buy_date + lines.append("({})".format(current_buy_datetime .humanize(granularity=["day", "hour", "minute"]))) lines.append("*Buy Amount:* {} ({:.8f} {})" - .format(cur_buy_amount, filled_trades[str(x)]["cost"], base_currency)) + .format(cur_buy_amount, filled_trades[x]["cost"], base_currency)) lines.append("*Average Buy Price:* {} ({:.2%} from 1st buy rate)" .format(cur_buy_average, price_to_1st_buy)) - lines.append("*Filled at:* {}".format(filled_trades[str(x)]["order_filled_date"])) + lines.append("*Order filled at:* {}".format(filled_trades[x]["order_filled_date"])) lines.append("({}d {}h {}m {}s from previous buy)" .format(days, hours, minutes, seconds)) return lines @@ -437,7 +437,6 @@ class Telegram(RPCHandler): messages = [] for r in results: r['open_date_hum'] = arrow.get(r['open_date']).humanize() - r['filled_buys'] = r.get('filled_buys', []) r['num_buys'] = len(r['filled_buys']) r['sell_reason'] = r.get('sell_reason', "") r['position_adjustment_enable'] = r.get('position_adjustment_enable', False) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a91340e39..2a1964732 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -109,8 +109,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_order': None, 'exchange': 'binance', 'position_adjustment_enable': False, - 'filled_buys': {'0': {'amount': 91.07468123, 'average': 1.098e-05, - 'cost': 0.0009999999999054, 'order_filled_date': ANY, 'price': 1.098e-05}}, + 'filled_buys': [{'amount': 91.07468123, 'average': 1.098e-05, + 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', + 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, + 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, + 'remaining': ANY, 'status': ANY}], + 'filled_sells': [] } mocker.patch('freqtrade.exchange.Exchange.get_rate', @@ -179,8 +183,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_order': None, 'exchange': 'binance', 'position_adjustment_enable': False, - 'filled_buys': {'0': {'amount': 91.07468123, 'average': 1.098e-05, - 'cost': 0.0009999999999054, 'order_filled_date': ANY, 'price': 1.098e-05}}, + 'filled_buys': [{'amount': 91.07468123, 'average': 1.098e-05, + 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', + 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, + 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, + 'remaining': ANY, 'status': ANY}], + 'filled_sells': [] } diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1d638eed1..600568580 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -201,7 +201,8 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'stoploss_current_dist_ratio': -0.0002, 'stop_loss_ratio': -0.0001, 'open_order': '(limit buy rem=0.00000000)', - 'is_open': True + 'is_open': True, + 'filled_buys': [] }]), ) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d6b12ea8c..1691af820 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -903,7 +903,8 @@ def test_to_json(default_conf, fee): 'buy_tag': None, 'timeframe': None, 'exchange': 'binance', - 'filled_buys': {} + 'filled_buys': [], + 'filled_sells': [] } # Simulate dry_run entries @@ -971,7 +972,8 @@ def test_to_json(default_conf, fee): 'buy_tag': 'buys_signal_001', 'timeframe': None, 'exchange': 'binance', - 'filled_buys': {} + 'filled_buys': [], + 'filled_sells': [] } From 4b9d55dbe275a3a06ecaeb90d23dc04d442a9551 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jan 2022 18:58:51 +0100 Subject: [PATCH 013/114] Add test for backtest dataprovider (should cache the correct candle) --- tests/optimize/test_backtesting.py | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index bc408a059..3af431f87 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -21,6 +21,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange from freqtrade.enums import RunMode, SellType from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.misc import get_strategy_run_id from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import LocalTrade @@ -650,6 +651,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: timerange=timerange) processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) + result = backtesting.backtest( processed=deepcopy(processed), start_date=min_date, @@ -741,6 +743,46 @@ def test_processed(default_conf, mocker, testdatadir) -> None: assert col in cols +def test_backtest_dataprovider_analyzed_df(default_conf, fee, mocker, testdatadir) -> None: + default_conf['use_sell_signal'] = False + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + mocker.patch("freqtrade.exchange.Exchange.get_min_pair_stake_amount", return_value=0.00001) + patch_exchange(mocker) + backtesting = Backtesting(default_conf) + backtesting._set_strategy(backtesting.strategylist[0]) + timerange = TimeRange('date', None, 1517227800, 0) + data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], + timerange=timerange) + processed = backtesting.strategy.advise_all_indicators(data) + min_date, max_date = get_timerange(processed) + + global count + count = 0 + + def tmp_confirm_entry(pair, current_time, **kwargs): + dp = backtesting.strategy.dp + df, _ = dp.get_analyzed_dataframe(pair, backtesting.strategy.timeframe) + current_candle = df.iloc[-1].squeeze() + assert current_candle['buy'] == 1 + + candle_date = timeframe_to_next_date(backtesting.strategy.timeframe, current_candle['date']) + assert candle_date == current_time + # These asserts don't properly raise as they are nested, + # therefore we increment count and assert for that. + global count + count = count + 1 + + backtesting.strategy.confirm_trade_entry = tmp_confirm_entry + backtesting.backtest( + processed=deepcopy(processed), + start_date=min_date, + end_date=max_date, + max_open_trades=10, + position_stacking=False, + ) + assert count == 5 + + def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None: # While this test IS a copy of test_backtest_pricecontours, it's needed to ensure # results do not carry-over to the next run, which is not given by using parametrize. From 5d0c2bcb448259e6198f483f52c9b14fbe217ee2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jan 2022 17:09:19 +0100 Subject: [PATCH 014/114] Shift candles after pushing them to dataprovider this will ensure that the signals are not shifted in callbacks closes #6234 --- freqtrade/optimize/backtesting.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8e52a62fa..207fd279c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -275,6 +275,13 @@ class Backtesting: # Trim startup period from analyzed dataframe df_analyzed = processed[pair] = pair_data = trim_dataframe( df_analyzed, self.timerange, startup_candles=self.required_startup) + # Update dataprovider cache + self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) + + # Create a copy of the dataframe before shifting, that way the buy signal/tag + # remains on the correct candle for callbacks. + df_analyzed = df_analyzed.copy() + # To avoid using data from future, we use buy/sell signals shifted # from the previous candle df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) @@ -282,9 +289,6 @@ class Backtesting: df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) df_analyzed.loc[:, 'exit_tag'] = df_analyzed.loc[:, 'exit_tag'].shift(1) - # Update dataprovider cache - self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) - df_analyzed = df_analyzed.drop(df_analyzed.head(1).index) # Convert from Pandas to list for performance reasons From 15d5389564c5aa5297ed9ce6d123f63ba290747b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Jan 2022 07:57:43 +0100 Subject: [PATCH 015/114] Update /health endpoint to be in local timezone --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/rpc.py | 8 ++++++-- freqtrade/rpc/telegram.py | 3 +-- tests/rpc/test_rpc.py | 3 ++- tests/rpc/test_rpc_apiserver.py | 3 ++- 6 files changed, 14 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 17d345f48..c98c9a804 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -100,7 +100,7 @@ class FreqtradeBot(LoggingMixin): self._exit_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) - self.last_process = datetime.utcfromtimestamp(0.0) + self.last_process = datetime(1970, 1, 1, tzinfo=timezone.utc) def notify_status(self, msg: str) -> None: """ @@ -189,7 +189,7 @@ class FreqtradeBot(LoggingMixin): self.enter_positions() Trade.commit() - self.last_process = datetime.utcnow() + self.last_process = datetime.now(timezone.utc) def process_stopped(self) -> None: """ diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 5709639b3..d3a0f3e7d 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -386,3 +386,4 @@ class SysInfo(BaseModel): class Health(BaseModel): last_process: datetime + last_process_ts: int diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index fa301ed60..f57253562 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union import arrow import psutil from dateutil.relativedelta import relativedelta +from dateutil.tz import tzlocal from numpy import NAN, inf, int64, mean from pandas import DataFrame @@ -1031,7 +1032,10 @@ class RPC: "ram_pct": psutil.virtual_memory().percent } - def _health(self) -> Dict[str, str]: + def _health(self) -> Dict[str, Union[str, int]]: + last_p = self._freqtrade.last_process return { - 'last_process': str(self._freqtrade.last_process) + 'last_process': str(last_p), + 'last_process_loc': last_p.astimezone(tzlocal()).strftime(DATETIME_PRINT_FORMAT), + 'last_process_ts': int(last_p.timestamp()), } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 13ad98e0e..74f56b4c1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1319,8 +1319,7 @@ class Telegram(RPCHandler): """ try: health = self._rpc._health() - message = f"Last process: `{health['last_process']}`" - logger.debug(message) + message = f"Last process: `{health['last_process_loc']}`" self._send_msg(message) except RPCException as e: self._send_msg(str(e)) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 9d8db06f2..03c068966 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1280,4 +1280,5 @@ def test_rpc_health(mocker, default_conf) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtradebot) result = rpc._health() - assert result['last_process'] == '1970-01-01 00:00:00' + assert result['last_process'] == '1970-01-01 00:00:00+00:00' + assert result['last_process_ts'] == 0 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3e891baa8..1c33dd928 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1451,4 +1451,5 @@ def test_health(botclient): assert_response(rc) ret = rc.json() - assert ret['last_process'] == '1970-01-01T00:00:00' + assert ret['last_process_ts'] == 0 + assert ret['last_process'] == '1970-01-01T00:00:00+00:00' From d1d520769ebf60083e4db6bd569acd46a7ac3f3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jan 2022 14:25:33 +0100 Subject: [PATCH 016/114] Improve status table for position adjust don't show "/max" if no maximum is set closes #6317 --- freqtrade/rpc/rpc.py | 6 ++++-- tests/rpc/test_rpc.py | 4 ++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a45ba8ea5..ed41dbb01 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -252,9 +252,11 @@ class RPC: profit_str ] if self._config.get('position_adjustment_enable', False): - max_buy = self._config['max_entry_position_adjustment'] + 1 + max_buy_str = '' + if self._config.get('max_entry_position_adjustment', -1) > 0: + max_buy_str = f"/{self._config['max_entry_position_adjustment'] + 1}" filled_buys = trade.nr_of_successful_buys - detail_trade.append(f"{filled_buys}/{max_buy}") + detail_trade.append(f"{filled_buys}{max_buy_str}") trades_list.append(detail_trade) profitcol = "Profit" if self._fiat_converter: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 27c509c94..46828b325 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -221,9 +221,13 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert '-0.06' == f'{fiat_profit_sum:.2f}' rpc._config['position_adjustment_enable'] = True + rpc._config['max_entry_position_adjustment'] = 3 result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "# Buys" in headers assert len(result[0]) == 5 + # 4th column should be 1/4 - as 1 order filled (a total of 4 is possible) + # 3 on top of the initial one. + assert result[0][4] == '1/4' mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) From aea84dc11771c345f6e2344fe8e5b4032b7a7b88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Jan 2022 20:16:58 +0100 Subject: [PATCH 017/114] Limit should default to None when calling pair_candles --- freqtrade/rpc/api_server/api_v1.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 4c430dd46..2ebc3d083 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -214,7 +214,8 @@ def reload_config(rpc: RPC = Depends(get_rpc)): @router.get('/pair_candles', response_model=PairHistory, tags=['candle data']) -def pair_candles(pair: str, timeframe: str, limit: Optional[int], rpc: RPC = Depends(get_rpc)): +def pair_candles( + pair: str, timeframe: str, limit: Optional[int] = None, rpc: RPC = Depends(get_rpc)): return rpc._rpc_analysed_dataframe(pair, timeframe, limit) From da39ca66503934df3afbfa7a6c03d36c88d8996b Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 30 Jan 2022 21:19:05 +0900 Subject: [PATCH 018/114] fix new-config --- docs/utils.md | 2 +- freqtrade/commands/build_config_commands.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index 4a032db26..c6e795e60 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -59,7 +59,7 @@ $ freqtrade new-config --config config_binance.json ? Do you want to enable Dry-run (simulated trades)? Yes ? Please insert your stake currency: BTC ? Please insert your stake amount: 0.05 -? Please insert max_open_trades (Integer or 'unlimited'): 3 +? Please insert max_open_trades (Integer or -1 for unlimited open trades): 3 ? Please insert your desired timeframe (e.g. 5m): 5m ? Please insert your display Currency (for reporting): USD ? Select exchange binance diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 9e95cc455..ca3f11a21 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -76,12 +76,9 @@ def ask_user_config() -> Dict[str, Any]: { "type": "text", "name": "max_open_trades", - "message": f"Please insert max_open_trades (Integer or '{UNLIMITED_STAKE_AMOUNT}'):", + "message": "Please insert max_open_trades (Integer or -1 for unlimited open trades):", "default": "3", - "validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_int(val), - "filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"' - if val == UNLIMITED_STAKE_AMOUNT - else val + "validate": lambda val: validate_is_int(val) }, { "type": "select", From 7aae9565c7f101fc1fd13d68a81e8dc78ec8307d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jan 2022 14:07:21 +0100 Subject: [PATCH 019/114] Only look at buy orders when looking to recalculate trade value --- freqtrade/persistence/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 76886d8ae..411db3fa7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -569,8 +569,8 @@ class LocalTrade(): return float(f"{profit_ratio:.8f}") def recalc_trade_from_orders(self): - # We need at least 2 orders for averaging amounts and rates. - if len(self.orders) < 2: + # We need at least 2 entry orders for averaging amounts and rates. + if len(self.select_filled_orders('buy')) < 2: # Just in case, still recalc open trade value self.recalc_open_trade_value() return From 287e90af8ec91f8ae1fecb539d7be28192c68609 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 03:01:18 +0000 Subject: [PATCH 020/114] Bump ccxt from 1.70.45 to 1.71.46 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.70.45 to 1.71.46. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.70.45...1.71.46) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1b26b5146..79ad03108 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.1 pandas==1.4.0 pandas-ta==0.3.14b -ccxt==1.70.45 +ccxt==1.71.46 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 From 7d066d81c19997e841facb383a14a29ced7878d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 03:01:22 +0000 Subject: [PATCH 021/114] Bump mkdocs-material from 8.1.8 to 8.1.9 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.1.8 to 8.1.9. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.1.8...8.1.9) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 0368c90ff..f64a0ea7c 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.3 -mkdocs-material==8.1.8 +mkdocs-material==8.1.9 mdx_truly_sane_lists==1.2 pymdown-extensions==9.1 From dbf2226841c15d15b40941738a34cba201518056 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 03:01:25 +0000 Subject: [PATCH 022/114] Bump nbconvert from 6.4.0 to 6.4.1 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 6.4.0 to 6.4.1. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/6.4.0...6.4.1) --- updated-dependencies: - dependency-name: nbconvert dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 59867ff3a..a83d0be95 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,7 +17,7 @@ isort==5.10.1 time-machine==2.6.0 # Convert jupyter notebooks to markdown documents -nbconvert==6.4.0 +nbconvert==6.4.1 # mypy types types-cachetools==4.2.9 From 15dbdfe130d26d38520f3c5ef504a2954510c28f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 03:01:27 +0000 Subject: [PATCH 023/114] Bump types-python-dateutil from 2.8.8 to 2.8.9 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.8 to 2.8.9. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 59867ff3a..a3fd7a4a5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,4 +26,4 @@ types-requests==2.27.7 types-tabulate==0.8.5 # Extensions to datetime library -types-python-dateutil==2.8.8 \ No newline at end of file +types-python-dateutil==2.8.9 \ No newline at end of file From ab5c1e6c1ef3e0454488056b2251f99cca571acd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 03:01:31 +0000 Subject: [PATCH 024/114] Bump uvicorn from 0.17.0 to 0.17.1 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.17.0 to 0.17.1. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.17.0...0.17.1) --- updated-dependencies: - dependency-name: uvicorn dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1b26b5146..68be86bd0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ sdnotify==0.3.2 # API Server fastapi==0.73.0 -uvicorn==0.17.0 +uvicorn==0.17.1 pyjwt==2.3.0 aiofiles==0.8.0 psutil==5.9.0 From 8a9b70cc493a42047d7d737d7d9f185e2d8ce8c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 03:01:34 +0000 Subject: [PATCH 025/114] Bump pytest-mock from 3.6.1 to 3.7.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.6.1 to 3.7.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.6.1...v3.7.0) --- updated-dependencies: - dependency-name: pytest-mock dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 59867ff3a..bd1ed861f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ mypy==0.931 pytest==6.2.5 pytest-asyncio==0.17.2 pytest-cov==3.0.0 -pytest-mock==3.6.1 +pytest-mock==3.7.0 pytest-random-order==1.0.4 isort==5.10.1 # For datetime mocking From 65526e9803d651e0babe8f9fa8ba6cef5c364bfa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 03:01:42 +0000 Subject: [PATCH 026/114] Bump arrow from 1.2.1 to 1.2.2 Bumps [arrow](https://github.com/arrow-py/arrow) from 1.2.1 to 1.2.2. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/compare/1.2.1...1.2.2) --- updated-dependencies: - dependency-name: arrow dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1b26b5146..127299e36 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ cryptography==36.0.1 aiohttp==3.8.1 SQLAlchemy==1.4.31 python-telegram-bot==13.10 -arrow==1.2.1 +arrow==1.2.2 cachetools==4.2.2 requests==2.27.1 urllib3==1.26.8 From bc5adc018825059c1195c6e9986251471d79c55a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Jan 2022 03:01:46 +0000 Subject: [PATCH 027/114] Bump prompt-toolkit from 3.0.24 to 3.0.26 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.24 to 3.0.26. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.24...3.0.26) --- updated-dependencies: - dependency-name: prompt-toolkit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1b26b5146..fc71d1b05 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,6 +41,6 @@ psutil==5.9.0 colorama==0.4.4 # Building config files interactively questionary==1.10.0 -prompt-toolkit==3.0.24 +prompt-toolkit==3.0.26 # Extensions to datetime library python-dateutil==2.8.2 From f35c6545c13316215c9b518f511ec6f9b982d4f0 Mon Sep 17 00:00:00 2001 From: TheJoeSchr <8218910+TheJoeSchr@users.noreply.github.com> Date: Mon, 31 Jan 2022 20:04:19 +0100 Subject: [PATCH 028/114] Update strategy-callbacks.md fix typo --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 3a30a2a28..1904dd206 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -74,7 +74,7 @@ class AwesomeStrategy(IStrategy): Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged. !!! Tip - You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged. + You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this action will be logged. !!! Tip Returning `0` or `None` will prevent trades from being placed. From 1a838680e74b59a312c49879ee5bea6425f2b151 Mon Sep 17 00:00:00 2001 From: TheJoeSchr <8218910+TheJoeSchr@users.noreply.github.com> Date: Mon, 31 Jan 2022 20:23:02 +0100 Subject: [PATCH 029/114] docs: fix typo and fix markdown list rendering --- docs/data-analysis.md | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 17da98935..9a79ee5ed 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -1,6 +1,6 @@ -# Analyzing bot data with Jupyter notebooks +# Analyzing bot data with Jupyter notebooks -You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/` after initializing the user directory with `freqtrade create-userdir --userdir user_data`. +You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/` after initializing the user directory with `freqtrade create-userdir --userdir user_data`. ## Quick start with docker @@ -41,32 +41,35 @@ ipython kernel install --user --name=freqtrade !!! Warning Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually. -## Recommended workflow +## Recommended workflow -| Task | Tool | - --- | --- -Bot operations | CLI +| Task | Tool | + --- | --- +Bot operations | CLI Repetitive tasks | Shell scripts -Data analysis & visualization | Notebook +Data analysis & visualization | Notebook 1. Use the CLI to + * download historical data * run a backtest * run with real-time data - * export results + * export results 1. Collect these actions in shell scripts + * save complicated commands with arguments - * execute multi-step operations + * execute multi-step operations * automate testing strategies and preparing data for analysis 1. Use a notebook to + * visualize data - * munge and plot to generate insights + * mangle and plot to generate insights -## Example utility snippets +## Example utility snippets -### Change directory to root +### Change directory to root Jupyter notebooks execute from the notebook directory. The following snippet searches for the project root, so relative paths remain consistent. From 479b560549954589b4b7acd7df65ec4f3c680ce0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Feb 2022 06:15:57 +0100 Subject: [PATCH 030/114] Add missing entry_tag argument to custom_entry_tag documentation --- docs/strategy-callbacks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 1904dd206..bff5bd998 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -54,7 +54,7 @@ Called before entering a trade, makes it possible to manage your position size w class AwesomeStrategy(IStrategy): def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + entry_tag: Optional[str], **kwargs) -> float: dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) current_candle = dataframe.iloc[-1].squeeze() From d96a354a3e2d3fc6f7532c85e6a96ceea7b70e64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Feb 2022 18:50:22 +0100 Subject: [PATCH 031/114] Version bump ccxt to 1.71.73 closes #6339 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8ef2d5f7c..a3e225164 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.1 pandas==1.4.0 pandas-ta==0.3.14b -ccxt==1.71.46 +ccxt==1.71.73 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 From 64d0c75bbb6d0482e345e4155ccae54010d148f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Feb 2022 19:11:51 +0100 Subject: [PATCH 032/114] Fix deprecation warnings --- freqtrade/data/history/history_utils.py | 4 ++-- tests/plugins/test_pairlist.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index e6b8db322..7cdee8b71 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -5,7 +5,7 @@ from pathlib import Path from typing import Dict, List, Optional, Tuple import arrow -from pandas import DataFrame +from pandas import DataFrame, concat from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS @@ -208,7 +208,7 @@ def _download_pair_history(pair: str, *, else: # Run cleaning again to ensure there were no duplicate candles # Especially between existing and new data. - data = clean_ohlcv_dataframe(data.append(new_dataframe), timeframe, pair, + data = clean_ohlcv_dataframe(concat([data, new_dataframe], axis=0), timeframe, pair, fill_missing=False, drop_incomplete=False) logger.debug("New Start: %s", diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 76a067cee..219933bb4 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -4,6 +4,7 @@ import logging import time from unittest.mock import MagicMock, PropertyMock +import pandas as pd import pytest import time_machine @@ -492,7 +493,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ohlcv_data = { ('ETH/BTC', '1d'): ohlcv_history, ('TKN/BTC', '1d'): ohlcv_history, - ('LTC/BTC', '1d'): ohlcv_history.append(ohlcv_history), + ('LTC/BTC', '1d'): pd.concat([ohlcv_history, ohlcv_history]), ('XRP/BTC', '1d'): ohlcv_history, ('HOT/BTC', '1d'): ohlcv_history_high_vola, } From 0058abcc2d01185158c6df37ec5ad1f55dedc671 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Feb 2022 12:20:05 +0100 Subject: [PATCH 033/114] Fix crash when no min-stake-amount could be determined --- freqtrade/wallets.py | 7 ++++--- tests/test_wallets.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 98a39ea2d..e57739595 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -3,7 +3,7 @@ import logging from copy import deepcopy -from typing import Any, Dict, NamedTuple +from typing import Any, Dict, NamedTuple, Optional import arrow @@ -238,14 +238,15 @@ class Wallets: return self._check_available_stake_amount(stake_amount, available_amount) - def validate_stake_amount(self, pair, stake_amount, min_stake_amount): + def validate_stake_amount( + self, pair: str, stake_amount: Optional[float], min_stake_amount: Optional[float]): if not stake_amount: logger.debug(f"Stake amount is {stake_amount}, ignoring possible trade for {pair}.") return 0 max_stake_amount = self.get_available_stake_amount() - if min_stake_amount > max_stake_amount: + if min_stake_amount is not None and min_stake_amount > max_stake_amount: if self._log: logger.warning("Minimum stake amount > available balance.") return 0 diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 3e02cdb09..12297e5ed 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -188,6 +188,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r (9, 11, 100, 11), # Below min stake (1, 15, 10, 0), # Below min stake and min_stake > max_stake (20, 50, 100, 0), # Below min stake and stake * 1.3 > min_stake + (1000, None, 1000, 1000), # No min-stake-amount could be determined ]) def test_validate_stake_amount(mocker, default_conf, From 29879bb415125162874cf97da761f6ab80e884ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Feb 2022 19:11:35 +0100 Subject: [PATCH 034/114] Update wording to entry/exit --- freqtrade/persistence/models.py | 20 +++++++++++--------- freqtrade/rpc/telegram.py | 7 ++++--- tests/rpc/test_rpc.py | 28 ++++++++++++++++------------ tests/rpc/test_rpc_telegram.py | 2 +- tests/test_persistence.py | 8 ++++---- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a78eff6af..319a8749a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -172,6 +172,7 @@ class Order(_DECL_BASE): 'cost': self.cost if self.cost else 0, 'filled': self.filled, 'ft_order_side': self.ft_order_side, + 'is_open': self.ft_is_open, 'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT) if self.order_date else None, 'order_timestamp': int(self.order_date.replace( @@ -181,6 +182,7 @@ class Order(_DECL_BASE): 'order_filled_timestamp': int(self.order_filled_date.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None, 'order_type': self.order_type, + 'pair': self.ft_pair, 'price': self.price, 'remaining': self.remaining, 'status': self.status, @@ -304,14 +306,14 @@ class LocalTrade(): def to_json(self) -> Dict[str, Any]: filled_orders = self.select_filled_orders() - filled_buys = [] - filled_sells = [] + filled_entries = [] + filled_exits = [] if len(filled_orders) > 0: - for x in range(len(filled_orders)): - if filled_orders[x].ft_order_side == 'buy': - filled_buys.append(filled_orders[x].to_json()) - elif filled_orders[x].ft_order_side == 'sell': - filled_sells.append(filled_orders[x].to_json()) + for order in filled_orders: + if order.ft_order_side == 'buy': + filled_entries.append(order.to_json()) + if order.ft_order_side == 'sell': + filled_exits.append(order.to_json()) return { 'trade_id': self.id, @@ -376,8 +378,8 @@ class LocalTrade(): 'max_rate': self.max_rate, 'open_order_id': self.open_order_id, - 'filled_buys': filled_buys, - 'filled_sells': filled_sells, + 'filled_entry_orders': filled_entries, + 'filled_exit_orders': filled_exits, } @staticmethod diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4943f9df2..d4d6ae7af 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -437,7 +437,7 @@ class Telegram(RPCHandler): messages = [] for r in results: r['open_date_hum'] = arrow.get(r['open_date']).humanize() - r['num_buys'] = len(r['filled_buys']) + r['num_entries'] = len(r['filled_entry_orders']) r['sell_reason'] = r.get('sell_reason', "") r['position_adjustment_enable'] = r.get('position_adjustment_enable', False) lines = [ @@ -478,8 +478,9 @@ class Telegram(RPCHandler): else: lines.append("*Open Order:* `{open_order}`") - if len(r['filled_buys']) > 1: - lines_detail = self._prepare_buy_details(r['filled_buys'], r['base_currency']) + if len(r['filled_entry_orders']) > 1: + lines_detail = self._prepare_buy_details( + r['filled_entry_orders'], r['base_currency']) lines.extend(lines_detail) # Filter empty lines using list-comprehension diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 7e3334c46..1cdb0e4e8 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -109,12 +109,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_order': None, 'exchange': 'binance', 'position_adjustment_enable': False, - 'filled_buys': [{'amount': 91.07468123, 'average': 1.098e-05, - 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', - 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, - 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, - 'remaining': ANY, 'status': ANY}], - 'filled_sells': [] + 'filled_entry_orders': [{ + 'amount': 91.07468123, 'average': 1.098e-05, + 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', + 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, + 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, + 'is_open': False, 'pair': 'ETH/BTC', + 'remaining': ANY, 'status': ANY}], + 'filled_exit_orders': [] } mocker.patch('freqtrade.exchange.Exchange.get_rate', @@ -183,12 +185,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_order': None, 'exchange': 'binance', 'position_adjustment_enable': False, - 'filled_buys': [{'amount': 91.07468123, 'average': 1.098e-05, - 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', - 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, - 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, - 'remaining': ANY, 'status': ANY}], - 'filled_sells': [] + 'filled_entry_orders': [{ + 'amount': 91.07468123, 'average': 1.098e-05, + 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', + 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, + 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, + 'is_open': False, 'pair': 'ETH/BTC', + 'remaining': ANY, 'status': ANY}], + 'filled_exit_orders': [] } diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 600568580..13ec3f316 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -202,7 +202,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'stop_loss_ratio': -0.0001, 'open_order': '(limit buy rem=0.00000000)', 'is_open': True, - 'filled_buys': [] + 'filled_entry_orders': [] }]), ) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index f86d9605c..d2cb91d5e 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -903,8 +903,8 @@ def test_to_json(default_conf, fee): 'buy_tag': None, 'timeframe': None, 'exchange': 'binance', - 'filled_buys': [], - 'filled_sells': [] + 'filled_entry_orders': [], + 'filled_exit_orders': [] } # Simulate dry_run entries @@ -972,8 +972,8 @@ def test_to_json(default_conf, fee): 'buy_tag': 'buys_signal_001', 'timeframe': None, 'exchange': 'binance', - 'filled_buys': [], - 'filled_sells': [] + 'filled_entry_orders': [], + 'filled_exit_orders': [] } From 1e6362debffd8ca0a2a84c93d98ed7f65889cd89 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Feb 2022 19:41:45 +0100 Subject: [PATCH 035/114] Add test for new /status telegram message --- freqtrade/rpc/rpc.py | 2 -- freqtrade/rpc/telegram.py | 11 ++++---- tests/conftest_trades.py | 1 + tests/rpc/test_rpc.py | 2 -- tests/rpc/test_rpc_telegram.py | 47 ++++++++++++++++++++++++++++++++++ 5 files changed, 53 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d731f6fa4..ed41dbb01 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -213,8 +213,6 @@ class RPC: order['type'], order['side'], order['remaining'] ) if order else None, )) - cp_cfg = self._config - trade_dict['position_adjustment_enable'] = cp_cfg['position_adjustment_enable'] results.append(trade_dict) return results diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d4d6ae7af..ba6d8c75a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -391,8 +391,8 @@ class Telegram(RPCHandler): sumA += (filled_trades[y]["amount"] * filled_trades[y]["average"]) sumB += filled_trades[y]["amount"] prev_avg_price = sumA/sumB - price_to_1st_buy = (cur_buy_average - filled_trades[0]["average"]) \ - / filled_trades[0]["average"] + price_to_1st_buy = ((cur_buy_average - filled_trades[0]["average"]) + / filled_trades[0]["average"]) minus_on_buy = (cur_buy_average - prev_avg_price)/prev_avg_price dur_buys = current_buy_datetime - arrow.get(filled_trades[x-1]["order_filled_date"]) days = dur_buys.days @@ -433,13 +433,12 @@ class Telegram(RPCHandler): trade_ids = [int(i) for i in context.args if i.isnumeric()] results = self._rpc._rpc_trade_status(trade_ids=trade_ids) - + position_adjust = self._config.get('position_adjustment_enable', False) messages = [] for r in results: r['open_date_hum'] = arrow.get(r['open_date']).humanize() r['num_entries'] = len(r['filled_entry_orders']) r['sell_reason'] = r.get('sell_reason', "") - r['position_adjustment_enable'] = r.get('position_adjustment_enable', False) lines = [ "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Current Pair:* {pair}", @@ -448,8 +447,8 @@ class Telegram(RPCHandler): "*Sell Reason:* `{sell_reason}`" if r['sell_reason'] else "", ] - if r['position_adjustment_enable']: - lines.append("*Number of Buy(s):* `{num_buys}`") + if position_adjust: + lines.append("*Number of Buy(s):* `{num_entries}`") lines.extend([ "*Open Rate:* `{open_rate:.8f}`", diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 4496df37d..70a2a99a2 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -14,6 +14,7 @@ def mock_order_1(): 'side': 'buy', 'type': 'limit', 'price': 0.123, + 'average': 0.123, 'amount': 123.0, 'filled': 123.0, 'remaining': 0.0, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 1cdb0e4e8..05e9c1da8 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -108,7 +108,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', - 'position_adjustment_enable': False, 'filled_entry_orders': [{ 'amount': 91.07468123, 'average': 1.098e-05, 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', @@ -184,7 +183,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', - 'position_adjustment_enable': False, 'filled_entry_orders': [{ 'amount': 91.07468123, 'average': 1.098e-05, 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 13ec3f316..afa7a6a67 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -23,6 +23,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade +from freqtrade.persistence.models import Order from freqtrade.rpc import RPC from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.telegram import Telegram, authorized_only @@ -218,6 +219,52 @@ def test_telegram_status(default_conf, update, mocker) -> None: assert status_table.call_count == 1 +@pytest.mark.usefixtures("init_persistence") +def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: + update.message.chat.id = "123" + default_conf['telegram']['enabled'] = False + default_conf['telegram']['chat_id'] = "123" + default_conf['position_adjustment_enable'] = True + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_order=MagicMock(return_value=None), + get_rate=MagicMock(return_value=0.22), + ) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + + create_mock_trades(fee) + trades = Trade.get_open_trades() + trade = trades[0] + trade.orders.append(Order( + order_id='5412vbb', + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=trade.open_rate * 0.95, + average=trade.open_rate * 0.95, + filled=trade.amount, + remaining=0, + cost=trade.amount, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ) + ) + trade.recalc_trade_from_orders() + Trade.commit() + + telegram._status(update=update, context=MagicMock()) + assert msg_mock.call_count == 4 + msg = msg_mock.call_args_list[0][0][0] + assert re.search(r'Number of Buy.*2', msg) + assert re.search(r'Average Buy Price', msg) + assert re.search(r'Order filled at', msg) + + def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 3 mocker.patch.multiple( From f8faf748df2d36ef97bfb327b3885423cb385b23 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Feb 2022 19:47:03 +0100 Subject: [PATCH 036/114] Simplify prepare_buy_details --- freqtrade/rpc/telegram.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ba6d8c75a..857aec3d6 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -369,32 +369,32 @@ class Telegram(RPCHandler): else: return "\N{CROSS MARK}" - def _prepare_buy_details(self, filled_trades, base_currency): + def _prepare_buy_details(self, filled_orders, base_currency): """ Prepare details of trade with buy adjustment enabled """ lines = [] - for x in range(len(filled_trades)): - current_buy_datetime = arrow.get(filled_trades[x]["order_filled_date"]) - cur_buy_amount = filled_trades[x]["amount"] - cur_buy_average = filled_trades[x]["average"] + for x, order in enumerate(filled_orders): + current_buy_datetime = arrow.get(order["order_filled_date"]) + cur_buy_amount = order["amount"] + cur_buy_average = order["average"] lines.append(" ") if x == 0: lines.append("*Buy #{}:*".format(x+1)) lines.append("*Buy Amount:* {} ({:.8f} {})" - .format(cur_buy_amount, filled_trades[x]["cost"], base_currency)) + .format(cur_buy_amount, order["cost"], base_currency)) lines.append("*Average Buy Price:* {}".format(cur_buy_average)) else: sumA = 0 sumB = 0 for y in range(x): - sumA += (filled_trades[y]["amount"] * filled_trades[y]["average"]) - sumB += filled_trades[y]["amount"] + sumA += (filled_orders[y]["amount"] * filled_orders[y]["average"]) + sumB += filled_orders[y]["amount"] prev_avg_price = sumA/sumB - price_to_1st_buy = ((cur_buy_average - filled_trades[0]["average"]) - / filled_trades[0]["average"]) + price_to_1st_buy = ((cur_buy_average - filled_orders[0]["average"]) + / filled_orders[0]["average"]) minus_on_buy = (cur_buy_average - prev_avg_price)/prev_avg_price - dur_buys = current_buy_datetime - arrow.get(filled_trades[x-1]["order_filled_date"]) + dur_buys = current_buy_datetime - arrow.get(filled_orders[x-1]["order_filled_date"]) days = dur_buys.days hours, remainder = divmod(dur_buys.seconds, 3600) minutes, seconds = divmod(remainder, 60) @@ -402,10 +402,10 @@ class Telegram(RPCHandler): lines.append("({})".format(current_buy_datetime .humanize(granularity=["day", "hour", "minute"]))) lines.append("*Buy Amount:* {} ({:.8f} {})" - .format(cur_buy_amount, filled_trades[x]["cost"], base_currency)) + .format(cur_buy_amount, order["cost"], base_currency)) lines.append("*Average Buy Price:* {} ({:.2%} from 1st buy rate)" .format(cur_buy_average, price_to_1st_buy)) - lines.append("*Order filled at:* {}".format(filled_trades[x]["order_filled_date"])) + lines.append("*Order filled at:* {}".format(order["order_filled_date"])) lines.append("({}d {}h {}m {}s from previous buy)" .format(days, hours, minutes, seconds)) return lines From a3e045f69d028cb2ad9a26b6bce14343f321ec0d Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Tue, 1 Feb 2022 19:31:38 +0100 Subject: [PATCH 037/114] Plotting: add alias `--backtest-filename` for `--export-filename` makes it easier to discover how to use this argument --- docs/plotting.md | 4 ++-- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 16 +++++++++++----- freqtrade/configuration/configuration.py | 9 +++++++++ freqtrade/plot/plotting.py | 6 +++++- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index a812f2429..ccfbb12cb 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -318,8 +318,8 @@ optional arguments: Specify what timerange of data to use. --export EXPORT Export backtest results, argument are: trades. Example: `--export=trades` - --export-filename PATH - Save backtest results to the file with this filename. + --export-filename PATH, --backtest-filename PATH + Use backtest results from this filename. Requires `--export` to be set as well. Example: `--export-filename=user_data/backtest_results/backtest _today.json` diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 290865a04..26648ea6f 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -74,7 +74,7 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", "timerange", "timeframe", "no_trades"] -ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", +ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "backtestfilename", "db_url", "trade_source", "timeframe", "plot_auto_open"] ARGS_INSTALL_UI = ["erase_ui_only", 'ui_version'] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index fc5542c52..f081e809c 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -182,11 +182,17 @@ AVAILABLE_CLI_OPTIONS = { ), "exportfilename": Arg( - '--export-filename', - help='Save backtest results to the file with this filename. ' - 'Requires `--export` to be set as well. ' - 'Example: `--export-filename=user_data/backtest_results/backtest_today.json`', - metavar='PATH', + "--export-filename", + help="Save backtest results to the file with this filename. " + "Requires `--export` to be set as well. " + "Example: `--export-filename=user_data/backtest_results/backtest_today.json`", + metavar="PATH", + ), + "backtestfilename": Arg( + "--backtest-filename", + help="Use backtest results from this filename." + "Example: `--backtest-filename=user_data/backtest_results/backtest_today.json`", + metavar="PATH", ), "disableparamexport": Arg( '--disable-param-export', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 3ac2e3ddd..5bff9a91c 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -430,6 +430,15 @@ class Configuration: self._args_to_config(config, argname='dataformat_trades', logstring='Using "{}" to store trades data.') + if self.args.get('backtestfilename'): + self._args_to_config(config, argname='backtestfilename', + logstring='Fetching backtest results from {} ...') + config['backtestfilename'] = Path(config['backtestfilename']) + else: + config['backtestfilename'] = (config['user_data_dir'] + / 'backtest_results') + + def _process_data_options(self, config: Dict[str, Any]) -> None: self._args_to_config(config, argname='new_pairs_days', diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 3b322696c..17cc11550 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -61,7 +61,11 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): startup_candles, min_date) no_trades = False - filename = config.get('exportfilename') + for arg_name in ['exportfilename', 'backtestfilename']: + filename = config.get(arg_name) + if filename is not None: + break + if config.get('no_trades', False): no_trades = True elif config['trade_source'] == 'file': From e84a58de289c6fbf1366561992a62d3a31f8afec Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Wed, 2 Feb 2022 12:45:03 +0100 Subject: [PATCH 038/114] fix: don't use different configuration keys, just add as 2nd argument --- freqtrade/commands/arguments.py | 3 +-- freqtrade/commands/cli_options.py | 9 ++------- freqtrade/configuration/configuration.py | 7 ------- freqtrade/plot/plotting.py | 8 ++------ 4 files changed, 5 insertions(+), 22 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 26648ea6f..2de06864a 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -74,8 +74,7 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", "timerange", "timeframe", "no_trades"] -ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "backtestfilename", "db_url", - "trade_source", "timeframe", "plot_auto_open"] +ARGS_PLOT_PROFIT = [ "pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "timeframe", "plot_auto_open", ] ARGS_INSTALL_UI = ["erase_ui_only", 'ui_version'] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index f081e809c..11fcc6b81 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -183,17 +183,12 @@ AVAILABLE_CLI_OPTIONS = { ), "exportfilename": Arg( "--export-filename", - help="Save backtest results to the file with this filename. " + "--backtest-filename", + help="Use this filename for backtest results." "Requires `--export` to be set as well. " "Example: `--export-filename=user_data/backtest_results/backtest_today.json`", metavar="PATH", ), - "backtestfilename": Arg( - "--backtest-filename", - help="Use backtest results from this filename." - "Example: `--backtest-filename=user_data/backtest_results/backtest_today.json`", - metavar="PATH", - ), "disableparamexport": Arg( '--disable-param-export', help="Disable automatic hyperopt parameter export.", diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 5bff9a91c..adfe8b698 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -430,13 +430,6 @@ class Configuration: self._args_to_config(config, argname='dataformat_trades', logstring='Using "{}" to store trades data.') - if self.args.get('backtestfilename'): - self._args_to_config(config, argname='backtestfilename', - logstring='Fetching backtest results from {} ...') - config['backtestfilename'] = Path(config['backtestfilename']) - else: - config['backtestfilename'] = (config['user_data_dir'] - / 'backtest_results') def _process_data_options(self, config: Dict[str, Any]) -> None: diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 17cc11550..90c6c1bce 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -61,12 +61,8 @@ def init_plotscript(config, markets: List, startup_candles: int = 0): startup_candles, min_date) no_trades = False - for arg_name in ['exportfilename', 'backtestfilename']: - filename = config.get(arg_name) - if filename is not None: - break - - if config.get('no_trades', False): + filename = config.get("exportfilename") + if config.get("no_trades", False): no_trades = True elif config['trade_source'] == 'file': if not filename.is_dir() and not filename.is_file(): From 761f7fdefb045380e488764f8cd1abb19adda21c Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Fri, 4 Feb 2022 12:56:28 +0100 Subject: [PATCH 039/114] fix: linter --- freqtrade/commands/arguments.py | 3 ++- freqtrade/configuration/configuration.py | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 2de06864a..201ec09bf 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -74,7 +74,8 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", "timerange", "timeframe", "no_trades"] -ARGS_PLOT_PROFIT = [ "pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "timeframe", "plot_auto_open", ] +ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", + "trade_source", "timeframe", "plot_auto_open", ] ARGS_INSTALL_UI = ["erase_ui_only", 'ui_version'] diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index adfe8b698..1ba17a04d 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -430,10 +430,7 @@ class Configuration: self._args_to_config(config, argname='dataformat_trades', logstring='Using "{}" to store trades data.') - - def _process_data_options(self, config: Dict[str, Any]) -> None: - self._args_to_config(config, argname='new_pairs_days', logstring='Detected --new-pairs-days: {}') From c12e5a3b6c9c4d09fbc988f7f0ec5a107890a2f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jan 2022 19:39:42 +0100 Subject: [PATCH 040/114] Initial idea backtesting order timeout --- freqtrade/optimize/backtesting.py | 40 ++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3dd8986d3..4b9d7bbf1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -380,6 +380,10 @@ class Backtesting: return trade + def _get_order_filled(self, rate: float, row: Tuple) -> bool: + """ Rate is within candle, therefore filled""" + return row[LOW_IDX] < rate < row[HIGH_IDX] + def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: @@ -405,6 +409,7 @@ class Backtesting: closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) # call the custom exit price,with default value as previous closerate current_profit = trade.calc_profit_ratio(closerate) + order_closed = True if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL): # Custom exit pricing only for sell-signals closerate = strategy_safe_wrapper(self.strategy.custom_exit_price, @@ -412,8 +417,7 @@ class Backtesting: pair=trade.pair, trade=trade, current_time=sell_row[DATE_IDX], proposed_rate=closerate, current_profit=current_profit) - # Use the maximum between close_rate and low as we cannot sell outside of a candle. - closerate = min(max(closerate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) + order_closed = self._get_order_filled(closerate, sell_row) # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['sell'] @@ -437,6 +441,21 @@ class Backtesting: trade.sell_reason = sell_row[EXIT_TAG_IDX] trade.close(closerate, show_msg=False) + order = Order( + ft_is_open=order_closed, + ft_pair=trade.pair, + symbol=trade.pair, + ft_order_side="buy", + side="buy", + order_type="market", + status="closed", + price=closerate, + average=closerate, + amount=trade.amount, + filled=trade.amount, + cost=trade.amount * closerate + ) + trade.orders.append(order) return trade return None @@ -480,9 +499,6 @@ class Backtesting: pair=pair, current_time=current_time, proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate - # Move rate to within the candle's low/high rate - propose_rate = min(max(propose_rate, row[LOW_IDX]), row[HIGH_IDX]) - min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0 max_stake_amount = self.wallets.get_available_stake_amount() @@ -534,9 +550,10 @@ class Backtesting: orders=[] ) trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) + order_filled = self._get_order_filled(propose_rate, row) order = Order( - ft_is_open=False, + ft_is_open=order_filled, ft_pair=trade.pair, symbol=trade.pair, ft_order_side="buy", @@ -552,6 +569,8 @@ class Backtesting: filled=amount, cost=stake_amount + trade.fee_open ) + if not order_filled: + trade.open_order_id = 'buy' trade.orders.append(order) if pos_adjust: trade.recalc_trade_from_orders() @@ -647,6 +666,15 @@ class Backtesting: indexes[pair] = row_index self.dataprovider._set_dataframe_max_index(row_index) + # Check order filling + for open_trade in list(open_trades[pair]): + # TODO: should open orders be stored in a separate list? + if open_trade.open_order_id: + # FIXME: check order filling + # * Get open order + # * check if filled + open_trade.open_order_id = None + # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected # don't open on the last row From f7a1cabe23018945d561b482fbe32fb836004347 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jan 2022 19:52:08 +0100 Subject: [PATCH 041/114] Add first version to fill orders "later" in backtesting --- freqtrade/optimize/backtesting.py | 12 ++++++++---- freqtrade/persistence/models.py | 7 +++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4b9d7bbf1..0e9d95f53 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -670,10 +670,11 @@ class Backtesting: for open_trade in list(open_trades[pair]): # TODO: should open orders be stored in a separate list? if open_trade.open_order_id: - # FIXME: check order filling - # * Get open order - # * check if filled - open_trade.open_order_id = None + order = open_trade.select_order(is_open=True) + # Check for timeout!! + if self._get_order_filled(order.price): + open_trade.open_order_id = None + order.ft_is_open = False # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected @@ -698,6 +699,9 @@ class Backtesting: LocalTrade.add_bt_trade(trade) for trade in list(open_trades[pair]): + # TODO: This could be avoided with a separate list + if trade.open_order_id: + continue # also check the buying candle for sell conditions. trade_entry = self._get_sell_trade_entry(trade, row) # Sell occurred diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ff500b549..ee7ad3fdd 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -635,14 +635,17 @@ class LocalTrade(): if self.stop_loss_pct is not None and self.open_rate is not None: self.adjust_stop_loss(self.open_rate, self.stop_loss_pct) - def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: + def select_order( + self, order_side: str = None, is_open: Optional[bool] = None) -> Optional[Order]: """ Finds latest order for this orderside and status :param order_side: Side of the order (either 'buy' or 'sell') :param is_open: Only search for open orders? :return: latest Order object if it exists, else None """ - orders = [o for o in self.orders if o.side == order_side] + orders = self.orders + if order_side: + orders = [o for o in self.orders if o.side == order_side] if is_open is not None: orders = [o for o in orders if o.ft_is_open == is_open] if len(orders) > 0: From 15698dd1ca5d92f678aa1eefc3fc1841d95ca9a2 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Wed, 19 Jan 2022 11:42:24 +0200 Subject: [PATCH 042/114] Fix errors so it runs, implement timeout handling. --- freqtrade/optimize/backtesting.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 0e9d95f53..d26fe1411 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -539,6 +539,7 @@ class Backtesting: trade = LocalTrade( pair=pair, open_rate=propose_rate, + open_rate_requested=propose_rate, open_date=current_time, stake_amount=stake_amount, amount=amount, @@ -553,7 +554,8 @@ class Backtesting: order_filled = self._get_order_filled(propose_rate, row) order = Order( - ft_is_open=order_filled, + order_date=current_time, + ft_is_open=not order_filled, ft_pair=trade.pair, symbol=trade.pair, ft_order_side="buy", @@ -566,7 +568,8 @@ class Backtesting: price=propose_rate, average=propose_rate, amount=amount, - filled=amount, + filled=amount if order_filled else 0, + remaining=0 if order_filled else amount, cost=stake_amount + trade.fee_open ) if not order_filled: @@ -671,10 +674,28 @@ class Backtesting: # TODO: should open orders be stored in a separate list? if open_trade.open_order_id: order = open_trade.select_order(is_open=True) - # Check for timeout!! - if self._get_order_filled(order.price): + if order is None: + continue + if self._get_order_filled(order.price, row): open_trade.open_order_id = None order.ft_is_open = False + order.filled = order.price + order.remaining = 0 + timeout = self.config['unfilledtimeout'].get(order.side, 0) + if 0 < timeout <= (tmp - order.order_date).seconds / 60: + open_trade.open_order_id = None + order.ft_is_open = False + order.filled = 0 + order.remaining = 0 + if order.side == 'buy': + # Close trade due to buy timeout expiration. + open_trade_count -= 1 + open_trades[pair].remove(open_trade) + LocalTrade.trades_open.remove(open_trade) + # trades.append(trade_entry) # TODO: Needed or not? + elif order.side == 'sell': + # Close sell order and retry selling on next signal. + del open_trade.orders[open_trade.orders.index(order)] # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected From 9140679bf45e949ed0ef02d483a5154d38e416db Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 22 Jan 2022 15:11:33 +0200 Subject: [PATCH 043/114] Backtest order timeout continued. --- freqtrade/optimize/backtesting.py | 158 +++++++++++++++++------------- 1 file changed, 88 insertions(+), 70 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d26fe1411..a251c75d8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -63,6 +63,8 @@ class Backtesting: LoggingMixin.show_output = False self.config = config self.results: Dict[str, Any] = {} + self.trade_id_counter: int = 0 + self.order_id_counter: int = 0 config['dry_run'] = True self.run_ids: Dict[str, str] = {} @@ -409,7 +411,6 @@ class Backtesting: closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) # call the custom exit price,with default value as previous closerate current_profit = trade.calc_profit_ratio(closerate) - order_closed = True if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL): # Custom exit pricing only for sell-signals closerate = strategy_safe_wrapper(self.strategy.custom_exit_price, @@ -417,7 +418,6 @@ class Backtesting: pair=trade.pair, trade=trade, current_time=sell_row[DATE_IDX], proposed_rate=closerate, current_profit=current_profit) - order_closed = self._get_order_filled(closerate, sell_row) # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['sell'] @@ -440,20 +440,26 @@ class Backtesting: ): trade.sell_reason = sell_row[EXIT_TAG_IDX] - trade.close(closerate, show_msg=False) + self.order_id_counter += 1 order = Order( - ft_is_open=order_closed, + id=self.order_id_counter, + ft_trade_id=trade.id, + order_date=sell_row[DATE_IDX].to_pydatetime(), + order_update_date=sell_row[DATE_IDX].to_pydatetime(), + ft_is_open=True, ft_pair=trade.pair, + order_id=str(self.order_id_counter), symbol=trade.pair, - ft_order_side="buy", - side="buy", - order_type="market", - status="closed", + ft_order_side="sell", + side="sell", + order_type=self.strategy.order_types['sell'], + status="open", price=closerate, average=closerate, amount=trade.amount, - filled=trade.amount, - cost=trade.amount * closerate + filled=0, + remaining=trade.amount, + cost=trade.amount * closerate, ) trade.orders.append(order) return trade @@ -494,10 +500,13 @@ class Backtesting: current_time = row[DATE_IDX].to_pydatetime() entry_tag = row[BUY_TAG_IDX] if len(row) >= BUY_TAG_IDX + 1 else None # let's call the custom entry price, using the open price as default price - propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=row[OPEN_IDX])( - pair=pair, current_time=current_time, - proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate + order_type = self.strategy.order_types['buy'] + propose_rate = row[OPEN_IDX] + if order_type == 'limit': + propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, + default_retval=row[OPEN_IDX])( + pair=pair, current_time=current_time, + proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0 max_stake_amount = self.wallets.get_available_stake_amount() @@ -507,7 +516,7 @@ class Backtesting: try: stake_amount = self.wallets.get_trade_stake_amount(pair, None) except DependencyException: - return trade + return None stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( @@ -522,7 +531,6 @@ class Backtesting: # If not pos adjust, trade is None return trade - order_type = self.strategy.order_types['buy'] time_in_force = self.strategy.order_time_in_force['sell'] # Confirm trade entry: if not pos_adjust: @@ -533,16 +541,21 @@ class Backtesting: return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): + self.order_id_counter += 1 amount = round(stake_amount / propose_rate, 8) if trade is None: # Enter trade + self.trade_id_counter += 1 trade = LocalTrade( + id=self.trade_id_counter, + open_order_id=self.order_id_counter, pair=pair, open_rate=propose_rate, open_rate_requested=propose_rate, open_date=current_time, stake_amount=stake_amount, amount=amount, + amount_requested=amount, fee_open=self.fee, fee_close=self.fee, is_open=True, @@ -554,29 +567,28 @@ class Backtesting: order_filled = self._get_order_filled(propose_rate, row) order = Order( - order_date=current_time, + id=self.order_id_counter, + ft_trade_id=trade.id, ft_is_open=not order_filled, ft_pair=trade.pair, + order_id=str(self.order_id_counter), symbol=trade.pair, ft_order_side="buy", side="buy", - order_type="market", - status="closed", + order_type=order_type, + status="open", order_date=current_time, order_filled_date=current_time, order_update_date=current_time, price=propose_rate, average=propose_rate, amount=amount, - filled=amount if order_filled else 0, - remaining=0 if order_filled else amount, - cost=stake_amount + trade.fee_open + filled=0, + remaining=amount, + cost=stake_amount + trade.fee_open, ) - if not order_filled: - trade.open_order_id = 'buy' trade.orders.append(order) - if pos_adjust: - trade.recalc_trade_from_orders() + trade.recalc_trade_from_orders() return trade @@ -589,6 +601,8 @@ class Backtesting: for pair in open_trades.keys(): if len(open_trades[pair]) > 0: for trade in open_trades[pair]: + if trade.open_order_id: + continue sell_row = data[pair][-1] trade.close_date = sell_row[DATE_IDX].to_pydatetime() @@ -638,7 +652,7 @@ class Backtesting: # Indexes per pair, so some pairs are allowed to have a missing start. indexes: Dict = defaultdict(int) - tmp = start_date + timedelta(minutes=self.timeframe_min) + current_time = start_date + timedelta(minutes=self.timeframe_min) open_trades: Dict[str, List[LocalTrade]] = defaultdict(list) open_trade_count = 0 @@ -647,7 +661,7 @@ class Backtesting: (end_date - start_date) / timedelta(minutes=self.timeframe_min))) # Loop timerange and get candle for each pair at that point in time - while tmp <= end_date: + while current_time <= end_date: open_trade_count_start = open_trade_count self.check_abort() for i, pair in enumerate(data): @@ -662,48 +676,21 @@ class Backtesting: continue # Waits until the time-counter reaches the start of the data for this pair. - if row[DATE_IDX] > tmp: + if row[DATE_IDX] > current_time: continue row_index += 1 indexes[pair] = row_index self.dataprovider._set_dataframe_max_index(row_index) - # Check order filling - for open_trade in list(open_trades[pair]): - # TODO: should open orders be stored in a separate list? - if open_trade.open_order_id: - order = open_trade.select_order(is_open=True) - if order is None: - continue - if self._get_order_filled(order.price, row): - open_trade.open_order_id = None - order.ft_is_open = False - order.filled = order.price - order.remaining = 0 - timeout = self.config['unfilledtimeout'].get(order.side, 0) - if 0 < timeout <= (tmp - order.order_date).seconds / 60: - open_trade.open_order_id = None - order.ft_is_open = False - order.filled = 0 - order.remaining = 0 - if order.side == 'buy': - # Close trade due to buy timeout expiration. - open_trade_count -= 1 - open_trades[pair].remove(open_trade) - LocalTrade.trades_open.remove(open_trade) - # trades.append(trade_entry) # TODO: Needed or not? - elif order.side == 'sell': - # Close sell order and retry selling on next signal. - del open_trade.orders[open_trade.orders.index(order)] - + # 1. Process buys. # without positionstacking, we can only have one open trade per pair. # max_open_trades must be respected # don't open on the last row if ( (position_stacking or len(open_trades[pair]) == 0) and self.trade_slot_available(max_open_trades, open_trade_count_start) - and tmp != end_date + and current_time != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 and not PairLocks.is_pair_locked(pair, row[DATE_IDX]) @@ -717,29 +704,60 @@ class Backtesting: open_trade_count += 1 # logger.debug(f"{pair} - Emulate creation of new trade: {trade}.") open_trades[pair].append(trade) - LocalTrade.add_bt_trade(trade) for trade in list(open_trades[pair]): - # TODO: This could be avoided with a separate list - if trade.open_order_id: - continue - # also check the buying candle for sell conditions. - trade_entry = self._get_sell_trade_entry(trade, row) - # Sell occurred - if trade_entry: + # 2. Process buy orders. + order = trade.select_order('buy', is_open=True) + if order and self._get_order_filled(order.price, row): + order.order_filled_date = row[DATE_IDX] + trade.open_order_id = None + order.filled = order.amount + order.status = 'closed' + order.ft_is_open = False + LocalTrade.add_bt_trade(trade) + + # 3. Create sell orders (if any) + if not trade.open_order_id: + self._get_sell_trade_entry(trade, row) # Place sell order if necessary + + # 4. Process sell orders. + order = trade.select_order('sell', is_open=True) + if order and self._get_order_filled(order.price, row): + trade.open_order_id = None + order.order_filled_date = trade.close_date = row[DATE_IDX] + order.filled = order.amount + order.status = 'closed' + order.ft_is_open = False + trade.close(order.price, show_msg=False) + # logger.debug(f"{pair} - Backtesting sell {trade}") open_trade_count -= 1 open_trades[pair].remove(trade) - LocalTrade.close_bt_trade(trade) - trades.append(trade_entry) + trades.append(trade) if enable_protections: self.protections.stop_per_pair(pair, row[DATE_IDX]) - self.protections.global_stop(tmp) + self.protections.global_stop(current_time) + + # 5. Cancel expired buy/sell orders. + for order in [o for o in trade.orders if o.ft_is_open]: + timeout = self.config['unfilledtimeout'].get(order.side, 0) + if 0 < timeout <= (current_time - order.order_date).seconds / 60: + trade.open_order_id = None + order.ft_is_open = False + order.filled = 0 + order.remaining = 0 + if order.side == 'buy': + # Close trade due to buy timeout expiration. + open_trade_count -= 1 + open_trades[pair].remove(trade) + elif order.side == 'sell': + # Close sell order and retry selling on next signal. + del trade.orders[trade.orders.index(order)] # Move time one configured time_interval ahead. self.progress.increment() - tmp += timedelta(minutes=self.timeframe_min) + current_time += timedelta(minutes=self.timeframe_min) trades += self.handle_left_open(open_trades, data=data) self.wallets.update() From 49cecf1cb25d82d0610d104c3bd012cd42efa0b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jan 2022 15:08:54 +0100 Subject: [PATCH 044/114] Small cosmetic fix --- freqtrade/optimize/backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a251c75d8..badc4111d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -416,7 +416,7 @@ class Backtesting: closerate = strategy_safe_wrapper(self.strategy.custom_exit_price, default_retval=closerate)( pair=trade.pair, trade=trade, - current_time=sell_row[DATE_IDX], + current_time=sell_candle_time, proposed_rate=closerate, current_profit=current_profit) # Confirm trade exit: @@ -444,8 +444,8 @@ class Backtesting: order = Order( id=self.order_id_counter, ft_trade_id=trade.id, - order_date=sell_row[DATE_IDX].to_pydatetime(), - order_update_date=sell_row[DATE_IDX].to_pydatetime(), + order_date=sell_candle_time, + order_update_date=sell_candle_time, ft_is_open=True, ft_pair=trade.pair, order_id=str(self.order_id_counter), From 44e616c2643ffee62784556181740ed275a05901 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Jan 2022 15:44:33 +0100 Subject: [PATCH 045/114] Add unfilledtimeout to required props for backtesting --- freqtrade/constants.py | 1 + freqtrade/optimize/backtesting.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d94e8d850..d7ba0bf98 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -456,6 +456,7 @@ SCHEMA_BACKTEST_REQUIRED = [ 'dry_run_wallet', 'dataformat_ohlcv', 'dataformat_trades', + 'unfilledtimeout', ] SCHEMA_MINIMAL_REQUIRED = [ diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index badc4111d..ef0c6c833 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -564,12 +564,11 @@ class Backtesting: orders=[] ) trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) - order_filled = self._get_order_filled(propose_rate, row) order = Order( id=self.order_id_counter, ft_trade_id=trade.id, - ft_is_open=not order_filled, + ft_is_open=True, ft_pair=trade.pair, order_id=str(self.order_id_counter), symbol=trade.pair, From f4149ee46282abd4f8205be4dd74531c077c3438 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jan 2022 15:27:18 +0100 Subject: [PATCH 046/114] Force ROI to be within candle --- freqtrade/optimize/backtesting.py | 12 +++++++++--- tests/optimize/test_backtesting.py | 3 ++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ef0c6c833..31cb20ee7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -355,7 +355,10 @@ class Backtesting: # use Open rate if open_rate > calculated sell rate return sell_row[OPEN_IDX] - return close_rate + # Use the maximum between close_rate and low as we + # cannot sell outside of a candle. + # Applies when a new ROI setting comes in place and the whole candle is above that. + return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) else: # This should not be reached... @@ -384,7 +387,7 @@ class Backtesting: def _get_order_filled(self, rate: float, row: Tuple) -> bool: """ Rate is within candle, therefore filled""" - return row[LOW_IDX] < rate < row[HIGH_IDX] + return row[LOW_IDX] <= rate <= row[HIGH_IDX] def _get_sell_trade_entry_for_candle(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]: @@ -563,6 +566,9 @@ class Backtesting: exchange='backtesting', orders=[] ) + else: + trade.open_order_id = self.order_id_counter + trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) order = Order( @@ -697,7 +703,7 @@ class Backtesting: trade = self._enter_trade(pair, row) if trade: # TODO: hacky workaround to avoid opening > max_open_trades - # This emulates previous behaviour - not sure if this is correct + # This emulates previous behavior - not sure if this is correct # Prevents buying if the trade-slot was freed in this candle open_trade_count_start += 1 open_trade_count += 1 diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 3af431f87..84ffc1548 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -635,7 +635,8 @@ def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: assert res.sell_reason == SellType.ROI.value # Sell at minute 3 (not available above!) assert res.close_date_utc == datetime(2020, 1, 1, 5, 3, tzinfo=timezone.utc) - assert round(res.close_rate, 3) == round(209.0225, 3) + sell_order = res.select_order('sell', True) + assert sell_order is not None def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: From 090554f19784b4387837e1b6103acd2aebfff182 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jan 2022 15:41:05 +0100 Subject: [PATCH 047/114] Try fill backtest order imediately for adjusted order --- freqtrade/optimize/backtesting.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 31cb20ee7..ff139c674 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -566,8 +566,6 @@ class Backtesting: exchange='backtesting', orders=[] ) - else: - trade.open_order_id = self.order_id_counter trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) @@ -592,6 +590,12 @@ class Backtesting: remaining=amount, cost=stake_amount + trade.fee_open, ) + if pos_adjust and self._get_order_filled(order.price, row): + order.filled = order.amount + order.status = 'closed' + order.ft_is_open = False + else: + trade.open_order_id = self.order_id_counter trade.orders.append(order) trade.recalc_trade_from_orders() From 7ac44380f7ec67e86b2db2c06aa10a8746bb9b8e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jan 2022 15:44:13 +0100 Subject: [PATCH 048/114] Extract backtest order closing to models class --- freqtrade/optimize/backtesting.py | 14 +++----------- freqtrade/persistence/models.py | 6 ++++++ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ff139c674..220d7557e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -591,9 +591,7 @@ class Backtesting: cost=stake_amount + trade.fee_open, ) if pos_adjust and self._get_order_filled(order.price, row): - order.filled = order.amount - order.status = 'closed' - order.ft_is_open = False + order.close_bt_order(current_time) else: trade.open_order_id = self.order_id_counter trade.orders.append(order) @@ -718,11 +716,8 @@ class Backtesting: # 2. Process buy orders. order = trade.select_order('buy', is_open=True) if order and self._get_order_filled(order.price, row): - order.order_filled_date = row[DATE_IDX] + order.close_bt_order(current_time) trade.open_order_id = None - order.filled = order.amount - order.status = 'closed' - order.ft_is_open = False LocalTrade.add_bt_trade(trade) # 3. Create sell orders (if any) @@ -733,10 +728,7 @@ class Backtesting: order = trade.select_order('sell', is_open=True) if order and self._get_order_filled(order.price, row): trade.open_order_id = None - order.order_filled_date = trade.close_date = row[DATE_IDX] - order.filled = order.amount - order.status = 'closed' - order.ft_is_open = False + trade.close_date = current_time trade.close(order.price, show_msg=False) # logger.debug(f"{pair} - Backtesting sell {trade}") diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index ee7ad3fdd..6dd4ceaad 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -188,6 +188,12 @@ class Order(_DECL_BASE): 'status': self.status, } + def close_bt_order(self, close_date: datetime): + self.order_filled_date = close_date + self.filled = self.amount + self.status = 'closed' + self.ft_is_open = False + @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): """ From 6637dacd7f0556b6076f63f099fed6d9e9899de1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jan 2022 17:17:03 +0100 Subject: [PATCH 049/114] Extract protections in backtesting --- freqtrade/optimize/backtesting.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 220d7557e..2a7602cd3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -630,6 +630,11 @@ class Backtesting: self.rejected_trades += 1 return False + def run_protections(self, enable_protections, pair: str, current_time: datetime): + if enable_protections: + self.protections.stop_per_pair(pair, current_time) + self.protections.global_stop(current_time) + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, @@ -736,9 +741,7 @@ class Backtesting: open_trades[pair].remove(trade) LocalTrade.close_bt_trade(trade) trades.append(trade) - if enable_protections: - self.protections.stop_per_pair(pair, row[DATE_IDX]) - self.protections.global_stop(current_time) + self.run_protections(enable_protections, pair, current_time) # 5. Cancel expired buy/sell orders. for order in [o for o in trade.orders if o.ft_is_open]: From 1e603985c5a6ca15cf2f08e20781b6c3955ef3af Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jan 2022 17:39:23 +0100 Subject: [PATCH 050/114] Extract backtesting order cancelling --- freqtrade/optimize/backtesting.py | 41 ++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2a7602cd3..5793da02b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -635,6 +635,28 @@ class Backtesting: self.protections.stop_per_pair(pair, current_time) self.protections.global_stop(current_time) + def check_order_cancel(self, trade: LocalTrade, current_time) -> bool: + """ + Check if an order has been canceled. + Returns True if the trade should be Deleted (initial order was canceled). + """ + for order in [o for o in trade.orders if o.ft_is_open]: + + timedout = self.strategy.ft_check_timed_out(order.side, trade, {}, current_time) + if timedout: + if order.side == 'buy': + if trade.nr_of_successful_buys == 0: + # Remove trade due to buy timeout expiration. + return True + else: + # Close additional buy order + del trade.orders[trade.orders.index(order)] + if order.side == 'sell': + # Close sell order and retry selling on next signal. + del trade.orders[trade.orders.index(order)] + + return False + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, @@ -744,20 +766,11 @@ class Backtesting: self.run_protections(enable_protections, pair, current_time) # 5. Cancel expired buy/sell orders. - for order in [o for o in trade.orders if o.ft_is_open]: - timeout = self.config['unfilledtimeout'].get(order.side, 0) - if 0 < timeout <= (current_time - order.order_date).seconds / 60: - trade.open_order_id = None - order.ft_is_open = False - order.filled = 0 - order.remaining = 0 - if order.side == 'buy': - # Close trade due to buy timeout expiration. - open_trade_count -= 1 - open_trades[pair].remove(trade) - elif order.side == 'sell': - # Close sell order and retry selling on next signal. - del trade.orders[trade.orders.index(order)] + canceled = self.check_order_cancel(trade, current_time) + if canceled: + # Close trade due to buy timeout expiration. + open_trade_count -= 1 + open_trades[pair].remove(trade) # Move time one configured time_interval ahead. self.progress.increment() From 4ea79a32e4e2083cae89a5b0dd3315889b60d2d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jan 2022 17:47:37 +0100 Subject: [PATCH 051/114] Use Order object for ft_timeout check --- freqtrade/freqtradebot.py | 14 ++++++++------ freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/persistence/models.py | 14 ++++++++++++++ freqtrade/strategy/interface.py | 10 +++++----- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 572ceeabf..279bb6161 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -987,18 +987,20 @@ class FreqtradeBot(LoggingMixin): fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) + order_obj = trade.select_order_by_order_id(trade.open_order_id) + if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( fully_cancelled - or self.strategy.ft_check_timed_out( - 'buy', trade, order, datetime.now(timezone.utc)) - )): + or (order_obj and self.strategy.ft_check_timed_out( + 'buy', trade, order_obj, datetime.now(timezone.utc)) + ))): self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( fully_cancelled - or self.strategy.ft_check_timed_out( - 'sell', trade, order, datetime.now(timezone.utc))) - ): + or (order_obj and self.strategy.ft_check_timed_out( + 'sell', trade, order_obj, datetime.now(timezone.utc)) + ))): self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) canceled_count = trade.get_exit_order_count() max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5793da02b..082fe0d5e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -593,7 +593,7 @@ class Backtesting: if pos_adjust and self._get_order_filled(order.price, row): order.close_bt_order(current_time) else: - trade.open_order_id = self.order_id_counter + trade.open_order_id = str(self.order_id_counter) trade.orders.append(order) trade.recalc_trade_from_orders() @@ -642,7 +642,7 @@ class Backtesting: """ for order in [o for o in trade.orders if o.ft_is_open]: - timedout = self.strategy.ft_check_timed_out(order.side, trade, {}, current_time) + timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time) if timedout: if order.side == 'buy': if trade.nr_of_successful_buys == 0: diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 6dd4ceaad..2e0f0753a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,6 +132,10 @@ class Order(_DECL_BASE): order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) + @property + def order_date_utc(self): + return self.order_date.replace(tzinfo=timezone.utc) + def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' @@ -641,6 +645,16 @@ class LocalTrade(): if self.stop_loss_pct is not None and self.open_rate is not None: self.adjust_stop_loss(self.open_rate, self.stop_loss_pct) + def select_order_by_order_id(self, order_id: str) -> Optional[Order]: + """ + Finds order object by Order id. + :param order_id: Exchange order id + """ + orders = [o for o in self.orders if o.order_id == order_id] + if orders: + return orders[0] + return None + def select_order( self, order_side: str = None, is_open: Optional[bool] = None) -> Optional[Order]: """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 78dae6c5d..0bd7834e2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -18,6 +18,7 @@ from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade +from freqtrade.persistence.models import LocalTrade, Order from freqtrade.strategy.hyper import HyperStrategyMixin from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators, _create_and_merge_informative_pair, @@ -862,23 +863,22 @@ class IStrategy(ABC, HyperStrategyMixin): else: return current_profit > roi - def ft_check_timed_out(self, side: str, trade: Trade, order: Dict, + def ft_check_timed_out(self, side: str, trade: LocalTrade, order: Order, current_time: datetime) -> bool: """ FT Internal method. Check if timeout is active, and if the order is still open and timed out """ timeout = self.config.get('unfilledtimeout', {}).get(side) - ordertime = arrow.get(order['datetime']).datetime if timeout is not None: timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') timeout_kwargs = {timeout_unit: -timeout} timeout_threshold = current_time + timedelta(**timeout_kwargs) - timedout = (order['status'] == 'open' and order['side'] == side - and ordertime < timeout_threshold) + timedout = (order.status == 'open' and order.side == side + and order.order_date_utc < timeout_threshold) if timedout: return True - time_method = self.check_sell_timeout if order['side'] == 'sell' else self.check_buy_timeout + time_method = self.check_sell_timeout if order.side == 'sell' else self.check_buy_timeout return strategy_safe_wrapper(time_method, default_retval=False)( From e08006ea25e50be918686e03b036f3a4e0c1e45b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jan 2022 19:25:09 +0100 Subject: [PATCH 052/114] Adjust tests to use order Object --- tests/conftest.py | 24 ++++++++++++++++++++++-- tests/test_freqtradebot.py | 12 ++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 20e027c2e..185ed5dc2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,7 +19,7 @@ from freqtrade.edge import PairInfo from freqtrade.enums import RunMode from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import LocalTrade, Trade, init_db +from freqtrade.persistence import LocalTrade, Order, Trade, init_db from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, @@ -1982,7 +1982,7 @@ def import_fails() -> None: @pytest.fixture(scope="function") def open_trade(): - return Trade( + trade = Trade( pair='ETH/BTC', open_rate=0.00001099, exchange='binance', @@ -1994,6 +1994,26 @@ def open_trade(): open_date=arrow.utcnow().shift(minutes=-601).datetime, is_open=True ) + trade.orders = [ + Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=False, + order_id='123456789', + status="closed", + symbol=trade.pair, + order_type="market", + side="buy", + price=trade.open_rate, + average=trade.open_rate, + filled=trade.amount, + remaining=0, + cost=trade.open_rate * trade.amount, + order_date=trade.open_date, + order_filled_date=trade.open_date, + ) + ] + return trade @pytest.fixture(scope="function") diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 523696759..a84616516 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2042,6 +2042,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, li def test_check_handle_timedout_buy(default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, fee, mocker) -> None: rpc_mock = patch_RPCManager(mocker) + limit_buy_order_old['id'] = open_trade.open_order_id limit_buy_cancel = deepcopy(limit_buy_order_old) limit_buy_cancel['status'] = 'canceled' cancel_order_mock = MagicMock(return_value=limit_buy_cancel) @@ -2126,6 +2127,8 @@ def test_check_handle_timedout_buy_exception(default_conf_usdt, ticker_usdt, def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, limit_sell_order_old, mocker, open_trade, caplog) -> None: default_conf_usdt["unfilledtimeout"] = {"buy": 1440, "sell": 1440, "exit_timeout_count": 1} + limit_sell_order_old['id'] = open_trade.open_order_id + rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() patch_exchange(mocker) @@ -2174,7 +2177,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l # 2nd canceled trade - Fail execute sell caplog.clear() - open_trade.open_order_id = 'order_id_2' + open_trade.open_order_id = limit_sell_order_old['id'] mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit', side_effect=DependencyException) @@ -2185,7 +2188,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l caplog.clear() # 2nd canceled trade ... - open_trade.open_order_id = 'order_id_2' + open_trade.open_order_id = limit_sell_order_old['id'] freqtrade.check_handle_timedout() assert log_has_re('Emergencyselling trade.*', caplog) assert et_mock.call_count == 1 @@ -2195,6 +2198,7 @@ def test_check_handle_timedout_sell(default_conf_usdt, ticker_usdt, limit_sell_o open_trade) -> None: rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() + limit_sell_order_old['id'] = open_trade.open_order_id patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -2253,6 +2257,7 @@ def test_check_handle_cancelled_sell(default_conf_usdt, ticker_usdt, limit_sell_ def test_check_handle_timedout_partial(default_conf_usdt, ticker_usdt, limit_buy_order_old_partial, open_trade, mocker) -> None: rpc_mock = patch_RPCManager(mocker) + limit_buy_order_old_partial['id'] = open_trade.open_order_id limit_buy_canceled = deepcopy(limit_buy_order_old_partial) limit_buy_canceled['status'] = 'canceled' @@ -2283,6 +2288,7 @@ def test_check_handle_timedout_partial_fee(default_conf_usdt, ticker_usdt, open_ limit_buy_order_old_partial, trades_for_order, limit_buy_order_old_partial_canceled, mocker) -> None: rpc_mock = patch_RPCManager(mocker) + limit_buy_order_old_partial['id'] = open_trade.open_order_id cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=0)) patch_exchange(mocker) @@ -2322,6 +2328,8 @@ def test_check_handle_timedout_partial_except(default_conf_usdt, ticker_usdt, op fee, limit_buy_order_old_partial, trades_for_order, limit_buy_order_old_partial_canceled, mocker) -> None: rpc_mock = patch_RPCManager(mocker) + limit_buy_order_old_partial_canceled['id'] = open_trade.open_order_id + limit_buy_order_old_partial['id'] = open_trade.open_order_id cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) patch_exchange(mocker) mocker.patch.multiple( From 58fad72778cc9f4e11e766d3a02ffe48bafeb6dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jan 2022 19:35:46 +0100 Subject: [PATCH 053/114] Update wallets when necessary closes #6321 --- freqtrade/optimize/backtesting.py | 6 +++++- freqtrade/wallets.py | 5 +++-- tests/optimize/test_backtesting.py | 2 ++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 082fe0d5e..945022b66 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -381,6 +381,7 @@ class Backtesting: if stake_amount is not None and stake_amount > 0.0: pos_trade = self._enter_trade(trade.pair, row, stake_amount, trade) if pos_trade is not None: + self.wallets.update() return pos_trade return trade @@ -517,7 +518,7 @@ class Backtesting: pos_adjust = trade is not None if not pos_adjust: try: - stake_amount = self.wallets.get_trade_stake_amount(pair, None) + stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False) except DependencyException: return None @@ -746,6 +747,7 @@ class Backtesting: order.close_bt_order(current_time) trade.open_order_id = None LocalTrade.add_bt_trade(trade) + self.wallets.update() # 3. Create sell orders (if any) if not trade.open_order_id: @@ -763,6 +765,7 @@ class Backtesting: open_trades[pair].remove(trade) LocalTrade.close_bt_trade(trade) trades.append(trade) + self.wallets.update() self.run_protections(enable_protections, pair, current_time) # 5. Cancel expired buy/sell orders. @@ -771,6 +774,7 @@ class Backtesting: # Close trade due to buy timeout expiration. open_trade_count -= 1 open_trades[pair].remove(trade) + self.wallets.update() # Move time one configured time_interval ahead. self.progress.increment() diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index e57739595..93f3d3800 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -211,7 +211,7 @@ class Wallets: return stake_amount - def get_trade_stake_amount(self, pair: str, edge=None) -> float: + def get_trade_stake_amount(self, pair: str, edge=None, update: bool = True) -> float: """ Calculate stake amount for the trade :return: float: Stake amount @@ -219,7 +219,8 @@ class Wallets: """ stake_amount: float # Ensure wallets are uptodate. - self.update() + if update: + self.update() val_tied_up = Trade.total_open_trades_stakes() available_amount = self.get_available_stake_amount() diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 84ffc1548..649a43b32 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -521,6 +521,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: # Fake 2 trades, so there's not enough amount for the next trade left. LocalTrade.trades_open.append(trade) LocalTrade.trades_open.append(trade) + backtesting.wallets.update() trade = backtesting._enter_trade(pair, row=row) assert trade is None LocalTrade.trades_open.pop() @@ -528,6 +529,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: assert trade is not None backtesting.strategy.custom_stake_amount = lambda **kwargs: 123.5 + backtesting.wallets.update() trade = backtesting._enter_trade(pair, row=row) assert trade assert trade.stake_amount == 123.5 From 9bf86bbe273668d9a8a6f72303a4533bd090534b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jan 2022 20:00:11 +0100 Subject: [PATCH 054/114] Extract backtesting row validation to separate function --- freqtrade/optimize/backtesting.py | 32 ++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 945022b66..4da8390af 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -658,6 +658,22 @@ class Backtesting: return False + def validate_row( + self, data: Dict, pair: str, row_index: int, current_time: datetime) -> Optional[Tuple]: + try: + # Row is treated as "current incomplete candle". + # Buy / sell signals are shifted by 1 to compensate for this. + row = data[pair][row_index] + except IndexError: + # missing Data for one pair at the end. + # Warnings for this are shown during data loading + return None + + # Waits until the time-counter reaches the start of the data for this pair. + if row[DATE_IDX] > current_time: + return None + return row + def backtest(self, processed: Dict, start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False, @@ -701,17 +717,8 @@ class Backtesting: self.check_abort() for i, pair in enumerate(data): row_index = indexes[pair] - try: - # Row is treated as "current incomplete candle". - # Buy / sell signals are shifted by 1 to compensate for this. - row = data[pair][row_index] - except IndexError: - # missing Data for one pair at the end. - # Warnings for this are shown during data loading - continue - - # Waits until the time-counter reaches the start of the data for this pair. - if row[DATE_IDX] > current_time: + row = self.validate_row(data, pair, row_index, current_time) + if not row: continue row_index += 1 @@ -769,8 +776,7 @@ class Backtesting: self.run_protections(enable_protections, pair, current_time) # 5. Cancel expired buy/sell orders. - canceled = self.check_order_cancel(trade, current_time) - if canceled: + if self.check_order_cancel(trade, current_time): # Close trade due to buy timeout expiration. open_trade_count -= 1 open_trades[pair].remove(trade) From 808cefe526aed74f355d2a7c4ed8b55e6fe96eb0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Feb 2022 10:12:03 +0100 Subject: [PATCH 055/114] Update order_selection logic --- freqtrade/persistence/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2e0f0753a..dfa98d97f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -650,9 +650,9 @@ class LocalTrade(): Finds order object by Order id. :param order_id: Exchange order id """ - orders = [o for o in self.orders if o.order_id == order_id] - if orders: - return orders[0] + for o in self.orders: + if o.order_id == order_id: + return o return None def select_order( From 2a59ef7311d07e0fcc971432564e218f8489a60e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Feb 2022 15:12:29 +0100 Subject: [PATCH 056/114] Add detail tests for timeout behaviour --- tests/optimize/__init__.py | 1 + tests/optimize/test_backtest_detail.py | 47 ++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 68088d2d5..b5f14056c 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -36,6 +36,7 @@ class BTContainer(NamedTuple): trailing_stop_positive_offset: float = 0.0 use_sell_signal: bool = False use_custom_stoploss: bool = False + custom_entry_price: Optional[float] = None def _get_frame_time_from_offset(offset): diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index f41b6101c..06517fff8 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, W0212, line-too-long, C0103, C0330, unused-argument import logging +from unittest.mock import MagicMock import pytest @@ -534,6 +535,47 @@ tc33 = BTContainer(data=[ )] ) +# Test 34: Custom-entry-price below all candles should timeout - so no trade happens. +tc34 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5500, 4951, 5000, 6172, 0, 0], # timeout + [2, 4900, 5250, 4500, 5100, 6172, 0, 0], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.0, + custom_entry_price=4200, trades=[] +) + +# Test 35: Custom-entry-price above all candles should timeout - so no trade happens. +tc35 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5500, 4951, 5000, 6172, 0, 0], # Timeout + [2, 4900, 5250, 4500, 5100, 6172, 0, 0], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.0, + custom_entry_price=7200, trades=[] +) + +# Test 36: Custom-entry-price around candle low +# Causes immediate ROI exit. This is currently expected behavior (#6261) +# https://github.com/freqtrade/freqtrade/issues/6261 +# But may change at a later point. +tc36 = BTContainer(data=[ + # D O H L C V B S BT + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5500, 4951, 5000, 6172, 0, 0], # enter trade (signal on last candle) and stop + [2, 4900, 5250, 4500, 5100, 6172, 0, 0], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.1, + custom_entry_price=4952, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] +) + + TESTS = [ tc0, tc1, @@ -569,6 +611,9 @@ TESTS = [ tc31, tc32, tc33, + tc34, + tc35, + tc36, ] @@ -597,6 +642,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: backtesting.required_startup = 0 backtesting.strategy.advise_buy = lambda a, m: frame backtesting.strategy.advise_sell = lambda a, m: frame + if data.custom_entry_price: + backtesting.strategy.custom_entry_price = MagicMock(return_value=data.custom_entry_price) backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss caplog.set_level(logging.DEBUG) From 22173851d6b8e8aa22d7adb8877e0e59775b6e2c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Feb 2022 15:20:05 +0100 Subject: [PATCH 057/114] Detail tests for custom exit pricing --- tests/optimize/__init__.py | 1 + tests/optimize/test_backtest_detail.py | 37 +++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index b5f14056c..ce6ea0f0c 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -37,6 +37,7 @@ class BTContainer(NamedTuple): use_sell_signal: bool = False use_custom_stoploss: bool = False custom_entry_price: Optional[float] = None + custom_exit_price: Optional[float] = None def _get_frame_time_from_offset(offset): diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 06517fff8..42d68593a 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -566,7 +566,7 @@ tc35 = BTContainer(data=[ tc36 = BTContainer(data=[ # D O H L C V B S BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], - [1, 5000, 5500, 4951, 5000, 6172, 0, 0], # enter trade (signal on last candle) and stop + [1, 5000, 5500, 4951, 5000, 6172, 0, 0], # Enter and immediate ROI [2, 4900, 5250, 4500, 5100, 6172, 0, 0], [3, 5100, 5100, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], @@ -576,6 +576,37 @@ tc36 = BTContainer(data=[ ) +# Test 37: Custom exit price below all candles +# causes sell signal timeout +tc37 = BTContainer(data=[ + # D O H L C V B S BT + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5500, 4951, 5000, 6172, 0, 0], + [2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout + [3, 5100, 5100, 4950, 4950, 6172, 0, 0], + [4, 5000, 5100, 4950, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, + use_sell_signal=True, + custom_exit_price=4552, + trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)] +) + +# Test 38: Custom exit price above all candles +# causes sell signal timeout +tc38 = BTContainer(data=[ + # D O H L C V B S BT + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5500, 4951, 5000, 6172, 0, 0], + [2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout + [3, 5100, 5100, 4950, 4950, 6172, 0, 0], + [4, 5000, 5100, 4950, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, + use_sell_signal=True, + custom_exit_price=6052, + trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)] +) + + TESTS = [ tc0, tc1, @@ -614,6 +645,8 @@ TESTS = [ tc34, tc35, tc36, + tc37, + tc38, ] @@ -644,6 +677,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: backtesting.strategy.advise_sell = lambda a, m: frame if data.custom_entry_price: backtesting.strategy.custom_entry_price = MagicMock(return_value=data.custom_entry_price) + if data.custom_exit_price: + backtesting.strategy.custom_exit_price = MagicMock(return_value=data.custom_exit_price) backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss caplog.set_level(logging.DEBUG) From 82006ff1db64b13874a7a22c863d8a72724fa6f4 Mon Sep 17 00:00:00 2001 From: Bloodhunter4rc Date: Sat, 5 Feb 2022 22:19:42 +0100 Subject: [PATCH 058/114] Update setup.sh --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 1df9df606..e1c9b1cba 100755 --- a/setup.sh +++ b/setup.sh @@ -36,7 +36,7 @@ function check_installed_python() { fi done - echo "No usable python found. Please make sure to have python3.7 or newer installed." + echo "No usable python found. Please make sure to have python3.8 or newer installed." exit 1 } From cfaf13c90ff1a7baf7fc0dbaab97ea8c67815a0d Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 6 Feb 2022 02:21:16 +0000 Subject: [PATCH 059/114] update --- freqtrade/rpc/telegram.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 8b33e8dfa..9079e1361 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -435,6 +435,7 @@ class Telegram(RPCHandler): results = self._rpc._rpc_trade_status(trade_ids=trade_ids) position_adjust = self._config.get('position_adjustment_enable', False) + max_entries = self._config.get('max_entry_position_adjustment', -1) messages = [] for r in results: r['open_date_hum'] = arrow.get(r['open_date']).humanize() From c4a54cc9cd01803b3fde8120711657a4e42fcecb Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 6 Feb 2022 02:31:14 +0000 Subject: [PATCH 060/114] refinement of status --- freqtrade/rpc/telegram.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9079e1361..7500c0568 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -450,7 +450,10 @@ class Telegram(RPCHandler): ] if position_adjust: - lines.append("*Number of Buy(s):* `{num_entries}`") + max_buy_str = '' + if max_entries > 0: + max_buy_str = f"/{max_entries + 1}" + lines.append("*Number of Buy(s):* `{num_entries}`" + max_buy_str) lines.extend([ "*Open Rate:* `{open_rate:.8f}`", From 0477070faa3de8ba0aff9a2a86cc2c65a7118fbc Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 6 Feb 2022 02:49:02 +0000 Subject: [PATCH 061/114] hide some lines if trade is closed --- freqtrade/rpc/telegram.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7500c0568..06da7862e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -442,7 +442,7 @@ class Telegram(RPCHandler): r['num_entries'] = len(r['filled_entry_orders']) r['sell_reason'] = r.get('sell_reason', "") lines = [ - "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", + "*Trade ID:* `{trade_id}`" + ("` (since {open_date_hum})`" if r['is_open'] else ""), "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", @@ -460,27 +460,28 @@ class Telegram(RPCHandler): "*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "", "*Open Date:* `{open_date}`", "*Close Date:* `{close_date}`" if r['close_date'] else "", - "*Current Rate:* `{current_rate:.8f}`", + "*Current Rate:* `{current_rate:.8f}`" if r['is_open'] else "", ("*Current Profit:* " if r['is_open'] else "*Close Profit: *") + "`{profit_ratio:.2%}`", ]) - if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] - and r['initial_stop_loss_ratio'] is not None): - # Adding initial stoploss only if it is different from stoploss - lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` " - "`({initial_stop_loss_ratio:.2%})`") + if r['is_open']: + if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] + and r['initial_stop_loss_ratio'] is not None): + # Adding initial stoploss only if it is different from stoploss + lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` " + "`({initial_stop_loss_ratio:.2%})`") - # Adding stoploss and stoploss percentage only if it is not None - lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " + - ("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else "")) - lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " - "`({stoploss_current_dist_ratio:.2%})`") - if r['open_order']: - if r['sell_order_status']: - lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`") - else: - lines.append("*Open Order:* `{open_order}`") + # Adding stoploss and stoploss percentage only if it is not None + lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " + + ("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else "")) + lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " + "`({stoploss_current_dist_ratio:.2%})`") + if r['open_order']: + if r['sell_order_status']: + lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`") + else: + lines.append("*Open Order:* `{open_order}`") if len(r['filled_entry_orders']) > 1: lines_detail = self._prepare_buy_details( From 131b2d68d8f6b071dd3ab412627c6199cbc110a0 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 6 Feb 2022 03:04:23 +0000 Subject: [PATCH 062/114] reduce complexity (flake8) --- freqtrade/rpc/telegram.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 06da7862e..5da98f21b 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -450,9 +450,7 @@ class Telegram(RPCHandler): ] if position_adjust: - max_buy_str = '' - if max_entries > 0: - max_buy_str = f"/{max_entries + 1}" + max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "") lines.append("*Number of Buy(s):* `{num_entries}`" + max_buy_str) lines.extend([ From 4cf514e293984b0a6596352d9c84f1ceca0dd6d0 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 6 Feb 2022 03:16:56 +0000 Subject: [PATCH 063/114] fix flake8 --- freqtrade/rpc/telegram.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5da98f21b..e3916a7c7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -442,7 +442,8 @@ class Telegram(RPCHandler): r['num_entries'] = len(r['filled_entry_orders']) r['sell_reason'] = r.get('sell_reason', "") lines = [ - "*Trade ID:* `{trade_id}`" + ("` (since {open_date_hum})`" if r['is_open'] else ""), + "*Trade ID:* `{trade_id}`" + + ("` (since {open_date_hum})`" if r['is_open'] else ""), "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", @@ -468,23 +469,22 @@ class Telegram(RPCHandler): and r['initial_stop_loss_ratio'] is not None): # Adding initial stoploss only if it is different from stoploss lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` " - "`({initial_stop_loss_ratio:.2%})`") + "`({initial_stop_loss_ratio:.2%})`") # Adding stoploss and stoploss percentage only if it is not None lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " + - ("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else "")) + ("`({stop_loss_ratio:.2%})`" if r['stop_loss_ratio'] else "")) lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " - "`({stoploss_current_dist_ratio:.2%})`") + "`({stoploss_current_dist_ratio:.2%})`") if r['open_order']: if r['sell_order_status']: lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`") else: lines.append("*Open Order:* `{open_order}`") - if len(r['filled_entry_orders']) > 1: - lines_detail = self._prepare_buy_details( - r['filled_entry_orders'], r['base_currency']) - lines.extend(lines_detail) + lines_detail = self._prepare_buy_details( + r['filled_entry_orders'], r['base_currency']) + lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else "")) # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line]).format(**r)) From 6b9696057d3d81cc8ad60e576c48b4146f2755cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Feb 2022 07:47:10 +0100 Subject: [PATCH 064/114] Update documentation to require python3.8 everywhere --- docs/installation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 2a1c3db0a..92aa59498 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -24,7 +24,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). !!! Note - Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. + Python3.8 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. Also, python headers (`python-dev` / `python-devel`) must be available for the installation to complete successfully. !!! Warning "Up-to-date clock" @@ -54,7 +54,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. !!! Note - Python3.7 or higher and the corresponding pip are assumed to be available. + Python3.8 or higher and the corresponding pip are assumed to be available. === "Debian/Ubuntu" #### Install necessary dependencies @@ -69,7 +69,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces === "RaspberryPi/Raspbian" The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/). - This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. + This image comes with python3.9 preinstalled, making it easy to get freqtrade up and running. Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. @@ -169,7 +169,7 @@ You can as well update, configure and reset the codebase of your bot with `./scr ** --install ** With this option, the script will install the bot and most dependencies: -You will need to have git and python3.7+ installed beforehand for this to work. +You will need to have git and python3.8+ installed beforehand for this to work. * Mandatory software as: `ta-lib` * Setup your virtualenv under `.env/` From 2a3ab1ef6147a0dd649f27ecc3df273372c23f75 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 6 Feb 2022 07:08:27 +0000 Subject: [PATCH 065/114] 1 more line to be hidden --- freqtrade/rpc/telegram.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e3916a7c7..eca519e77 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -370,7 +370,7 @@ class Telegram(RPCHandler): else: return "\N{CROSS MARK}" - def _prepare_buy_details(self, filled_orders, base_currency): + def _prepare_buy_details(self, filled_orders, base_currency, is_open): """ Prepare details of trade with buy adjustment enabled """ @@ -400,8 +400,9 @@ class Telegram(RPCHandler): hours, remainder = divmod(dur_buys.seconds, 3600) minutes, seconds = divmod(remainder, 60) lines.append("*Buy #{}:* at {:.2%} avg profit".format(x+1, minus_on_buy)) - lines.append("({})".format(current_buy_datetime - .humanize(granularity=["day", "hour", "minute"]))) + if is_open: + lines.append("({})".format(current_buy_datetime + .humanize(granularity=["day", "hour", "minute"]))) lines.append("*Buy Amount:* {} ({:.8f} {})" .format(cur_buy_amount, order["cost"], base_currency)) lines.append("*Average Buy Price:* {} ({:.2%} from 1st buy rate)" @@ -483,7 +484,7 @@ class Telegram(RPCHandler): lines.append("*Open Order:* `{open_order}`") lines_detail = self._prepare_buy_details( - r['filled_entry_orders'], r['base_currency']) + r['filled_entry_orders'], r['base_currency'], r['is_open']) lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else "")) # Filter empty lines using list-comprehension From 17d748dd4c3156e353fb4cafea2ff7163accaa54 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Feb 2022 13:19:00 +0100 Subject: [PATCH 066/114] Improve handling of left_open_trades --- freqtrade/optimize/backtesting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4da8390af..737cede9d 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -609,7 +609,8 @@ class Backtesting: for pair in open_trades.keys(): if len(open_trades[pair]) > 0: for trade in open_trades[pair]: - if trade.open_order_id: + if trade.open_order_id and trade.nr_of_successful_buys == 0: + # Ignore trade if buy-order did not fill yet continue sell_row = data[pair][-1] From 644442e2f9eadede920eea40051096950e1fa287 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Feb 2022 13:37:31 +0100 Subject: [PATCH 067/114] Track timedout orders --- docs/bot-basics.md | 1 + docs/strategy-callbacks.md | 6 ++++-- freqtrade/optimize/backtesting.py | 4 ++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index a9a2628f6..8c6303063 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -62,6 +62,7 @@ This loop will be repeated again and again until the bot is stopped. * Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested. * Call `custom_stoploss()` and `custom_sell()` to find custom exit points. * For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle). + * Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_buy_timeout()` / `check_sell_timeout()` strategy callbacks. * Generate backtest report output !!! Note diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index bff5bd998..9cdcb65bd 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -467,7 +467,8 @@ class AwesomeStrategy(IStrategy): 'sell': 60 * 25 } - def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: + def check_buy_timeout(self, pair: str, trade: Trade, order: dict, + current_time: datetime, **kwargs) -> bool: ob = self.dp.orderbook(pair, 1) current_price = ob['bids'][0][0] # Cancel buy order if price is more than 2% above the order. @@ -476,7 +477,8 @@ class AwesomeStrategy(IStrategy): return False - def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: + def check_sell_timeout(self, pair: str, trade: Trade, order: dict, + current_time: datetime, **kwargs) -> bool: ob = self.dp.orderbook(pair, 1) current_price = ob['asks'][0][0] # Cancel sell order if price is more than 2% below the order. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 737cede9d..cc4eb5351 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -233,6 +233,7 @@ class Backtesting: PairLocks.reset_locks() Trade.reset_trades() self.rejected_trades = 0 + self.timedout_orders = 0 self.dataprovider.clear_cache() if enable_protections: self._load_protections(self.strategy) @@ -646,6 +647,7 @@ class Backtesting: timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time) if timedout: + self.timedout_orders += 1 if order.side == 'buy': if trade.nr_of_successful_buys == 0: # Remove trade due to buy timeout expiration. @@ -796,6 +798,8 @@ class Backtesting: 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), 'rejected_signals': self.rejected_trades, + # TODO: timedout_orders should be shown as part of results. + # 'timedout_orders': self.timedout_orders, 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), } From 8f2425e49f856a38a09c7a4a3ec17b5dc446b4a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Feb 2022 14:06:46 +0100 Subject: [PATCH 068/114] Add rudimentary tests for pg-specific stuff --- tests/test_persistence.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 8305fe0f5..df989b645 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -13,6 +13,7 @@ from sqlalchemy import create_engine, text from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db +from freqtrade.persistence.migrations import get_last_sequence_ids, set_sequence_ids from tests.conftest import create_mock_trades, create_mock_trades_usdt, log_has, log_has_re @@ -679,6 +680,38 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): caplog) +def test_migrate_get_last_sequence_ids(): + engine = MagicMock() + engine.begin = MagicMock() + engine.name = 'postgresql' + get_last_sequence_ids(engine, 'trades_bak', 'orders_bak') + + assert engine.begin.call_count == 2 + engine.reset_mock() + engine.begin.reset_mock() + + engine.name = 'somethingelse' + get_last_sequence_ids(engine, 'trades_bak', 'orders_bak') + + assert engine.begin.call_count == 0 + + +def test_migrate_set_sequence_ids(): + engine = MagicMock() + engine.begin = MagicMock() + engine.name = 'postgresql' + set_sequence_ids(engine, 22, 55) + + assert engine.begin.call_count == 1 + engine.reset_mock() + engine.begin.reset_mock() + + engine.name = 'somethingelse' + set_sequence_ids(engine, 22, 55) + + assert engine.begin.call_count == 0 + + def test_adjust_stop_loss(fee): trade = Trade( pair='ADA/USDT', From da73e754b4c6310c00382b39a96baa4c58a9423b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Feb 2022 14:19:21 +0100 Subject: [PATCH 069/114] Explicit map coingecko symbol to ID for bnb and sol closes #6361 --- freqtrade/rpc/fiat_convert.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/fiat_convert.py b/freqtrade/rpc/fiat_convert.py index f65fd2d54..82a6a4778 100644 --- a/freqtrade/rpc/fiat_convert.py +++ b/freqtrade/rpc/fiat_convert.py @@ -17,6 +17,15 @@ from freqtrade.constants import SUPPORTED_FIAT logger = logging.getLogger(__name__) +# Manually map symbol to ID for some common coins +# with duplicate coingecko entries +coingecko_mapping = { + 'eth': 'ethereum', + 'bnb': 'binancecoin', + 'sol': 'solana', +} + + class CryptoToFiatConverter: """ Main class to initiate Crypto to FIAT. @@ -77,8 +86,9 @@ class CryptoToFiatConverter: else: return None found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol] - if crypto_symbol == 'eth': - found = [x for x in self._coinlistings if x['id'] == 'ethereum'] + + if crypto_symbol in coingecko_mapping.keys(): + found = [x for x in self._coinlistings if x['id'] == coingecko_mapping[crypto_symbol]] if len(found) == 1: return found[0]['id'] From 7232324eb7e4cc3e1fb98f2bcf2f27fbb65df6d2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Feb 2022 14:33:31 +0100 Subject: [PATCH 070/114] Update missing doc segment --- docs/strategy-callbacks.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 9cdcb65bd..24b81d2dc 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -389,8 +389,8 @@ class AwesomeStrategy(IStrategy): If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate. !!! Warning "Backtesting" - While Custom prices are supported in backtesting (starting with 2021.12), prices will be moved to within the candle's high/low prices. - This behavior is currently being tested, and might be changed at a later point. + Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range. + Orders that don't fill immediately are subject to regular timeout handling. `custom_exit_price()` is only called for sells of type Sell_signal and Custom sell. All other sell-types will use regular backtesting prices. ## Custom order timeout rules @@ -400,7 +400,8 @@ Simple, time-based order-timeouts can be configured either via strategy or in th However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not. !!! Note - Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances. + Backtesting fills orders if their price falls within the candle's low/high range. + The below callbacks will be called for orders that don't fill automatically (which use custom pricing). ### Custom order timeout example From b657d2d8de8c32b32aa285be311818f1ab4ed9dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Feb 2022 14:38:04 +0100 Subject: [PATCH 071/114] Add Github funding --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..3fa04cb6f --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [xmatthias] From 0b01fcf0477b0eb78b8f0d1eb1f2d0c7b5e76dcd Mon Sep 17 00:00:00 2001 From: zx <54022220+ediziks@users.noreply.github.com> Date: Sun, 6 Feb 2022 15:40:54 +0100 Subject: [PATCH 072/114] Add ProfitDrawdownHyperoptLoss method --- docs/hyperopt.md | 3 +- freqtrade/constants.py | 2 +- .../optimize/hyperopt_loss_profit_drawdown.py | 29 +++++++++++++++++++ tests/optimize/test_hyperoptloss.py | 3 +- 4 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 freqtrade/optimize/hyperopt_loss_profit_drawdown.py diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b7b6cb772..27d5a8761 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -116,7 +116,7 @@ optional arguments: ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily, - CalmarHyperOptLoss, MaxDrawDownHyperOptLoss + CalmarHyperOptLoss, MaxDrawDownHyperOptLoss, ProfitDrawDownHyperOptLoss --disable-param-export Disable automatic hyperopt parameter export. --ignore-missing-spaces, --ignore-unparameterized-spaces @@ -525,6 +525,7 @@ Currently, the following loss functions are builtin: * `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation. * `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown. * `CalmarHyperOptLoss` - Optimizes Calmar Ratio calculated on trade returns relative to max drawdown. +* `ProfitDrawDownHyperOptLoss` - Optimizes by max Profit & min Drawdown objective. `DRAWDOWN_MULT` variable within the hyperoptloss file can be adjusted to be stricter or more flexible on drawdown purposes. Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d94e8d850..e7782b6d2 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -26,7 +26,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily', 'CalmarHyperOptLoss', - 'MaxDrawDownHyperOptLoss'] + 'MaxDrawDownHyperOptLoss', 'ProfitDrawDownHyperOptLoss'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', diff --git a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py new file mode 100644 index 000000000..8bb8cd9d4 --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py @@ -0,0 +1,29 @@ +""" +ProfitDrawDownHyperOptLoss + +This module defines the alternative HyperOptLoss class based on Profit & +Drawdown objective which can be used for Hyperoptimization. + +Possible to change `DRAWDOWN_MULT` to penalize drawdown objective for +individual needs. +""" +from pandas import DataFrame +from freqtrade.optimize.hyperopt import IHyperOptLoss +from freqtrade.data.btanalysis import calculate_max_drawdown + +# higher numbers penalize drawdowns more severely +DRAWDOWN_MULT = 0.075 + + +class ProfitDrawDownHyperOptLoss(IHyperOptLoss): + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, *args, **kwargs) -> float: + total_profit = results["profit_abs"].sum() + + # from freqtrade.optimize.optimize_reports.generate_strategy_stats() + try: + _, _, _, _, max_drawdown_per = calculate_max_drawdown(results, value_col="profit_ratio") + except ValueError: + max_drawdown_per = 0 + + return -1 * (total_profit * (1 - max_drawdown_per * DRAWDOWN_MULT)) diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index e4a2eec2e..e3f6daf6c 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -86,6 +86,7 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> "SharpeHyperOptLossDaily", "MaxDrawDownHyperOptLoss", "CalmarHyperOptLoss", + "ProfitDrawDownHyperOptLoss", ]) def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunction) -> None: @@ -106,7 +107,7 @@ def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunct config=default_conf, processed=None, backtest_stats={'profit_total': hyperopt_results['profit_abs'].sum()} - ) + ) over = hl.hyperopt_loss_function( results_over, trade_count=len(results_over), From c19f3950da26c1053e73145a6b1688451f39747f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Feb 2022 16:02:18 +0100 Subject: [PATCH 073/114] Add losing trade to usdt_mock_trades --- tests/conftest.py | 5 ++- tests/conftest_trades_usdt.py | 57 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 20e027c2e..92deb6568 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,7 +25,8 @@ from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, mock_trade_5, mock_trade_6) from tests.conftest_trades_usdt import (mock_trade_usdt_1, mock_trade_usdt_2, mock_trade_usdt_3, - mock_trade_usdt_4, mock_trade_usdt_5, mock_trade_usdt_6) + mock_trade_usdt_4, mock_trade_usdt_5, mock_trade_usdt_6, + mock_trade_usdt_7) logging.getLogger('').setLevel(logging.INFO) @@ -258,6 +259,8 @@ def create_mock_trades_usdt(fee, use_db: bool = True): trade = mock_trade_usdt_6(fee) add_trade(trade) + trade = mock_trade_usdt_7(fee) + add_trade(trade) if use_db: Trade.commit() diff --git a/tests/conftest_trades_usdt.py b/tests/conftest_trades_usdt.py index 1a03f0381..7093678e4 100644 --- a/tests/conftest_trades_usdt.py +++ b/tests/conftest_trades_usdt.py @@ -303,3 +303,60 @@ def mock_trade_usdt_6(fee): o = Order.parse_from_ccxt_object(mock_order_usdt_6_sell(), 'LTC/USDT', 'sell') trade.orders.append(o) return trade + + +def mock_order_usdt_7(): + return { + 'id': 'prod_buy_7', + 'symbol': 'LTC/USDT', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 10.0, + 'amount': 2.0, + 'filled': 2.0, + 'remaining': 0.0, + } + + +def mock_order_usdt_7_sell(): + return { + 'id': 'prod_sell_7', + 'symbol': 'LTC/USDT', + 'status': 'closed', + 'side': 'sell', + 'type': 'limit', + 'price': 8.0, + 'amount': 2.0, + 'filled': 2.0, + 'remaining': 0.0, + } + + +def mock_trade_usdt_7(fee): + """ + Simulate prod entry with open sell order + """ + trade = Trade( + pair='LTC/USDT', + stake_amount=20.0, + amount=2.0, + amount_requested=2.0, + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5), + fee_open=fee.return_value, + fee_close=fee.return_value, + is_open=False, + open_rate=10.0, + close_rate=8.0, + close_profit=-0.2, + close_profit_abs=-4.0, + exchange='binance', + strategy='SampleStrategy', + open_order_id="prod_sell_6", + timeframe=5, + ) + o = Order.parse_from_ccxt_object(mock_order_usdt_7(), 'LTC/USDT', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object(mock_order_usdt_7_sell(), 'LTC/USDT', 'sell') + trade.orders.append(o) + return trade From ef086d438cd8b69903fe26e64b27cc14760abc9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Feb 2022 16:03:14 +0100 Subject: [PATCH 074/114] Update PerformanceFilter test to run with USDT pairs --- tests/plugins/test_pairlist.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 219933bb4..da1bda066 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -15,7 +15,7 @@ from freqtrade.persistence import Trade from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver -from tests.conftest import (create_mock_trades, get_patched_exchange, get_patched_freqtradebot, +from tests.conftest import (create_mock_trades_usdt, get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re, num_log_has) @@ -715,29 +715,31 @@ def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None: @pytest.mark.usefixtures("init_persistence") -def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None: - whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC') - whitelist_conf['pairlists'] = [ +def test_PerformanceFilter_lookback(mocker, default_conf_usdt, fee, caplog) -> None: + default_conf_usdt['exchange']['pair_whitelist'].extend(['ADA/USDT', 'XRP/USDT', 'ETC/USDT']) + default_conf_usdt['pairlists'] = [ {"method": "StaticPairList"}, {"method": "PerformanceFilter", "minutes": 60, "min_profit": 0.01} ] mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) - exchange = get_patched_exchange(mocker, whitelist_conf) - pm = PairListManager(exchange, whitelist_conf) + exchange = get_patched_exchange(mocker, default_conf_usdt) + pm = PairListManager(exchange, default_conf_usdt) pm.refresh_pairlist() - assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + assert pm.whitelist == ['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'] with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: - create_mock_trades(fee) + create_mock_trades_usdt(fee) pm.refresh_pairlist() - assert pm.whitelist == ['XRP/BTC'] + assert pm.whitelist == ['XRP/USDT'] assert log_has_re(r'Removing pair .* since .* is below .*', caplog) # Move to "outside" of lookback window, so original sorting is restored. t.move_to("2021-09-01 07:00:00 +00:00") pm.refresh_pairlist() - assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + assert pm.whitelist == ['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'] + + def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: From 5eb5029856d9596b1d0175abc5394133c965dfb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Feb 2022 16:19:11 +0100 Subject: [PATCH 075/114] Performancefilter - improve sorting Ordering of Pairs without history should remain identical, so pairs with positive performance move to the front, and negative pairs move to the back. closes #4893 --- .../plugins/pairlist/PerformanceFilter.py | 3 +- tests/conftest_trades_usdt.py | 3 +- tests/plugins/test_pairlist.py | 31 +++++++++++++++++-- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index d3196d3ae..5b02a47ab 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -60,6 +60,7 @@ class PerformanceFilter(IPairList): # Get pairlist from performance dataframe values list_df = pd.DataFrame({'pair': pairlist}) + list_df['prior_idx'] = list_df.index # Set initial value for pairs with no trades to 0 # Sort the list using: @@ -67,7 +68,7 @@ class PerformanceFilter(IPairList): # - then count (low to high, so as to favor same performance with fewer trades) # - then pair name alphametically sorted_df = list_df.merge(performance, on='pair', how='left')\ - .fillna(0).sort_values(by=['count', 'pair'], ascending=True)\ + .fillna(0).sort_values(by=['count', 'prior_idx'], ascending=True)\ .sort_values(by=['profit_ratio'], ascending=False) if self._min_profit is not None: removed = sorted_df[sorted_df['profit_ratio'] < self._min_profit] diff --git a/tests/conftest_trades_usdt.py b/tests/conftest_trades_usdt.py index 7093678e4..508e54f03 100644 --- a/tests/conftest_trades_usdt.py +++ b/tests/conftest_trades_usdt.py @@ -342,7 +342,8 @@ def mock_trade_usdt_7(fee): stake_amount=20.0, amount=2.0, amount_requested=2.0, - open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5), + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), + close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=5), fee_open=fee.return_value, fee_close=fee.return_value, is_open=False, diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index da1bda066..52158a889 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -740,6 +740,33 @@ def test_PerformanceFilter_lookback(mocker, default_conf_usdt, fee, caplog) -> N assert pm.whitelist == ['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'] +@pytest.mark.usefixtures("init_persistence") +def test_PerformanceFilter_keep_mid_order(mocker, default_conf_usdt, fee, caplog) -> None: + default_conf_usdt['exchange']['pair_whitelist'].extend(['ADA/USDT', 'ETC/USDT']) + default_conf_usdt['pairlists'] = [ + {"method": "StaticPairList", "allow_inactive": True}, + {"method": "PerformanceFilter", "minutes": 60, } + ] + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + exchange = get_patched_exchange(mocker, default_conf_usdt) + pm = PairListManager(exchange, default_conf_usdt) + pm.refresh_pairlist() + + assert pm.whitelist == ['ETH/USDT', 'LTC/USDT', 'XRP/USDT', + 'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'ETC/USDT'] + + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + create_mock_trades_usdt(fee) + pm.refresh_pairlist() + assert pm.whitelist == ['XRP/USDT', 'ETC/USDT', 'ETH/USDT', + 'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'LTC/USDT'] + # assert log_has_re(r'Removing pair .* since .* is below .*', caplog) + + # Move to "outside" of lookback window, so original sorting is restored. + t.move_to("2021-09-01 07:00:00 +00:00") + pm.refresh_pairlist() + assert pm.whitelist == ['ETH/USDT', 'LTC/USDT', 'XRP/USDT', + 'NEO/USDT', 'TKN/USDT', 'ADA/USDT', 'ETC/USDT'] def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: @@ -1170,13 +1197,13 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): {'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 2}, {'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 100}], ['TKN/BTC', 'ETH/BTC', 'LTC/BTC']), - # Tie in performance and count, broken by alphabetical sort + # Tie in performance and count, broken by prior sorting sort ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], [{'pair': 'LTC/BTC', 'profit_ratio': -0.0501, 'count': 1}, {'pair': 'TKN/BTC', 'profit_ratio': -0.0501, 'count': 1}, {'pair': 'ETH/BTC', 'profit_ratio': -0.0501, 'count': 1}], - ['ETH/BTC', 'LTC/BTC', 'TKN/BTC']), + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), ]) def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, allowlist_result, tickers, markets, ohlcv_history_list): From ee2a7a968bfbf0082be6c4c19492da085ef3edeb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Feb 2022 16:26:00 +0100 Subject: [PATCH 076/114] Add tests for /status on closed trades --- tests/rpc/test_rpc_telegram.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index da0bff4d8..6227bc4ad 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -263,6 +263,34 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: assert re.search(r'Number of Buy.*2', msg) assert re.search(r'Average Buy Price', msg) assert re.search(r'Order filled at', msg) + assert re.search(r'Close Date:', msg) is None + assert re.search(r'Close Profit:', msg) is None + + +@pytest.mark.usefixtures("init_persistence") +def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None: + update.message.chat.id = "123" + default_conf['telegram']['enabled'] = False + default_conf['telegram']['chat_id'] = "123" + default_conf['position_adjustment_enable'] = True + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_order=MagicMock(return_value=None), + get_rate=MagicMock(return_value=0.22), + ) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + + create_mock_trades(fee) + trades = Trade.get_trades([Trade.is_open.is_(False)]) + trade = trades[0] + context = MagicMock() + context.args = [str(trade.id)] + telegram._status(update=update, context=context) + assert msg_mock.call_count == 1 + msg = msg_mock.call_args_list[0][0][0] + assert re.search(r'Close Date:', msg) + assert re.search(r'Close Profit:', msg) def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: From 6b5f63d4d61e40764fa3e6d92ec482fa570229ef Mon Sep 17 00:00:00 2001 From: zx <54022220+ediziks@users.noreply.github.com> Date: Sun, 6 Feb 2022 16:20:25 +0100 Subject: [PATCH 077/114] change profit_ratio by profit_abs --- freqtrade/optimize/hyperopt_loss_profit_drawdown.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py index 8bb8cd9d4..104bacd2a 100644 --- a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py @@ -22,8 +22,8 @@ class ProfitDrawDownHyperOptLoss(IHyperOptLoss): # from freqtrade.optimize.optimize_reports.generate_strategy_stats() try: - _, _, _, _, max_drawdown_per = calculate_max_drawdown(results, value_col="profit_ratio") + profit_abs, _, _, _, _ = calculate_max_drawdown(results, value_col="profit_ratio") except ValueError: - max_drawdown_per = 0 + profit_abs = 0 - return -1 * (total_profit * (1 - max_drawdown_per * DRAWDOWN_MULT)) + return -1 * (total_profit * (1 - profit_abs * DRAWDOWN_MULT)) From 7d3b80fbde373208cfd340f707ea6fdd0b35b4ce Mon Sep 17 00:00:00 2001 From: zx <54022220+ediziks@users.noreply.github.com> Date: Sun, 6 Feb 2022 17:13:09 +0100 Subject: [PATCH 078/114] isort fix and leftover cleaning --- .DS_Store | Bin 0 -> 10244 bytes .../optimize/hyperopt_loss_profit_drawdown.py | 7 ++++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a67fb52e74d3dc023db3b44f1beea46a3926f230 GIT binary patch literal 10244 zcmeHM&1w`u5Uz=3)!mtN_0CQ<2SKF8*@Z45sjj; zJb4_if``Ys^*tl5=O$zXd!iM}XoI$Bnb!NXwL=+D29yD1Kp9X5{sso{o6XPYxRv@= z29yD1AZLKjhZv1z;$+WCb9CTkN&v_Ty3K>nr~`~m>}29(&q`y8Ic@b2In&6M7?Cx{ zc+BdMiIY7mtvN;3oFeBraurHsyu&Uw>J%kb>RTC52KpJ`+I@m%s6i?1i28j4J)5oO z+`>xM?pVX>yK?PzR;$%pSshx~j=izn&+l&^WYIXJMQ6=N6k~dvAT!j&jl-6Ywleu( z${+Kw{kY$Hbn=VAmfNC@Ur zGFZ#UL-dC6@H%$r8SYf*hK^;||t3^6_CHIV4XzlGxxYtFftLn15IDZo*2I3$B-o?ifle9ZUX zdiC*oe%{y^y1wtCapCuDf~jeYR*`!tJ;C1I#7c&Hyr z1vJ}+hd?91KAE5UeAeaj1Skh+NjwRh(b4?GmsobISLJAcW6I^EXCGiF&=CG#FR}kI zoRVGMlQ?4<$V=S+%T>PgwOA{_bKc=8m@mLpqABpX;({)5dVQ0)C?QQ-a|1-edA;_vi>wijpMzsFt`wCf4ntRrux|g-?zf%AI E2BFcLxBvhE literal 0 HcmV?d00001 diff --git a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py index 104bacd2a..6df605c82 100644 --- a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py @@ -8,8 +8,10 @@ Possible to change `DRAWDOWN_MULT` to penalize drawdown objective for individual needs. """ from pandas import DataFrame -from freqtrade.optimize.hyperopt import IHyperOptLoss + from freqtrade.data.btanalysis import calculate_max_drawdown +from freqtrade.optimize.hyperopt import IHyperOptLoss + # higher numbers penalize drawdowns more severely DRAWDOWN_MULT = 0.075 @@ -20,9 +22,8 @@ class ProfitDrawDownHyperOptLoss(IHyperOptLoss): def hyperopt_loss_function(results: DataFrame, trade_count: int, *args, **kwargs) -> float: total_profit = results["profit_abs"].sum() - # from freqtrade.optimize.optimize_reports.generate_strategy_stats() try: - profit_abs, _, _, _, _ = calculate_max_drawdown(results, value_col="profit_ratio") + profit_abs, _, _, _, _ = calculate_max_drawdown(results, value_col="profit_abs") except ValueError: profit_abs = 0 From 6d91a5ecbd9a510427328c80e41ac61638e2c84f Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Mon, 7 Feb 2022 01:03:54 +0000 Subject: [PATCH 079/114] Change "buy" and "sell" to "entry" and "exit" --- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/telegram.py | 50 +++++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 8e122d74d..37b246b1a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -264,7 +264,7 @@ class RPC: profitcol += " (" + fiat_display_currency + ")" if self._config.get('position_adjustment_enable', False): - columns = ['ID', 'Pair', 'Since', profitcol, '# Buys'] + columns = ['ID', 'Pair', 'Since', profitcol, '# Entries'] else: columns = ['ID', 'Pair', 'Since', profitcol] return trades_list, columns, fiat_profit_sum diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index eca519e77..94aea7726 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -370,21 +370,21 @@ class Telegram(RPCHandler): else: return "\N{CROSS MARK}" - def _prepare_buy_details(self, filled_orders, base_currency, is_open): + def _prepare_entry_details(self, filled_orders, base_currency, is_open): """ - Prepare details of trade with buy adjustment enabled + Prepare details of trade with entry adjustment enabled """ lines = [] for x, order in enumerate(filled_orders): - current_buy_datetime = arrow.get(order["order_filled_date"]) - cur_buy_amount = order["amount"] - cur_buy_average = order["average"] + current_entry_datetime = arrow.get(order["order_filled_date"]) + cur_entry_amount = order["amount"] + cur_entry_average = order["average"] lines.append(" ") if x == 0: - lines.append("*Buy #{}:*".format(x+1)) - lines.append("*Buy Amount:* {} ({:.8f} {})" - .format(cur_buy_amount, order["cost"], base_currency)) - lines.append("*Average Buy Price:* {}".format(cur_buy_average)) + lines.append("*Entry #{}:*".format(x+1)) + lines.append("*Entry Amount:* {} ({:.8f} {})" + .format(cur_entry_amount, order["cost"], base_currency)) + lines.append("*Average Entry Price:* {}".format(cur_entry_average)) else: sumA = 0 sumB = 0 @@ -392,23 +392,23 @@ class Telegram(RPCHandler): sumA += (filled_orders[y]["amount"] * filled_orders[y]["average"]) sumB += filled_orders[y]["amount"] prev_avg_price = sumA/sumB - price_to_1st_buy = ((cur_buy_average - filled_orders[0]["average"]) + price_to_1st_entry = ((cur_entry_average - filled_orders[0]["average"]) / filled_orders[0]["average"]) - minus_on_buy = (cur_buy_average - prev_avg_price)/prev_avg_price - dur_buys = current_buy_datetime - arrow.get(filled_orders[x-1]["order_filled_date"]) - days = dur_buys.days - hours, remainder = divmod(dur_buys.seconds, 3600) + minus_on_entry = (cur_entry_average - prev_avg_price)/prev_avg_price + dur_entry = current_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"]) + days = dur_entry.days + hours, remainder = divmod(dur_entry.seconds, 3600) minutes, seconds = divmod(remainder, 60) - lines.append("*Buy #{}:* at {:.2%} avg profit".format(x+1, minus_on_buy)) + lines.append("*Entry #{}:* at {:.2%} avg profit".format(x+1, minus_on_entry)) if is_open: - lines.append("({})".format(current_buy_datetime + lines.append("({})".format(current_entry_datetime .humanize(granularity=["day", "hour", "minute"]))) - lines.append("*Buy Amount:* {} ({:.8f} {})" - .format(cur_buy_amount, order["cost"], base_currency)) - lines.append("*Average Buy Price:* {} ({:.2%} from 1st buy rate)" - .format(cur_buy_average, price_to_1st_buy)) + lines.append("*Entry Amount:* {} ({:.8f} {})" + .format(cur_entry_amount, order["cost"], base_currency)) + lines.append("*Average Entry Price:* {} ({:.2%} from 1st entry rate)" + .format(cur_entry_average, price_to_1st_entry)) lines.append("*Order filled at:* {}".format(order["order_filled_date"])) - lines.append("({}d {}h {}m {}s from previous buy)" + lines.append("({}d {}h {}m {}s from previous entry)" .format(days, hours, minutes, seconds)) return lines @@ -447,13 +447,13 @@ class Telegram(RPCHandler): ("` (since {open_date_hum})`" if r['is_open'] else ""), "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", - "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", - "*Sell Reason:* `{sell_reason}`" if r['sell_reason'] else "", + "*Entry Tag:* `{buy_tag}`" if r['buy_tag'] else "", + "*Exit Reason:* `{sell_reason}`" if r['sell_reason'] else "", ] if position_adjust: max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "") - lines.append("*Number of Buy(s):* `{num_entries}`" + max_buy_str) + lines.append("*Number of Entries:* `{num_entries}`" + max_buy_str) lines.extend([ "*Open Rate:* `{open_rate:.8f}`", @@ -483,7 +483,7 @@ class Telegram(RPCHandler): else: lines.append("*Open Order:* `{open_order}`") - lines_detail = self._prepare_buy_details( + lines_detail = self._prepare_entry_details( r['filled_entry_orders'], r['base_currency'], r['is_open']) lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else "")) From 099a03f190f4ec1a942f761d9b03940a8a8c1db7 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Mon, 7 Feb 2022 01:52:59 +0000 Subject: [PATCH 080/114] flake8 --- freqtrade/rpc/telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 94aea7726..b5c8bee4a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -376,7 +376,7 @@ class Telegram(RPCHandler): """ lines = [] for x, order in enumerate(filled_orders): - current_entry_datetime = arrow.get(order["order_filled_date"]) + cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_amount = order["amount"] cur_entry_average = order["average"] lines.append(" ") @@ -393,15 +393,15 @@ class Telegram(RPCHandler): sumB += filled_orders[y]["amount"] prev_avg_price = sumA/sumB price_to_1st_entry = ((cur_entry_average - filled_orders[0]["average"]) - / filled_orders[0]["average"]) + / filled_orders[0]["average"]) minus_on_entry = (cur_entry_average - prev_avg_price)/prev_avg_price - dur_entry = current_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"]) + dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"]) days = dur_entry.days hours, remainder = divmod(dur_entry.seconds, 3600) minutes, seconds = divmod(remainder, 60) lines.append("*Entry #{}:* at {:.2%} avg profit".format(x+1, minus_on_entry)) if is_open: - lines.append("({})".format(current_entry_datetime + lines.append("({})".format(cur_entry_datetime .humanize(granularity=["day", "hour", "minute"]))) lines.append("*Entry Amount:* {} ({:.8f} {})" .format(cur_entry_amount, order["cost"], base_currency)) From e24c837e1ffc64846d8eb5c8b63cfe422dfd4d82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 03:01:13 +0000 Subject: [PATCH 081/114] Bump scipy from 1.7.3 to 1.8.0 Bumps [scipy](https://github.com/scipy/scipy) from 1.7.3 to 1.8.0. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.7.3...v1.8.0) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 122243bf2..e4698918a 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.7.3 +scipy==1.8.0 scikit-learn==1.0.2 scikit-optimize==0.9.0 filelock==3.4.2 From 22e395af875a4cfc69073bdfff719c6b288001c1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 03:01:18 +0000 Subject: [PATCH 082/114] Bump pytest from 6.2.5 to 7.0.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.5 to 7.0.0. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/6.2.5...7.0.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index be6c1f091..8274481c8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==3.3.1 flake8==4.0.1 flake8-tidy-imports==4.6.0 mypy==0.931 -pytest==6.2.5 +pytest==7.0.0 pytest-asyncio==0.17.2 pytest-cov==3.0.0 pytest-mock==3.7.0 From 1e43683283cdd27c13e5cf938a0bbacad5725b69 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 03:01:27 +0000 Subject: [PATCH 083/114] Bump ccxt from 1.71.73 to 1.72.36 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.71.73 to 1.72.36. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.71.73...1.72.36) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a3e225164..1d1fa8602 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.1 pandas==1.4.0 pandas-ta==0.3.14b -ccxt==1.71.73 +ccxt==1.72.36 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 From 576d5a5b48db5240f19f6a4dea731c1e738e4a28 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 03:01:29 +0000 Subject: [PATCH 084/114] Bump types-requests from 2.27.7 to 2.27.8 Bumps [types-requests](https://github.com/python/typeshed) from 2.27.7 to 2.27.8. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index be6c1f091..b4f936951 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,7 +22,7 @@ nbconvert==6.4.1 # mypy types types-cachetools==4.2.9 types-filelock==3.2.5 -types-requests==2.27.7 +types-requests==2.27.8 types-tabulate==0.8.5 # Extensions to datetime library From 110a270a0be09275d82c20948999a17a1dd662c3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 03:01:31 +0000 Subject: [PATCH 085/114] Bump uvicorn from 0.17.1 to 0.17.4 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.17.1 to 0.17.4. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.17.1...0.17.4) --- updated-dependencies: - dependency-name: uvicorn dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a3e225164..3794c883a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ sdnotify==0.3.2 # API Server fastapi==0.73.0 -uvicorn==0.17.1 +uvicorn==0.17.4 pyjwt==2.3.0 aiofiles==0.8.0 psutil==5.9.0 From b8af4bf8fedb9248e6ae855e3f8c16da720d52f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 03:01:43 +0000 Subject: [PATCH 086/114] Bump mkdocs-material from 8.1.9 to 8.1.10 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.1.9 to 8.1.10. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.1.9...8.1.10) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index f64a0ea7c..ad6d43f17 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.3 -mkdocs-material==8.1.9 +mkdocs-material==8.1.10 mdx_truly_sane_lists==1.2 pymdown-extensions==9.1 From 94b546228bc184fefdc3e8bd1a1ea31cf3a23e5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 03:01:48 +0000 Subject: [PATCH 087/114] Bump python-telegram-bot from 13.10 to 13.11 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.10 to 13.11. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.10...v13.11) --- updated-dependencies: - dependency-name: python-telegram-bot dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a3e225164..241c687f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ ccxt==1.71.73 cryptography==36.0.1 aiohttp==3.8.1 SQLAlchemy==1.4.31 -python-telegram-bot==13.10 +python-telegram-bot==13.11 arrow==1.2.2 cachetools==4.2.2 requests==2.27.1 From 2893d0b50db94d7aa6f98e29674e57468b476dc2 Mon Sep 17 00:00:00 2001 From: zx <54022220+ediziks@users.noreply.github.com> Date: Mon, 7 Feb 2022 06:22:27 +0100 Subject: [PATCH 088/114] proper var name --- freqtrade/optimize/hyperopt_loss_profit_drawdown.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py index 6df605c82..957663827 100644 --- a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py @@ -14,7 +14,7 @@ from freqtrade.optimize.hyperopt import IHyperOptLoss # higher numbers penalize drawdowns more severely -DRAWDOWN_MULT = 0.075 +DRAWDOWN_MULT = 0.01 class ProfitDrawDownHyperOptLoss(IHyperOptLoss): @@ -23,8 +23,8 @@ class ProfitDrawDownHyperOptLoss(IHyperOptLoss): total_profit = results["profit_abs"].sum() try: - profit_abs, _, _, _, _ = calculate_max_drawdown(results, value_col="profit_abs") + max_drawdown_abs, _, _, _, _ = calculate_max_drawdown(results, value_col="profit_abs") except ValueError: - profit_abs = 0 + max_drawdown_abs = 0 - return -1 * (total_profit * (1 - profit_abs * DRAWDOWN_MULT)) + return -1 * (total_profit * (1 - max_drawdown_abs * DRAWDOWN_MULT)) From f31fa07b3f90f608daa35e6bd21708935691d9c8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Feb 2022 05:28:26 +0000 Subject: [PATCH 089/114] Bump numpy from 1.22.1 to 1.22.2 Bumps [numpy](https://github.com/numpy/numpy) from 1.22.1 to 1.22.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.22.1...v1.22.2) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1d1fa8602..f3ccd39d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.22.1 +numpy==1.22.2 pandas==1.4.0 pandas-ta==0.3.14b From 8cdb6e0774dbbfe2e869d589d5a2a17d2e94da6b Mon Sep 17 00:00:00 2001 From: zx <54022220+ediziks@users.noreply.github.com> Date: Mon, 7 Feb 2022 06:31:16 +0100 Subject: [PATCH 090/114] DRAWDOWN_MULT back to a higher value as built-in for safer HOs first --- freqtrade/optimize/hyperopt_loss_profit_drawdown.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py index 957663827..2bba58333 100644 --- a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py @@ -14,7 +14,7 @@ from freqtrade.optimize.hyperopt import IHyperOptLoss # higher numbers penalize drawdowns more severely -DRAWDOWN_MULT = 0.01 +DRAWDOWN_MULT = 0.075 class ProfitDrawDownHyperOptLoss(IHyperOptLoss): From 5047492f5a43ded695b9f441331f1ed2d59880a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Feb 2022 07:20:44 +0100 Subject: [PATCH 091/114] gitignore .ds_store files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 16df71194..34c751242 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ freqtrade-plot.html freqtrade-profit-plot.html freqtrade/rpc/api_server/ui/* +# Macos related +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] From 7811a36ae91f564287b5dade852a599e2a6b103b Mon Sep 17 00:00:00 2001 From: zx <54022220+ediziks@users.noreply.github.com> Date: Mon, 7 Feb 2022 07:44:13 +0100 Subject: [PATCH 092/114] max_drawdown_abs calc fix & .DS_Store deletition --- .DS_Store | Bin 10244 -> 0 bytes .../optimize/hyperopt_loss_profit_drawdown.py | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index a67fb52e74d3dc023db3b44f1beea46a3926f230..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10244 zcmeHM&1w`u5Uz=3)!mtN_0CQ<2SKF8*@Z45sjj; zJb4_if``Ys^*tl5=O$zXd!iM}XoI$Bnb!NXwL=+D29yD1Kp9X5{sso{o6XPYxRv@= z29yD1AZLKjhZv1z;$+WCb9CTkN&v_Ty3K>nr~`~m>}29(&q`y8Ic@b2In&6M7?Cx{ zc+BdMiIY7mtvN;3oFeBraurHsyu&Uw>J%kb>RTC52KpJ`+I@m%s6i?1i28j4J)5oO z+`>xM?pVX>yK?PzR;$%pSshx~j=izn&+l&^WYIXJMQ6=N6k~dvAT!j&jl-6Ywleu( z${+Kw{kY$Hbn=VAmfNC@Ur zGFZ#UL-dC6@H%$r8SYf*hK^;||t3^6_CHIV4XzlGxxYtFftLn15IDZo*2I3$B-o?ifle9ZUX zdiC*oe%{y^y1wtCapCuDf~jeYR*`!tJ;C1I#7c&Hyr z1vJ}+hd?91KAE5UeAeaj1Skh+NjwRh(b4?GmsobISLJAcW6I^EXCGiF&=CG#FR}kI zoRVGMlQ?4<$V=S+%T>PgwOA{_bKc=8m@mLpqABpX;({)5dVQ0)C?QQ-a|1-edA;_vi>wijpMzsFt`wCf4ntRrux|g-?zf%AI E2BFcLxBvhE diff --git a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py index 2bba58333..90240e80f 100644 --- a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py @@ -23,7 +23,8 @@ class ProfitDrawDownHyperOptLoss(IHyperOptLoss): total_profit = results["profit_abs"].sum() try: - max_drawdown_abs, _, _, _, _ = calculate_max_drawdown(results, value_col="profit_abs") + _, _, _, _, _, max_drawdown_abs = calculate_max_drawdown(results, value_col="profit_abs") + # max_drawdown_abs = calculate_max_drawdown(results, value_col="profit_abs")[5] except ValueError: max_drawdown_abs = 0 From 92d1f2b9452d39d85bf4e2f5a027501aec074647 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Mon, 7 Feb 2022 07:31:35 +0000 Subject: [PATCH 093/114] fix tests --- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_telegram.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index cf5ede99f..1c713ee86 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -239,7 +239,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: rpc._config['position_adjustment_enable'] = True rpc._config['max_entry_position_adjustment'] = 3 result, headers, fiat_profit_sum = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') - assert "# Buys" in headers + assert "# Entries" in headers assert len(result[0]) == 5 # 4th column should be 1/4 - as 1 order filled (a total of 4 is possible) # 3 on top of the initial one. diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 6227bc4ad..796147609 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -260,8 +260,8 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 4 msg = msg_mock.call_args_list[0][0][0] - assert re.search(r'Number of Buy.*2', msg) - assert re.search(r'Average Buy Price', msg) + assert re.search(r'Number of Entries.*2', msg) + assert re.search(r'Average Entry Price', msg) assert re.search(r'Order filled at', msg) assert re.search(r'Close Date:', msg) is None assert re.search(r'Close Profit:', msg) is None From 4bce64b4271803429f31d03d5bdbe4502b1203e0 Mon Sep 17 00:00:00 2001 From: zx <54022220+ediziks@users.noreply.github.com> Date: Mon, 7 Feb 2022 14:12:07 +0100 Subject: [PATCH 094/114] commented method deletition --- freqtrade/optimize/hyperopt_loss_profit_drawdown.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py index 90240e80f..5bd12ff52 100644 --- a/freqtrade/optimize/hyperopt_loss_profit_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss_profit_drawdown.py @@ -23,8 +23,7 @@ class ProfitDrawDownHyperOptLoss(IHyperOptLoss): total_profit = results["profit_abs"].sum() try: - _, _, _, _, _, max_drawdown_abs = calculate_max_drawdown(results, value_col="profit_abs") - # max_drawdown_abs = calculate_max_drawdown(results, value_col="profit_abs")[5] + max_drawdown_abs = calculate_max_drawdown(results, value_col="profit_abs")[5] except ValueError: max_drawdown_abs = 0 From 380e383eee5d9f04d568834b9188c8051e1b9537 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Feb 2022 18:38:28 +0100 Subject: [PATCH 095/114] Add log_Responses to full config example #6374 --- config_examples/config_full.example.json | 1 + 1 file changed, 1 insertion(+) diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index 5202954f4..895a0af87 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -86,6 +86,7 @@ "key": "your_exchange_key", "secret": "your_exchange_secret", "password": "", + "log_responses": false, "ccxt_config": {}, "ccxt_async_config": {}, "pair_whitelist": [ From 036c2888b45b5c8695cf9cab9551cdf604d6d7eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Feb 2022 18:49:30 +0100 Subject: [PATCH 096/114] Track timedout entry/exit orders --- docs/backtesting.md | 3 +++ freqtrade/optimize/backtesting.py | 10 ++++++---- freqtrade/optimize/optimize_reports.py | 5 +++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 7420c1dec..e7846b1f8 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -313,6 +313,7 @@ A backtesting result will look like that: | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | Rejected Buy signals | 3089 | +| Entry/Exit Timeouts | 0 / 0 | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -400,6 +401,7 @@ It contains some useful key metrics about performance of your strategy on backte | Avg. Duration Winners | 4:23:00 | | Avg. Duration Loser | 6:55:00 | | Rejected Buy signals | 3089 | +| Entry/Exit Timeouts | 0 / 0 | | | | | Min balance | 0.00945123 BTC | | Max balance | 0.01846651 BTC | @@ -429,6 +431,7 @@ It contains some useful key metrics about performance of your strategy on backte - `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade). - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached. +- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used). - `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period. - `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$. - `Drawdown`: Maximum, absolute drawdown experienced. Difference between Drawdown High and Subsequent Low point. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cc4eb5351..a06c1fee8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -233,7 +233,8 @@ class Backtesting: PairLocks.reset_locks() Trade.reset_trades() self.rejected_trades = 0 - self.timedout_orders = 0 + self.timedout_entry_orders = 0 + self.timedout_exit_orders = 0 self.dataprovider.clear_cache() if enable_protections: self._load_protections(self.strategy) @@ -647,8 +648,8 @@ class Backtesting: timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time) if timedout: - self.timedout_orders += 1 if order.side == 'buy': + self.timedout_entry_orders += 1 if trade.nr_of_successful_buys == 0: # Remove trade due to buy timeout expiration. return True @@ -656,6 +657,7 @@ class Backtesting: # Close additional buy order del trade.orders[trade.orders.index(order)] if order.side == 'sell': + self.timedout_exit_orders += 1 # Close sell order and retry selling on next signal. del trade.orders[trade.orders.index(order)] @@ -798,8 +800,8 @@ class Backtesting: 'config': self.strategy.config, 'locks': PairLocks.get_all_locks(), 'rejected_signals': self.rejected_trades, - # TODO: timedout_orders should be shown as part of results. - # 'timedout_orders': self.timedout_orders, + 'timedout_entry_orders': self.timedout_entry_orders, + 'timedout_exit_orders': self.timedout_exit_orders, 'final_balance': self.wallets.get_total(self.strategy.config['stake_currency']), } diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 859238af3..5b1c2e135 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -436,6 +436,8 @@ def generate_strategy_stats(pairlist: List[str], 'dry_run_wallet': starting_balance, 'final_balance': content['final_balance'], 'rejected_signals': content['rejected_signals'], + 'timedout_entry_orders': content['timedout_entry_orders'], + 'timedout_exit_orders': content['timedout_exit_orders'], 'max_open_trades': max_open_trades, 'max_open_trades_setting': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), @@ -726,6 +728,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')), + ('Entry/Exit Timeouts', + f"{strat_results.get('timedout_entry_orders', 'N/A')} / " + f"{strat_results.get('timedout_exit_orders', 'N/A')}"), ('', ''), # Empty line to improve readability ('Min balance', round_coin_value(strat_results['csum_min'], From 85767d0d70e8a3683e370b4c54e6d701a0fa4912 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Feb 2022 19:33:22 +0100 Subject: [PATCH 097/114] Add timedout_*_orders to tests --- tests/optimize/test_backtesting.py | 12 ++++++++++++ tests/optimize/test_hyperopt.py | 4 ++++ tests/optimize/test_optimize_reports.py | 4 ++++ 3 files changed, 20 insertions(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 649a43b32..d61dffac4 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -1023,6 +1023,8 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'config': default_conf, 'locks': [], 'rejected_signals': 20, + 'timedout_entry_orders': 0, + 'timedout_exit_orders': 0, 'final_balance': 1000, }) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', @@ -1131,6 +1133,8 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'config': default_conf, 'locks': [], 'rejected_signals': 20, + 'timedout_entry_orders': 0, + 'timedout_exit_orders': 0, 'final_balance': 1000, }, { @@ -1138,6 +1142,8 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'config': default_conf, 'locks': [], 'rejected_signals': 20, + 'timedout_entry_orders': 0, + 'timedout_exit_orders': 0, 'final_balance': 1000, } ]) @@ -1240,6 +1246,8 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, 'config': default_conf, 'locks': [], 'rejected_signals': 20, + 'timedout_entry_orders': 0, + 'timedout_exit_orders': 0, 'final_balance': 1000, }, { @@ -1247,6 +1255,8 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, 'config': default_conf, 'locks': [], 'rejected_signals': 20, + 'timedout_entry_orders': 0, + 'timedout_exit_orders': 0, 'final_balance': 1000, } ]) @@ -1308,6 +1318,8 @@ def test_backtest_start_multi_strat_caching(default_conf, mocker, caplog, testda 'config': default_conf, 'locks': [], 'rejected_signals': 20, + 'timedout_entry_orders': 0, + 'timedout_exit_orders': 0, 'final_balance': 1000, }) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 1f7c2ee8c..2328585dd 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -364,6 +364,8 @@ def test_hyperopt_format_results(hyperopt): 'locks': [], 'final_balance': 0.02, 'rejected_signals': 2, + 'timedout_entry_orders': 0, + 'timedout_exit_orders': 0, 'backtest_start_time': 1619718665, 'backtest_end_time': 1619718665, } @@ -431,6 +433,8 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'config': hyperopt_conf, 'locks': [], 'rejected_signals': 20, + 'timedout_entry_orders': 0, + 'timedout_exit_orders': 0, 'final_balance': 1000, } diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 68257f4d8..c8768e236 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -82,6 +82,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): 'locks': [], 'final_balance': 1000.02, 'rejected_signals': 20, + 'timedout_entry_orders': 0, + 'timedout_exit_orders': 0, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, 'run_id': '123', @@ -131,6 +133,8 @@ def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): 'locks': [], 'final_balance': 1000.02, 'rejected_signals': 20, + 'timedout_entry_orders': 0, + 'timedout_exit_orders': 0, 'backtest_start_time': Arrow.utcnow().int_timestamp, 'backtest_end_time': Arrow.utcnow().int_timestamp, 'run_id': '124', From d2dbe8f8d02c397e993b49a9a2a57c9f0b72e1a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Feb 2022 06:47:55 +0100 Subject: [PATCH 098/114] Improve doc wording --- docs/strategy-callbacks.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 24b81d2dc..555352d21 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -390,7 +390,7 @@ class AwesomeStrategy(IStrategy): !!! Warning "Backtesting" Custom prices are supported in backtesting (starting with 2021.12), and orders will fill if the price falls within the candle's low/high range. - Orders that don't fill immediately are subject to regular timeout handling. + Orders that don't fill immediately are subject to regular timeout handling, which happens once per (detail) candle. `custom_exit_price()` is only called for sells of type Sell_signal and Custom sell. All other sell-types will use regular backtesting prices. ## Custom order timeout rules @@ -401,7 +401,7 @@ However, freqtrade also offers a custom callback for both order types, which all !!! Note Backtesting fills orders if their price falls within the candle's low/high range. - The below callbacks will be called for orders that don't fill automatically (which use custom pricing). + The below callbacks will be called once per (detail) candle for orders that don't fill immediately (which use custom pricing). ### Custom order timeout example From b192c82731105ad02da5e61e4451a1aff9e399d3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Feb 2022 07:10:54 +0100 Subject: [PATCH 099/114] Only call "custom_exit_price" for limit orders --- freqtrade/optimize/backtesting.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a06c1fee8..6bf0a7270 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -417,16 +417,19 @@ class Backtesting: closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) # call the custom exit price,with default value as previous closerate current_profit = trade.calc_profit_ratio(closerate) + order_type = self.strategy.order_types['sell'] if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL): # Custom exit pricing only for sell-signals - closerate = strategy_safe_wrapper(self.strategy.custom_exit_price, - default_retval=closerate)( - pair=trade.pair, trade=trade, - current_time=sell_candle_time, - proposed_rate=closerate, current_profit=current_profit) + if order_type == 'limit': + closerate = strategy_safe_wrapper(self.strategy.custom_exit_price, + default_retval=closerate)( + pair=trade.pair, trade=trade, + current_time=sell_candle_time, + proposed_rate=closerate, current_profit=current_profit) # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['sell'] + if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount, rate=closerate, @@ -458,7 +461,7 @@ class Backtesting: symbol=trade.pair, ft_order_side="sell", side="sell", - order_type=self.strategy.order_types['sell'], + order_type=order_type, status="open", price=closerate, average=closerate, @@ -537,7 +540,7 @@ class Backtesting: # If not pos adjust, trade is None return trade - time_in_force = self.strategy.order_time_in_force['sell'] + time_in_force = self.strategy.order_time_in_force['buy'] # Confirm trade entry: if not pos_adjust: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( From 118ae8a3d0248603ff4cf203de956b196d5756d6 Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Tue, 1 Feb 2022 19:34:50 +0100 Subject: [PATCH 100/114] Fix api_schemas/json_encoders by manually converting NaT values to empty Strings makes import of datetime columns more robust by first checking if value is null because strftime can't handle NaT values use `isnull()` because it handles all NaN/None/NaT cases --- freqtrade/rpc/api_server/api_schemas.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index c280f453c..b0ffeee7e 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,4 +1,5 @@ from datetime import date, datetime +from pandas import isnull from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel @@ -355,7 +356,9 @@ class PairHistory(BaseModel): class Config: json_encoders = { - datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT), + datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT) + # needed for aslong NaT doesn't work with strftime + if not isnull(v) else "", } From 926b01798138331877c6a004e8476e9af26f9dfa Mon Sep 17 00:00:00 2001 From: Joe Schr Date: Tue, 8 Feb 2022 16:42:39 +0100 Subject: [PATCH 101/114] Fix freqUI charts not displaying when dtype(datetime) column has NaT values fix dataframe_to_dict() issues by replacing NaT empty string and prepare for proper `.replace({NaT})` fix --- freqtrade/rpc/rpc.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 8e122d74d..97614a7b1 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -12,7 +12,7 @@ import psutil from dateutil.relativedelta import relativedelta from dateutil.tz import tzlocal from numpy import NAN, inf, int64, mean -from pandas import DataFrame +from pandas import DataFrame, NaT, isnull from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange @@ -963,6 +963,24 @@ class RPC: sell_mask = (dataframe['sell'] == 1) sell_signals = int(sell_mask.sum()) dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close'] + """ + band-aid until this is fixed: + https://github.com/pandas-dev/pandas/issues/45836 + """ + datetime_types = ['datetime', 'datetime64', 'datetime64[ns, UTC]'] + date_columns = dataframe.select_dtypes(include=datetime_types) + for date_column in date_columns: + # replace NaT with empty string, + # because if replaced with `None` + # it will be casted into NaT again + dataframe[date_column] = dataframe[date_column].apply( + lambda x: '' if isnull(x) else x) + + """ + try this if above pandas Issue#45836 is fixed: + https://github.com/pandas-dev/pandas/issues/45836 + """ + # dataframe = dataframe.replace({NaT: None}) dataframe = dataframe.replace([inf, -inf], NAN) dataframe = dataframe.replace({NAN: None}) From dcf8ad36f9ccb04ee725eaf65e83a7720a5fe315 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Feb 2022 19:10:29 +0100 Subject: [PATCH 102/114] Backtesting should not allow unrealistic (automatic-filling) orders. --- freqtrade/optimize/backtesting.py | 9 +++++++-- tests/optimize/test_backtest_detail.py | 14 ++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6bf0a7270..6c5933a51 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -426,7 +426,9 @@ class Backtesting: pair=trade.pair, trade=trade, current_time=sell_candle_time, proposed_rate=closerate, current_profit=current_profit) - + # We can't place orders lower than current low. + # freqtrade does not support this in live, and the order would fill immediately + closerate = max(closerate, sell_row[LOW_IDX]) # Confirm trade exit: time_in_force = self.strategy.order_time_in_force['sell'] @@ -515,7 +517,10 @@ class Backtesting: propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=row[OPEN_IDX])( pair=pair, current_time=current_time, - proposed_rate=row[OPEN_IDX], entry_tag=entry_tag) # default value is the open rate + proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate + # We can't place orders higher than current high (otherwise it'd be a stop limit buy) + # which freqtrade does not support in live. + propose_rate = min(propose_rate, row[HIGH_IDX]) min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0 max_stake_amount = self.wallets.get_available_stake_amount() diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 42d68593a..3164e11b9 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -547,7 +547,7 @@ tc34 = BTContainer(data=[ custom_entry_price=4200, trades=[] ) -# Test 35: Custom-entry-price above all candles should timeout - so no trade happens. +# Test 35: Custom-entry-price above all candles should have rate adjusted to "entry candle high" tc35 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -555,8 +555,10 @@ tc35 = BTContainer(data=[ [2, 4900, 5250, 4500, 5100, 6172, 0, 0], [3, 5100, 5100, 4650, 4750, 6172, 0, 0], [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], - stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.0, - custom_entry_price=7200, trades=[] + stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, + custom_entry_price=7200, trades=[ + BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=1) + ] ) # Test 36: Custom-entry-price around candle low @@ -577,7 +579,7 @@ tc36 = BTContainer(data=[ # Test 37: Custom exit price below all candles -# causes sell signal timeout +# Price adjusted to candle Low. tc37 = BTContainer(data=[ # D O H L C V B S BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -585,10 +587,10 @@ tc37 = BTContainer(data=[ [2, 4900, 5250, 4900, 5100, 6172, 0, 1], # exit - but timeout [3, 5100, 5100, 4950, 4950, 6172, 0, 0], [4, 5000, 5100, 4950, 4950, 6172, 0, 0]], - stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.0, + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01, use_sell_signal=True, custom_exit_price=4552, - trades=[BTrade(sell_reason=SellType.FORCE_SELL, open_tick=1, close_tick=4)] + trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=3)] ) # Test 38: Custom exit price above all candles From 172e018d2dba4d3dc4241e12a2279a0d7191d1e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Feb 2022 19:21:27 +0100 Subject: [PATCH 103/114] Add probit to list of non-working exchanges closes #6379 --- freqtrade/exchange/common.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 3916ee8f7..a6d3896eb 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -27,7 +27,8 @@ API_FETCH_ORDER_RETRY_COUNT = 5 BAD_EXCHANGES = { "bitmex": "Various reasons.", - "phemex": "Does not provide history. ", + "phemex": "Does not provide history.", + "probit": "Requires additional, regular calls to `signIn()`.", "poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.", } From 1d10d2c87c0a53acfe11569da6a54c8b9952dcd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Feb 2022 19:45:39 +0100 Subject: [PATCH 104/114] Okex -> okx --- README.md | 2 +- docs/exchanges.md | 8 ++++---- docs/index.md | 2 +- freqtrade/commands/build_config_commands.py | 4 ++-- freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/common.py | 1 + freqtrade/exchange/exchange.py | 2 +- freqtrade/exchange/{okex.py => okx.py} | 4 ++-- tests/exchange/test_ccxt_compat.py | 2 +- 9 files changed, 14 insertions(+), 13 deletions(-) rename freqtrade/exchange/{okex.py => okx.py} (84%) diff --git a/README.md b/README.md index b67e16010..d85eef71a 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even - [X] [FTX](https://ftx.com) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Kraken](https://kraken.com/) -- [X] [OKEX](https://www.okex.com/) +- [X] [OKX](https://www.okx.com/) - [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested diff --git a/docs/exchanges.md b/docs/exchanges.md index 374a6b8cc..e79abf220 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -182,13 +182,13 @@ Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force) For Kucoin, please add `"KCS/"` to your blacklist to avoid issues. Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore. -## OKEX +## OKX -OKEX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: +OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: ```json "exchange": { - "name": "okex", + "name": "okx", "key": "your_exchange_key", "secret": "your_exchange_secret", "password": "your_exchange_api_key_password", @@ -197,7 +197,7 @@ OKEX requires a passphrase for each api key, you will therefore need to add this ``` !!! Warning - OKEX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode. + OKX only provides 100 candles per api call. Therefore, the strategy will only have a pretty low amount of data available in backtesting mode. ## Gate.io diff --git a/docs/index.md b/docs/index.md index 1f8f15704..7ae110320 100644 --- a/docs/index.md +++ b/docs/index.md @@ -41,7 +41,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [FTX](https://ftx.com) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Kraken](https://kraken.com/) -- [X] [OKEX](https://www.okex.com/) +- [X] [OKX](https://www.okx.com/) - [ ] [potentially many others through ccxt](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index ca3f11a21..4c722c810 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -112,7 +112,7 @@ def ask_user_config() -> Dict[str, Any]: "ftx", "kucoin", "gateio", - "okex", + "okx", Separator(), "other", ], @@ -140,7 +140,7 @@ def ask_user_config() -> Dict[str, Any]: "type": "password", "name": "exchange_key_password", "message": "Insert Exchange API Key password", - "when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okex') + "when": lambda x: not x['dry_run'] and x['exchange_name'] in ('kucoin', 'okx') }, { "type": "confirm", diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index b356a8147..9dc2b8480 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -20,4 +20,4 @@ from freqtrade.exchange.gateio import Gateio from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kucoin import Kucoin -from freqtrade.exchange.okex import Okex +from freqtrade.exchange.okx import Okx diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index a6d3896eb..fad905b04 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -35,6 +35,7 @@ BAD_EXCHANGES = { MAP_EXCHANGE_CHILDCLASS = { 'binanceus': 'binance', 'binanceje': 'binance', + 'okex': 'okx', } diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 004fb2437..a2217a02e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1587,7 +1587,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non def is_exchange_officially_supported(exchange_name: str) -> bool: - return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okex'] + return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx'] def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: diff --git a/freqtrade/exchange/okex.py b/freqtrade/exchange/okx.py similarity index 84% rename from freqtrade/exchange/okex.py rename to freqtrade/exchange/okx.py index e68ee4a48..8e2cccf46 100644 --- a/freqtrade/exchange/okex.py +++ b/freqtrade/exchange/okx.py @@ -7,8 +7,8 @@ from freqtrade.exchange import Exchange logger = logging.getLogger(__name__) -class Okex(Exchange): - """Okex exchange class. +class Okx(Exchange): + """Okx exchange class. Contains adjustments needed for Freqtrade to work with this exchange. """ diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 44c664c92..09523bd59 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -53,7 +53,7 @@ EXCHANGES = { 'hasQuoteVolume': True, 'timeframe': '5m', }, - 'okex': { + 'okx': { 'pair': 'BTC/USDT', 'stake_currency': 'USDT', 'hasQuoteVolume': True, From 6191288ff942c3037c8ab0ff4cf8b71d49c9627d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Feb 2022 06:36:17 +0100 Subject: [PATCH 105/114] Add test for NaT problematic --- freqtrade/rpc/api_server/api_schemas.py | 3 --- freqtrade/rpc/rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 19 +++++++++++++++++++ 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index b0ffeee7e..6f358155e 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,5 +1,4 @@ from datetime import date, datetime -from pandas import isnull from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel @@ -357,8 +356,6 @@ class PairHistory(BaseModel): class Config: json_encoders = { datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT) - # needed for aslong NaT doesn't work with strftime - if not isnull(v) else "", } diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 97614a7b1..0ca105d6b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -12,7 +12,7 @@ import psutil from dateutil.relativedelta import relativedelta from dateutil.tz import tzlocal from numpy import NAN, inf, int64, mean -from pandas import DataFrame, NaT, isnull +from pandas import DataFrame, isnull from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 1c33dd928..eabbfc252 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -7,6 +7,7 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock +import pandas as pd import pytest import uvicorn from fastapi import FastAPI @@ -1181,6 +1182,24 @@ def test_api_pair_candles(botclient, ohlcv_history): 0.7039405, 8.885e-05, 0, 0, 1511686800000, None, None] ]) + ohlcv_history['sell'] = ohlcv_history['sell'].astype('float64') + ohlcv_history.at[0, 'sell'] = float('inf') + ohlcv_history['date1'] = ohlcv_history['date'] + ohlcv_history.at[0, 'date1'] = pd.NaT + + ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history) + rc = client_get(client, + f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") + assert_response(rc) + assert (rc.json()['data'] == + [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, + None, 0, None, '', 1511686200000, None, None], + ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, + 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0.0, '2017-11-26 08:55:00', + 1511686500000, 8.893e-05, None], + ['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, + 0.7039405, 8.885e-05, 0, 0.0, '2017-11-26 09:00:00', 1511686800000, None, None] + ]) def test_api_pair_history(botclient, ohlcv_history): From 4e2f06fe9c706b67746c4346649a3ca2999f463b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Feb 2022 06:48:26 +0100 Subject: [PATCH 106/114] Simplify band-aid code --- freqtrade/rpc/rpc.py | 13 +++---------- tests/rpc/test_rpc_apiserver.py | 2 +- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 0ca105d6b..033e32843 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -12,7 +12,7 @@ import psutil from dateutil.relativedelta import relativedelta from dateutil.tz import tzlocal from numpy import NAN, inf, int64, mean -from pandas import DataFrame, isnull +from pandas import DataFrame, NaT from freqtrade import __version__ from freqtrade.configuration.timerange import TimeRange @@ -973,16 +973,9 @@ class RPC: # replace NaT with empty string, # because if replaced with `None` # it will be casted into NaT again - dataframe[date_column] = dataframe[date_column].apply( - lambda x: '' if isnull(x) else x) + dataframe[date_column] = dataframe[date_column].astype(object).replace({NaT: None}) - """ - try this if above pandas Issue#45836 is fixed: - https://github.com/pandas-dev/pandas/issues/45836 - """ - # dataframe = dataframe.replace({NaT: None}) - dataframe = dataframe.replace([inf, -inf], NAN) - dataframe = dataframe.replace({NAN: None}) + dataframe = dataframe.replace({inf: None, -inf: None, NAN: None}) res = { 'pair': pair, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index eabbfc252..5b19e5e05 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1193,7 +1193,7 @@ def test_api_pair_candles(botclient, ohlcv_history): assert_response(rc) assert (rc.json()['data'] == [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, - None, 0, None, '', 1511686200000, None, None], + None, 0, None, None, 1511686200000, None, None], ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0.0, '2017-11-26 08:55:00', 1511686500000, 8.893e-05, None], From 6d3803fa2222f4e0dac1950f71934cda1c1803e4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Feb 2022 20:13:27 +0100 Subject: [PATCH 107/114] Add TokenBot promo --- README.md | 4 ++++ docs/assets/TokenBot-Freqtrade-banner.png | Bin 0 -> 61805 bytes docs/index.md | 6 ++++++ 3 files changed, 10 insertions(+) create mode 100644 docs/assets/TokenBot-Freqtrade-banner.png diff --git a/README.md b/README.md index d85eef71a..c9522f620 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,10 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) +## Sponsored promotion + +[![tokenbot-promo](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/TokenBot-Freqtrade-banner.png)](https://www.tokenbot.com) + ## Disclaimer This software is for educational purposes only. Do not risk money which diff --git a/docs/assets/TokenBot-Freqtrade-banner.png b/docs/assets/TokenBot-Freqtrade-banner.png new file mode 100644 index 0000000000000000000000000000000000000000..6087056b6e8faa9abe64188a8d22d1b006fc1895 GIT binary patch literal 61805 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

Xsp72QbApE=F8&^*MUF`}vlfUM<#ps3!t6s! z5M0xDR6%75Z=XIba!E>sYct>=i34MP@>Ut5GP*zbECmY}v|6a8^~B=XB4VuV8W8;7 z+P<%!sMd?R;0JEo`R|{D;->K6f(#|o4>b1J^6t1oNHa>>C^@TZ_Shs`5c70ULQn@9 zn`UC7^bLh1wEn#Q_HDyB%4XfI2~zhn+UtBOiIGXAkJYA$3Q+kK;gG=tkS7gmYW__} z=uzaKBTC=DU(h&k);s8f&q;ch&|B;3>fr3)P_ez=qd)P*H*bw0w?>F0`s)McdyKb) zgxP8FSt)7&uuGJ}RW9Bynku=u;W#;AP$^TrC{?Kh^-)t(d-l90mN?jLR@OS;&uH{G zgFtzrqfO}7NZ{UAsxa*@RfvZ9dEeb03kv~rQwjKa@FOl#D@~Cg?Hw9*JzgE_v}OtY_U)TF)kkP}C`nsrdo+bN7$%q`$WY&p zWoBp;$&)4?oGr|(NUCVUUCd_qUSmV1;n7!>HPUKyjB5rOlHo_S;z^X?&{#id+(3To zfW?goZ|?900EHWfRC@s*vPnVHj5tdR!EA!o9M#6Tdj98UzPoTpfdJ=hptV*qV_52b!+}vk+pA9W+<%dNF5&!ezV`I$-(G|tj zR@Mq-bO-Of-^X;e>zTDTy^`zG>&3Bp9lp;3oRoU(Uu$R<&|!p?X+nC8mCLqQ8#JC7 zbe10eT)cE>D^^yb(6-_fi)B-N5a0&q2f(;b#FwexpGhQZ*SXM@3%3ke-hY+70rlMe zRHIB(a`2y(8h}S@FwAvC>f>S{1ao~t49OFPAKRSM9-PiMDpu{9s{tqvz@sa&-|uE|zO8bJtp|@w z9_#n;aF^r$$>3Wx;8mju$+04xoG#WnV&)&$1{WL4dc09ls+#qzu}vcweoZyamLA!y6h2F|n(w(qYX;TQ;-Rf;0gY zzh?~xNqbyhNTOJyDiYE_k`3Z~Pz_+;vEepSnbS~G+Rcp_fk`05yap5DaX$3*HUF&d z+qXpa7Vi7o6Ie^mQe~;v0G-l5btyGuAMWd^)4!RLDOHXtJi-1w?m2-+Z~RH6Tj+n} zqo61a(Kj+zv(K8F$4^qA7zX=#_lS_+#3T7haFEKF>$RlGsPOcxtkFJErW!Hji#{%p zm+t=EU_D*-v`%#qjS?f}_qjMI*2t)%QsqNmK^Yey7kECbT(Ld2m5RYio-p`^kc-#%Xc;`e8m21~@#*PZCR+iB zMz5%^xGM3UfD-|0DBRzFPa~Ckaa1aa4{Y$0l9m`iv*ab<&?Y%$V(b`?pRj)(g++Ej zKqPI>l#W0qY8)(ZUg?A=Tm9}H8|}%1!tzRjhes5GtXMi6>dUiCLAq)yuu?cRke0lV zb8pmS8KUxkiO7q`jBq|K)1gf{{yj*|{x~*Q@MnQlm1y4WmvP1y4Dw4{U`qJ2$Ni!4 zc)=n->dpp+i0Pt>{kHCNm{x;+Z>rLq)M^^FL5s~~pO%_i17aI$-gYy1@V>G6dAlw{G$6fIN6M;-jbqkVEg-)Lq zhR@7Mp-?D-D5?1l509{3Gz0@7u_DORL4{2#C}dx&?iK*2^xgk^v%C;dUnBMA7Z=O4=LYf$_-x1LSULIMk(=St+sQiL2)O}xKj#lY0F)N!Ogw$>MvNu-dlH&=X ztR|G2yGkXHpzPso~HVJS#9Eb6Q-dZv%m$4Iq0Dh-$G1*AFn6~P^_jg zjMF7!UT)BCwt|){zJ1k3!zQ$*m<_g)g>{YPZfk9i1=8163RoSkJGuaOR(g4_5|3yP zCSv`eewkM`AQz)ZsMc#PjxYAwqPUhS{0V5%xoV4Dq7c$>?W*-lSVY9ZI1Fatagi=x zaxBm3O0PEHtTCulv-OzTtSj*Gj9Kmvl6WB@aGA^2s*UJL|4S13OBEZZB30^#OB^`{ zJOR)Ie!;}VBp5Q4C_uvO8L^jF<}dI6{VlKC4F~0jNE-`r?HSqm35v}XwURhM)hGmZ z0+HYQ6+?*5U-!B#K(q@##+i-=KG8w~6-?b7+Cjz=cRS|uyp&il)e=JAKI?Ikl|{?{ z^*x@hw&7K<{k37)uUyfB)I}0}r`f946p4@9Ujp;~Mi`|q=(Q^q{pTErmi?N5&jsPB z_6H`egw$kfZmvJ$e2wjRZhktzNXV;g_L}lvkNIp*6qrzGN8dD;ZttIuU)74CNF3Z% ztS|z%c#xe+&ay$J0>aWIUrXU6rt(_~;qY7Oh>0BK!30 zB1^ckWUWdI{WtF*C?;=+2whnz^vmjiuPRVO&RZ_YH&iLSMEu?kS7#wCNS)nz@>HPn zT&vjDW$mF+s^j8MVsywQ=k^}XIu4j!c+nbz9Ny*@+7b0+H-{KVEiR1SP~Uuhb!G2d zXQrh!`QEu!k`yySNr?qBL9vf?+7&)dvfb%XK~Iku?RE~y@A>Y-!P7$eTgo?#Er$V` zN4NaLzBYx4mUsZxeF=lkDX{&%OT=Mpu- zkxB;fH5>Bh*Zg~A2%taZ(rAkunhU|SzA4RCMDe6|nUx#yRxGAOb**4oaE~eE9}>)- z$_O$s*{RZKj9OphLI?zK8Jnf@+;n+h3@SWC&r^Hgj~{*C)C?@v`gzadq>NvpII{(s zqpZB4?0N>?B8h{)2Y!*Q<2bpx&NsL?6iWSI8OKeOatH2HjDS>~&a~@A$Ls3ivTE1F zq`zQ#UsDt!kVLT*)ZSQ62P->KZh?9s8{>I`zIV=-zeKGYY%COJmZsiSJx#s2D?8$7~*~^WOp4cI$(t& zrq%)sSgz8tvs2(5O-t>hE z^-h|6i6A{Z!9Tg*P_;@C<2CvnZ7YTa?!RQ^-x`d~9T8DhcCx_Acgf3~w{$YbN_x?q zy8DBJ0V$cg-@@z~I9j3;_{JZi1VzZ8aU1~^VxPREY`YtRRN=Y&!CzVBZ;Z(zr!r-d z8ghl|dABRsSRy(M1Xu4}+kdw~(F#8@5B@gu^XFgZe)n(yf1WR$(Mt%ODKjC-LlT|< ziT=s+>Gl#QvCN`WUjAk*Q703aNH--VN|Hhgn&dB^7Ob8OwHdqyPTsNmv`5T&?{6NT zotZzeDAP24lI!BWq-bsBeoqG+chbAzp~#tHyFCFPUO^ND&3Z>OsmMuMY~hJ%%Icew zrHIicidAKTiVBStue%t0!4l2uAs)LPrl*bPJ^nVib_7qOsY+5x%Gb^NrGEySq?$ws zPU5meF`PW^Ou62cyC3Xg<$9=veYN4Z03;vp1?sBP(*qhNSW=%(GK<5|G9qF=GnFP^ z&@tVe98*5SyY`;&`PioTehv|tY@9YHs9Y;s%~Go(W?FCuzUsdfuNOwV(~}$FH-Sgu zysO8v&w3@?N8_PDI?Z8&p$Ut;h?-+4gbnw5wS*66LL|urx(?EO2GZ2YUv=k`+?YxX zB5XmAKMRIXFijg=AFR0wq$#q$?znY5@^3gQK8V4&rC2Yl3@dHP!iG=xdrFo>1P-+k zbJhGz8YB6EhlZ~fzJ!{%!zK{I`amm!$64z3auk!v`}N`yma8$B$`kwrHq*mX!}c@Sb2s3N!2$PK2Vw|0!zDg?#vTB?5`+TQV)|H+ z!>dwIP;9vT_WDN?HP7M#T}CoP_GE*GPtYzIQpgm5TA#ya_1f*NdKdXca&fzwfD9t$ z)e50!yFxGouvfC#+?{~V@4NQQ5AGzIs)NG#+5f4v!RPjEUbNVCw;dv$&~a2=7K+~*Ok`K*aUZk& zwyzvUKwQMkbK70irlo87PPYHkj8G4Q(1W7My8k^y1}c>yKKB`+Mv;jltYMUOJY5TvS)vA6~QZ9b2UsKWa0 zbr?m0s=KoNN{5*W5XefKS{6)OeEf8~RyVq~+9MVkb#&BzI0q#~zS5KMi4oB5!FHiJ zljcsZnSUC!wCXK=&o@U?xbis=Vyu1@4E9n!u?PQf5fueQp;|>&(AxrNi0(@K*paen z5}<|Ub2k>P-$gykG@-&WKl_$7tTxuv)bz8OiJF<6g|uiJm6lf?Mn-OUh?Fi4nRY{o zvZ}iVu{j@1vpHF<#~f(Q4H-6h<@LMxNv5*l8~P`{{s_g!#TqW`oq8s0-K84p!gS!6B+c(JCii;5f3 z-AV!!-HltEhon*bM0=s~0vC$nLB6(b_}7>CZ~h4xDkhb8W{bG>KY7?1z%1ZD=5IA? zKnE&@#FQ5L(Zk3{hKJLvsmaF9Zy4d4*KXT?*E}~!O38bAykwsNrA*^8)*jKfMdIvF zV!ob3G;5tt^!Di~F#L>am}ZV`+;9I1X}2IbnB?ziM6b7H>TiV$p6!nQ$_M70lA#Er zme<)*r6(DRa8z$RI?RogOmntH(pQUjG~f3u;e{vu);O@=E@PAc7A? z2I?*JcTfG^HFvV@=!XLXky-T z{tir2Nsik$;_EoOidDe!!9_Q&r7<(emEs^ja3b)nz44`gN|7;R8X4<)ytdNRq{(8C zKIDjzS%f+()ddL(jk`!LIU<||n0Ol|TRFlXIa*7^Tt^JEq4NadsbTG(&Cq8ibQq5- zCi7o{!*|~K?+vycTLzaK0K0NHF&S@6H9K3O;q+?ibjH%3M}=qq+5(%D@L#(BFRPnP zObrlC_}a6M+&$tI_0Ks;MsG)S%huNtN0~?%ndH*gA#8@*rtfhYVAs9z{~6pW#!MCf zAuTDP3|o>UOJ|EqgMF7@PprDY6|Jl`l&{SoPh;TigN_aphIVRWV>3n`*)yBW=`P9m z#6m3St%?^7$vkt*gmOCXdIzeMyZ(dO%36a<5n9?W0wQ+Q#FDQhS@99RMHlQ!YA$o&j8$y2TmMdC+pTvbEiG@*zYj}qW z!5p>3=zLdosaav9(CZ;IWSlq*4f?EkknCQL4i5e_>D0$(ByFt;x1M>GrQYx&q$9%_~Nbs_Pdi6X~3JS=s?p$ayIf5>0jU7!SSoaN6y)HZz zxc-6sz%Ti~OyRLD*6kO7XwM77_{#QU((gZmN_1-7sTbqF!FYvKFD#Ugm=SF3>>pY9 zc-XFt#M~POM{@9RB6ypLIBgDi?d{(@y8%qy$>}Wc=-_^~OkvvDP{9ZQpiW4P1N5QmOT6?=QTE|f%rq3Wd2 zmlzWuiby3b{dy&&!ox@>Xc+STz=4^4lzw*l7b-!a;LuLMPZ5kxUnvzkIZ8?=k&-KW zC%2oM8(?+6jV~G+q>1F_^tz4L1d&mCnr*pw7VK7=3gU~~Jm_D(DweYC7YJz=_ZrsK zU()eT3>4#a;=l=jz`TLWWz6Cs3|TwR_K}O5cfGA@UA{}W63{|GVRpx;7famc40Lj^ zRe0q*XyUa-kCVxj_-(B``|a1)R(PG+vI!K|be9B^n_6pOqqRRVWU(XqQd3)Yckf94 zbEn|rzVd#9h6WB+3^JhyIoyCU+_@L8cAr(3#c;1-+&E&OP+SPh@GY|sVbK2=> z7u%wWRI&wYSB;GBXnDI^^z0$iy;VG~JXMOnZ2kKlswRhu=6->j3)BOZi!&Uyy3RWf zkf4r|P9B#W=W#kndoeLOyXc+zQ5u0fPlkr(dY7td>?p2Ep_`}ue;MG#r}w2L{%bp| zH5#~)f({cm7`;>R>_CD*nzBW`j@LW3T12+D zb3T6uTkxZ`T`(TJ5`lobvqkjN^u98q?hhSxs;LDHeZJQc3e=d+=`dJ#_jM1mZYdLk z51mZd|7&)iJ@PB~xf(WeusV{SPN&!NWVS9DziCRNseU>VeWlO;NgSGnbUyOx?P^La z0`m>gf0xp>0Noa)EK6YDEv5FWN(a?1#`r0s9mfE%$Yz8$fh4hJ1~*7_d_*{FsA4qK zCVCK9;`J=`=UZsfYJnsddIo5*c?gECJh(_9KYLV8m=L2(e%7OKWGQS8^lqqEubkO| zR>t$Fo!xDC{x>rV)IA+f-=#sVLFIvLV{>l1VrjL=XkYvP3umFBk?w%%iro6QY~&$dJ_P4LG+3GvK|!(o`~Y2chm(2s)+j&__k(V} zEIBA{cA%bMV$fldnXdiecw!>$)|m(b6#_dyp>KETP=cYeHnk9yt&sYNy-rfFf0UWN z3JakZ$PZCjOmg}*60`%=tJ9ubT^)--ipRy@!Z9N9lV*R*g9CDepokP9EOC{4r7ynN z|7Rp?;Vg;@kf`ahC}Y*((yc;D6dMnt!3!wg_+EA` zg-e)kATsTBAyk_6y-f2cc3nPLOO0HP#M{<2QX9?tpow0DqHEtL@ zUg|&4@Gxt|a+%_K5)q)ZD+y-$P2+MH5eKUyB*_G7PDfUjrwT{6{UjW@DDqZc z)B$)tI|`pI=p~b{=ykn+HP@FLt+!qr26)DHw9k^h*d`4mrDHAnD1RoLar$T6{kd8P zaBHmn#UB+S-p3R^mRZAGNE#M?==SvI{wzHjtol};4YEL34Bu!5+21-s^=rwr=wDN` z{?d$8ga6e6oT|40e#fL*-(j!Aea@A9jrtct`aGe$T6G=c-_|IX$eLXtji<2~rY1yu*sI(uV zO=8N{3r`_Q#J%@$cN9m5NsUG%y+8Ihxa;m@1#@Q7WYD3)P)~KH2OiCu%SRMwHj;*$ z^jf^->OJ&dQyUsaMwgQ1srnM^-)(8=OLjl+Esg>z3WaE~J`wLRA#V6xeNnYLHg4X+ za_6G{Sh!WCiliy|JeTHR$i@sSGRmuHf5>h>S2^nyK_Jx zu_3C1pC;R`M#^Ob$wesd&8!nY z1E=r8mT$^+*yaPdWWnR#{}TLHf}cyu2W&imvF>0=KN?E;CXZ)t(V2&S-kS0kUg;?J zAmnx?qMQpABFSL2nu#+?w|Y<}UtB%~EIThepH~LOGg%{Bp1XaCc8g)^tlpSCTcKq% ziKIa;y4AY91M@w7c-5X3sUR1O@aEFxuPSGABrBAflw1D_8b!PQZt>vXYeqaw4t zw~SQ3E($2a2>4yQvn|&r&T*K2lrBPV;&EAbs;PU6RApA-aHEvGhB!egmyRt9qm@^) z>awn@BNoHA#G0r3JHiD$-yf(MC{Tp{M$Xh*&DCm0BqJ!7g;-_zQHb?fxpdClJy1YE zw1YMnXZO>1<4wuWvg3U!0fnr8Ws9U)$;i5pLqy`#wMW?B34hDT;5(|+EmiLHd7GOQ z9!OH-nu*u7VqLbqz4ML=tN#i89g>N!ZqN8}B&aHWhvV8jP~yeRR(L#T&lAPh?asBM zvHv!?a62V!lq*ArnYpYSG)T{wMu@i+kd~WVJ9NE+4$k*Fhhg(mQdZy(L*oH zlKsr_A&=@?e8yyzk5l~t8MTu113>`EZ>lWk{Jdh`sQ$i5MZhZ4P@yl*YgSQVVG>Ya zMmivIaW5<_#1RpY?ZB9s!Si~hM_=W!cr5`WXynvs}{T@d|f%H9lwJ550r~c~E z3^(^0X^{rq?jY-u)eMK)j(_m>Twyd8YWF57o+aueX$0y}Gr#7w{7Pw^ua7E(v5q#v zY`9Q`>R+rernVU@wQf^L{btOXgWRdlxm?i7Z~P~PcBnRL2x|^rf&hX~g4Ruh#$~OP znYkc6L)02e!xPF0TeDVVnG0C78OK-zA=v&_1PGL?T-gCdg6MQIDKFqE*zizaKZ#Hz zWb-+^)3b(!hYzpI$%Dt9UoaY+3IPwL`HCZjU~=A9aT@wj!l6V&IZD? zEnfNMwp%PgKbpZU3#bxV(x69E-X_3cCf}@5oYJwrX z^iRq^a!RFJ$g#vJK2VEm>tq(m;+Lbs7}b7%9m^i62eX946ighx#fR%YHz}7YT5N7_ zXjWTTS<$}ku==*XWsl55f_1g0^M)h2*SY(*yCsVo$X_BrtdjKQAEFy9VV*|(8V)}; zT{IL6-s*;XkC@;KVNV00zWxFa)o@TY#|UNoM%@M4-?mWxT|6|2$TVT`r9eMou|pvr zNH@@7z(`KN;{%@zmRP93)lNjasXudp4@P-i>jSY65EBKZ)%~e^Y(HALZPr@6M@lsA zJP1Ssd!KOZR%nh$35RCKwuG ziOcvsx*|?FA3SU_SEWU`HvYG-c9`GG&Do{Dr5Bfc{pC_F+wgoCOrOQ=f(Q`_uzunS6h3D~fO<-d*5X($z6No&s z@>i=+06;=uFoDC5BatnqzRn`2+&XgYit`PbT!01^piJ9L%$>HdSz>2@%HsZi zJY7{(lwI3aK@dS&KuJ+Ty1S*jyOHj0P*Pezx^w96M!G?|mF}(~X8!#ie7|cBJaD;Y z_VesJukbaPh8(n9U+#`-Q(*tI#9r+rw6Gr0Y4M?q(lk@#^(I_Lst1YgRREAiJzN+n zr;t(+QFd~2zL>9A^L>~ziKnohDa)-zM(jo>)?RDE`?gv9D>k;>?%F@Ck)A_iiV|wQ zbW4i3ol}^B?<)r`BlJOssL!1c@CI9eG*Ixi8_FN|3H;ERcH(5x>1j~;Ac}0 z9{vp4f+&iAph^CTD7Wz9*;@?rN&kP4to&!V_^V{NCg-11lgLSg&NIKknGA*AL6$9% z9;z1PQ)%P}sQ)_#L=8^!_@VGkW1OT1`-DyTKQ8~Rib9_3*ZvmtKYNx9n3oWZmx1;v zhu;@EzC<*`yn>=81Cm92;?k@w$7Mh){Y2H?jW1I@w>ozJ-#;48eqG@S0k32T+ zq+m}|FiV0xc`=Sg=$lNhO|NHIjk6&k*u!j=JJu`?0tWJZ)dpQmP7ZJ_0O@mkBqMJj zDA(q^DbfAF(QoniB0P6@d<|3PQ^XTwaUnSgko6S>eBZYLnE%$+RnT4KvKvEf;{Z+} zy~5!3Yw~a^p6qAgWEMF>$?55MI2mULv&)k|F?>3{8HxgGj{Ryw7|c&c2e9Du+)nwz zRiczpP60Sn_~&PMq%n&e)!X5*p)6MG>j*L&ZegJCw#SU{gRMj4Wt->+X$qgmy*^60 zgQ<6>W$D-E-^hw2KZQq!!?BZkcToX~ur*W$%+nM2_^TZE0ZlIQ{C(b`sRuJYuCSP~ z6d>m4+K^`%bQv;%T82i|;Tli?yCTa($@_LVa;GdQ`_KU*D6AKxt_@?HyGM}%XUA6oza6rPgVc+P@0U66h-^G~3#~02~85yYX3T zRe;&=?(8A7&go~5nZOqf8uZ?bXlX7>+JubLG&{rpOa|up&^9*zeB{s&G7;>>&3ql*?;EYt=SYP{5#!_>L1* z&|HUk_mF_`f`daWON2bOUvvOakhyr4>Ma%};SL4Z;}g3QzOcCXjHLK@jDV6;$=Q;G zwo*?m9i0TG*JyB_-cE!cqH#q?+mc|vT(m;KLWnDy+mBX`zUOl4@ z?Wx$>S8De%C8zKHo@MaO_r1AunPVm=?rYQM28wlWUht8u@vA5*{?IGga#%$Bh=In0 z24ZbVMSl?Pb^Cju-~N`iS))>aaV|Qv^9TYT88n>aUiW{8g*Y@g7#$`*I64ZH05VKj z_yKQ;q6REr_loWweFPGuT>-&CajIN8L0Xh#!cTaZT2!nyv$C={hqUKge0dI6oivS< zs;W7-vzV&r)SFTW1DwAsY4&y7NakYnpFa1E07^IEmvtrG!>Q~(qFHLRZifB_u9Gg~4^;}aW(pRwg<(VQq7Qbt z4bE2l`#T8|`>3PL!kx5~WP>s0gfQTVk1k zF9=AbeD>Y>2eY(<4^j9iec!)-(BUAyv*K%#k^$QyUn{I( zB!lSzLPX*83D~37UjOw2EJXka0XsvG!bIN_rX{EUT52~(W_c4JWk>*f!`>yU)&~Oj_E9%&hz%W?oxUOmtf-u7a!coJ7k zf1BlBP}0(>gM&fBIGim~(tF)^Hq&Q_iiP-ecYX<;2C1OeXF6P?5$EP3yVYe3&`EyEmz8%c4RnYZsOA~IKb_)-JD^+;ZN+2Mcr{u zDSV%{o&bZ~pv@z1nr|%iJ8&Pl%>)Yi?vBp2qHuB!$`1D!l>3Lvi@UM3tG*LQXZH@* z+hj@y*Nvc;+PBP@h@7^1D-e700&G9P8~=x}yVf2dq*MpS2pO$)b6MheXD!G5c$4f^P zKPVN?aM-4ygI@^rX#36F`-Lz+F@Jz=1TWv7^2@-iy4WZB`l>;C2CO#lCkRc?gPR-r z1?iCy@IY#%Ak!7Pf{MUExLVWc`l221;rjb1Up!-Sa`7id2-T!(_UAmiuSW)*&4G91 zg-5y_|56M9Yqq8)^_ihR9!@S0lkxK(4(43mo+1@O%#Ez`rY*tNfQTbk0dROwkX{}F z;>g1_jF(=Ga}98Iq6qmV%y*-fL#^*U2uoj!O)11yQ$_3fT`@`SR*dgxx*x1~foc-W zn#hfJsOfOQ4EzGD^y9wst?6V~73f0W_qw zE?orV@LZJhA)uSk>3;iJy-3FS^1MXXsH@8k2*!6q=>cdSV3%deGjeKGo+_)Uq|}5E zPi$j1a)*1r9!MN9^h`*RCSiMf zM>yKx@ixYTIfmliG_dFq(7cELCX z_;CRUc#HuZ^9RqJtGBZy-u1WEQ=q>9>~la|h##6PgA8q71iUZ;BZQ=;-Xz9#04M#K z{4-bOKd#8j%Q@S%hF)ETpX*rH7?8{o7cIcwkcd99x53`~Yr$zb{9~5ZEwyY{OwnTD z?~fgN!h2|aky33*260Sx_D5SR8DhS zk#nu5nCHTi)*7ewx}Ct($Kw+oxLBKBT|J?~@su$HoJLH-f`Dl1dC@xycOzj<=8-1D z%oKFLyeXFejBO(3lFEi8{m?dpHbWER+><9=RtK=ELx&T%!u9{wB(S!(%eZs{t-^Ah zr***Bi+V#5kUceZ4nCluO%`zW0X3*}O`IdEaD$gXmAKgc}9FaGef$(qLExcz}wby$6GE78VDG&dTG%PSZ9DvI&+bb1aR9KWEWorVG}^9YR|lt4B%%Lbz>o?hgEF-&^>46+CVopw{MO%a z{T)e6&!*!Z8c&A2ip_wH$ee28 z;qX0*iP8FkdG+^u?ZZv_@6LB??N1&+sN+uFIaO9XjpIl`UjAWyW=eFNAW$1udOJ=m z?PSHzxJ<}K_II6sBGx(b=*Tws?m8$vA0@6?Id^`)0MR3h*PFe=D?=m){6Z@)?YYEb zn7X0d>3^1Ei$ze_2BG$xyzVtT=gsdf@$DnjolH=@N#~c(f$M`iyLyfhdc4ZA1xR z82}vyS8Lcu<8MMrY%JtE{_%JUQ1cnk^cErFS^oF!IKAsTP(L_|S{1Z`dLz|m`2K>z zayY;5kdvY6PFFCq2HFzf_H6Kx;iL4?%bMrU{O_MXze;^S-FKS=gLHizMg&Xl622N) zfxoB0R`H&_59uyyaRyIUUPTmgn`xr zAJ_9%s6Vj&%S{jc`i*RGXt8^>M+}Zm7b^)!4UMML!tu3SDY_lLBmc5YSARrFX?B`n+(<|i z!@QI#ZC#*U}wt1-oy0BMD|}OqnsDB(?#_F@R@3NEW{LklY(g1p4HU? zpb_>_{Q7XFxn3TyKV&;>FT0a1_+~GaKTvV{L;u(=5RxGZBN2a6LiByy+Xjej|HpzN zW=!zj-pY2Y@!GPmFqkk6_2qd}_;p~~CeHW_p8_SH-l7Bo|(N%{WPL1E}${@Y!Cg}&Yh-+ej&u5PWzb$snDA>&gjF>OF-=yKg%HGB} zfx~Hcd+Jnc=a);os@SPFy8Nv*t?_q+Fe%+?mezi;qj%+;m@j`994Xz+8JgFP*TGeF z{;x8?l>!j?8)z-&k^HLwK>2^q)g%Gh1wf5|hASsEG?Gj>mc)&sIS@Q^tk%1c@b;|w4Jy;S+WitFF(6RwVdbAt{mxgJ2a z=2({AoHP$kBUaH%_B{FR&xD4G_#5jB#N6Efp&5*rdEq+yhdMSnzctZJ>Uazl6R->9 zua)v18f|6=r-n#`Cx89A0y1Kme16-9^SLM=&_04&WCff!Gcw-D_kdE>D)WJmqm18J zwG-HU0|g4|1P5hf>8`+CDh1u{_X&WZGQ7=OR;HPnC8MJQB#HQeJgDOAa=bhQ1u@uu zT{NBF_ZV8Mc(i#33LBuK!ECl7^vLi>m?pMdyZYnj>OCA`%q^7>?>}2u$B(Jjp)5yw zTL8#}02Y}OMXJA-8%gXYtI*v#f@0k3hr4s;XOVumJDZiyrFtJ7*_^BhM$@Axkp+H6 zQpDx=JxJXV0tk#9&C-fD4fPO6=*<)->qqaYfHxN#4J7CTfCr}zK+om+dEw>sjDnSa z0Z38?-%il)N|`Q^I$=0tIflkD>LsxCxgOTzsm@}14`fbw0Fb(PJ-Z=3zFm_}PFAwR zk^k>pltilaDY*G}hZEo(S#YO3vt)9EM3hc9odap#lOn7}ky*~rLyglcZdZE3_{R&e z$u#i4kEgPBTeztOU-VYXq$<7#AhaD##|0Z}j*-4urExm*jsIZ*RQ`^a4=5=a)NQB8 z_W$mKqY`Ox6v~jn+SH!q9YWgYw0QG?(5C1Q|mksnO-i} z2|P1_5RgWmc=Z)6-^UM3@2&9>g|B^lL6Vuz75n0itb-`rL0Q1>Mc{JL`_?8Jqgl4Y z?fN9-jrHM7VrpEqN4L0*4|Rt8;bisf!g(OTiOnRt_8MEBmAV5#ON=+rZ1$9}zBoYN zNav~_%aVtJv*q-&>@BqLYu1Fvc%`s3U2W)s8W|E(SiP|4s8S1Af=UzErmBxnWY|`m=AHoGfXD-u67nFNf(`kLZV0RSRE*XTKtU^`ho$ z6Y>u(B}cfJ80OazlA4^znpPcYjX!3qaA+Jy`=_L-m$+-|I37zf9j!8{uBYwm4>bvy zHP?S9&4u?JO>Tn5nDcG*lmwJ;c4;eD47i_B`|mdTRIyoLw)<=!X;rqRs97_k8wj2P zE)=_+vEEz}hl`YS`uRaG1*+hT1yKfDs}FQ(bYDi`Ztu;2SlE_bO`nWG3deO+PpJHi zTV!CNWni~radiG5iF<+Z`5%qAmImkZ8z9`2()hRF<>i_y(fiWqy}ch8g8G`l1X+zS zyFplNe&-P7_N0p8e!Zokl+l|=E#le2y@GF7YhO>eI{5Q4p zu~ecdu$lv(dyKuC4b)48OJ#&nwUpl{u(eJW{|S&gdHR$j0iAi_3Uz; zR7UZVkU}L3taM=aBZV1lwZm+3f*eh^E1oLQX#!xxJlkSsTL*vFmSr8pUyFUouGniN|RhZeOSIm%g47ud#NuBcppav`xD8em?1{F2f_T8 zM>q(i`^=^nK7OIsGdlao7$m2wkle^%d5F$c|HF=A+2`LZ_T4gHLaw(5%D0>>8&8gT z-7RYm^6Uk)p30l=N3X3L*SvQxIW^i2yuPF`NafR&LVY*Sx^a*KyE()g{Nc4NR}p~@Y_Y|rmN@FDdrn@`Gux7}~L_VpcD`3bGd)pO>_BDt6-hsNbb9di(DGB-@V z#1-Onv{azx#AL5Zmo;+EDf62P@Z$X}az073`X>D|>-s+vQ}5V17o+3C&Kh8BQG+ol zf55_51RD6uIbLVlw>EBiia+7Qjl00DqJcx*4y>54VlW5k-@D_n&bqDl!|N5UpLnT9P-A;7%d zZe86mUqWdHcgLmA$?JZ~Uo#`#mb>>s1M< z>2N(>jJ1|S)-Z6Ht8XHy-$T_^eXtb?P9nV@J4=XlZQis^$3%&ud_bgD$UccguAW6Uwf{WFKeAN6MbR9N+xu*bq%)5-a`vS17a{($N3md zw6NXka8(9ck#h0ayLY%^FJG#q$MU;wq}$%c<4}zkkF*-IqD?2<^1CeydMglB7X9pE z8oi8{i1IfJG87g6gdLT2b|=Fav+IAa*O_>K86z)!=jrWT0X-Vqr4%H_!O<$-LmF`G zgw=eeFDIW0$u&Hi?2uVY-=|Pz)1$)^j4YGB?XdQ-m90R47Y4?JvX$XR%yv+NBLC5>3iS>0Qpca4x6|V8hQ%fGS3SN zc`4_=tu^3z2e3HrS@KE!hvw+vJ7;mnw*2>+_9mMX-jklmMtfls{QKFQ(jaQ!D4=gJ zHpe;Tb^U8&O*939gU5fP+Wi9eU_z}D0;Z_Ewu61{XI+jk zHt*OU85~dVkdLXv$lW&bAuYE@3(Yb*(*?eviSC}{Ev>D_{i9)iZsFFjc29h`Zh$IH zO6OU@PfUsHYZK#-=Hp)Dj!Prm2m@p78gijNAY7~!JL@1DaIUPbCbn@;JDL<^iHeZZmOA2M*dn?&)`LLA-!OrxkO;=Dz7cv$avJS0hB zalmNiA|l)N-a3?Q5#qQv}*7u({mjTCJbJ^d~MdF=lWwh??YO7m9aEjs9*IlD0zPm?dkEI z+4~@$Xa6DRdr!!%yF=hKByLQlG13_pm>z;Jms0hZ|Lv%3W$W^-C9gV;E?Adu8MfrA zF$c4kcD|)kvrARJJ<5;WiD)N^Pu=(jrhaQfqzuK@yfEGb{mNa1t0rDhW%c!Q-|t8% zsWzVARc`$LO@0}utEuU7HE0PtCM8^vI4O%x$jWk0V!)e-4@8c(u6NU+T$Vmq8YlY2 zeDjst&Na|PLLn_rFY5ik&2dsMBS#m5vb}J_Mmx?9)9ooS{V(*y4bm z4W^%96Dnx|oe-Cz?Kf#Ms{Zf_(F&y-G7tUsk`JF$OGU_bUqe3- z&YK8|JD$5#B$AaWZVE?|GNypD(Fq|c+S0>^%fDF(Q$l7e9%frc#^hiZktl+X7w0B{ z-Wtu{o0a!rgV~wu@T;j&t#}$j+Tv~&eCYz?zU9ib|A!c$h-n_8{yI*w7vRIaPkW$Q{`EDW9B)necVU=qxzy7G+NnEGzO2I? zx&?XXI|cl$K*MxBLI3dZK&Qj~P-?%npFT6!t)+rY9e#0p`;OIa;nd$(U?c21xzgCz ziwCG@rFN~$zqtMDgWy)bpP%s}!1FUz-?=+5GMsh(z`;;r$tn@5Y-YPm5yU-`?(WB4CI{d@)#z@5pS1!{C@ zNsH{(S|UzTQU!l_c%UpO9?n1YE-Xk=MYTT@2|(cch1d7_J)4tqr0yr}zjVsB%T+NCGSbUVgXX;83w~@DWW!B$}%>bV0CdU>TD8!cobny17eK zUS8izI}u7#torAVUq7C~^bX$Q>-oxcWsO1-Q4i5qaN5KF3`Mj)5e*Rkef!s9J;xMe zibpLP_0r+?s2x{IQlDm%TD-gDgMr#cl0^Z?31=ir(gTF z{aCXP^dNDp)%}F)XPi@e)1c>raZx`ije2=#G;b9Df{Qo zF3zORmakKxI}`NY=)`;iE*^p!^0lqaLjzHy0ydhRzyj;B-Sc)|WtWxJNw-pv2D=(7 zHpdnR2aL;B3T8EJi`^osGgM-kW1yH;U~RS|E7_ zz9`#|lSPO#uz9xH{Q6!_71{oUbo|l>&RRTaMxq#_`2n! zrK{WSY|efp}|?> z<;!LINxxnB=bgd{qU2#wQBIEaZwUJ(KN)v(gRvqdKYv7Lum0<>@-8y?HzC1p778Q; z_sy2Rv@dpcYgDBjuBP>#ZA(0b(K$O=7A`$&c*T0`0+fFZvu`ZMyKFt1t^LHEm)rG#$s$EHV4cUG63AF> z{cN&K(As!=4>);#@M9O~?8OX?twK?+7 z5?ug`h;7blX^W|#+#+C`NGRxhv$`310G= z7uRNIcvkr@#y^=akNwFij47I1B)nsBNk3aBT2s!*&IfJstH4}ZqUFr_u;Wk?Yx zRP+CW`x2NdgAXVaV-r(7KR}05Sn=&vWhchR)rzI1Wn{jy`tc1Gbabrc7zsxa4zOF` z;$UNwX;MwN0?douYGH98q2#9o@775BrN1Mt_Pph+5=y^RwZqCd+zU2hh53=ujxtkH z;+>%PLpZx}6A+-jvb(kQb_+jFLApG#(6U;SEY;J9S$Od)*mnxuJU&Y}y_S0^cO^y|9gvI^xfk&w)8Li2gi3#!X5cMPrX?C_RGM|h&IX#8G*=u?@fS!c8I`+ed@AY~< zQzIjr)gOFGJnJ;v-0aN6Z=wh-fgd}?0k*RFn#%s}E-~)b>E_|~@v-Oah|nC0S9|vX zZ)3A?9xaWli9qyb8TM-{t5z`nAgX*88dI5M7%L|H_zQUxdh^1&@s~73MP);OnBHJe zu4VZ4_8hk_r;};MC&B>t%BU#Abir2~o;npB%q6N;<>R?@zov`DZ;8Z|&@YxzQN!=D zQ0?rTAkHAQpdfCrUmyFrdQKl{0^7&7AColD34Rk*KXk z-jr^t<>zRigErFkw04NP+u7(kLgG9Oy1+t)@>Vwz%rH$s|Ez_DB0K*7Ocx4yxJ?r_ zj5%^=$C{)wPRdaj+AD@MUjM^)GRo;=ZD`W02Q9qXLR1{KoT+z~>&c&+Qbs@KV6oSz z5##C27U&N7x*drLYIrQD=qOBM<%g3wv-kF#$$0vmiK(%frhaeFaClNVZ2wM_#>D#% z8T(i5d9+hbSwl%wGZ1~ zPsV-}LoVt3X8Y>ui^JZyrO-9bw>6JbPYeky1I?00e4NcnRnG~{;WxeGyI%x;p1@{U znsxDT2;bolF8Uh+Y0T`|gDWq7&yk&Plm?d;)tah-Dm7Y(MvLz&;?%q`-pfr33&X$% zywa4myKLG~;=sle4Jn$HKyu@`T=Xcg-_|5I)@ry%-mO2sJ7Y#u*~U!RJZ*AK{hjft zfxubke*YS&T)lLjFw)K!v0F~J@_!Jfuu{dZE2evT--bza)H@5(N$ zgEl2kdzKa|)HH0Gbr0@w{*;V`7tWk~mI$esYvbE`ogqIEqsEO+yDn()5X)GKt${Ye z1)?@pyc7Yi0n?RNyBgo>wFXWP(@h5f0oOQX-3O@X;7@nS@s>brF zlV9lUZm=PQ-n%Sw7M-N@K(wcPjV(W62(h?8-=jVU9f?cwv5NiI(Imc^=0 z_}Y_s-Vy0zre~G9jm&7at{(9zN?|;3EiJXO6bY4IX{MM}g$>qk9|l`9?3tRp=4Pzw z>sNpLt7u&|k13U+Nzf)HXcB1OhJ9ssR8}i!SLM7@Q1Z%uyRl!#6r9Uc74pq_I1f-Ab! z_hB3iCP5oe9HY)yL-Qh_r2pXfSD|IFh`bt1HnX!+!Iaj)(}M;FM<*G92%Nd>oiXDsMl&R|!lN$qF3>W*^v1iD~2yt`Suz~SVTGNb7!@@-j8~&fKT-yB{ETZFD zQ63){e*IHEf2GtftDOFHV*?F?F~x~S#1gGHR@H|Jlq`hDhTIXM+dXIltj&~a2ZPaFH{RQ#uhpR;jeVYH+w z8i9hM;AZK=G+R$4zZ6Xzn4$ijVP>dEnW#}mr84a@{HVXoi*|^1nj<2TM@6nJn;^d^IrR{a_QXPHuH48L-(M-*L-aglOUExUN zJ_1ADek!yBZ~YZJiZn(*H&=|LgFG*B`#1U_4-~_5kA4i9Zb_Yf~UrD z^pbzrAB&y*IT?5?MI{G14VCf&Sx%exMSAoA~P&36F)6cc;zm6DeegUFPw%@sj}#qVa1W}{^fGQP6990uEh$N& zf%zF^_8^*=dp2A63VNAzx|Og^BqkaX5~7Ysx8!O8okBK9%S2k#YSvJ;>z%E1-|KR` zd@KiM#zS=8!@XL5Xh08ASE~76woHB+5oAT5)kZ_M)_LG|DWZs*H#^EmIV|V z4A!?!UO>Bwztloaua40YBC>eH&t9!%*FgaZ*>E+ac51ul2~+uQ#ZMrcMJvMZa(w-R zT~Cae>{w@4eh@sTmda0c7QYo7nw%kqhl&1rpJN{;68*;v*h;gtL_Wffl7Ii+02a6{ zHLGCSolHBr_&VlQ^|1VUont8NL#^+!fL}$2WN(9%&B<_AOWujEQPlxa+v)m8byG&$ zeV2P{4f{gf4!e#8-fx``;2M;Rc%>7+?&9LYVzDH54wmzU>dGt7G*C0*uc4}HGIY$KKsiew!Dm-q_ySsk*GD(vB2`nN zy-;No84AE>i5te(6&c97i;bO}r!*V0y8z~1R`AUOlvto2d>^$F^F~Jxn$72Qw^fhU zi8=m1EI_FnVe5+hfz7u-+0{dYUm|xuR)KyPf~rBhvCg{Q8talv9Tty~cA>-g+;o(_%v=;YD zoDM7n??-d3hI}U5Jm^g$vY?y}PdQs$C&g6yN3mBy(2J-0^_TXav8%(5=|uQA?OS-H zAklx@HCFn{!$oc8?T(?sL8Kk3C@}ziq zLNBfv>9b}*P6`c0)_}pdcTXzz7KU<*_rN~rWT9c=ylA=cv`n*M>H1#fR#y+*LGks^ zYuXX%zYwYgGX_lvjbgra*#Yw3pjO#g{&YOm^#EAF^=h3jCiRH*nBeqllY4PJKY{=5 z5ys$ljnT*Z%Tn&0UHlFf-=V0(;+v3_wiE&Ln_(MXm0P_}3DV+x%s>CVeW0eJ^Rk`0 zkhy`h143IThqWg-%T4wep93Yp3<0m>G}_0snE-7SZ@r2~B=2{2Joo)3?&KtgD|&$E}$tRt)fgek4s1KtE_J?#=^J#bT& z)CF)PDC3pe!U%x6Jo~IX*k*93OK1tgo6gfJ{n_h#>I$Y0{O({er}OX=t};x_y=8wx zcUassgpp3`$|y5~h<3b-Sir?;D&Urw^u=fX6;k4r0Uw_c$11aH_Z=4GhAXe9gP8fJ zyPhna#v^7uaJ;10R!Y}h*XAF@lmKSn{#y0Yba}4*rX@;ycDuUlUa7Gftw*6|`d1=2 zF&-kN`1>oFjim2QaZV=CKWgau==gD!blP{{p(s_ZBbjY&K8*G;CARG{^P~bsM=EEs)ce?P-`xM2~ z6AF+`O!MZhWN`U^J94dl_x~m?cNsJqV(iCCHj(Q#+`592NZZrU|Jg=w9TT5f#{}GC z{1IrV*sN2$KyK#U@=L*6^vCRy-;?-*k}PDdBl7A)P&y$~m3LYL$|D7SoJv0*zwc$C zo|qV(9*HW=^*AwdKOg(?R+w|)$Dao_jS>^ldM%n;Xb{~F0lTY6LHL5YA@oVPoB`@S z@vl0sFin1)d#wWOJAdI1Ty)xzz)we=adwFW{pL!jD)jBSJU3Rhq?{Y=7JCf-13CCT zwW{w{a8np-!5VFymM&v#(L2-x>k2#AqTxc@J3De|_x;hw59Ku^pe}UTUy8Jh7|-go z(U(-30Z@|C$-3p`f%x@!HOMsl;*zq|FG_9m-#-|rd9bC#Y$7(BOl36MnNQnkKXcv< z5D*QlHf6SaJ(rNd^8F8Do1tlexCz_;{*7lrZq7puQVj0E zthZLr_A*F(2Zb1@(-ZnF8{emxzVj>k#n@pPX!H^3%;04je2M$GFBs%^P5x=N_s47g zF_DD)n{}ql+29}4S?WYwzSF6TB>j5y$|Ny0mD%$55osrUnkJz5^DGE$#v6B9%do}6 zs{-Fc_5LPi)XqKDzbz2ltYtn zGk?A*>4`xIvwK7@D0DXp$KS;4sCSrBqD;oMEf0hkV~V_54P9B;s+`E;aq7kOJ2@Xq zvt;b7V`5+^ky{S>F^KuPh>%kmuwGKq(~WtS%M_bH17)$k_EBxELY6Y)dg&5SmIg=5 zBn>$-NF$=dWMyT4{6I6++}Vkm0qsb^t^E%62jocvA8vP6 zcPOY7(!I?_X~8kd9LzB=b;WEpft}uJD?0G*Dyf&s{kLzY&0@Avb#{haH`p@ro;^B)$95$ym^2l`#%@j0`#czGi=F0v zI>pSz^{ucNAc7=fGMAc7{3}e82Hc3X1&lon|K^^ZVJa|U_(}Ae$_T90IZAklR((G0 z@A<;L52b=SzMj9{TeRG(>R9(}?QRpTVW3AnH;@ z#yviarp8F9dFJ96_IqDaP3gG$Vla62qoQN$1;wwiH(c48KW~tSd4^tp2*NC>S!gQa zzAPL~sSn8VE}K3W&uDi!DeLNvvBVCQhg8ac3;Y#aiqCNxl~mZ?q^P1j^+E+QZ6GPn z>?>8jdIC10U^+;I%$2V^WbF(A#ltvUL|0L<4x9t9GLQ9SCVGV@aCUjC)pqr^H2;1{ zNJ+I5w(9rb8Ub}st(>RIQuwDKxIf=Xh{Q!5{vI$aQdIE$9@f`)a3VpA3UF~twTC~e zvzQB}QSxuaXHj*U_O@x*AZ!_@#CMXn6TqIiO|+bW+vTju(@oq#va88lT$R47o9QZ2 zgDQjDIhJ&dMJ^5QeOUo2b{}r0NX*c(e$RhD2!3YcjfBdTW3QcVz8innXZ$E)biib;zcCV zr7bo|`R1s#(2z~u1{dcgbv7@x<&$-uMNIhV!jF-{R7IZuO0=3ez=q9%!pLaY!reVM zLv4fe(!W7h}lupP#9uyg27xbXBL8Yn0*t$?50fYj(2x+IdHJt2uzkNOPJl49gzDfxBypkWQMr`+dt)T2a<%lC1aVgGi~d%!V}(ME zvr-w=(e~^p{L=jS@@w{Vd;eTkd5%~Z0cHRGQZS8&-buh;0-Kc};iQb1O z(R+)i(R+`G(Mu#5B1jP>gy_8!-G~IydzTPGh!Fj5=iHq0zV{Ef^Mmo(Yxe5v`;_$* zdBDbzcaGX)Mx6EHH`+q(m(#mJs#n$Q$(q1YN!fp?7JX?20>W4@&SxWwR0p{4g#FY} zad3VnakAabGGd&O2%Wr*r@^TPnn?N_f{)Rc%?i1{mi^7^sBMIccQ08jbOL+oF%H;ZgkAnr1M<;T7HM_+ zyfP`S4U?fxQ0Zc2{k>eII^tT;iBE7m%Y8>hO)WZ_=si22L-3$B$Z;uBD}VgBex$+x4)n#vq5ntN1(1451 z^l#%NK`=ES3gtD~;F$yBtdg+N)T0MM@haQECHR%a&u(~-&WrKo=cU&MfZQhimvO%i)I5-c4w3vVPOnNoKi~QOLX6?scSU~9Ch^WHlEM0F?$NmUTz zbE}y*PJQJQt@8~UuO1fXyW9h;GFb1oRQNoV3^n#OPxF{)`OM>%4rLk}?HM9uwp2!?5lR&!%FtR6MjmX_s5!mXdm?g_qz6tg!0S5 z({f$FD(?j^_y|#Hc5re=3vBw$)m7u!*{UP8y~D%%Tl0;hcb>z(2~YG2YjxeM+>a?Veaz(sb94@X_^J^o(1uApa!A>#?q0=T!F845<)U&}=?JUXuy556867Nm%cR3|s{wR-nUQ8J>TXbR=XL~ zdU|mNyf5K36SDHY+Qp%nFIjzUb#-%hcedKJ00_K_Phc!f_|kTxFDp3Dba#;O@Uq_T z^kN{r%<=Bs;_>{qZ`8uWvo6bm`!z;pX>MfIiK= zi3*i)o59>T40klI155+Y7?_~!LV)3&WsQ4P_+n*g_-k!<&b9~XdEMQC0;wO0hO~UnWJ|VX&>Dd30@nyZeEc9Q;m%97PcOy;WuCa) z_zWzJbyMa`C%#&}K#yYzFMof@aD^=i@7FP$@alvFf!y3qt@qNu->B!O5L2qjixz$@ zyG;mWZVLWBQ#v=-0uSKr67_vPrqC`SE?#AtB?v4{?Y;VUZ-H%;DgFGT548&w029-p zj|d5l>bGM@r2sn4IHcG$E>Jw2O{?}Od5cbyxx_?`}w6s?yZuQk`Fm5W@?l!l%fyQN9Xc?|ei`BXMFm;uTet-i0G zo}$17@FOPDH_iRmH6moCYAu$qSq7SqE9ZAs?q@N)o!laUBx;W36GSQTJ;P#0tu4-- zxchtncDJX;HB(23=Anb=K3`dKSH_5ew!W>;yPNjIjAm)i)KzvgW4`-WFgOWgLi0@0 zH)^C4-*($o8u9t~JzXyTEht7usv6V77m%P+@P^kyWkF~Cu!`>+yD#BGz}?@XfI}{+ zn?3oZvJQj#qQp=QAS85l+C937GORJ;P9AW)ks|7+D~qIOS`)U$t?E;%WguOz9cAY6 z2J)i|jiB$6YzQSMFlud_`~UC)%~RX2+_X?#yIB!fbPEZqWh=GaeIpYOkUkthq%0b? zt3JVg&g>dP%Y@*c-I=HIHN9J4NR_XLb#v*(X?lrFY%3*oJ2(R;=FZJi z*XsKivQsQReZ%|WZK|effandqN7*hGo6BV-pS2#HLK>rvj>NS2o*BLG?hjkRfp}xS zK9<1Vq~nh@Dc@_pfE3E(>8qtIDFQCS*HMDc!`{e1UcKAUseJ=?=Tv6}HwaZ5T>&%S zXV-wUw6tpj7LKk&E0T{FMLuU3vEILYWsCweKA0_*5)dJ=c^_u&VagqK!PrrA#=1$9 z>zMJVyAm5iQ4J&&dy2;S{AOgrYf(FN&WE_^UUQa3xr`-CAE_Rcn}r&hITXE zmBEUb0co)LaCG<6dvRwvF|Uq?I=Uo_Ezch;cT!)ZaPN(~swQ0ZDRe!8#2d3BE&zh$ zIB-l21T1~v36ad~vctxv+=;lLRq;?7gA_Jr#JXZR&fRMJu;gzwt~-YIM2x#|6;K-g zzJ-*)$Ppj3x@aZ=)s3_03avL*(*`tE(L{Y6y8Ugu*f6~;0UAq38hhu#2|tg_WJ~wt zkc@nr()ZnKS9&g2>)Qga*I@X$gKRr|W5VOXIZ3pklQV~AHyo5Uv$F4zYf3~QTGJTc zGh*?cr(`Mpl6S5H2Nk4*ZvAElZ&J2P?gszWu+%R`mAF{r8opuIfbvh)J_lId(E6{E zo&#Uz?}Y7%y8#PVKuj+)y}97-_ACW}V0`p?Us*f9OeJJa5JWVNg0*llyR{@LS)8XLs(SO>pBM zbwEY*>{bPCbQKPy7H0uU_J!six5&V?^_zw0{|m$La@sV7P-I;bi}+{I4iIu2qR0DZBQI<@8>ub z`s*^U#s;(9!{IG*3JbPv5jYLXz6gWuC%zRHKR>zu1mgF&oHlFnDoRt)m88c4pppXv z+%%?X`Dp}$y-w@G0573u2!QJxpB@6DKE>H)%q%JaqbC|qBb0Af`j&)lkkQ-8o_7D3 zQ^878G595$?=6}r=x#n<@64gcy3p5h6YpkYtJ08fj~ri`eIU$x0%nF>tu%^57`uD^ zWMQg$YpEcsM8WSc_pK2}uvj8)g33r4Y9RgXBd;lBY?aQ_tBs+@{8JcDGO1cY#iRdY z@`!@^G>KS&hn+{bw`@gKae$9azKPpw=Pnhr15tv{)om*|eRBNwg=+hw*UV3c?kv3D zodWEj%Jb%sf_xL32iLU$Rc7HmTi_lfXlF#6TYTU8f6rzw3ce)F{6hlw9Z#w!2=%RX z;rYx>oLSuNFbxlVpxzC{M7FI~RIJF`tY7y~E*iZ2c7R#jTerX3d>zNrZmU8Vcs<0h$NWlW~zevYl3;(;T`<4DchM?4{~Z#QR#Ny)TMhRcVFurHg`*# z3E=y~;zL2q9^vJGw+KKB;WUbA31rBp7HVUsk%S7TQ8cZ~SXHA$j;KB6Cl^Wm6;i8} zT|p$DdTA6$Ie0JOhwHnl@4jia(2j9~7Hxx_c5d8J+^vKwAF;}j3alAUPp1heB)t?#c z1jSJ^Y?xtGqLvvp9%C*W4Ibvwb(UHVIHeQ8-Pzs7m&X@efn4Uk(p2rej68Y4rh9^a zdag{)PU=0C8&O8Mf)sW@FEecjf#&KS>k|1}_@rqpYx>b_)sX+(WzUX^a8X_nN^K?y z!uiwgy#^a}7mK|cT$(5&usuITIBpr@jUN=mN|y~rKSns}@hY6z|M{`<;gU$TSSnMY z8Qc*Kx7X=2Uga{BEa)1PrLM?)y0VdGm(8KB4U>i=byM0R{wx$CPPl!U9in)bR9b~T zTmT2f(7!G007@r$#h^%%Eg>Eg&;Z@aqF+_TtAQiKvm?<^5pK8P+IvoaD##%uC6Fw- zsrk)cQ4XHTmcXbA!f&Srn-b{}SrMaOP$BU%D;Xay&<)un*^-Fp8Nm5-96*}cb7%w5 zx>DK|YF@1`UZj|`Q-bc1>_$~yjIJRj&25v$tR`%lhnI!~0a@E^=x5-060 zY+GRUJGdt@j<@J^iC%y&3?tyPv!rYHq3`_Ele(e z6u-5jmsB16CUBtV9t}TbsCICoAbDgRZb&SHOhUs3NsbLA)fxB%7cRNSgy-q;id3X9 z+naICOOOOV1d3OGs3=8R>z%LHz_@?O2Ru3;JQ7sYU?NlcdsVLZcJF)YcSz~^)j^$F z>0}bG32)+9>k>Wra6xC_lw5iDM8<)*2Yq>c8Ih@V(*y*9EorDI8HFgM1o$>3a>d%I z16hJOgDa(YyOcX<8pL0yAovbVNX&sy+hohEV6Gg=$NbH?YqE_ld-`xH5!-XNVENy2 zP7>;G8VM$O6qg4Rn>!j%OIk!30xc42KK-0Wz|E6`f|j$y4`#xg-Kj@D8zjqLsC>VY zajm2BmTus7%5%QFqL#3srhu)Rj~D{Vk8rOxSb$-Fh~y#xsVc6ihF$iuh^R<+q9E;{ zN|gK^8=_SlZ3FbLO6xKq^iP5Q>y(x?xC|`ZytF2yd<|oiiH9U)$;#V{*&7Bdjl*9% z#tU+OtE>8VQROw>pxl>$B=NWjMQ%OCzG8aFrj5uGh@2~rj`L<{I2K(yH=&pp#&@Zb zkNj949S2gJvBKUqrI=?!X<}1q8Uy40-PKi)I$|{m^~5f1J!VQX>jp!~CM9LO<24J* zB6|mqNK#b?R`+MtxWDLSP?~I)Ue>hFW);xi6mn+YskME#&=ep(L&9DU zhy8xy9?y#6Wd%RqjTA@O#D9_Q51?R5ITvZ9`Rlk{{kfr>?s>$ggG#x97fZxEAk$`& zerv+yUDzKQUP9Xvyxh1CE_lVP; z(i~C3icG;v8?eOG65Gl4%Qv(I7*0cU7N-#+W4;M zIIJbQq_N#sl?T%eAy4j3ar*;w2qQ6Pq710*Zi+c4=SQVv$rw(8+;DtH*5;@dHKjG$ zwcxC1sU%dltwTcn`Jm?qd56rDjC};@wB1y*!`2}~V^aWn>JgiVLsAl5obYQ{3rIQb zMJN9k@k*vVp~wTSv;cXew;RdlORzp>xJe3of#>%<7iSvte{- zAud{7do8?>y`ji1SoIbfm?e92^I^_!;gA+Oa&I?O;Kx#C;*P5W6c1PzUu~6pl6$yJ z+$Z;k-{KZ<#a+R~-ouG!CeIcZ)dNC1X4Rn#a7 zURLGUDWQf$V6<}G!tGZCGbiq;HZ`obEB`uN-a_eZzL95q$;dAyUgGDkqJZMRH>N`u zcNF;Ve}QfkmigEcK>n0P51*6$-L1c#6#aB}{g;MqAWR8oyY0~bzPv6euy!AJ^Oj;d z_*I0zCbgRKl+2O{2wf$nLLRb(2!J||_772kbJ>W@ajiMNOIWP4g>%f^r@ua#gmShC zLpVc!>-dg2*9Nm%h02dGiXu29YjOmf8UZyQ4}er5_jZFN;a64D>7qIY1%YcZztXR+}wA~QzSYjUbn-kQQpQO4T+$g~NHmnS!HQreM*(~H>3BD@LkK2|YKiWIVi;W(5Mp!AO~ z_}ed~h%7)gR>4GnIvUMad>wzVzKWQguAc!Fg~IhSKEU_pj&%^8vOCZ2ceF{J7$EDx z%+ZWF;l~QU9{y1%;@9b_PHeS99wUPnI}C|GC7go~biqz=8^me$>y1<-)Um4sR^d=c z&=vZdc71y1eX915?U5j%Cj*KlQc^&}yx-aq0Q#40V3(h4PNhF!Agmdb-pf=)0{iwM zHYI8Ozrp{4;<0Cj9c$4gUK;HV{IMqnrX=1tk@iiYN?Zvbv8+~$O}r|Y_s=I)*9Uj? zYSeG{$86b(ficN(fAv|d(P*S?5mA&tRBX%3n1KV25k?=N=J=CM~R$3q>IB zBB^=V099M)zbJj}J6W?Sy`~bsKk1H5e5+gSa|e%S47orU46zJn&mXu<_%BO%rZT8m z2|Uu;_ag-+svSRS3M%$<hVNN}p zoK-h-*1zF@r|{{+%JjsLM9tUpv-F7?=6aUUSZ}UQePT7#L1zaYA?p9&J|=}GvfGx; zi??E~wRrw+p3V4nIpJOaTYcLcWm?aM(d~4 z64%_!@1NlE!9_Vs;kD}QOS+4L7jz>n*#X=?rW+PrZZ6o?fMm_=Z+J*^Qd}`mIq&g60-T}5_%NT z-R>g}g&!KMUO)M&tpnX4fP z5doV$tfrQE6E`=c@siqukJxV*BAU#72F8fjy%!?Do)VAHqBg1FU$W=bk(~bc4FTT% zStJ*+W0N_Y^pZF9L^0xMjB57w>UPIJM|l_(LAxaU`7XmMQ9kDiyW56rpmHZqy;2wr zDW5@|(M)_^yd3GR}? zb!Qr+9rcnCP1k(w%?)i@UXd?rYK+1?q^G&eTPnjn=>J2i!D^A$PohrX$X0?^y>x2M zq&fVGpL;G~(EZD3U^))qbYqGGif}>%Nrycf$T3LovuDGN>Jol_E&>fIyjFq*4jj5> z)ELilFO^;vD2zhyn#(1FB=dcqLT?M-=1#fxcZwsh)YeyOR+4h5ui`pAIE^?G z5Gb=dJg9b1KCfN{wZ9bGV?0M)7`{}~&|Acdmk#mb&*pF(gcWMYpg^lJAj{DH}B^?oNo?D0e2jwqD&>@_8n zqENRzUq1U&4O6G`FcZ-WLdBoBOikAsMCkV)a2@rjzyX0EqN;v=BkC`RAEa&H{E!xp z%eD>v!Qd)!VuOr4h{61#ApVw7u>fl!I^&*Z?tG^FWfoIOdJqH1Ok8+v5S-D76e99} zg%?K`&-UXqPUa3ESi+csVkeCWX^%H|+DH0IE=VMIIy$hL-T)U+HhybphfUBjuz-CU ze);Nvd7}UpI|7jeUkQwCQ4P`R&?BAfRbCLH{!9i+aq{`)f}9Z&UZ4tUkRq#^G*OWN zsUz2CB(aMQPVvx@MRZa)LY((-U$k9BVEI+0BRg-avY*kjgFoC2Oj^h6 zgW`;zLFL2by_F+KJ!0n(1kZYs;9{BQrQR0sClqu5HShDRsfk?Cj2uGVh)U24u zC`cCN{tq$HV2l_Fj?m$tLfvbCuxwUezg zqT9`tlc^xGtYWo6yo=x=wQT}`y+{9|G~4-1P8ah7R*yFlp=x6+hpv6v%$|S~5(%*S z@c3vJC|quzXYwC~P~lCzYth#T-a8328NqK^`yiwCVwhxRH`_;bSv!D#sgdda zN?)bZ`&2?jvyQQ9YG<3e905CI(db;-Ej=O{85pPgjr5`%a9KJD#-0y0ITuP&*@;J$ zB(xVvG3T{Gp6{B|k~y}p1#&dh(#|l09ZI5Wr1FZ8S3H@57B3gB@B8SjA&Uc7k@-h| zvt+P;w*pxKLe7e$E>e(@GkMMV_KYG@ElNR!@S9kkS;61>4Q+N}D8vITqN0~ViXV>J zypdv>v#q+%f~;JX5p+F;r^IO0bj-RMz%b|mK!rXd3_`UfRoqE-J*2%rVS|1?B2 jj!(Cw_y_FuGLOO^npjla3ev~`0Ur%j9hEX=o5=qG;zOek literal 0 HcmV?d00001 diff --git a/docs/index.md b/docs/index.md index 7ae110320..087e37265 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,6 +20,12 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python We strongly recommend you to have basic coding skills and Python knowledge. Do not hesitate to read the source code and understand the mechanisms of this bot, algorithms and techniques implemented in it. +![freqtrade screenshot](assets/freqtrade-screenshot.png) + +## Sponsored promotion + +[![tokenbot-promo](assets/TokenBot-Freqtrade-banner.png)](https://www.tokenbot.com) + ## Features - Develop your Strategy: Write your strategy in python, using [pandas](https://pandas.pydata.org/). Example strategies to inspire you are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). From 1ba9b70afc82a876ee9b3be09aa50621ee37e590 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Feb 2022 19:58:03 +0100 Subject: [PATCH 108/114] Improve index/readme wording --- README.md | 4 ++-- docs/index.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c9522f620..2df995358 100644 --- a/README.md +++ b/README.md @@ -61,9 +61,9 @@ Please find the complete documentation on the [freqtrade website](https://www.fr - [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/). - [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists. - [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. +- [x] **Builtin WebUI**: Builtin web UI to manage your bot. - [x] **Manageable via Telegram**: Manage the bot with Telegram. -- [x] **Display profit/loss in fiat**: Display your profit/loss in 33 fiat. -- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss. +- [x] **Display profit/loss in fiat**: Display your profit/loss in fiat currency. - [x] **Performance status report**: Provide a performance status of your current trades. ## Quick start diff --git a/docs/index.md b/docs/index.md index 087e37265..7de882f23 100644 --- a/docs/index.md +++ b/docs/index.md @@ -35,7 +35,7 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python - Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade. - Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode). - Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital. -- Control/Monitor: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.). +- Control/Monitor: Use Telegram or a WebUI (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.). - Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md). ## Supported exchange marketplaces From a6a041526a56c55aefca35e624a666a39ccc1f46 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Feb 2022 20:12:20 +0100 Subject: [PATCH 109/114] Update intro message --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2df995358..f3a7cf0ef 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Documentation](https://readthedocs.org/projects/freqtrade/badge/)](https://www.freqtrade.io) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) -Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. +Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) diff --git a/docs/index.md b/docs/index.md index 7de882f23..3a3b62db3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,7 +11,7 @@ ## Introduction -Freqtrade is a crypto-currency algorithmic trading software developed in python (3.8+) and supported on Windows, macOS and Linux. +Freqtrade is a free and open source crypto trading bot written in Python. It is designed to support all major exchanges and be controlled via Telegram or webUI. It contains backtesting, plotting and money management tools as well as strategy optimization by machine learning. !!! Danger "DISCLAIMER" This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. From 45c03f1440354b1eaf00044f086cfcbf7a19977f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Feb 2022 07:02:44 +0100 Subject: [PATCH 110/114] Docstrings are evaluated, while comments are not --- freqtrade/rpc/api_server/api_schemas.py | 2 +- freqtrade/rpc/rpc.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 6f358155e..c280f453c 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -355,7 +355,7 @@ class PairHistory(BaseModel): class Config: json_encoders = { - datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT) + datetime: lambda v: v.strftime(DATETIME_PRINT_FORMAT), } diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 033e32843..6a9692f21 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -963,10 +963,9 @@ class RPC: sell_mask = (dataframe['sell'] == 1) sell_signals = int(sell_mask.sum()) dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close'] - """ - band-aid until this is fixed: - https://github.com/pandas-dev/pandas/issues/45836 - """ + + # band-aid until this is fixed: + # https://github.com/pandas-dev/pandas/issues/45836 datetime_types = ['datetime', 'datetime64', 'datetime64[ns, UTC]'] date_columns = dataframe.select_dtypes(include=datetime_types) for date_column in date_columns: From 7252cf47fb62394515d2cfa7fc2554547fe70221 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Feb 2022 07:20:19 +0100 Subject: [PATCH 111/114] Update url to include campaign tracking --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f3a7cf0ef..9b25775af 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is ## Sponsored promotion -[![tokenbot-promo](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/TokenBot-Freqtrade-banner.png)](https://www.tokenbot.com) +[![tokenbot-promo](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/TokenBot-Freqtrade-banner.png)](https://tokenbot.com/?utm_source=github&utm_medium=freqtrade&utm_campaign=algodevs) ## Disclaimer diff --git a/docs/index.md b/docs/index.md index 3a3b62db3..9fb302a91 100644 --- a/docs/index.md +++ b/docs/index.md @@ -24,7 +24,7 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is ## Sponsored promotion -[![tokenbot-promo](assets/TokenBot-Freqtrade-banner.png)](https://www.tokenbot.com) +[![tokenbot-promo](assets/TokenBot-Freqtrade-banner.png)](https://tokenbot.com/?utm_source=github&utm_medium=freqtrade&utm_campaign=algodevs) ## Features From af984bdc0d1c2dee3e88b7d3ea28bdb1a2a65b56 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Feb 2022 09:32:53 +0100 Subject: [PATCH 112/114] Update comment regarting NaT replacement --- freqtrade/rpc/rpc.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 789d962f8..a6eaae0e5 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -969,9 +969,7 @@ class RPC: datetime_types = ['datetime', 'datetime64', 'datetime64[ns, UTC]'] date_columns = dataframe.select_dtypes(include=datetime_types) for date_column in date_columns: - # replace NaT with empty string, - # because if replaced with `None` - # it will be casted into NaT again + # replace NaT with `None` dataframe[date_column] = dataframe[date_column].astype(object).replace({NaT: None}) dataframe = dataframe.replace({inf: None, -inf: None, NAN: None}) From be84a028c18bdbfd58dea8a51b6d59b77b672a8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Feb 2022 07:03:19 +0100 Subject: [PATCH 113/114] Avoid mixed types in the api for /stats --- freqtrade/rpc/api_server/api_schemas.py | 2 +- freqtrade/rpc/rpc.py | 6 +++--- freqtrade/rpc/telegram.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index c280f453c..b3912a2b5 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -109,7 +109,7 @@ class SellReason(BaseModel): class Stats(BaseModel): sell_reasons: Dict[str, SellReason] - durations: Dict[str, Union[str, float]] + durations: Dict[str, Optional[float]] class DailyRecord(BaseModel): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a6eaae0e5..b9414e3f1 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -440,9 +440,9 @@ class RPC: trade_dur = (trade.close_date - trade.open_date).total_seconds() dur[trade_win_loss(trade)].append(trade_dur) - wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else 'N/A' - draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else 'N/A' - losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A' + wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else None + draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else None + losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else None durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} return {'sell_reasons': sell_reasons, 'durations': durations} diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b5c8bee4a..c7248f354 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -766,9 +766,9 @@ class Telegram(RPCHandler): duration_msg = tabulate( [ ['Wins', str(timedelta(seconds=durations['wins'])) - if durations['wins'] != 'N/A' else 'N/A'], + if durations['wins'] is not None else 'N/A'], ['Losses', str(timedelta(seconds=durations['losses'])) - if durations['losses'] != 'N/A' else 'N/A'] + if durations['losses'] is not None else 'N/A'] ], headers=['', 'Avg. Duration'] ) From 6a59103869c8459515592393d603d6916226f412 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Feb 2022 19:40:36 +0100 Subject: [PATCH 114/114] update wallets in backtesting to ensure a fresh wallet is used closes #6388 --- freqtrade/optimize/backtesting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6c5933a51..3f569649a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -709,7 +709,8 @@ class Backtesting: """ trades: List[LocalTrade] = [] self.prepare_backtest(enable_protections) - + # Ensure wallets are uptodate (important for --strategy-list) + self.wallets.update() # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) data: Dict = self._get_ohlcv_as_lists(processed)