diff --git a/freqtrade/persistence/base.py b/freqtrade/persistence/base.py index fc2dac75e..5f5c40dea 100644 --- a/freqtrade/persistence/base.py +++ b/freqtrade/persistence/base.py @@ -1,4 +1,3 @@ - from sqlalchemy.orm import DeclarativeBase, Session, scoped_session diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index 4d3bd5218..5b37a50eb 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -23,16 +23,17 @@ class _CustomData(ModelBase): - One trade can have many metadata entries - One metadata entry can only be associated with one Trade """ - __tablename__ = 'trade_custom_data' + + __tablename__ = "trade_custom_data" __allow_unmapped__ = True session: ClassVar[SessionType] # Uniqueness should be ensured over pair, order_id # its likely that order_id is unique per Pair on some exchanges. - __table_args__ = (UniqueConstraint('ft_trade_id', 'cd_key', name="_trade_id_cd_key"),) + __table_args__ = (UniqueConstraint("ft_trade_id", "cd_key", name="_trade_id_cd_key"),) id = mapped_column(Integer, primary_key=True) - ft_trade_id = mapped_column(Integer, ForeignKey('trades.id'), index=True) + ft_trade_id = mapped_column(Integer, ForeignKey("trades.id"), index=True) trade = relationship("Trade", back_populates="custom_data") @@ -46,17 +47,22 @@ class _CustomData(ModelBase): value: Any = None def __repr__(self): - create_time = (self.created_at.strftime(DATETIME_PRINT_FORMAT) - if self.created_at is not None else None) - update_time = (self.updated_at.strftime(DATETIME_PRINT_FORMAT) - if self.updated_at is not None else None) - return (f'CustomData(id={self.id}, key={self.cd_key}, type={self.cd_type}, ' + - f'value={self.cd_value}, trade_id={self.ft_trade_id}, created={create_time}, ' + - f'updated={update_time})') + create_time = ( + self.created_at.strftime(DATETIME_PRINT_FORMAT) if self.created_at is not None else None + ) + update_time = ( + self.updated_at.strftime(DATETIME_PRINT_FORMAT) if self.updated_at is not None else None + ) + return ( + f"CustomData(id={self.id}, key={self.cd_key}, type={self.cd_type}, " + + f"value={self.cd_value}, trade_id={self.ft_trade_id}, created={create_time}, " + + f"updated={update_time})" + ) @classmethod - def query_cd(cls, key: Optional[str] = None, - trade_id: Optional[int] = None) -> Sequence['_CustomData']: + def query_cd( + cls, key: Optional[str] = None, trade_id: Optional[int] = None + ) -> Sequence["_CustomData"]: """ Get all CustomData, if trade_id is not specified return will be for generic values not tied to a trade @@ -80,17 +86,17 @@ class CustomDataWrapper: use_db = True custom_data: List[_CustomData] = [] - unserialized_types = ['bool', 'float', 'int', 'str'] + unserialized_types = ["bool", "float", "int", "str"] @staticmethod def _convert_custom_data(data: _CustomData) -> _CustomData: if data.cd_type in CustomDataWrapper.unserialized_types: data.value = data.cd_value - if data.cd_type == 'bool': - data.value = data.cd_value.lower() == 'true' - elif data.cd_type == 'int': + if data.cd_type == "bool": + data.value = data.cd_value.lower() == "true" + elif data.cd_type == "int": data.value = int(data.cd_value) - elif data.cd_type == 'float': + elif data.cd_type == "float": data.value = float(data.cd_value) else: data.value = json.loads(data.cd_value) @@ -111,31 +117,32 @@ class CustomDataWrapper: @staticmethod def get_custom_data(*, trade_id: int, key: Optional[str] = None) -> List[_CustomData]: - if CustomDataWrapper.use_db: filters = [ _CustomData.ft_trade_id == trade_id, ] if key is not None: filters.append(_CustomData.cd_key.ilike(key)) - filtered_custom_data = _CustomData.session.scalars(select(_CustomData).filter( - *filters)).all() + filtered_custom_data = _CustomData.session.scalars( + select(_CustomData).filter(*filters) + ).all() else: filtered_custom_data = [ - data_entry for data_entry in CustomDataWrapper.custom_data + data_entry + for data_entry in CustomDataWrapper.custom_data if (data_entry.ft_trade_id == trade_id) ] if key is not None: filtered_custom_data = [ - data_entry for data_entry in filtered_custom_data + data_entry + for data_entry in filtered_custom_data if (data_entry.cd_key.casefold() == key.casefold()) ] return [CustomDataWrapper._convert_custom_data(d) for d in filtered_custom_data] @staticmethod def set_custom_data(trade_id: int, key: str, value: Any) -> None: - value_type = type(value).__name__ if value_type not in CustomDataWrapper.unserialized_types: diff --git a/freqtrade/persistence/key_value_store.py b/freqtrade/persistence/key_value_store.py index 6da7265d6..93960a102 100644 --- a/freqtrade/persistence/key_value_store.py +++ b/freqtrade/persistence/key_value_store.py @@ -12,22 +12,23 @@ ValueTypes = Union[str, datetime, float, int] class ValueTypesEnum(str, Enum): - STRING = 'str' - DATETIME = 'datetime' - FLOAT = 'float' - INT = 'int' + STRING = "str" + DATETIME = "datetime" + FLOAT = "float" + INT = "int" class KeyStoreKeys(str, Enum): - BOT_START_TIME = 'bot_start_time' - STARTUP_TIME = 'startup_time' + BOT_START_TIME = "bot_start_time" + STARTUP_TIME = "startup_time" class _KeyValueStoreModel(ModelBase): """ Pair Locks database model. """ - __tablename__ = 'KeyValueStore' + + __tablename__ = "KeyValueStore" session: ClassVar[SessionType] id: Mapped[int] = mapped_column(primary_key=True) @@ -56,8 +57,11 @@ class KeyValueStore: :param key: Key to store the value for - can be used in get-value to retrieve the key :param value: Value to store - can be str, datetime, float or int """ - kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( - _KeyValueStoreModel.key == key).first() + kv = ( + _KeyValueStoreModel.session.query(_KeyValueStoreModel) + .filter(_KeyValueStoreModel.key == key) + .first() + ) if kv is None: kv = _KeyValueStoreModel(key=key) if isinstance(value, str): @@ -73,7 +77,7 @@ class KeyValueStore: kv.value_type = ValueTypesEnum.INT kv.int_value = value else: - raise ValueError(f'Unknown value type {kv.value_type}') + raise ValueError(f"Unknown value type {kv.value_type}") _KeyValueStoreModel.session.add(kv) _KeyValueStoreModel.session.commit() @@ -83,8 +87,11 @@ class KeyValueStore: Delete the value for the given key. :param key: Key to delete the value for """ - kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( - _KeyValueStoreModel.key == key).first() + kv = ( + _KeyValueStoreModel.session.query(_KeyValueStoreModel) + .filter(_KeyValueStoreModel.key == key) + .first() + ) if kv is not None: _KeyValueStoreModel.session.delete(kv) _KeyValueStoreModel.session.commit() @@ -95,8 +102,11 @@ class KeyValueStore: Get the value for the given key. :param key: Key to get the value for """ - kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( - _KeyValueStoreModel.key == key).first() + kv = ( + _KeyValueStoreModel.session.query(_KeyValueStoreModel) + .filter(_KeyValueStoreModel.key == key) + .first() + ) if kv is None: return None if kv.value_type == ValueTypesEnum.STRING: @@ -108,7 +118,7 @@ class KeyValueStore: if kv.value_type == ValueTypesEnum.INT: return kv.int_value # This should never happen unless someone messed with the database manually - raise ValueError(f'Unknown value type {kv.value_type}') # pragma: no cover + raise ValueError(f"Unknown value type {kv.value_type}") # pragma: no cover @staticmethod def get_string_value(key: KeyStoreKeys) -> Optional[str]: @@ -116,9 +126,14 @@ class KeyValueStore: Get the value for the given key. :param key: Key to get the value for """ - kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( - _KeyValueStoreModel.key == key, - _KeyValueStoreModel.value_type == ValueTypesEnum.STRING).first() + kv = ( + _KeyValueStoreModel.session.query(_KeyValueStoreModel) + .filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.STRING, + ) + .first() + ) if kv is None: return None return kv.string_value @@ -129,9 +144,14 @@ class KeyValueStore: Get the value for the given key. :param key: Key to get the value for """ - kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( - _KeyValueStoreModel.key == key, - _KeyValueStoreModel.value_type == ValueTypesEnum.DATETIME).first() + kv = ( + _KeyValueStoreModel.session.query(_KeyValueStoreModel) + .filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.DATETIME, + ) + .first() + ) if kv is None or kv.datetime_value is None: return None return kv.datetime_value.replace(tzinfo=timezone.utc) @@ -142,9 +162,14 @@ class KeyValueStore: Get the value for the given key. :param key: Key to get the value for """ - kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( - _KeyValueStoreModel.key == key, - _KeyValueStoreModel.value_type == ValueTypesEnum.FLOAT).first() + kv = ( + _KeyValueStoreModel.session.query(_KeyValueStoreModel) + .filter( + _KeyValueStoreModel.key == key, + _KeyValueStoreModel.value_type == ValueTypesEnum.FLOAT, + ) + .first() + ) if kv is None: return None return kv.float_value @@ -155,9 +180,13 @@ class KeyValueStore: Get the value for the given key. :param key: Key to get the value for """ - kv = _KeyValueStoreModel.session.query(_KeyValueStoreModel).filter( - _KeyValueStoreModel.key == key, - _KeyValueStoreModel.value_type == ValueTypesEnum.INT).first() + kv = ( + _KeyValueStoreModel.session.query(_KeyValueStoreModel) + .filter( + _KeyValueStoreModel.key == key, _KeyValueStoreModel.value_type == ValueTypesEnum.INT + ) + .first() + ) if kv is None: return None return kv.int_value @@ -168,12 +197,13 @@ def set_startup_time(): sets bot_start_time to the first trade open date - or "now" on new databases. sets startup_time to "now" """ - st = KeyValueStore.get_value('bot_start_time') + st = KeyValueStore.get_value("bot_start_time") if st is None: from freqtrade.persistence import Trade + t = Trade.session.query(Trade).order_by(Trade.open_date.asc()).first() if t is not None: - KeyValueStore.store_value('bot_start_time', t.open_date_utc) + KeyValueStore.store_value("bot_start_time", t.open_date_utc) else: - KeyValueStore.store_value('bot_start_time', datetime.now(timezone.utc)) - KeyValueStore.store_value('startup_time', datetime.now(timezone.utc)) + KeyValueStore.store_value("bot_start_time", datetime.now(timezone.utc)) + KeyValueStore.store_value("startup_time", datetime.now(timezone.utc)) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index b07a05632..a2e4ec681 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -25,8 +25,8 @@ def get_column_def(columns: List, column: str, default: str) -> str: def get_backup_name(tabs: List[str], backup_prefix: str): table_back_name = backup_prefix for i, table_back_name in enumerate(tabs): - table_back_name = f'{backup_prefix}{i}' - logger.debug(f'trying {table_back_name}') + table_back_name = f"{backup_prefix}{i}" + logger.debug(f"trying {table_back_name}") return table_back_name @@ -35,21 +35,22 @@ def get_last_sequence_ids(engine, trade_back_name: str, order_back_name: str): order_id: Optional[int] = None trade_id: Optional[int] = None - if engine.name == 'postgresql': + if engine.name == "postgresql": with engine.begin() as connection: 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")) + 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, pairlock_id=None): - - if engine.name == 'postgresql': + if engine.name == "postgresql": with engine.begin() as connection: if order_id: connection.execute(text(f"ALTER SEQUENCE orders_id_seq RESTART WITH {order_id}")) @@ -57,84 +58,95 @@ def set_sequence_ids(engine, order_id, trade_id, pairlock_id=None): connection.execute(text(f"ALTER SEQUENCE trades_id_seq RESTART WITH {trade_id}")) if pairlock_id: connection.execute( - text(f"ALTER SEQUENCE pairlocks_id_seq RESTART WITH {pairlock_id}")) + text(f"ALTER SEQUENCE pairlocks_id_seq RESTART WITH {pairlock_id}") + ) def drop_index_on_table(engine, inspector, table_bak_name): with engine.begin() as connection: # drop indexes on backup table in new session for index in inspector.get_indexes(table_bak_name): - if engine.name == 'mysql': + if engine.name == "mysql": connection.execute(text(f"drop index {index['name']} on {table_bak_name}")) else: connection.execute(text(f"drop index {index['name']}")) def migrate_trades_and_orders_table( - decl_base, inspector, engine, - trade_back_name: str, cols: List, - order_back_name: str, cols_order: List): - base_currency = get_column_def(cols, 'base_currency', 'null') - stake_currency = get_column_def(cols, 'stake_currency', 'null') - 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') - fee_close = get_column_def(cols, 'fee_close', 'fee') - fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null') - fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null') - open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null') - close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') - stop_loss = get_column_def(cols, 'stop_loss', '0.0') - stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null') - initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') - initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null') + decl_base, + inspector, + engine, + trade_back_name: str, + cols: List, + order_back_name: str, + cols_order: List, +): + base_currency = get_column_def(cols, "base_currency", "null") + stake_currency = get_column_def(cols, "stake_currency", "null") + 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") + fee_close = get_column_def(cols, "fee_close", "fee") + fee_close_cost = get_column_def(cols, "fee_close_cost", "null") + fee_close_currency = get_column_def(cols, "fee_close_currency", "null") + open_rate_requested = get_column_def(cols, "open_rate_requested", "null") + close_rate_requested = get_column_def(cols, "close_rate_requested", "null") + stop_loss = get_column_def(cols, "stop_loss", "0.0") + stop_loss_pct = get_column_def(cols, "stop_loss_pct", "null") + initial_stop_loss = get_column_def(cols, "initial_stop_loss", "0.0") + initial_stop_loss_pct = get_column_def(cols, "initial_stop_loss_pct", "null") is_stop_loss_trailing = get_column_def( - cols, 'is_stop_loss_trailing', - f'coalesce({stop_loss_pct}, 0.0) <> coalesce({initial_stop_loss_pct}, 0.0)') - max_rate = get_column_def(cols, 'max_rate', '0.0') - min_rate = get_column_def(cols, 'min_rate', 'null') - exit_reason = get_column_def(cols, 'sell_reason', get_column_def(cols, 'exit_reason', 'null')) - strategy = get_column_def(cols, 'strategy', 'null') - enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null')) - realized_profit = get_column_def(cols, 'realized_profit', '0.0') + cols, + "is_stop_loss_trailing", + f"coalesce({stop_loss_pct}, 0.0) <> coalesce({initial_stop_loss_pct}, 0.0)", + ) + max_rate = get_column_def(cols, "max_rate", "0.0") + min_rate = get_column_def(cols, "min_rate", "null") + exit_reason = get_column_def(cols, "sell_reason", get_column_def(cols, "exit_reason", "null")) + strategy = get_column_def(cols, "strategy", "null") + enter_tag = get_column_def(cols, "buy_tag", get_column_def(cols, "enter_tag", "null")) + realized_profit = get_column_def(cols, "realized_profit", "0.0") - trading_mode = get_column_def(cols, 'trading_mode', 'null') + trading_mode = get_column_def(cols, "trading_mode", "null") # Leverage Properties - leverage = get_column_def(cols, 'leverage', '1.0') - liquidation_price = get_column_def(cols, 'liquidation_price', - get_column_def(cols, 'isolated_liq', 'null')) + leverage = get_column_def(cols, "leverage", "1.0") + liquidation_price = get_column_def( + cols, "liquidation_price", get_column_def(cols, "isolated_liq", "null") + ) # sqlite does not support literals for booleans - if engine.name == 'postgresql': - is_short = get_column_def(cols, 'is_short', 'false') + if engine.name == "postgresql": + is_short = get_column_def(cols, "is_short", "false") else: - is_short = get_column_def(cols, 'is_short', '0') + is_short = get_column_def(cols, "is_short", "0") # Futures Properties - interest_rate = get_column_def(cols, 'interest_rate', '0.0') - funding_fees = get_column_def(cols, 'funding_fees', '0.0') - funding_fee_running = get_column_def(cols, 'funding_fee_running', 'null') - max_stake_amount = get_column_def(cols, 'max_stake_amount', 'stake_amount') + interest_rate = get_column_def(cols, "interest_rate", "0.0") + funding_fees = get_column_def(cols, "funding_fees", "0.0") + funding_fee_running = get_column_def(cols, "funding_fee_running", "null") + max_stake_amount = get_column_def(cols, "max_stake_amount", "stake_amount") # If ticker-interval existed use that, else null. - if has_column(cols, 'ticker_interval'): - timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') + if has_column(cols, "ticker_interval"): + timeframe = get_column_def(cols, "timeframe", "ticker_interval") else: - timeframe = get_column_def(cols, 'timeframe', 'null') + timeframe = get_column_def(cols, "timeframe", "null") - open_trade_value = get_column_def(cols, 'open_trade_value', - f'amount * open_rate * (1 + {fee_open})') + open_trade_value = get_column_def( + cols, "open_trade_value", f"amount * open_rate * (1 + {fee_open})" + ) close_profit_abs = get_column_def( - cols, 'close_profit_abs', - f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") - exit_order_status = get_column_def(cols, 'exit_order_status', - get_column_def(cols, 'sell_order_status', 'null')) - amount_requested = get_column_def(cols, 'amount_requested', 'amount') + cols, "close_profit_abs", f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}" + ) + exit_order_status = get_column_def( + cols, "exit_order_status", get_column_def(cols, "sell_order_status", "null") + ) + amount_requested = get_column_def(cols, "amount_requested", "amount") - amount_precision = get_column_def(cols, 'amount_precision', 'null') - price_precision = get_column_def(cols, 'price_precision', 'null') - precision_mode = get_column_def(cols, 'precision_mode', 'null') - contract_size = get_column_def(cols, 'contract_size', 'null') + amount_precision = get_column_def(cols, "amount_precision", "null") + price_precision = get_column_def(cols, "price_precision", "null") + precision_mode = get_column_def(cols, "precision_mode", "null") + contract_size = get_column_def(cols, "contract_size", "null") # Schema migration necessary with engine.begin() as connection: @@ -151,7 +163,8 @@ def migrate_trades_and_orders_table( # Copy data back - following the correct schema with engine.begin() as connection: - connection.execute(text(f"""insert into trades + connection.execute( + text(f"""insert into trades (id, exchange, pair, base_currency, stake_currency, is_open, fee_open, fee_open_cost, fee_open_currency, fee_close, fee_close_cost, fee_close_currency, open_rate, @@ -196,7 +209,8 @@ def migrate_trades_and_orders_table( {precision_mode} precision_mode, {contract_size} contract_size, {max_stake_amount} max_stake_amount from {trade_back_name} - """)) + """) + ) migrate_orders_table(engine, order_back_name, cols_order) set_sequence_ids(engine, order_id, trade_id) @@ -212,19 +226,19 @@ def drop_orders_table(engine, table_back_name: str): def migrate_orders_table(engine, table_back_name: str, cols_order: List): - - ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null') - average = get_column_def(cols_order, 'average', 'null') - stop_price = get_column_def(cols_order, 'stop_price', 'null') - funding_fee = get_column_def(cols_order, 'funding_fee', '0.0') - ft_amount = get_column_def(cols_order, 'ft_amount', 'coalesce(amount, 0.0)') - ft_price = get_column_def(cols_order, 'ft_price', 'coalesce(price, 0.0)') - ft_cancel_reason = get_column_def(cols_order, 'ft_cancel_reason', 'null') - ft_order_tag = get_column_def(cols_order, 'ft_order_tag', 'null') + ft_fee_base = get_column_def(cols_order, "ft_fee_base", "null") + average = get_column_def(cols_order, "average", "null") + stop_price = get_column_def(cols_order, "stop_price", "null") + funding_fee = get_column_def(cols_order, "funding_fee", "0.0") + ft_amount = get_column_def(cols_order, "ft_amount", "coalesce(amount, 0.0)") + ft_price = get_column_def(cols_order, "ft_price", "coalesce(price, 0.0)") + ft_cancel_reason = get_column_def(cols_order, "ft_cancel_reason", "null") + ft_order_tag = get_column_def(cols_order, "ft_order_tag", "null") # sqlite does not support literals for booleans with engine.begin() as connection: - connection.execute(text(f""" + 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, stop_price, order_date, order_filled_date, order_update_date, ft_fee_base, funding_fee, @@ -237,36 +251,36 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List): {ft_amount} ft_amount, {ft_price} ft_price, {ft_cancel_reason} ft_cancel_reason, {ft_order_tag} ft_order_tag from {table_back_name} - """)) + """) + ) -def migrate_pairlocks_table( - decl_base, inspector, engine, - pairlock_back_name: str, cols: List): - +def migrate_pairlocks_table(decl_base, inspector, engine, pairlock_back_name: str, cols: List): # Schema migration necessary with engine.begin() as connection: connection.execute(text(f"alter table pairlocks rename to {pairlock_back_name}")) drop_index_on_table(engine, inspector, pairlock_back_name) - side = get_column_def(cols, 'side', "'*'") + side = get_column_def(cols, "side", "'*'") # let SQLAlchemy create the schema as required decl_base.metadata.create_all(engine) # Copy data back - following the correct schema with engine.begin() as connection: - connection.execute(text(f"""insert into pairlocks + connection.execute( + text(f"""insert into pairlocks (id, pair, side, reason, lock_time, lock_end_time, active) select id, pair, {side} side, reason, lock_time, lock_end_time, active from {pairlock_back_name} - """)) + """) + ) def set_sqlite_to_wal(engine): - if engine.name == 'sqlite' and str(engine.url) != 'sqlite://': + if engine.name == "sqlite" and str(engine.url) != "sqlite://": # Set Mode to with engine.begin() as connection: connection.execute(text("PRAGMA journal_mode=wal")) @@ -274,7 +288,6 @@ def set_sqlite_to_wal(engine): def fix_old_dry_orders(engine): with engine.begin() as connection: - # Update current dry-run Orders where # - stoploss order is Open (will be replaced eventually) # 2nd query: @@ -283,26 +296,28 @@ def fix_old_dry_orders(engine): # - current Order trade_id not equal to current Trade.id # - current Order not stoploss - stmt = update(Order).where( - Order.ft_is_open.is_(True), - Order.ft_order_side == 'stoploss', - Order.order_id.like('dry%'), - - ).values(ft_is_open=False) + stmt = ( + update(Order) + .where( + Order.ft_is_open.is_(True), + Order.ft_order_side == "stoploss", + Order.order_id.like("dry%"), + ) + .values(ft_is_open=False) + ) connection.execute(stmt) # Close dry-run orders for closed trades. - stmt = update(Order).where( - Order.ft_is_open.is_(True), - Order.ft_trade_id.not_in( - select( - Trade.id - ).where(Trade.is_open.is_(True)) - ), - Order.ft_order_side != 'stoploss', - Order.order_id.like('dry%') - - ).values(ft_is_open=False) + stmt = ( + update(Order) + .where( + Order.ft_is_open.is_(True), + Order.ft_trade_id.not_in(select(Trade.id).where(Trade.is_open.is_(True))), + Order.ft_order_side != "stoploss", + Order.order_id.like("dry%"), + ) + .values(ft_is_open=False) + ) connection.execute(stmt) @@ -312,15 +327,15 @@ def check_migrate(engine, decl_base, previous_tables) -> None: """ inspector = inspect(engine) - cols_trades = inspector.get_columns('trades') - cols_orders = inspector.get_columns('orders') - cols_pairlocks = inspector.get_columns('pairlocks') - 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') - pairlock_tabs = get_table_names_for_table(inspector, 'pairlocks') - pairlock_table_bak_name = get_backup_name(pairlock_tabs, 'pairlocks_bak') + cols_trades = inspector.get_columns("trades") + cols_orders = inspector.get_columns("orders") + cols_pairlocks = inspector.get_columns("pairlocks") + 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") + pairlock_tabs = get_table_names_for_table(inspector, "pairlocks") + pairlock_table_bak_name = get_backup_name(pairlock_tabs, "pairlocks_bak") # Check if migration necessary # Migrates both trades and orders table! @@ -328,27 +343,37 @@ def check_migrate(engine, decl_base, previous_tables) -> None: # or not has_column(cols_orders, 'funding_fee')): migrating = False # if not has_column(cols_trades, 'funding_fee_running'): - if not has_column(cols_orders, 'ft_order_tag'): + if not has_column(cols_orders, "ft_order_tag"): migrating = True - logger.info(f"Running database migration for trades - " - f"backup: {table_back_name}, {order_table_bak_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_trades, - order_table_bak_name, cols_orders) + decl_base, + inspector, + engine, + table_back_name, + cols_trades, + order_table_bak_name, + cols_orders, + ) - if not has_column(cols_pairlocks, 'side'): + if not has_column(cols_pairlocks, "side"): migrating = True - logger.info(f"Running database migration for pairlocks - " - f"backup: {pairlock_table_bak_name}") + logger.info( + f"Running database migration for pairlocks - " f"backup: {pairlock_table_bak_name}" + ) migrate_pairlocks_table( decl_base, inspector, engine, pairlock_table_bak_name, cols_pairlocks ) - if 'orders' not in previous_tables and 'trades' in previous_tables: + if "orders" not in previous_tables and "trades" in previous_tables: raise OperationalException( "Your database seems to be very old. " "Please update to freqtrade 2022.3 to migrate this database or " - "start with a fresh database.") + "start with a fresh database." + ) set_sqlite_to_wal(engine) fix_old_dry_orders(engine) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 1a69b271c..261148baa 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -1,6 +1,7 @@ """ This module contains the class to persist trades into SQLite """ + import logging import threading from contextvars import ContextVar @@ -23,7 +24,7 @@ from freqtrade.persistence.trade_model import Order, Trade logger = logging.getLogger(__name__) -REQUEST_ID_CTX_KEY: Final[str] = 'request_id' +REQUEST_ID_CTX_KEY: Final[str] = "request_id" _request_id_ctx_var: ContextVar[Optional[str]] = ContextVar(REQUEST_ID_CTX_KEY, default=None) @@ -39,7 +40,7 @@ def get_request_or_thread_id() -> Optional[str]: return id -_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' +_SQL_DOCS_URL = "http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls" def init_db(db_url: str) -> None: @@ -52,35 +53,44 @@ def init_db(db_url: str) -> None: """ kwargs: Dict[str, Any] = {} - if db_url == 'sqlite:///': + 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, - }) + f"Bad db-url {db_url}. For in-memory database, please use `sqlite://`." + ) + if db_url == "sqlite://": + kwargs.update( + { + "poolclass": StaticPool, + } + ) # Take care of thread ownership - if db_url.startswith('sqlite://'): - kwargs.update({ - 'connect_args': {'check_same_thread': False}, - }) + if db_url.startswith("sqlite://"): + kwargs.update( + { + "connect_args": {"check_same_thread": False}, + } + ) try: engine = create_engine(db_url, future=True, **kwargs) except NoSuchModuleError: - raise OperationalException(f"Given value for db_url: '{db_url}' " - f"is no valid database URL! (See {_SQL_DOCS_URL})") + raise OperationalException( + f"Given value for db_url: '{db_url}' " + f"is no valid database URL! (See {_SQL_DOCS_URL})" + ) # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # Scoped sessions proxy requests to the appropriate thread-local session. # Since we also use fastAPI, we need to make it aware of the request id, too - Trade.session = scoped_session(sessionmaker( - bind=engine, autoflush=False), scopefunc=get_request_or_thread_id) + Trade.session = scoped_session( + sessionmaker(bind=engine, autoflush=False), scopefunc=get_request_or_thread_id + ) Order.session = Trade.session PairLock.session = Trade.session _KeyValueStoreModel.session = Trade.session - _CustomData.session = scoped_session(sessionmaker(bind=engine, autoflush=True), - scopefunc=get_request_or_thread_id) + _CustomData.session = scoped_session( + sessionmaker(bind=engine, autoflush=True), scopefunc=get_request_or_thread_id + ) previous_tables = inspect(engine).get_table_names() ModelBase.metadata.create_all(engine) diff --git a/freqtrade/persistence/pairlock.py b/freqtrade/persistence/pairlock.py index 1b254c2b2..2ea2991c2 100644 --- a/freqtrade/persistence/pairlock.py +++ b/freqtrade/persistence/pairlock.py @@ -12,7 +12,8 @@ class PairLock(ModelBase): """ Pair Locks database model. """ - __tablename__ = 'pairlocks' + + __tablename__ = "pairlocks" session: ClassVar[SessionType] id: Mapped[int] = mapped_column(primary_key=True) @@ -32,43 +33,48 @@ class PairLock(ModelBase): lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT) lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT) return ( - f'PairLock(id={self.id}, pair={self.pair}, side={self.side}, lock_time={lock_time}, ' - f'lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})') + f"PairLock(id={self.id}, pair={self.pair}, side={self.side}, lock_time={lock_time}, " + f"lock_end_time={lock_end_time}, reason={self.reason}, active={self.active})" + ) @staticmethod def query_pair_locks( - pair: Optional[str], now: datetime, side: str = '*') -> ScalarResult['PairLock']: + pair: Optional[str], now: datetime, side: str = "*" + ) -> ScalarResult["PairLock"]: """ Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty :param now: Datetime object (generated via datetime.now(timezone.utc)). """ - filters = [PairLock.lock_end_time > now, - # Only active locks - PairLock.active.is_(True), ] + filters = [ + PairLock.lock_end_time > now, + # Only active locks + PairLock.active.is_(True), + ] if pair: filters.append(PairLock.pair == pair) - if side != '*': - filters.append(or_(PairLock.side == side, PairLock.side == '*')) + if side != "*": + filters.append(or_(PairLock.side == side, PairLock.side == "*")) else: - filters.append(PairLock.side == '*') + filters.append(PairLock.side == "*") return PairLock.session.scalars(select(PairLock).filter(*filters)) @staticmethod - def get_all_locks() -> ScalarResult['PairLock']: + def get_all_locks() -> ScalarResult["PairLock"]: return PairLock.session.scalars(select(PairLock)) def to_json(self) -> Dict[str, Any]: return { - 'id': self.id, - 'pair': self.pair, - 'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT), - 'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000), - 'lock_end_time': self.lock_end_time.strftime(DATETIME_PRINT_FORMAT), - 'lock_end_timestamp': int(self.lock_end_time.replace(tzinfo=timezone.utc - ).timestamp() * 1000), - 'reason': self.reason, - 'side': self.side, - 'active': self.active, + "id": self.id, + "pair": self.pair, + "lock_time": self.lock_time.strftime(DATETIME_PRINT_FORMAT), + "lock_timestamp": int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000), + "lock_end_time": self.lock_end_time.strftime(DATETIME_PRINT_FORMAT), + "lock_end_timestamp": int( + self.lock_end_time.replace(tzinfo=timezone.utc).timestamp() * 1000 + ), + "reason": self.reason, + "side": self.side, + "active": self.active, } diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index dd6bacf3a..616906658 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -21,7 +21,7 @@ class PairLocks: use_db = True locks: List[PairLock] = [] - timeframe: str = '' + timeframe: str = "" @staticmethod def reset_locks() -> None: @@ -32,8 +32,14 @@ class PairLocks: PairLocks.locks = [] @staticmethod - def lock_pair(pair: str, until: datetime, reason: Optional[str] = None, *, - now: Optional[datetime] = None, side: str = '*') -> PairLock: + def lock_pair( + pair: str, + until: datetime, + reason: Optional[str] = None, + *, + now: Optional[datetime] = None, + side: str = "*", + ) -> PairLock: """ Create PairLock from now to "until". Uses database by default, unless PairLocks.use_db is set to False, @@ -50,7 +56,7 @@ class PairLocks: lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until), reason=reason, side=side, - active=True + active=True, ) if PairLocks.use_db: PairLock.session.add(lock) @@ -60,8 +66,9 @@ class PairLocks: return lock @staticmethod - def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None, - side: str = '*') -> Sequence[PairLock]: + def get_pair_locks( + pair: Optional[str], now: Optional[datetime] = None, side: str = "*" + ) -> Sequence[PairLock]: """ Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty @@ -74,17 +81,22 @@ class PairLocks: if PairLocks.use_db: return PairLock.query_pair_locks(pair, now, side).all() else: - locks = [lock for lock in PairLocks.locks if ( - lock.lock_end_time >= now - and lock.active is True - and (pair is None or lock.pair == pair) - and (lock.side == '*' or lock.side == side) - )] + locks = [ + lock + for lock in PairLocks.locks + if ( + lock.lock_end_time >= now + and lock.active is True + and (pair is None or lock.pair == pair) + and (lock.side == "*" or lock.side == side) + ) + ] return locks @staticmethod def get_pair_longest_lock( - pair: str, now: Optional[datetime] = None, side: str = '*') -> Optional[PairLock]: + pair: str, now: Optional[datetime] = None, side: str = "*" + ) -> Optional[PairLock]: """ Get the lock that expires the latest for the pair given. """ @@ -93,7 +105,7 @@ class PairLocks: return locks[0] if locks else None @staticmethod - def unlock_pair(pair: str, now: Optional[datetime] = None, side: str = '*') -> None: + def unlock_pair(pair: str, now: Optional[datetime] = None, side: str = "*") -> None: """ Release all locks for this pair. :param pair: Pair to unlock @@ -124,10 +136,11 @@ class PairLocks: if PairLocks.use_db: # used in live modes logger.info(f"Releasing all locks with reason '{reason}':") - filters = [PairLock.lock_end_time > now, - PairLock.active.is_(True), - PairLock.reason == reason - ] + filters = [ + PairLock.lock_end_time > now, + PairLock.active.is_(True), + PairLock.reason == reason, + ] locks = PairLock.session.scalars(select(PairLock).filter(*filters)).all() for lock in locks: logger.info(f"Releasing lock for {lock.pair} with reason '{reason}'.") @@ -141,7 +154,7 @@ class PairLocks: lock.active = False @staticmethod - def is_global_lock(now: Optional[datetime] = None, side: str = '*') -> bool: + def is_global_lock(now: Optional[datetime] = None, side: str = "*") -> bool: """ :param now: Datetime object (generated via datetime.now(timezone.utc)). defaults to datetime.now(timezone.utc) @@ -149,10 +162,10 @@ class PairLocks: if not now: now = datetime.now(timezone.utc) - return len(PairLocks.get_pair_locks('*', now, side)) > 0 + return len(PairLocks.get_pair_locks("*", now, side)) > 0 @staticmethod - def is_pair_locked(pair: str, now: Optional[datetime] = None, side: str = '*') -> bool: + def is_pair_locked(pair: str, now: Optional[datetime] = None, side: str = "*") -> bool: """ :param pair: Pair to check for :param now: Datetime object (generated via datetime.now(timezone.utc)). @@ -161,9 +174,8 @@ class PairLocks: if not now: now = datetime.now(timezone.utc) - return ( - len(PairLocks.get_pair_locks(pair, now, side)) > 0 - or PairLocks.is_global_lock(now, side) + return len(PairLocks.get_pair_locks(pair, now, side)) > 0 or PairLocks.is_global_lock( + now, side ) @staticmethod diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index b5346660e..3cc3de731 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1,6 +1,7 @@ """ This module contains the class to persist trades into SQLite """ + import logging from collections import defaultdict from dataclasses import dataclass @@ -70,16 +71,17 @@ class Order(ModelBase): Mirrors CCXT Order structure """ - __tablename__ = 'orders' + + __tablename__ = "orders" __allow_unmapped__ = True session: ClassVar[SessionType] # Uniqueness should be ensured over pair, order_id # its likely that order_id is unique per Pair on some exchanges. - __table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),) + __table_args__ = (UniqueConstraint("ft_pair", "order_id", name="_order_pair_order_id"),) id: Mapped[int] = mapped_column(Integer, primary_key=True) - ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey('trades.id'), index=True) + ft_trade_id: Mapped[int] = mapped_column(Integer, ForeignKey("trades.id"), index=True) _trade_live: Mapped["Trade"] = relationship("Trade", back_populates="orders", lazy="immediate") _trade_bt: "LocalTrade" = None # type: ignore @@ -110,17 +112,18 @@ class Order(ModelBase): funding_fee: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) ft_fee_base: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) - ft_order_tag: Mapped[Optional[str]] = mapped_column(String(CUSTOM_TAG_MAX_LENGTH), - nullable=True) + ft_order_tag: Mapped[Optional[str]] = mapped_column( + String(CUSTOM_TAG_MAX_LENGTH), nullable=True + ) @property def order_date_utc(self) -> datetime: - """ Order-date with UTC timezoneinfo""" + """Order-date with UTC timezoneinfo""" return self.order_date.replace(tzinfo=timezone.utc) @property def order_filled_utc(self) -> Optional[datetime]: - """ last order-date with UTC timezoneinfo""" + """last order-date with UTC timezoneinfo""" return ( self.order_filled_date.replace(tzinfo=timezone.utc) if self.order_filled_date else None ) @@ -149,8 +152,9 @@ class Order(ModelBase): @property def safe_remaining(self) -> float: return ( - self.remaining if self.remaining is not None else - self.safe_amount - (self.filled or 0.0) + self.remaining + if self.remaining is not None + else self.safe_amount - (self.filled or 0.0) ) @property @@ -167,35 +171,36 @@ class Order(ModelBase): @property def stake_amount(self) -> float: - """ Amount in stake currency used for this order""" + """Amount in stake currency used for this order""" return self.safe_amount * self.safe_price / self.trade.leverage def __repr__(self): - - return (f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, " - f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, " - f"status={self.status}, date={self.order_date_utc:{DATETIME_PRINT_FORMAT}})") + return ( + f"Order(id={self.id}, trade={self.ft_trade_id}, order_id={self.order_id}, " + f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, " + f"status={self.status}, date={self.order_date_utc:{DATETIME_PRINT_FORMAT}})" + ) def update_from_ccxt_object(self, order): """ Update Order from ccxt response Only updates if fields are available from ccxt - """ - if self.order_id != str(order['id']): + if self.order_id != str(order["id"]): raise DependencyException("Order-id's don't match") - self.status = safe_value_fallback(order, 'status', default_value=self.status) - self.symbol = safe_value_fallback(order, 'symbol', default_value=self.symbol) - self.order_type = safe_value_fallback(order, 'type', default_value=self.order_type) - self.side = safe_value_fallback(order, 'side', default_value=self.side) - self.price = safe_value_fallback(order, 'price', default_value=self.price) - self.amount = safe_value_fallback(order, 'amount', default_value=self.amount) - self.filled = safe_value_fallback(order, 'filled', default_value=self.filled) - self.average = safe_value_fallback(order, 'average', default_value=self.average) - self.remaining = safe_value_fallback(order, 'remaining', default_value=self.remaining) - self.cost = safe_value_fallback(order, 'cost', default_value=self.cost) - self.stop_price = safe_value_fallback(order, 'stopPrice', default_value=self.stop_price) - order_date = safe_value_fallback(order, 'timestamp') + self.status = safe_value_fallback(order, "status", default_value=self.status) + self.symbol = safe_value_fallback(order, "symbol", default_value=self.symbol) + self.order_type = safe_value_fallback(order, "type", default_value=self.order_type) + self.side = safe_value_fallback(order, "side", default_value=self.side) + self.price = safe_value_fallback(order, "price", default_value=self.price) + self.amount = safe_value_fallback(order, "amount", default_value=self.amount) + self.filled = safe_value_fallback(order, "filled", default_value=self.filled) + self.average = safe_value_fallback(order, "average", default_value=self.average) + self.remaining = safe_value_fallback(order, "remaining", default_value=self.remaining) + self.cost = safe_value_fallback(order, "cost", default_value=self.cost) + self.stop_price = safe_value_fallback(order, "stopPrice", default_value=self.stop_price) + order_date = safe_value_fallback(order, "timestamp") if order_date: self.order_date = datetime.fromtimestamp(order_date / 1000, tz=timezone.utc) elif not self.order_date: @@ -204,35 +209,37 @@ class Order(ModelBase): self.ft_is_open = True if self.status in NON_OPEN_EXCHANGE_STATES: self.ft_is_open = False - if (order.get('filled', 0.0) or 0.0) > 0 and not self.order_filled_date: + if (order.get("filled", 0.0) or 0.0) > 0 and not self.order_filled_date: self.order_filled_date = dt_from_ts( - safe_value_fallback(order, 'lastTradeTimestamp', default_value=dt_ts()) + safe_value_fallback(order, "lastTradeTimestamp", default_value=dt_ts()) ) self.order_update_date = datetime.now(timezone.utc) - def to_ccxt_object(self, stopPriceName: str = 'stopPrice') -> Dict[str, Any]: + def to_ccxt_object(self, stopPriceName: str = "stopPrice") -> Dict[str, Any]: order: Dict[str, Any] = { - 'id': self.order_id, - 'symbol': self.ft_pair, - 'price': self.price, - 'average': self.average, - 'amount': self.amount, - 'cost': self.cost, - 'type': self.order_type, - 'side': self.ft_order_side, - 'filled': self.filled, - 'remaining': self.remaining, - 'datetime': self.order_date_utc.strftime('%Y-%m-%dT%H:%M:%S.%f'), - 'timestamp': int(self.order_date_utc.timestamp() * 1000), - 'status': self.status, - 'fee': None, - 'info': {}, + "id": self.order_id, + "symbol": self.ft_pair, + "price": self.price, + "average": self.average, + "amount": self.amount, + "cost": self.cost, + "type": self.order_type, + "side": self.ft_order_side, + "filled": self.filled, + "remaining": self.remaining, + "datetime": self.order_date_utc.strftime("%Y-%m-%dT%H:%M:%S.%f"), + "timestamp": int(self.order_date_utc.timestamp() * 1000), + "status": self.status, + "fee": None, + "info": {}, } - if self.ft_order_side == 'stoploss': - order.update({ - stopPriceName: self.stop_price, - 'ft_order_type': 'stoploss', - }) + if self.ft_order_side == "stoploss": + order.update( + { + stopPriceName: self.stop_price, + "ft_order_type": "stoploss", + } + ) return order @@ -242,48 +249,55 @@ class Order(ModelBase): Only used for backtesting. """ resp = { - 'amount': self.safe_amount, - 'safe_price': self.safe_price, - 'ft_order_side': self.ft_order_side, - 'order_filled_timestamp': dt_ts_none(self.order_filled_utc), - 'ft_is_entry': self.ft_order_side == entry_side, - 'ft_order_tag': self.ft_order_tag, + "amount": self.safe_amount, + "safe_price": self.safe_price, + "ft_order_side": self.ft_order_side, + "order_filled_timestamp": dt_ts_none(self.order_filled_utc), + "ft_is_entry": self.ft_order_side == entry_side, + "ft_order_tag": self.ft_order_tag, } if not minified: - resp.update({ - 'pair': self.ft_pair, - 'order_id': self.order_id, - 'status': self.status, - 'average': round(self.average, 8) if self.average else 0, - 'cost': self.cost if self.cost else 0, - 'filled': self.filled, - '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( - 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, - 'order_type': self.order_type, - 'price': self.price, - 'remaining': self.remaining, - 'ft_fee_base': self.ft_fee_base, - 'funding_fee': self.funding_fee, - }) + resp.update( + { + "pair": self.ft_pair, + "order_id": self.order_id, + "status": self.status, + "average": round(self.average, 8) if self.average else 0, + "cost": self.cost if self.cost else 0, + "filled": self.filled, + "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(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, + "order_type": self.order_type, + "price": self.price, + "remaining": self.remaining, + "ft_fee_base": self.ft_fee_base, + "funding_fee": self.funding_fee, + } + ) return resp - def close_bt_order(self, close_date: datetime, trade: 'LocalTrade'): + def close_bt_order(self, close_date: datetime, trade: "LocalTrade"): self.order_filled_date = close_date self.filled = self.amount self.remaining = 0 - self.status = 'closed' + self.status = "closed" self.ft_is_open = False # Assign funding fees to Order. # Assumes backtesting will use date_last_filled_utc to calculate future funding fees. self.funding_fee = trade.funding_fee_running trade.funding_fee_running = 0.0 - if (self.ft_order_side == trade.entry_side and self.price): + if self.ft_order_side == trade.entry_side and self.price: trade.open_rate = self.price trade.recalc_trade_from_orders() if trade.nr_of_successful_entries == 1: @@ -292,7 +306,7 @@ class Order(ModelBase): trade.adjust_stop_loss(trade.open_rate, trade.stop_loss_pct) @staticmethod - def update_orders(orders: List['Order'], order: Dict[str, Any]): + def update_orders(orders: List["Order"], order: Dict[str, Any]): """ Get all non-closed orders - useful when trying to batch-update orders """ @@ -300,7 +314,7 @@ class Order(ModelBase): logger.warning(f"{order} is not a valid response object.") return - filtered_orders = [o for o in orders if o.order_id == order.get('id')] + filtered_orders = [o for o in orders if o.order_id == order.get("id")] if filtered_orders: oobj = filtered_orders[0] oobj.update_from_ccxt_object(order) @@ -310,25 +324,30 @@ class Order(ModelBase): @classmethod def parse_from_ccxt_object( - cls, order: Dict[str, Any], pair: str, side: str, - amount: Optional[float] = None, price: Optional[float] = None) -> Self: + cls, + order: Dict[str, Any], + pair: str, + side: str, + amount: Optional[float] = None, + price: Optional[float] = None, + ) -> Self: """ Parse an order from a ccxt object and return a new order Object. Optional support for overriding amount and price is only used for test simplification. """ o = cls( - order_id=str(order['id']), + order_id=str(order["id"]), ft_order_side=side, ft_pair=pair, - ft_amount=amount if amount else order['amount'], - ft_price=price if price else order['price'], - ) + ft_amount=amount if amount else order["amount"], + ft_price=price if price else order["price"], + ) o.update_from_ccxt_object(order) return o @staticmethod - def get_open_orders() -> Sequence['Order']: + def get_open_orders() -> Sequence["Order"]: """ Retrieve open orders from the database :return: List of open orders @@ -336,7 +355,7 @@ class Order(ModelBase): return Order.session.scalars(select(Order).filter(Order.ft_is_open.is_(True))).all() @staticmethod - def order_by_id(order_id: str) -> Optional['Order']: + def order_by_id(order_id: str) -> Optional["Order"]: """ Retrieve order based on order_id :return: Order or None @@ -348,14 +367,14 @@ class LocalTrade: """ Trade database model. Used in backtesting - must be aligned to Trade model! - """ + use_db: bool = False # Trades container for backtesting - trades: List['LocalTrade'] = [] - trades_open: List['LocalTrade'] = [] + trades: List["LocalTrade"] = [] + trades_open: List["LocalTrade"] = [] # Copy of trades_open - but indexed by pair - bt_trades_open_pp: Dict[str, List['LocalTrade']] = defaultdict(list) + bt_trades_open_pp: Dict[str, List["LocalTrade"]] = defaultdict(list) bt_open_open_trade_count: int = 0 total_profit: float = 0 realized_profit: float = 0 @@ -364,17 +383,17 @@ class LocalTrade: orders: List[Order] = [] - exchange: str = '' - pair: str = '' - base_currency: Optional[str] = '' - stake_currency: Optional[str] = '' + exchange: str = "" + pair: str = "" + base_currency: Optional[str] = "" + stake_currency: Optional[str] = "" is_open: bool = True fee_open: float = 0.0 fee_open_cost: Optional[float] = None - fee_open_currency: Optional[str] = '' + fee_open_currency: Optional[str] = "" fee_close: Optional[float] = 0.0 fee_close_cost: Optional[float] = None - fee_close_currency: Optional[str] = '' + fee_close_currency: Optional[str] = "" open_rate: float = 0.0 open_rate_requested: Optional[float] = None # open_trade_value - calculated via _calc_open_trade_value @@ -402,9 +421,9 @@ class LocalTrade: max_rate: Optional[float] = None # Lowest price reached min_rate: Optional[float] = None - exit_reason: Optional[str] = '' - exit_order_status: Optional[str] = '' - strategy: Optional[str] = '' + exit_reason: Optional[str] = "" + exit_order_status: Optional[str] = "" + strategy: Optional[str] = "" enter_tag: Optional[str] = None timeframe: Optional[int] = None @@ -449,14 +468,14 @@ class LocalTrade: @property def has_no_leverage(self) -> bool: """Returns true if this is a non-leverage, non-short trade""" - return ((self.leverage == 1.0 or self.leverage is None) and not self.is_short) + return (self.leverage == 1.0 or self.leverage is None) and not self.is_short @property def borrowed(self) -> float: """ - The amount of currency borrowed from the exchange for leverage trades - If a long trade, the amount is in base currency - If a short trade, the amount is in the other currency being traded + The amount of currency borrowed from the exchange for leverage trades + If a long trade, the amount is in base currency + If a short trade, the amount is in the other currency being traded """ if self.has_no_leverage: return 0.0 @@ -467,7 +486,7 @@ class LocalTrade: @property def _date_last_filled_utc(self) -> Optional[datetime]: - """ Date of the last filled order""" + """Date of the last filled order""" orders = self.select_filled_orders() if orders: return max(o.order_filled_utc for o in orders if o.order_filled_utc) @@ -475,7 +494,7 @@ class LocalTrade: @property def date_last_filled_utc(self) -> datetime: - """ Date of the last filled order - or open_date if no orders are filled""" + """Date of the last filled order - or open_date if no orders are filled""" dt_last_filled = self._date_last_filled_utc if not dt_last_filled: return self.open_date_utc @@ -483,11 +502,10 @@ class LocalTrade: @property def date_entry_fill_utc(self) -> Optional[datetime]: - """ Date of the first filled order""" + """Date of the first filled order""" orders = self.select_filled_orders(self.entry_side) - if ( - orders - and len(filled_date := [o.order_filled_utc for o in orders if o.order_filled_utc]) + if orders and len( + filled_date := [o.order_filled_utc for o in orders if o.order_filled_utc] ): return min(filled_date) return None @@ -533,9 +551,9 @@ class LocalTrade: Compatibility layer for asset - which can be empty for old trades. """ try: - return self.base_currency or self.pair.split('/')[0] + return self.base_currency or self.pair.split("/")[0] except IndexError: - return '' + return "" @property def safe_quote_currency(self) -> str: @@ -543,16 +561,16 @@ class LocalTrade: Compatibility layer for asset - which can be empty for old trades. """ try: - return self.stake_currency or self.pair.split('/')[1].split(':')[0] + return self.stake_currency or self.pair.split("/")[1].split(":")[0] except IndexError: - return '' + return "" @property def open_orders(self) -> List[Order]: """ All open orders for this trade excluding stoploss orders """ - return [o for o in self.orders if o.ft_is_open and o.ft_order_side != 'stoploss'] + return [o for o in self.orders if o.ft_is_open and o.ft_order_side != "stoploss"] @property def has_open_orders(self) -> bool: @@ -560,8 +578,7 @@ class LocalTrade: True if there are open orders for this trade excluding stoploss orders """ open_orders_wo_sl = [ - o for o in self.orders - if o.ft_order_side not in ['stoploss'] and o.ft_is_open + o for o in self.orders if o.ft_order_side not in ["stoploss"] and o.ft_is_open ] return len(open_orders_wo_sl) > 0 @@ -570,10 +587,7 @@ class LocalTrade: """ All open stoploss orders for this trade """ - return [ - o for o in self.orders - if o.ft_order_side in ['stoploss'] and o.ft_is_open - ] + return [o for o in self.orders if o.ft_order_side in ["stoploss"] and o.ft_is_open] @property def has_open_sl_orders(self) -> bool: @@ -581,8 +595,7 @@ class LocalTrade: True if there are open stoploss orders for this trade """ open_sl_orders = [ - o for o in self.orders - if o.ft_order_side in ['stoploss'] and o.ft_is_open + o for o in self.orders if o.ft_order_side in ["stoploss"] and o.ft_is_open ] return len(open_sl_orders) > 0 @@ -591,16 +604,12 @@ class LocalTrade: """ All stoploss orders for this trade """ - return [ - o for o in self.orders - if o.ft_order_side in ['stoploss'] - ] + return [o for o in self.orders if o.ft_order_side in ["stoploss"]] @property def open_orders_ids(self) -> List[str]: open_orders_ids_wo_sl = [ - oo.order_id for oo in self.open_orders - if oo.ft_order_side not in ['stoploss'] + oo.order_id for oo in self.open_orders if oo.ft_order_side not in ["stoploss"] ] return open_orders_ids_wo_sl @@ -611,17 +620,18 @@ class LocalTrade: self.orders = [] if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None: raise OperationalException( - f"{self.trading_mode.value} trading requires param interest_rate on trades") + f"{self.trading_mode.value} trading requires param interest_rate on trades" + ) def __repr__(self): open_since = ( - self.open_date_utc.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' + self.open_date_utc.strftime(DATETIME_PRINT_FORMAT) if self.is_open else "closed" ) return ( - f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' - f'is_short={self.is_short or False}, leverage={self.leverage or 1.0}, ' - f'open_rate={self.open_rate:.8f}, open_since={open_since})' + f"Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, " + f"is_short={self.is_short or False}, leverage={self.leverage or 1.0}, " + f"open_rate={self.open_rate:.8f}, open_since={open_since})" ) def to_json(self, minified: bool = False) -> Dict[str, Any]: @@ -634,85 +644,93 @@ class LocalTrade: orders_json = [order.to_json(self.entry_side, minified) for order in filled_or_open_orders] return { - 'trade_id': self.id, - 'pair': self.pair, - 'base_currency': self.safe_base_currency, - 'quote_currency': self.safe_quote_currency, - 'is_open': self.is_open, - 'exchange': self.exchange, - 'amount': round(self.amount, 8), - 'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None, - 'stake_amount': round(self.stake_amount, 8), - 'max_stake_amount': round(self.max_stake_amount, 8) if self.max_stake_amount else None, - 'strategy': self.strategy, - 'enter_tag': self.enter_tag, - 'timeframe': self.timeframe, - - 'fee_open': self.fee_open, - 'fee_open_cost': self.fee_open_cost, - 'fee_open_currency': self.fee_open_currency, - 'fee_close': self.fee_close, - 'fee_close_cost': self.fee_close_cost, - 'fee_close_currency': self.fee_close_currency, - - 'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT), - 'open_timestamp': dt_ts_none(self.open_date_utc), - 'open_fill_date': (self.date_entry_fill_utc.strftime(DATETIME_PRINT_FORMAT) - if self.date_entry_fill_utc else None), - 'open_fill_timestamp': dt_ts_none(self.date_entry_fill_utc), - 'open_rate': self.open_rate, - 'open_rate_requested': self.open_rate_requested, - 'open_trade_value': round(self.open_trade_value, 8), - - 'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT) - if self.close_date else None), - 'close_timestamp': dt_ts_none(self.close_date_utc), - 'realized_profit': self.realized_profit or 0.0, + "trade_id": self.id, + "pair": self.pair, + "base_currency": self.safe_base_currency, + "quote_currency": self.safe_quote_currency, + "is_open": self.is_open, + "exchange": self.exchange, + "amount": round(self.amount, 8), + "amount_requested": round(self.amount_requested, 8) if self.amount_requested else None, + "stake_amount": round(self.stake_amount, 8), + "max_stake_amount": round(self.max_stake_amount, 8) if self.max_stake_amount else None, + "strategy": self.strategy, + "enter_tag": self.enter_tag, + "timeframe": self.timeframe, + "fee_open": self.fee_open, + "fee_open_cost": self.fee_open_cost, + "fee_open_currency": self.fee_open_currency, + "fee_close": self.fee_close, + "fee_close_cost": self.fee_close_cost, + "fee_close_currency": self.fee_close_currency, + "open_date": self.open_date.strftime(DATETIME_PRINT_FORMAT), + "open_timestamp": dt_ts_none(self.open_date_utc), + "open_fill_date": ( + self.date_entry_fill_utc.strftime(DATETIME_PRINT_FORMAT) + if self.date_entry_fill_utc + else None + ), + "open_fill_timestamp": dt_ts_none(self.date_entry_fill_utc), + "open_rate": self.open_rate, + "open_rate_requested": self.open_rate_requested, + "open_trade_value": round(self.open_trade_value, 8), + "close_date": ( + self.close_date.strftime(DATETIME_PRINT_FORMAT) if self.close_date else None + ), + "close_timestamp": dt_ts_none(self.close_date_utc), + "realized_profit": self.realized_profit or 0.0, # Close-profit corresponds to relative realized_profit ratio - 'realized_profit_ratio': self.close_profit or None, - 'close_rate': self.close_rate, - 'close_rate_requested': self.close_rate_requested, - 'close_profit': self.close_profit, # Deprecated - 'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, - 'close_profit_abs': self.close_profit_abs, # Deprecated - - 'trade_duration_s': (int((self.close_date_utc - self.open_date_utc).total_seconds()) - if self.close_date else None), - 'trade_duration': (int((self.close_date_utc - self.open_date_utc).total_seconds() // 60) - if self.close_date else None), - - 'profit_ratio': self.close_profit, - 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, - 'profit_abs': self.close_profit_abs, - - 'exit_reason': self.exit_reason, - 'exit_order_status': self.exit_order_status, - 'stop_loss_abs': self.stop_loss, - 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, - 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, - 'stoploss_last_update': (self.stoploss_last_update_utc.strftime(DATETIME_PRINT_FORMAT) - if self.stoploss_last_update_utc else None), - 'stoploss_last_update_timestamp': dt_ts_none(self.stoploss_last_update_utc), - 'initial_stop_loss_abs': self.initial_stop_loss, - 'initial_stop_loss_ratio': (self.initial_stop_loss_pct - if self.initial_stop_loss_pct else None), - 'initial_stop_loss_pct': (self.initial_stop_loss_pct * 100 - if self.initial_stop_loss_pct else None), - 'min_rate': self.min_rate, - 'max_rate': self.max_rate, - - 'leverage': self.leverage, - 'interest_rate': self.interest_rate, - 'liquidation_price': self.liquidation_price, - 'is_short': self.is_short, - 'trading_mode': self.trading_mode, - 'funding_fees': self.funding_fees, - 'amount_precision': self.amount_precision, - 'price_precision': self.price_precision, - 'precision_mode': self.precision_mode, - 'contract_size': self.contract_size, - 'has_open_orders': self.has_open_orders, - 'orders': orders_json, + "realized_profit_ratio": self.close_profit or None, + "close_rate": self.close_rate, + "close_rate_requested": self.close_rate_requested, + "close_profit": self.close_profit, # Deprecated + "close_profit_pct": round(self.close_profit * 100, 2) if self.close_profit else None, + "close_profit_abs": self.close_profit_abs, # Deprecated + "trade_duration_s": ( + int((self.close_date_utc - self.open_date_utc).total_seconds()) + if self.close_date + else None + ), + "trade_duration": ( + int((self.close_date_utc - self.open_date_utc).total_seconds() // 60) + if self.close_date + else None + ), + "profit_ratio": self.close_profit, + "profit_pct": round(self.close_profit * 100, 2) if self.close_profit else None, + "profit_abs": self.close_profit_abs, + "exit_reason": self.exit_reason, + "exit_order_status": self.exit_order_status, + "stop_loss_abs": self.stop_loss, + "stop_loss_ratio": self.stop_loss_pct if self.stop_loss_pct else None, + "stop_loss_pct": (self.stop_loss_pct * 100) if self.stop_loss_pct else None, + "stoploss_last_update": ( + self.stoploss_last_update_utc.strftime(DATETIME_PRINT_FORMAT) + if self.stoploss_last_update_utc + else None + ), + "stoploss_last_update_timestamp": dt_ts_none(self.stoploss_last_update_utc), + "initial_stop_loss_abs": self.initial_stop_loss, + "initial_stop_loss_ratio": ( + self.initial_stop_loss_pct if self.initial_stop_loss_pct else None + ), + "initial_stop_loss_pct": ( + self.initial_stop_loss_pct * 100 if self.initial_stop_loss_pct else None + ), + "min_rate": self.min_rate, + "max_rate": self.max_rate, + "leverage": self.leverage, + "interest_rate": self.interest_rate, + "liquidation_price": self.liquidation_price, + "is_short": self.is_short, + "trading_mode": self.trading_mode, + "funding_fees": self.funding_fees, + "amount_precision": self.amount_precision, + "price_precision": self.price_precision, + "precision_mode": self.precision_mode, + "contract_size": self.contract_size, + "has_open_orders": self.has_open_orders, + "orders": orders_json, } @staticmethod @@ -762,8 +780,13 @@ class LocalTrade: self.stop_loss_pct = -1 * abs(percent) - def adjust_stop_loss(self, current_price: float, stoploss: Optional[float], - initial: bool = False, allow_refresh: bool = False) -> None: + def adjust_stop_loss( + self, + current_price: float, + stoploss: Optional[float], + initial: bool = False, + allow_refresh: bool = False, + ) -> None: """ This adjusts the stop loss to it's most recently observed setting :param current_price: Current rate the asset is traded @@ -782,14 +805,21 @@ class LocalTrade: else: new_loss = float(current_price * (1 - abs(stoploss / leverage))) - stop_loss_norm = price_to_precision(new_loss, self.price_precision, self.precision_mode, - rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) + stop_loss_norm = price_to_precision( + new_loss, + self.price_precision, + self.precision_mode, + rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP, + ) # no stop loss assigned yet if self.initial_stop_loss_pct is None: self.__set_stop_loss(stop_loss_norm, stoploss) self.initial_stop_loss = price_to_precision( - stop_loss_norm, self.price_precision, self.precision_mode, - rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP) + stop_loss_norm, + self.price_precision, + self.precision_mode, + rounding_mode=ROUND_DOWN if self.is_short else ROUND_UP, + ) self.initial_stop_loss_pct = -1 * abs(stoploss) # evaluate if the stop loss needs to be updated @@ -818,7 +848,8 @@ class LocalTrade: f"initial_stop_loss={self.initial_stop_loss:.8f}, " f"stop_loss={self.stop_loss:.8f}. " f"Trailing stoploss saved us: " - f"{float(self.stop_loss) - float(self.initial_stop_loss or 0.0):.8f}.") + f"{float(self.stop_loss) - float(self.initial_stop_loss or 0.0):.8f}." + ) def update_trade(self, order: Order, recalculating: bool = False) -> None: """ @@ -828,11 +859,11 @@ class LocalTrade: """ # Ignore open and cancelled orders - if order.status == 'open' or order.safe_price is None: + if order.status == "open" or order.safe_price is None: return - logger.info(f'Updating trade (id={self.id}) ...') - if order.ft_order_side != 'stoploss': + logger.info(f"Updating trade (id={self.id}) ...") + if order.ft_order_side != "stoploss": order.funding_fee = self.funding_fee_running # Reset running funding fees self.funding_fee_running = 0.0 @@ -844,29 +875,29 @@ class LocalTrade: self.amount = order.safe_amount_after_fee if self.is_open: payment = "SELL" if self.is_short else "BUY" - logger.info(f'{order_type}_{payment} has been fulfilled for {self}.') + logger.info(f"{order_type}_{payment} has been fulfilled for {self}.") self.recalc_trade_from_orders() elif order.ft_order_side == self.exit_side: if self.is_open: payment = "BUY" if self.is_short else "SELL" # * On margin shorts, you buy a little bit more than the amount (amount + interest) - logger.info(f'{order_type}_{payment} has been fulfilled for {self}.') + logger.info(f"{order_type}_{payment} has been fulfilled for {self}.") - elif order.ft_order_side == 'stoploss' and order.status not in ('open', ): + elif order.ft_order_side == "stoploss" and order.status not in ("open",): self.close_rate_requested = self.stop_loss self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value if self.is_open and order.safe_filled > 0: - logger.info(f'{order_type} is hit for {self}.') + logger.info(f"{order_type} is hit for {self}.") else: - raise ValueError(f'Unknown order type: {order.order_type}') + raise ValueError(f"Unknown order type: {order.order_type}") if order.ft_order_side != self.entry_side: - amount_tr = amount_to_contract_precision(self.amount, self.amount_precision, - self.precision_mode, self.contract_size) - if ( - isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC) - or (not recalculating and order.safe_amount_after_fee > amount_tr) + amount_tr = amount_to_contract_precision( + self.amount, self.amount_precision, self.precision_mode, self.contract_size + ) + if isclose(order.safe_amount_after_fee, amount_tr, abs_tol=MATH_CLOSE_PREC) or ( + not recalculating and order.safe_amount_after_fee > amount_tr ): # When recalculating a trade, only coming out to 0 can force a close self.close(order.safe_price) @@ -883,14 +914,17 @@ class LocalTrade: self.close_rate = rate self.close_date = self.close_date or self._date_last_filled_utc or dt_now() self.is_open = False - self.exit_order_status = 'closed' + self.exit_order_status = "closed" self.recalc_trade_from_orders(is_closing=True) if show_msg: - logger.info(f"Marking {self} as closed as the trade is fulfilled " - "and found no open orders for it.") + logger.info( + f"Marking {self} as closed as the trade is fulfilled " + "and found no open orders for it." + ) - def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float], - side: str) -> None: + def update_fee( + self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float], side: str + ) -> None: """ Update Fee parameters. Only acts once per side """ @@ -926,8 +960,13 @@ class LocalTrade: Get amount of failed exiting orders assumes full exits. """ - return len([o for o in self.orders if o.ft_order_side == self.exit_side - and o.status in CANCELED_EXCHANGE_STATES]) + return len( + [ + o + for o in self.orders + if o.ft_order_side == self.exit_side and o.status in CANCELED_EXCHANGE_STATES + ] + ) def _calc_open_trade_value(self, amount: float, open_rate: float) -> float: """ @@ -969,7 +1008,6 @@ class LocalTrade: return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours) def _calc_base_close(self, amount: FtPrecise, rate: float, fee: Optional[float]) -> FtPrecise: - close_trade = amount * FtPrecise(rate) fees = close_trade * FtPrecise(fee or 0.0) @@ -993,8 +1031,7 @@ class LocalTrade: if trading_mode == TradingMode.SPOT: return float(self._calc_base_close(amount1, rate, self.fee_close)) - elif (trading_mode == TradingMode.MARGIN): - + elif trading_mode == TradingMode.MARGIN: total_interest = self.calculate_interest() if self.is_short: @@ -1004,7 +1041,7 @@ class LocalTrade: # Currency already owned for longs, no need to purchase return float(self._calc_base_close(amount1, rate, self.fee_close) - total_interest) - elif (trading_mode == TradingMode.FUTURES): + elif trading_mode == TradingMode.FUTURES: funding_fees = self.funding_fees or 0.0 # Positive funding_fees -> Trade has gained from fees. # Negative funding_fees -> Trade had to pay the fees. @@ -1014,10 +1051,12 @@ class LocalTrade: return float(self._calc_base_close(amount1, rate, self.fee_close)) + funding_fees else: raise OperationalException( - f"{self.trading_mode.value} trading is not yet available using freqtrade") + f"{self.trading_mode.value} trading is not yet available using freqtrade" + ) - def calc_profit(self, rate: float, amount: Optional[float] = None, - open_rate: Optional[float] = None) -> float: + def calc_profit( + self, rate: float, amount: Optional[float] = None, open_rate: Optional[float] = None + ) -> float: """ Calculate the absolute profit in stake currency between Close and Open trade Deprecated - only available for backwards compatibility @@ -1029,8 +1068,9 @@ class LocalTrade: prof = self.calculate_profit(rate, amount, open_rate) return prof.profit_abs - def calculate_profit(self, rate: float, amount: Optional[float] = None, - open_rate: Optional[float] = None) -> ProfitStruct: + def calculate_profit( + self, rate: float, amount: Optional[float] = None, open_rate: Optional[float] = None + ) -> ProfitStruct: """ Calculate profit metrics (absolute, ratio, total, total ratio). All calculations include fees. @@ -1063,7 +1103,8 @@ class LocalTrade: total_profit_abs = profit_abs + self.realized_profit total_profit_ratio = ( (total_profit_abs / self.max_stake_amount) * self.leverage - if self.max_stake_amount else 0.0 + if self.max_stake_amount + else 0.0 ) total_profit_ratio = float(f"{total_profit_ratio:.8f}") profit_abs = float(f"{profit_abs:.8f}") @@ -1076,8 +1117,8 @@ class LocalTrade: ) def calc_profit_ratio( - self, rate: float, amount: Optional[float] = None, - open_rate: Optional[float] = None) -> float: + self, rate: float, amount: Optional[float] = None, open_rate: Optional[float] = None + ) -> float: """ Calculates the profit as ratio (including fee). :param rate: rate to compare with. @@ -1092,10 +1133,10 @@ class LocalTrade: else: open_trade_value = self._calc_open_trade_value(amount, open_rate) - short_close_zero = (self.is_short and close_trade_value == 0.0) - long_close_zero = (not self.is_short and open_trade_value == 0.0) + short_close_zero = self.is_short and close_trade_value == 0.0 + long_close_zero = not self.is_short and open_trade_value == 0.0 - if (short_close_zero or long_close_zero): + if short_close_zero or long_close_zero: return 0.0 else: if self.is_short: @@ -1121,7 +1162,7 @@ class LocalTrade: for i, o in enumerate(self.orders): if o.ft_is_open or not o.filled: continue - funding_fees += (o.funding_fee or 0.0) + funding_fees += o.funding_fee or 0.0 tmp_amount = FtPrecise(o.safe_amount_after_fee) tmp_price = FtPrecise(o.safe_price) @@ -1151,7 +1192,7 @@ class LocalTrade: close_profit = (close_profit_abs / total_stake) * self.leverage else: total_stake = total_stake + self._calc_open_trade_value(tmp_amount, price) - max_stake_amount += (tmp_amount * price) + max_stake_amount += tmp_amount * price self.funding_fees = funding_fees self.max_stake_amount = float(max_stake_amount) @@ -1161,7 +1202,8 @@ class LocalTrade: self.close_profit_abs = prof.profit_abs current_amount_tr = amount_to_contract_precision( - float(current_amount), self.amount_precision, self.precision_mode, self.contract_size) + float(current_amount), self.amount_precision, self.precision_mode, self.contract_size + ) if current_amount_tr > 0.0: # Trade is still open # Leverage not updated, as we don't allow changing leverage through DCA at the moment. @@ -1188,8 +1230,12 @@ class LocalTrade: return o return None - def select_order(self, order_side: Optional[str] = None, - is_open: Optional[bool] = None, only_filled: bool = False) -> Optional[Order]: + def select_order( + self, + order_side: Optional[str] = None, + is_open: Optional[bool] = None, + only_filled: bool = False, + ) -> Optional[Order]: """ Finds latest order for this orderside and status :param order_side: ft_order_side of the order (either 'buy', 'sell' or 'stoploss') @@ -1209,32 +1255,38 @@ class LocalTrade: else: return None - def select_filled_orders(self, order_side: Optional[str] = None) -> List['Order']: + def select_filled_orders(self, order_side: Optional[str] = None) -> List["Order"]: """ Finds filled orders for this order side. Will not return open orders which already partially filled. :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) or (order_side is None)) - and o.ft_is_open is False - and o.filled - and o.status in NON_OPEN_EXCHANGE_STATES] + 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 + and o.status in NON_OPEN_EXCHANGE_STATES + ] - def select_filled_or_open_orders(self) -> List['Order']: + def select_filled_or_open_orders(self) -> List["Order"]: """ Finds filled or open orders :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_is_open is False - and (o.filled or 0) > 0 - and o.status in NON_OPEN_EXCHANGE_STATES - ) - or (o.ft_is_open is True and o.status is not None) - ] + return [ + o + for o in self.orders + if ( + o.ft_is_open is False + and (o.filled or 0) > 0 + and o.status in NON_OPEN_EXCHANGE_STATES + ) + or (o.ft_is_open is True and o.status is not None) + ] def set_custom_data(self, key: str, value: Any) -> None: """ @@ -1295,7 +1347,7 @@ class LocalTrade: :return: int count of buy orders that have been filled for this trade. """ - return len(self.select_filled_orders('buy')) + return len(self.select_filled_orders("buy")) @property def nr_of_successful_sells(self) -> int: @@ -1304,11 +1356,11 @@ class LocalTrade: WARNING: Please use nr_of_successful_exits for short support. :return: int count of sell orders that have been filled for this trade. """ - return len(self.select_filled_orders('sell')) + return len(self.select_filled_orders("sell")) @property def sell_reason(self) -> Optional[str]: - """ DEPRECATED! Please use exit_reason instead.""" + """DEPRECATED! Please use exit_reason instead.""" return self.exit_reason @property @@ -1316,10 +1368,13 @@ class LocalTrade: return self.close_rate or self.close_rate_requested or 0.0 @staticmethod - def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None, - open_date: Optional[datetime] = None, - close_date: Optional[datetime] = None, - ) -> List['LocalTrade']: + def get_trades_proxy( + *, + pair: Optional[str] = None, + is_open: Optional[bool] = None, + open_date: Optional[datetime] = None, + close_date: Optional[datetime] = None, + ) -> List["LocalTrade"]: """ Helper function to query Trades. Returns a List of trades, filtered on the parameters given. @@ -1350,8 +1405,9 @@ class LocalTrade: if open_date: sel_trades = [trade for trade in sel_trades if trade.open_date > open_date] if close_date: - sel_trades = [trade for trade in sel_trades if trade.close_date - and trade.close_date > close_date] + sel_trades = [ + trade for trade in sel_trades if trade.close_date and trade.close_date > close_date + ] return sel_trades @@ -1407,8 +1463,7 @@ class LocalTrade: logger.info(f"Found open trade: {trade}") # skip case if trailing-stop changed the stoploss already. - if (not trade.is_stop_loss_trailing - and trade.initial_stop_loss_pct != desired_stoploss): + if not trade.is_stop_loss_trailing and trade.initial_stop_loss_pct != desired_stoploss: # Stoploss value got changed logger.info(f"Stoploss for {trade} needs adjustment...") @@ -1428,6 +1483,7 @@ class LocalTrade: :return: Trade instance """ import rapidjson + data = rapidjson.loads(json_str) trade = cls( __FROM_JSON=True, @@ -1453,8 +1509,11 @@ class LocalTrade: open_rate=data["open_rate"], open_rate_requested=data["open_rate_requested"], open_trade_value=data["open_trade_value"], - close_date=(datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc) - if data["close_timestamp"] else None), + close_date=( + datetime.fromtimestamp(data["close_timestamp"] // 1000, tz=timezone.utc) + if data["close_timestamp"] + else None + ), realized_profit=data["realized_profit"], close_rate=data["close_rate"], close_rate_requested=data["close_rate_requested"], @@ -1474,13 +1533,12 @@ class LocalTrade: is_short=data["is_short"], trading_mode=data["trading_mode"], funding_fees=data["funding_fees"], - amount_precision=data.get('amount_precision', None), - price_precision=data.get('price_precision', None), - precision_mode=data.get('precision_mode', None), - contract_size=data.get('contract_size', None), + amount_precision=data.get("amount_precision", None), + price_precision=data.get("price_precision", None), + precision_mode=data.get("precision_mode", None), + contract_size=data.get("contract_size", None), ) for order in data["orders"]: - order_obj = Order( amount=order["amount"], ft_amount=order["amount"], @@ -1493,9 +1551,11 @@ class LocalTrade: cost=order["cost"], filled=order["filled"], order_date=datetime.strptime(order["order_date"], DATETIME_PRINT_FORMAT), - order_filled_date=(datetime.fromtimestamp( - order["order_filled_timestamp"] // 1000, tz=timezone.utc) - if order["order_filled_timestamp"] else None), + order_filled_date=( + datetime.fromtimestamp(order["order_filled_timestamp"] // 1000, tz=timezone.utc) + if order["order_filled_timestamp"] + else None + ), order_type=order["order_type"], price=order["price"], ft_price=order["price"], @@ -1515,7 +1575,8 @@ class Trade(ModelBase, LocalTrade): Note: Fields must be aligned with LocalTrade class """ - __tablename__ = 'trades' + + __tablename__ = "trades" session: ClassVar[SessionType] use_db: bool = True @@ -1523,11 +1584,11 @@ class Trade(ModelBase, LocalTrade): id: Mapped[int] = mapped_column(Integer, primary_key=True) # type: ignore orders: Mapped[List[Order]] = relationship( - "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin", - innerjoin=True) # type: ignore + "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin", innerjoin=True + ) # type: ignore custom_data: Mapped[List[_CustomData]] = relationship( - "_CustomData", cascade="all, delete-orphan", - lazy="raise") + "_CustomData", cascade="all, delete-orphan", lazy="raise" + ) exchange: Mapped[str] = mapped_column(String(25), nullable=False) # type: ignore pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) # type: ignore @@ -1536,61 +1597,46 @@ class Trade(ModelBase, LocalTrade): is_open: Mapped[bool] = mapped_column(nullable=False, default=True, index=True) # type: ignore fee_open: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) # type: ignore fee_open_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore - fee_open_currency: Mapped[Optional[str]] = mapped_column( - String(25), nullable=True) # type: ignore - fee_close: Mapped[Optional[float]] = mapped_column( - Float(), nullable=False, default=0.0) # type: ignore + fee_open_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) # type: ignore + fee_close: Mapped[Optional[float]] = mapped_column(Float(), nullable=False, default=0.0) # type: ignore fee_close_cost: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore - fee_close_currency: Mapped[Optional[str]] = mapped_column( - String(25), nullable=True) # type: ignore + fee_close_currency: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) # type: ignore open_rate: Mapped[float] = mapped_column(Float()) # type: ignore - open_rate_requested: Mapped[Optional[float]] = mapped_column( - Float(), nullable=True) # type: ignore + open_rate_requested: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore # open_trade_value - calculated via _calc_open_trade_value open_trade_value: Mapped[float] = mapped_column(Float(), nullable=True) # type: ignore close_rate: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore close_rate_requested: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore - realized_profit: Mapped[float] = mapped_column( - Float(), default=0.0, nullable=True) # type: ignore + realized_profit: Mapped[float] = mapped_column(Float(), default=0.0, nullable=True) # type: ignore close_profit: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore close_profit_abs: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore stake_amount: Mapped[float] = mapped_column(Float(), nullable=False) # type: ignore max_stake_amount: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore amount: Mapped[float] = mapped_column(Float()) # type: ignore amount_requested: Mapped[Optional[float]] = mapped_column(Float()) # type: ignore - open_date: Mapped[datetime] = mapped_column( - nullable=False, default=datetime.now) # type: ignore + open_date: Mapped[datetime] = mapped_column(nullable=False, default=datetime.now) # type: ignore close_date: Mapped[Optional[datetime]] = mapped_column() # type: ignore # absolute value of the stop loss stop_loss: Mapped[float] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore # percentage value of the stop loss stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore # absolute value of the initial stop loss - initial_stop_loss: Mapped[Optional[float]] = mapped_column( - Float(), nullable=True, default=0.0) # type: ignore + initial_stop_loss: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore # percentage value of the initial stop loss - initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column( - Float(), nullable=True) # type: ignore - is_stop_loss_trailing: Mapped[bool] = mapped_column( - nullable=False, default=False) # type: ignore + initial_stop_loss_pct: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore + is_stop_loss_trailing: Mapped[bool] = mapped_column(nullable=False, default=False) # type: ignore # absolute value of the highest reached price - max_rate: Mapped[Optional[float]] = mapped_column( - Float(), nullable=True, default=0.0) # type: ignore + max_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=0.0) # type: ignore # Lowest price reached min_rate: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore - exit_reason: Mapped[Optional[str]] = mapped_column( - String(CUSTOM_TAG_MAX_LENGTH), nullable=True) # type: ignore - exit_order_status: Mapped[Optional[str]] = mapped_column( - String(100), nullable=True) # type: ignore + exit_reason: Mapped[Optional[str]] = mapped_column(String(CUSTOM_TAG_MAX_LENGTH), nullable=True) # type: ignore + exit_order_status: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore strategy: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # type: ignore - enter_tag: Mapped[Optional[str]] = mapped_column( - String(CUSTOM_TAG_MAX_LENGTH), nullable=True) # type: ignore + enter_tag: Mapped[Optional[str]] = mapped_column(String(CUSTOM_TAG_MAX_LENGTH), nullable=True) # type: ignore timeframe: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore - trading_mode: Mapped[TradingMode] = mapped_column( - Enum(TradingMode), nullable=True) # type: ignore - amount_precision: Mapped[Optional[float]] = mapped_column( - Float(), nullable=True) # type: ignore + trading_mode: Mapped[TradingMode] = mapped_column(Enum(TradingMode), nullable=True) # type: ignore + amount_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore price_precision: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore precision_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) # type: ignore contract_size: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore @@ -1598,28 +1644,26 @@ class Trade(ModelBase, LocalTrade): # Leverage trading properties leverage: Mapped[float] = mapped_column(Float(), nullable=True, default=1.0) # type: ignore is_short: Mapped[bool] = mapped_column(nullable=False, default=False) # type: ignore - liquidation_price: Mapped[Optional[float]] = mapped_column( - Float(), nullable=True) # type: ignore + liquidation_price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) # type: ignore # Margin Trading Properties - interest_rate: Mapped[float] = mapped_column( - Float(), nullable=False, default=0.0) # type: ignore + interest_rate: Mapped[float] = mapped_column(Float(), nullable=False, default=0.0) # type: ignore # Futures properties - funding_fees: Mapped[Optional[float]] = mapped_column( - Float(), nullable=True, default=None) # type: ignore + funding_fees: Mapped[Optional[float]] = mapped_column(Float(), nullable=True, default=None) # type: ignore funding_fee_running: Mapped[Optional[float]] = mapped_column( - Float(), nullable=True, default=None) # type: ignore + Float(), nullable=True, default=None + ) # type: ignore def __init__(self, **kwargs): - from_json = kwargs.pop('__FROM_JSON', None) + from_json = kwargs.pop("__FROM_JSON", None) super().__init__(**kwargs) if not from_json: # Skip recalculation when loading from json self.realized_profit = 0 self.recalc_open_trade_value() - @validates('enter_tag', 'exit_reason') + @validates("enter_tag", "exit_reason") def validate_string_len(self, key, value): max_len = getattr(self.__class__, key).prop.columns[0].type.length if value and len(value) > max_len: @@ -1627,7 +1671,6 @@ class Trade(ModelBase, LocalTrade): return value def delete(self) -> None: - for order in self.orders: Order.session.delete(order) @@ -1645,10 +1688,13 @@ class Trade(ModelBase, LocalTrade): Trade.session.rollback() @staticmethod - def get_trades_proxy(*, pair: Optional[str] = None, is_open: Optional[bool] = None, - open_date: Optional[datetime] = None, - close_date: Optional[datetime] = None, - ) -> List['LocalTrade']: + def get_trades_proxy( + *, + pair: Optional[str] = None, + is_open: Optional[bool] = None, + open_date: Optional[datetime] = None, + close_date: Optional[datetime] = None, + ) -> List["LocalTrade"]: """ Helper function to query Trades.j Returns a List of trades, filtered on the parameters given. @@ -1670,9 +1716,7 @@ class Trade(ModelBase, LocalTrade): return cast(List[LocalTrade], Trade.get_trades(trade_filter).all()) else: return LocalTrade.get_trades_proxy( - pair=pair, is_open=is_open, - open_date=open_date, - close_date=close_date + pair=pair, is_open=is_open, open_date=open_date, close_date=close_date ) @staticmethod @@ -1687,7 +1731,7 @@ class Trade(ModelBase, LocalTrade): :return: unsorted query object """ if not Trade.use_db: - raise NotImplementedError('`Trade.get_trades()` not supported in backtesting mode.') + raise NotImplementedError("`Trade.get_trades()` not supported in backtesting mode.") if trade_filter is not None: if not isinstance(trade_filter, list): trade_filter = [trade_filter] @@ -1701,7 +1745,7 @@ class Trade(ModelBase, LocalTrade): return this_query @staticmethod - def get_trades(trade_filter=None, include_orders: bool = True) -> ScalarResult['Trade']: + def get_trades(trade_filter=None, include_orders: bool = True) -> ScalarResult["Trade"]: """ Helper function to query Trades using filters. NOTE: Not supported in Backtesting. @@ -1722,10 +1766,13 @@ class Trade(ModelBase, LocalTrade): Returns all open trades which don't have open fees set correctly NOTE: Not supported in Backtesting. """ - return Trade.get_trades([Trade.fee_open_currency.is_(None), - Trade.orders.any(), - Trade.is_open.is_(True), - ]).all() + return Trade.get_trades( + [ + Trade.fee_open_currency.is_(None), + Trade.orders.any(), + Trade.is_open.is_(True), + ] + ).all() @staticmethod def get_closed_trades_without_assigned_fees(): @@ -1733,10 +1780,13 @@ class Trade(ModelBase, LocalTrade): Returns all closed trades which don't have fees set correctly NOTE: Not supported in Backtesting. """ - return Trade.get_trades([Trade.fee_close_currency.is_(None), - Trade.orders.any(), - Trade.is_open.is_(False), - ]).all() + return Trade.get_trades( + [ + Trade.fee_close_currency.is_(None), + Trade.orders.any(), + Trade.is_open.is_(False), + ] + ).all() @staticmethod def get_total_closed_profit() -> float: @@ -1748,8 +1798,10 @@ class Trade(ModelBase, LocalTrade): select(func.sum(Trade.close_profit_abs)).filter(Trade.is_open.is_(False)) ).scalar_one() else: - total_profit = sum(t.close_profit_abs # type: ignore - for t in LocalTrade.get_trades_proxy(is_open=False)) + total_profit = sum( + t.close_profit_abs # type: ignore + for t in LocalTrade.get_trades_proxy(is_open=False) + ) return total_profit or 0 @staticmethod @@ -1764,7 +1816,8 @@ class Trade(ModelBase, LocalTrade): ) else: total_open_stake_amount = sum( - t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True)) + t.stake_amount for t in LocalTrade.get_trades_proxy(is_open=True) + ) return total_open_stake_amount or 0 @staticmethod @@ -1781,22 +1834,23 @@ class Trade(ModelBase, LocalTrade): pair_rates = Trade.session.execute( select( Trade.pair, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(*filters) + func.sum(Trade.close_profit).label("profit_sum"), + func.sum(Trade.close_profit_abs).label("profit_sum_abs"), + func.count(Trade.pair).label("count"), + ) + .filter(*filters) .group_by(Trade.pair) - .order_by(desc('profit_sum_abs')) - ).all() + .order_by(desc("profit_sum_abs")) + ).all() return [ { - 'pair': pair, - 'profit_ratio': profit, - 'profit': round(profit * 100, 2), # Compatibility mode - 'profit_pct': round(profit * 100, 2), - 'profit_abs': profit_abs, - 'count': count + "pair": pair, + "profit_ratio": profit, + "profit": round(profit * 100, 2), # Compatibility mode + "profit_pct": round(profit * 100, 2), + "profit_abs": profit_abs, + "count": count, } for pair, profit, profit_abs, count in pair_rates ] @@ -1810,27 +1864,28 @@ class Trade(ModelBase, LocalTrade): """ filters: List = [Trade.is_open.is_(False)] - if (pair is not None): + if pair is not None: filters.append(Trade.pair == pair) enter_tag_perf = Trade.session.execute( select( Trade.enter_tag, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(*filters) + func.sum(Trade.close_profit).label("profit_sum"), + func.sum(Trade.close_profit_abs).label("profit_sum_abs"), + func.count(Trade.pair).label("count"), + ) + .filter(*filters) .group_by(Trade.enter_tag) - .order_by(desc('profit_sum_abs')) + .order_by(desc("profit_sum_abs")) ).all() return [ { - 'enter_tag': enter_tag if enter_tag is not None else "Other", - 'profit_ratio': profit, - 'profit_pct': round(profit * 100, 2), - 'profit_abs': profit_abs, - 'count': count + "enter_tag": enter_tag if enter_tag is not None else "Other", + "profit_ratio": profit, + "profit_pct": round(profit * 100, 2), + "profit_abs": profit_abs, + "count": count, } for enter_tag, profit, profit_abs, count in enter_tag_perf ] @@ -1844,26 +1899,27 @@ class Trade(ModelBase, LocalTrade): """ filters: List = [Trade.is_open.is_(False)] - if (pair is not None): + if pair is not None: filters.append(Trade.pair == pair) sell_tag_perf = Trade.session.execute( select( Trade.exit_reason, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(*filters) + func.sum(Trade.close_profit).label("profit_sum"), + func.sum(Trade.close_profit_abs).label("profit_sum_abs"), + func.count(Trade.pair).label("count"), + ) + .filter(*filters) .group_by(Trade.exit_reason) - .order_by(desc('profit_sum_abs')) + .order_by(desc("profit_sum_abs")) ).all() return [ { - 'exit_reason': exit_reason if exit_reason is not None else "Other", - 'profit_ratio': profit, - 'profit_pct': round(profit * 100, 2), - 'profit_abs': profit_abs, - 'count': count + "exit_reason": exit_reason if exit_reason is not None else "Other", + "profit_ratio": profit, + "profit_pct": round(profit * 100, 2), + "profit_abs": profit_abs, + "count": count, } for exit_reason, profit, profit_abs, count in sell_tag_perf ] @@ -1877,19 +1933,20 @@ class Trade(ModelBase, LocalTrade): """ filters: List = [Trade.is_open.is_(False)] - if (pair is not None): + if pair is not None: filters.append(Trade.pair == pair) mix_tag_perf = Trade.session.execute( select( Trade.id, Trade.enter_tag, Trade.exit_reason, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(*filters) + func.sum(Trade.close_profit).label("profit_sum"), + func.sum(Trade.close_profit_abs).label("profit_sum_abs"), + func.count(Trade.pair).label("count"), + ) + .filter(*filters) .group_by(Trade.id) - .order_by(desc('profit_sum_abs')) + .order_by(desc("profit_sum_abs")) ).all() resp: List[Dict] = [] @@ -1897,24 +1954,28 @@ class Trade(ModelBase, LocalTrade): enter_tag = enter_tag if enter_tag is not None else "Other" exit_reason = exit_reason if exit_reason is not None else "Other" - if (exit_reason is not None and enter_tag is not None): + if exit_reason is not None and enter_tag is not None: mix_tag = enter_tag + " " + exit_reason i = 0 if not any(item["mix_tag"] == mix_tag for item in resp): - resp.append({'mix_tag': mix_tag, - 'profit_ratio': profit, - 'profit_pct': round(profit * 100, 2), - 'profit_abs': profit_abs, - 'count': count}) + resp.append( + { + "mix_tag": mix_tag, + "profit_ratio": profit, + "profit_pct": round(profit * 100, 2), + "profit_abs": profit_abs, + "count": count, + } + ) else: while i < len(resp): if resp[i]["mix_tag"] == mix_tag: resp[i] = { - 'mix_tag': mix_tag, - 'profit_ratio': profit + resp[i]["profit_ratio"], - 'profit_pct': round(profit + resp[i]["profit_ratio"] * 100, 2), - 'profit_abs': profit_abs + resp[i]["profit_abs"], - 'count': 1 + resp[i]["count"] + "mix_tag": mix_tag, + "profit_ratio": profit + resp[i]["profit_ratio"], + "profit_pct": round(profit + resp[i]["profit_ratio"] * 100, 2), + "profit_abs": profit_abs + resp[i]["profit_abs"], + "count": 1 + resp[i]["count"], } i += 1 @@ -1932,12 +1993,10 @@ class Trade(ModelBase, LocalTrade): filters.append(Trade.close_date >= start_date) best_pair = Trade.session.execute( - select( - Trade.pair, - func.sum(Trade.close_profit).label('profit_sum') - ).filter(*filters) + select(Trade.pair, func.sum(Trade.close_profit).label("profit_sum")) + .filter(*filters) .group_by(Trade.pair) - .order_by(desc('profit_sum')) + .order_by(desc("profit_sum")) ).first() return best_pair @@ -1949,15 +2008,10 @@ class Trade(ModelBase, LocalTrade): NOTE: Not supported in Backtesting. :returns: Tuple containing (pair, profit_sum) """ - filters = [ - Order.status == 'closed' - ] + filters = [Order.status == "closed"] if start_date: filters.append(Order.order_filled_date >= start_date) trading_volume = Trade.session.execute( - select( - func.sum(Order.cost).label('volume') - ).filter( - *filters - )).scalar_one() + select(func.sum(Order.cost).label("volume")).filter(*filters) + ).scalar_one() return trading_volume or 0.0 diff --git a/freqtrade/persistence/usedb_context.py b/freqtrade/persistence/usedb_context.py index 732f0b0f8..3266ca157 100644 --- a/freqtrade/persistence/usedb_context.py +++ b/freqtrade/persistence/usedb_context.py @@ -1,4 +1,3 @@ - from freqtrade.persistence.custom_data import CustomDataWrapper from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import Trade @@ -20,13 +19,13 @@ def enable_database_use() -> None: Cleanup function to restore database usage. """ PairLocks.use_db = True - PairLocks.timeframe = '' + PairLocks.timeframe = "" Trade.use_db = True CustomDataWrapper.use_db = True class FtNoDBContext: - def __init__(self, timeframe: str = ''): + def __init__(self, timeframe: str = ""): self.timeframe = timeframe def __enter__(self):