From 5dca183b7b275268227fe122c54c8dd305eb7954 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Jan 2022 19:37:01 +0100 Subject: [PATCH 001/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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/154] 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 29879bb415125162874cf97da761f6ab80e884ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Feb 2022 19:11:35 +0100 Subject: [PATCH 016/154] 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 017/154] 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 018/154] 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 019/154] 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 020/154] 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 021/154] 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 022/154] 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 023/154] 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 024/154] 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 025/154] 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 026/154] 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 027/154] 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 028/154] 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 029/154] 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 030/154] 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 031/154] 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 032/154] 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 033/154] 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 034/154] 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 035/154] 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 036/154] 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 037/154] 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 038/154] 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 039/154] 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 040/154] 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 041/154] 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 042/154] 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 043/154] 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 044/154] 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 045/154] 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 046/154] 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 047/154] 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 048/154] 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 049/154] 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 050/154] 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 051/154] 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 052/154] 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 053/154] 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 054/154] 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 055/154] 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 056/154] 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 057/154] 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 058/154] 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 059/154] 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 060/154] 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 061/154] 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 062/154] 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 063/154] 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 064/154] 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 065/154] 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 066/154] 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 067/154] 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 068/154] 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 069/154] 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 070/154] 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 071/154] 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 072/154] 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 073/154] 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 074/154] 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 075/154] 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 076/154] 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 077/154] 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 078/154] 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 079/154] 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 080/154] 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 081/154] 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 082/154] 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 083/154] 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 084/154] 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 085/154] 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 086/154] 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 087/154] 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 088/154] 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 089/154] 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! 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 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 090/154] 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 091/154] 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 092/154] 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 093/154] 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 094/154] 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 095/154] 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 096/154] 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) From d563bfc3d045c5f6f4d4aa5d0ea55327ce2bada5 Mon Sep 17 00:00:00 2001 From: lukasgor Date: Fri, 11 Feb 2022 13:10:44 +0100 Subject: [PATCH 097/154] feature: add buy tag to forcebuy --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 3 ++- freqtrade/rpc/rpc.py | 5 +++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index b3912a2b5..bab4a8c45 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -280,6 +280,7 @@ class ForceBuyPayload(BaseModel): price: Optional[float] ordertype: Optional[OrderTypeValues] stakeamount: Optional[float] + buy_tag: Optional[str] class ForceSellPayload(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 256f82a8c..c155da416 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -136,8 +136,9 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None stake_amount = payload.stakeamount if payload.stakeamount else None + buy_tag = payload.buy_tag if payload.buy_tag else None - trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount) + trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount, buy_tag) if trade: return ForceBuyResponse.parse_obj(trade.to_json()) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b9414e3f1..377134542 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -717,7 +717,8 @@ class RPC: return {'result': f'Created sell order for trade {trade_id}.'} def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None, - stake_amount: Optional[float] = None) -> Optional[Trade]: + stake_amount: Optional[float] = None, + buy_tag: Optional[str] = None) -> Optional[Trade]: """ Handler for forcebuy Buys a pair trade at the given or current price @@ -751,7 +752,7 @@ class RPC: order_type = self._freqtrade.strategy.order_types.get( 'forcebuy', self._freqtrade.strategy.order_types['buy']) if self._freqtrade.execute_entry(pair, stake_amount, price, - ordertype=order_type, trade=trade): + ordertype=order_type, trade=trade, buy_tag=buy_tag): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade From 6511b3bec2d174494adb815f63a31c06d08f7fd3 Mon Sep 17 00:00:00 2001 From: lukasgor Date: Fri, 11 Feb 2022 15:31:15 +0100 Subject: [PATCH 098/154] fix: rename buy_tag to entry_tag --- freqtrade/rpc/api_server/api_schemas.py | 2 +- freqtrade/rpc/api_server/api_v1.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index bab4a8c45..ede5dcf0b 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -280,7 +280,7 @@ class ForceBuyPayload(BaseModel): price: Optional[float] ordertype: Optional[OrderTypeValues] stakeamount: Optional[float] - buy_tag: Optional[str] + entry_tag: Optional[str] class ForceSellPayload(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index c155da416..f072e2b14 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -136,9 +136,9 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None stake_amount = payload.stakeamount if payload.stakeamount else None - buy_tag = payload.buy_tag if payload.buy_tag else None + entry_tag = payload.entry_tag if payload.entry_tag else None - trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount, buy_tag) + trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount, entry_tag) if trade: return ForceBuyResponse.parse_obj(trade.to_json()) From c9cfc246f1599ef79f13fcb79ea93d806171b8b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Feb 2022 19:37:35 +0100 Subject: [PATCH 099/154] Sort /forcebuy pairs alphabetically, add cancel button closes #6389 --- freqtrade/rpc/telegram.py | 16 ++++++++++------ tests/rpc/test_rpc_telegram.py | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c7248f354..0a634ffae 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -914,10 +914,11 @@ class Telegram(RPCHandler): self._send_msg(str(e)) def _forcebuy_action(self, pair, price=None): - try: - self._rpc._rpc_forcebuy(pair, price) - except RPCException as e: - self._send_msg(str(e)) + if pair != 'cancel': + try: + self._rpc._rpc_forcebuy(pair, price) + except RPCException as e: + self._send_msg(str(e)) def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: @@ -947,10 +948,13 @@ class Telegram(RPCHandler): self._forcebuy_action(pair, price) else: whitelist = self._rpc._rpc_whitelist()['whitelist'] - pairs = [InlineKeyboardButton(text=pair, callback_data=pair) for pair in whitelist] + pair_buttons = [ + InlineKeyboardButton(text=pair, callback_data=pair) for pair in sorted(whitelist)] + buttons_aligned = self._layout_inline_keyboard(pair_buttons) + buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')]) self._send_msg(msg="Which pair?", - keyboard=self._layout_inline_keyboard(pairs)) + keyboard=buttons_aligned) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 796147609..67a6c72fe 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1260,7 +1260,8 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: assert msg_mock.call_args_list[0][1]['msg'] == 'Which pair?' # assert msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy' keyboard = msg_mock.call_args_list[0][1]['keyboard'] - assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4 + # One additional button - cancel + assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 5 update = MagicMock() update.callback_query = MagicMock() update.callback_query.data = 'XRP/USDT' From 08803524bd27c8a5c06c4d92cc81b4cf032a9bce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Feb 2022 15:21:36 +0100 Subject: [PATCH 100/154] align variable naming to use current_time --- freqtrade/strategy/interface.py | 11 ++++++----- tests/strategy/test_interface.py | 6 ++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 0bd7834e2..2f3657059 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -687,7 +687,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: return False - def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, + def should_sell(self, trade: Trade, rate: float, current_time: datetime, buy: bool, sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ @@ -704,7 +704,8 @@ class IStrategy(ABC, HyperStrategyMixin): trade.adjust_min_max_rates(high or current_rate, low or current_rate) stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, - current_time=date, current_profit=current_profit, + current_time=current_time, + current_profit=current_profit, force_stoploss=force_stoploss, low=low, high=high) # Set current rate to high for backtesting sell @@ -714,7 +715,7 @@ class IStrategy(ABC, HyperStrategyMixin): # if buy signal and ignore_roi is set, we don't need to evaluate min_roi. roi_reached = (not (buy and self.ignore_roi_if_buy_signal) and self.min_roi_reached(trade=trade, current_profit=current_profit, - current_time=date)) + current_time=current_time)) sell_signal = SellType.NONE custom_reason = '' @@ -730,8 +731,8 @@ class IStrategy(ABC, HyperStrategyMixin): sell_signal = SellType.SELL_SIGNAL else: custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)( - pair=trade.pair, trade=trade, current_time=date, current_rate=current_rate, - current_profit=current_profit) + pair=trade.pair, trade=trade, current_time=current_time, + current_rate=current_rate, current_profit=current_profit) if custom_reason: sell_signal = SellType.CUSTOM_SELL if isinstance(custom_reason, str): diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index fd1c2753f..174ce95c6 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -437,7 +437,8 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili strategy.custom_stoploss = custom_stop now = arrow.utcnow().datetime - sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit), trade=trade, + current_rate = trade.open_rate * (1 + profit) + sl_flag = strategy.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=now, current_profit=profit, force_stoploss=0, high=None) assert isinstance(sl_flag, SellCheckTuple) @@ -447,8 +448,9 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili else: assert sl_flag.sell_flag is True assert round(trade.stop_loss, 2) == adjusted + current_rate2 = trade.open_rate * (1 + profit2) - sl_flag = strategy.stop_loss_reached(current_rate=trade.open_rate * (1 + profit2), trade=trade, + sl_flag = strategy.stop_loss_reached(current_rate=current_rate2, trade=trade, current_time=now, current_profit=profit2, force_stoploss=0, high=None) assert sl_flag.sell_type == expected2 From 119d4d5204534ba25b0d2d10510f4f21f04094ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Feb 2022 17:06:03 +0100 Subject: [PATCH 101/154] select_order should use ft_order_side, not the exchange specific one --- freqtrade/persistence/models.py | 4 ++-- tests/test_persistence.py | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index dfa98d97f..5f2db1050 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -659,13 +659,13 @@ class LocalTrade(): 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 order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss') :param is_open: Only search for open orders? :return: latest Order object if it exists, else None """ orders = self.orders if order_side: - orders = [o for o in self.orders if o.side == order_side] + orders = [o for o in self.orders if o.ft_order_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: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 97851fdc4..b8f7a3336 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1277,11 +1277,14 @@ def test_select_order(fee): order = trades[4].select_order('buy', False) assert order is not None + trades[4].orders[1].ft_order_side = 'sell' order = trades[4].select_order('sell', True) assert order is not None + + trades[4].orders[1].ft_order_side = 'stoploss' + order = trades[4].select_order('stoploss', None) + assert order is not None assert order.ft_order_side == 'stoploss' - order = trades[4].select_order('sell', False) - assert order is None def test_Trade_object_idem(): From c769e9757d6454c921806b279c7fc5997b90381b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Feb 2022 20:13:12 +0100 Subject: [PATCH 102/154] Improve "order refind" to also work for stoploss orders --- freqtrade/freqtradebot.py | 28 ++------------ tests/test_freqtradebot.py | 77 ++++++++++---------------------------- 2 files changed, 23 insertions(+), 82 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 279bb6161..fce85baa3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -298,28 +298,6 @@ class FreqtradeBot(LoggingMixin): self.update_trade_state(trade, order.order_id, send_msg=False) def handle_insufficient_funds(self, trade: Trade): - """ - Determine if we ever opened a sell order for this trade. - If not, try update buy fees - otherwise "refind" the open order we obviously lost. - """ - sell_order = trade.select_order('sell', None) - if sell_order: - self.refind_lost_order(trade) - else: - self.reupdate_enter_order_fees(trade) - - def reupdate_enter_order_fees(self, trade: Trade): - """ - Get buy order from database, and try to reupdate. - Handles trades where the initial fee-update did not work. - """ - logger.info(f"Trying to reupdate buy fees for {trade}") - order = trade.select_order('buy', False) - if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id, send_msg=False) - - def refind_lost_order(self, trade): """ Try refinding a lost trade. Only used when InsufficientFunds appears on sell orders (stoploss or sell). @@ -332,9 +310,6 @@ class FreqtradeBot(LoggingMixin): if not order.ft_is_open: logger.debug(f"Order {order} is no longer open.") continue - if order.ft_order_side == 'buy': - # Skip buy side - this is handled by reupdate_buy_order_fees - continue try: fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, order.ft_order_side == 'stoploss') @@ -346,6 +321,9 @@ class FreqtradeBot(LoggingMixin): if fo and fo['status'] == 'open': # Assume this as the open order trade.open_order_id = order.order_id + elif order.ft_order_side == 'buy': + if fo and fo['status'] == 'open': + trade.open_order_id = order.order_id if fo: logger.info(f"Found {order} for trade {trade}.") self.update_trade_state(trade, order.order_id, fo, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a84616516..4bbf26362 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4110,15 +4110,17 @@ def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog): freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + return_value={'status': 'open'}) create_mock_trades(fee) trades = Trade.get_trades().all() - freqtrade.reupdate_enter_order_fees(trades[0]) - assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) + freqtrade.handle_insufficient_funds(trades[3]) + # assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 1 - assert mock_uts.call_args_list[0][0][0] == trades[0] - assert mock_uts.call_args_list[0][0][1] == mock_order_1()['id'] - assert log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) + assert mock_uts.call_args_list[0][0][0] == trades[3] + assert mock_uts.call_args_list[0][0][1] == mock_order_4()['id'] + assert log_has_re(r"Trying to refind lost order for .*", caplog) mock_uts.reset_mock() caplog.clear() @@ -4136,52 +4138,13 @@ def test_reupdate_enter_order_fees(mocker, default_conf_usdt, fee, caplog): ) Trade.query.session.add(trade) - freqtrade.reupdate_enter_order_fees(trade) - assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) + freqtrade.handle_insufficient_funds(trade) + # assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 0 - assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) @pytest.mark.usefixtures("init_persistence") -def test_handle_insufficient_funds(mocker, default_conf_usdt, fee): - freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') - mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees') - create_mock_trades(fee) - trades = Trade.get_trades().all() - - # Trade 0 has only a open buy order, no closed order - freqtrade.handle_insufficient_funds(trades[0]) - assert mock_rlo.call_count == 0 - assert mock_bof.call_count == 1 - - mock_rlo.reset_mock() - mock_bof.reset_mock() - - # Trade 1 has closed buy and sell orders - freqtrade.handle_insufficient_funds(trades[1]) - assert mock_rlo.call_count == 1 - assert mock_bof.call_count == 0 - - mock_rlo.reset_mock() - mock_bof.reset_mock() - - # Trade 2 has closed buy and sell orders - freqtrade.handle_insufficient_funds(trades[2]) - assert mock_rlo.call_count == 1 - assert mock_bof.call_count == 0 - - mock_rlo.reset_mock() - mock_bof.reset_mock() - - # Trade 3 has an opne buy order - freqtrade.handle_insufficient_funds(trades[3]) - assert mock_rlo.call_count == 0 - assert mock_bof.call_count == 1 - - -@pytest.mark.usefixtures("init_persistence") -def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): +def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, caplog): caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') @@ -4204,7 +4167,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): assert trade.open_order_id is None assert trade.stoploss_order_id is None - freqtrade.refind_lost_order(trade) + freqtrade.handle_insufficient_funds(trade) order = mock_order_1() assert log_has_re(r"Order Order(.*order_id=" + order['id'] + ".*) is no longer open.", caplog) assert mock_fo.call_count == 0 @@ -4222,13 +4185,13 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): assert trade.open_order_id is None assert trade.stoploss_order_id is None - freqtrade.refind_lost_order(trade) + freqtrade.handle_insufficient_funds(trade) order = mock_order_4() assert log_has_re(r"Trying to refind Order\(.*", caplog) - assert mock_fo.call_count == 0 - assert mock_uts.call_count == 0 - # No change to orderid - as update_trade_state is mocked - assert trade.open_order_id is None + assert mock_fo.call_count == 1 + assert mock_uts.call_count == 1 + # Found open buy order + assert trade.open_order_id is not None assert trade.stoploss_order_id is None caplog.clear() @@ -4240,11 +4203,11 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): assert trade.open_order_id is None assert trade.stoploss_order_id is None - freqtrade.refind_lost_order(trade) + freqtrade.handle_insufficient_funds(trade) order = mock_order_5_stoploss() assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 1 - assert mock_uts.call_count == 1 + assert mock_uts.call_count == 2 # stoploss_order_id is "refound" and added to the trade assert trade.open_order_id is None assert trade.stoploss_order_id is not None @@ -4259,7 +4222,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): assert trade.open_order_id is None assert trade.stoploss_order_id is None - freqtrade.refind_lost_order(trade) + freqtrade.handle_insufficient_funds(trade) order = mock_order_6_sell() assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 1 @@ -4275,7 +4238,7 @@ def test_refind_lost_order(mocker, default_conf_usdt, fee, caplog): side_effect=ExchangeError()) order = mock_order_5_stoploss() - freqtrade.refind_lost_order(trades[4]) + freqtrade.handle_insufficient_funds(trades[4]) assert log_has(f"Error updating {order['id']}.", caplog) From b1b8167b5e8278c76a4eacb2852c6e6d5fc08916 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Feb 2022 19:14:57 +0100 Subject: [PATCH 103/154] Update stop documentation closes #6393 --- docs/stoploss.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 4f8ac9e94..4d28846f1 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -2,6 +2,7 @@ The `stoploss` configuration parameter is loss as ratio that should trigger a sale. For example, value `-0.10` will cause immediate sell if the profit dips below -10% for a given trade. This parameter is optional. +Stoploss calculations do include fees, so a stoploss of -10% is placed exactly 10% below the entry point. Most of the strategy files already include the optimal `stoploss` value. @@ -30,7 +31,7 @@ These modes can be configured with these values: ### stoploss_on_exchange and stoploss_on_exchange_limit_ratio Enable or Disable stop loss on exchange. -If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. +If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order fills. This will protect you against sudden crashes in market, as the order execution happens purely within the exchange, and has no potential network overhead. If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. `stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this. From ca6291479420848d21a9389a397ea7fe6e8a00ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 03:01:28 +0000 Subject: [PATCH 104/154] Bump prompt-toolkit from 3.0.26 to 3.0.28 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.26 to 3.0.28. - [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.26...3.0.28) --- 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 ba20a169f..4710b800c 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.26 +prompt-toolkit==3.0.28 # Extensions to datetime library python-dateutil==2.8.2 From be8accebd85b30d16eed7fef76f26a88152af1d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 03:01:31 +0000 Subject: [PATCH 105/154] Bump plotly from 5.5.0 to 5.6.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 5.5.0 to 5.6.0. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v5.5.0...v5.6.0) --- updated-dependencies: - dependency-name: plotly dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 990edc3c8..bb2132f87 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.5.0 +plotly==5.6.0 From 03d4002be88be10ad8d7ebff6480b764fbbdab6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 03:01:35 +0000 Subject: [PATCH 106/154] Bump pymdown-extensions from 9.1 to 9.2 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 9.1 to 9.2. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/9.1...9.2) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-minor ... 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 ad6d43f17..445e45723 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.3 mkdocs-material==8.1.10 mdx_truly_sane_lists==1.2 -pymdown-extensions==9.1 +pymdown-extensions==9.2 From 1674beed9121f6d19286530886c243949328468c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 03:01:43 +0000 Subject: [PATCH 107/154] Bump ccxt from 1.72.36 to 1.72.98 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.72.36 to 1.72.98. - [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.72.36...1.72.98) --- updated-dependencies: - dependency-name: ccxt 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 ba20a169f..aaee3ca24 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.2 pandas==1.4.0 pandas-ta==0.3.14b -ccxt==1.72.36 +ccxt==1.72.98 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 From b18e44bc43d2b0f5eabe1294a989a53f74acdecc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 03:01:46 +0000 Subject: [PATCH 108/154] Bump pytest from 7.0.0 to 7.0.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 7.0.0 to 7.0.1. - [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/7.0.0...7.0.1) --- updated-dependencies: - dependency-name: pytest 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 3fcdaab63..3ad19cdd3 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==7.0.0 +pytest==7.0.1 pytest-asyncio==0.17.2 pytest-cov==3.0.0 pytest-mock==3.7.0 From 22036d69d83befb7f7d62e0e1d6e9a7a03e8c190 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 03:01:50 +0000 Subject: [PATCH 109/154] Bump mkdocs-material from 8.1.10 to 8.1.11 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.1.10 to 8.1.11. - [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.10...8.1.11) --- 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 ad6d43f17..11e5bcc72 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.3 -mkdocs-material==8.1.10 +mkdocs-material==8.1.11 mdx_truly_sane_lists==1.2 pymdown-extensions==9.1 From 7f8e956b44fe38c423867e8e479aa99c40c6309c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 03:01:52 +0000 Subject: [PATCH 110/154] Bump types-requests from 2.27.8 to 2.27.9 Bumps [types-requests](https://github.com/python/typeshed) from 2.27.8 to 2.27.9. - [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 3fcdaab63..05d2c0f87 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.8 +types-requests==2.27.9 types-tabulate==0.8.5 # Extensions to datetime library From 04c20afecebaf33bcedc27921292efab3a804f9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 03:01:56 +0000 Subject: [PATCH 111/154] Bump nbconvert from 6.4.1 to 6.4.2 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 6.4.1 to 6.4.2. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/6.4.1...6.4.2) --- 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 3fcdaab63..2c26b67ed 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.1 +nbconvert==6.4.2 # mypy types types-cachetools==4.2.9 From 5062c17ac03872b1e36d668ce8f41d834db96964 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 03:02:07 +0000 Subject: [PATCH 112/154] Bump pandas from 1.4.0 to 1.4.1 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/main/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: pandas 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 ba20a169f..a7d3a84b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.22.2 -pandas==1.4.0 +pandas==1.4.1 pandas-ta==0.3.14b ccxt==1.72.36 From 5cc6c2afe1c4e012b5c998b9e485aa601e658c80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 05:28:10 +0000 Subject: [PATCH 113/154] Bump pytest-asyncio from 0.17.2 to 0.18.1 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.17.2 to 0.18.1. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.17.2...v0.18.1) --- updated-dependencies: - dependency-name: pytest-asyncio 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 3ad19cdd3..72fea0828 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==4.0.1 flake8-tidy-imports==4.6.0 mypy==0.931 pytest==7.0.1 -pytest-asyncio==0.17.2 +pytest-asyncio==0.18.1 pytest-cov==3.0.0 pytest-mock==3.7.0 pytest-random-order==1.0.4 From acd7f26a9d14e75ffa4129d4cd6086992db63052 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Feb 2022 19:57:51 +0100 Subject: [PATCH 114/154] update tc36 to properly cover #6261 --- tests/optimize/test_backtest_detail.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 3164e11b9..c23fd8f44 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -568,11 +568,11 @@ 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 and immediate ROI + [1, 5000, 5500, 4951, 4999, 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]], - stop_loss=-0.01, roi={"0": 0.10}, profit_perc=0.1, + stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, custom_entry_price=4952, trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] ) From 30f6dbfc406453fd2783031b2a2817738c136327 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Feb 2022 20:02:38 +0100 Subject: [PATCH 115/154] Attempt fix for #6261 --- freqtrade/optimize/backtesting.py | 14 +++++++++++++- tests/optimize/test_backtest_detail.py | 8 ++++---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3f569649a..3126e1943 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -357,6 +357,15 @@ class Backtesting: # use Open rate if open_rate > calculated sell rate return sell_row[OPEN_IDX] + if ( + trade_dur == 0 + and trade.open_rate < sell_row[OPEN_IDX] # trade-open > open_rate + and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # Red candle + ): + # ROI on opening candles with custom pricing can only + # trigger if the entry was at Open or lower. + # details: https: // github.com/freqtrade/freqtrade/issues/6261 + raise ValueError("Opening candle ROI on red candles.") # 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. @@ -414,7 +423,10 @@ class Backtesting: trade.close_date = sell_candle_time trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60) - closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) + try: + closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) + except ValueError: + return None # 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'] diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index c23fd8f44..5ad67d9a0 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -562,9 +562,9 @@ tc35 = BTContainer(data=[ ) # 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. +# Would cause immediate ROI exit, but since the trade was entered +# below open, we treat this as cheating, and delay the sell by 1 candle. +# details: https://github.com/freqtrade/freqtrade/issues/6261 tc36 = BTContainer(data=[ # D O H L C V B S BT [0, 5000, 5050, 4950, 5000, 6172, 1, 0], @@ -574,7 +574,7 @@ tc36 = BTContainer(data=[ [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, custom_entry_price=4952, - trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] ) From 7b2e33b0bcecb7518bbfcf921f197ecfa6a14b27 Mon Sep 17 00:00:00 2001 From: Maik H Date: Mon, 14 Feb 2022 20:21:42 +0100 Subject: [PATCH 116/154] corrects typo --- docs/includes/pairlists.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 031397719..cec5ceb19 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -246,7 +246,7 @@ On exchanges that deduct fees from the receiving currency (e.g. FTX) - this can The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. This option is disabled by default, and will only apply if set to > 0. -For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied. +For `PriceFilter` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied. Calculation example: From 64b98989d2be20fe0fc85dc52929f63e6b4f9fce Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Feb 2022 19:25:32 +0100 Subject: [PATCH 117/154] Update open candle ROI condition --- freqtrade/optimize/backtesting.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3126e1943..b423771ca 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -359,12 +359,15 @@ class Backtesting: if ( trade_dur == 0 - and trade.open_rate < sell_row[OPEN_IDX] # trade-open > open_rate + # Red candle (for longs), TODO: green candle (for shorts) and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # Red candle + and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate + and close_rate > sell_row[CLOSE_IDX] ): # ROI on opening candles with custom pricing can only # trigger if the entry was at Open or lower. # details: https: // github.com/freqtrade/freqtrade/issues/6261 + # If open_rate is < open, only allow sells below the close on red candles. raise ValueError("Opening candle ROI on red candles.") # Use the maximum between close_rate and low as we # cannot sell outside of a candle. From 3787b747ae60f21780f0a72a864db7aae5142687 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Feb 2022 20:01:35 +0100 Subject: [PATCH 118/154] Simplify api schema by not using union types --- freqtrade/rpc/api_server/api_backtest.py | 7 +++++-- freqtrade/rpc/api_server/api_schemas.py | 4 ++-- freqtrade/rpc/rpc.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 97b7b7989..a008702a9 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -32,6 +32,10 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac for setting in settings.keys(): if settings[setting] is not None: btconfig[setting] = settings[setting] + try: + btconfig['stake_amount'] = float(btconfig['stake_amount']) + except ValueError: + pass # Force dry-run for backtesting btconfig['dry_run'] = True @@ -57,8 +61,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac ): from freqtrade.optimize.backtesting import Backtesting ApiServer._bt = Backtesting(btconfig) - if ApiServer._bt.timeframe_detail: - ApiServer._bt.load_bt_data_detail() + ApiServer._bt.load_bt_data_detail() else: ApiServer._bt.config = btconfig ApiServer._bt.init_backtest() diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index ede5dcf0b..e22cf82b3 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -149,7 +149,7 @@ class ShowConfig(BaseModel): api_version: float dry_run: bool stake_currency: str - stake_amount: Union[float, str] + stake_amount: str available_capital: Optional[float] stake_currency_decimals: int max_open_trades: int @@ -366,7 +366,7 @@ class BacktestRequest(BaseModel): timeframe_detail: Optional[str] timerange: Optional[str] max_open_trades: Optional[int] - stake_amount: Optional[Union[float, str]] + stake_amount: Optional[str] enable_protections: bool dry_run_wallet: Optional[float] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 377134542..5912a0ecd 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -112,7 +112,7 @@ class RPC: 'dry_run': config['dry_run'], 'stake_currency': config['stake_currency'], 'stake_currency_decimals': decimals_per_coin(config['stake_currency']), - 'stake_amount': config['stake_amount'], + 'stake_amount': str(config['stake_amount']), 'available_capital': config.get('available_capital'), 'max_open_trades': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), From 78a93b60526640c272fc725e3f37e4e76e6c9191 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Feb 2022 20:15:03 +0100 Subject: [PATCH 119/154] noqa --- freqtrade/rpc/api_server/api_backtest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index a008702a9..8b86b8005 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -20,6 +20,7 @@ router = APIRouter() @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) +# flake8: noqa: C901 async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks, config=Depends(get_config)): """Start backtesting if not done so already""" From b043697d701d9169a2e21760933d1861ebfc2dab Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Wed, 16 Feb 2022 12:19:48 +0900 Subject: [PATCH 120/154] Update config_full.example.json --- 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 895a0af87..d675fb1a9 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -8,6 +8,7 @@ "amend_last_stake_amount": false, "last_stake_amount_min_ratio": 0.5, "dry_run": true, + "dry_run_wallet": 1000, "cancel_open_orders_on_exit": false, "timeframe": "5m", "trailing_stop": false, From e7bfb4fd5c22696fb87b1753a29f6c814c996ced Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Feb 2022 13:42:39 +0100 Subject: [PATCH 121/154] Add test case for "sell below close" case --- tests/optimize/test_backtest_detail.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 5ad67d9a0..977563eeb 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -577,10 +577,24 @@ tc36 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] ) - -# Test 37: Custom exit price below all candles -# Price adjusted to candle Low. +# Test 37: Custom-entry-price around candle low +# Would cause immediate ROI exit below close +# details: https://github.com/freqtrade/freqtrade/issues/6261 tc37 = BTContainer(data=[ + # D O H L C V B S BT + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5400, 5500, 4951, 5100, 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]], + stop_loss=-0.10, roi={"0": 0.01}, profit_perc=0.01, + custom_entry_price=4952, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=1)] +) + +# Test 38: Custom exit price below all candles +# Price adjusted to candle Low. +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], @@ -593,9 +607,9 @@ tc37 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=3)] ) -# Test 38: Custom exit price above all candles +# Test 39: Custom exit price above all candles # causes sell signal timeout -tc38 = BTContainer(data=[ +tc39 = 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], @@ -649,6 +663,7 @@ TESTS = [ tc36, tc37, tc38, + tc39, ] From e60553b8f7b6400cddba5fafd04419ee5241f069 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Feb 2022 19:21:04 +0100 Subject: [PATCH 122/154] Add max_entry_position hyperopt to docs closes #6356 --- docs/hyperopt.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 27d5a8761..19d8cd692 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -508,6 +508,46 @@ class MyAwesomeStrategy(IStrategy): You will then obviously also change potential interesting entries to parameters to allow hyper-optimization. +### Optimizing `max_entry_position_adjustment` + +While `max_entry_position_adjustment` is not a separate space, it can still be used in hyperopt by using the property approach shown above. + +``` python +from pandas import DataFrame +from functools import reduce + +import talib.abstract as ta + +from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalParameter, + IStrategy, IntParameter) +import freqtrade.vendor.qtpylib.indicators as qtpylib + +class MyAwesomeStrategy(IStrategy): + stoploss = -0.05 + timeframe = '15m' + + # Define the parameter spaces + max_epa = CategoricalParameter([-1, 0, 1, 3, 5, 10], default=1, space="buy", optimize=True) + + @property + def max_entry_position_adjustment(self): + return self.max_epa.value + + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # ... +``` + +??? Tip "Using `IntParameter`" + You can also use the `IntParameter` for this optimization, but you must explicitly return an integer: + ``` python + max_epa = IntParameter(-1, 10, default=1, space="buy", optimize=True) + + @property + def max_entry_position_adjustment(self): + return int(self.max_epa.value) + ``` + ## Loss-functions Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. From eb88c0f71b2dda0f5aab31f2fcaa7d1d95d0f0bc Mon Sep 17 00:00:00 2001 From: Kavinkumar <33546454+mkavinkumar1@users.noreply.github.com> Date: Fri, 18 Feb 2022 19:25:56 +0530 Subject: [PATCH 123/154] fixed stake amount --- tests/rpc/test_rpc_telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 67a6c72fe..f39c627f1 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2031,7 +2031,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', - 'stake_amount': 0.001, + 'stake_amount': 0.015, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', 'fiat_currency': None, @@ -2044,7 +2044,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' - '*Total:* `(0.00100000 BTC)`') + '*Total:* `(0.01500000 BTC)`') def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: From 95d4a11bb1564bfc5a555f9ae02fedf6af6f8a6a Mon Sep 17 00:00:00 2001 From: Kavinkumar <33546454+mkavinkumar1@users.noreply.github.com> Date: Fri, 18 Feb 2022 19:32:37 +0530 Subject: [PATCH 124/154] add precision --- tests/rpc/test_rpc_telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f39c627f1..e42d6bfa7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2031,7 +2031,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', - 'stake_amount': 0.015, + 'stake_amount': 0.01465333, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', 'fiat_currency': None, @@ -2044,7 +2044,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' - '*Total:* `(0.01500000 BTC)`') + '*Total:* `(0.01465333 BTC)`') def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: From 60d1e7fc6578e57ebd27ad05b37e4de63e1ed20f Mon Sep 17 00:00:00 2001 From: Kavinkumar <33546454+mkavinkumar1@users.noreply.github.com> Date: Fri, 18 Feb 2022 19:47:45 +0530 Subject: [PATCH 125/154] fix stake amt --- tests/rpc/test_rpc_telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index e42d6bfa7..4d00095e4 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1734,7 +1734,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', - 'stake_amount': 0.001, + 'stake_amount': 0.01465333, 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', 'fiat_currency': 'USD', @@ -1751,7 +1751,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ - '*Total:* `(0.00100000 BTC, 12.345 USD)`' + '*Total:* `(0.01465333 BTC, 180.895 USD)`' freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} caplog.clear() @@ -1825,7 +1825,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: 'buy_tag': 'buy_signal_01', 'exchange': 'Binance', 'pair': 'ETH/BTC', - 'stake_amount': 0.001, + 'stake_amount': 0.01465333, # 'stake_amount_fiat': 0.0, 'stake_currency': 'BTC', 'fiat_currency': 'USD', @@ -1839,7 +1839,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: '*Buy Tag:* `buy_signal_01`\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ - '*Total:* `(0.00100000 BTC, 12.345 USD)`' + '*Total:* `(0.01465333 BTC, 180.895 USD)`' def test_send_msg_sell_notification(default_conf, mocker) -> None: From 0bbbe2e96c5e58413a6611db7d8c967514727fe2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 06:36:23 +0100 Subject: [PATCH 126/154] Add test for #6429 --- tests/test_freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 4bbf26362..08d98b42d 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2436,6 +2436,9 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_buy_order_ mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason) assert log_has_re(r"Order .* for .* not cancelled.", caplog) + # min_pair_stake empty should not crash + mocker.patch('freqtrade.exchange.Exchange.get_min_pair_stake_amount', return_value=None) + assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason) @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], From 3785f04be7c8bbeb01e9f849ad194ac77dfeeeeb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 06:38:11 +0100 Subject: [PATCH 127/154] Handle empty min stake amount as observed on FTX (closes #6429) --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/persistence/models.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fce85baa3..5f2b72e1e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1021,12 +1021,12 @@ class FreqtradeBot(LoggingMixin): # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: - filled_val = order.get('filled', 0.0) or 0.0 + filled_val: float = order.get('filled', 0.0) or 0.0 filled_stake = filled_val * trade.open_rate minstake = self.exchange.get_min_pair_stake_amount( trade.pair, trade.open_rate, self.strategy.stoploss) - if filled_val > 0 and filled_stake < minstake: + if filled_val > 0 and minstake and filled_stake < minstake: logger.warning( f"Order {trade.open_order_id} for {trade.pair} not cancelled, " f"as the filled amount of {filled_val} would result in an unsellable trade.") diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5f2db1050..b8ea5848f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -799,11 +799,11 @@ class Trade(_DECL_BASE, LocalTrade): fee_close = Column(Float, nullable=False, default=0.0) fee_close_cost = Column(Float, nullable=True) fee_close_currency = Column(String(25), nullable=True) - open_rate = Column(Float) + open_rate: float = Column(Float) open_rate_requested = Column(Float) # open_trade_value - calculated via _calc_open_trade_value open_trade_value = Column(Float) - close_rate = Column(Float) + close_rate: Optional[float] = Column(Float) close_rate_requested = Column(Float) close_profit = Column(Float) close_profit_abs = Column(Float) From a32aed2225544073eb020d91ce1806e04b2d4ce1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 10:07:32 +0100 Subject: [PATCH 128/154] Update FTX stoploss code to avoid exception for stoploss-market orders closes #6430, closes #6392 --- freqtrade/exchange/ftx.py | 19 +++++++++++-------- tests/exchange/test_ftx.py | 21 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index e9eb2fe19..a8bf9abac 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -106,15 +106,18 @@ class Ftx(Exchange): if order[0].get('status') == 'closed': # Trigger order was triggered ... real_order_id = order[0].get('info', {}).get('orderId') + # OrderId may be None for stoploss-market orders + # But contains "average" in these cases. + if real_order_id: + order1 = self._api.fetch_order(real_order_id, pair) + self._log_exchange_response('fetch_stoploss_order1', order1) + # Fake type to stop - as this was really a stop order. + order1['id_stop'] = order1['id'] + order1['id'] = order_id + order1['type'] = 'stop' + order1['status_stop'] = 'triggered' + return order1 - order1 = self._api.fetch_order(real_order_id, pair) - self._log_exchange_response('fetch_stoploss_order1', order1) - # Fake type to stop - as this was really a stop order. - order1['id_stop'] = order1['id'] - order1['id'] = order_id - order1['type'] = 'stop' - order1['status_stop'] = 'triggered' - return order1 return order[0] else: raise InvalidOrderException(f"Could not get stoploss order for id {order_id}") diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 3794bb79c..c2fb90c9d 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -125,7 +125,7 @@ def test_stoploss_adjust_ftx(mocker, default_conf): assert not exchange.stoploss_adjust(1501, order) -def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): +def test_fetch_stoploss_order_ftx(default_conf, mocker, limit_sell_order): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 @@ -147,9 +147,15 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] - api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': 'closed'}]) + # stoploss Limit order + api_mock.fetch_orders = MagicMock(return_value=[ + {'id': 'X', 'status': 'closed', + 'info': { + 'orderId': 'mocked_limit_sell', + }}]) api_mock.fetch_order = MagicMock(return_value=limit_sell_order) + # No orderId field - no call to fetch_order resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') assert resp assert api_mock.fetch_order.call_count == 1 @@ -158,6 +164,17 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): assert resp['type'] == 'stop' assert resp['status_stop'] == 'triggered' + # Stoploss market order + # Contains no new Order, but "average" instead + order = {'id': 'X', 'status': 'closed', 'info': {'orderId': None}, 'average': 0.254} + api_mock.fetch_orders = MagicMock(return_value=[order]) + api_mock.fetch_order.reset_mock() + resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') + assert resp + # fetch_order not called (no regular order ID) + assert api_mock.fetch_order.call_count == 0 + assert order == order + with pytest.raises(InvalidOrderException): api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') From a7a25bb28514426531702f260edaeb11b0c9d994 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 16:35:17 +0100 Subject: [PATCH 129/154] Update "round coin value" to trim trailing zeros --- freqtrade/misc.py | 13 +++++++++---- tests/test_misc.py | 7 +++++-- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 2a27f1660..133014f39 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -29,18 +29,23 @@ def decimals_per_coin(coin: str): return DECIMALS_PER_COIN.get(coin, DECIMAL_PER_COIN_FALLBACK) -def round_coin_value(value: float, coin: str, show_coin_name=True) -> str: +def round_coin_value( + value: float, coin: str, show_coin_name=True, keep_trailing_zeros=False) -> str: """ Get price value for this coin :param value: Value to be printed :param coin: Which coin are we printing the price / value for :param show_coin_name: Return string in format: "222.22 USDT" or "222.22" + :param keep_trailing_zeros: Keep trailing zeros "222.200" vs. "222.2" :return: Formatted / rounded value (with or without coin name) """ + val = f"{value:.{decimals_per_coin(coin)}f}" + if not keep_trailing_zeros: + val = val.rstrip('0').rstrip('.') if show_coin_name: - return f"{value:.{decimals_per_coin(coin)}f} {coin}" - else: - return f"{value:.{decimals_per_coin(coin)}f}" + val = f"{val} {coin}" + + return val def shorten_date(_date: str) -> str: diff --git a/tests/test_misc.py b/tests/test_misc.py index 21a00f3be..4fd5338ad 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -21,16 +21,19 @@ def test_decimals_per_coin(): def test_round_coin_value(): assert round_coin_value(222.222222, 'USDT') == '222.222 USDT' - assert round_coin_value(222.2, 'USDT') == '222.200 USDT' + assert round_coin_value(222.2, 'USDT', keep_trailing_zeros=True) == '222.200 USDT' + assert round_coin_value(222.2, 'USDT') == '222.2 USDT' assert round_coin_value(222.12745, 'EUR') == '222.127 EUR' assert round_coin_value(0.1274512123, 'BTC') == '0.12745121 BTC' assert round_coin_value(0.1274512123, 'ETH') == '0.12745 ETH' assert round_coin_value(222.222222, 'USDT', False) == '222.222' - assert round_coin_value(222.2, 'USDT', False) == '222.200' + assert round_coin_value(222.2, 'USDT', False) == '222.2' + assert round_coin_value(222.00, 'USDT', False) == '222' assert round_coin_value(222.12745, 'EUR', False) == '222.127' assert round_coin_value(0.1274512123, 'BTC', False) == '0.12745121' assert round_coin_value(0.1274512123, 'ETH', False) == '0.12745' + assert round_coin_value(222.2, 'USDT', False, True) == '222.200' def test_shorten_date() -> None: From d610b6305de54e0cabb60350681313d00324d988 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Feb 2022 16:39:47 +0100 Subject: [PATCH 130/154] Improve /balance output by removing trailing zeros --- freqtrade/optimize/hyperopt_tools.py | 4 ++-- freqtrade/rpc/telegram.py | 13 +++++++------ tests/rpc/test_rpc_telegram.py | 6 +++--- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 61a10c32b..8c84f772a 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -373,7 +373,7 @@ class HyperoptTools(): trials[f"Max Drawdown{' (Acct)' if has_account_drawdown else ''}"] = trials.apply( lambda x: "{} {}".format( - round_coin_value(x['max_drawdown_abs'], stake_currency), + round_coin_value(x['max_drawdown_abs'], stake_currency, keep_trailing_zeros=True), (f"({x['max_drawdown_account']:,.2%})" if has_account_drawdown else f"({x['max_drawdown']:,.2%})" @@ -388,7 +388,7 @@ class HyperoptTools(): trials['Profit'] = trials.apply( lambda x: '{} {}'.format( - round_coin_value(x['Total profit'], stake_currency), + round_coin_value(x['Total profit'], stake_currency, keep_trailing_zeros=True), f"({x['Profit']:,.2%})".rjust(10, ' ') ).rjust(25+len(stake_currency)) if x['Total profit'] != 0.0 else '--'.rjust(25+len(stake_currency)), diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0a634ffae..da613fab8 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -790,12 +790,13 @@ class Telegram(RPCHandler): output = '' if self._config['dry_run']: output += "*Warning:* Simulated balances in Dry Mode.\n" - - output += ("Starting capital: " - f"`{result['starting_capital']}` {self._config['stake_currency']}" - ) - output += (f" `{result['starting_capital_fiat']}` " - f"{self._config['fiat_display_currency']}.\n" + starting_cap = round_coin_value( + result['starting_capital'], self._config['stake_currency']) + output += f"Starting capital: `{starting_cap}`" + starting_cap_fiat = round_coin_value( + result['starting_capital_fiat'], self._config['fiat_display_currency'] + ) if result['starting_capital_fiat'] > 0 else '' + output += (f" `, {starting_cap_fiat}`.\n" ) if result['starting_capital_fiat'] > 0 else '.\n' total_dust_balance = 0 diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 4d00095e4..640f9305c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -770,7 +770,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert 'No closed trade' in msg_mock.call_args_list[-1][0][0] assert '*ROI:* All trades' in msg_mock.call_args_list[-1][0][0] mocker.patch('freqtrade.wallets.Wallets.get_starting_balance', return_value=0.01) - assert ('∙ `-0.00000500 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`' + assert ('∙ `-0.000005 BTC (-0.50%) (-0.0 \N{GREEK CAPITAL LETTER SIGMA}%)`' in msg_mock.call_args_list[-1][0][0]) msg_mock.reset_mock() @@ -845,7 +845,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick assert '*XRP:*' not in result assert 'Balance:' in result assert 'Est. BTC:' in result - assert 'BTC: 12.00000000' in result + assert 'BTC: 12' in result assert "*3 Other Currencies (< 0.0001 BTC):*" in result assert 'BTC: 0.00000309' in result @@ -874,7 +874,7 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 assert "*Warning:* Simulated balances in Dry Mode." in result - assert "Starting capital: `1000` BTC" in result + assert "Starting capital: `1000 BTC`" in result def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: From c13eed2178819dfde379a803f13afca81869831d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Feb 2022 19:15:55 +0100 Subject: [PATCH 131/154] use Order object to update trade --- freqtrade/freqtradebot.py | 9 ++++--- freqtrade/persistence/models.py | 47 ++++++++++++++++++++------------- 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5f2b72e1e..9166f43e9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.enums import RPCMessageType, RunMode, SellType, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, - InvalidOrderException, PricingError) + InvalidOrderException, OperationalException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin @@ -1358,8 +1358,11 @@ class FreqtradeBot(LoggingMixin): return True order = self.handle_order_fee(trade, order) - - trade.update(order) + order_obj = trade.select_order_by_order_id(order['id']) + if not order_obj: + # TODO: this can't happen! + raise OperationalException("order-obj not found!") + trade.update(order_obj) trade.recalc_trade_from_orders() Trade.commit() diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b8ea5848f..e6daa08ba 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -113,14 +113,15 @@ class Order(_DECL_BASE): trade = relationship("Trade", back_populates="orders") - ft_order_side = Column(String(25), nullable=False) - ft_pair = Column(String(25), nullable=False) + # order_side can only be 'buy', 'sell' or 'stoploss' + ft_order_side: str = Column(String(25), nullable=False) + ft_pair: str = Column(String(25), nullable=False) ft_is_open = Column(Boolean, nullable=False, default=True, index=True) order_id = Column(String(255), nullable=False, index=True) status = Column(String(255), nullable=True) symbol = Column(String(25), nullable=True) - order_type = Column(String(50), nullable=True) + order_type: str = Column(String(50), nullable=True) side = Column(String(25), nullable=True) price = Column(Float, nullable=True) average = Column(Float, nullable=True) @@ -133,9 +134,18 @@ class Order(_DECL_BASE): order_update_date = Column(DateTime, nullable=True) @property - def order_date_utc(self): + def order_date_utc(self) -> datetime: + """ Order-date with UTC timezoneinfo""" return self.order_date.replace(tzinfo=timezone.utc) + @property + def safe_price(self) -> float: + return self.average or self.price + + @property + def safe_filled(self) -> float: + return self.filled or self.amount + def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' @@ -452,40 +462,39 @@ class LocalTrade(): f"Trailing stoploss saved us: " f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") - def update(self, order: Dict) -> None: + def update(self, order: Order) -> None: """ Updates this entity with amount and actual open/close rates. :param order: order retrieved by exchange.fetch_order() :return: None """ - order_type = order['type'] # Ignore open and cancelled orders - if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None: + if order.status == 'open' or safe_value_fallback(order, 'average', 'price') is None: return - logger.info('Updating trade (id=%s) ...', self.id) + logger.info(f'Updating trade (id={self.id}) ...') - if order_type in ('market', 'limit') and order['side'] == 'buy': + if order.ft_order_side == 'buy': # Update open rate and actual amount - self.open_rate = float(safe_value_fallback(order, 'average', 'price')) - self.amount = float(safe_value_fallback(order, 'filled', 'amount')) + self.open_rate = order.safe_price + self.amount = order.safe_filled if self.is_open: - logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') + logger.info(f'{order.order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None self.recalc_trade_from_orders() - elif order_type in ('market', 'limit') and order['side'] == 'sell': + elif order.ft_order_side == 'sell': if self.is_open: - logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') - self.close(safe_value_fallback(order, 'average', 'price')) - elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): + logger.info(f'{order.order_type.upper()}_SELL has been fulfilled for {self}.') + self.close(order.safe_price) + elif order.ft_order_side == 'stoploss': self.stoploss_order_id = None self.close_rate_requested = self.stop_loss self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value if self.is_open: - logger.info(f'{order_type.upper()} is hit for {self}.') - self.close(safe_value_fallback(order, 'average', 'price')) + logger.info(f'{order.order_type.upper()} is hit for {self}.') + self.close(order.safe_price) else: - raise ValueError(f'Unknown order type: {order_type}') + raise ValueError(f'Unknown order type: {order.order_type}') Trade.commit() def close(self, rate: float, *, show_msg: bool = True) -> None: From 1b1216fc87b49050ab81e64f41ee346cbcaee04b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Feb 2022 19:18:19 +0100 Subject: [PATCH 132/154] Rename update_trade method --- freqtrade/freqtradebot.py | 3 ++- freqtrade/persistence/models.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9166f43e9..b8dfc6b08 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1362,7 +1362,8 @@ class FreqtradeBot(LoggingMixin): if not order_obj: # TODO: this can't happen! raise OperationalException("order-obj not found!") - trade.update(order_obj) + trade.update_trade(order_obj) + # TODO: is the below necessary? it's already done in update_trade for filled buys trade.recalc_trade_from_orders() Trade.commit() diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e6daa08ba..52aea387f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -151,7 +151,7 @@ class Order(_DECL_BASE): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' f'side={self.side}, order_type={self.order_type}, status={self.status})') - def update_from_ccxt_object(self, order): + def update_from_ccxt_object(self, order) -> 'Order': """ Update Order from ccxt response Only updates if fields are available from ccxt - @@ -178,6 +178,7 @@ class Order(_DECL_BASE): if (order.get('filled', 0.0) or 0.0) > 0: self.order_filled_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc) + return self def to_json(self) -> Dict[str, Any]: return { @@ -462,14 +463,14 @@ class LocalTrade(): f"Trailing stoploss saved us: " f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") - def update(self, order: Order) -> None: + def update_trade(self, order: Order) -> None: """ Updates this entity with amount and actual open/close rates. :param order: order retrieved by exchange.fetch_order() :return: None """ # Ignore open and cancelled orders - if order.status == 'open' or safe_value_fallback(order, 'average', 'price') is None: + if order.status == 'open' or order.safe_price is None: return logger.info(f'Updating trade (id={self.id}) ...') From 508e677d70d8f4fd5825d94055c32280fecb9ffd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Feb 2022 20:40:50 +0100 Subject: [PATCH 133/154] Fix some tests to call update_trade with order object --- freqtrade/persistence/models.py | 1 - tests/conftest.py | 2 +- tests/test_freqtradebot.py | 39 +++++++++++++++++-------- tests/test_persistence.py | 50 ++++++++++++++++++++++----------- 4 files changed, 62 insertions(+), 30 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 52aea387f..731d57262 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -16,7 +16,6 @@ from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.enums import SellType from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate diff --git a/tests/conftest.py b/tests/conftest.py index 630223d55..043cc6fcf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2208,7 +2208,7 @@ def limit_sell_order_usdt_open(): 'id': 'mocked_limit_sell_usdt', 'type': 'limit', 'side': 'sell', - 'pair': 'mocked', + 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'timestamp': arrow.utcnow().int_timestamp, 'price': 2.20, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 08d98b42d..50d060a39 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -227,7 +227,8 @@ def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker, freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) ############################################# # stoploss shoud be hit @@ -292,7 +293,8 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, fee, assert trade.exchange == 'binance' # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.open_rate == 2.0 assert trade.amount == 30.0 @@ -1803,7 +1805,8 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_ assert trade time.sleep(0.01) # Race condition fix - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) assert trade.is_open is True freqtrade.wallets.update() @@ -1812,7 +1815,9 @@ def test_handle_trade(default_conf_usdt, limit_buy_order_usdt, limit_sell_order_ assert trade.open_order_id == limit_sell_order_usdt['id'] # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object( + limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell') + trade.update_trade(oobj) assert trade.close_rate == 2.2 assert trade.close_profit == 0.09451372 @@ -1962,8 +1967,11 @@ def test_close_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, trade = Trade.query.first() assert trade - trade.update(limit_buy_order_usdt) - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) + oobj = Order.parse_from_ccxt_object( + limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell') + trade.update_trade(oobj) assert trade.is_open is False with pytest.raises(DependencyException, match=r'.*closed trade.*'): @@ -3103,7 +3111,8 @@ def test_sell_profit_only( freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) freqtrade.wallets.update() patch_get_signal(freqtrade, value=(False, True, None, None)) assert freqtrade.handle_trade(trade) is handle_first @@ -3139,7 +3148,9 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_ trade = Trade.query.first() amnt = trade.amount - trade.update(limit_buy_order_usdt) + + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) patch_get_signal(freqtrade, value=(False, True, None, None)) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985)) @@ -3247,7 +3258,8 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt, freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) freqtrade.wallets.update() patch_get_signal(freqtrade, value=(True, True, None, None)) assert freqtrade.handle_trade(trade) is False @@ -3437,7 +3449,8 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) # Sell due to min_roi_reached patch_get_signal(freqtrade, value=(True, False, None, None)) assert freqtrade.handle_trade(trade) is True @@ -3812,7 +3825,8 @@ def test_order_book_depth_of_market( assert len(Trade.query.all()) == 1 # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.open_rate == 2.0 assert whitelist == default_conf_usdt['exchange']['pair_whitelist'] @@ -3906,7 +3920,8 @@ def test_order_book_ask_strategy( assert trade time.sleep(0.01) # Race condition fix - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) freqtrade.wallets.update() assert trade.is_open is True diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b8f7a3336..3fa47daa9 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -108,7 +108,8 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca assert trade.close_date is None trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.open_order_id is None assert trade.open_rate == 2.00 assert trade.close_profit is None @@ -119,7 +120,8 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca caplog.clear() trade.open_order_id = 'something' - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert trade.open_order_id is None assert trade.close_rate == 2.20 assert trade.close_profit == round(0.0945137157107232, 8) @@ -146,7 +148,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, ) trade.open_order_id = 'something' - trade.update(market_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(market_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.open_order_id is None assert trade.open_rate == 2.0 assert trade.close_profit is None @@ -158,7 +161,8 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, caplog.clear() trade.is_open = True trade.open_order_id = 'something' - trade.update(market_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(market_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert trade.open_order_id is None assert trade.close_rate == 2.2 assert trade.close_profit == round(0.0945137157107232, 8) @@ -181,9 +185,11 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt ) trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade._calc_open_trade_value() == 60.15 - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert isclose(trade.calc_close_trade_value(), 65.835) # Profit in USDT @@ -236,7 +242,8 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee): ) trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.calc_close_trade_value() == 0.0 @@ -257,7 +264,8 @@ def test_update_open_order(limit_buy_order_usdt): assert trade.close_date is None limit_buy_order_usdt['status'] = 'open' - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) assert trade.open_order_id is None assert trade.close_profit is None @@ -276,8 +284,9 @@ def test_update_invalid_order(limit_buy_order_usdt): exchange='binance', ) limit_buy_order_usdt['type'] = 'invalid' + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'meep') with pytest.raises(ValueError, match=r'Unknown order type'): - trade.update(limit_buy_order_usdt) + trade.update_trade(oobj) @pytest.mark.usefixtures("init_persistence") @@ -304,7 +313,8 @@ def test_calc_open_trade_value(limit_buy_order_usdt, fee): exchange='binance', ) trade.open_order_id = 'open_trade' - trade.update(limit_buy_order_usdt) # Buy @ 2.0 + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) # Buy @ 2.0 # Get the open rate price with the standard fee rate assert trade._calc_open_trade_value() == 60.15 @@ -325,14 +335,16 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee exchange='binance', ) trade.open_order_id = 'close_trade' - trade.update(limit_buy_order_usdt) # Buy @ 2.0 + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) # Buy @ 2.0 # Get the close rate price with a custom close rate and a regular fee rate assert trade.calc_close_trade_value(rate=2.5) == 74.8125 # Get the close rate price with a custom close rate and a custom fee rate assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 74.775 # Test when we apply a Sell order, and ask price with a custom fee rate - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert trade.calc_close_trade_value(fee=0.005) == 65.67 @@ -409,7 +421,9 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): exchange='binance', ) trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) # Buy @ 2.0 + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + + trade.update_trade(oobj) # Buy @ 2.0 # Custom closing rate and regular fee rate # Higher than open rate - 2.1 quote @@ -424,7 +438,8 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): assert trade.calc_profit(rate=1.9, fee=0.003) == round(-3.320999999999998, 8) # Test when we apply a Sell order. Sell higher than open rate @ 2.2 - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert trade.calc_profit() == round(5.684999999999995, 8) # Test with a custom fee rate on the close trade @@ -443,7 +458,9 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee): exchange='binance' ) trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) # Buy @ 2.0 + + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, 'ADA/USDT', 'buy') + trade.update_trade(oobj) # Buy @ 2.0 # Higher than open rate - 2.1 quote assert trade.calc_profit_ratio(rate=2.1) == round(0.04476309226932673, 8) @@ -457,7 +474,8 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee): assert trade.calc_profit_ratio(rate=1.9, fee=0.003) == round(-0.05521197007481293, 8) # Test when we apply a Sell order. Sell higher than open rate @ 2.2 - trade.update(limit_sell_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_sell_order_usdt, 'ADA/USDT', 'sell') + trade.update_trade(oobj) assert trade.calc_profit_ratio() == round(0.0945137157107232, 8) # Test with a custom fee rate on the close trade From 874c161f78ccda10c76eac9c8d092e10d8c96ce8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Feb 2022 07:33:46 +0100 Subject: [PATCH 134/154] Update more tests to use order_obj to update trade --- freqtrade/freqtradebot.py | 2 +- tests/conftest.py | 2 +- tests/rpc/test_rpc.py | 53 +++++++++++++++--------- tests/rpc/test_rpc_telegram.py | 74 +++++++++++++++++++++------------- tests/test_freqtradebot.py | 47 +++++++++++++++------ 5 files changed, 117 insertions(+), 61 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b8dfc6b08..06fcb8a9a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1361,7 +1361,7 @@ class FreqtradeBot(LoggingMixin): order_obj = trade.select_order_by_order_id(order['id']) if not order_obj: # TODO: this can't happen! - raise OperationalException("order-obj not found!") + raise OperationalException(f"order-obj for {order['id']} not found!") trade.update_trade(order_obj) # TODO: is the below necessary? it's already done in update_trade for filled buys trade.recalc_trade_from_orders() diff --git a/tests/conftest.py b/tests/conftest.py index 043cc6fcf..a7f23ea76 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1221,7 +1221,7 @@ def limit_sell_order_open(): 'id': 'mocked_limit_sell', 'type': 'limit', 'side': 'sell', - 'pair': 'mocked', + 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), 'timestamp': arrow.utcnow().int_timestamp, 'price': 0.00001173, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 1c713ee86..e7b09ab74 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -11,6 +11,7 @@ from freqtrade.edge import PairInfo from freqtrade.enums import State from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade +from freqtrade.persistence.models import Order from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -277,8 +278,10 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, assert trade # Simulate buy & sell - trade.update(limit_buy_order) - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -415,28 +418,32 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, freqtradebot.enter_positions() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'sell') + trade.update_trade(oobj) # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_sell_up ) - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False freqtradebot.enter_positions() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_sell_up ) - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -495,14 +502,16 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, freqtradebot.enter_positions() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_sell_up, get_fee=fee ) - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -754,13 +763,13 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: mocker.patch( 'freqtrade.exchange.Exchange.fetch_order', side_effect=[{ - 'id': '1234', + 'id': trade.orders[0].order_id, 'status': 'open', 'type': 'limit', 'side': 'buy', 'filled': filled_amount }, { - 'id': '1234', + 'id': trade.orders[0].order_id, 'status': 'closed', 'type': 'limit', 'side': 'buy', @@ -840,10 +849,12 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -874,10 +885,12 @@ def test_buy_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -946,10 +959,12 @@ def test_sell_reason_performance_handle(default_conf, ticker, limit_buy_order, f assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -1018,10 +1033,12 @@ def test_mix_tag_performance_handle(default_conf, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 640f9305c..44107db81 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -418,10 +418,12 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -461,8 +463,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, trades = Trade.query.all() for trade in trades: - trade.update(limit_buy_order) - trade.update(limit_sell_order) + trade.update_trade(oobj) + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -527,10 +529,12 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -574,8 +578,8 @@ def test_weekly_handle(default_conf, update, ticker, limit_buy_order, fee, trades = Trade.query.all() for trade in trades: - trade.update(limit_buy_order) - trade.update(limit_sell_order) + trade.update_trade(oobj) + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -643,10 +647,12 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobjs = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -690,8 +696,8 @@ def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, trades = Trade.query.all() for trade in trades: - trade.update(limit_buy_order) - trade.update(limit_sell_order) + trade.update_trade(oobj) + trade.update_trade(oobjs) trade.close_date = datetime.utcnow() trade.is_open = False @@ -761,7 +767,9 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) + context = MagicMock() # Test with invalid 2nd argument (should silently pass) context.args = ["aaa"] @@ -776,7 +784,9 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, # Update the ticker with a market going up mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) - trade.update(limit_sell_order) + # Simulate fulfilled LIMIT_SELL order for trade + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.now(timezone.utc) trade.is_open = False @@ -1286,10 +1296,12 @@ def test_telegram_performance_handle(default_conf, update, ticker, fee, assert trade # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -1313,13 +1325,15 @@ def test_telegram_buy_tag_performance_handle(default_conf, update, ticker, fee, freqtradebot.enter_positions() trade = Trade.query.first() assert trade + trade.buy_tag = "TESTBUY" # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) - trade.buy_tag = "TESTBUY" # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -1356,13 +1370,14 @@ def test_telegram_sell_reason_performance_handle(default_conf, update, ticker, f freqtradebot.enter_positions() trade = Trade.query.first() assert trade - - # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) - trade.sell_reason = 'TESTSELL' + # Simulate fulfilled LIMIT_BUY order for trade + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) + # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False @@ -1399,15 +1414,16 @@ def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee, freqtradebot.enter_positions() trade = Trade.query.first() assert trade - - # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) - trade.buy_tag = "TESTBUY" trade.sell_reason = "TESTSELL" + # Simulate fulfilled LIMIT_BUY order for trade + oobj = Order.parse_from_ccxt_object(limit_buy_order, limit_buy_order['symbol'], 'buy') + trade.update_trade(oobj) + # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + oobj = Order.parse_from_ccxt_object(limit_sell_order, limit_sell_order['symbol'], 'sell') + trade.update_trade(oobj) trade.close_date = datetime.utcnow() trade.is_open = False diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 50d060a39..79f297bff 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -984,11 +984,17 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, trade = Trade.query.first() trade.is_open = True trade.open_order_id = None - trade.stoploss_order_id = 100 + trade.stoploss_order_id = "100" + trade.orders.append(Order( + ft_order_side='stoploss', + order_id='100', + ft_pair=trade.pair, + ft_is_open=True, + )) assert trade stoploss_order_hit = MagicMock(return_value={ - 'id': 100, + 'id': "100", 'status': 'closed', 'type': 'stop_loss_limit', 'price': 3, @@ -1634,9 +1640,9 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order_usdt['amount']) - + order_id = limit_buy_order_usdt['id'] trade = Trade( - open_order_id=123, + open_order_id=order_id, fee_open=0.001, fee_close=0.001, open_rate=0.01, @@ -1644,29 +1650,35 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap amount=11, exchange="binance", ) + trade.orders.append(Order( + ft_order_side='buy', + price=0.01, + order_id=order_id, + + )) assert not freqtrade.update_trade_state(trade, None) assert log_has_re(r'Orderid for trade .* is empty.', caplog) caplog.clear() # Add datetime explicitly since sqlalchemy defaults apply only once written to database - freqtrade.update_trade_state(trade, '123') + freqtrade.update_trade_state(trade, order_id) # Test amount not modified by fee-logic assert not log_has_re(r'Applying fee to .*', caplog) caplog.clear() assert trade.open_order_id is None assert trade.amount == limit_buy_order_usdt['amount'] - trade.open_order_id = '123' + trade.open_order_id = order_id mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) assert trade.amount != 90.81 # test amount modified by fee-logic - freqtrade.update_trade_state(trade, '123') + freqtrade.update_trade_state(trade, order_id) assert trade.amount == 90.81 assert trade.open_order_id is None trade.is_open = True trade.open_order_id = None # Assert we call handle_trade() if trade is feasible for execution - freqtrade.update_trade_state(trade, '123') + freqtrade.update_trade_state(trade, order_id) assert log_has_re('Found open order for.*', caplog) limit_buy_order_usdt_new = deepcopy(limit_buy_order_usdt) @@ -1675,7 +1687,7 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=ValueError) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt_new) - res = freqtrade.update_trade_state(trade, '123') + res = freqtrade.update_trade_state(trade, order_id) # Cancelled empty assert res is True @@ -1687,6 +1699,8 @@ def test_update_trade_state(mocker, default_conf_usdt, limit_buy_order_usdt, cap def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, limit_buy_order_usdt, fee, mocker, initial_amount, has_rounding_fee, caplog): trades_for_order[0]['amount'] = initial_amount + order_id = "oid_123456" + limit_buy_order_usdt['id'] = order_id mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) @@ -1702,10 +1716,18 @@ def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, l open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, - open_order_id="123456", + open_order_id=order_id, is_open=True, ) - freqtrade.update_trade_state(trade, '123456', limit_buy_order_usdt) + trade.orders.append( + Order( + ft_order_side='buy', + ft_pair=trade.pair, + ft_is_open=True, + order_id=order_id, + ) + ) + freqtrade.update_trade_state(trade, order_id, limit_buy_order_usdt) assert trade.amount != amount assert trade.amount == limit_buy_order_usdt['amount'] if has_rounding_fee: @@ -3362,7 +3384,8 @@ def test_trailing_stop_loss_positive( freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order_usdt) + oobj = Order.parse_from_ccxt_object(limit_buy_order_usdt, limit_buy_order_usdt['symbol'], 'buy') + trade.update_trade(oobj) caplog.set_level(logging.DEBUG) # stop-loss not reached assert freqtrade.handle_trade(trade) is False From e9f451406c4209ee7a8903af97aeda5b62b0b132 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Feb 2022 07:43:45 +0100 Subject: [PATCH 135/154] Use correct order id --- freqtrade/freqtradebot.py | 4 ++-- tests/test_freqtradebot.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 06fcb8a9a..f2b076532 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1358,10 +1358,10 @@ class FreqtradeBot(LoggingMixin): return True order = self.handle_order_fee(trade, order) - order_obj = trade.select_order_by_order_id(order['id']) + order_obj = trade.select_order_by_order_id(order_id) if not order_obj: # TODO: this can't happen! - raise OperationalException(f"order-obj for {order['id']} not found!") + raise OperationalException(f"order-obj for {order_id} not found!") trade.update_trade(order_obj) # TODO: is the below necessary? it's already done in update_trade for filled buys trade.recalc_trade_from_orders() diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 79f297bff..3effce2f5 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1786,7 +1786,7 @@ def test_update_trade_state_sell(default_conf_usdt, trades_for_order, limit_sell fee_open=0.0025, fee_close=0.0025, open_date=arrow.utcnow().datetime, - open_order_id="123456", + open_order_id=limit_sell_order_usdt_open['id'], is_open=True, ) order = Order.parse_from_ccxt_object(limit_sell_order_usdt_open, 'LTC/ETH', 'sell') @@ -2016,7 +2016,7 @@ def test_bot_loop_start_called_once(mocker, default_conf_usdt, caplog): def test_check_handle_timedout_buy_usercustom(default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade, fee, mocker) -> None: default_conf_usdt["unfilledtimeout"] = {"buy": 1400, "sell": 30} - + limit_buy_order_old['id'] = open_trade.open_order_id rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock(return_value=limit_buy_order_old) cancel_buy_order = deepcopy(limit_buy_order_old) From db540dc99078fce9ca48652f472ca43fba8cf0ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Feb 2022 14:21:22 +0100 Subject: [PATCH 136/154] Orders should also store fee if in receiving currency --- freqtrade/freqtradebot.py | 10 ++++++---- freqtrade/persistence/models.py | 16 +++++++++++++--- tests/test_freqtradebot.py | 10 ++++++++-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f2b076532..e44193f21 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1357,8 +1357,8 @@ class FreqtradeBot(LoggingMixin): # Handling of this will happen in check_handle_timedout. return True - order = self.handle_order_fee(trade, order) order_obj = trade.select_order_by_order_id(order_id) + order = self.handle_order_fee(trade, order_obj, order) if not order_obj: # TODO: this can't happen! raise OperationalException(f"order-obj for {order_id} not found!") @@ -1415,14 +1415,16 @@ class FreqtradeBot(LoggingMixin): return real_amount return amount - def handle_order_fee(self, trade: Trade, order: Dict[str, Any]) -> Dict[str, Any]: + def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> Dict[str, Any]: # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order) if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, abs_tol=constants.MATH_CLOSE_PREC): - order['amount'] = new_amount - order.pop('filled', None) + # TODO: ?? + # order['amount'] = new_amount + order_obj.ft_fee_base = trade.amount - new_amount + # order.pop('filled', None) except DependencyException as exception: logger.warning("Could not update trade amount: %s", exception) return order diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 731d57262..0ee50d1c7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,6 +132,8 @@ class Order(_DECL_BASE): order_filled_date = Column(DateTime, nullable=True) order_update_date = Column(DateTime, nullable=True) + ft_fee_base = Column(Float, nullable=True) + @property def order_date_utc(self) -> datetime: """ Order-date with UTC timezoneinfo""" @@ -143,7 +145,15 @@ class Order(_DECL_BASE): @property def safe_filled(self) -> float: - return self.filled or self.amount + return self.filled or self.amount or 0.0 + + @property + def safe_fee_base(self) -> float: + return self.ft_fee_base or 0.0 + + @property + def safe_amount_after_fee(self) -> float: + return self.safe_filled - self.safe_fee_base def __repr__(self): @@ -477,7 +487,7 @@ class LocalTrade(): if order.ft_order_side == 'buy': # Update open rate and actual amount self.open_rate = order.safe_price - self.amount = order.safe_filled + self.amount = order.safe_amount_after_fee if self.is_open: logger.info(f'{order.order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None @@ -637,7 +647,7 @@ class LocalTrade(): (o.status not in NON_OPEN_EXCHANGE_STATES)): continue - tmp_amount = o.amount + tmp_amount = o.safe_amount_after_fee tmp_price = o.average or o.price if o.filled is not None: tmp_amount = o.filled diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3effce2f5..735a95231 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1729,9 +1729,14 @@ def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, l ) freqtrade.update_trade_state(trade, order_id, limit_buy_order_usdt) assert trade.amount != amount - assert trade.amount == limit_buy_order_usdt['amount'] + log_text = r'Applying fee on amount for .*' if has_rounding_fee: - assert log_has_re(r'Applying fee on amount for .*', caplog) + assert pytest.approx(trade.amount) == 29.992 + assert log_has_re(log_text, caplog) + else: + assert pytest.approx(trade.amount) == limit_buy_order_usdt['amount'] + assert not log_has_re(log_text, caplog) + def test_update_trade_state_exception(mocker, default_conf_usdt, @@ -2319,6 +2324,7 @@ def test_check_handle_timedout_partial_fee(default_conf_usdt, ticker_usdt, open_ limit_buy_order_old_partial_canceled, mocker) -> None: rpc_mock = patch_RPCManager(mocker) limit_buy_order_old_partial['id'] = open_trade.open_order_id + limit_buy_order_old_partial_canceled['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) From dc7bcf5dda0c94e6cab7e7943c8475522de9c462 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Feb 2022 14:27:26 +0100 Subject: [PATCH 137/154] Update failing test --- freqtrade/freqtradebot.py | 3 ++- freqtrade/persistence/models.py | 1 - tests/test_freqtradebot.py | 1 - tests/test_integration.py | 7 ++++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e44193f21..8007d520e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1415,7 +1415,8 @@ class FreqtradeBot(LoggingMixin): return real_amount return amount - def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> Dict[str, Any]: + def handle_order_fee( + self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> Dict[str, Any]: # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 0ee50d1c7..49ce34158 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -187,7 +187,6 @@ class Order(_DECL_BASE): if (order.get('filled', 0.0) or 0.0) > 0: self.order_filled_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc) - return self def to_json(self) -> Dict[str, Any]: return { diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 735a95231..d7b47174b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1738,7 +1738,6 @@ def test_update_trade_state_withorderdict(default_conf_usdt, trades_for_order, l assert not log_has_re(log_text, caplog) - def test_update_trade_state_exception(mocker, default_conf_usdt, limit_buy_order_usdt, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) diff --git a/tests/test_integration.py b/tests/test_integration.py index ed38f1fec..db3b1b5fc 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -4,6 +4,7 @@ import pytest from freqtrade.enums import SellType from freqtrade.persistence import Trade +from freqtrade.persistence.models import Order from freqtrade.rpc.rpc import RPC from freqtrade.strategy.interface import SellCheckTuple from tests.conftest import get_patched_freqtradebot, patch_get_signal @@ -94,7 +95,11 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, trades = Trade.query.all() # Make sure stoploss-order is open and trade is bought (since we mock update_trade_state) for trade in trades: - trade.stoploss_order_id = 3 + stoploss_order_closed['id'] = '3' + oobj = Order.parse_from_ccxt_object(stoploss_order_closed, trade.pair, 'stoploss') + + trade.orders.append(oobj) + trade.stoploss_order_id = '3' trade.open_order_id = None n = freqtrade.exit_positions(trades) From 6fb5b22a8e6acd41fb1a9eeb8fc2e6a4fd7d4bd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Feb 2022 15:36:25 +0100 Subject: [PATCH 138/154] Some cleanup --- freqtrade/freqtradebot.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8007d520e..e43f2a158 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.enums import RPCMessageType, RunMode, SellType, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, - InvalidOrderException, OperationalException, PricingError) + InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin @@ -1358,10 +1358,8 @@ class FreqtradeBot(LoggingMixin): return True order_obj = trade.select_order_by_order_id(order_id) - order = self.handle_order_fee(trade, order_obj, order) - if not order_obj: - # TODO: this can't happen! - raise OperationalException(f"order-obj for {order_id} not found!") + self.handle_order_fee(trade, order_obj, order) + trade.update_trade(order_obj) # TODO: is the below necessary? it's already done in update_trade for filled buys trade.recalc_trade_from_orders() @@ -1415,20 +1413,15 @@ class FreqtradeBot(LoggingMixin): return real_amount return amount - def handle_order_fee( - self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> Dict[str, Any]: + def handle_order_fee(self, trade: Trade, order_obj: Order, order: Dict[str, Any]) -> None: # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order) if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, abs_tol=constants.MATH_CLOSE_PREC): - # TODO: ?? - # order['amount'] = new_amount order_obj.ft_fee_base = trade.amount - new_amount - # order.pop('filled', None) except DependencyException as exception: logger.warning("Could not update trade amount: %s", exception) - return order def get_real_amount(self, trade: Trade, order: Dict) -> float: """ From a24586cd41211b8dc99a9f70d3bc8d1c71a86d93 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Feb 2022 16:32:04 +0100 Subject: [PATCH 139/154] Update migrations for new column --- freqtrade/persistence/migrations.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 60c0eb5f9..288345e18 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -57,7 +57,7 @@ def set_sequence_ids(engine, order_id, trade_id): def migrate_trades_and_orders_table( decl_base, inspector, engine, trade_back_name: str, cols: List, - order_back_name: str): + order_back_name: str, cols_order: List): 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') @@ -141,7 +141,7 @@ def migrate_trades_and_orders_table( from {trade_back_name} """)) - migrate_orders_table(engine, order_back_name, cols) + migrate_orders_table(engine, order_back_name, cols_order) set_sequence_ids(engine, order_id, trade_id) @@ -171,17 +171,19 @@ def drop_orders_table(engine, table_back_name: str): connection.execute(text("drop table orders")) -def migrate_orders_table(engine, table_back_name: str, cols: List): +def migrate_orders_table(engine, table_back_name: str, cols_order: List): + + ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null') # let SQLAlchemy create the schema as required 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, status, symbol, order_type, side, price, amount, filled, average, remaining, cost, - order_date, order_filled_date, order_update_date) + order_date, order_filled_date, order_update_date, ft_fee_base) select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, null average, remaining, cost, - order_date, order_filled_date, order_update_date + order_date, order_filled_date, order_update_date, {ft_fee_base} from {table_back_name} """)) @@ -193,6 +195,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: inspector = inspect(engine) cols = inspector.get_columns('trades') + cols_orders = inspector.get_columns('orders') 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') @@ -200,11 +203,12 @@ 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'): + # if not has_column(cols, 'buy_tag'): + if 'orders' not in previous_tables or not has_column(cols_orders, 'ft_fee_base'): 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) + decl_base, inspector, engine, table_back_name, cols, order_table_bak_name, cols_orders) # Reread columns - the above recreated the table! inspector = inspect(engine) cols = inspector.get_columns('trades') From fddacfedaae87fcf3c0b5e4ac5bba76d5cd7d50b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Feb 2022 16:34:35 +0100 Subject: [PATCH 140/154] Remove returntype --- freqtrade/freqtradebot.py | 3 +++ freqtrade/persistence/migrations.py | 3 --- freqtrade/persistence/models.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e43f2a158..99872ff0b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1358,6 +1358,9 @@ class FreqtradeBot(LoggingMixin): return True order_obj = trade.select_order_by_order_id(order_id) + if not order_obj: + raise DependencyException( + f"Order_obj not found for {order_id}. This should not have happened.") self.handle_order_fee(trade, order_obj, order) trade.update_trade(order_obj) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 288345e18..e4ff4bc37 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -209,9 +209,6 @@ def check_migrate(engine, decl_base, previous_tables) -> None: 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, cols_orders) - # Reread columns - the above recreated the table! - inspector = inspect(engine) - cols = inspector.get_columns('trades') if 'orders' not in previous_tables and 'trades' in previous_tables: logger.info('Moving open orders to Orders table.') diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 49ce34158..d12a345e0 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -160,7 +160,7 @@ class Order(_DECL_BASE): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' f'side={self.side}, order_type={self.order_type}, status={self.status})') - def update_from_ccxt_object(self, order) -> 'Order': + def update_from_ccxt_object(self, order): """ Update Order from ccxt response Only updates if fields are available from ccxt - From 21b5f56f7d031a45b6ffb79ef4651c7cb02bbf81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 03:01:39 +0000 Subject: [PATCH 141/154] Bump types-requests from 2.27.9 to 2.27.10 Bumps [types-requests](https://github.com/python/typeshed) from 2.27.9 to 2.27.10. - [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 3a2fa71e0..c52032a60 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,7 +22,7 @@ nbconvert==6.4.2 # mypy types types-cachetools==4.2.9 types-filelock==3.2.5 -types-requests==2.27.9 +types-requests==2.27.10 types-tabulate==0.8.5 # Extensions to datetime library From d1cded3532e96c6e76703f4e73c8055cb3b0b4f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 03:01:45 +0000 Subject: [PATCH 142/154] Bump filelock from 3.4.2 to 3.6.0 Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.4.2 to 3.6.0. - [Release notes](https://github.com/tox-dev/py-filelock/releases) - [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/py-filelock/compare/3.4.2...3.6.0) --- updated-dependencies: - dependency-name: filelock 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 e4698918a..aeb7be035 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,6 +5,6 @@ scipy==1.8.0 scikit-learn==1.0.2 scikit-optimize==0.9.0 -filelock==3.4.2 +filelock==3.6.0 joblib==1.1.0 progressbar2==4.0.0 From dc8e9bab44c94c971bf8409082b54d95981298f8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 03:01:51 +0000 Subject: [PATCH 143/154] Bump fastapi from 0.73.0 to 0.74.0 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.73.0 to 0.74.0. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.73.0...0.74.0) --- updated-dependencies: - dependency-name: fastapi 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 eaf257ca8..5b4be4f37 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ python-rapidjson==1.5 sdnotify==0.3.2 # API Server -fastapi==0.73.0 +fastapi==0.74.0 uvicorn==0.17.4 pyjwt==2.3.0 aiofiles==0.8.0 From 317487fefc8cea707d1767466a032af40929ed72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 03:01:58 +0000 Subject: [PATCH 144/154] Bump ccxt from 1.72.98 to 1.73.70 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.72.98 to 1.73.70. - [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.72.98...1.73.70) --- 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 eaf257ca8..b396b10ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.2 pandas==1.4.1 pandas-ta==0.3.14b -ccxt==1.72.98 +ccxt==1.73.70 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 From d354f1f84c22addbba79a2f39290f91ab447b932 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 03:02:06 +0000 Subject: [PATCH 145/154] Bump mkdocs-material from 8.1.11 to 8.2.1 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.1.11 to 8.2.1. - [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.11...8.2.1) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-minor ... 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 d3772ebd7..3e7fa2044 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.3 -mkdocs-material==8.1.11 +mkdocs-material==8.2.1 mdx_truly_sane_lists==1.2 pymdown-extensions==9.2 From 7b6a0f7a19bb65ecb0d77a41fb808ed865f88404 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 05:28:52 +0000 Subject: [PATCH 146/154] Bump uvicorn from 0.17.4 to 0.17.5 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.17.4 to 0.17.5. - [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.4...0.17.5) --- 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 5b4be4f37..7fa7db899 100644 --- a/requirements.txt +++ b/requirements.txt @@ -32,7 +32,7 @@ sdnotify==0.3.2 # API Server fastapi==0.74.0 -uvicorn==0.17.4 +uvicorn==0.17.5 pyjwt==2.3.0 aiofiles==0.8.0 psutil==5.9.0 From b9a99bd0b73a25ec59f3bf696a406fe05b2b5386 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Feb 2022 05:28:53 +0000 Subject: [PATCH 147/154] Bump python-rapidjson from 1.5 to 1.6 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 1.5 to 1.6. - [Release notes](https://github.com/python-rapidjson/python-rapidjson/releases) - [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst) - [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v1.5...v1.6) --- updated-dependencies: - dependency-name: python-rapidjson 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 5b4be4f37..7f0c3cb6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ blosc==1.10.6 py_find_1st==1.1.5 # Load ticker files 30% faster -python-rapidjson==1.5 +python-rapidjson==1.6 # Notify systemd sdnotify==0.3.2 From 02ce0dc02ef85db0485af262f81bce768c89091a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Feb 2022 19:31:58 +0100 Subject: [PATCH 148/154] Set journal mode to wal for sqlite databases closes #6353 --- freqtrade/persistence/migrations.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 60c0eb5f9..5817c5a97 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -186,6 +186,13 @@ def migrate_orders_table(engine, table_back_name: str, cols: List): """)) +def set_sqlite_to_wal(engine): + if engine.name == 'sqlite' and str(engine.url) != 'sqlite://': + # Set Mode to + with engine.begin() as connection: + connection.execute(text("PRAGMA journal_mode=wal")) + + def check_migrate(engine, decl_base, previous_tables) -> None: """ Checks if migration is necessary and migrates if necessary @@ -212,3 +219,4 @@ 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) + set_sqlite_to_wal(engine) From 1f9ed0beff5256bbc881d4df2989381fb36f563f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Feb 2022 19:39:55 +0100 Subject: [PATCH 149/154] Add test for wal mode --- freqtrade/persistence/models.py | 3 +++ tests/test_persistence.py | 11 ++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b8ea5848f..5eddb7b3d 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -39,6 +39,9 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: """ kwargs = {} + if db_url == 'sqlite:///': + raise OperationalException( + f'Bad db-url {db_url}. For in-memory database, please use `sqlite://`.') if db_url == 'sqlite://': kwargs.update({ 'poolclass': StaticPool, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b8f7a3336..b2dccc75c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -33,13 +33,18 @@ def test_init_custom_db_url(default_conf, tmpdir): init_db(default_conf['db_url'], default_conf['dry_run']) assert Path(filename).is_file() + r = Trade._session.execute(text("PRAGMA journal_mode")) + assert r.first() == ('wal',) -def test_init_invalid_db_url(default_conf): +def test_init_invalid_db_url(): # Update path to a value other than default, but still in-memory - default_conf.update({'db_url': 'unknown:///some.url'}) with pytest.raises(OperationalException, match=r'.*no valid database URL*'): - init_db(default_conf['db_url'], default_conf['dry_run']) + init_db('unknown:///some.url', True) + + with pytest.raises(OperationalException, match=r'Bad db-url.*For in-memory database, pl.*'): + init_db('sqlite:///', True) + def test_init_prod_db(default_conf, mocker): From 5a4f30d1bda54af561a9fc089b9f4a7a1ee0181a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Feb 2022 20:07:41 +0100 Subject: [PATCH 150/154] Don't specially handle empty results. --- freqtrade/rpc/rpc.py | 5 ----- tests/rpc/test_rpc_telegram.py | 2 +- tests/test_persistence.py | 1 - 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5912a0ecd..7a602978e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -599,11 +599,6 @@ class RPC: 'est_stake': est_stake or 0, 'stake': stake_currency, }) - if total == 0.0: - if self._freqtrade.config['dry_run']: - raise RPCException('Running in Dry Run, balances are not available.') - else: - raise RPCException('All balances are zero.') value = self._fiat_converter.convert_amount( total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 640f9305c..353aa959f 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -861,7 +861,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert 'All balances are zero.' in result + assert 'Starting capital: `0 BTC' in result def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b2dccc75c..f695aab8b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -46,7 +46,6 @@ def test_init_invalid_db_url(): init_db('sqlite:///', True) - def test_init_prod_db(default_conf, mocker): default_conf.update({'dry_run': False}) default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL}) From 731eb99713b8a7803b8496a2a6c44a9d92b7df32 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Feb 2022 19:17:50 +0100 Subject: [PATCH 151/154] Update mock-trade creation to rollback first --- tests/conftest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 630223d55..8f1e75439 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -201,6 +201,9 @@ def create_mock_trades(fee, use_db: bool = True): """ Create some fake trades ... """ + if use_db: + Trade.query.session.rollback() + def add_trade(trade): if use_db: Trade.query.session.add(trade) From 42df65d4ec20196f1903e4ec4b7f9310f04ce39b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Feb 2022 06:25:21 +0100 Subject: [PATCH 152/154] Make sure backtesting is cleaned up in tests --- freqtrade/optimize/backtesting.py | 3 ++- tests/optimize/test_backtesting.py | 11 ++++++++--- tests/rpc/test_rpc_apiserver.py | 1 + 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b423771ca..eca643732 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -128,7 +128,8 @@ class Backtesting: def __del__(self): self.cleanup() - def cleanup(self): + @staticmethod + def cleanup(): LoggingMixin.show_output = True PairLocks.use_db = True Trade.use_db = True diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index d61dffac4..a8998eb63 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -52,6 +52,13 @@ def trim_dictlist(dict_list, num): return new +@pytest.fixture(autouse=True) +def backtesting_cleanup() -> None: + yield None + + Backtesting.cleanup() + + def load_data_test(what, testdatadir): timerange = TimeRange.parse_timerange('1510694220-1510700340') data = history.load_pair_history(pair='UNITTEST/BTC', datadir=testdatadir, @@ -553,8 +560,6 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: trade = backtesting._enter_trade(pair, row=row) assert trade is None - backtesting.cleanup() - def test_backtest__get_sell_trade_entry(default_conf, fee, mocker) -> None: default_conf['use_sell_signal'] = False @@ -1423,7 +1428,7 @@ def test_get_strategy_run_id(default_conf_usdt): default_conf_usdt.update({ 'strategy': 'StrategyTestV2', 'max_open_trades': float('inf') - }) + }) strategy = StrategyResolver.load_strategy(default_conf_usdt) x = get_strategy_run_id(strategy) assert isinstance(x, str) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5b19e5e05..544321860 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1108,6 +1108,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): data='{"tradeid": "1"}') assert_response(rc, 502) assert rc.json() == {"error": "Error querying /api/v1/forcesell: invalid argument"} + Trade.query.session.rollback() ftbot.enter_positions() From 3b1b66bee8a1bf3f58080a4ab74406e901834718 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Feb 2022 07:40:49 +0100 Subject: [PATCH 153/154] Prevent backtest starting when not in webserver mode #6455 --- freqtrade/rpc/api_server/api_backtest.py | 10 +++++----- freqtrade/rpc/api_server/deps.py | 7 +++++++ tests/rpc/test_rpc_apiserver.py | 5 +++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 8b86b8005..757ed8aac 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -8,7 +8,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc from freqtrade.enums import BacktestState from freqtrade.exceptions import DependencyException from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse -from freqtrade.rpc.api_server.deps import get_config +from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode from freqtrade.rpc.api_server.webserver import ApiServer from freqtrade.rpc.rpc import RPCException @@ -22,7 +22,7 @@ router = APIRouter() @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) # flake8: noqa: C901 async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks, - config=Depends(get_config)): + config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): """Start backtesting if not done so already""" if ApiServer._bgtask_running: raise RPCException('Bot Background task already running') @@ -121,7 +121,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac @router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) -def api_get_backtest(): +def api_get_backtest(ws_mode=Depends(is_webserver_mode)): """ Get backtesting result. Returns Result after backtesting has been ran. @@ -157,7 +157,7 @@ def api_get_backtest(): @router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) -def api_delete_backtest(): +def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): """Reset backtesting""" if ApiServer._bgtask_running: return { @@ -183,7 +183,7 @@ def api_delete_backtest(): @router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest']) -def api_backtest_abort(): +def api_backtest_abort(ws_mode=Depends(is_webserver_mode)): if not ApiServer._bgtask_running: return { "status": "not_running", diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index b428d9c6d..f5e61602e 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, Optional from fastapi import Depends +from freqtrade.enums import RunMode from freqtrade.persistence import Trade from freqtrade.rpc.rpc import RPC, RPCException @@ -38,3 +39,9 @@ def get_exchange(config=Depends(get_config)): ApiServer._exchange = ExchangeResolver.load_exchange( config['exchange']['name'], config) return ApiServer._exchange + + +def is_webserver_mode(config=Depends(get_config)): + if config['runmode'] != RunMode.WEBSERVER: + raise RPCException('Bot is not in the correct state') + return None diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 544321860..de7dca47b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1350,6 +1350,11 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): ftbot, client = botclient mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + rc = client_get(client, f"{BASE_URI}/backtest") + # Backtest prevented in default mode + assert_response(rc, 502) + + ftbot.config['runmode'] = RunMode.WEBSERVER # Backtesting not started yet rc = client_get(client, f"{BASE_URI}/backtest") assert_response(rc) From e88b022cd475d09d2a610b0575fd6a1e8f56f080 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Feb 2022 12:07:09 +0100 Subject: [PATCH 154/154] Version bump 2022.2 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 54cecbec2..e7d2dbf1b 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2022.1' +__version__ = '2022.2' if __version__ == 'develop':