From 10917a280a2a72944e3425bc18b4ff2d04570331 Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Tue, 31 May 2022 12:26:07 +0300 Subject: [PATCH 001/327] Add initial structure and wrapping. --- freqtrade/optimize/backtesting.py | 5 +- freqtrade/persistence/__init__.py | 1 + freqtrade/persistence/keyvalue.py | 57 ++++++++++ freqtrade/persistence/keyvalue_middleware.py | 108 +++++++++++++++++++ freqtrade/persistence/models.py | 2 + freqtrade/persistence/trade_model.py | 19 ++++ 6 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 freqtrade/persistence/keyvalue.py create mode 100644 freqtrade/persistence/keyvalue_middleware.py diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4e604898f..c552d8790 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -30,7 +30,7 @@ from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_signal_candles, store_backtest_stats) -from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade +from freqtrade.persistence import KeyValues, LocalTrade, Order, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -151,6 +151,7 @@ class Backtesting: LoggingMixin.show_output = True PairLocks.use_db = True Trade.use_db = True + KeyValues.use_db = True # ??? def init_backtest_detail(self): # Load detail timeframe if specified @@ -294,6 +295,8 @@ class Backtesting: Trade.use_db = False PairLocks.reset_locks() Trade.reset_trades() + KeyValues.use_db = False + KeyValues.reset_keyvalues() self.rejected_trades = 0 self.timedout_entry_orders = 0 self.timedout_exit_orders = 0 diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index ab6e2f6a5..0158f588c 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 +from freqtrade.persistence.keyvalue_middleware import KeyValues from freqtrade.persistence.models import clean_dry_run_db, cleanup_db, init_db from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import LocalTrade, Order, Trade diff --git a/freqtrade/persistence/keyvalue.py b/freqtrade/persistence/keyvalue.py new file mode 100644 index 000000000..60fa903a1 --- /dev/null +++ b/freqtrade/persistence/keyvalue.py @@ -0,0 +1,57 @@ +from datetime import datetime +from typing import Optional + +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint +from sqlalchemy.orm import Query, relationship + +from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.persistence.base import _DECL_BASE + + +class KeyValue(_DECL_BASE): + """ + KeyValue database model + Keeps records of metadata as key/value store + for trades or global persistant values + One to many relationship with Trades: + - One trade can have many metadata entries + - One metadata entry can only be associated with one Trade + """ + __tablename__ = 'keyvalue' + # 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', 'kv_key', name="_trade_id_kv_key"),) + + id = Column(Integer, primary_key=True) + ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True, default=0) + + trade = relationship("Trade", back_populates="keyvalues") + + kv_key = Column(String(255), nullable=False) + kv_type = Column(String(25), nullable=False) + kv_value = Column(Text, nullable=False) + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + updated_at = Column(DateTime, nullable=True) + + 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'KeyValue(id={self.id}, key={self.kv_key}, type={self.kv_type}, ', + f'value={self.kv_value}, trade_id={self.ft_trade_id}, created={create_time}, ', + f'updated={update_time})') + + @staticmethod + def query_kv(key: Optional[str] = None, trade_id: Optional[int] = None) -> Query: + """ + Get all keyvalues, if trade_id is not specified + return will be for generic values not tied to a trade + :param trade_id: id of the Trade + """ + key = key if key is not None else "%" + + filters = [KeyValue.ft_trade_id == trade_id if trade_id is not None else 0, + KeyValue.kv_key.ilike(key)] + + return KeyValue.query.filter(*filters) diff --git a/freqtrade/persistence/keyvalue_middleware.py b/freqtrade/persistence/keyvalue_middleware.py new file mode 100644 index 000000000..24c74610a --- /dev/null +++ b/freqtrade/persistence/keyvalue_middleware.py @@ -0,0 +1,108 @@ +import json +import logging +from datetime import datetime +from typing import Any, List, Optional + +from freqtrade.persistence.keyvalue import KeyValue + + +logger = logging.getLogger(__name__) + + +class KeyValues(): + """ + KeyValues middleware class + Abstracts the database layer away so it becomes optional - which will be necessary to support + backtesting and hyperopt in the future. + """ + + use_db = True + kvals: List[KeyValue] = [] + unserialized_types = ['bool', 'float', 'int', 'str'] + + @staticmethod + def reset_keyvalues() -> None: + """ + Resets all key-value pairs. Only active for backtesting mode. + """ + if not KeyValues.use_db: + KeyValues.kvals = [] + + @staticmethod + def get_kval(key: Optional[str] = None, trade_id: Optional[int] = None) -> List[KeyValue]: + if trade_id is None: + trade_id = 0 + + if KeyValues.use_db: + filtered_kvals = KeyValue.query_kv(trade_id=trade_id, key=key).all() + for index, kval in enumerate(filtered_kvals): + if kval.kv_type not in KeyValues.unserialized_types: + kval.kv_value = json.loads(kval.kv_value) + filtered_kvals[index] = kval + return filtered_kvals + else: + filtered_kvals = [kval for kval in KeyValues.kvals if (kval.ft_trade_id == trade_id)] + if key is not None: + filtered_kvals = [ + kval for kval in filtered_kvals if (kval.kv_key.casefold() == key.casefold())] + return filtered_kvals + + @staticmethod + def set_kval(key: str, value: Any, trade_id: Optional[int] = None) -> None: + + logger.warning(f"[set_kval] key: {key} trade_id: {trade_id} value: {value}") + value_type = type(value).__name__ + value_db = None + + if value_type not in KeyValues.unserialized_types: + try: + value_db = json.dumps(value) + except TypeError as e: + logger.warning(f"could not serialize {key} value due to {e}") + else: + value_db = str(value) + + if trade_id is None: + trade_id = 0 + + kvals = KeyValues.get_kval(key=key, trade_id=trade_id) + if kvals: + kv = kvals[0] + kv.kv_value = value + kv.updated_at = datetime.utcnow() + else: + kv = KeyValue( + ft_trade_id=trade_id, + kv_key=key, + kv_type=value_type, + kv_value=value, + created_at=datetime.utcnow() + ) + + if KeyValues.use_db and value_db is not None: + kv.kv_value = value_db + KeyValue.query.session.add(kv) + KeyValue.query.session.commit() + elif not KeyValues.use_db: + kv_index = -1 + for index, kval in enumerate(KeyValues.kvals): + if kval.ft_trade_id == trade_id and kval.kv_key == key: + kv_index = index + break + + if kv_index >= 0: + kval.kv_type = value_type + kval.value = value + kval.updated_at = datetime.utcnow() + + KeyValues.kvals[kv_index] = kval + else: + KeyValues.kvals.append(kv) + + @staticmethod + def get_all_kvals() -> List[KeyValue]: + + if KeyValues.use_db: + return KeyValue.query.all() + else: + return KeyValues.kvals diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index c31e50892..6a279ab5c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -10,6 +10,7 @@ from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException from freqtrade.persistence.base import _DECL_BASE +from freqtrade.persistence.keyvalue import KeyValue from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock from freqtrade.persistence.trade_model import Order, Trade @@ -59,6 +60,7 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: Trade.query = Trade._session.query_property() Order.query = Trade._session.query_property() PairLock.query = Trade._session.query_property() + KeyValue.query = Trade._session.query_property() previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 358e776e3..b097f6574 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -15,6 +15,8 @@ from freqtrade.enums import ExitType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.leverage import interest from freqtrade.persistence.base import _DECL_BASE +from freqtrade.persistence.keyvalue import KeyValue +from freqtrade.persistence.keyvalue_middleware import KeyValues logger = logging.getLogger(__name__) @@ -206,6 +208,7 @@ class LocalTrade(): id: int = 0 orders: List[Order] = [] + keyvalues: List[KeyValue] = [] exchange: str = '' pair: str = '' @@ -870,6 +873,12 @@ class LocalTrade(): (o.filled or 0) > 0 and o.status in NON_OPEN_EXCHANGE_STATES] + def set_kval(self, key: str, value: Any) -> None: + KeyValues.set_kval(key=key, value=value, trade_id=self.id) + + def get_kval(self, key: Optional[str]) -> List[KeyValue]: + return KeyValues.get_kval(key=key, trade_id=self.id) + @property def nr_of_successful_entries(self) -> int: """ @@ -1000,6 +1009,7 @@ class Trade(_DECL_BASE, LocalTrade): id = Column(Integer, primary_key=True) orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan", lazy="joined") + keyvalues = relationship("KeyValue", order_by="KeyValue.id", cascade="all, delete-orphan") exchange = Column(String(25), nullable=False) pair = Column(String(25), nullable=False, index=True) @@ -1070,6 +1080,9 @@ class Trade(_DECL_BASE, LocalTrade): for order in self.orders: Order.query.session.delete(order) + for kval in self.keyvalues: + KeyValue.query.session.delete(kval) + Trade.query.session.delete(self) Trade.commit() @@ -1345,3 +1358,9 @@ class Trade(_DECL_BASE, LocalTrade): .group_by(Trade.pair) \ .order_by(desc('profit_sum')).first() return best_pair + + def set_kval(self, key: str, value: Any) -> None: + super().set_kval(key=key, value=value) + + def get_kval(self, key: Optional[str]) -> List[KeyValue]: + return super().get_kval(key=key) From 096e98a68c1e6025da4ffc688928a6e6d27cb20f Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Tue, 31 May 2022 16:16:57 +0300 Subject: [PATCH 002/327] Remove stray debug message. --- freqtrade/persistence/keyvalue_middleware.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/persistence/keyvalue_middleware.py b/freqtrade/persistence/keyvalue_middleware.py index 24c74610a..8248143ce 100644 --- a/freqtrade/persistence/keyvalue_middleware.py +++ b/freqtrade/persistence/keyvalue_middleware.py @@ -50,7 +50,6 @@ class KeyValues(): @staticmethod def set_kval(key: str, value: Any, trade_id: Optional[int] = None) -> None: - logger.warning(f"[set_kval] key: {key} trade_id: {trade_id} value: {value}") value_type = type(value).__name__ value_db = None From de01aaf290a965e071b083ae28ce1db31d3f97bf Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Tue, 31 May 2022 16:17:31 +0300 Subject: [PATCH 003/327] Add documentation details. --- docs/strategy-advanced.md | 70 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 374c675a2..45961c59d 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -11,7 +11,7 @@ If you're just getting started, please be familiar with the methods described in !!! Tip You can get a strategy template containing all below methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` -## Storing information +## Storing information (Non-Persistent) Storing information can be accomplished by creating a new dictionary within the strategy class. @@ -40,6 +40,74 @@ class AwesomeStrategy(IStrategy): !!! Note If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. +## Storing information (Persistent) + +Storing information can also be performed in a persistent manner. Freqtrade allows storing/retrieving user custom information associated with a specific trade. +Using a trade object handle information can be stored using `trade_obj.set_kval(key='my_key', value=my_value)` and retrieved using `trade_obj.get_kval(key='my_key')`. +Each data entry is associated with a trade and a user supplied key (of type `string`). This means that this can only be used in callbacks that also provide a trade object handle. +For the data to be able to be stored within the database it must be serialized. This is done by converting it to a JSON formatted string. + +```python +from freqtrade.persistence import Trade +from datetime import timedelta + +class AwesomeStrategy(IStrategy): + + def bot_loop_start(self, **kwargs) -> None: + for trade in Trade.get_open_order_trades(): + fills = trade.select_filled_orders(trade.entry_side) + if trade.pair == 'ETH/USDT': + trade_entry_type = trade.get_kval(key='entry_type') + if trade_entry_type is None: + trade_entry_type = 'breakout' if 'entry_1' in trade.enter_tag else 'dip' + elif fills > 1: + trade_entry_type = 'buy_up' + trade.set_kval(key='entry_type', value=trade_entry_type) + return super().bot_loop_start(**kwargs) + + def adjust_entry_price(self, trade: Trade, order: Optional[Order], pair: str, + current_time: datetime, proposed_rate: float, current_order_rate: float, + entry_tag: Optional[str], side: str, **kwargs) -> float: + # Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair. + if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10) > trade.open_date_utc and order.filled == 0.0: + dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) + current_candle = dataframe.iloc[-1].squeeze() + # store information about entry adjustment + existing_count = trade.get_kval(key='num_entry_adjustments') + if not existing_count: + existing_count = 1 + else: + existing_count += 1 + trade.set_kval(key='num_entry_adjustments', value=existing_count) + + # adjust order price + return current_candle['sma_200'] + + # default: maintain existing order + return current_order_rate + + def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs): + + entry_adjustment_count = trade.get_kval(key='num_entry_adjustments') + trade_entry_type = trade.get_kval(key='entry_type') + if entry_adjustment_count is None: + if current_profit > 0.01 and (current_time - timedelta(minutes=100) > trade.open_date_utc): + return True, 'exit_1' + else + if entry_adjustment_count > 0 and if current_profit > 0.05: + return True, 'exit_2' + if trade_entry_type == 'breakout' and current_profit > 0.1: + return True, 'exit_3 + + return False, None +``` + +!!! Note + It is recommended that simple data types are used `[bool, int, float, str]` to ensure no issues when serializing the data that needs to be stored. + +!!! Warning + If supplied data cannot be serialized a warning is logged and the entry for the specified `key` will contain `None` as data. + ## Dataframe access You may access dataframe in various strategy functions by querying it from dataprovider. From abda02572b4248391dc2a9f4b9bc98f095735beb Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Sun, 5 Jun 2022 12:18:07 +0300 Subject: [PATCH 004/327] Fix KeyValue __repr__. --- freqtrade/persistence/keyvalue.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/keyvalue.py b/freqtrade/persistence/keyvalue.py index 60fa903a1..2ed64f255 100644 --- a/freqtrade/persistence/keyvalue.py +++ b/freqtrade/persistence/keyvalue.py @@ -38,8 +38,8 @@ class KeyValue(_DECL_BASE): 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'KeyValue(id={self.id}, key={self.kv_key}, type={self.kv_type}, ', - f'value={self.kv_value}, trade_id={self.ft_trade_id}, created={create_time}, ', + return (f'KeyValue(id={self.id}, key={self.kv_key}, type={self.kv_type}, ' + + f'value={self.kv_value}, trade_id={self.ft_trade_id}, created={create_time}, ' + f'updated={update_time})') @staticmethod From be169a23f4e3ed307871a45635b3da89f347a133 Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Mon, 13 Jun 2022 20:00:21 +0300 Subject: [PATCH 005/327] Add a new session for KeyValues. --- freqtrade/persistence/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f2d75fec7..5ba0a28bd 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -58,7 +58,8 @@ def init_db(db_url: str) -> None: Trade.query = Trade._session.query_property() Order.query = Trade._session.query_property() PairLock.query = Trade._session.query_property() - KeyValue.query = Trade._session.query_property() + KeyValue._session = scoped_session(sessionmaker(bind=engine, autoflush=True)) + KeyValue.query = KeyValue._session.query_property() previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) From f3dee5ec4f98592c1363f5c53c6f375f1476f4fd Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Mon, 13 Jun 2022 20:02:06 +0300 Subject: [PATCH 006/327] Update handling for query_kv when no Key is supplied. --- freqtrade/persistence/keyvalue.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/keyvalue.py b/freqtrade/persistence/keyvalue.py index 2ed64f255..d3d1454b7 100644 --- a/freqtrade/persistence/keyvalue.py +++ b/freqtrade/persistence/keyvalue.py @@ -49,9 +49,9 @@ class KeyValue(_DECL_BASE): return will be for generic values not tied to a trade :param trade_id: id of the Trade """ - key = key if key is not None else "%" - - filters = [KeyValue.ft_trade_id == trade_id if trade_id is not None else 0, - KeyValue.kv_key.ilike(key)] + filters = [] + filters.append(KeyValue.ft_trade_id == trade_id if trade_id is not None else 0) + if key is not None: + filters.append(KeyValue.kv_key.ilike(key)) return KeyValue.query.filter(*filters) From c719860a164a95a0718bc4437476396198e5b093 Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Mon, 13 Jun 2022 20:03:22 +0300 Subject: [PATCH 007/327] get_kval() -> get_kvals(). Update docs also. --- docs/strategy-advanced.md | 10 +++++----- freqtrade/persistence/trade_model.py | 13 +++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 45961c59d..765dd3fab 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -43,7 +43,7 @@ class AwesomeStrategy(IStrategy): ## Storing information (Persistent) Storing information can also be performed in a persistent manner. Freqtrade allows storing/retrieving user custom information associated with a specific trade. -Using a trade object handle information can be stored using `trade_obj.set_kval(key='my_key', value=my_value)` and retrieved using `trade_obj.get_kval(key='my_key')`. +Using a trade object handle information can be stored using `trade_obj.set_kval(key='my_key', value=my_value)` and retrieved using `trade_obj.get_kvals(key='my_key')`. Each data entry is associated with a trade and a user supplied key (of type `string`). This means that this can only be used in callbacks that also provide a trade object handle. For the data to be able to be stored within the database it must be serialized. This is done by converting it to a JSON formatted string. @@ -57,7 +57,7 @@ class AwesomeStrategy(IStrategy): for trade in Trade.get_open_order_trades(): fills = trade.select_filled_orders(trade.entry_side) if trade.pair == 'ETH/USDT': - trade_entry_type = trade.get_kval(key='entry_type') + trade_entry_type = trade.get_kvals(key='entry_type').kv_value if trade_entry_type is None: trade_entry_type = 'breakout' if 'entry_1' in trade.enter_tag else 'dip' elif fills > 1: @@ -73,7 +73,7 @@ class AwesomeStrategy(IStrategy): dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) current_candle = dataframe.iloc[-1].squeeze() # store information about entry adjustment - existing_count = trade.get_kval(key='num_entry_adjustments') + existing_count = trade.get_kvals(key='num_entry_adjustments').kv_value if not existing_count: existing_count = 1 else: @@ -88,8 +88,8 @@ class AwesomeStrategy(IStrategy): def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs): - entry_adjustment_count = trade.get_kval(key='num_entry_adjustments') - trade_entry_type = trade.get_kval(key='entry_type') + entry_adjustment_count = trade.get_kvals(key='num_entry_adjustments').kv_value + trade_entry_type = trade.get_kvals(key='entry_type').kv_value if entry_adjustment_count is None: if current_profit > 0.01 and (current_time - timedelta(minutes=100) > trade.open_date_utc): return True, 'exit_1' diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 83d400412..ce9fde59e 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -923,7 +923,7 @@ class LocalTrade(): def set_kval(self, key: str, value: Any) -> None: KeyValues.set_kval(key=key, value=value, trade_id=self.id) - def get_kval(self, key: Optional[str]) -> List[KeyValue]: + def get_kvals(self, key: Optional[str]) -> List[KeyValue]: return KeyValues.get_kval(key=key, trade_id=self.id) @property @@ -1127,12 +1127,13 @@ class Trade(_DECL_BASE, LocalTrade): for order in self.orders: Order.query.session.delete(order) - for kval in self.keyvalues: - KeyValue.query.session.delete(kval) - Trade.query.session.delete(self) Trade.commit() + for kval in self.keyvalues: + KeyValue.query.session.delete(kval) + KeyValue.query.session.commit() + @staticmethod def commit(): Trade.query.session.commit() @@ -1409,5 +1410,5 @@ class Trade(_DECL_BASE, LocalTrade): def set_kval(self, key: str, value: Any) -> None: super().set_kval(key=key, value=value) - def get_kval(self, key: Optional[str]) -> List[KeyValue]: - return super().get_kval(key=key) + def get_kvals(self, key: Optional[str]) -> List[KeyValue]: + return super().get_kvals(key=key) From 4f799cc9db8ed061af8385a775f9c1df67e421f0 Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Mon, 13 Jun 2022 20:04:14 +0300 Subject: [PATCH 008/327] Add /list_kvals command for TG and underlying RPC. --- freqtrade/rpc/rpc.py | 20 +++++++++++++ freqtrade/rpc/telegram.py | 51 +++++++++++++++++++++++++++++++++- tests/rpc/test_rpc_telegram.py | 2 +- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a98e3f96d..929ab4150 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -843,6 +843,26 @@ class RPC: 'cancel_order_count': c_count, } + def _rpc_list_kvals(self, trade_id: int, key: Optional[str]) -> List[Dict[str, Any]]: + # Query for trade + trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() + if trade is None: + return [] + # Query keyvals + keyvals = trade.get_kvals(key=key) + return [ + { + 'id': kval.id, + 'ft_trade_id': kval.ft_trade_id, + 'kv_key': kval.kv_key, + 'kv_type': kval.kv_type, + 'kv_value': kval.kv_value, + 'created_at': kval.created_at, + 'updated_at': kval.updated_at + } + for kval in keyvals + ] + def _rpc_performance(self) -> List[Dict[str, Any]]: """ Handler for performance. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e456b1eef..f5bed167d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -182,6 +182,7 @@ class Telegram(RPCHandler): CommandHandler('health', self._health), CommandHandler('help', self._help), CommandHandler('version', self._version), + CommandHandler('list_kvals', self._list_kvals), ] callbacks = [ CallbackQueryHandler(self._status_table, pattern='update_status_table'), @@ -1459,7 +1460,9 @@ class Telegram(RPCHandler): "*/stats:* `Shows Wins / losses by Sell reason as well as " "Avg. holding durationsfor buys and sells.`\n" "*/help:* `This help message`\n" - "*/version:* `Show version`" + "*/version:* `Show version`\n" + "*/list_kvals :* `List key-value for Trade ID and Key combo.`\n" + "`If no Key is supplied it will list all key-value pairs found for that Trade ID.`" ) self._send_msg(message, parse_mode=ParseMode.MARKDOWN) @@ -1539,6 +1542,52 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) + @authorized_only + def _list_kvals(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /list_kvals . + List keyvalues for specified trade (and key if supplied). + :param bot: telegram bot + :param update: message update + :return: None + """ + try: + if not context.args or len(context.args) == 0: + raise RPCException("Trade-id not set.") + trade_id = int(context.args[0]) + key = None if len(context.args) < 2 else str(context.args[1]) + + results = self._rpc._rpc_list_kvals(trade_id, key) + logger.warning(len(results)) + logger.warning(results) + messages = [] + if len(results) > 0: + messages = ['Found key-value pair' + 's: \n' if key is None else ': \n'] + for result in results: + lines = [ + f"*Key:* `{result['kv_key']}`", + f"*ID:* `{result['id']}`", + f"*Trade ID:* `{result['ft_trade_id']}`", + f"*Type:* `{result['kv_type']}`", + f"*Value:* `{result['kv_value']}`", + f"*Create Date:* `{result['created_at']}`", + f"*Update Date:* `{result['updated_at']}`" + ] + # Filter empty lines using list-comprehension + messages.append("\n".join([line for line in lines if line])) + for msg in messages: + logger.warning(msg) + self._send_msg(msg) + else: + message = f"Didn't find any key-value pairs for Trade ID: `{trade_id}`" + logger.warning(message) + message += f" and Key: `{key}`." if key is not None else "" + logger.warning(message) + self._send_msg(message) + + except RPCException as e: + self._send_msg(str(e)) + def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: if reload_able: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2bc4fc5c3..ee0bac9e5 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -102,7 +102,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['count'], ['locks'], ['unlock', 'delete_locks'], " "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " "['stopbuy'], ['whitelist'], ['blacklist'], ['blacklist_delete', 'bl_delete'], " - "['logs'], ['edge'], ['health'], ['help'], ['version']" + "['logs'], ['edge'], ['health'], ['help'], ['version'], ['list_kvals']" "]") assert log_has(message_str, caplog) From 3ad8111d118437d697a95543b4b6bf3b8f7dcab4 Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Tue, 14 Jun 2022 13:26:45 +0300 Subject: [PATCH 009/327] Remove stray debug messages. --- freqtrade/rpc/telegram.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a64242511..c29ec6daa 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1536,8 +1536,6 @@ class Telegram(RPCHandler): key = None if len(context.args) < 2 else str(context.args[1]) results = self._rpc._rpc_list_kvals(trade_id, key) - logger.warning(len(results)) - logger.warning(results) messages = [] if len(results) > 0: messages = ['Found key-value pair' + 's: \n' if key is None else ': \n'] @@ -1554,13 +1552,11 @@ class Telegram(RPCHandler): # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line])) for msg in messages: - logger.warning(msg) self._send_msg(msg) else: message = f"Didn't find any key-value pairs for Trade ID: `{trade_id}`" logger.warning(message) message += f" and Key: `{key}`." if key is not None else "" - logger.warning(message) self._send_msg(message) except RPCException as e: From 24b6ce450b3ce4c95bb808686bdbbba1ade3662a Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Tue, 14 Jun 2022 13:27:50 +0300 Subject: [PATCH 010/327] Further cleanup. --- freqtrade/rpc/telegram.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index c29ec6daa..665621975 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1555,7 +1555,6 @@ class Telegram(RPCHandler): self._send_msg(msg) else: message = f"Didn't find any key-value pairs for Trade ID: `{trade_id}`" - logger.warning(message) message += f" and Key: `{key}`." if key is not None else "" self._send_msg(message) From 9fdb8b07accd04b4f629d075d52d6339e02ebaa2 Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Sun, 19 Jun 2022 15:56:50 +0300 Subject: [PATCH 011/327] Rename persistant storage infrastructure. --- docs/strategy-advanced.md | 14 +-- freqtrade/optimize/backtesting.py | 8 +- freqtrade/persistence/__init__.py | 2 +- freqtrade/persistence/keyvalue.py | 30 +++--- freqtrade/persistence/keyvalue_middleware.py | 108 ++++++++++--------- freqtrade/persistence/models.py | 6 +- freqtrade/persistence/trade_model.py | 30 +++--- freqtrade/rpc/rpc.py | 22 ++-- freqtrade/rpc/telegram.py | 18 ++-- tests/rpc/test_rpc_telegram.py | 2 +- 10 files changed, 123 insertions(+), 117 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 765dd3fab..9cd05d4f6 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -43,7 +43,7 @@ class AwesomeStrategy(IStrategy): ## Storing information (Persistent) Storing information can also be performed in a persistent manner. Freqtrade allows storing/retrieving user custom information associated with a specific trade. -Using a trade object handle information can be stored using `trade_obj.set_kval(key='my_key', value=my_value)` and retrieved using `trade_obj.get_kvals(key='my_key')`. +Using a trade object handle information can be stored using `trade_obj.set_custom_data(key='my_key', value=my_value)` and retrieved using `trade_obj.get_custom_data(key='my_key')`. Each data entry is associated with a trade and a user supplied key (of type `string`). This means that this can only be used in callbacks that also provide a trade object handle. For the data to be able to be stored within the database it must be serialized. This is done by converting it to a JSON formatted string. @@ -57,12 +57,12 @@ class AwesomeStrategy(IStrategy): for trade in Trade.get_open_order_trades(): fills = trade.select_filled_orders(trade.entry_side) if trade.pair == 'ETH/USDT': - trade_entry_type = trade.get_kvals(key='entry_type').kv_value + trade_entry_type = trade.get_custom_data(key='entry_type').kv_value if trade_entry_type is None: trade_entry_type = 'breakout' if 'entry_1' in trade.enter_tag else 'dip' elif fills > 1: trade_entry_type = 'buy_up' - trade.set_kval(key='entry_type', value=trade_entry_type) + trade.set_custom_data(key='entry_type', value=trade_entry_type) return super().bot_loop_start(**kwargs) def adjust_entry_price(self, trade: Trade, order: Optional[Order], pair: str, @@ -73,12 +73,12 @@ class AwesomeStrategy(IStrategy): dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) current_candle = dataframe.iloc[-1].squeeze() # store information about entry adjustment - existing_count = trade.get_kvals(key='num_entry_adjustments').kv_value + existing_count = trade.get_custom_data(key='num_entry_adjustments').kv_value if not existing_count: existing_count = 1 else: existing_count += 1 - trade.set_kval(key='num_entry_adjustments', value=existing_count) + trade.set_custom_data(key='num_entry_adjustments', value=existing_count) # adjust order price return current_candle['sma_200'] @@ -88,8 +88,8 @@ class AwesomeStrategy(IStrategy): def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs): - entry_adjustment_count = trade.get_kvals(key='num_entry_adjustments').kv_value - trade_entry_type = trade.get_kvals(key='entry_type').kv_value + entry_adjustment_count = trade.get_custom_data(key='num_entry_adjustments').kv_value + trade_entry_type = trade.get_custom_data(key='entry_type').kv_value if entry_adjustment_count is None: if current_profit > 0.01 and (current_time - timedelta(minutes=100) > trade.open_date_utc): return True, 'exit_1' diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 14cc8d2ef..3071fb019 100755 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -30,7 +30,7 @@ from freqtrade.optimize.bt_progress import BTProgress from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_signal_candles, store_backtest_stats) -from freqtrade.persistence import KeyValues, LocalTrade, Order, PairLocks, Trade +from freqtrade.persistence import CustomDataWrapper, LocalTrade, Order, PairLocks, Trade from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -151,7 +151,7 @@ class Backtesting: LoggingMixin.show_output = True PairLocks.use_db = True Trade.use_db = True - KeyValues.use_db = True # ??? + CustomDataWrapper.use_db = True def init_backtest_detail(self): # Load detail timeframe if specified @@ -300,8 +300,8 @@ class Backtesting: Trade.use_db = False PairLocks.reset_locks() Trade.reset_trades() - KeyValues.use_db = False - KeyValues.reset_keyvalues() + CustomDataWrapper.use_db = False + CustomDataWrapper.reset_custom_data() self.rejected_trades = 0 self.timedout_entry_orders = 0 self.timedout_exit_orders = 0 diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 635445e40..12cb68a10 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa: F401 -from freqtrade.persistence.keyvalue_middleware import KeyValues +from freqtrade.persistence.keyvalue_middleware import CustomDataWrapper from freqtrade.persistence.models import cleanup_db, init_db from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import LocalTrade, Order, Trade diff --git a/freqtrade/persistence/keyvalue.py b/freqtrade/persistence/keyvalue.py index d3d1454b7..1f85467dd 100644 --- a/freqtrade/persistence/keyvalue.py +++ b/freqtrade/persistence/keyvalue.py @@ -8,28 +8,28 @@ from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.persistence.base import _DECL_BASE -class KeyValue(_DECL_BASE): +class CustomData(_DECL_BASE): """ - KeyValue database model + CustomData database model Keeps records of metadata as key/value store for trades or global persistant values One to many relationship with Trades: - One trade can have many metadata entries - One metadata entry can only be associated with one Trade """ - __tablename__ = 'keyvalue' + __tablename__ = 'trade_custom_data' # 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', 'kv_key', name="_trade_id_kv_key"),) + __table_args__ = (UniqueConstraint('ft_trade_id', 'cd_key', name="_trade_id_cd_key"),) id = Column(Integer, primary_key=True) ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True, default=0) - trade = relationship("Trade", back_populates="keyvalues") + trade = relationship("Trade", back_populates="custom_data") - kv_key = Column(String(255), nullable=False) - kv_type = Column(String(25), nullable=False) - kv_value = Column(Text, nullable=False) + cd_key = Column(String(255), nullable=False) + cd_type = Column(String(25), nullable=False) + cd_value = Column(Text, nullable=False) created_at = Column(DateTime, nullable=False, default=datetime.utcnow) updated_at = Column(DateTime, nullable=True) @@ -38,20 +38,20 @@ class KeyValue(_DECL_BASE): 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'KeyValue(id={self.id}, key={self.kv_key}, type={self.kv_type}, ' + - f'value={self.kv_value}, trade_id={self.ft_trade_id}, created={create_time}, ' + + 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})') @staticmethod - def query_kv(key: Optional[str] = None, trade_id: Optional[int] = None) -> Query: + def query_cd(key: Optional[str] = None, trade_id: Optional[int] = None) -> Query: """ - Get all keyvalues, if trade_id is not specified + Get all CustomData, if trade_id is not specified return will be for generic values not tied to a trade :param trade_id: id of the Trade """ filters = [] - filters.append(KeyValue.ft_trade_id == trade_id if trade_id is not None else 0) + filters.append(CustomData.ft_trade_id == trade_id if trade_id is not None else 0) if key is not None: - filters.append(KeyValue.kv_key.ilike(key)) + filters.append(CustomData.cd_key.ilike(key)) - return KeyValue.query.filter(*filters) + return CustomData.query.filter(*filters) diff --git a/freqtrade/persistence/keyvalue_middleware.py b/freqtrade/persistence/keyvalue_middleware.py index 8248143ce..0f3c745ad 100644 --- a/freqtrade/persistence/keyvalue_middleware.py +++ b/freqtrade/persistence/keyvalue_middleware.py @@ -3,57 +3,63 @@ import logging from datetime import datetime from typing import Any, List, Optional -from freqtrade.persistence.keyvalue import KeyValue +from freqtrade.persistence.keyvalue import CustomData logger = logging.getLogger(__name__) -class KeyValues(): +class CustomDataWrapper(): """ - KeyValues middleware class + CustomData middleware class Abstracts the database layer away so it becomes optional - which will be necessary to support backtesting and hyperopt in the future. """ use_db = True - kvals: List[KeyValue] = [] + custom_data: List[CustomData] = [] unserialized_types = ['bool', 'float', 'int', 'str'] @staticmethod - def reset_keyvalues() -> None: + def reset_custom_data() -> None: """ Resets all key-value pairs. Only active for backtesting mode. """ - if not KeyValues.use_db: - KeyValues.kvals = [] + if not CustomDataWrapper.use_db: + CustomDataWrapper.custom_data = [] @staticmethod - def get_kval(key: Optional[str] = None, trade_id: Optional[int] = None) -> List[KeyValue]: + def get_custom_data(key: Optional[str] = None, + trade_id: Optional[int] = None) -> List[CustomData]: if trade_id is None: trade_id = 0 - if KeyValues.use_db: - filtered_kvals = KeyValue.query_kv(trade_id=trade_id, key=key).all() - for index, kval in enumerate(filtered_kvals): - if kval.kv_type not in KeyValues.unserialized_types: - kval.kv_value = json.loads(kval.kv_value) - filtered_kvals[index] = kval - return filtered_kvals + if CustomDataWrapper.use_db: + filtered_custom_data = CustomData.query_cd(trade_id=trade_id, key=key).all() + for index, data_entry in enumerate(filtered_custom_data): + if data_entry.cd_type not in CustomDataWrapper.unserialized_types: + data_entry.cd_value = json.loads(data_entry.cd_value) + filtered_custom_data[index] = data_entry + return filtered_custom_data else: - filtered_kvals = [kval for kval in KeyValues.kvals if (kval.ft_trade_id == trade_id)] + filtered_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_kvals = [ - kval for kval in filtered_kvals if (kval.kv_key.casefold() == key.casefold())] - return filtered_kvals + filtered_custom_data = [ + data_entry for data_entry in filtered_custom_data + if (data_entry.cd_key.casefold() == key.casefold()) + ] + return filtered_custom_data @staticmethod - def set_kval(key: str, value: Any, trade_id: Optional[int] = None) -> None: + def set_custom_data(key: str, value: Any, trade_id: Optional[int] = None) -> None: value_type = type(value).__name__ value_db = None - if value_type not in KeyValues.unserialized_types: + if value_type not in CustomDataWrapper.unserialized_types: try: value_db = json.dumps(value) except TypeError as e: @@ -64,44 +70,44 @@ class KeyValues(): if trade_id is None: trade_id = 0 - kvals = KeyValues.get_kval(key=key, trade_id=trade_id) - if kvals: - kv = kvals[0] - kv.kv_value = value - kv.updated_at = datetime.utcnow() + custom_data = CustomDataWrapper.get_custom_data(key=key, trade_id=trade_id) + if custom_data: + data_entry = custom_data[0] + data_entry.cd_value = value + data_entry.updated_at = datetime.utcnow() else: - kv = KeyValue( - ft_trade_id=trade_id, - kv_key=key, - kv_type=value_type, - kv_value=value, - created_at=datetime.utcnow() + data_entry = CustomData( + ft_trade_id=trade_id, + cd_key=key, + cd_type=value_type, + cd_value=value, + created_at=datetime.utcnow() ) - if KeyValues.use_db and value_db is not None: - kv.kv_value = value_db - KeyValue.query.session.add(kv) - KeyValue.query.session.commit() - elif not KeyValues.use_db: - kv_index = -1 - for index, kval in enumerate(KeyValues.kvals): - if kval.ft_trade_id == trade_id and kval.kv_key == key: - kv_index = index + if CustomDataWrapper.use_db and value_db is not None: + data_entry.cd_value = value_db + CustomData.query.session.add(data_entry) + CustomData.query.session.commit() + elif not CustomDataWrapper.use_db: + cd_index = -1 + for index, data_entry in enumerate(CustomDataWrapper.custom_data): + if data_entry.ft_trade_id == trade_id and data_entry.cd_key == key: + cd_index = index break - if kv_index >= 0: - kval.kv_type = value_type - kval.value = value - kval.updated_at = datetime.utcnow() + if cd_index >= 0: + data_entry.cd_type = value_type + data_entry.value = value + data_entry.updated_at = datetime.utcnow() - KeyValues.kvals[kv_index] = kval + CustomDataWrapper.custom_data[cd_index] = data_entry else: - KeyValues.kvals.append(kv) + CustomDataWrapper.custom_data.append(data_entry) @staticmethod - def get_all_kvals() -> List[KeyValue]: + def get_all_custom_data() -> List[CustomData]: - if KeyValues.use_db: - return KeyValue.query.all() + if CustomDataWrapper.use_db: + return CustomData.query.all() else: - return KeyValues.kvals + return CustomDataWrapper.custom_data diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5ba0a28bd..a4c01b119 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -10,7 +10,7 @@ from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException from freqtrade.persistence.base import _DECL_BASE -from freqtrade.persistence.keyvalue import KeyValue +from freqtrade.persistence.keyvalue import CustomData from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock from freqtrade.persistence.trade_model import Order, Trade @@ -58,8 +58,8 @@ def init_db(db_url: str) -> None: Trade.query = Trade._session.query_property() Order.query = Trade._session.query_property() PairLock.query = Trade._session.query_property() - KeyValue._session = scoped_session(sessionmaker(bind=engine, autoflush=True)) - KeyValue.query = KeyValue._session.query_property() + CustomData._session = scoped_session(sessionmaker(bind=engine, autoflush=True)) + CustomData.query = CustomData._session.query_property() previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 53abe638b..ac7ba4833 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -15,8 +15,8 @@ from freqtrade.enums import ExitType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.leverage import interest from freqtrade.persistence.base import _DECL_BASE -from freqtrade.persistence.keyvalue import KeyValue -from freqtrade.persistence.keyvalue_middleware import KeyValues +from freqtrade.persistence.keyvalue import CustomData +from freqtrade.persistence.keyvalue_middleware import CustomDataWrapper logger = logging.getLogger(__name__) @@ -240,7 +240,7 @@ class LocalTrade(): id: int = 0 orders: List[Order] = [] - keyvalues: List[KeyValue] = [] + custom_data: List[CustomData] = [] exchange: str = '' pair: str = '' @@ -880,11 +880,11 @@ class LocalTrade(): or (o.ft_is_open is True and o.status is not None) ] - def set_kval(self, key: str, value: Any) -> None: - KeyValues.set_kval(key=key, value=value, trade_id=self.id) + def set_custom_data(self, key: str, value: Any) -> None: + CustomDataWrapper.set_custom_data(key=key, value=value, trade_id=self.id) - def get_kvals(self, key: Optional[str]) -> List[KeyValue]: - return KeyValues.get_kval(key=key, trade_id=self.id) + def get_custom_data(self, key: Optional[str]) -> List[CustomData]: + return CustomDataWrapper.get_custom_data(key=key, trade_id=self.id) @property def nr_of_successful_entries(self) -> int: @@ -1016,7 +1016,7 @@ class Trade(_DECL_BASE, LocalTrade): id = Column(Integer, primary_key=True) orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan", lazy="joined") - keyvalues = relationship("KeyValue", order_by="KeyValue.id", cascade="all, delete-orphan") + custom_data = relationship("CustomData", order_by="CustomData.id", cascade="all, delete-orphan") exchange = Column(String(25), nullable=False) pair = Column(String(25), nullable=False, index=True) @@ -1090,9 +1090,9 @@ class Trade(_DECL_BASE, LocalTrade): Trade.query.session.delete(self) Trade.commit() - for kval in self.keyvalues: - KeyValue.query.session.delete(kval) - KeyValue.query.session.commit() + for entry in self.custom_data: + CustomData.query.session.delete(entry) + CustomData.query.session.commit() @staticmethod def commit(): @@ -1367,11 +1367,11 @@ class Trade(_DECL_BASE, LocalTrade): .order_by(desc('profit_sum')).first() return best_pair - def set_kval(self, key: str, value: Any) -> None: - super().set_kval(key=key, value=value) + def set_custom_data(self, key: str, value: Any) -> None: + super().set_custom_data(key=key, value=value) - def get_kvals(self, key: Optional[str]) -> List[KeyValue]: - return super().get_kvals(key=key) + def get_custom_data(self, key: Optional[str]) -> List[CustomData]: + return super().get_custom_data(key=key) @staticmethod def get_trading_volume(start_date: datetime = datetime.fromtimestamp(0)) -> float: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 608f51bcd..cee2007ff 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -805,24 +805,24 @@ class RPC: 'cancel_order_count': c_count, } - def _rpc_list_kvals(self, trade_id: int, key: Optional[str]) -> List[Dict[str, Any]]: + def _rpc_list_custom_data(self, trade_id: int, key: Optional[str]) -> List[Dict[str, Any]]: # Query for trade trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() if trade is None: return [] - # Query keyvals - keyvals = trade.get_kvals(key=key) + # Query custom_data + custom_data = trade.get_custom_data(key=key) return [ { - 'id': kval.id, - 'ft_trade_id': kval.ft_trade_id, - 'kv_key': kval.kv_key, - 'kv_type': kval.kv_type, - 'kv_value': kval.kv_value, - 'created_at': kval.created_at, - 'updated_at': kval.updated_at + 'id': data_entry.id, + 'ft_trade_id': data_entry.ft_trade_id, + 'cd_key': data_entry.cd_key, + 'cd_type': data_entry.cd_type, + 'cd_value': data_entry.cd_value, + 'created_at': data_entry.created_at, + 'updated_at': data_entry.updated_at } - for kval in keyvals + for data_entry in custom_data ] def _rpc_performance(self) -> List[Dict[str, Any]]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ae4da9904..4af7c5d5d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -192,7 +192,7 @@ class Telegram(RPCHandler): CommandHandler('health', self._health), CommandHandler('help', self._help), CommandHandler('version', self._version), - CommandHandler('list_kvals', self._list_kvals), + CommandHandler('list_custom_data', self._list_custom_data), ] callbacks = [ CallbackQueryHandler(self._status_table, pattern='update_status_table'), @@ -1453,7 +1453,7 @@ class Telegram(RPCHandler): "Avg. holding durationsfor buys and sells.`\n" "*/help:* `This help message`\n" "*/version:* `Show version`\n" - "*/list_kvals :* `List key-value for Trade ID and Key combo.`\n" + "*/list_custom_data :* `List custom_data for Trade ID & Key combo.`\n" "`If no Key is supplied it will list all key-value pairs found for that Trade ID.`" ) @@ -1535,10 +1535,10 @@ class Telegram(RPCHandler): ) @authorized_only - def _list_kvals(self, update: Update, context: CallbackContext) -> None: + def _list_custom_data(self, update: Update, context: CallbackContext) -> None: """ - Handler for /list_kvals . - List keyvalues for specified trade (and key if supplied). + Handler for /list_custom_data . + List custom_data for specified trade (and key if supplied). :param bot: telegram bot :param update: message update :return: None @@ -1549,17 +1549,17 @@ class Telegram(RPCHandler): trade_id = int(context.args[0]) key = None if len(context.args) < 2 else str(context.args[1]) - results = self._rpc._rpc_list_kvals(trade_id, key) + results = self._rpc._rpc_list_custom_data(trade_id, key) messages = [] if len(results) > 0: messages = ['Found key-value pair' + 's: \n' if key is None else ': \n'] for result in results: lines = [ - f"*Key:* `{result['kv_key']}`", + f"*Key:* `{result['cd_key']}`", f"*ID:* `{result['id']}`", f"*Trade ID:* `{result['ft_trade_id']}`", - f"*Type:* `{result['kv_type']}`", - f"*Value:* `{result['kv_value']}`", + f"*Type:* `{result['cd_type']}`", + f"*Value:* `{result['cd_value']}`", f"*Create Date:* `{result['created_at']}`", f"*Update Date:* `{result['updated_at']}`" ] diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 789d10a02..39e33a8e3 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -103,7 +103,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['count'], ['locks'], ['unlock', 'delete_locks'], " "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " "['stopbuy'], ['whitelist'], ['blacklist'], ['blacklist_delete', 'bl_delete'], " - "['logs'], ['edge'], ['health'], ['help'], ['version'], ['list_kvals']" + "['logs'], ['edge'], ['health'], ['help'], ['version'], ['list_custom_data']" "]") assert log_has(message_str, caplog) From 365527508bbded7a6fb42e0d3351c018c682820a Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Sun, 19 Jun 2022 15:59:21 +0300 Subject: [PATCH 012/327] Rename files. --- freqtrade/persistence/{keyvalue.py => custom_data.py} | 0 .../{keyvalue_middleware.py => custom_data_middleware.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename freqtrade/persistence/{keyvalue.py => custom_data.py} (100%) rename freqtrade/persistence/{keyvalue_middleware.py => custom_data_middleware.py} (100%) diff --git a/freqtrade/persistence/keyvalue.py b/freqtrade/persistence/custom_data.py similarity index 100% rename from freqtrade/persistence/keyvalue.py rename to freqtrade/persistence/custom_data.py diff --git a/freqtrade/persistence/keyvalue_middleware.py b/freqtrade/persistence/custom_data_middleware.py similarity index 100% rename from freqtrade/persistence/keyvalue_middleware.py rename to freqtrade/persistence/custom_data_middleware.py From ce9d9d7e60b03566ecfab0f5eae6bf72eff89c01 Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Sun, 19 Jun 2022 16:02:24 +0300 Subject: [PATCH 013/327] Finish renaming persistant storage infrastructure. --- freqtrade/persistence/__init__.py | 2 +- freqtrade/persistence/custom_data_middleware.py | 2 +- freqtrade/persistence/models.py | 2 +- freqtrade/persistence/trade_model.py | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 12cb68a10..bf0a8dcbf 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa: F401 -from freqtrade.persistence.keyvalue_middleware import CustomDataWrapper +from freqtrade.persistence.custom_data_middleware import CustomDataWrapper from freqtrade.persistence.models import cleanup_db, init_db from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import LocalTrade, Order, Trade diff --git a/freqtrade/persistence/custom_data_middleware.py b/freqtrade/persistence/custom_data_middleware.py index 0f3c745ad..2fe4bd931 100644 --- a/freqtrade/persistence/custom_data_middleware.py +++ b/freqtrade/persistence/custom_data_middleware.py @@ -3,7 +3,7 @@ import logging from datetime import datetime from typing import Any, List, Optional -from freqtrade.persistence.keyvalue import CustomData +from freqtrade.persistence.custom_data import CustomData logger = logging.getLogger(__name__) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a4c01b119..16076adb9 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -10,7 +10,7 @@ from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException from freqtrade.persistence.base import _DECL_BASE -from freqtrade.persistence.keyvalue import CustomData +from freqtrade.persistence.custom_data import CustomData from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock from freqtrade.persistence.trade_model import Order, Trade diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index ac7ba4833..582e91d3d 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -15,8 +15,8 @@ from freqtrade.enums import ExitType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.leverage import interest from freqtrade.persistence.base import _DECL_BASE -from freqtrade.persistence.keyvalue import CustomData -from freqtrade.persistence.keyvalue_middleware import CustomDataWrapper +from freqtrade.persistence.custom_data import CustomData +from freqtrade.persistence.custom_data_middleware import CustomDataWrapper logger = logging.getLogger(__name__) From c8ba8106e668ea92c6f57d577f2d9e4ae750b4b3 Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Sun, 19 Jun 2022 17:24:13 +0300 Subject: [PATCH 014/327] Update telegram reporting. --- freqtrade/rpc/telegram.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4af7c5d5d..6bd68fd3d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1552,7 +1552,9 @@ class Telegram(RPCHandler): results = self._rpc._rpc_list_custom_data(trade_id, key) messages = [] if len(results) > 0: - messages = ['Found key-value pair' + 's: \n' if key is None else ': \n'] + messages.append( + 'Found custom-data entr' + ('ies: ' if len(results) > 1 else 'y: ') + ) for result in results: lines = [ f"*Key:* `{result['cd_key']}`", @@ -1568,7 +1570,7 @@ class Telegram(RPCHandler): for msg in messages: self._send_msg(msg) else: - message = f"Didn't find any key-value pairs for Trade ID: `{trade_id}`" + message = f"Didn't find any custom-data entries for Trade ID: `{trade_id}`" message += f" and Key: `{key}`." if key is not None else "" self._send_msg(message) From 8494bea64f0b7534553b28c15bf1b5a1a791e383 Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Sun, 19 Jun 2022 19:59:14 +0300 Subject: [PATCH 015/327] Handle max message length. --- freqtrade/rpc/telegram.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6bd68fd3d..6b3ccaac8 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1568,6 +1568,10 @@ class Telegram(RPCHandler): # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line])) for msg in messages: + if len(msg) > MAX_TELEGRAM_MESSAGE_LENGTH: + msg = "Message dropped because length exceeds " + msg += f"maximum allowed characters: {MAX_TELEGRAM_MESSAGE_LENGTH}" + logger.warning(msg) self._send_msg(msg) else: message = f"Didn't find any custom-data entries for Trade ID: `{trade_id}`" From c420304b33b94f67533864345201893ea28f2b9b Mon Sep 17 00:00:00 2001 From: eSeR1805 Date: Sun, 19 Jun 2022 20:03:56 +0300 Subject: [PATCH 016/327] Delete custom data before the trade. --- freqtrade/persistence/trade_model.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 582e91d3d..7e0314738 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1087,12 +1087,12 @@ class Trade(_DECL_BASE, LocalTrade): for order in self.orders: Order.query.session.delete(order) - Trade.query.session.delete(self) - Trade.commit() - for entry in self.custom_data: CustomData.query.session.delete(entry) + CustomData.query.session.commit() + Trade.query.session.delete(self) + Trade.commit() @staticmethod def commit(): From cac777cb214499b70f0cc187bc9d4888897fe3cc Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Wed, 4 Oct 2023 13:09:44 -0400 Subject: [PATCH 017/327] add property has_open_sl_orders to trade model --- freqtrade/persistence/trade_model.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 631585127..48fed1782 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -523,6 +523,17 @@ class LocalTrade: ] return len(open_orders_wo_sl) > 0 + @property + def has_open_sl_orders(self) -> int: + """ + 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 + ] + return len(open_sl_orders) > 0 + @property def open_orders_ids(self) -> List[str]: open_orders_ids_wo_sl = [ From 9214af69012a6e73503c174f54ff9534e053447d Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Thu, 5 Oct 2023 22:24:17 -0400 Subject: [PATCH 018/327] update cancel_stoploss_on_exchange to cancel all sl orders of trade --- freqtrade/freqtradebot.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 02d43432d..ebc146ede 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -904,18 +904,18 @@ class FreqtradeBot(LoggingMixin): def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade: # First cancelling stoploss on exchange ... - if trade.stoploss_order_id: - try: - logger.info(f"Canceling stoploss on exchange for {trade}") - co = self.exchange.cancel_stoploss_order_with_result( - trade.stoploss_order_id, trade.pair, trade.amount) - self.update_trade_state(trade, trade.stoploss_order_id, co, stoploss_order=True) - - # Reset stoploss order id. - trade.stoploss_order_id = None - except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id} " - f"for pair {trade.pair}") + if trade.has_open_sl_orders: + for o in trade.orders: + if o.ft_order_side == 'stoploss' and o.ft_is_open: + try: + logger.info(f"Canceling stoploss on exchange for {trade} " + f"order: {o.order_id}") + co = self.exchange.cancel_stoploss_order_with_result( + o.order_id, trade.pair, trade.amount) + self.update_trade_state(trade, o.order_id, co, stoploss_order=True) + except InvalidOrderException: + logger.exception(f"Could not cancel stoploss order {o.order_id} " + f"for pair {trade.pair}") return trade def get_valid_enter_price_and_stake( From d5a0759051497c977499c38d747ca953aac1c99f Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 6 Oct 2023 10:29:57 -0400 Subject: [PATCH 019/327] add open_sl_orders helper, use it in cancel_stoploss_on_exchange --- freqtrade/freqtradebot.py | 22 ++++++++++------------ freqtrade/persistence/trade_model.py | 10 ++++++++++ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ebc146ede..ff3c36bf8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -904,18 +904,16 @@ class FreqtradeBot(LoggingMixin): def cancel_stoploss_on_exchange(self, trade: Trade) -> Trade: # First cancelling stoploss on exchange ... - if trade.has_open_sl_orders: - for o in trade.orders: - if o.ft_order_side == 'stoploss' and o.ft_is_open: - try: - logger.info(f"Canceling stoploss on exchange for {trade} " - f"order: {o.order_id}") - co = self.exchange.cancel_stoploss_order_with_result( - o.order_id, trade.pair, trade.amount) - self.update_trade_state(trade, o.order_id, co, stoploss_order=True) - except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {o.order_id} " - f"for pair {trade.pair}") + for oslo in trade.open_sl_orders: + try: + logger.info(f"Canceling stoploss on exchange for {trade} " + f"order: {oslo.order_id}") + co = self.exchange.cancel_stoploss_order_with_result( + oslo.order_id, trade.pair, trade.amount) + self.update_trade_state(trade, oslo.order_id, co, stoploss_order=True) + except InvalidOrderException: + logger.exception(f"Could not cancel stoploss order {oslo.order_id} " + f"for pair {trade.pair}") return trade def get_valid_enter_price_and_stake( diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 48fed1782..e483dcc24 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -523,6 +523,16 @@ class LocalTrade: ] return len(open_orders_wo_sl) > 0 + @property + def open_sl_orders(self) -> List[Order]: + """ + 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 + ] + @property def has_open_sl_orders(self) -> int: """ From 2565d509a614b0d0f6730b5d1f819d912c3818f5 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 6 Oct 2023 10:38:14 -0400 Subject: [PATCH 020/327] remove legacy sl management code from handle_insufficient_funds --- freqtrade/freqtradebot.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ff3c36bf8..7d78779ff 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -439,10 +439,6 @@ class FreqtradeBot(LoggingMixin): try: fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, order.ft_order_side == 'stoploss') - if order.ft_order_side == 'stoploss': - if fo and fo['status'] == 'open': - # Assume this as the open stoploss order - trade.stoploss_order_id = order.order_id if fo: logger.info(f"Found {order} for trade {trade}.") self.update_trade_state(trade, order.order_id, fo, From ea828ccb4a8164f96a0853ae7e9e0b5a06e52adb Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 6 Oct 2023 10:40:23 -0400 Subject: [PATCH 021/327] remove legacy sl management code from create_stoploss_order --- freqtrade/freqtradebot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7d78779ff..a8f5665c8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1189,7 +1189,6 @@ class FreqtradeBot(LoggingMixin): order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss', trade.amount, stop_price) trade.orders.append(order_obj) - trade.stoploss_order_id = str(stoploss_order['id']) trade.stoploss_last_update = datetime.now(timezone.utc) return True except InsufficientFundsError as e: @@ -1198,13 +1197,11 @@ class FreqtradeBot(LoggingMixin): self.handle_insufficient_funds(trade) except InvalidOrderException as e: - trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Exiting the trade forcefully') self.emergency_exit(trade, stop_price) except ExchangeError: - trade.stoploss_order_id = None logger.exception('Unable to place a stoploss order on exchange.') return False From 9d82de15d43dcb98e5c6c61912f0709315c874b2 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 6 Oct 2023 15:56:08 -0400 Subject: [PATCH 022/327] first updated proposition of handle_stoploss_on_exchange, add sl_orders helper --- freqtrade/freqtradebot.py | 100 ++++++++++++++++----------- freqtrade/persistence/trade_model.py | 10 +++ 2 files changed, 70 insertions(+), 40 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a8f5665c8..3338805f7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1215,27 +1215,30 @@ class FreqtradeBot(LoggingMixin): """ logger.debug('Handling stoploss on exchange %s ...', trade) - stoploss_order = None - try: - # First we check if there is already a stoploss on exchange - stoploss_order = self.exchange.fetch_stoploss_order( - trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None - except InvalidOrderException as exception: - logger.warning('Unable to fetch stoploss order: %s', exception) + stoploss_orders = [] + for slo in trade.sl_orders: + stoploss_order = None + try: + # First we check if there is already a stoploss on exchange + stoploss_order = self.exchange.fetch_stoploss_order( + slo.order_id, trade.pair) if slo.order_id else None + except InvalidOrderException as exception: + logger.warning('Unable to fetch stoploss order: %s', exception) - if stoploss_order: - self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, - stoploss_order=True) + if stoploss_order: + stoploss_orders.append(stoploss_order) + self.update_trade_state(trade, slo.order_id, stoploss_order, + stoploss_order=True) - # We check if stoploss order is fulfilled - if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): - trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value - self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, - stoploss_order=True) - self._notify_exit(trade, "stoploss", True) - self.handle_protections(trade.pair, trade.trade_direction) - return True + # We check if stoploss order is fulfilled + if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): + trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value + self.update_trade_state(trade, slo.order_id, stoploss_order, + stoploss_order=True) + self._notify_exit(trade, "stoploss", True) + self.handle_protections(trade.pair, trade.trade_direction) + return True if trade.has_open_orders or not trade.is_open: # Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case @@ -1244,7 +1247,7 @@ class FreqtradeBot(LoggingMixin): return False # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange - if not stoploss_order: + if len(stoploss_orders) == 0: stop_price = trade.stoploss_or_liquidation if self.edge: stoploss = self.edge.get_stoploss(pair=trade.pair) @@ -1258,27 +1261,7 @@ class FreqtradeBot(LoggingMixin): # in which case the trade will be closed - which we must check below. return False - # If stoploss order is canceled for some reason we add it again - if (trade.is_open - and stoploss_order - and stoploss_order['status'] in ('canceled', 'cancelled')): - if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation): - return False - else: - logger.warning('Stoploss order was cancelled, but unable to recreate one.') - - # Finally we check if stoploss on exchange should be moved up because of trailing. - # Triggered Orders are now real orders - so don't replace stoploss anymore - if ( - trade.is_open and stoploss_order - and stoploss_order.get('status_stop') != 'triggered' - and (self.config.get('trailing_stop', False) - or self.config.get('use_custom_stoploss', False)) - ): - # if trailing stoploss is enabled we check if stoploss value has changed - # in which case we cancel stoploss order and put another one with new - # value immediately - self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) + self.manage_trade_stoploss_orders(trade, stoploss_orders) return False @@ -1314,6 +1297,43 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") + def manage_trade_stoploss_orders(self, trade, stoploss_orders): + """ + Check to see if stoploss on exchange should be updated + in case of trailing stoploss on exchange + :param trade: Corresponding Trade + :param stoploss_orders: Current on exchange stoploss orders + :return: None + """ + # If all stoploss orderd are canceled for some reason we add it again + canceled_sl_orders = [o for o in stoploss_orders if o.status in ['canceled', 'cancelled']] + if ( + trade.is_open and + len(stoploss_orders) > 0 and + len(stoploss_orders) == len(canceled_sl_orders) + ): + if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation): + return False + else: + logger.warning('All Stoploss orders are cancelled, but unable to recreate one.') + + active_sl_orders = [o for o in stoploss_orders if o not in canceled_sl_orders] + if len(active_sl_orders) > 0: + last_active_sl_order = active_sl_orders[-1] + # Finally we check if stoploss on exchange should be moved up because of trailing. + # Triggered Orders are now real orders - so don't replace stoploss anymore + if (trade.is_open and + last_active_sl_order.get('status_stop') != 'triggered' and + (self.config.get('trailing_stop', False) or + self.config.get('use_custom_stoploss', False))): + # if trailing stoploss is enabled we check if stoploss value has changed + # in which case we cancel stoploss order and put another one with new + # value immediately + self.handle_trailing_stoploss_on_exchange(trade, last_active_sl_order) + + # TODO cancel remaining_active_sl_orders active_sl_orders[:-1] + return + def manage_open_orders(self) -> None: """ Management of open orders on exchange. Unfilled orders might be cancelled if timeout diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index e483dcc24..ee6531030 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -544,6 +544,16 @@ class LocalTrade: ] return len(open_sl_orders) > 0 + @property + def sl_orders(self) -> List[Order]: + """ + All stoploss orders for this trade + """ + 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 = [ From df8f1b93285b33599d1b4c26aeeeb575a2221afd Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 6 Oct 2023 15:58:51 -0400 Subject: [PATCH 023/327] update manage_trade_stoploss_orders description --- freqtrade/freqtradebot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3338805f7..6f5ba41ae 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1299,8 +1299,7 @@ class FreqtradeBot(LoggingMixin): def manage_trade_stoploss_orders(self, trade, stoploss_orders): """ - Check to see if stoploss on exchange should be updated - in case of trailing stoploss on exchange + Perform required actions acording to existing stoploss orders of trade :param trade: Corresponding Trade :param stoploss_orders: Current on exchange stoploss orders :return: None From 2bb68ca53d23634ba6ff5a8a7005bff6866d85c6 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 6 Oct 2023 16:08:40 -0400 Subject: [PATCH 024/327] remove stoploss_order_id from LocalTrade class --- freqtrade/persistence/trade_model.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index ee6531030..9c671d4b3 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -366,8 +366,6 @@ class LocalTrade: # percentage value of the initial stop loss initial_stop_loss_pct: Optional[float] = None is_stop_loss_trailing: bool = False - # stoploss order id which is on exchange - stoploss_order_id: Optional[str] = None # last update time of the stoploss order on exchange stoploss_last_update: Optional[datetime] = None # absolute value of the highest reached price From aaa82e1fa9c1f12db70526c463c55b1ac1c2221f Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 6 Oct 2023 16:34:03 -0400 Subject: [PATCH 025/327] remove all occurence of stoploss_order_id in trade_model, update api schemas, update rpc_delete --- freqtrade/persistence/trade_model.py | 6 ------ freqtrade/rpc/api_server/api_schemas.py | 1 - freqtrade/rpc/rpc.py | 17 +++++++++-------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 9c671d4b3..26834ae48 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -636,7 +636,6 @@ class LocalTrade: '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_order_id': self.stoploss_order_id, 'stoploss_last_update': (self.stoploss_last_update.strftime(DATETIME_PRINT_FORMAT) if self.stoploss_last_update else None), 'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( @@ -787,7 +786,6 @@ class LocalTrade: logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.') elif order.ft_order_side == 'stoploss' and order.status not in ('open', ): - self.stoploss_order_id = None self.close_rate_requested = self.stop_loss self.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value if self.is_open and order.safe_filled > 0: @@ -1378,9 +1376,6 @@ class Trade(ModelBase, LocalTrade): Float(), nullable=True) # type: ignore is_stop_loss_trailing: Mapped[bool] = mapped_column( nullable=False, default=False) # type: ignore - # stoploss order id which is on exchange - stoploss_order_id: Mapped[Optional[str]] = mapped_column( - String(255), nullable=True, index=True) # type: ignore # last update time of the stoploss order on exchange stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True) # type: ignore # absolute value of the highest reached price @@ -1805,7 +1800,6 @@ class Trade(ModelBase, LocalTrade): exit_order_status=data["exit_order_status"], stop_loss=data["stop_loss_abs"], stop_loss_pct=data["stop_loss_ratio"], - stoploss_order_id=data["stoploss_order_id"], stoploss_last_update=( datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000, tz=timezone.utc) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 97f6251bc..4f154f3a3 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -299,7 +299,6 @@ class TradeSchema(BaseModel): stop_loss_abs: Optional[float] = None stop_loss_ratio: Optional[float] = None stop_loss_pct: Optional[float] = None - stoploss_order_id: Optional[str] = None stoploss_last_update: Optional[str] = None stoploss_last_update_timestamp: Optional[int] = None initial_stop_loss_abs: Optional[float] = None diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 0abac3975..3ee4bbc91 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -978,15 +978,16 @@ class RPC: except (ExchangeError): pass - # cancel stoploss on exchange ... + # cancel stoploss on exchange orders ... if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange') - and trade.stoploss_order_id): - try: - self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id, - trade.pair) - c_count += 1 - except (ExchangeError): - pass + and trade.has_open_sl_orders): + + for oslo in trade.open_sl_orders: + try: + self._freqtrade.exchange.cancel_stoploss_order(oslo.order_id, trade.pair) + c_count += 1 + except (ExchangeError): + pass trade.delete() self._freqtrade.wallets.update() From e8be011e2bdd6c2960fa223520b31d6d45cbbff8 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 6 Oct 2023 17:01:12 -0400 Subject: [PATCH 026/327] update manage_trade_stoploss_orders: remove unrelevant TODO --- freqtrade/freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6f5ba41ae..834002495 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1330,7 +1330,6 @@ class FreqtradeBot(LoggingMixin): # value immediately self.handle_trailing_stoploss_on_exchange(trade, last_active_sl_order) - # TODO cancel remaining_active_sl_orders active_sl_orders[:-1] return def manage_open_orders(self) -> None: From c2b32769a19fb9dd04af654570b9ca6e74d4e496 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Nov 2023 06:48:20 +0100 Subject: [PATCH 027/327] Remove further occurance in bot file --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 852e71a6b..69b81a67c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1080,7 +1080,7 @@ class FreqtradeBot(LoggingMixin): if ( not trade.has_open_orders - and not trade.stoploss_order_id + and not trade.has_open_sl_orders and not self.wallets.check_exit_amount(trade) ): logger.warning( From 41e40e6214093ab89d2cebcbd27a0f5e2fd5eea3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Nov 2023 06:48:27 +0100 Subject: [PATCH 028/327] Update some initial tests --- tests/conftest_trades.py | 1 - tests/conftest_trades_usdt.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index a2276ae16..9ac43d73d 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -266,7 +266,6 @@ def mock_trade_5(fee, is_short: bool): exchange='binance', strategy='SampleStrategy', enter_tag='TEST1', - stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455', timeframe=5, is_short=is_short, stop_loss_pct=0.10, diff --git a/tests/conftest_trades_usdt.py b/tests/conftest_trades_usdt.py index d73a53605..cf3109090 100644 --- a/tests/conftest_trades_usdt.py +++ b/tests/conftest_trades_usdt.py @@ -282,7 +282,6 @@ def mock_trade_usdt_5(fee, is_short: bool): open_rate=2.0, exchange='binance', strategy='SampleStrategy', - stoploss_order_id=f'prod_stoploss_3455_{direc(is_short)}', timeframe=5, is_short=is_short, ) From 13780d5963ce6ee74268da2076bb78a2788ca678 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 Dec 2023 17:22:33 +0100 Subject: [PATCH 029/327] Remove further usage --- freqtrade/persistence/migrations.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index bb6c04922..a5e3c4640 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -91,7 +91,6 @@ def migrate_trades_and_orders_table( 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)') - stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') min_rate = get_column_def(cols, 'min_rate', 'null') @@ -160,7 +159,7 @@ def migrate_trades_and_orders_table( open_rate_requested, close_rate, close_rate_requested, close_profit, stake_amount, amount, amount_requested, open_date, close_date, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, - is_stop_loss_trailing, stoploss_order_id, stoploss_last_update, + is_stop_loss_trailing, stoploss_last_update, max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag, timeframe, open_trade_value, close_profit_abs, trading_mode, leverage, liquidation_price, is_short, @@ -180,7 +179,7 @@ def migrate_trades_and_orders_table( {initial_stop_loss} initial_stop_loss, {initial_stop_loss_pct} initial_stop_loss_pct, {is_stop_loss_trailing} is_stop_loss_trailing, - {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, + {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, case when {exit_reason} = 'sell_signal' then 'exit_signal' when {exit_reason} = 'custom_sell' then 'custom_exit' From b33a9059abfa3d5e50957096613ae60c16f5bc12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 Dec 2023 17:35:02 +0100 Subject: [PATCH 030/327] Fix some more tests --- freqtrade/persistence/migrations.py | 24 ++++++++++++------------ tests/persistence/test_persistence.py | 2 -- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index a5e3c4640..fc67448eb 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -280,19 +280,19 @@ def fix_old_dry_orders(engine): # - current Trade is closed # - current Order trade_id not equal to current Trade.id # - current Order not stoploss + # TODO: is this still necessary ? how can this be done now ? + # stmt = update(Order).where( + # Order.ft_is_open.is_(True), + # tuple_(Order.ft_trade_id, Order.order_id).not_in( + # select( + # Trade.id, Trade.stoploss_order_id + # ).where(Trade.stoploss_order_id.is_not(None)) + # ), + # Order.ft_order_side == 'stoploss', + # Order.order_id.like('dry%'), - stmt = update(Order).where( - Order.ft_is_open.is_(True), - tuple_(Order.ft_trade_id, Order.order_id).not_in( - select( - Trade.id, Trade.stoploss_order_id - ).where(Trade.stoploss_order_id.is_not(None)) - ), - Order.ft_order_side == 'stoploss', - Order.order_id.like('dry%'), - - ).values(ft_is_open=False) - connection.execute(stmt) + # ).values(ft_is_open=False) + # connection.execute(stmt) # Close dry-run orders for closed trades. stmt = update(Order).where( diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 5829f8b71..95db7bc0f 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -1432,7 +1432,6 @@ def test_to_json(fee): 'stop_loss_abs': None, 'stop_loss_ratio': None, 'stop_loss_pct': None, - 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss_abs': None, @@ -1500,7 +1499,6 @@ def test_to_json(fee): 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, - 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss_abs': None, From 8234879b583f41a151ba214df12a85d6740253e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 22 Dec 2023 17:38:14 +0100 Subject: [PATCH 031/327] stoploss_order_id removal tests --- tests/rpc/test_rpc.py | 1 - tests/rpc/test_rpc_apiserver.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 7ea9dae89..ebbc62af6 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -63,7 +63,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stop_loss_abs': 9.89e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, - 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, 'initial_stop_loss_abs': 9.89e-06, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 17b0399d9..f89b06d39 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1175,7 +1175,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'stop_loss_abs': ANY, 'stop_loss_pct': ANY, 'stop_loss_ratio': ANY, - 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, 'initial_stop_loss_abs': 0.0, @@ -1379,7 +1378,6 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, - 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, 'initial_stop_loss_abs': None, From 15058d3ce675deac6dd3e6ac7e24bed89643c984 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 09:16:13 +0100 Subject: [PATCH 032/327] Add type hints to manage_trade_orders, fix content ... --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 816d35cdc..40cd6cecd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1299,7 +1299,7 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") - def manage_trade_stoploss_orders(self, trade, stoploss_orders): + def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: Dict): """ Perform required actions acording to existing stoploss orders of trade :param trade: Corresponding Trade @@ -1307,7 +1307,7 @@ class FreqtradeBot(LoggingMixin): :return: None """ # If all stoploss orderd are canceled for some reason we add it again - canceled_sl_orders = [o for o in stoploss_orders if o.status in ['canceled', 'cancelled']] + canceled_sl_orders = [o for o in stoploss_orders if o['status'] in ['canceled', 'cancelled']] if ( trade.is_open and len(stoploss_orders) > 0 and From 28e2bfaf1cbe374b04a9088d07ce715801429b06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 09:25:35 +0100 Subject: [PATCH 033/327] Fix types of "has" calls --- freqtrade/persistence/trade_model.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 13578ff6a..a4a785c55 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -512,7 +512,7 @@ class LocalTrade: return [o for o in self.orders if o.ft_is_open and o.ft_order_side != 'stoploss'] @property - def has_open_orders(self) -> int: + def has_open_orders(self) -> bool: """ True if there are open orders for this trade excluding stoploss orders """ @@ -533,7 +533,7 @@ class LocalTrade: ] @property - def has_open_sl_orders(self) -> int: + def has_open_sl_orders(self) -> bool: """ True if there are open stoploss orders for this trade """ From c35b308adabcf7c73a77d23d95df77cdbc79f1a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 09:25:39 +0100 Subject: [PATCH 034/327] Fix some tests --- tests/test_freqtradebot.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 06d40dfb0..61c63d064 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1118,12 +1118,11 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_sho freqtrade.enter_positions() trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short - trade.stoploss_order_id = None trade.is_open = True trades = [trade] freqtrade.exit_positions(trades) - assert trade.stoploss_order_id == '13434334' + assert trade.has_open_sl_orders is True assert stoploss.call_count == 1 assert trade.is_open is True @@ -1535,7 +1534,7 @@ def test_create_stoploss_order_invalid_order( caplog.clear() rpc_mock.reset_mock() freqtrade.create_stoploss_order(trade, 200) - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value assert log_has("Unable to place a stoploss order on exchange. ", caplog) assert log_has("Exiting the trade forcefully", caplog) @@ -1589,14 +1588,13 @@ def test_create_stoploss_order_insufficient_funds( caplog.clear() freqtrade.create_stoploss_order(trade, 200) # stoploss_orderid was empty before - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False assert mock_insuf.call_count == 1 mock_insuf.reset_mock() - trade.stoploss_order_id = 'stoploss_orderid' freqtrade.create_stoploss_order(trade, 200) # No change to stoploss-orderid - assert trade.stoploss_order_id == 'stoploss_orderid' + assert trade.has_open_sl_orders is False assert mock_insuf.call_count == 1 @@ -5679,7 +5677,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap trade = trades[1] reset_open_orders(trade) assert not trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False freqtrade.handle_insufficient_funds(trade) order = trade.orders[0] @@ -5689,7 +5687,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap assert mock_uts.call_count == 0 # No change to orderid - as update_trade_state is mocked assert not trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False caplog.clear() mock_fo.reset_mock() @@ -5700,7 +5698,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap # This part in not relevant anymore # assert not trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False freqtrade.handle_insufficient_funds(trade) order = mock_order_4(is_short=is_short) @@ -5708,8 +5706,8 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap assert mock_fo.call_count == 1 assert mock_uts.call_count == 1 # Found open buy order - assert trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_orders is True + assert trade.has_open_sl_orders is False caplog.clear() mock_fo.reset_mock() @@ -5718,7 +5716,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap trade = trades[4] reset_open_orders(trade) assert not trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders freqtrade.handle_insufficient_funds(trade) order = mock_order_5_stoploss(is_short=is_short) @@ -5727,7 +5725,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap assert mock_uts.call_count == 2 # stoploss_order_id is "refound" and added to the trade assert not trade.has_open_orders - assert trade.stoploss_order_id is not None + assert trade.has_open_sl_orders is True caplog.clear() mock_fo.reset_mock() @@ -5738,7 +5736,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap reset_open_orders(trade) # This part in not relevant anymore # assert not trade.has_open_orders - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False freqtrade.handle_insufficient_funds(trade) order = mock_order_6_sell(is_short=is_short) @@ -5747,7 +5745,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap assert mock_uts.call_count == 1 # sell-orderid is "refound" and added to the trade assert trade.open_orders_ids[0] == order['id'] - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False caplog.clear() From 6427144934983ee5c1ba25ac7277c0d6440e236e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 09:27:32 +0100 Subject: [PATCH 035/327] Fix stoploss test --- tests/test_freqtradebot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 61c63d064..a2337fa2a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4130,11 +4130,11 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( freqtrade.manage_open_orders() trade = Trade.session.scalars(select(Trade)).first() trades = [trade] - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False freqtrade.exit_positions(trades) assert trade - assert trade.stoploss_order_id == '123' + assert trade.has_open_sl_orders is True assert not trade.has_open_orders # Assuming stoploss on exchange is hit @@ -4161,7 +4161,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_executed) freqtrade.exit_positions(trades) - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False assert trade.is_open is False assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 4 From c6ffe82a7a72f1293b901d4685685dd0be429cdc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 09:29:45 +0100 Subject: [PATCH 036/327] Update more tests --- tests/test_freqtradebot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a2337fa2a..ca8b6a4b9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1345,11 +1345,11 @@ def test_handle_stoploss_on_exchange_partial( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.stoploss_order_id = None assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 - assert trade.stoploss_order_id == "101" + assert trade.has_open_sl_orders is True + assert trade.open_sl_orders[-1].order_id == "101" assert trade.amount == 30 stop_order_dict.update({'id': "102"}) # Stoploss on exchange is cancelled on exchange, but filled partially. @@ -1369,7 +1369,7 @@ def test_handle_stoploss_on_exchange_partial( # Stoploss filled partially ... assert trade.amount == 15 - assert trade.stoploss_order_id == "102" + assert trade.open_sl_orders[-1].order_id == "102" @pytest.mark.parametrize("is_short", [False, True]) From cbfebd397c1149e9aa8543b9e7995788ba866de0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 09:47:33 +0100 Subject: [PATCH 037/327] Use a trade for test that actually has an open stop order --- tests/persistence/test_trade_fromjson.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/persistence/test_trade_fromjson.py b/tests/persistence/test_trade_fromjson.py index bb5e77f22..302a81c54 100644 --- a/tests/persistence/test_trade_fromjson.py +++ b/tests/persistence/test_trade_fromjson.py @@ -54,7 +54,6 @@ def test_trade_fromjson(): "stop_loss_abs": 0.1981, "stop_loss_ratio": -0.216, "stop_loss_pct": -21.6, - "stoploss_order_id": null, "stoploss_last_update": "2022-10-18 09:13:42", "stoploss_last_update_timestamp": 1666077222000, "initial_stop_loss_abs": 0.1981, From ae3f62cf9be4ae875de3ca48274c2146e60217e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 09:50:42 +0100 Subject: [PATCH 038/327] Fix RPC tests --- tests/rpc/test_rpc.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 869150e3d..4d43660e5 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -354,7 +354,6 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): rpc._rpc_delete('200') trades = Trade.session.scalars(select(Trade)).all() - trades[2].stoploss_order_id = '102' trades[2].orders.append( Order( ft_order_side='stoploss', From 1db4732648de9fb269def2a88402f9da3af2fcab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 09:57:05 +0100 Subject: [PATCH 039/327] Fix some more tests --- tests/test_freqtradebot.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ca8b6a4b9..3467f037e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1403,11 +1403,11 @@ def test_handle_stoploss_on_exchange_partial_cancel_here( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.stoploss_order_id = None assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 - assert trade.stoploss_order_id == "101" + assert trade.has_open_sl_orders is True + assert trade.open_sl_orders[-1].order_id == "101" assert trade.amount == 30 stop_order_dict.update({'id': "102"}) # Stoploss on exchange is open. @@ -1440,7 +1440,8 @@ def test_handle_stoploss_on_exchange_partial_cancel_here( # Canceled Stoploss filled partially ... assert log_has_re('Cancelling current stoploss on exchange.*', caplog) - assert trade.stoploss_order_id == "102" + assert trade.has_open_sl_orders is True + assert trade.open_sl_orders[-1].order_id == "102" assert trade.amount == 15 @@ -4027,7 +4028,17 @@ def test_execute_trade_exit_sloe_cancel_exception( PairLock.session = MagicMock() freqtrade.config['dry_run'] = False - trade.stoploss_order_id = "abcd" + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='abcd', + status='open', + ) + ) freqtrade.execute_trade_exit(trade=trade, limit=1234, exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)) From 33bd433c2299cd98a9c7dead01a47c317e555d4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 10:31:03 +0100 Subject: [PATCH 040/327] Don't run against all orders, only consider open sl orders. --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 40cd6cecd..fec2ed6a4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1219,7 +1219,7 @@ class FreqtradeBot(LoggingMixin): logger.debug('Handling stoploss on exchange %s ...', trade) stoploss_orders = [] - for slo in trade.sl_orders: + for slo in trade.open_sl_orders: stoploss_order = None try: # First we check if there is already a stoploss on exchange From 600e311b3ee116555d62fef0d6ab1dd4cfe58e0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 10:35:30 +0100 Subject: [PATCH 041/327] Fix test test_handle_stoploss_on_exchange_custom_stop --- tests/test_freqtradebot.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3467f037e..95635edff 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1866,6 +1866,7 @@ def test_handle_stoploss_on_exchange_custom_stop( exit_order, ]), get_fee=fee, + is_cancel_order_result_suitable=MagicMock(return_value=True), ) mocker.patch.multiple( EXMS, @@ -1896,7 +1897,6 @@ def test_handle_stoploss_on_exchange_custom_stop( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.stoploss_order_id = '100' trade.stoploss_last_update = dt_now() - timedelta(minutes=601) trade.orders.append( Order( @@ -1908,8 +1908,8 @@ def test_handle_stoploss_on_exchange_custom_stop( order_id='100', ) ) - - stoploss_order_hanging = MagicMock(return_value={ + Trade.commit() + slo = { 'id': '100', 'status': 'open', 'type': 'stop_loss_limit', @@ -1918,9 +1918,17 @@ def test_handle_stoploss_on_exchange_custom_stop( 'info': { 'stopPrice': '2.0805' } - }) + } + slo_canceled = deepcopy(slo) + slo_canceled.update({'status': 'canceled'}) - mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging) + def fetch_stoploss_order_mock(order_id, *args, **kwargs): + x = deepcopy(slo) + x['id'] = order_id + return x + + mocker.patch(f'{EXMS}.fetch_stoploss_order', MagicMock(fetch_stoploss_order_mock)) + mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=slo_canceled) assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1939,7 +1947,6 @@ def test_handle_stoploss_on_exchange_custom_stop( stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'}) mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) - trade.stoploss_order_id = '100' # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1953,10 +1960,12 @@ def test_handle_stoploss_on_exchange_custom_stop( # setting stoploss_on_exchange_interval to 0 seconds freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 + cancel_order_mock.assert_not_called() + stoploss_order_mock.assert_not_called() assert freqtrade.handle_stoploss_on_exchange(trade) is False - cancel_order_mock.assert_called_once_with('100', 'ETH/USDT') + cancel_order_mock.assert_called_once_with('13434334', 'ETH/USDT') # Long uses modified ask - offset, short modified bid + offset stoploss_order_mock.assert_called_once_with( amount=pytest.approx(trade.amount), From 68f9402384aad8db16418abd5c32c0e364f5eb26 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 10:44:21 +0100 Subject: [PATCH 042/327] Fix further test --- tests/test_freqtradebot.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 95635edff..a37d88fd6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1759,9 +1759,6 @@ def test_handle_stoploss_on_exchange_trailing_error( {'id': exit_order['id']}, ]), get_fee=fee, - ) - mocker.patch.multiple( - EXMS, create_stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1783,10 +1780,8 @@ def test_handle_stoploss_on_exchange_trailing_error( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.stoploss_order_id = "abcd" trade.stop_loss = 0.2 trade.stoploss_last_update = (dt_now() - timedelta(minutes=601)).replace(tzinfo=None) - trade.is_short = is_short stoploss_order_hanging = { 'id': "abcd", @@ -1798,6 +1793,16 @@ def test_handle_stoploss_on_exchange_trailing_error( 'stopPrice': '0.1' } } + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=3, + order_id='abcd', + ) + ) mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=InvalidOrderException()) mocker.patch(f'{EXMS}.fetch_stoploss_order', @@ -1807,6 +1812,8 @@ def test_handle_stoploss_on_exchange_trailing_error( # Still try to create order assert stoploss.call_count == 1 + # TODO: Is this actually correct ? This will create a new order every time, + assert len(trade.open_sl_orders) == 2 # Fail creating stoploss order trade.stoploss_last_update = dt_now() - timedelta(minutes=601) @@ -1814,7 +1821,7 @@ def test_handle_stoploss_on_exchange_trailing_error( cancel_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order') mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) - assert cancel_mock.call_count == 1 + assert cancel_mock.call_count == 2 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog) From a39b329e3baa84dbfe27658d087ccbefb7d2500e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 23 Dec 2023 15:30:24 +0100 Subject: [PATCH 043/327] Fix line-length --- freqtrade/freqtradebot.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fec2ed6a4..e414f9e82 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1307,12 +1307,13 @@ class FreqtradeBot(LoggingMixin): :return: None """ # If all stoploss orderd are canceled for some reason we add it again - canceled_sl_orders = [o for o in stoploss_orders if o['status'] in ['canceled', 'cancelled']] + canceled_sl_orders = [o for o in stoploss_orders + if o['status'] in ('canceled', 'cancelled')] if ( - trade.is_open and - len(stoploss_orders) > 0 and - len(stoploss_orders) == len(canceled_sl_orders) - ): + trade.is_open and + len(stoploss_orders) > 0 and + len(stoploss_orders) == len(canceled_sl_orders) + ): if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation): return False else: From f0073078e9a4950f84e78cc02d03b6b124055d79 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jan 2024 11:27:42 +0100 Subject: [PATCH 044/327] Fix stop order test --- tests/test_freqtradebot.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a37d88fd6..bafb13a9f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1468,7 +1468,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, ) mocker.patch.multiple( EXMS, - fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}), + fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': '100'}), create_stoploss=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf_usdt) @@ -1478,7 +1478,6 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, trade = Trade.session.scalars(select(Trade)).first() assert trade.is_short == is_short trade.is_open = True - trade.stoploss_order_id = "100" trade.orders.append( Order( ft_order_side='stoploss', @@ -1493,8 +1492,8 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, assert trade assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert log_has_re(r'Stoploss order was cancelled, but unable to recreate one.*', caplog) - assert trade.stoploss_order_id is None + assert log_has_re(r'All Stoploss orders are cancelled, but unable to recreate one\.', caplog) + assert trade.has_open_sl_orders is False assert trade.is_open is True From 501e256c587456d7c19eb822288a08a1969617d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jan 2024 16:25:42 +0100 Subject: [PATCH 045/327] Fix further stoploss test --- tests/test_freqtradebot.py | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 703c06118..763ca8b48 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1656,7 +1656,7 @@ def test_handle_stoploss_on_exchange_trailing( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.stoploss_order_id = '100' + assert trade.has_open_sl_orders is False trade.stoploss_last_update = dt_now() - timedelta(minutes=20) trade.orders.append( Order( @@ -1669,24 +1669,31 @@ def test_handle_stoploss_on_exchange_trailing( ) ) - stoploss_order_hanging = MagicMock(return_value={ + stoploss_order_hanging = { 'id': '100', 'status': 'open', 'type': 'stop_loss_limit', 'price': hang_price, 'average': 2, + 'fee': {}, + 'amount': 0, 'info': { 'stopPrice': stop_price[0] } - }) + } + stoploss_order_cancel = deepcopy(stoploss_order_hanging) + stoploss_order_cancel['status'] = 'canceled' - mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value=stoploss_order_hanging) + mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=stoploss_order_cancel) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert trade.stoploss_order_id == '13434334' + assert len(trade.open_sl_orders) == 1 + + assert trade.open_sl_orders[-1].order_id == '13434334' # price jumped 2x mocker.patch( @@ -1698,14 +1705,17 @@ def test_handle_stoploss_on_exchange_trailing( }) ) - cancel_order_mock = MagicMock() + cancel_order_mock = MagicMock(return_value={ + 'id': '13434334', 'status': 'canceled', 'fee': {}, 'amount': trade.amount}) stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'}) + mocker.patch(f'{EXMS}.fetch_stoploss_order') mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert len(trade.open_sl_orders) == 1 cancel_order_mock.assert_not_called() stoploss_order_mock.assert_not_called() @@ -1736,8 +1746,14 @@ def test_handle_stoploss_on_exchange_trailing( 'last': bid[1], }) ) + mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', + return_value={'id': 'so1', 'status': 'canceled'}) + assert len(trade.open_sl_orders) == 1 + assert trade.open_sl_orders[-1].order_id == 'so1' + assert freqtrade.handle_trade(trade) is True - assert trade.stoploss_order_id is None + assert trade.is_open is False + assert trade.has_open_sl_orders is False @pytest.mark.parametrize("is_short", [False, True]) From 206809d2e7a8e9876bec38c6aeefd9bf02d3d3c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jan 2024 17:05:21 +0100 Subject: [PATCH 046/327] Update emergency sell test --- tests/test_freqtradebot.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d07c1db23..33b0828a9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1279,7 +1279,7 @@ def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, i trade = Trade.session.scalars(select(Trade)).first() assert trade.is_short == is_short assert trade.is_open - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False # emergency exit triggered # Trailing stop should not act anymore @@ -1294,7 +1294,6 @@ def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, i 'remaining': enter_order['amount'], 'info': {'stopPrice': 22}, }]) - trade.stoploss_order_id = "107" trade.stoploss_last_update = dt_now() - timedelta(hours=1) trade.stop_loss = 24 trade.exit_reason = None @@ -1311,14 +1310,14 @@ def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, i ) freqtrade.config['trailing_stop'] = True stoploss = MagicMock(side_effect=InvalidOrderException()) - + assert trade.has_open_sl_orders is True Trade.commit() mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', side_effect=InvalidOrderException()) mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled) mocker.patch(f'{EXMS}.create_stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False assert trade.is_open is False assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT) From e199083287f9311d11400699b549620e613af6f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jan 2024 17:10:33 +0100 Subject: [PATCH 047/327] Fix test ... --- tests/test_freqtradebot.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 33b0828a9..6738fc5ae 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1161,11 +1161,11 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ trade = Trade.session.scalars(select(Trade)).first() assert trade.is_short == is_short assert trade.is_open - assert trade.stoploss_order_id is None + assert trade.has_open_sl_orders is False assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 - assert trade.stoploss_order_id == "13434334" + assert trade.open_sl_orders[-1].order_id == "13434334" # Second case: when stoploss is set but it is not yet hit # should do nothing and return false @@ -1176,7 +1176,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ assert freqtrade.handle_stoploss_on_exchange(trade) is False hanging_stoploss_order.assert_called_once_with('13434334', trade.pair) - assert trade.stoploss_order_id == "13434334" + assert len(trade.open_sl_orders) == 1 + assert trade.open_sl_orders[-1].order_id == "13434334" # Third case: when stoploss was set but it was canceled for some reason # should set a stoploss immediately and return False @@ -1192,7 +1193,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 - assert trade.stoploss_order_id == "103_1" + assert len(trade.open_sl_orders) == 1 + assert trade.open_sl_orders[-1].order_id == "103_1" assert trade.amount == amount_before # Fourth case: when stoploss is set and it is hit @@ -1218,7 +1220,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) - assert trade.stoploss_order_id is None + assert len(trade.open_sl_orders) == 0 assert trade.is_open is False caplog.clear() @@ -1226,26 +1228,27 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ trade.is_open = True freqtrade.handle_stoploss_on_exchange(trade) assert log_has('Unable to place a stoploss order on exchange.', caplog) - assert trade.stoploss_order_id is None + assert len(trade.open_sl_orders) == 0 # Fifth case: fetch_order returns InvalidOrder # It should try to add stoploss order stop_order_dict.update({'id': "105"}) - trade.stoploss_order_id = "105" stoploss.reset_mock() mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException()) mocker.patch(f'{EXMS}.create_stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) + assert len(trade.open_sl_orders) == 1 assert stoploss.call_count == 1 # Sixth case: Closed Trade # Should not create new order - trade.stoploss_order_id = None trade.is_open = False + trade.open_sl_orders[-1].ft_is_open = False stoploss.reset_mock() mocker.patch(f'{EXMS}.fetch_order') mocker.patch(f'{EXMS}.create_stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert trade.has_open_sl_orders is False assert stoploss.call_count == 0 From 14660f54f8fca7767155746ab3de05bca99ca9fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jan 2024 19:22:16 +0100 Subject: [PATCH 048/327] Remove duplicate call to update_trade_state --- freqtrade/freqtradebot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0b30d89a4..3b01f7756 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1229,8 +1229,6 @@ class FreqtradeBot(LoggingMixin): # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): trade.exit_reason = ExitType.STOPLOSS_ON_EXCHANGE.value - self.update_trade_state(trade, slo.order_id, stoploss_order, - stoploss_order=True) self._notify_exit(trade, "stoploss", True) self.handle_protections(trade.pair, trade.trade_direction) return True From dc9c4da95e8f38fb8a249cea6be068b6bf68eb2c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jan 2024 19:22:46 +0100 Subject: [PATCH 049/327] Improve integration test stability --- tests/test_integration.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 2e7f38fc8..ffb955f11 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -49,7 +49,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, stoploss_order_closed['filled'] = stoploss_order_closed['amount'] # Sell first trade based on stoploss, keep 2nd and 3rd trade open - stop_orders = [stoploss_order_closed, stoploss_order_open, stoploss_order_open] + stop_orders = [stoploss_order_closed, stoploss_order_open.copy(), stoploss_order_open.copy()] stoploss_order_mock = MagicMock( side_effect=stop_orders) # Sell 3rd trade (not called for the first trade) @@ -100,9 +100,10 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, stop_order = stop_orders[idx] stop_order['id'] = f"stop{idx}" oobj = Order.parse_from_ccxt_object(stop_order, trade.pair, 'stoploss') + oobj.ft_is_open = True trade.orders.append(oobj) - trade.stoploss_order_id = f"stop{idx}" + assert len(trade.open_sl_orders) == 1 n = freqtrade.exit_positions(trades) assert n == 2 @@ -113,6 +114,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, # Only order for 3rd trade needs to be cancelled assert cancel_order_mock.call_count == 1 + assert stoploss_order_mock.call_count == 3 # Wallets must be updated between stoploss cancellation and selling, and will be updated again # during update_trade_state assert wallets_mock.call_count == 4 From 9f3c6f2dcc2fc4982823a6b581b52c3bc1ae9c38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jan 2024 06:48:32 +0100 Subject: [PATCH 050/327] Fix some tests and comments --- tests/test_freqtradebot.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6738fc5ae..57cd1a2f5 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1198,8 +1198,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_ assert trade.amount == amount_before # Fourth case: when stoploss is set and it is hit - # should unset stoploss_order_id and return true - # as a trade actually happened + # should return true as a trade actually happened caplog.clear() stop_order_dict.update({'id': "103_1"}) @@ -1871,7 +1870,6 @@ def test_stoploss_on_exchange_price_rounding( price_to_precision=price_mock, ) freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - open_trade_usdt.stoploss_order_id = '13434334' open_trade_usdt.stop_loss = 222.55 freqtrade.handle_trailing_stoploss_on_exchange(open_trade_usdt, {}) @@ -2078,7 +2076,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_orde freqtrade.enter_positions() trade = Trade.session.scalars(select(Trade)).first() trade.is_open = True - trade.stoploss_order_id = '100' + trade.stoploss_last_update = dt_now() trade.orders.append( Order( @@ -4194,8 +4192,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( assert not trade.has_open_orders # Assuming stoploss on exchange is hit - # stoploss_order_id should become None - # and trade should be sold at the price of stoploss + # trade should be sold at the price of stoploss, with exit_reaeon STOPLOSS_ON_EXCHANGE stoploss_executed = MagicMock(return_value={ "id": "123", "timestamp": 1542707426845, @@ -5721,7 +5718,6 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap def reset_open_orders(trade): - trade.stoploss_order_id = None trade.is_short = is_short create_mock_trades(fee, is_short=is_short) @@ -5779,7 +5775,7 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 1 assert mock_uts.call_count == 2 - # stoploss_order_id is "refound" and added to the trade + # stoploss order is "refound" and added to the trade assert not trade.has_open_orders assert trade.has_open_sl_orders is True From 59b34865740cdeb170971034c58cf1f8c7914022 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jan 2024 06:49:49 +0100 Subject: [PATCH 051/327] Update migrations --- freqtrade/persistence/migrations.py | 21 +++++++++------------ tests/persistence/test_migrations.py | 6 +++--- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index fc67448eb..2970da918 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -276,23 +276,20 @@ 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: # - current Order is open # - current Trade is closed # - current Order trade_id not equal to current Trade.id # - current Order not stoploss - # TODO: is this still necessary ? how can this be done now ? - # stmt = update(Order).where( - # Order.ft_is_open.is_(True), - # tuple_(Order.ft_trade_id, Order.order_id).not_in( - # select( - # Trade.id, Trade.stoploss_order_id - # ).where(Trade.stoploss_order_id.is_not(None)) - # ), - # Order.ft_order_side == 'stoploss', - # Order.order_id.like('dry%'), - # ).values(ft_is_open=False) - # connection.execute(stmt) + 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( diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index f2bb0b2f1..6ef098cb3 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -74,7 +74,7 @@ def test_init_dryrun_db(default_conf, tmpdir): assert Path(filename).is_file() -def test_migrate_new(mocker, default_conf, fee, caplog): +def test_migrate(mocker, default_conf, fee, caplog): """ Test Database migration (starting with new pairformat) """ @@ -277,7 +277,6 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.exit_reason is None assert trade.strategy is None assert trade.timeframe == '5m' - assert trade.stoploss_order_id == 'dry_stop_order_id222' assert trade.stoploss_last_update is None assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak2", caplog) @@ -294,9 +293,10 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert orders[0].order_id == 'dry_buy_order' assert orders[0].ft_order_side == 'buy' + # All dry-run stoploss orders will be closed assert orders[-1].order_id == 'dry_stop_order_id222' assert orders[-1].ft_order_side == 'stoploss' - assert orders[-1].ft_is_open is True + assert orders[-1].ft_is_open is False assert orders[1].order_id == 'dry_buy_order22' assert orders[1].ft_order_side == 'buy' From b9a43b8e248a53dd28a26b9eeedcfced61f8bf92 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jan 2024 07:12:49 +0100 Subject: [PATCH 052/327] Don't store 'stoploss_last_updated' explicitly it can easily be derived from the very last stoploss order. --- freqtrade/freqtradebot.py | 1 - freqtrade/persistence/migrations.py | 4 +--- freqtrade/persistence/trade_model.py | 20 ++++++-------------- 3 files changed, 7 insertions(+), 18 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3b01f7756..26631eb30 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1184,7 +1184,6 @@ class FreqtradeBot(LoggingMixin): order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss', trade.amount, stop_price) trade.orders.append(order_obj) - trade.stoploss_last_update = datetime.now(timezone.utc) return True except InsufficientFundsError as e: logger.warning(f"Unable to place stoploss order {e}.") diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 2970da918..eb55cf455 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -91,7 +91,6 @@ def migrate_trades_and_orders_table( 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)') - stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') 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')) @@ -159,7 +158,7 @@ def migrate_trades_and_orders_table( open_rate_requested, close_rate, close_rate_requested, close_profit, stake_amount, amount, amount_requested, open_date, close_date, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, - is_stop_loss_trailing, stoploss_last_update, + is_stop_loss_trailing, max_rate, min_rate, exit_reason, exit_order_status, strategy, enter_tag, timeframe, open_trade_value, close_profit_abs, trading_mode, leverage, liquidation_price, is_short, @@ -179,7 +178,6 @@ def migrate_trades_and_orders_table( {initial_stop_loss} initial_stop_loss, {initial_stop_loss_pct} initial_stop_loss_pct, {is_stop_loss_trailing} is_stop_loss_trailing, - {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, case when {exit_reason} = 'sell_signal' then 'exit_signal' when {exit_reason} = 'custom_sell' then 'custom_exit' diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 7d88294b0..9db13dabc 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -364,8 +364,6 @@ class LocalTrade: # percentage value of the initial stop loss initial_stop_loss_pct: Optional[float] = None is_stop_loss_trailing: bool = False - # last update time of the stoploss order on exchange - stoploss_last_update: Optional[datetime] = None # absolute value of the highest reached price max_rate: Optional[float] = None # Lowest price reached @@ -455,8 +453,8 @@ class LocalTrade: @property def stoploss_last_update_utc(self): - if self.stoploss_last_update: - return self.stoploss_last_update.replace(tzinfo=timezone.utc) + if self.has_open_sl_orders: + return max(o.order_date_utc for o in self.open_sl_orders) return None @property @@ -638,10 +636,10 @@ class LocalTrade: '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.strftime(DATETIME_PRINT_FORMAT) - if self.stoploss_last_update else None), - 'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( - tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update 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': int(self.stoploss_last_update_utc.timestamp() * 1000 + ) if self.stoploss_last_update_utc else None, '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), @@ -1378,10 +1376,6 @@ class LocalTrade: exit_order_status=data["exit_order_status"], stop_loss=data["stop_loss_abs"], stop_loss_pct=data["stop_loss_ratio"], - stoploss_last_update=( - datetime.fromtimestamp(data["stoploss_last_update_timestamp"] // 1000, - tz=timezone.utc) - if data["stoploss_last_update_timestamp"] else None), initial_stop_loss=data["initial_stop_loss_abs"], initial_stop_loss_pct=data["initial_stop_loss_ratio"], min_rate=data["min_rate"], @@ -1487,8 +1481,6 @@ class Trade(ModelBase, LocalTrade): Float(), nullable=True) # type: ignore is_stop_loss_trailing: Mapped[bool] = mapped_column( nullable=False, default=False) # type: ignore - # last update time of the stoploss order on exchange - stoploss_last_update: Mapped[Optional[datetime]] = mapped_column(nullable=True) # type: ignore # absolute value of the highest reached price max_rate: Mapped[Optional[float]] = mapped_column( Float(), nullable=True, default=0.0) # type: ignore From acbea4e26ffe9e6e7ed927ca243a9537a65b8dfe Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jan 2024 18:15:21 +0100 Subject: [PATCH 053/327] Fix some tests after update_stoploss_date removal --- tests/test_freqtradebot.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 57cd1a2f5..2c14fdca1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1675,6 +1675,7 @@ def test_handle_stoploss_on_exchange_trailing( ft_amount=trade.amount, ft_price=trade.stop_loss, order_id='100', + order_date=dt_now() - timedelta(minutes=20), ) ) @@ -1767,8 +1768,9 @@ def test_handle_stoploss_on_exchange_trailing( @pytest.mark.parametrize("is_short", [False, True]) def test_handle_stoploss_on_exchange_trailing_error( - mocker, default_conf_usdt, fee, caplog, limit_order, is_short + mocker, default_conf_usdt, fee, caplog, limit_order, is_short, time_machine ) -> None: + time_machine.move_to(dt_now() - timedelta(minutes=601)) enter_order = limit_order[entry_side(is_short)] exit_order = limit_order[exit_side(is_short)] # When trailing stoploss is set @@ -1809,7 +1811,6 @@ def test_handle_stoploss_on_exchange_trailing_error( trade.is_short = is_short trade.is_open = True trade.stop_loss = 0.2 - trade.stoploss_last_update = (dt_now() - timedelta(minutes=601)).replace(tzinfo=None) stoploss_order_hanging = { 'id': "abcd", @@ -1829,12 +1830,14 @@ def test_handle_stoploss_on_exchange_trailing_error( ft_amount=trade.amount, ft_price=3, order_id='abcd', + order_date=dt_now(), ) ) mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=InvalidOrderException()) mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value=stoploss_order_hanging) + time_machine.shift(timedelta(minutes=50)) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog) @@ -1844,10 +1847,10 @@ def test_handle_stoploss_on_exchange_trailing_error( assert len(trade.open_sl_orders) == 2 # Fail creating stoploss order - trade.stoploss_last_update = dt_now() - timedelta(minutes=601) caplog.clear() cancel_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order') mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError()) + time_machine.shift(timedelta(minutes=50)) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 2 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog) From 88ba82d4fd4ee74bf86bdc599d7dceb5b1c58b02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jan 2024 18:17:31 +0100 Subject: [PATCH 054/327] Fix more tests --- tests/test_freqtradebot.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2c14fdca1..2d962ae1b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1383,8 +1383,9 @@ def test_handle_stoploss_on_exchange_partial( @pytest.mark.parametrize("is_short", [False, True]) def test_handle_stoploss_on_exchange_partial_cancel_here( - mocker, default_conf_usdt, fee, is_short, limit_order, caplog) -> None: + mocker, default_conf_usdt, fee, is_short, limit_order, caplog, time_machine) -> None: stop_order_dict = {'id': "101", "status": "open"} + time_machine.move_to(dt_now()) default_conf_usdt['trailing_stop'] = True stoploss = MagicMock(return_value=stop_order_dict) enter_order = limit_order[entry_side(is_short)] @@ -1443,7 +1444,7 @@ def test_handle_stoploss_on_exchange_partial_cancel_here( }) mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel) - trade.stoploss_last_update = dt_now() - timedelta(minutes=10) + time_machine.shift(timedelta(minutes=15)) assert freqtrade.handle_stoploss_on_exchange(trade) is False # Canceled Stoploss filled partially ... @@ -1934,7 +1935,6 @@ def test_handle_stoploss_on_exchange_custom_stop( trade = Trade.session.scalars(select(Trade)).first() trade.is_short = is_short trade.is_open = True - trade.stoploss_last_update = dt_now() - timedelta(minutes=601) trade.orders.append( Order( ft_order_side='stoploss', @@ -1942,6 +1942,7 @@ def test_handle_stoploss_on_exchange_custom_stop( ft_is_open=True, ft_amount=trade.amount, ft_price=trade.stop_loss, + order_date=dt_now() - timedelta(minutes=601), order_id='100', ) ) From 6eaf42fe33fb1106c0241ca86cd6a1d3092cda4f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jan 2024 18:17:53 +0100 Subject: [PATCH 055/327] Default order_date to dt_now if it's not set via ccxt and wasn't previously set. --- freqtrade/persistence/trade_model.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 9db13dabc..1484c006f 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -170,6 +170,8 @@ class Order(ModelBase): 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: + self.order_date = dt_now() self.ft_is_open = True if self.status in NON_OPEN_EXCHANGE_STATES: From 58058f0197ee331832ca1c1889eae2aba718b2ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jan 2024 18:20:23 +0100 Subject: [PATCH 056/327] Fix migration test --- tests/persistence/test_migrations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/persistence/test_migrations.py b/tests/persistence/test_migrations.py index 6ef098cb3..a6a107a5e 100644 --- a/tests/persistence/test_migrations.py +++ b/tests/persistence/test_migrations.py @@ -277,7 +277,6 @@ def test_migrate(mocker, default_conf, fee, caplog): assert trade.exit_reason is None assert trade.strategy is None assert trade.timeframe == '5m' - assert trade.stoploss_last_update is None assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak2", caplog) assert log_has("Running database migration for trades - backup: trades_bak2, orders_bak0", From 3ab226a0965fdaf7428a75e5d6b5245bc812272c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jan 2024 18:24:03 +0100 Subject: [PATCH 057/327] Remove unused import --- freqtrade/persistence/migrations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index eb55cf455..cf2e06f71 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -1,7 +1,7 @@ import logging from typing import List, Optional -from sqlalchemy import inspect, select, text, tuple_, update +from sqlalchemy import inspect, select, text, update from freqtrade.exceptions import OperationalException from freqtrade.persistence.trade_model import Order, Trade From e76888882dadae9286ed1dcc64830b487c9fd5ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 19 Jan 2024 18:59:52 +0100 Subject: [PATCH 058/327] Fix typehint --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 26631eb30..73fa9fa68 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1289,7 +1289,7 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") - def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: Dict): + def manage_trade_stoploss_orders(self, trade: Trade, stoploss_orders: List[Dict]): """ Perform required actions acording to existing stoploss orders of trade :param trade: Corresponding Trade From ad0f88796bd6854eb0840e258ea5b29a926d9c60 Mon Sep 17 00:00:00 2001 From: Shane Date: Sat, 20 Jan 2024 10:42:37 +1100 Subject: [PATCH 059/327] fix: logical error Fix logical error in the conditional checks for model classes. The `elif` statement that looks for "lightgbm.sklearn" or "xgb" in the model class string is now broken into two separate conditions because the old condition would always evaluate to `True` due to the non-empty string "xgb". --- freqtrade/freqai/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqai/utils.py b/freqtrade/freqai/utils.py index 4428d9578..bc335bf20 100644 --- a/freqtrade/freqai/utils.py +++ b/freqtrade/freqai/utils.py @@ -118,10 +118,12 @@ def plot_feature_importance(model: Any, pair: str, dk: FreqaiDataKitchen, mdl = models[label] if "catboost.core" in str(mdl.__class__): feature_importance = mdl.get_feature_importance() - elif "lightgbm.sklearn" or "xgb" in str(mdl.__class__): + elif "lightgbm.sklearn" in str(mdl.__class__): + feature_importance = mdl.feature_importances_ + elif "xgb" in str(mdl.__class__): feature_importance = mdl.feature_importances_ else: - logger.info('Model type not support for generating feature importances.') + logger.info('Model type does not support for generating feature importances.') return # Data preparation From b02e15b1624cf238f968f6b72e04d8f430dd457b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jan 2024 03:27:55 +0000 Subject: [PATCH 060/327] Bump lightgbm from 4.2.0 to 4.3.0 Bumps [lightgbm](https://github.com/microsoft/LightGBM) from 4.2.0 to 4.3.0. - [Release notes](https://github.com/microsoft/LightGBM/releases) - [Commits](https://github.com/microsoft/LightGBM/compare/v4.2.0...v4.3.0) --- updated-dependencies: - dependency-name: lightgbm dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 988ac2143..2d33efc3c 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -6,7 +6,7 @@ scikit-learn==1.4.0 joblib==1.3.2 catboost==1.2.2; 'arm' not in platform_machine and python_version < '3.12' -lightgbm==4.2.0 +lightgbm==4.3.0 xgboost==2.0.3 tensorboard==2.15.1 datasieve==0.1.7 From d82bfc9fad00abcd750bb5ec32740582b906ecf9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jan 2024 19:09:25 +0100 Subject: [PATCH 061/327] Add order_tag to orders model --- freqtrade/persistence/migrations.py | 6 ++++-- freqtrade/persistence/trade_model.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index bb6c04922..4c748b15d 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -223,6 +223,7 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List): 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: @@ -230,13 +231,14 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List): 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, - ft_amount, ft_price, ft_cancel_reason + ft_amount, ft_price, ft_cancel_reason, ft_order_tag ) select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id, status, symbol, order_type, side, price, amount, filled, {average} average, remaining, cost, {stop_price} stop_price, order_date, order_filled_date, order_update_date, {ft_fee_base} ft_fee_base, {funding_fee} funding_fee, - {ft_amount} ft_amount, {ft_price} ft_price, {ft_cancel_reason} ft_cancel_reason + {ft_amount} ft_amount, {ft_price} ft_price, {ft_cancel_reason} ft_cancel_reason, + {ft_order_tag} ft_order_tag from {table_back_name} """)) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 856a33abf..005017dbb 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -89,6 +89,8 @@ 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) @property def order_date_utc(self) -> datetime: From ccd4c715ca14bb37f46ddd2c1dc56a78c4c6331e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jan 2024 19:12:53 +0100 Subject: [PATCH 062/327] add order-tag to serialize / unserialize methods --- freqtrade/persistence/trade_model.py | 2 ++ freqtrade/rpc/api_server/api_schemas.py | 1 + 2 files changed, 3 insertions(+) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 005017dbb..2202b6e95 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -242,6 +242,7 @@ class Order(ModelBase): 'remaining': self.remaining, 'ft_fee_base': self.ft_fee_base, 'funding_fee': self.funding_fee, + 'ft_order_tag': self.ft_order_tag, }) return resp @@ -1407,6 +1408,7 @@ class LocalTrade: ft_price=order["price"], remaining=order["remaining"], funding_fee=order.get("funding_fee", None), + ft_order_tag=order.get("ft_order_tag", None), ) trade.orders.append(order_obj) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 20a614798..791f70fa0 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -261,6 +261,7 @@ class OrderSchema(BaseModel): order_timestamp: Optional[int] = None order_filled_timestamp: Optional[int] = None ft_fee_base: Optional[float] = None + ft_order_tag: Optional[str] = None class TradeSchema(BaseModel): From 39ffee381b99eb3e37db6ab32f9b2fea96824333 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jan 2024 19:23:18 +0100 Subject: [PATCH 063/327] Improve type hint --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2aa8a23d6..e3384d3e2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -685,7 +685,7 @@ class Backtesting: return None def _exit_trade(self, trade: LocalTrade, sell_row: Tuple, - close_rate: float, amount: Optional[float] = None) -> Optional[LocalTrade]: + close_rate: float, amount: float) -> Optional[LocalTrade]: self.order_id_counter += 1 exit_candle_time = sell_row[DATE_IDX].to_pydatetime() order_type = self.strategy.order_types['exit'] From e8288a34c94c09e914ebf45346f024c3b9a593c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jan 2024 19:56:30 +0100 Subject: [PATCH 064/327] add ft_order_tag to backtesting --- freqtrade/optimize/backtesting.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e3384d3e2..29fafdb97 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -681,11 +681,11 @@ class Backtesting: trade.exit_reason = exit_reason - return self._exit_trade(trade, row, close_rate, amount_) + return self._exit_trade(trade, row, close_rate, amount_, exit_reason) return None - def _exit_trade(self, trade: LocalTrade, sell_row: Tuple, - close_rate: float, amount: float) -> Optional[LocalTrade]: + def _exit_trade(self, trade: LocalTrade, sell_row: Tuple, close_rate: float, + amount: float, exit_reason: Optional[str]) -> Optional[LocalTrade]: self.order_id_counter += 1 exit_candle_time = sell_row[DATE_IDX].to_pydatetime() order_type = self.strategy.order_types['exit'] @@ -712,6 +712,7 @@ class Backtesting: filled=0, remaining=amount, cost=amount * close_rate, + ft_order_tag=exit_reason, ) order._trade_bt = trade trade.orders.append(order) @@ -944,6 +945,7 @@ class Backtesting: filled=0, remaining=amount, cost=amount * propose_rate + trade.fee_open, + ft_order_tag=entry_tag, ) order._trade_bt = trade trade.orders.append(order) @@ -963,7 +965,8 @@ class Backtesting: # Ignore trade if entry-order did not fill yet continue exit_row = data[pair][-1] - self._exit_trade(trade, exit_row, exit_row[OPEN_IDX], trade.amount) + self._exit_trade(trade, exit_row, exit_row[OPEN_IDX], trade.amount, + ExitType.FORCE_EXIT.value) trade.orders[-1].close_bt_order(exit_row[DATE_IDX].to_pydatetime(), trade) trade.close_date = exit_row[DATE_IDX].to_pydatetime() From 95e51bf816971e42f94d6e3c4b96ed50af9c34c7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jan 2024 20:05:40 +0100 Subject: [PATCH 065/327] allow adjust_trade_position to return tuples in backtesting --- freqtrade/optimize/backtesting.py | 14 +++++++++++--- freqtrade/strategy/interface.py | 4 +++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 29fafdb97..8386a04e3 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -537,14 +537,22 @@ class Backtesting: min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1) max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate) stake_available = self.wallets.get_available_stake_amount() - stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, - default_retval=None, supress_error=True)( + resp = strategy_safe_wrapper(self.strategy.adjust_trade_position, + default_retval=None, supress_error=True)( trade=trade, # type: ignore[arg-type] current_time=current_time, current_rate=current_rate, current_profit=current_profit, min_stake=min_stake, max_stake=min(max_stake, stake_available), current_entry_rate=current_rate, current_exit_rate=current_rate, current_entry_profit=current_profit, current_exit_profit=current_profit) + order_tag = '' + if isinstance(resp, tuple): + if len(resp) >= 1: + stake_amount = resp[0] + if len(resp) > 1: + order_tag = resp[1] or '' + else: + stake_amount = resp # Check if we should increase our position if stake_amount is not None and stake_amount > 0.0: @@ -569,7 +577,7 @@ class Backtesting: if min_stake and remaining != 0 and remaining < min_stake: # Remaining stake is too low to be sold. return trade - exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT) + exit_ = ExitCheckTuple(ExitType.PARTIAL_EXIT, order_tag) pos_trade = self._get_exit_for_signal(trade, row, exit_, current_time, amount) if pos_trade is not None: order = pos_trade.orders[-1] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 7f10c2ea2..341dd0687 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -511,7 +511,8 @@ class IStrategy(ABC, HyperStrategyMixin): min_stake: Optional[float], max_stake: float, current_entry_rate: float, current_exit_rate: float, current_entry_profit: float, current_exit_profit: float, - **kwargs) -> Optional[float]: + **kwargs + ) -> Union[Optional[float], Tuple[Optional[float], Optional[str]]]: """ Custom trade adjustment logic, returning the stake amount that a trade should be increased or decreased. @@ -537,6 +538,7 @@ class IStrategy(ABC, HyperStrategyMixin): :return float: Stake amount to adjust your trade, Positive values to increase position, Negative values to decrease position. Return None for no action. + Optionally, return a tuple with a 2nd element with an order reason """ return None From 535ff387ff6a870d386a4fd05cf3152c7f6bde0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jan 2024 20:12:37 +0100 Subject: [PATCH 066/327] add order_tag handlig to running bot --- freqtrade/freqtradebot.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0d7cef827..3355ed8d4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -645,8 +645,8 @@ class FreqtradeBot(LoggingMixin): max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate) stake_available = self.wallets.get_available_stake_amount() logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") - stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position, - default_retval=None, supress_error=True)( + resp = strategy_safe_wrapper(self.strategy.adjust_trade_position, + default_retval=None, supress_error=True)( trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_entry_rate, current_profit=current_entry_profit, min_stake=min_entry_stake, @@ -654,6 +654,14 @@ class FreqtradeBot(LoggingMixin): current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate, current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit ) + order_tag = '' + if isinstance(resp, tuple): + if len(resp) >= 1: + stake_amount = resp[0] + if len(resp) > 1: + order_tag = resp[1] or '' + else: + stake_amount = resp if stake_amount is not None and stake_amount > 0.0: # We should increase our position @@ -665,7 +673,8 @@ class FreqtradeBot(LoggingMixin): else: logger.debug("Max adjustment entries is set to unlimited.") self.execute_entry(trade.pair, stake_amount, price=current_entry_rate, - trade=trade, is_short=trade.is_short, mode='pos_adjust') + trade=trade, is_short=trade.is_short, mode='pos_adjust', + enter_tag=order_tag) if stake_amount is not None and stake_amount < 0.0: # We should decrease our position @@ -684,7 +693,7 @@ class FreqtradeBot(LoggingMixin): return self.execute_trade_exit(trade, current_exit_rate, exit_check=ExitCheckTuple( - exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount) + exit_type=ExitType.PARTIAL_EXIT), sub_trade_amt=amount, exit_tag=order_tag) def _check_depth_of_market(self, pair: str, conf: Dict, side: SignalDirection) -> bool: """ @@ -782,6 +791,7 @@ class FreqtradeBot(LoggingMixin): leverage=leverage ) order_obj = Order.parse_from_ccxt_object(order, pair, side, amount, enter_limit_requested) + order_obj.ft_order_tag = enter_tag order_id = order['id'] order_status = order.get('status') logger.info(f"Order {order_id} was created for {pair} and status is {order_status}.") @@ -1753,6 +1763,7 @@ class FreqtradeBot(LoggingMixin): return False order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side, amount, limit) + order_obj.ft_order_tag = exit_reason trade.orders.append(order_obj) trade.exit_order_status = '' From 830a004dfda7d34dcf88c95ab9147b5e090271f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jan 2024 20:21:47 +0100 Subject: [PATCH 067/327] Move response handling to interface wrappermethod --- freqtrade/freqtradebot.py | 11 +---------- freqtrade/optimize/backtesting.py | 14 +++----------- freqtrade/strategy/interface.py | 30 ++++++++++++++++++++++++++++++ 3 files changed, 34 insertions(+), 21 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3355ed8d4..0eb1c608a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -645,8 +645,7 @@ class FreqtradeBot(LoggingMixin): max_entry_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_entry_rate) stake_available = self.wallets.get_available_stake_amount() logger.debug(f"Calling adjust_trade_position for pair {trade.pair}") - resp = strategy_safe_wrapper(self.strategy.adjust_trade_position, - default_retval=None, supress_error=True)( + stake_amount, order_tag = self.strategy._adjust_trade_position_internal( trade=trade, current_time=datetime.now(timezone.utc), current_rate=current_entry_rate, current_profit=current_entry_profit, min_stake=min_entry_stake, @@ -654,14 +653,6 @@ class FreqtradeBot(LoggingMixin): current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate, current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit ) - order_tag = '' - if isinstance(resp, tuple): - if len(resp) >= 1: - stake_amount = resp[0] - if len(resp) > 1: - order_tag = resp[1] or '' - else: - stake_amount = resp if stake_amount is not None and stake_amount > 0.0: # We should increase our position diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8386a04e3..7c7fa60ed 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -537,22 +537,14 @@ class Backtesting: min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, current_rate, -0.1) max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, current_rate) stake_available = self.wallets.get_available_stake_amount() - resp = strategy_safe_wrapper(self.strategy.adjust_trade_position, - default_retval=None, supress_error=True)( + stake_amount, order_tag = self.strategy._adjust_trade_position_internal( trade=trade, # type: ignore[arg-type] current_time=current_time, current_rate=current_rate, current_profit=current_profit, min_stake=min_stake, max_stake=min(max_stake, stake_available), current_entry_rate=current_rate, current_exit_rate=current_rate, - current_entry_profit=current_profit, current_exit_profit=current_profit) - order_tag = '' - if isinstance(resp, tuple): - if len(resp) >= 1: - stake_amount = resp[0] - if len(resp) > 1: - order_tag = resp[1] or '' - else: - stake_amount = resp + current_entry_profit=current_profit, current_exit_profit=current_profit + ) # Check if we should increase our position if stake_amount is not None and stake_amount > 0.0: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 341dd0687..564d306d7 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -727,6 +727,36 @@ class IStrategy(ABC, HyperStrategyMixin): _ft_stop_uses_after_fill = False + def _adjust_trade_position_internal( + self, trade: Trade, current_time: datetime, + current_rate: float, current_profit: float, + min_stake: Optional[float], max_stake: float, + current_entry_rate: float, current_exit_rate: float, + current_entry_profit: float, current_exit_profit: float, + **kwargs + ) -> Tuple[Optional[float], str]: + """ + wrapper around adjust_trade_position to handle the return value + """ + resp = strategy_safe_wrapper(self.adjust_trade_position, + default_retval=(None, ''), supress_error=True)( + trade=trade, current_time=current_time, + current_rate=current_rate, current_profit=current_profit, + min_stake=min_stake, max_stake=max_stake, + current_entry_rate=current_entry_rate, current_exit_rate=current_exit_rate, + current_entry_profit=current_entry_profit, current_exit_profit=current_exit_profit, + **kwargs + ) + order_tag = '' + if isinstance(resp, tuple): + if len(resp) >= 1: + stake_amount = resp[0] + if len(resp) > 1: + order_tag = resp[1] or '' + else: + stake_amount = resp + return stake_amount, order_tag + def __informative_pairs_freqai(self) -> ListPairsWithTimeframes: """ Create informative-pairs needed for FreqAI From 2d704a77b55749650920a198a093e7f23352eed0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jan 2024 22:42:03 +0100 Subject: [PATCH 068/327] Improve formatting --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 564d306d7..2630c3547 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -734,7 +734,7 @@ class IStrategy(ABC, HyperStrategyMixin): current_entry_rate: float, current_exit_rate: float, current_entry_profit: float, current_exit_profit: float, **kwargs - ) -> Tuple[Optional[float], str]: + ) -> Tuple[Optional[float], str]: """ wrapper around adjust_trade_position to handle the return value """ From 398b93beefd73fbe0a2cf175b1fda4ced1a6ae88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jan 2024 22:48:26 +0100 Subject: [PATCH 069/327] Fix rpc test --- tests/rpc/test_rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 785efc522..ca81ea0e6 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -99,7 +99,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, 'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY, 'remaining': ANY, 'status': ANY, 'ft_is_entry': True, 'ft_fee_base': None, - 'funding_fee': ANY, + 'funding_fee': ANY, 'ft_order_tag': None, }], } mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) From d664e76834566ad49f4c08d31b0044333a116c5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jan 2024 23:05:01 +0100 Subject: [PATCH 070/327] Add some tests --- tests/optimize/test_backtesting_adjust_position.py | 6 ++++-- tests/test_freqtradebot.py | 8 ++++++-- tests/test_integration.py | 5 +++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 56b04b3fd..7f7bbb29f 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -148,7 +148,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera assert pytest.approx(trade.amount) == 47.61904762 * leverage assert len(trade.orders) == 1 # Increase position by 100 - backtesting.strategy.adjust_trade_position = MagicMock(return_value=100) + backtesting.strategy.adjust_trade_position = MagicMock(return_value=(100, 'PartIncrease')) trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time) @@ -156,6 +156,7 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera assert pytest.approx(trade.stake_amount) == 200.0 assert pytest.approx(trade.amount) == 95.23809524 * leverage assert len(trade.orders) == 2 + assert trade.orders[-1].ft_order_tag == 'PartIncrease' assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791) # Reduce by more than amount - no change to trade. @@ -171,13 +172,14 @@ def test_backtest_position_adjustment_detailed(default_conf, fee, mocker, levera assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791) # Reduce position by 50 - backtesting.strategy.adjust_trade_position = MagicMock(return_value=-100) + backtesting.strategy.adjust_trade_position = MagicMock(return_value=(-100, 'partDecrease')) trade = backtesting._get_adjust_trade_entry_for_candle(trade, row, current_time) assert trade assert pytest.approx(trade.stake_amount) == 100.0 assert pytest.approx(trade.amount) == 47.61904762 * leverage assert len(trade.orders) == 3 + assert trade.orders[-1].ft_order_tag == 'partDecrease' assert trade.nr_of_successful_entries == 2 assert trade.nr_of_successful_exits == 1 assert pytest.approx(trade.liquidation_price) == (0.1038916 if leverage == 1 else 1.2127791) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e61d5804d..8e17604ab 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -6725,11 +6725,15 @@ def test_check_and_call_adjust_trade_position(mocker, default_conf_usdt, fee, ca ) create_mock_trades(fee) caplog.set_level(logging.DEBUG) - freqtrade.strategy.adjust_trade_position = MagicMock(return_value=10) + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(10, 'aaaa')) freqtrade.process_open_trade_positions() assert log_has_re(r"Max adjustment entries for .* has been reached\.", caplog) + assert freqtrade.strategy.adjust_trade_position.call_count == 1 caplog.clear() - freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-10) + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-10, 'partial_exit_c')) freqtrade.process_open_trade_positions() assert log_has_re(r"LIMIT_SELL has been fulfilled.*", caplog) + assert freqtrade.strategy.adjust_trade_position.call_count == 1 + trade = Trade.get_trades(trade_filter=[Trade.id == 5]).first() + assert trade.orders[-1].ft_order_tag == 'partial_exit_c' diff --git a/tests/test_integration.py b/tests/test_integration.py index 2e7f38fc8..94253dffb 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -536,7 +536,7 @@ def test_dca_order_adjust_entry_replace_fails( # Create DCA order for 2nd trade (so we have 2 open orders on 2 trades) # this 2nd order won't fill. - freqtrade.strategy.adjust_trade_position = MagicMock(return_value=20) + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(20, 'PeNF')) freqtrade.process() @@ -627,12 +627,13 @@ def test_dca_exiting(default_conf_usdt, ticker_usdt, fee, mocker, caplog, levera assert log_has_re( r"Remaining amount of \d\.\d+.* would be smaller than the minimum of 10.", caplog) - freqtrade.strategy.adjust_trade_position = MagicMock(return_value=-20) + freqtrade.strategy.adjust_trade_position = MagicMock(return_value=(-20, 'PES')) freqtrade.process() trade = Trade.get_trades().first() assert len(trade.orders) == 2 assert trade.orders[-1].ft_order_side == 'sell' + assert trade.orders[-1].ft_order_tag == 'PES' assert pytest.approx(trade.stake_amount) == 40.198 assert pytest.approx(trade.amount) == 20.099 * leverage assert trade.open_rate == 2.0 From 79b8496f38c0b8427f068a2618258921b1cfda1a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jan 2024 23:05:19 +0100 Subject: [PATCH 071/327] Fix backtesting not setting entry_tag out of position adjustments --- freqtrade/optimize/backtesting.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7c7fa60ed..21e9c75cc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -554,7 +554,8 @@ class Backtesting: check_adjust_entry = (entry_count <= self.strategy.max_entry_position_adjustment) if check_adjust_entry: pos_trade = self._enter_trade( - trade.pair, row, 'short' if trade.is_short else 'long', stake_amount, trade) + trade.pair, row, 'short' if trade.is_short else 'long', stake_amount, trade, + entry_tag1=order_tag) if pos_trade is not None: self.wallets.update() return pos_trade @@ -836,7 +837,9 @@ class Backtesting: stake_amount: Optional[float] = None, trade: Optional[LocalTrade] = None, requested_rate: Optional[float] = None, - requested_stake: Optional[float] = None) -> Optional[LocalTrade]: + requested_stake: Optional[float] = None, + entry_tag1: Optional[str] = None + ) -> Optional[LocalTrade]: """ :param trade: Trade to adjust - initial entry if None :param requested_rate: Adjusted entry rate @@ -844,7 +847,7 @@ class Backtesting: """ current_time = row[DATE_IDX].to_pydatetime() - entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None + entry_tag = entry_tag1 or (row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None) # let's call the custom entry price, using the open price as default price order_type = self.strategy.order_types['entry'] pos_adjust = trade is not None and requested_rate is None From 6806fab1b5339499ec825988a637541dbb2eddb4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jan 2024 06:32:29 +0100 Subject: [PATCH 072/327] Fix migration not triggering --- freqtrade/persistence/migrations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 4c748b15d..f4d5a7174 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -333,8 +333,8 @@ def check_migrate(engine, decl_base, previous_tables) -> None: # if ('orders' not in previous_tables # or not has_column(cols_orders, 'funding_fee')): migrating = False - # if not has_column(cols_orders, 'ft_cancel_reason'): - if not has_column(cols_trades, 'funding_fee_running'): + # if not has_column(cols_trades, 'funding_fee_running'): + 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}") From 0fa0f49b75fd4ad2b6a88741a27d827b9588fa64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Jan 2024 07:20:39 +0100 Subject: [PATCH 073/327] Add adjustment order tagging in strategy callbacks docs --- docs/strategy-callbacks.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 7242e9c90..2292b7ed0 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -767,6 +767,7 @@ This callback is **not** called when there is an open order (either buy or sell) `adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible. Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position (negative values will decrease your position), no matter if it's a long or short trade. +Adjustment orders can be assigned with a tag by returning a 2 element Tuple, with the first element being the adjustment amount, and the 2nd element the tag (e.g. `return 250, 'increase_favorable_conditions'`). Modifications to leverage are not possible, and the stake-amount returned is assumed to be before applying leverage. @@ -833,7 +834,8 @@ class DigDeeperStrategy(IStrategy): min_stake: Optional[float], max_stake: float, current_entry_rate: float, current_exit_rate: float, current_entry_profit: float, current_exit_profit: float, - **kwargs) -> Optional[float]: + **kwargs + ) -> Union[Optional[float], Tuple[Optional[float], Optional[str]]]: """ Custom trade adjustment logic, returning the stake amount that a trade should be increased or decreased. @@ -859,11 +861,12 @@ class DigDeeperStrategy(IStrategy): :return float: Stake amount to adjust your trade, Positive values to increase position, Negative values to decrease position. Return None for no action. + Optionally, return a tuple with a 2nd element with an order reason """ if current_profit > 0.05 and trade.nr_of_successful_exits == 0: # Take half of the profit at +5% - return -(trade.stake_amount / 2) + return -(trade.stake_amount / 2), 'half_profit_5%' if current_profit > -0.05: return None @@ -891,7 +894,7 @@ class DigDeeperStrategy(IStrategy): stake_amount = filled_entries[0].stake_amount # This then calculates current safety order size stake_amount = stake_amount * (1 + (count_of_entries * 0.25)) - return stake_amount + return stake_amount, '1/3rd_increase' except Exception as exception: return None From 78a1c7247a35bc2f8af037f443dc6c5cda21e2b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Jan 2024 07:25:15 +0100 Subject: [PATCH 074/327] keep ft_order_tag for backtest responses --- freqtrade/persistence/trade_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 2202b6e95..0c59df33d 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -221,6 +221,7 @@ class Order(ModelBase): 'order_filled_timestamp': int(self.order_filled_date.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None, 'ft_is_entry': self.ft_order_side == entry_side, + 'ft_order_tag': self.ft_order_tag, } if not minified: resp.update({ @@ -242,7 +243,6 @@ class Order(ModelBase): 'remaining': self.remaining, 'ft_fee_base': self.ft_fee_base, 'funding_fee': self.funding_fee, - 'ft_order_tag': self.ft_order_tag, }) return resp From 6ba896609004626492ae21b423fff0c113a37d01 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Jan 2024 07:25:46 +0100 Subject: [PATCH 075/327] chore: Add minified parameter docstring to to_json method --- freqtrade/persistence/trade_model.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 0c59df33d..7e3cf970f 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -214,6 +214,10 @@ class Order(ModelBase): return order def to_json(self, entry_side: str, minified: bool = False) -> Dict[str, Any]: + """ + :param minified: If True, only return a subset of the data is returned. + Only used for backtesting. + """ resp = { 'amount': self.safe_amount, 'safe_price': self.safe_price, From d1a96af5e8b2543895797aa4a2ae345fdcad558d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Jan 2024 07:28:36 +0100 Subject: [PATCH 076/327] Add ft_order_Tag to backtest test --- tests/optimize/test_backtesting.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 87e92071f..603fcc310 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -742,14 +742,18 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: 'orders': [ [ {'amount': 0.00957442, 'safe_price': 0.104445, 'ft_order_side': 'buy', - 'order_filled_timestamp': 1517251200000, 'ft_is_entry': True}, + 'order_filled_timestamp': 1517251200000, 'ft_is_entry': True, + 'ft_order_tag': ''}, {'amount': 0.00957442, 'safe_price': 0.10496853383458644, 'ft_order_side': 'sell', - 'order_filled_timestamp': 1517265300000, 'ft_is_entry': False} + 'order_filled_timestamp': 1517265300000, 'ft_is_entry': False, + 'ft_order_tag': 'roi'} ], [ {'amount': 0.0097064, 'safe_price': 0.10302485, 'ft_order_side': 'buy', - 'order_filled_timestamp': 1517283000000, 'ft_is_entry': True}, + 'order_filled_timestamp': 1517283000000, 'ft_is_entry': True, + 'ft_order_tag': ''}, {'amount': 0.0097064, 'safe_price': 0.10354126528822055, 'ft_order_side': 'sell', - 'order_filled_timestamp': 1517285400000, 'ft_is_entry': False} + 'order_filled_timestamp': 1517285400000, 'ft_is_entry': False, + 'ft_order_tag': 'roi'} ] ] }) From 67c3bad97790de375e696254c842cfe6f4e4ceae Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Feb 2024 06:43:41 +0100 Subject: [PATCH 077/327] Fix misspelled comment --- tests/test_freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b3df537f8..bd0b131af 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4209,7 +4209,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( assert not trade.has_open_orders # Assuming stoploss on exchange is hit - # trade should be sold at the price of stoploss, with exit_reaeon STOPLOSS_ON_EXCHANGE + # trade should be sold at the price of stoploss, with exit_reason STOPLOSS_ON_EXCHANGE stoploss_executed = MagicMock(return_value={ "id": "123", "timestamp": 1542707426845, From ad121c19b02d65134df952291ffe9da563d03143 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Feb 2024 06:57:12 +0100 Subject: [PATCH 078/327] Allow <1m timeframes for utility modes --- freqtrade/exchange/exchange.py | 8 ++++++-- tests/exchange/test_exchange.py | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 4b2d52a68..85a77fe5e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -23,7 +23,7 @@ from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHAN BuySell, Config, EntryExit, ExchangeConfig, ListPairsWithTimeframes, MakerTaker, OBLiteral, PairWithTimeframe) from freqtrade.data.converter import clean_ohlcv_dataframe, ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, TradingMode +from freqtrade.enums import OPTIMIZE_MODES, CandleType, MarginMode, PriceType, RunMode, TradingMode from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) @@ -595,7 +595,11 @@ class Exchange: raise OperationalException( f"Invalid timeframe '{timeframe}'. This exchange supports: {self.timeframes}") - if timeframe and timeframe_to_minutes(timeframe) < 1: + if ( + timeframe + and self._config['runmode'] != RunMode.UTIL_EXCHANGE + and timeframe_to_minutes(timeframe) < 1 + ): raise OperationalException("Timeframes < 1m are currently not supported by Freqtrade.") def validate_ordertypes(self, order_types: Dict) -> None: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index f686959fc..29e458cdd 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -9,7 +9,7 @@ import ccxt import pytest from pandas import DataFrame -from freqtrade.enums import CandleType, MarginMode, TradingMode +from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) @@ -796,7 +796,9 @@ def test_validate_timeframes_failed(default_conf, mocker): mocker.patch(f'{EXMS}._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch(f'{EXMS}._load_markets', MagicMock(return_value={})) - mocker.patch(f'{EXMS}.validate_pairs', MagicMock()) + mocker.patch(f'{EXMS}.validate_pairs') + mocker.patch(f'{EXMS}.validate_stakecurrency') + mocker.patch(f'{EXMS}.validate_pricing') with pytest.raises(OperationalException, match=r"Invalid timeframe '3m'. This exchange supports.*"): Exchange(default_conf) @@ -806,6 +808,10 @@ def test_validate_timeframes_failed(default_conf, mocker): match=r"Timeframes < 1m are currently not supported by Freqtrade."): Exchange(default_conf) + # Will not raise an exception in util mode. + default_conf['runmode'] = RunMode.UTIL_EXCHANGE + Exchange(default_conf) + def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): default_conf["timeframe"] = "3m" From 0d14b7a800eaecc38335e27913359f630f3e3a77 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Feb 2024 07:03:44 +0100 Subject: [PATCH 079/327] perf: only create detail timedelta object once for detail backtests --- freqtrade/optimize/backtesting.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 21e9c75cc..43aa00a65 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -185,13 +185,14 @@ class Backtesting: # Load detail timeframe if specified self.timeframe_detail = str(self.config.get('timeframe_detail', '')) if self.timeframe_detail: - self.timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail) - if self.timeframe_min <= self.timeframe_detail_min: + timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail) + self.timeframe_detail_td = timedelta(minutes=timeframe_detail_min) + if self.timeframe_min <= timeframe_detail_min: raise OperationalException( "Detail timeframe must be smaller than strategy timeframe.") else: - self.timeframe_detail_min = 0 + self.timeframe_detail_td = timedelta(seconds=0) self.detail_data: Dict[str, DataFrame] = {} self.futures_data: Dict[str, DataFrame] = {} @@ -1268,7 +1269,7 @@ class Backtesting: open_trade_count_start = self.backtest_loop( det_row, pair, current_time_det, end_date, open_trade_count_start, trade_dir, is_first) - current_time_det += timedelta(minutes=self.timeframe_detail_min) + current_time_det += self.timeframe_detail_td is_first = False else: self.dataprovider._set_dataframe_max_date(current_time) From 80f6fbbae9970c05cbbb3f3ce76f3debe62bc9a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Feb 2024 07:07:58 +0100 Subject: [PATCH 080/327] chore: Move bot-related tests to test subdir --- tests/{ => freqtradebot}/test_freqtradebot.py | 0 tests/{ => freqtradebot}/test_integration.py | 0 tests/{ => freqtradebot}/test_worker.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tests/{ => freqtradebot}/test_freqtradebot.py (100%) rename tests/{ => freqtradebot}/test_integration.py (100%) rename tests/{ => freqtradebot}/test_worker.py (100%) diff --git a/tests/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py similarity index 100% rename from tests/test_freqtradebot.py rename to tests/freqtradebot/test_freqtradebot.py diff --git a/tests/test_integration.py b/tests/freqtradebot/test_integration.py similarity index 100% rename from tests/test_integration.py rename to tests/freqtradebot/test_integration.py diff --git a/tests/test_worker.py b/tests/freqtradebot/test_worker.py similarity index 100% rename from tests/test_worker.py rename to tests/freqtradebot/test_worker.py From 57df7d9ecaabd4a724b37b7868308dcd6eb3abd3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Feb 2024 07:23:19 +0100 Subject: [PATCH 081/327] chore: convert test dir to package --- tests/freqtradebot/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/freqtradebot/__init__.py diff --git a/tests/freqtradebot/__init__.py b/tests/freqtradebot/__init__.py new file mode 100644 index 000000000..e69de29bb From 8469484998333f1edbf063563c5fddf4c113acf6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Feb 2024 07:25:53 +0100 Subject: [PATCH 082/327] chore: Split stoploss tests from freqtradebot stoploss on exchange tests are quiet extensive, and deserve their own test file. --- tests/freqtradebot/test_freqtradebot.py | 1315 ---------------- .../freqtradebot/test_stoploss_on_exchange.py | 1334 +++++++++++++++++ 2 files changed, 1334 insertions(+), 1315 deletions(-) create mode 100644 tests/freqtradebot/test_stoploss_on_exchange.py diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index bd0b131af..ca6f29078 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -20,7 +20,6 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie TemporaryError) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Order, PairLocks, Trade -from freqtrade.persistence.models import PairLock from freqtrade.plugins.protections.iprotection import ProtectionReturn from freqtrade.util.datetime_helpers import dt_now, dt_utc from freqtrade.worker import Worker @@ -1090,1070 +1089,6 @@ def test_execute_entry_min_leverage(mocker, default_conf_usdt, fee, limit_order, # assert trade.stake_amount == 2 -@pytest.mark.parametrize("is_short", [False, True]) -def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short, fee) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 1.9, - 'ask': 2.2, - 'last': 1.9 - }), - create_order=MagicMock(return_value=limit_order[entry_side(is_short)]), - get_fee=fee, - ) - order = limit_order[entry_side(is_short)] - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch(f'{EXMS}.fetch_order', return_value=order) - mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[]) - - stoploss = MagicMock(return_value={'id': 13434334}) - mocker.patch(f'{EXMS}.create_stoploss', stoploss) - - freqtrade = FreqtradeBot(default_conf_usdt) - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short - trade.is_open = True - trades = [trade] - - freqtrade.exit_positions(trades) - assert trade.has_open_sl_orders is True - assert stoploss.call_count == 1 - assert trade.is_open is True - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short, - limit_order) -> None: - stop_order_dict = {'id': "13434334"} - stoploss = MagicMock(return_value=stop_order_dict) - enter_order = limit_order[entry_side(is_short)] - exit_order = limit_order[exit_side(is_short)] - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 1.9, - 'ask': 2.2, - 'last': 1.9 - }), - create_order=MagicMock(side_effect=[ - enter_order, - exit_order, - ]), - get_fee=fee, - create_stoploss=stoploss - ) - freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - - # First case: when stoploss is not yet set but the order is open - # should get the stoploss order id immediately - # and should return false as no trade actually happened - - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - assert trade.is_short == is_short - assert trade.is_open - assert trade.has_open_sl_orders is False - - assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert stoploss.call_count == 1 - assert trade.open_sl_orders[-1].order_id == "13434334" - - # Second case: when stoploss is set but it is not yet hit - # should do nothing and return false - trade.is_open = True - - hanging_stoploss_order = MagicMock(return_value={'id': '13434334', 'status': 'open'}) - mocker.patch(f'{EXMS}.fetch_stoploss_order', hanging_stoploss_order) - - assert freqtrade.handle_stoploss_on_exchange(trade) is False - hanging_stoploss_order.assert_called_once_with('13434334', trade.pair) - assert len(trade.open_sl_orders) == 1 - assert trade.open_sl_orders[-1].order_id == "13434334" - - # Third case: when stoploss was set but it was canceled for some reason - # should set a stoploss immediately and return False - caplog.clear() - trade.is_open = True - - canceled_stoploss_order = MagicMock(return_value={'id': '13434334', 'status': 'canceled'}) - mocker.patch(f'{EXMS}.fetch_stoploss_order', canceled_stoploss_order) - stoploss.reset_mock() - amount_before = trade.amount - - stop_order_dict.update({'id': "103_1"}) - - assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert stoploss.call_count == 1 - assert len(trade.open_sl_orders) == 1 - assert trade.open_sl_orders[-1].order_id == "103_1" - assert trade.amount == amount_before - - # Fourth case: when stoploss is set and it is hit - # should return true as a trade actually happened - caplog.clear() - stop_order_dict.update({'id': "103_1"}) - - trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short - trade.is_open = True - - stoploss_order_hit = MagicMock(return_value={ - 'id': "103_1", - 'status': 'closed', - 'type': 'stop_loss_limit', - 'price': 3, - 'average': 2, - 'filled': enter_order['amount'], - 'remaining': 0, - 'amount': enter_order['amount'], - }) - mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) - assert freqtrade.handle_stoploss_on_exchange(trade) is True - assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) - assert len(trade.open_sl_orders) == 0 - assert trade.is_open is False - caplog.clear() - - mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError()) - trade.is_open = True - freqtrade.handle_stoploss_on_exchange(trade) - assert log_has('Unable to place a stoploss order on exchange.', caplog) - assert len(trade.open_sl_orders) == 0 - - # Fifth case: fetch_order returns InvalidOrder - # It should try to add stoploss order - stop_order_dict.update({'id': "105"}) - stoploss.reset_mock() - mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch(f'{EXMS}.create_stoploss', stoploss) - freqtrade.handle_stoploss_on_exchange(trade) - assert len(trade.open_sl_orders) == 1 - assert stoploss.call_count == 1 - - # Sixth case: Closed Trade - # Should not create new order - trade.is_open = False - trade.open_sl_orders[-1].ft_is_open = False - stoploss.reset_mock() - mocker.patch(f'{EXMS}.fetch_order') - mocker.patch(f'{EXMS}.create_stoploss', stoploss) - assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert trade.has_open_sl_orders is False - assert stoploss.call_count == 0 - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, is_short, - limit_order) -> None: - stop_order_dict = {'id': "13434334"} - stoploss = MagicMock(return_value=stop_order_dict) - enter_order = limit_order[entry_side(is_short)] - exit_order = limit_order[exit_side(is_short)] - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 1.9, - 'ask': 2.2, - 'last': 1.9 - }), - create_order=MagicMock(side_effect=[ - enter_order, - exit_order, - ]), - get_fee=fee, - create_stoploss=stoploss - ) - freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - assert trade.is_short == is_short - assert trade.is_open - assert trade.has_open_sl_orders is False - - # emergency exit triggered - # Trailing stop should not act anymore - stoploss_order_cancelled = MagicMock(side_effect=[{ - 'id': "107", - 'status': 'canceled', - 'type': 'stop_loss_limit', - 'price': 3, - 'average': 2, - 'amount': enter_order['amount'], - 'filled': 0, - 'remaining': enter_order['amount'], - 'info': {'stopPrice': 22}, - }]) - trade.stoploss_last_update = dt_now() - timedelta(hours=1) - trade.stop_loss = 24 - trade.exit_reason = None - trade.orders.append( - Order( - ft_order_side='stoploss', - ft_pair=trade.pair, - ft_is_open=True, - ft_amount=trade.amount, - ft_price=trade.stop_loss, - order_id='107', - status='open', - ) - ) - freqtrade.config['trailing_stop'] = True - stoploss = MagicMock(side_effect=InvalidOrderException()) - assert trade.has_open_sl_orders is True - Trade.commit() - mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', - side_effect=InvalidOrderException()) - mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled) - mocker.patch(f'{EXMS}.create_stoploss', stoploss) - assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert trade.has_open_sl_orders is False - assert trade.is_open is False - assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT) - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_handle_stoploss_on_exchange_partial( - mocker, default_conf_usdt, fee, is_short, limit_order) -> None: - stop_order_dict = {'id': "101", "status": "open"} - stoploss = MagicMock(return_value=stop_order_dict) - enter_order = limit_order[entry_side(is_short)] - exit_order = limit_order[exit_side(is_short)] - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 1.9, - 'ask': 2.2, - 'last': 1.9 - }), - create_order=MagicMock(side_effect=[ - enter_order, - exit_order, - ]), - get_fee=fee, - create_stoploss=stoploss - ) - freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short - trade.is_open = True - - assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert stoploss.call_count == 1 - assert trade.has_open_sl_orders is True - assert trade.open_sl_orders[-1].order_id == "101" - assert trade.amount == 30 - stop_order_dict.update({'id': "102"}) - # Stoploss on exchange is cancelled on exchange, but filled partially. - # Must update trade amount to guarantee successful exit. - stoploss_order_hit = MagicMock(return_value={ - 'id': "101", - 'status': 'canceled', - 'type': 'stop_loss_limit', - 'price': 3, - 'average': 2, - 'filled': trade.amount / 2, - 'remaining': trade.amount / 2, - 'amount': enter_order['amount'], - }) - mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) - assert freqtrade.handle_stoploss_on_exchange(trade) is False - # Stoploss filled partially ... - assert trade.amount == 15 - - assert trade.open_sl_orders[-1].order_id == "102" - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_handle_stoploss_on_exchange_partial_cancel_here( - mocker, default_conf_usdt, fee, is_short, limit_order, caplog, time_machine) -> None: - stop_order_dict = {'id': "101", "status": "open"} - time_machine.move_to(dt_now()) - default_conf_usdt['trailing_stop'] = True - stoploss = MagicMock(return_value=stop_order_dict) - enter_order = limit_order[entry_side(is_short)] - exit_order = limit_order[exit_side(is_short)] - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 1.9, - 'ask': 2.2, - 'last': 1.9 - }), - create_order=MagicMock(side_effect=[ - enter_order, - exit_order, - ]), - get_fee=fee, - create_stoploss=stoploss - ) - freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short - trade.is_open = True - - assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert stoploss.call_count == 1 - assert trade.has_open_sl_orders is True - assert trade.open_sl_orders[-1].order_id == "101" - assert trade.amount == 30 - stop_order_dict.update({'id': "102"}) - # Stoploss on exchange is open. - # Freqtrade cancels the stop - but cancel returns a partial filled order. - stoploss_order_hit = MagicMock(return_value={ - 'id': "101", - 'status': 'open', - 'type': 'stop_loss_limit', - 'price': 3, - 'average': 2, - 'filled': 0, - 'remaining': trade.amount, - 'amount': enter_order['amount'], - }) - stoploss_order_cancel = MagicMock(return_value={ - 'id': "101", - 'status': 'canceled', - 'type': 'stop_loss_limit', - 'price': 3, - 'average': 2, - 'filled': trade.amount / 2, - 'remaining': trade.amount / 2, - 'amount': enter_order['amount'], - }) - mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) - mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel) - time_machine.shift(timedelta(minutes=15)) - - assert freqtrade.handle_stoploss_on_exchange(trade) is False - # Canceled Stoploss filled partially ... - assert log_has_re('Cancelling current stoploss on exchange.*', caplog) - - assert trade.has_open_sl_orders is True - assert trade.open_sl_orders[-1].order_id == "102" - assert trade.amount == 15 - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short, - limit_order) -> None: - # Sixth case: stoploss order was cancelled but couldn't create new one - enter_order = limit_order[entry_side(is_short)] - exit_order = limit_order[exit_side(is_short)] - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 1.9, - 'ask': 2.2, - 'last': 1.9 - }), - create_order=MagicMock(side_effect=[ - enter_order, - exit_order, - ]), - get_fee=fee, - ) - mocker.patch.multiple( - EXMS, - fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': '100'}), - create_stoploss=MagicMock(side_effect=ExchangeError()), - ) - freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - assert trade.is_short == is_short - trade.is_open = True - trade.orders.append( - Order( - ft_order_side='stoploss', - ft_pair=trade.pair, - ft_is_open=True, - ft_amount=trade.amount, - ft_price=trade.stop_loss, - order_id='100', - status='open', - ) - ) - assert trade - - assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert log_has_re(r'All Stoploss orders are cancelled, but unable to recreate one\.', caplog) - assert trade.has_open_sl_orders is False - assert trade.is_open is True - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_create_stoploss_order_invalid_order( - mocker, default_conf_usdt, caplog, fee, is_short, limit_order -): - open_order = limit_order[entry_side(is_short)] - order = limit_order[exit_side(is_short)] - rpc_mock = patch_RPCManager(mocker) - patch_exchange(mocker) - create_order_mock = MagicMock(side_effect=[ - open_order, - order, - ]) - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 1.9, - 'ask': 2.2, - 'last': 1.9 - }), - create_order=create_order_mock, - get_fee=fee, - ) - mocker.patch.multiple( - EXMS, - fetch_order=MagicMock(return_value={'status': 'canceled'}), - create_stoploss=MagicMock(side_effect=InvalidOrderException()), - ) - freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short - caplog.clear() - rpc_mock.reset_mock() - freqtrade.create_stoploss_order(trade, 200) - assert trade.has_open_sl_orders is False - assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value - assert log_has("Unable to place a stoploss order on exchange. ", caplog) - assert log_has("Exiting the trade forcefully", caplog) - - # Should call a market sell - assert create_order_mock.call_count == 2 - assert create_order_mock.call_args[1]['ordertype'] == 'market' - assert create_order_mock.call_args[1]['pair'] == trade.pair - assert create_order_mock.call_args[1]['amount'] == trade.amount - - # Rpc is sending first buy, then sell - assert rpc_mock.call_count == 2 - assert rpc_mock.call_args_list[0][0][0]['exit_reason'] == ExitType.EMERGENCY_EXIT.value - assert rpc_mock.call_args_list[0][0][0]['order_type'] == 'market' - assert rpc_mock.call_args_list[0][0][0]['type'] == 'exit' - assert rpc_mock.call_args_list[1][0][0]['type'] == 'exit_fill' - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_create_stoploss_order_insufficient_funds( - mocker, default_conf_usdt, caplog, fee, limit_order, is_short -): - exit_order = limit_order[exit_side(is_short)]['id'] - freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - - mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 1.9, - 'ask': 2.2, - 'last': 1.9 - }), - create_order=MagicMock(side_effect=[ - limit_order[entry_side(is_short)], - exit_order, - ]), - get_fee=fee, - fetch_order=MagicMock(return_value={'status': 'canceled'}), - ) - mocker.patch.multiple( - EXMS, - create_stoploss=MagicMock(side_effect=InsufficientFundsError()), - ) - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short - caplog.clear() - freqtrade.create_stoploss_order(trade, 200) - # stoploss_orderid was empty before - assert trade.has_open_sl_orders is False - assert mock_insuf.call_count == 1 - mock_insuf.reset_mock() - - freqtrade.create_stoploss_order(trade, 200) - # No change to stoploss-orderid - assert trade.has_open_sl_orders is False - assert mock_insuf.call_count == 1 - - -@pytest.mark.parametrize("is_short,bid,ask,stop_price,hang_price", [ - (False, [4.38, 4.16], [4.4, 4.17], ['2.0805', 4.4 * 0.95], 3), - (True, [1.09, 1.21], [1.1, 1.22], ['2.321', 1.09 * 1.05], 1.5), -]) -@pytest.mark.usefixtures("init_persistence") -def test_handle_stoploss_on_exchange_trailing( - mocker, default_conf_usdt, fee, is_short, bid, ask, limit_order, stop_price, hang_price, - time_machine, -) -> None: - # When trailing stoploss is set - enter_order = limit_order[entry_side(is_short)] - exit_order = limit_order[exit_side(is_short)] - stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'}) - start_dt = dt_now() - time_machine.move_to(start_dt, tick=False) - patch_RPCManager(mocker) - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 2.19, - 'ask': 2.2, - 'last': 2.19, - }), - create_order=MagicMock(side_effect=[ - enter_order, - exit_order, - ]), - get_fee=fee, - ) - mocker.patch.multiple( - EXMS, - create_stoploss=stoploss, - stoploss_adjust=MagicMock(return_value=True), - ) - - # enabling TSL - default_conf_usdt['trailing_stop'] = True - - # disabling ROI - default_conf_usdt['minimal_roi']['0'] = 999999999 - - freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - - # enabling stoploss on exchange - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - - # setting stoploss - freqtrade.strategy.stoploss = 0.05 if is_short else -0.05 - - # setting stoploss_on_exchange_interval to 60 seconds - freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 - - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short - trade.is_open = True - assert trade.has_open_sl_orders is False - trade.stoploss_last_update = dt_now() - timedelta(minutes=20) - trade.orders.append( - Order( - ft_order_side='stoploss', - ft_pair=trade.pair, - ft_is_open=True, - ft_amount=trade.amount, - ft_price=trade.stop_loss, - order_id='100', - order_date=dt_now() - timedelta(minutes=20), - ) - ) - - stoploss_order_hanging = { - 'id': '100', - 'status': 'open', - 'type': 'stop_loss_limit', - 'price': hang_price, - 'average': 2, - 'fee': {}, - 'amount': 0, - 'info': { - 'stopPrice': stop_price[0] - } - } - stoploss_order_cancel = deepcopy(stoploss_order_hanging) - stoploss_order_cancel['status'] = 'canceled' - - mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value=stoploss_order_hanging) - mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=stoploss_order_cancel) - - # stoploss initially at 5% - assert freqtrade.handle_trade(trade) is False - assert freqtrade.handle_stoploss_on_exchange(trade) is False - - assert len(trade.open_sl_orders) == 1 - - assert trade.open_sl_orders[-1].order_id == '13434334' - - # price jumped 2x - mocker.patch( - f'{EXMS}.fetch_ticker', - MagicMock(return_value={ - 'bid': bid[0], - 'ask': ask[0], - 'last': bid[0], - }) - ) - - cancel_order_mock = MagicMock(return_value={ - 'id': '13434334', 'status': 'canceled', 'fee': {}, 'amount': trade.amount}) - stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'}) - mocker.patch(f'{EXMS}.fetch_stoploss_order') - mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) - mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) - - # stoploss should not be updated as the interval is 60 seconds - assert freqtrade.handle_trade(trade) is False - assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert len(trade.open_sl_orders) == 1 - cancel_order_mock.assert_not_called() - stoploss_order_mock.assert_not_called() - - # Move time by 10s ... so stoploss order should be replaced. - time_machine.move_to(start_dt + timedelta(minutes=10), tick=False) - - assert freqtrade.handle_trade(trade) is False - assert trade.stop_loss == stop_price[1] - - assert freqtrade.handle_stoploss_on_exchange(trade) is False - - cancel_order_mock.assert_called_once_with('13434334', 'ETH/USDT') - stoploss_order_mock.assert_called_once_with( - amount=30, - pair='ETH/USDT', - order_types=freqtrade.strategy.order_types, - stop_price=stop_price[1], - side=exit_side(is_short), - leverage=1.0 - ) - - # price fell below stoploss, so dry-run sells trade. - mocker.patch( - f'{EXMS}.fetch_ticker', - MagicMock(return_value={ - 'bid': bid[1], - 'ask': ask[1], - 'last': bid[1], - }) - ) - mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', - return_value={'id': 'so1', 'status': 'canceled'}) - assert len(trade.open_sl_orders) == 1 - assert trade.open_sl_orders[-1].order_id == 'so1' - - assert freqtrade.handle_trade(trade) is True - assert trade.is_open is False - assert trade.has_open_sl_orders is False - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_handle_stoploss_on_exchange_trailing_error( - mocker, default_conf_usdt, fee, caplog, limit_order, is_short, time_machine -) -> None: - time_machine.move_to(dt_now() - timedelta(minutes=601)) - enter_order = limit_order[entry_side(is_short)] - exit_order = limit_order[exit_side(is_short)] - # When trailing stoploss is set - stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'}) - patch_exchange(mocker) - - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 1.9, - 'ask': 2.2, - 'last': 1.9 - }), - create_order=MagicMock(side_effect=[ - {'id': enter_order['id']}, - {'id': exit_order['id']}, - ]), - get_fee=fee, - create_stoploss=stoploss, - stoploss_adjust=MagicMock(return_value=True), - ) - - # enabling TSL - default_conf_usdt['trailing_stop'] = True - - freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - # enabling stoploss on exchange - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - - # setting stoploss - freqtrade.strategy.stoploss = 0.05 if is_short else -0.05 - - # setting stoploss_on_exchange_interval to 60 seconds - freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short - trade.is_open = True - trade.stop_loss = 0.2 - - stoploss_order_hanging = { - 'id': "abcd", - 'status': 'open', - 'type': 'stop_loss_limit', - 'price': 3, - 'average': 2, - 'info': { - 'stopPrice': '0.1' - } - } - trade.orders.append( - Order( - ft_order_side='stoploss', - ft_pair=trade.pair, - ft_is_open=True, - ft_amount=trade.amount, - ft_price=3, - order_id='abcd', - order_date=dt_now(), - ) - ) - mocker.patch(f'{EXMS}.cancel_stoploss_order', - side_effect=InvalidOrderException()) - mocker.patch(f'{EXMS}.fetch_stoploss_order', - return_value=stoploss_order_hanging) - time_machine.shift(timedelta(minutes=50)) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) - assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog) - - # Still try to create order - assert stoploss.call_count == 1 - # TODO: Is this actually correct ? This will create a new order every time, - assert len(trade.open_sl_orders) == 2 - - # Fail creating stoploss order - caplog.clear() - cancel_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order') - mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError()) - time_machine.shift(timedelta(minutes=50)) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) - assert cancel_mock.call_count == 2 - assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog) - - -def test_stoploss_on_exchange_price_rounding( - mocker, default_conf_usdt, fee, open_trade_usdt) -> None: - patch_RPCManager(mocker) - mocker.patch.multiple( - EXMS, - get_fee=fee, - ) - price_mock = MagicMock(side_effect=lambda p, s, **kwargs: int(s)) - stoploss_mock = MagicMock(return_value={'id': '13434334'}) - adjust_mock = MagicMock(return_value=False) - mocker.patch.multiple( - EXMS, - create_stoploss=stoploss_mock, - stoploss_adjust=adjust_mock, - price_to_precision=price_mock, - ) - freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - open_trade_usdt.stop_loss = 222.55 - - freqtrade.handle_trailing_stoploss_on_exchange(open_trade_usdt, {}) - assert price_mock.call_count == 1 - assert adjust_mock.call_count == 1 - assert adjust_mock.call_args_list[0][0][0] == 222 - - -@pytest.mark.parametrize("is_short", [False, True]) -@pytest.mark.usefixtures("init_persistence") -def test_handle_stoploss_on_exchange_custom_stop( - mocker, default_conf_usdt, fee, is_short, limit_order -) -> None: - enter_order = limit_order[entry_side(is_short)] - exit_order = limit_order[exit_side(is_short)] - # When trailing stoploss is set - stoploss = MagicMock(return_value={'id': 13434334, 'status': 'open'}) - patch_RPCManager(mocker) - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 1.9, - 'ask': 2.2, - 'last': 1.9 - }), - create_order=MagicMock(side_effect=[ - enter_order, - exit_order, - ]), - get_fee=fee, - is_cancel_order_result_suitable=MagicMock(return_value=True), - ) - mocker.patch.multiple( - EXMS, - create_stoploss=stoploss, - stoploss_adjust=MagicMock(return_value=True), - ) - - # enabling TSL - default_conf_usdt['use_custom_stoploss'] = True - - # disabling ROI - default_conf_usdt['minimal_roi']['0'] = 999999999 - - freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - - # enabling stoploss on exchange - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - - # setting stoploss - freqtrade.strategy.custom_stoploss = lambda *args, **kwargs: -0.04 - - # setting stoploss_on_exchange_interval to 60 seconds - freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 - - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short - trade.is_open = True - trade.orders.append( - Order( - ft_order_side='stoploss', - ft_pair=trade.pair, - ft_is_open=True, - ft_amount=trade.amount, - ft_price=trade.stop_loss, - order_date=dt_now() - timedelta(minutes=601), - order_id='100', - ) - ) - Trade.commit() - slo = { - 'id': '100', - 'status': 'open', - 'type': 'stop_loss_limit', - 'price': 3, - 'average': 2, - 'info': { - 'stopPrice': '2.0805' - } - } - slo_canceled = deepcopy(slo) - slo_canceled.update({'status': 'canceled'}) - - def fetch_stoploss_order_mock(order_id, *args, **kwargs): - x = deepcopy(slo) - x['id'] = order_id - return x - - mocker.patch(f'{EXMS}.fetch_stoploss_order', MagicMock(fetch_stoploss_order_mock)) - mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=slo_canceled) - - assert freqtrade.handle_trade(trade) is False - assert freqtrade.handle_stoploss_on_exchange(trade) is False - - # price jumped 2x - mocker.patch( - f'{EXMS}.fetch_ticker', - MagicMock(return_value={ - 'bid': 4.38 if not is_short else 1.9 / 2, - 'ask': 4.4 if not is_short else 2.2 / 2, - 'last': 4.38 if not is_short else 1.9 / 2, - }) - ) - - cancel_order_mock = MagicMock() - stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'}) - mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) - mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) - - # stoploss should not be updated as the interval is 60 seconds - assert freqtrade.handle_trade(trade) is False - assert freqtrade.handle_stoploss_on_exchange(trade) is False - cancel_order_mock.assert_not_called() - stoploss_order_mock.assert_not_called() - - assert freqtrade.handle_trade(trade) is False - assert trade.stop_loss == 4.4 * 0.96 if not is_short else 1.1 - assert trade.stop_loss_pct == -0.04 if not is_short else 0.04 - - # setting stoploss_on_exchange_interval to 0 seconds - freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 - cancel_order_mock.assert_not_called() - stoploss_order_mock.assert_not_called() - - assert freqtrade.handle_stoploss_on_exchange(trade) is False - - cancel_order_mock.assert_called_once_with('13434334', 'ETH/USDT') - # Long uses modified ask - offset, short modified bid + offset - stoploss_order_mock.assert_called_once_with( - amount=pytest.approx(trade.amount), - pair='ETH/USDT', - order_types=freqtrade.strategy.order_types, - stop_price=4.4 * 0.96 if not is_short else 0.95 * 1.04, - side=exit_side(is_short), - leverage=1.0 - ) - - # price fell below stoploss, so dry-run sells trade. - mocker.patch( - f'{EXMS}.fetch_ticker', - MagicMock(return_value={ - 'bid': 4.17, - 'ask': 4.19, - 'last': 4.17 - }) - ) - assert freqtrade.handle_trade(trade) is True - - -def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_order) -> None: - - enter_order = limit_order['buy'] - exit_order = limit_order['sell'] - enter_order['average'] = 2.19 - # When trailing stoploss is set - stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'}) - patch_RPCManager(mocker) - patch_exchange(mocker) - patch_edge(mocker) - edge_conf['max_open_trades'] = float('inf') - edge_conf['dry_run_wallet'] = 999.9 - edge_conf['exchange']['name'] = 'binance' - mocker.patch.multiple( - EXMS, - fetch_ticker=MagicMock(return_value={ - 'bid': 2.19, - 'ask': 2.2, - 'last': 2.19 - }), - create_order=MagicMock(side_effect=[ - enter_order, - exit_order, - ]), - get_fee=fee, - create_stoploss=stoploss, - ) - - # enabling TSL - edge_conf['trailing_stop'] = True - edge_conf['trailing_stop_positive'] = 0.01 - edge_conf['trailing_stop_positive_offset'] = 0.011 - - # disabling ROI - edge_conf['minimal_roi']['0'] = 999999999 - - freqtrade = FreqtradeBot(edge_conf) - - # enabling stoploss on exchange - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - - # setting stoploss - freqtrade.strategy.stoploss = -0.02 - - # setting stoploss_on_exchange_interval to 0 seconds - freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 - - patch_get_signal(freqtrade) - - freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist) - - freqtrade.enter_positions() - trade = Trade.session.scalars(select(Trade)).first() - trade.is_open = True - - trade.stoploss_last_update = dt_now() - trade.orders.append( - Order( - ft_order_side='stoploss', - ft_pair=trade.pair, - ft_is_open=True, - ft_amount=trade.amount, - ft_price=trade.stop_loss, - order_id='100', - ) - ) - - stoploss_order_hanging = MagicMock(return_value={ - 'id': '100', - 'status': 'open', - 'type': 'stop_loss_limit', - 'price': 3, - 'average': 2, - 'stopPrice': '2.178' - }) - - mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging) - - # stoploss initially at 20% as edge dictated it. - assert freqtrade.handle_trade(trade) is False - assert freqtrade.handle_stoploss_on_exchange(trade) is False - assert pytest.approx(trade.stop_loss) == 1.76 - - cancel_order_mock = MagicMock() - stoploss_order_mock = MagicMock() - mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) - mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) - - # price goes down 5% - mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={ - 'bid': 2.19 * 0.95, - 'ask': 2.2 * 0.95, - 'last': 2.19 * 0.95 - })) - assert freqtrade.handle_trade(trade) is False - assert freqtrade.handle_stoploss_on_exchange(trade) is False - - # stoploss should remain the same - assert pytest.approx(trade.stop_loss) == 1.76 - - # stoploss on exchange should not be canceled - cancel_order_mock.assert_not_called() - - # price jumped 2x - mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={ - 'bid': 4.38, - 'ask': 4.4, - 'last': 4.38 - })) - - assert freqtrade.handle_trade(trade) is False - assert freqtrade.handle_stoploss_on_exchange(trade) is False - - # stoploss should be set to 1% as trailing is on - assert trade.stop_loss == 4.4 * 0.99 - cancel_order_mock.assert_called_once_with('100', 'NEO/BTC') - stoploss_order_mock.assert_called_once_with( - amount=30, - pair='NEO/BTC', - order_types=freqtrade.strategy.order_types, - stop_price=4.4 * 0.99, - side='sell', - leverage=1.0 - ) - - @pytest.mark.parametrize('return_value,side_effect,log_message', [ (False, None, 'Found no enter signals for whitelisted currencies. Trying again...'), (None, DependencyException, 'Unable to create trade for ETH/USDT: ') @@ -3988,257 +2923,7 @@ def test_execute_trade_exit_custom_exit_price( } == last_msg -@pytest.mark.parametrize("is_short", [False, True]) -def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( - default_conf_usdt, ticker_usdt, fee, is_short, ticker_usdt_sell_down, - ticker_usdt_sell_up, mocker) -> None: - rpc_mock = patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - EXMS, - fetch_ticker=ticker_usdt, - get_fee=fee, - _dry_is_price_crossed=MagicMock(return_value=False), - ) - patch_whitelist(mocker, default_conf_usdt) - freqtrade = FreqtradeBot(default_conf_usdt) - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - # Create some test data - freqtrade.enter_positions() - - trade = Trade.session.scalars(select(Trade)).first() - assert trade.is_short == is_short - assert trade - - # Decrease the price and sell it - mocker.patch.multiple( - EXMS, - fetch_ticker=ticker_usdt_sell_up if is_short else ticker_usdt_sell_down - ) - - default_conf_usdt['dry_run'] = True - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - # Setting trade stoploss to 0.01 - - trade.stop_loss = 2.0 * 1.01 if is_short else 2.0 * 0.99 - freqtrade.execute_trade_exit( - trade=trade, limit=trade.stop_loss, - exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)) - - assert rpc_mock.call_count == 2 - last_msg = rpc_mock.call_args_list[-1][0][0] - - assert { - 'type': RPCMessageType.EXIT, - 'trade_id': 1, - 'exchange': 'Binance', - 'pair': 'ETH/USDT', - 'direction': 'Short' if trade.is_short else 'Long', - 'leverage': 1.0, - 'gain': 'loss', - 'limit': 2.02 if is_short else 1.98, - 'order_rate': 2.02 if is_short else 1.98, - 'amount': pytest.approx(29.70297029 if is_short else 30.0), - 'order_type': 'limit', - 'buy_tag': None, - 'enter_tag': None, - 'open_rate': 2.02 if is_short else 2.0, - 'current_rate': 2.2 if is_short else 2.0, - 'profit_amount': -0.3 if is_short else -0.8985, - 'profit_ratio': -0.00501253 if is_short else -0.01493766, - 'stake_currency': 'USDT', - 'quote_currency': 'USDT', - 'fiat_currency': 'USD', - 'base_currency': 'ETH', - 'exit_reason': ExitType.STOP_LOSS.value, - 'open_date': ANY, - 'close_date': ANY, - 'close_rate': ANY, - 'sub_trade': False, - 'cumulative_profit': 0.0, - 'stake_amount': pytest.approx(60), - 'is_final_exit': False, - 'final_profit_ratio': None, - } == last_msg - - -def test_execute_trade_exit_sloe_cancel_exception( - mocker, default_conf_usdt, ticker_usdt, fee, caplog) -> None: - freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) - mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300)) - create_order_mock = MagicMock(side_effect=[ - {'id': '12345554'}, - {'id': '12345555'}, - ]) - patch_exchange(mocker) - mocker.patch.multiple( - EXMS, - fetch_ticker=ticker_usdt, - get_fee=fee, - create_order=create_order_mock, - ) - - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - patch_get_signal(freqtrade) - freqtrade.enter_positions() - - trade = Trade.session.scalars(select(Trade)).first() - PairLock.session = MagicMock() - - freqtrade.config['dry_run'] = False - trade.orders.append( - Order( - ft_order_side='stoploss', - ft_pair=trade.pair, - ft_is_open=True, - ft_amount=trade.amount, - ft_price=trade.stop_loss, - order_id='abcd', - status='open', - ) - ) - - freqtrade.execute_trade_exit(trade=trade, limit=1234, - exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)) - assert create_order_mock.call_count == 2 - assert log_has('Could not cancel stoploss order abcd for pair ETH/USDT', caplog) - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_execute_trade_exit_with_stoploss_on_exchange( - default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, is_short, mocker) -> None: - - default_conf_usdt['exchange']['name'] = 'binance' - rpc_mock = patch_RPCManager(mocker) - patch_exchange(mocker) - stoploss = MagicMock(return_value={ - 'id': 123, - 'status': 'open', - 'info': { - 'foo': 'bar' - } - }) - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee') - - cancel_order = MagicMock(return_value=True) - mocker.patch.multiple( - EXMS, - fetch_ticker=ticker_usdt, - get_fee=fee, - amount_to_precision=lambda s, x, y: y, - price_to_precision=lambda s, x, y: y, - create_stoploss=stoploss, - cancel_stoploss_order=cancel_order, - _dry_is_price_crossed=MagicMock(side_effect=[True, False]), - ) - - freqtrade = FreqtradeBot(default_conf_usdt) - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) - - # Create some test data - freqtrade.enter_positions() - - trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short - assert trade - trades = [trade] - - freqtrade.manage_open_orders() - freqtrade.exit_positions(trades) - - # Increase the price and sell it - mocker.patch.multiple( - EXMS, - fetch_ticker=ticker_usdt_sell_up - ) - - freqtrade.execute_trade_exit( - trade=trade, - limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], - exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS) - ) - - trade = Trade.session.scalars(select(Trade)).first() - trade.is_short = is_short - assert trade - assert cancel_order.call_count == 1 - assert rpc_mock.call_count == 4 - - -@pytest.mark.parametrize("is_short", [False, True]) -def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( - default_conf_usdt, ticker_usdt, fee, mocker, is_short) -> None: - default_conf_usdt['exchange']['name'] = 'binance' - rpc_mock = patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - EXMS, - fetch_ticker=ticker_usdt, - get_fee=fee, - amount_to_precision=lambda s, x, y: y, - price_to_precision=lambda s, x, y: y, - _dry_is_price_crossed=MagicMock(side_effect=[False, True]), - ) - - stoploss = MagicMock(return_value={ - 'id': 123, - 'info': { - 'foo': 'bar' - } - }) - - mocker.patch(f'{EXMS}.create_stoploss', stoploss) - - freqtrade = FreqtradeBot(default_conf_usdt) - freqtrade.strategy.order_types['stoploss_on_exchange'] = True - patch_get_signal(freqtrade, enter_long=not is_short, enter_short=is_short) - - # Create some test data - freqtrade.enter_positions() - freqtrade.manage_open_orders() - trade = Trade.session.scalars(select(Trade)).first() - trades = [trade] - assert trade.has_open_sl_orders is False - - freqtrade.exit_positions(trades) - assert trade - assert trade.has_open_sl_orders is True - assert not trade.has_open_orders - - # Assuming stoploss on exchange is hit - # trade should be sold at the price of stoploss, with exit_reason STOPLOSS_ON_EXCHANGE - stoploss_executed = MagicMock(return_value={ - "id": "123", - "timestamp": 1542707426845, - "datetime": "2018-11-20T09:50:26.845Z", - "lastTradeTimestamp": None, - "symbol": "BTC/USDT", - "type": "stop_loss_limit", - "side": "buy" if is_short else "sell", - "price": 1.08801, - "amount": trade.amount, - "cost": 1.08801 * trade.amount, - "average": 1.08801, - "filled": trade.amount, - "remaining": 0.0, - "status": "closed", - "fee": None, - "trades": None - }) - mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_executed) - - freqtrade.exit_positions(trades) - assert trade.has_open_sl_orders is False - assert trade.is_open is False - assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value - assert rpc_mock.call_count == 4 - assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.ENTRY - assert rpc_mock.call_args_list[1][0][0]['amount'] > 20 - assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.ENTRY_FILL - assert rpc_mock.call_args_list[3][0][0]['type'] == RPCMessageType.EXIT_FILL @pytest.mark.parametrize( diff --git a/tests/freqtradebot/test_stoploss_on_exchange.py b/tests/freqtradebot/test_stoploss_on_exchange.py new file mode 100644 index 000000000..325fe549f --- /dev/null +++ b/tests/freqtradebot/test_stoploss_on_exchange.py @@ -0,0 +1,1334 @@ +from copy import deepcopy +from datetime import timedelta +from unittest.mock import ANY, MagicMock + +import pytest +from sqlalchemy import select + +from freqtrade.enums import ExitCheckTuple, ExitType, RPCMessageType +from freqtrade.exceptions import ExchangeError, InsufficientFundsError, InvalidOrderException +from freqtrade.freqtradebot import FreqtradeBot +from freqtrade.persistence import Order, Trade +from freqtrade.persistence.models import PairLock +from freqtrade.util.datetime_helpers import dt_now +from tests.conftest import (EXMS, get_patched_freqtradebot, log_has, log_has_re, patch_edge, + patch_exchange, patch_get_signal, patch_whitelist) +from tests.conftest_trades import entry_side, exit_side +from tests.freqtradebot.test_freqtradebot import patch_RPCManager + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short, fee) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(return_value=limit_order[entry_side(is_short)]), + get_fee=fee, + ) + order = limit_order[entry_side(is_short)] + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) + mocker.patch(f'{EXMS}.fetch_order', return_value=order) + mocker.patch(f'{EXMS}.get_trades_for_order', return_value=[]) + + stoploss = MagicMock(return_value={'id': 13434334}) + mocker.patch(f'{EXMS}.create_stoploss', stoploss) + + freqtrade = FreqtradeBot(default_conf_usdt) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + trade.is_open = True + trades = [trade] + + freqtrade.exit_positions(trades) + assert trade.has_open_sl_orders is True + assert stoploss.call_count == 1 + assert trade.is_open is True + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short, + limit_order) -> None: + stop_order_dict = {'id': "13434334"} + stoploss = MagicMock(return_value=stop_order_dict) + enter_order = limit_order[entry_side(is_short)] + exit_order = limit_order[exit_side(is_short)] + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(side_effect=[ + enter_order, + exit_order, + ]), + get_fee=fee, + create_stoploss=stoploss + ) + freqtrade = FreqtradeBot(default_conf_usdt) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + # First case: when stoploss is not yet set but the order is open + # should get the stoploss order id immediately + # and should return false as no trade actually happened + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + assert trade.is_short == is_short + assert trade.is_open + assert trade.has_open_sl_orders is False + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert stoploss.call_count == 1 + assert trade.open_sl_orders[-1].order_id == "13434334" + + # Second case: when stoploss is set but it is not yet hit + # should do nothing and return false + trade.is_open = True + + hanging_stoploss_order = MagicMock(return_value={'id': '13434334', 'status': 'open'}) + mocker.patch(f'{EXMS}.fetch_stoploss_order', hanging_stoploss_order) + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + hanging_stoploss_order.assert_called_once_with('13434334', trade.pair) + assert len(trade.open_sl_orders) == 1 + assert trade.open_sl_orders[-1].order_id == "13434334" + + # Third case: when stoploss was set but it was canceled for some reason + # should set a stoploss immediately and return False + caplog.clear() + trade.is_open = True + + canceled_stoploss_order = MagicMock(return_value={'id': '13434334', 'status': 'canceled'}) + mocker.patch(f'{EXMS}.fetch_stoploss_order', canceled_stoploss_order) + stoploss.reset_mock() + amount_before = trade.amount + + stop_order_dict.update({'id': "103_1"}) + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert stoploss.call_count == 1 + assert len(trade.open_sl_orders) == 1 + assert trade.open_sl_orders[-1].order_id == "103_1" + assert trade.amount == amount_before + + # Fourth case: when stoploss is set and it is hit + # should return true as a trade actually happened + caplog.clear() + stop_order_dict.update({'id': "103_1"}) + + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + trade.is_open = True + + stoploss_order_hit = MagicMock(return_value={ + 'id': "103_1", + 'status': 'closed', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'filled': enter_order['amount'], + 'remaining': 0, + 'amount': enter_order['amount'], + }) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) + assert freqtrade.handle_stoploss_on_exchange(trade) is True + assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) + assert len(trade.open_sl_orders) == 0 + assert trade.is_open is False + caplog.clear() + + mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError()) + trade.is_open = True + freqtrade.handle_stoploss_on_exchange(trade) + assert log_has('Unable to place a stoploss order on exchange.', caplog) + assert len(trade.open_sl_orders) == 0 + + # Fifth case: fetch_order returns InvalidOrder + # It should try to add stoploss order + stop_order_dict.update({'id': "105"}) + stoploss.reset_mock() + mocker.patch(f'{EXMS}.fetch_stoploss_order', side_effect=InvalidOrderException()) + mocker.patch(f'{EXMS}.create_stoploss', stoploss) + freqtrade.handle_stoploss_on_exchange(trade) + assert len(trade.open_sl_orders) == 1 + assert stoploss.call_count == 1 + + # Sixth case: Closed Trade + # Should not create new order + trade.is_open = False + trade.open_sl_orders[-1].ft_is_open = False + stoploss.reset_mock() + mocker.patch(f'{EXMS}.fetch_order') + mocker.patch(f'{EXMS}.create_stoploss', stoploss) + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert trade.has_open_sl_orders is False + assert stoploss.call_count == 0 + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_stoploss_on_exchange_emergency(mocker, default_conf_usdt, fee, is_short, + limit_order) -> None: + stop_order_dict = {'id': "13434334"} + stoploss = MagicMock(return_value=stop_order_dict) + enter_order = limit_order[entry_side(is_short)] + exit_order = limit_order[exit_side(is_short)] + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(side_effect=[ + enter_order, + exit_order, + ]), + get_fee=fee, + create_stoploss=stoploss + ) + freqtrade = FreqtradeBot(default_conf_usdt) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + assert trade.is_short == is_short + assert trade.is_open + assert trade.has_open_sl_orders is False + + # emergency exit triggered + # Trailing stop should not act anymore + stoploss_order_cancelled = MagicMock(side_effect=[{ + 'id': "107", + 'status': 'canceled', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'amount': enter_order['amount'], + 'filled': 0, + 'remaining': enter_order['amount'], + 'info': {'stopPrice': 22}, + }]) + trade.stoploss_last_update = dt_now() - timedelta(hours=1) + trade.stop_loss = 24 + trade.exit_reason = None + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='107', + status='open', + ) + ) + freqtrade.config['trailing_stop'] = True + stoploss = MagicMock(side_effect=InvalidOrderException()) + assert trade.has_open_sl_orders is True + Trade.commit() + mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', + side_effect=InvalidOrderException()) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_cancelled) + mocker.patch(f'{EXMS}.create_stoploss', stoploss) + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert trade.has_open_sl_orders is False + assert trade.is_open is False + assert trade.exit_reason == str(ExitType.EMERGENCY_EXIT) + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_stoploss_on_exchange_partial( + mocker, default_conf_usdt, fee, is_short, limit_order) -> None: + stop_order_dict = {'id': "101", "status": "open"} + stoploss = MagicMock(return_value=stop_order_dict) + enter_order = limit_order[entry_side(is_short)] + exit_order = limit_order[exit_side(is_short)] + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(side_effect=[ + enter_order, + exit_order, + ]), + get_fee=fee, + create_stoploss=stoploss + ) + freqtrade = FreqtradeBot(default_conf_usdt) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + trade.is_open = True + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert stoploss.call_count == 1 + assert trade.has_open_sl_orders is True + assert trade.open_sl_orders[-1].order_id == "101" + assert trade.amount == 30 + stop_order_dict.update({'id': "102"}) + # Stoploss on exchange is cancelled on exchange, but filled partially. + # Must update trade amount to guarantee successful exit. + stoploss_order_hit = MagicMock(return_value={ + 'id': "101", + 'status': 'canceled', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'filled': trade.amount / 2, + 'remaining': trade.amount / 2, + 'amount': enter_order['amount'], + }) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) + assert freqtrade.handle_stoploss_on_exchange(trade) is False + # Stoploss filled partially ... + assert trade.amount == 15 + + assert trade.open_sl_orders[-1].order_id == "102" + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_stoploss_on_exchange_partial_cancel_here( + mocker, default_conf_usdt, fee, is_short, limit_order, caplog, time_machine) -> None: + stop_order_dict = {'id': "101", "status": "open"} + time_machine.move_to(dt_now()) + default_conf_usdt['trailing_stop'] = True + stoploss = MagicMock(return_value=stop_order_dict) + enter_order = limit_order[entry_side(is_short)] + exit_order = limit_order[exit_side(is_short)] + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(side_effect=[ + enter_order, + exit_order, + ]), + get_fee=fee, + create_stoploss=stoploss + ) + freqtrade = FreqtradeBot(default_conf_usdt) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + trade.is_open = True + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert stoploss.call_count == 1 + assert trade.has_open_sl_orders is True + assert trade.open_sl_orders[-1].order_id == "101" + assert trade.amount == 30 + stop_order_dict.update({'id': "102"}) + # Stoploss on exchange is open. + # Freqtrade cancels the stop - but cancel returns a partial filled order. + stoploss_order_hit = MagicMock(return_value={ + 'id': "101", + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'filled': 0, + 'remaining': trade.amount, + 'amount': enter_order['amount'], + }) + stoploss_order_cancel = MagicMock(return_value={ + 'id': "101", + 'status': 'canceled', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'filled': trade.amount / 2, + 'remaining': trade.amount / 2, + 'amount': enter_order['amount'], + }) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hit) + mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', stoploss_order_cancel) + time_machine.shift(timedelta(minutes=15)) + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + # Canceled Stoploss filled partially ... + assert log_has_re('Cancelling current stoploss on exchange.*', caplog) + + assert trade.has_open_sl_orders is True + assert trade.open_sl_orders[-1].order_id == "102" + assert trade.amount == 15 + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short, + limit_order) -> None: + # Sixth case: stoploss order was cancelled but couldn't create new one + enter_order = limit_order[entry_side(is_short)] + exit_order = limit_order[exit_side(is_short)] + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(side_effect=[ + enter_order, + exit_order, + ]), + get_fee=fee, + ) + mocker.patch.multiple( + EXMS, + fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': '100'}), + create_stoploss=MagicMock(side_effect=ExchangeError()), + ) + freqtrade = FreqtradeBot(default_conf_usdt) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + assert trade.is_short == is_short + trade.is_open = True + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='100', + status='open', + ) + ) + assert trade + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert log_has_re(r'All Stoploss orders are cancelled, but unable to recreate one\.', caplog) + assert trade.has_open_sl_orders is False + assert trade.is_open is True + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_create_stoploss_order_invalid_order( + mocker, default_conf_usdt, caplog, fee, is_short, limit_order +): + open_order = limit_order[entry_side(is_short)] + order = limit_order[exit_side(is_short)] + rpc_mock = patch_RPCManager(mocker) + patch_exchange(mocker) + create_order_mock = MagicMock(side_effect=[ + open_order, + order, + ]) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=create_order_mock, + get_fee=fee, + ) + mocker.patch.multiple( + EXMS, + fetch_order=MagicMock(return_value={'status': 'canceled'}), + create_stoploss=MagicMock(side_effect=InvalidOrderException()), + ) + freqtrade = FreqtradeBot(default_conf_usdt) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + caplog.clear() + rpc_mock.reset_mock() + freqtrade.create_stoploss_order(trade, 200) + assert trade.has_open_sl_orders is False + assert trade.exit_reason == ExitType.EMERGENCY_EXIT.value + assert log_has("Unable to place a stoploss order on exchange. ", caplog) + assert log_has("Exiting the trade forcefully", caplog) + + # Should call a market sell + assert create_order_mock.call_count == 2 + assert create_order_mock.call_args[1]['ordertype'] == 'market' + assert create_order_mock.call_args[1]['pair'] == trade.pair + assert create_order_mock.call_args[1]['amount'] == trade.amount + + # Rpc is sending first buy, then sell + assert rpc_mock.call_count == 2 + assert rpc_mock.call_args_list[0][0][0]['exit_reason'] == ExitType.EMERGENCY_EXIT.value + assert rpc_mock.call_args_list[0][0][0]['order_type'] == 'market' + assert rpc_mock.call_args_list[0][0][0]['type'] == 'exit' + assert rpc_mock.call_args_list[1][0][0]['type'] == 'exit_fill' + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_create_stoploss_order_insufficient_funds( + mocker, default_conf_usdt, caplog, fee, limit_order, is_short +): + exit_order = limit_order[exit_side(is_short)]['id'] + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + + mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(side_effect=[ + limit_order[entry_side(is_short)], + exit_order, + ]), + get_fee=fee, + fetch_order=MagicMock(return_value={'status': 'canceled'}), + ) + mocker.patch.multiple( + EXMS, + create_stoploss=MagicMock(side_effect=InsufficientFundsError()), + ) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + caplog.clear() + freqtrade.create_stoploss_order(trade, 200) + # stoploss_orderid was empty before + assert trade.has_open_sl_orders is False + assert mock_insuf.call_count == 1 + mock_insuf.reset_mock() + + freqtrade.create_stoploss_order(trade, 200) + # No change to stoploss-orderid + assert trade.has_open_sl_orders is False + assert mock_insuf.call_count == 1 + + +@pytest.mark.parametrize("is_short,bid,ask,stop_price,hang_price", [ + (False, [4.38, 4.16], [4.4, 4.17], ['2.0805', 4.4 * 0.95], 3), + (True, [1.09, 1.21], [1.1, 1.22], ['2.321', 1.09 * 1.05], 1.5), +]) +@pytest.mark.usefixtures("init_persistence") +def test_handle_stoploss_on_exchange_trailing( + mocker, default_conf_usdt, fee, is_short, bid, ask, limit_order, stop_price, hang_price, + time_machine, +) -> None: + # When trailing stoploss is set + enter_order = limit_order[entry_side(is_short)] + exit_order = limit_order[exit_side(is_short)] + stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'}) + start_dt = dt_now() + time_machine.move_to(start_dt, tick=False) + patch_RPCManager(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 2.19, + 'ask': 2.2, + 'last': 2.19, + }), + create_order=MagicMock(side_effect=[ + enter_order, + exit_order, + ]), + get_fee=fee, + ) + mocker.patch.multiple( + EXMS, + create_stoploss=stoploss, + stoploss_adjust=MagicMock(return_value=True), + ) + + # enabling TSL + default_conf_usdt['trailing_stop'] = True + + # disabling ROI + default_conf_usdt['minimal_roi']['0'] = 999999999 + + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + + # enabling stoploss on exchange + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + # setting stoploss + freqtrade.strategy.stoploss = 0.05 if is_short else -0.05 + + # setting stoploss_on_exchange_interval to 60 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 + + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + trade.is_open = True + assert trade.has_open_sl_orders is False + trade.stoploss_last_update = dt_now() - timedelta(minutes=20) + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='100', + order_date=dt_now() - timedelta(minutes=20), + ) + ) + + stoploss_order_hanging = { + 'id': '100', + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': hang_price, + 'average': 2, + 'fee': {}, + 'amount': 0, + 'info': { + 'stopPrice': stop_price[0] + } + } + stoploss_order_cancel = deepcopy(stoploss_order_hanging) + stoploss_order_cancel['status'] = 'canceled' + + mocker.patch(f'{EXMS}.fetch_stoploss_order', return_value=stoploss_order_hanging) + mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=stoploss_order_cancel) + + # stoploss initially at 5% + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + assert len(trade.open_sl_orders) == 1 + + assert trade.open_sl_orders[-1].order_id == '13434334' + + # price jumped 2x + mocker.patch( + f'{EXMS}.fetch_ticker', + MagicMock(return_value={ + 'bid': bid[0], + 'ask': ask[0], + 'last': bid[0], + }) + ) + + cancel_order_mock = MagicMock(return_value={ + 'id': '13434334', 'status': 'canceled', 'fee': {}, 'amount': trade.amount}) + stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'}) + mocker.patch(f'{EXMS}.fetch_stoploss_order') + mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) + mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) + + # stoploss should not be updated as the interval is 60 seconds + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert len(trade.open_sl_orders) == 1 + cancel_order_mock.assert_not_called() + stoploss_order_mock.assert_not_called() + + # Move time by 10s ... so stoploss order should be replaced. + time_machine.move_to(start_dt + timedelta(minutes=10), tick=False) + + assert freqtrade.handle_trade(trade) is False + assert trade.stop_loss == stop_price[1] + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + cancel_order_mock.assert_called_once_with('13434334', 'ETH/USDT') + stoploss_order_mock.assert_called_once_with( + amount=30, + pair='ETH/USDT', + order_types=freqtrade.strategy.order_types, + stop_price=stop_price[1], + side=exit_side(is_short), + leverage=1.0 + ) + + # price fell below stoploss, so dry-run sells trade. + mocker.patch( + f'{EXMS}.fetch_ticker', + MagicMock(return_value={ + 'bid': bid[1], + 'ask': ask[1], + 'last': bid[1], + }) + ) + mocker.patch(f'{EXMS}.cancel_stoploss_order_with_result', + return_value={'id': 'so1', 'status': 'canceled'}) + assert len(trade.open_sl_orders) == 1 + assert trade.open_sl_orders[-1].order_id == 'so1' + + assert freqtrade.handle_trade(trade) is True + assert trade.is_open is False + assert trade.has_open_sl_orders is False + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_stoploss_on_exchange_trailing_error( + mocker, default_conf_usdt, fee, caplog, limit_order, is_short, time_machine +) -> None: + time_machine.move_to(dt_now() - timedelta(minutes=601)) + enter_order = limit_order[entry_side(is_short)] + exit_order = limit_order[exit_side(is_short)] + # When trailing stoploss is set + stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'}) + patch_exchange(mocker) + + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(side_effect=[ + {'id': enter_order['id']}, + {'id': exit_order['id']}, + ]), + get_fee=fee, + create_stoploss=stoploss, + stoploss_adjust=MagicMock(return_value=True), + ) + + # enabling TSL + default_conf_usdt['trailing_stop'] = True + + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + # enabling stoploss on exchange + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + # setting stoploss + freqtrade.strategy.stoploss = 0.05 if is_short else -0.05 + + # setting stoploss_on_exchange_interval to 60 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + trade.is_open = True + trade.stop_loss = 0.2 + + stoploss_order_hanging = { + 'id': "abcd", + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'info': { + 'stopPrice': '0.1' + } + } + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=3, + order_id='abcd', + order_date=dt_now(), + ) + ) + mocker.patch(f'{EXMS}.cancel_stoploss_order', + side_effect=InvalidOrderException()) + mocker.patch(f'{EXMS}.fetch_stoploss_order', + return_value=stoploss_order_hanging) + time_machine.shift(timedelta(minutes=50)) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/USDT.*", caplog) + + # Still try to create order + assert stoploss.call_count == 1 + # TODO: Is this actually correct ? This will create a new order every time, + assert len(trade.open_sl_orders) == 2 + + # Fail creating stoploss order + caplog.clear() + cancel_mock = mocker.patch(f'{EXMS}.cancel_stoploss_order') + mocker.patch(f'{EXMS}.create_stoploss', side_effect=ExchangeError()) + time_machine.shift(timedelta(minutes=50)) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + assert cancel_mock.call_count == 2 + assert log_has_re(r"Could not create trailing stoploss order for pair ETH/USDT\..*", caplog) + + +def test_stoploss_on_exchange_price_rounding( + mocker, default_conf_usdt, fee, open_trade_usdt) -> None: + patch_RPCManager(mocker) + mocker.patch.multiple( + EXMS, + get_fee=fee, + ) + price_mock = MagicMock(side_effect=lambda p, s, **kwargs: int(s)) + stoploss_mock = MagicMock(return_value={'id': '13434334'}) + adjust_mock = MagicMock(return_value=False) + mocker.patch.multiple( + EXMS, + create_stoploss=stoploss_mock, + stoploss_adjust=adjust_mock, + price_to_precision=price_mock, + ) + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + open_trade_usdt.stop_loss = 222.55 + + freqtrade.handle_trailing_stoploss_on_exchange(open_trade_usdt, {}) + assert price_mock.call_count == 1 + assert adjust_mock.call_count == 1 + assert adjust_mock.call_args_list[0][0][0] == 222 + + +@pytest.mark.parametrize("is_short", [False, True]) +@pytest.mark.usefixtures("init_persistence") +def test_handle_stoploss_on_exchange_custom_stop( + mocker, default_conf_usdt, fee, is_short, limit_order +) -> None: + enter_order = limit_order[entry_side(is_short)] + exit_order = limit_order[exit_side(is_short)] + # When trailing stoploss is set + stoploss = MagicMock(return_value={'id': 13434334, 'status': 'open'}) + patch_RPCManager(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 1.9, + 'ask': 2.2, + 'last': 1.9 + }), + create_order=MagicMock(side_effect=[ + enter_order, + exit_order, + ]), + get_fee=fee, + is_cancel_order_result_suitable=MagicMock(return_value=True), + ) + mocker.patch.multiple( + EXMS, + create_stoploss=stoploss, + stoploss_adjust=MagicMock(return_value=True), + ) + + # enabling TSL + default_conf_usdt['use_custom_stoploss'] = True + + # disabling ROI + default_conf_usdt['minimal_roi']['0'] = 999999999 + + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + + # enabling stoploss on exchange + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + # setting stoploss + freqtrade.strategy.custom_stoploss = lambda *args, **kwargs: -0.04 + + # setting stoploss_on_exchange_interval to 60 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 + + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + trade.is_open = True + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_date=dt_now() - timedelta(minutes=601), + order_id='100', + ) + ) + Trade.commit() + slo = { + 'id': '100', + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'info': { + 'stopPrice': '2.0805' + } + } + slo_canceled = deepcopy(slo) + slo_canceled.update({'status': 'canceled'}) + + def fetch_stoploss_order_mock(order_id, *args, **kwargs): + x = deepcopy(slo) + x['id'] = order_id + return x + + mocker.patch(f'{EXMS}.fetch_stoploss_order', MagicMock(fetch_stoploss_order_mock)) + mocker.patch(f'{EXMS}.cancel_stoploss_order', return_value=slo_canceled) + + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + # price jumped 2x + mocker.patch( + f'{EXMS}.fetch_ticker', + MagicMock(return_value={ + 'bid': 4.38 if not is_short else 1.9 / 2, + 'ask': 4.4 if not is_short else 2.2 / 2, + 'last': 4.38 if not is_short else 1.9 / 2, + }) + ) + + cancel_order_mock = MagicMock() + stoploss_order_mock = MagicMock(return_value={'id': 'so1', 'status': 'open'}) + mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) + mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) + + # stoploss should not be updated as the interval is 60 seconds + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + cancel_order_mock.assert_not_called() + stoploss_order_mock.assert_not_called() + + assert freqtrade.handle_trade(trade) is False + assert trade.stop_loss == 4.4 * 0.96 if not is_short else 1.1 + assert trade.stop_loss_pct == -0.04 if not is_short else 0.04 + + # setting stoploss_on_exchange_interval to 0 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 + cancel_order_mock.assert_not_called() + stoploss_order_mock.assert_not_called() + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + cancel_order_mock.assert_called_once_with('13434334', 'ETH/USDT') + # Long uses modified ask - offset, short modified bid + offset + stoploss_order_mock.assert_called_once_with( + amount=pytest.approx(trade.amount), + pair='ETH/USDT', + order_types=freqtrade.strategy.order_types, + stop_price=4.4 * 0.96 if not is_short else 0.95 * 1.04, + side=exit_side(is_short), + leverage=1.0 + ) + + # price fell below stoploss, so dry-run sells trade. + mocker.patch( + f'{EXMS}.fetch_ticker', + MagicMock(return_value={ + 'bid': 4.17, + 'ask': 4.19, + 'last': 4.17 + }) + ) + assert freqtrade.handle_trade(trade) is True + + +def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, limit_order) -> None: + + enter_order = limit_order['buy'] + exit_order = limit_order['sell'] + enter_order['average'] = 2.19 + # When trailing stoploss is set + stoploss = MagicMock(return_value={'id': '13434334', 'status': 'open'}) + patch_RPCManager(mocker) + patch_exchange(mocker) + patch_edge(mocker) + edge_conf['max_open_trades'] = float('inf') + edge_conf['dry_run_wallet'] = 999.9 + edge_conf['exchange']['name'] = 'binance' + mocker.patch.multiple( + EXMS, + fetch_ticker=MagicMock(return_value={ + 'bid': 2.19, + 'ask': 2.2, + 'last': 2.19 + }), + create_order=MagicMock(side_effect=[ + enter_order, + exit_order, + ]), + get_fee=fee, + create_stoploss=stoploss, + ) + + # enabling TSL + edge_conf['trailing_stop'] = True + edge_conf['trailing_stop_positive'] = 0.01 + edge_conf['trailing_stop_positive_offset'] = 0.011 + + # disabling ROI + edge_conf['minimal_roi']['0'] = 999999999 + + freqtrade = FreqtradeBot(edge_conf) + + # enabling stoploss on exchange + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + # setting stoploss + freqtrade.strategy.stoploss = -0.02 + + # setting stoploss_on_exchange_interval to 0 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 + + patch_get_signal(freqtrade) + + freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist) + + freqtrade.enter_positions() + trade = Trade.session.scalars(select(Trade)).first() + trade.is_open = True + + trade.stoploss_last_update = dt_now() + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='100', + ) + ) + + stoploss_order_hanging = MagicMock(return_value={ + 'id': '100', + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'stopPrice': '2.178' + }) + + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_order_hanging) + + # stoploss initially at 20% as edge dictated it. + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert pytest.approx(trade.stop_loss) == 1.76 + + cancel_order_mock = MagicMock() + stoploss_order_mock = MagicMock() + mocker.patch(f'{EXMS}.cancel_stoploss_order', cancel_order_mock) + mocker.patch(f'{EXMS}.create_stoploss', stoploss_order_mock) + + # price goes down 5% + mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={ + 'bid': 2.19 * 0.95, + 'ask': 2.2 * 0.95, + 'last': 2.19 * 0.95 + })) + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + # stoploss should remain the same + assert pytest.approx(trade.stop_loss) == 1.76 + + # stoploss on exchange should not be canceled + cancel_order_mock.assert_not_called() + + # price jumped 2x + mocker.patch(f'{EXMS}.fetch_ticker', MagicMock(return_value={ + 'bid': 4.38, + 'ask': 4.4, + 'last': 4.38 + })) + + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + # stoploss should be set to 1% as trailing is on + assert trade.stop_loss == 4.4 * 0.99 + cancel_order_mock.assert_called_once_with('100', 'NEO/BTC') + stoploss_order_mock.assert_called_once_with( + amount=30, + pair='NEO/BTC', + order_types=freqtrade.strategy.order_types, + stop_price=4.4 * 0.99, + side='sell', + leverage=1.0 + ) + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_trade_exit_down_stoploss_on_exchange_dry_run( + default_conf_usdt, ticker_usdt, fee, is_short, ticker_usdt_sell_down, + ticker_usdt_sell_up, mocker) -> None: + rpc_mock = patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=ticker_usdt, + get_fee=fee, + _dry_is_price_crossed=MagicMock(return_value=False), + ) + patch_whitelist(mocker, default_conf_usdt) + freqtrade = FreqtradeBot(default_conf_usdt) + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + # Create some test data + freqtrade.enter_positions() + + trade = Trade.session.scalars(select(Trade)).first() + assert trade.is_short == is_short + assert trade + + # Decrease the price and sell it + mocker.patch.multiple( + EXMS, + fetch_ticker=ticker_usdt_sell_up if is_short else ticker_usdt_sell_down + ) + + default_conf_usdt['dry_run'] = True + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + # Setting trade stoploss to 0.01 + + trade.stop_loss = 2.0 * 1.01 if is_short else 2.0 * 0.99 + freqtrade.execute_trade_exit( + trade=trade, limit=trade.stop_loss, + exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)) + + assert rpc_mock.call_count == 2 + last_msg = rpc_mock.call_args_list[-1][0][0] + + assert { + 'type': RPCMessageType.EXIT, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'ETH/USDT', + 'direction': 'Short' if trade.is_short else 'Long', + 'leverage': 1.0, + 'gain': 'loss', + 'limit': 2.02 if is_short else 1.98, + 'order_rate': 2.02 if is_short else 1.98, + 'amount': pytest.approx(29.70297029 if is_short else 30.0), + 'order_type': 'limit', + 'buy_tag': None, + 'enter_tag': None, + 'open_rate': 2.02 if is_short else 2.0, + 'current_rate': 2.2 if is_short else 2.0, + 'profit_amount': -0.3 if is_short else -0.8985, + 'profit_ratio': -0.00501253 if is_short else -0.01493766, + 'stake_currency': 'USDT', + 'quote_currency': 'USDT', + 'fiat_currency': 'USD', + 'base_currency': 'ETH', + 'exit_reason': ExitType.STOP_LOSS.value, + 'open_date': ANY, + 'close_date': ANY, + 'close_rate': ANY, + 'sub_trade': False, + 'cumulative_profit': 0.0, + 'stake_amount': pytest.approx(60), + 'is_final_exit': False, + 'final_profit_ratio': None, + } == last_msg + + +def test_execute_trade_exit_sloe_cancel_exception( + mocker, default_conf_usdt, ticker_usdt, fee, caplog) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + mocker.patch(f'{EXMS}.cancel_stoploss_order', side_effect=InvalidOrderException()) + mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300)) + create_order_mock = MagicMock(side_effect=[ + {'id': '12345554'}, + {'id': '12345555'}, + ]) + patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=ticker_usdt, + get_fee=fee, + create_order=create_order_mock, + ) + + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + patch_get_signal(freqtrade) + freqtrade.enter_positions() + + trade = Trade.session.scalars(select(Trade)).first() + PairLock.session = MagicMock() + + freqtrade.config['dry_run'] = False + trade.orders.append( + Order( + ft_order_side='stoploss', + ft_pair=trade.pair, + ft_is_open=True, + ft_amount=trade.amount, + ft_price=trade.stop_loss, + order_id='abcd', + status='open', + ) + ) + + freqtrade.execute_trade_exit(trade=trade, limit=1234, + exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS)) + assert create_order_mock.call_count == 2 + assert log_has('Could not cancel stoploss order abcd for pair ETH/USDT', caplog) + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_trade_exit_with_stoploss_on_exchange( + default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, is_short, mocker) -> None: + + default_conf_usdt['exchange']['name'] = 'binance' + rpc_mock = patch_RPCManager(mocker) + patch_exchange(mocker) + stoploss = MagicMock(return_value={ + 'id': 123, + 'status': 'open', + 'info': { + 'foo': 'bar' + } + }) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_order_fee') + + cancel_order = MagicMock(return_value=True) + mocker.patch.multiple( + EXMS, + fetch_ticker=ticker_usdt, + get_fee=fee, + amount_to_precision=lambda s, x, y: y, + price_to_precision=lambda s, x, y: y, + create_stoploss=stoploss, + cancel_stoploss_order=cancel_order, + _dry_is_price_crossed=MagicMock(side_effect=[True, False]), + ) + + freqtrade = FreqtradeBot(default_conf_usdt) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short) + + # Create some test data + freqtrade.enter_positions() + + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + assert trade + trades = [trade] + + freqtrade.manage_open_orders() + freqtrade.exit_positions(trades) + + # Increase the price and sell it + mocker.patch.multiple( + EXMS, + fetch_ticker=ticker_usdt_sell_up + ) + + freqtrade.execute_trade_exit( + trade=trade, + limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'], + exit_check=ExitCheckTuple(exit_type=ExitType.STOP_LOSS) + ) + + trade = Trade.session.scalars(select(Trade)).first() + trade.is_short = is_short + assert trade + assert cancel_order.call_count == 1 + assert rpc_mock.call_count == 4 + + +@pytest.mark.parametrize("is_short", [False, True]) +def test_may_execute_trade_exit_after_stoploss_on_exchange_hit( + default_conf_usdt, ticker_usdt, fee, mocker, is_short) -> None: + default_conf_usdt['exchange']['name'] = 'binance' + rpc_mock = patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + EXMS, + fetch_ticker=ticker_usdt, + get_fee=fee, + amount_to_precision=lambda s, x, y: y, + price_to_precision=lambda s, x, y: y, + _dry_is_price_crossed=MagicMock(side_effect=[False, True]), + ) + + stoploss = MagicMock(return_value={ + 'id': 123, + 'info': { + 'foo': 'bar' + } + }) + + mocker.patch(f'{EXMS}.create_stoploss', stoploss) + + freqtrade = FreqtradeBot(default_conf_usdt) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + patch_get_signal(freqtrade, enter_long=not is_short, enter_short=is_short) + + # Create some test data + freqtrade.enter_positions() + freqtrade.manage_open_orders() + trade = Trade.session.scalars(select(Trade)).first() + trades = [trade] + assert trade.has_open_sl_orders is False + + freqtrade.exit_positions(trades) + assert trade + assert trade.has_open_sl_orders is True + assert not trade.has_open_orders + + # Assuming stoploss on exchange is hit + # trade should be sold at the price of stoploss, with exit_reason STOPLOSS_ON_EXCHANGE + stoploss_executed = MagicMock(return_value={ + "id": "123", + "timestamp": 1542707426845, + "datetime": "2018-11-20T09:50:26.845Z", + "lastTradeTimestamp": None, + "symbol": "BTC/USDT", + "type": "stop_loss_limit", + "side": "buy" if is_short else "sell", + "price": 1.08801, + "amount": trade.amount, + "cost": 1.08801 * trade.amount, + "average": 1.08801, + "filled": trade.amount, + "remaining": 0.0, + "status": "closed", + "fee": None, + "trades": None + }) + mocker.patch(f'{EXMS}.fetch_stoploss_order', stoploss_executed) + + freqtrade.exit_positions(trades) + assert trade.has_open_sl_orders is False + assert trade.is_open is False + assert trade.exit_reason == ExitType.STOPLOSS_ON_EXCHANGE.value + assert rpc_mock.call_count == 4 + assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.ENTRY + assert rpc_mock.call_args_list[1][0][0]['amount'] > 20 + assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.ENTRY_FILL + assert rpc_mock.call_args_list[3][0][0]['type'] == RPCMessageType.EXIT_FILL From c5948693a30bedc64a86f003d101927d70585f2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Feb 2024 08:14:17 +0100 Subject: [PATCH 083/327] enable sub-minute backtest detail timeframes closes #9635 --- freqtrade/optimize/backtesting.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 43aa00a65..493c7567f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -23,7 +23,7 @@ from freqtrade.enums import (BacktestState, CandleType, ExitCheckTuple, ExitType TradingMode) from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import (amount_to_contract_precision, price_to_precision, - timeframe_to_minutes, timeframe_to_seconds) + timeframe_to_seconds) from freqtrade.exchange.exchange import Exchange from freqtrade.mixins import LoggingMixin from freqtrade.optimize.backtest_caching import get_strategy_run_id @@ -117,8 +117,9 @@ class Backtesting: raise OperationalException("Timeframe needs to be set in either " "configuration or as cli argument `--timeframe 5m`") self.timeframe = str(self.config.get('timeframe')) - self.timeframe_min = timeframe_to_minutes(self.timeframe) - self.timeframe_td = timedelta(minutes=self.timeframe_min) + self.timeframe_secs = timeframe_to_seconds(self.timeframe) + self.timeframe_min = self.timeframe_secs // 60 + self.timeframe_td = timedelta(seconds=self.timeframe_secs) self.disable_database_use() self.init_backtest_detail() self.pairlists = PairListManager(self.exchange, self.config, self.dataprovider) @@ -185,9 +186,9 @@ class Backtesting: # Load detail timeframe if specified self.timeframe_detail = str(self.config.get('timeframe_detail', '')) if self.timeframe_detail: - timeframe_detail_min = timeframe_to_minutes(self.timeframe_detail) - self.timeframe_detail_td = timedelta(minutes=timeframe_detail_min) - if self.timeframe_min <= timeframe_detail_min: + timeframe_detail_secs = timeframe_to_seconds(self.timeframe_detail) + self.timeframe_detail_td = timedelta(seconds=timeframe_detail_secs) + if self.timeframe_secs <= timeframe_detail_secs: raise OperationalException( "Detail timeframe must be smaller than strategy timeframe.") From 3711fa509819cf75f4fd9a6c0dea1e5b7a8da096 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Feb 2024 13:10:42 +0100 Subject: [PATCH 084/327] Improve formatting of leverage closes #9759 --- freqtrade/rpc/telegram.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e2fbe1529..f42e3ab51 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -353,7 +353,7 @@ class Telegram(RPCHandler): message += f"*Amount:* `{round_value(msg['amount'], 8)}`\n" message += f"*Direction:* `{msg['direction']}" if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0: - message += f" ({msg['leverage']:.1g}x)" + message += f" ({msg['leverage']:.3g}x)" message += "`\n" message += f"*Open Rate:* `{fmt_coin(msg['open_rate'], msg['quote_currency'])}`\n" if msg['type'] == RPCMessageType.ENTRY and msg['current_rate']: @@ -371,7 +371,7 @@ class Telegram(RPCHandler): microsecond=0) - msg['open_date'].replace(microsecond=0) duration_min = duration.total_seconds() / 60 - leverage_text = (f" ({msg['leverage']:.1g}x)" + leverage_text = (f" ({msg['leverage']:.3g}x)" if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0 else "") diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f85b19a01..2e6852876 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2017,7 +2017,7 @@ def test_send_msg_enter_notification(default_conf, mocker, caplog, message_type, telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg(msg) - leverage_text = f' ({leverage:.1g}x)' if leverage and leverage != 1.0 else '' + leverage_text = f' ({leverage:.3g}x)' if leverage and leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( f'\N{LARGE BLUE CIRCLE} *Binance (dry):* New Trade (#1)\n' @@ -2126,7 +2126,7 @@ def test_send_msg_entry_fill_notification(default_conf, mocker, message_type, en 'amount': 1333.3333333333335, 'open_date': dt_now() - timedelta(hours=1) }) - leverage_text = f' ({leverage:.1g}x)' if leverage != 1.0 else '' + leverage_text = f' ({leverage:.3g}x)' if leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( f'\N{CHECK MARK} *Binance (dry):* New Trade filled (#1)\n' f'*Pair:* `ETH/BTC`\n' @@ -2365,7 +2365,7 @@ def test_send_msg_exit_fill_notification(default_conf, mocker, direction, 'close_date': dt_now(), }) - leverage_text = f' ({leverage:.1g}x)`\n' if leverage and leverage != 1.0 else '`\n' + leverage_text = f' ({leverage:.3g}x)`\n' if leverage and leverage != 1.0 else '`\n' assert msg_mock.call_args[0][0] == ( '\N{WARNING SIGN} *Binance (dry):* Exited KEY/ETH (#1)\n' '*Profit:* `-57.41% (loss: -0.05746 ETH)`\n' @@ -2458,7 +2458,7 @@ def test_send_msg_buy_notification_no_fiat( 'open_date': dt_now() - timedelta(hours=1) }) - leverage_text = f' ({leverage:.1g}x)' if leverage and leverage != 1.0 else '' + leverage_text = f' ({leverage:.3g}x)' if leverage and leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( f'\N{LARGE BLUE CIRCLE} *Binance:* New Trade (#1)\n' '*Pair:* `ETH/BTC`\n' @@ -2510,7 +2510,7 @@ def test_send_msg_exit_notification_no_fiat( 'close_date': dt_now(), }) - leverage_text = f' ({leverage:.1g}x)' if leverage and leverage != 1.0 else '' + leverage_text = f' ({leverage:.3g}x)' if leverage and leverage != 1.0 else '' assert msg_mock.call_args[0][0] == ( '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' '*Unrealized Profit:* `-57.41% (loss: -0.05746 ETH)`\n' From 2989b427603b3e4b7c331335e5dfc4ba9de41236 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Feb 2024 13:16:32 +0100 Subject: [PATCH 085/327] Update cached binance leverage tiers --- .../exchange/binance_leverage_tiers.json | 6360 +++++++---------- 1 file changed, 2554 insertions(+), 3806 deletions(-) diff --git a/freqtrade/exchange/binance_leverage_tiers.json b/freqtrade/exchange/binance_leverage_tiers.json index 3a6e6b0a1..36c2e5996 100644 --- a/freqtrade/exchange/binance_leverage_tiers.json +++ b/freqtrade/exchange/binance_leverage_tiers.json @@ -227,104 +227,6 @@ } } ], - "1000LUNC/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "1000LUNC/USDT:USDT": [ { "tier": 1.0, @@ -569,7 +471,7 @@ } } ], - "1000SATS/USDT:USDT": [ + "1000RATS/USDT:USDT": [ { "tier": 1.0, "currency": "USDT", @@ -683,91 +585,91 @@ } } ], - "1000SHIB/BUSD:BUSD": [ + "1000SATS/USDT:USDT": [ { "tier": 1.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "10", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.015", "cum": "0.0" } }, { "tier": 2.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, + "maxNotional": 50000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", + "initialLeverage": "20", + "notionalCap": "50000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "25.0" + "cum": "50.0" } }, { "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", + "initialLeverage": "10", + "notionalCap": "200000", + "notionalFloor": "50000", "maintMarginRatio": "0.05", - "cum": "650.0" + "cum": "1300.0" } }, { "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "11300.0" } }, { "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1500000.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "5", "initialLeverage": "4", - "notionalCap": "1500000", - "notionalFloor": "250000", + "notionalCap": "1000000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "23800.0" } }, { "tier": 6.0, - "currency": "BUSD", - "minNotional": 1500000.0, + "currency": "USDT", + "minNotional": 1000000.0, "maxNotional": 3000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, @@ -775,25 +677,25 @@ "bracket": "6", "initialLeverage": "2", "notionalCap": "3000000", - "notionalFloor": "1500000", + "notionalFloor": "1000000", "maintMarginRatio": "0.25", - "cum": "199400.0" + "cum": "148800.0" } }, { "tier": 7.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 3000000.0, - "maxNotional": 3500000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "3500000", + "notionalCap": "5000000", "notionalFloor": "3000000", "maintMarginRatio": "0.5", - "cum": "949400.0" + "cum": "898800.0" } } ], @@ -1269,6 +1171,120 @@ } } ], + "ACE/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "ACH/USDT:USDT": [ { "tier": 1.0, @@ -1367,104 +1383,6 @@ } } ], - "ADA/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "500000", - "notionalFloor": "100000", - "maintMarginRatio": "0.05", - "cum": "2500.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.1", - "cum": "27500.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, - "info": { - "bracket": "4", - "initialLeverage": "3", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.15", - "cum": "77500.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.25", - "cum": "277500.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 5000000.0, - "maxNotional": 5500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "5500000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.5", - "cum": "1527500.0" - } - } - ], "ADA/USDT:USDT": [ { "tier": 1.0, @@ -1627,104 +1545,6 @@ } } ], - "AGIX/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "AGIX/USDT:USDT": [ { "tier": 1.0, @@ -1953,6 +1773,120 @@ } } ], + "AI/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "ALGO/USDT:USDT": [ { "tier": 1.0, @@ -2263,101 +2197,117 @@ } } ], - "AMB/BUSD:BUSD": [ + "ALT/USDT:USDT": [ { "tier": 1.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "8", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.015", "cum": "0.0" } }, { "tier": 2.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "7", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "25.0" + "cum": "50.0" } }, { "tier": 3.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "6", + "initialLeverage": "10", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "650.0" + "cum": "675.0" } }, { "tier": 4.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 100000.0, - "maxNotional": 250000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", + "notionalCap": "200000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "5675.0" } }, { "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "10675.0" } }, { "tier": 6.0, - "currency": "BUSD", + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 1500000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "1500000", + "notionalCap": "2000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "323175.0" } } ], @@ -2475,104 +2425,6 @@ } } ], - "ANC/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "ANKR/USDT:USDT": [ { "tier": 1.0, @@ -2785,104 +2637,6 @@ } } ], - "APE/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1200000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1200000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "APE/USDT:USDT": [ { "tier": 1.0, @@ -3159,120 +2913,6 @@ } } ], - "APT/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "5", - "initialLeverage": "4", - "notionalCap": "1500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1500000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1500000", - "maintMarginRatio": "0.25", - "cum": "199400.0" - } - }, - { - "tier": 7.0, - "currency": "BUSD", - "minNotional": 3000000.0, - "maxNotional": 4000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "4000000", - "notionalFloor": "3000000", - "maintMarginRatio": "0.5", - "cum": "949400.0" - } - } - ], "APT/USDT:USDT": [ { "tier": 1.0, @@ -4299,101 +3939,117 @@ } } ], - "AUCTION/BUSD:BUSD": [ + "AUCTION/USDT:USDT": [ { "tier": 1.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "8", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.015", "cum": "0.0" } }, { "tier": 2.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "7", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "25.0" + "cum": "50.0" } }, { "tier": 3.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "6", + "initialLeverage": "10", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "650.0" + "cum": "675.0" } }, { "tier": 4.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 100000.0, - "maxNotional": 250000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", + "notionalCap": "200000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "5675.0" } }, { "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "10675.0" } }, { "tier": 6.0, - "currency": "BUSD", + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 1500000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", - "notionalCap": "1500000", + "notionalCap": "2000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "323175.0" } } ], @@ -4495,120 +4151,6 @@ } } ], - "AVAX/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "5", - "initialLeverage": "4", - "notionalCap": "1500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1500000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1500000", - "maintMarginRatio": "0.25", - "cum": "199400.0" - } - }, - { - "tier": 7.0, - "currency": "BUSD", - "minNotional": 3000000.0, - "maxNotional": 4000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "4000000", - "notionalFloor": "3000000", - "maintMarginRatio": "0.5", - "cum": "949400.0" - } - } - ], "AVAX/USDT:USDT": [ { "tier": 1.0, @@ -5118,10 +4660,10 @@ "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", @@ -5132,16 +4674,32 @@ "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1500000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "136925.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1500000.0, "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.5", - "cum": "386925.0" + "cum": "511925.0" } } ], @@ -6324,10 +5882,10 @@ "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "4", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", @@ -6338,121 +5896,39 @@ "tier": 5.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "BNB/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 11.0, - "info": { - "bracket": "1", - "initialLeverage": "11", - "notionalCap": "100000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, - "info": { - "bracket": "2", - "initialLeverage": "10", - "notionalCap": "500000", - "notionalFloor": "100000", - "maintMarginRatio": "0.05", - "cum": "2500.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.1", - "cum": "27500.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, - "info": { - "bracket": "4", - "initialLeverage": "3", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.15", - "cum": "77500.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "5", "initialLeverage": "2", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "1500000", + "notionalFloor": "1000000", "maintMarginRatio": "0.25", - "cum": "277500.0" + "cum": "136875.0" } }, { "tier": 6.0, - "currency": "BUSD", - "minNotional": 5000000.0, - "maxNotional": 5500000.0, + "currency": "USDT", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "5500000", - "notionalFloor": "5000000", + "notionalCap": "3000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.5", - "cum": "1527500.0" + "cum": "511875.0" } } ], - "BNB/USDT:USDT": [ + "BNB/USDC:USDC": [ { "tier": 1.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 0.0, "maxNotional": 5000.0, "maintenanceMarginRate": 0.005, @@ -6468,7 +5944,7 @@ }, { "tier": 2.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 5000.0, "maxNotional": 10000.0, "maintenanceMarginRate": 0.006, @@ -6484,7 +5960,7 @@ }, { "tier": 3.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 10000.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, @@ -6500,7 +5976,7 @@ }, { "tier": 4.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 50000.0, "maxNotional": 250000.0, "maintenanceMarginRate": 0.02, @@ -6516,7 +5992,7 @@ }, { "tier": 5.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.05, @@ -6532,7 +6008,7 @@ }, { "tier": 6.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 1000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.1, @@ -6548,7 +6024,7 @@ }, { "tier": 7.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 5000000.0, "maxNotional": 10000000.0, "maintenanceMarginRate": 0.125, @@ -6564,7 +6040,7 @@ }, { "tier": 8.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 10000000.0, "maxNotional": 20000000.0, "maintenanceMarginRate": 0.15, @@ -6580,7 +6056,7 @@ }, { "tier": 9.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 20000000.0, "maxNotional": 30000000.0, "maintenanceMarginRate": 0.25, @@ -6596,7 +6072,7 @@ }, { "tier": 10.0, - "currency": "USDT", + "currency": "USDC", "minNotional": 30000000.0, "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, @@ -6611,6 +6087,168 @@ } } ], + "BNB/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, + "info": { + "bracket": "1", + "initialLeverage": "75", + "notionalCap": "10000", + "notionalFloor": "0", + "maintMarginRatio": "0.005", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, + "info": { + "bracket": "2", + "initialLeverage": "50", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.006", + "cum": "10.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 50000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 40.0, + "info": { + "bracket": "3", + "initialLeverage": "40", + "notionalCap": "100000", + "notionalFloor": "50000", + "maintMarginRatio": "0.01", + "cum": "210.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, + "info": { + "bracket": "4", + "initialLeverage": "25", + "notionalCap": "500000", + "notionalFloor": "100000", + "maintMarginRatio": "0.02", + "cum": "1210.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "5", + "initialLeverage": "10", + "notionalCap": "2000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.05", + "cum": "16210.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 2000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "6000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.1", + "cum": "116210.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.125", + "cum": "266210.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 10000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, + "info": { + "bracket": "8", + "initialLeverage": "3", + "notionalCap": "20000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.15", + "cum": "516210.0" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "2516210.0" + } + }, + { + "tier": 10.0, + "currency": "USDT", + "minNotional": 30000000.0, + "maxNotional": 50000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "10", + "initialLeverage": "1", + "notionalCap": "50000000", + "notionalFloor": "30000000", + "maintMarginRatio": "0.5", + "cum": "10016210.0" + } + } + ], "BNT/USDT:USDT": [ { "tier": 1.0, @@ -7051,17 +6689,17 @@ } } ], - "BTC/BUSD:BUSD": [ + "BTC/USDC:USDC": [ { "tier": 1.0, - "currency": "BUSD", + "currency": "USDC", "minNotional": 0.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.004, - "maxLeverage": 30.0, + "maxLeverage": 125.0, "info": { "bracket": "1", - "initialLeverage": "30", + "initialLeverage": "125", "notionalCap": "50000", "notionalFloor": "0", "maintMarginRatio": "0.004", @@ -7070,15 +6708,15 @@ }, { "tier": 2.0, - "currency": "BUSD", + "currency": "USDC", "minNotional": 50000.0, - "maxNotional": 250000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.005, - "maxLeverage": 25.0, + "maxLeverage": 100.0, "info": { "bracket": "2", - "initialLeverage": "25", - "notionalCap": "250000", + "initialLeverage": "100", + "notionalCap": "500000", "notionalFloor": "50000", "maintMarginRatio": "0.005", "cum": "50.0" @@ -7086,130 +6724,130 @@ }, { "tier": 3.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "currency": "USDC", + "minNotional": 500000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maxLeverage": 50.0, "info": { "bracket": "3", - "initialLeverage": "20", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "50", + "notionalCap": "10000000", + "notionalFloor": "500000", "maintMarginRatio": "0.01", - "cum": "1300.0" + "cum": "2550.0" } }, { "tier": 4.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 7500000.0, + "currency": "USDC", + "minNotional": 10000000.0, + "maxNotional": 80000000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maxLeverage": 20.0, "info": { "bracket": "4", - "initialLeverage": "15", - "notionalCap": "7500000", - "notionalFloor": "1000000", + "initialLeverage": "20", + "notionalCap": "80000000", + "notionalFloor": "10000000", "maintMarginRatio": "0.025", - "cum": "16300.0" + "cum": "152550.0" } }, { "tier": 5.0, - "currency": "BUSD", - "minNotional": 7500000.0, - "maxNotional": 40000000.0, + "currency": "USDC", + "minNotional": 80000000.0, + "maxNotional": 150000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "40000000", - "notionalFloor": "7500000", + "notionalCap": "150000000", + "notionalFloor": "80000000", "maintMarginRatio": "0.05", - "cum": "203800.0" + "cum": "2152550.0" } }, { "tier": 6.0, - "currency": "BUSD", - "minNotional": 40000000.0, - "maxNotional": 100000000.0, + "currency": "USDC", + "minNotional": 150000000.0, + "maxNotional": 300000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "100000000", - "notionalFloor": "40000000", + "notionalCap": "300000000", + "notionalFloor": "150000000", "maintMarginRatio": "0.1", - "cum": "2203800.0" + "cum": "9652550.0" } }, { "tier": 7.0, - "currency": "BUSD", - "minNotional": 100000000.0, - "maxNotional": 200000000.0, + "currency": "USDC", + "minNotional": 300000000.0, + "maxNotional": 450000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "200000000", - "notionalFloor": "100000000", + "notionalCap": "450000000", + "notionalFloor": "300000000", "maintMarginRatio": "0.125", - "cum": "4703800.0" + "cum": "17152550.0" } }, { "tier": 8.0, - "currency": "BUSD", - "minNotional": 200000000.0, - "maxNotional": 400000000.0, + "currency": "USDC", + "minNotional": 450000000.0, + "maxNotional": 600000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "8", "initialLeverage": "3", - "notionalCap": "400000000", - "notionalFloor": "200000000", + "notionalCap": "600000000", + "notionalFloor": "450000000", "maintMarginRatio": "0.15", - "cum": "9703800.0" + "cum": "28402550.0" } }, { "tier": 9.0, - "currency": "BUSD", - "minNotional": 400000000.0, - "maxNotional": 600000000.0, + "currency": "USDC", + "minNotional": 600000000.0, + "maxNotional": 800000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "9", "initialLeverage": "2", - "notionalCap": "600000000", - "notionalFloor": "400000000", + "notionalCap": "800000000", + "notionalFloor": "600000000", "maintMarginRatio": "0.25", - "cum": "49703800.0" + "cum": "88402550.0" } }, { "tier": 10.0, - "currency": "BUSD", - "minNotional": 600000000.0, - "maxNotional": 600500000.0, + "currency": "USDC", + "minNotional": 800000000.0, + "maxNotional": 1000000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "10", "initialLeverage": "1", - "notionalCap": "600500000", - "notionalFloor": "600000000", + "notionalCap": "1000000000", + "notionalFloor": "800000000", "maintMarginRatio": "0.5", - "cum": "199703800.0" + "cum": "288402550.0" } } ], @@ -7250,13 +6888,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 500000.0, - "maxNotional": 8000000.0, + "maxNotional": 10000000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 50.0, "info": { "bracket": "3", "initialLeverage": "50", - "notionalCap": "8000000", + "notionalCap": "10000000", "notionalFloor": "500000", "maintMarginRatio": "0.01", "cum": "2550.0" @@ -7265,117 +6903,117 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 8000000.0, - "maxNotional": 50000000.0, + "minNotional": 10000000.0, + "maxNotional": 80000000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "4", "initialLeverage": "20", - "notionalCap": "50000000", - "notionalFloor": "8000000", + "notionalCap": "80000000", + "notionalFloor": "10000000", "maintMarginRatio": "0.025", - "cum": "122550.0" + "cum": "152550.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 50000000.0, - "maxNotional": 80000000.0, + "minNotional": 80000000.0, + "maxNotional": 150000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "5", "initialLeverage": "10", - "notionalCap": "80000000", - "notionalFloor": "50000000", + "notionalCap": "150000000", + "notionalFloor": "80000000", "maintMarginRatio": "0.05", - "cum": "1372550.0" + "cum": "2152550.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 80000000.0, - "maxNotional": 100000000.0, + "minNotional": 150000000.0, + "maxNotional": 300000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "6", "initialLeverage": "5", - "notionalCap": "100000000", - "notionalFloor": "80000000", + "notionalCap": "300000000", + "notionalFloor": "150000000", "maintMarginRatio": "0.1", - "cum": "5372550.0" + "cum": "9652550.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 100000000.0, - "maxNotional": 120000000.0, + "minNotional": 300000000.0, + "maxNotional": 450000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "7", "initialLeverage": "4", - "notionalCap": "120000000", - "notionalFloor": "100000000", + "notionalCap": "450000000", + "notionalFloor": "300000000", "maintMarginRatio": "0.125", - "cum": "7872550.0" + "cum": "17152550.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 120000000.0, - "maxNotional": 200000000.0, + "minNotional": 450000000.0, + "maxNotional": 600000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, "info": { "bracket": "8", "initialLeverage": "3", - "notionalCap": "200000000", - "notionalFloor": "120000000", + "notionalCap": "600000000", + "notionalFloor": "450000000", "maintMarginRatio": "0.15", - "cum": "10872550.0" + "cum": "28402550.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 200000000.0, - "maxNotional": 300000000.0, + "minNotional": 600000000.0, + "maxNotional": 800000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "9", "initialLeverage": "2", - "notionalCap": "300000000", - "notionalFloor": "200000000", + "notionalCap": "800000000", + "notionalFloor": "600000000", "maintMarginRatio": "0.25", - "cum": "30872550.0" + "cum": "88402550.0" } }, { "tier": 10.0, "currency": "USDT", - "minNotional": 300000000.0, - "maxNotional": 500000000.0, + "minNotional": 800000000.0, + "maxNotional": 1000000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "10", "initialLeverage": "1", - "notionalCap": "500000000", - "notionalFloor": "300000000", + "notionalCap": "1000000000", + "notionalFloor": "800000000", "maintMarginRatio": "0.5", - "cum": "105872550.0" + "cum": "288402550.0" } } ], - "BTC/USDT:USDT-231229": [ + "BTC/USDT:USDT-240329": [ { "tier": 1.0, "currency": "USDT", @@ -7505,7 +7143,7 @@ } } ], - "BTC/USDT:USDT-240329": [ + "BTC/USDT:USDT-240628": [ { "tier": 1.0, "currency": "USDT", @@ -9657,104 +9295,6 @@ } } ], - "CVX/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.5", - "cum": "199400.0" - } - } - ], "CVX/USDT:USDT": [ { "tier": 1.0, @@ -10473,88 +10013,6 @@ } } ], - "DODO/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "2", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "625.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5625.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "4", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1200000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "1200000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], "DODOX/USDT:USDT": [ { "tier": 1.0, @@ -10669,101 +10127,165 @@ } } ], - "DOGE/BUSD:BUSD": [ + "DOGE/USDC:USDC": [ { "tier": 1.0, - "currency": "BUSD", + "currency": "USDC", "minNotional": 0.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 10.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "10", - "notionalCap": "100000", + "initialLeverage": "75", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.005", "cum": "0.0" } }, { "tier": 2.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 8.0, + "currency": "USDC", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "2", - "initialLeverage": "8", - "notionalCap": "500000", - "notionalFloor": "100000", - "maintMarginRatio": "0.05", - "cum": "2500.0" + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.006", + "cum": "5.0" } }, { "tier": 3.0, - "currency": "BUSD", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "currency": "USDC", + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.007, + "maxLeverage": 40.0, "info": { "bracket": "3", - "initialLeverage": "5", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.1", - "cum": "27500.0" + "initialLeverage": "40", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.007", + "cum": "15.0" } }, { "tier": 4.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, + "currency": "USDC", + "minNotional": 50000.0, + "maxNotional": 750000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 25.0, "info": { "bracket": "4", - "initialLeverage": "3", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.15", - "cum": "77500.0" + "initialLeverage": "25", + "notionalCap": "750000", + "notionalFloor": "50000", + "maintMarginRatio": "0.01", + "cum": "165.0" } }, { "tier": 5.0, - "currency": "BUSD", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "currency": "USDC", + "minNotional": 750000.0, + "maxNotional": 1100000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.25", - "cum": "277500.0" + "initialLeverage": "20", + "notionalCap": "1100000", + "notionalFloor": "750000", + "maintMarginRatio": "0.025", + "cum": "11415.0" } }, { "tier": 6.0, - "currency": "BUSD", - "minNotional": 5000000.0, - "maxNotional": 5200000.0, + "currency": "USDC", + "minNotional": 1100000.0, + "maxNotional": 2200000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "6", + "initialLeverage": "10", + "notionalCap": "2200000", + "notionalFloor": "1100000", + "maintMarginRatio": "0.05", + "cum": "38915.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 2200000.0, + "maxNotional": 5600000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "7", + "initialLeverage": "5", + "notionalCap": "5600000", + "notionalFloor": "2200000", + "maintMarginRatio": "0.1", + "cum": "148915.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 5600000.0, + "maxNotional": 7000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "8", + "initialLeverage": "4", + "notionalCap": "7000000", + "notionalFloor": "5600000", + "maintMarginRatio": "0.125", + "cum": "288915.0" + } + }, + { + "tier": 9.0, + "currency": "USDC", + "minNotional": 7000000.0, + "maxNotional": 18000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "18000000", + "notionalFloor": "7000000", + "maintMarginRatio": "0.25", + "cum": "1163915.0" + } + }, + { + "tier": 10.0, + "currency": "USDC", + "minNotional": 18000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "10", "initialLeverage": "1", - "notionalCap": "5200000", - "notionalFloor": "5000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "1527500.0" + "cum": "5663915.0" } } ], @@ -10929,120 +10451,6 @@ } } ], - "DOT/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "5", - "initialLeverage": "4", - "notionalCap": "1500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1500000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1500000", - "maintMarginRatio": "0.25", - "cum": "199400.0" - } - }, - { - "tier": 7.0, - "currency": "BUSD", - "minNotional": 3000000.0, - "maxNotional": 4000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "4000000", - "notionalFloor": "3000000", - "maintMarginRatio": "0.5", - "cum": "949400.0" - } - } - ], "DOT/USDT:USDT": [ { "tier": 1.0, @@ -11080,13 +10488,13 @@ "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 250000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { "bracket": "3", "initialLeverage": "25", - "notionalCap": "250000", + "notionalCap": "500000", "notionalFloor": "50000", "maintMarginRatio": "0.02", "cum": "535.0" @@ -11095,55 +10503,55 @@ { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 500000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalCap": "2000000", + "notionalFloor": "500000", "maintMarginRatio": "0.05", - "cum": "8035.0" + "cum": "15535.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, + "minNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "1000000", + "notionalCap": "5000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.1", - "cum": "58035.0" + "cum": "115535.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, + "minNotional": 5000000.0, + "maxNotional": 7000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "6", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "2000000", + "notionalCap": "7000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.125", - "cum": "108035.0" + "cum": "240535.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 5000000.0, + "minNotional": 7000000.0, "maxNotional": 10000000.0, "maintenanceMarginRate": 0.15, "maxLeverage": 3.0, @@ -11151,41 +10559,41 @@ "bracket": "7", "initialLeverage": "3", "notionalCap": "10000000", - "notionalFloor": "5000000", + "notionalFloor": "7000000", "maintMarginRatio": "0.15", - "cum": "233035.0" + "cum": "415535.0" } }, { "tier": 8.0, "currency": "USDT", "minNotional": 10000000.0, - "maxNotional": 50000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "8", "initialLeverage": "2", - "notionalCap": "50000000", + "notionalCap": "30000000", "notionalFloor": "10000000", "maintMarginRatio": "0.25", - "cum": "1233035.0" + "cum": "1415535.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 50000000.0, - "maxNotional": 100000000.0, + "minNotional": 30000000.0, + "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "9", "initialLeverage": "1", - "notionalCap": "100000000", - "notionalFloor": "50000000", + "notionalCap": "50000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "13733035.0" + "cum": "8915535.0" } } ], @@ -12051,104 +11459,6 @@ } } ], - "ETC/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "ETC/USDT:USDT": [ { "tier": 1.0, @@ -12473,17 +11783,17 @@ } } ], - "ETH/BUSD:BUSD": [ + "ETH/USDC:USDC": [ { "tier": 1.0, - "currency": "BUSD", + "currency": "USDC", "minNotional": 0.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.004, - "maxLeverage": 30.0, + "maxLeverage": 125.0, "info": { "bracket": "1", - "initialLeverage": "30", + "initialLeverage": "125", "notionalCap": "50000", "notionalFloor": "0", "maintMarginRatio": "0.004", @@ -12492,15 +11802,15 @@ }, { "tier": 2.0, - "currency": "BUSD", + "currency": "USDC", "minNotional": 50000.0, - "maxNotional": 100000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.005, - "maxLeverage": 25.0, + "maxLeverage": 100.0, "info": { "bracket": "2", - "initialLeverage": "25", - "notionalCap": "100000", + "initialLeverage": "100", + "notionalCap": "500000", "notionalFloor": "50000", "maintMarginRatio": "0.005", "cum": "50.0" @@ -12508,130 +11818,146 @@ }, { "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, + "currency": "USDC", + "minNotional": 500000.0, "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.0065, + "maxLeverage": 75.0, "info": { "bracket": "3", - "initialLeverage": "20", + "initialLeverage": "75", "notionalCap": "1000000", - "notionalFloor": "100000", - "maintMarginRatio": "0.01", - "cum": "550.0" + "notionalFloor": "500000", + "maintMarginRatio": "0.0065", + "cum": "800.0" } }, { "tier": 4.0, - "currency": "BUSD", + "currency": "USDC", "minNotional": 1000000.0, "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 15.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, "info": { "bracket": "4", - "initialLeverage": "15", + "initialLeverage": "50", "notionalCap": "5000000", "notionalFloor": "1000000", - "maintMarginRatio": "0.025", - "cum": "15550.0" + "maintMarginRatio": "0.01", + "cum": "4300.0" } }, { "tier": 5.0, - "currency": "BUSD", + "currency": "USDC", "minNotional": 5000000.0, - "maxNotional": 10000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maxNotional": 50000000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "5", - "initialLeverage": "10", - "notionalCap": "10000000", + "initialLeverage": "20", + "notionalCap": "50000000", "notionalFloor": "5000000", - "maintMarginRatio": "0.05", - "cum": "140550.0" + "maintMarginRatio": "0.02", + "cum": "54300.0" } }, { "tier": 6.0, - "currency": "BUSD", - "minNotional": 10000000.0, - "maxNotional": 20000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "currency": "USDC", + "minNotional": 50000000.0, + "maxNotional": 100000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "6", - "initialLeverage": "5", - "notionalCap": "20000000", - "notionalFloor": "10000000", - "maintMarginRatio": "0.1", - "cum": "640550.0" + "initialLeverage": "10", + "notionalCap": "100000000", + "notionalFloor": "50000000", + "maintMarginRatio": "0.05", + "cum": "1554300.0" } }, { "tier": 7.0, - "currency": "BUSD", - "minNotional": 20000000.0, - "maxNotional": 40000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "currency": "USDC", + "minNotional": 100000000.0, + "maxNotional": 150000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "7", - "initialLeverage": "4", - "notionalCap": "40000000", - "notionalFloor": "20000000", - "maintMarginRatio": "0.125", - "cum": "1140550.0" + "initialLeverage": "5", + "notionalCap": "150000000", + "notionalFloor": "100000000", + "maintMarginRatio": "0.1", + "cum": "6554300.0" } }, { "tier": 8.0, - "currency": "BUSD", - "minNotional": 40000000.0, - "maxNotional": 80000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, + "currency": "USDC", + "minNotional": 150000000.0, + "maxNotional": 300000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "8", - "initialLeverage": "3", - "notionalCap": "80000000", - "notionalFloor": "40000000", - "maintMarginRatio": "0.15", - "cum": "2140550.0" + "initialLeverage": "4", + "notionalCap": "300000000", + "notionalFloor": "150000000", + "maintMarginRatio": "0.125", + "cum": "10304300.0" } }, { "tier": 9.0, - "currency": "BUSD", - "minNotional": 80000000.0, - "maxNotional": 150000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "currency": "USDC", + "minNotional": 300000000.0, + "maxNotional": 400000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, "info": { "bracket": "9", - "initialLeverage": "2", - "notionalCap": "150000000", - "notionalFloor": "80000000", - "maintMarginRatio": "0.25", - "cum": "10140550.0" + "initialLeverage": "3", + "notionalCap": "400000000", + "notionalFloor": "300000000", + "maintMarginRatio": "0.15", + "cum": "17804300.0" } }, { "tier": 10.0, - "currency": "BUSD", - "minNotional": 150000000.0, - "maxNotional": 160000000.0, + "currency": "USDC", + "minNotional": 400000000.0, + "maxNotional": 500000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "10", + "initialLeverage": "2", + "notionalCap": "500000000", + "notionalFloor": "400000000", + "maintMarginRatio": "0.25", + "cum": "57804300.0" + } + }, + { + "tier": 11.0, + "currency": "USDC", + "minNotional": 500000000.0, + "maxNotional": 800000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "10", + "bracket": "11", "initialLeverage": "1", - "notionalCap": "160000000", - "notionalFloor": "150000000", + "notionalCap": "800000000", + "notionalFloor": "500000000", "maintMarginRatio": "0.5", - "cum": "47640550.0" + "cum": "182804300.0" } } ], @@ -12640,164 +11966,180 @@ "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.005, - "maxLeverage": 100.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.004, + "maxLeverage": 125.0, "info": { "bracket": "1", - "initialLeverage": "100", - "notionalCap": "200000", + "initialLeverage": "125", + "notionalCap": "50000", "notionalFloor": "0", - "maintMarginRatio": "0.005", + "maintMarginRatio": "0.004", "cum": "0.0" } }, { "tier": 2.0, "currency": "USDT", - "minNotional": 200000.0, - "maxNotional": 800000.0, - "maintenanceMarginRate": 0.0065, - "maxLeverage": 75.0, + "minNotional": 50000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 100.0, "info": { "bracket": "2", - "initialLeverage": "75", - "notionalCap": "800000", - "notionalFloor": "200000", - "maintMarginRatio": "0.0065", - "cum": "300.0" + "initialLeverage": "100", + "notionalCap": "500000", + "notionalFloor": "50000", + "maintMarginRatio": "0.005", + "cum": "50.0" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 800000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 50.0, + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.0065, + "maxLeverage": 75.0, "info": { "bracket": "3", - "initialLeverage": "50", - "notionalCap": "5000000", - "notionalFloor": "800000", - "maintMarginRatio": "0.01", - "cum": "3100.0" + "initialLeverage": "75", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.0065", + "cum": "800.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 30000000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 20.0, + "minNotional": 1000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, "info": { "bracket": "4", - "initialLeverage": "20", - "notionalCap": "30000000", - "notionalFloor": "5000000", - "maintMarginRatio": "0.02", - "cum": "53100.0" + "initialLeverage": "50", + "notionalCap": "5000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.01", + "cum": "4300.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 30000000.0, + "minNotional": 5000000.0, "maxNotional": 50000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 20.0, "info": { "bracket": "5", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "50000000", - "notionalFloor": "30000000", - "maintMarginRatio": "0.05", - "cum": "953100.0" + "notionalFloor": "5000000", + "maintMarginRatio": "0.02", + "cum": "54300.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 50000000.0, - "maxNotional": 70000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "maxNotional": 100000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "6", - "initialLeverage": "5", - "notionalCap": "70000000", + "initialLeverage": "10", + "notionalCap": "100000000", "notionalFloor": "50000000", - "maintMarginRatio": "0.1", - "cum": "3453100.0" + "maintMarginRatio": "0.05", + "cum": "1554300.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 70000000.0, - "maxNotional": 80000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 100000000.0, + "maxNotional": 150000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "7", - "initialLeverage": "4", - "notionalCap": "80000000", - "notionalFloor": "70000000", - "maintMarginRatio": "0.125", - "cum": "5203100.0" + "initialLeverage": "5", + "notionalCap": "150000000", + "notionalFloor": "100000000", + "maintMarginRatio": "0.1", + "cum": "6554300.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 80000000.0, - "maxNotional": 100000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, + "minNotional": 150000000.0, + "maxNotional": 300000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "8", - "initialLeverage": "3", - "notionalCap": "100000000", - "notionalFloor": "80000000", - "maintMarginRatio": "0.15", - "cum": "7203100.0" + "initialLeverage": "4", + "notionalCap": "300000000", + "notionalFloor": "150000000", + "maintMarginRatio": "0.125", + "cum": "10304300.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 100000000.0, - "maxNotional": 150000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 300000000.0, + "maxNotional": 400000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, "info": { "bracket": "9", - "initialLeverage": "2", - "notionalCap": "150000000", - "notionalFloor": "100000000", - "maintMarginRatio": "0.25", - "cum": "17203100.0" + "initialLeverage": "3", + "notionalCap": "400000000", + "notionalFloor": "300000000", + "maintMarginRatio": "0.15", + "cum": "17804300.0" } }, { "tier": 10.0, "currency": "USDT", - "minNotional": 150000000.0, - "maxNotional": 300000000.0, + "minNotional": 400000000.0, + "maxNotional": 500000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "10", + "initialLeverage": "2", + "notionalCap": "500000000", + "notionalFloor": "400000000", + "maintMarginRatio": "0.25", + "cum": "57804300.0" + } + }, + { + "tier": 11.0, + "currency": "USDT", + "minNotional": 500000000.0, + "maxNotional": 800000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "10", + "bracket": "11", "initialLeverage": "1", - "notionalCap": "300000000", - "notionalFloor": "150000000", + "notionalCap": "800000000", + "notionalFloor": "500000000", "maintMarginRatio": "0.5", - "cum": "54703100.0" + "cum": "182804300.0" } } ], - "ETH/USDT:USDT-231229": [ + "ETH/USDT:USDT-240329": [ { "tier": 1.0, "currency": "USDT", @@ -12927,7 +12269,7 @@ } } ], - "ETH/USDT:USDT-240329": [ + "ETH/USDT:USDT-240628": [ { "tier": 1.0, "currency": "USDT", @@ -13285,104 +12627,6 @@ } } ], - "FIL/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "FIL/USDT:USDT": [ { "tier": 1.0, @@ -13937,120 +13181,6 @@ } } ], - "FTM/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "5", - "initialLeverage": "4", - "notionalCap": "1500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1500000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1500000", - "maintMarginRatio": "0.25", - "cum": "199400.0" - } - }, - { - "tier": 7.0, - "currency": "BUSD", - "minNotional": 3000000.0, - "maxNotional": 4000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "4000000", - "notionalFloor": "3000000", - "maintMarginRatio": "0.5", - "cum": "949400.0" - } - } - ], "FTM/USDT:USDT": [ { "tier": 1.0, @@ -14181,104 +13311,6 @@ } } ], - "FTT/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 15000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "15000", - "notionalFloor": "0", - "maintMarginRatio": "0.025", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 15000.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "2", - "initialLeverage": "6", - "notionalCap": "50000", - "notionalFloor": "15000", - "maintMarginRatio": "0.05", - "cum": "375.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 50000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "3", - "initialLeverage": "5", - "notionalCap": "200000", - "notionalFloor": "50000", - "maintMarginRatio": "0.1", - "cum": "2875.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 200000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, - "info": { - "bracket": "4", - "initialLeverage": "3", - "notionalCap": "500000", - "notionalFloor": "200000", - "maintMarginRatio": "0.15", - "cum": "12875.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 500000.0, - "maxNotional": 800000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "800000", - "notionalFloor": "500000", - "maintMarginRatio": "0.25", - "cum": "62875.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 800000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "800000", - "maintMarginRatio": "0.5", - "cum": "262875.0" - } - } - ], "FTT/USDT:USDT": [ { "tier": 1.0, @@ -14459,104 +13491,6 @@ } } ], - "GAL/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.5", - "cum": "199400.0" - } - } - ], "GAL/USDT:USDT": [ { "tier": 1.0, @@ -14655,104 +13589,6 @@ } } ], - "GALA/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "GALA/USDT:USDT": [ { "tier": 1.0, @@ -15095,104 +13931,6 @@ } } ], - "GMT/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "GMT/USDT:USDT": [ { "tier": 1.0, @@ -16224,13 +14962,13 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, + "maxNotional": 50000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 15.0, "info": { "bracket": "2", "initialLeverage": "15", - "notionalCap": "25000", + "notionalCap": "50000", "notionalFloor": "5000", "maintMarginRatio": "0.025", "cum": "25.0" @@ -16239,39 +14977,39 @@ { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, + "minNotional": 50000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", + "notionalCap": "200000", + "notionalFloor": "50000", "maintMarginRatio": "0.05", - "cum": "650.0" + "cum": "1275.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "11275.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 2.0, @@ -16279,9 +15017,9 @@ "bracket": "5", "initialLeverage": "2", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "23775.0" } }, { @@ -16297,7 +15035,7 @@ "notionalCap": "5000000", "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "398775.0" } } ], @@ -17817,6 +16555,120 @@ } } ], + "JUP/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "KAS/USDT:USDT": [ { "tier": 1.0, @@ -18469,104 +17321,6 @@ } } ], - "LDO/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "LDO/USDT:USDT": [ { "tier": 1.0, @@ -18697,104 +17451,6 @@ } } ], - "LEVER/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.023, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.023", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "10.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "635.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5635.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11885.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386885.0" - } - } - ], "LEVER/USDT:USDT": [ { "tier": 1.0, @@ -19023,120 +17679,6 @@ } } ], - "LINK/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "5", - "initialLeverage": "4", - "notionalCap": "1500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1500000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1500000", - "maintMarginRatio": "0.25", - "cum": "199400.0" - } - }, - { - "tier": 7.0, - "currency": "BUSD", - "minNotional": 3000000.0, - "maxNotional": 4000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "4000000", - "notionalFloor": "3000000", - "maintMarginRatio": "0.5", - "cum": "949400.0" - } - } - ], "LINK/USDT:USDT": [ { "tier": 1.0, @@ -19789,117 +18331,117 @@ } } ], - "LTC/BUSD:BUSD": [ + "LSK/USDT:USDT": [ { "tier": 1.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, "info": { "bracket": "1", - "initialLeverage": "10", + "initialLeverage": "50", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.015", "cum": "0.0" } }, { "tier": 2.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 5000.0, "maxNotional": 25000.0, "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, + "maxLeverage": 20.0, "info": { "bracket": "2", - "initialLeverage": "8", + "initialLeverage": "20", "notionalCap": "25000", "notionalFloor": "5000", "maintMarginRatio": "0.025", - "cum": "25.0" + "cum": "50.0" } }, { "tier": 3.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 25000.0, "maxNotional": 100000.0, "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, + "maxLeverage": 10.0, "info": { "bracket": "3", - "initialLeverage": "6", + "initialLeverage": "10", "notionalCap": "100000", "notionalFloor": "25000", "maintMarginRatio": "0.05", - "cum": "650.0" + "cum": "675.0" } }, { "tier": 4.0, - "currency": "BUSD", + "currency": "USDT", "minNotional": 100000.0, - "maxNotional": 250000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", + "notionalCap": "200000", "notionalFloor": "100000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "5675.0" } }, { "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1500000.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "5", "initialLeverage": "4", - "notionalCap": "1500000", - "notionalFloor": "250000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "10675.0" } }, { "tier": 6.0, - "currency": "BUSD", - "minNotional": 1500000.0, - "maxNotional": 3000000.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "6", "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1500000", + "notionalCap": "1000000", + "notionalFloor": "500000", "maintMarginRatio": "0.25", - "cum": "199400.0" + "cum": "73175.0" } }, { "tier": 7.0, - "currency": "BUSD", - "minNotional": 3000000.0, - "maxNotional": 3200000.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "7", "initialLeverage": "1", - "notionalCap": "3200000", - "notionalFloor": "3000000", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.5", - "cum": "949400.0" + "cum": "323175.0" } } ], @@ -20407,6 +18949,120 @@ } } ], + "MANTA/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "MASK/USDT:USDT": [ { "tier": 1.0, @@ -20537,120 +19193,6 @@ } } ], - "MATIC/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "5", - "initialLeverage": "4", - "notionalCap": "1500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1500000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1500000", - "maintMarginRatio": "0.25", - "cum": "199400.0" - } - }, - { - "tier": 7.0, - "currency": "BUSD", - "minNotional": 3000000.0, - "maxNotional": 3500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "3500000", - "notionalFloor": "3000000", - "maintMarginRatio": "0.5", - "cum": "949400.0" - } - } - ], "MATIC/USDT:USDT": [ { "tier": 1.0, @@ -21497,6 +20039,120 @@ } } ], + "MOVR/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "MTL/USDT:USDT": [ { "tier": 1.0, @@ -21611,104 +20267,6 @@ } } ], - "NEAR/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "NEAR/USDT:USDT": [ { "tier": 1.0, @@ -21730,13 +20288,13 @@ "tier": 2.0, "currency": "USDT", "minNotional": 10000.0, - "maxNotional": 150000.0, + "maxNotional": 250000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "2", "initialLeverage": "20", - "notionalCap": "150000", + "notionalCap": "250000", "notionalFloor": "10000", "maintMarginRatio": "0.025", "cum": "100.0" @@ -21745,97 +20303,97 @@ { "tier": 3.0, "currency": "USDT", - "minNotional": 150000.0, - "maxNotional": 250000.0, + "minNotional": 250000.0, + "maxNotional": 750000.0, "maintenanceMarginRate": 0.03, "maxLeverage": 15.0, "info": { "bracket": "3", "initialLeverage": "15", - "notionalCap": "250000", - "notionalFloor": "150000", + "notionalCap": "750000", + "notionalFloor": "250000", "maintMarginRatio": "0.03", - "cum": "850.0" + "cum": "1350.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 600000.0, + "minNotional": 750000.0, + "maxNotional": 1500000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "600000", - "notionalFloor": "250000", + "notionalCap": "1500000", + "notionalFloor": "750000", "maintMarginRatio": "0.05", - "cum": "5850.0" + "cum": "16350.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 600000.0, - "maxNotional": 1600000.0, + "minNotional": 1500000.0, + "maxNotional": 4000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "1600000", - "notionalFloor": "600000", + "notionalCap": "4000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.1", - "cum": "35850.0" + "cum": "91350.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 1600000.0, - "maxNotional": 2000000.0, + "minNotional": 4000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "6", "initialLeverage": "4", - "notionalCap": "2000000", - "notionalFloor": "1600000", + "notionalCap": "5000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.125", - "cum": "75850.0" + "cum": "191350.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 6000000.0, + "minNotional": 5000000.0, + "maxNotional": 12000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "7", "initialLeverage": "2", - "notionalCap": "6000000", - "notionalFloor": "2000000", + "notionalCap": "12000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.25", - "cum": "325850.0" + "cum": "816350.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 6000000.0, - "maxNotional": 10000000.0, + "minNotional": 12000000.0, + "maxNotional": 20000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "8", "initialLeverage": "1", - "notionalCap": "10000000", - "notionalFloor": "6000000", + "notionalCap": "20000000", + "notionalFloor": "12000000", "maintMarginRatio": "0.5", - "cum": "1825850.0" + "cum": "3816350.0" } } ], @@ -21969,6 +20527,120 @@ } } ], + "NFP/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "NKN/USDT:USDT": [ { "tier": 1.0, @@ -22605,6 +21277,120 @@ } } ], + "ONDO/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "ONE/USDT:USDT": [ { "tier": 1.0, @@ -22936,13 +21722,13 @@ "tier": 1.0, "currency": "USDT", "minNotional": 0.0, - "maxNotional": 5000.0, + "maxNotional": 10000.0, "maintenanceMarginRate": 0.006, "maxLeverage": 50.0, "info": { "bracket": "1", "initialLeverage": "50", - "notionalCap": "5000", + "notionalCap": "10000", "notionalFloor": "0", "maintMarginRatio": "0.006", "cum": "0.0" @@ -22951,7 +21737,7 @@ { "tier": 2.0, "currency": "USDT", - "minNotional": 5000.0, + "minNotional": 10000.0, "maxNotional": 50000.0, "maintenanceMarginRate": 0.01, "maxLeverage": 25.0, @@ -22959,9 +21745,9 @@ "bracket": "2", "initialLeverage": "25", "notionalCap": "50000", - "notionalFloor": "5000", + "notionalFloor": "10000", "maintMarginRatio": "0.01", - "cum": "20.0" + "cum": "40.0" } }, { @@ -22977,87 +21763,87 @@ "notionalCap": "600000", "notionalFloor": "50000", "maintMarginRatio": "0.025", - "cum": "770.0" + "cum": "790.0" } }, { "tier": 4.0, "currency": "USDT", "minNotional": 600000.0, - "maxNotional": 1200000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "1200000", + "notionalCap": "2000000", "notionalFloor": "600000", "maintMarginRatio": "0.05", - "cum": "15770.0" + "cum": "15790.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 1200000.0, - "maxNotional": 3000000.0, + "minNotional": 2000000.0, + "maxNotional": 5000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "3000000", - "notionalFloor": "1200000", + "notionalCap": "5000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.1", - "cum": "75770.0" + "cum": "115790.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 3000000.0, - "maxNotional": 5000000.0, + "minNotional": 5000000.0, + "maxNotional": 7000000.0, "maintenanceMarginRate": 0.125, "maxLeverage": 4.0, "info": { "bracket": "6", "initialLeverage": "4", - "notionalCap": "5000000", - "notionalFloor": "3000000", + "notionalCap": "7000000", + "notionalFloor": "5000000", "maintMarginRatio": "0.125", - "cum": "150770.0" + "cum": "240790.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 5000000.0, - "maxNotional": 12000000.0, + "minNotional": 7000000.0, + "maxNotional": 18000000.0, "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "7", "initialLeverage": "2", - "notionalCap": "12000000", - "notionalFloor": "5000000", + "notionalCap": "18000000", + "notionalFloor": "7000000", "maintMarginRatio": "0.25", - "cum": "775770.0" + "cum": "1115790.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 12000000.0, - "maxNotional": 20000000.0, + "minNotional": 18000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "8", "initialLeverage": "1", - "notionalCap": "20000000", - "notionalFloor": "12000000", + "notionalCap": "30000000", + "notionalFloor": "18000000", "maintMarginRatio": "0.5", - "cum": "3775770.0" + "cum": "5615790.0" } } ], @@ -23181,14 +21967,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 5000.0, - "maintenanceMarginRate": 0.015, - "maxLeverage": 50.0, + "maintenanceMarginRate": 0.0065, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "50", + "initialLeverage": "75", "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.015", + "maintMarginRatio": "0.0065", "cum": "0.0" } }, @@ -23196,112 +21982,144 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "maxNotional": 75000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, "info": { "bracket": "2", - "initialLeverage": "25", - "notionalCap": "50000", + "initialLeverage": "50", + "notionalCap": "75000", "notionalFloor": "5000", - "maintMarginRatio": "0.02", - "cum": "25.0" + "maintMarginRatio": "0.01", + "cum": "17.5" } }, { "tier": 3.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 400000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "minNotional": 75000.0, + "maxNotional": 150000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 40.0, "info": { "bracket": "3", - "initialLeverage": "20", - "notionalCap": "400000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "275.0" + "initialLeverage": "40", + "notionalCap": "150000", + "notionalFloor": "75000", + "maintMarginRatio": "0.015", + "cum": "392.5" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 400000.0, - "maxNotional": 800000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "minNotional": 150000.0, + "maxNotional": 300000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "4", - "initialLeverage": "10", - "notionalCap": "800000", - "notionalFloor": "400000", - "maintMarginRatio": "0.05", - "cum": "10275.0" + "initialLeverage": "25", + "notionalCap": "300000", + "notionalFloor": "150000", + "maintMarginRatio": "0.02", + "cum": "1142.5" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 800000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "minNotional": 300000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "5", - "initialLeverage": "5", - "notionalCap": "2000000", - "notionalFloor": "800000", - "maintMarginRatio": "0.1", - "cum": "50275.0" + "initialLeverage": "20", + "notionalCap": "1000000", + "notionalFloor": "300000", + "maintMarginRatio": "0.025", + "cum": "2642.5" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 2000000.0, - "maxNotional": 4000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 1000000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "6", - "initialLeverage": "4", - "notionalCap": "4000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.125", - "cum": "100275.0" + "initialLeverage": "10", + "notionalCap": "3000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.05", + "cum": "27642.5" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 4000000.0, - "maxNotional": 8000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 3000000.0, + "maxNotional": 6000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "7", - "initialLeverage": "2", - "notionalCap": "8000000", - "notionalFloor": "4000000", - "maintMarginRatio": "0.25", - "cum": "600275.0" + "initialLeverage": "5", + "notionalCap": "6000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.1", + "cum": "177642.5" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 8000000.0, - "maxNotional": 15000000.0, + "minNotional": 6000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "8", + "initialLeverage": "4", + "notionalCap": "10000000", + "notionalFloor": "6000000", + "maintMarginRatio": "0.125", + "cum": "327642.5" + } + }, + { + "tier": 9.0, + "currency": "USDT", + "minNotional": 10000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "20000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.25", + "cum": "1577642.5" + } + }, + { + "tier": 10.0, + "currency": "USDT", + "minNotional": 20000000.0, + "maxNotional": 30000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "8", + "bracket": "10", "initialLeverage": "1", - "notionalCap": "15000000", - "notionalFloor": "8000000", + "notionalCap": "30000000", + "notionalFloor": "20000000", "maintMarginRatio": "0.5", - "cum": "2600275.0" + "cum": "6577642.5" } } ], @@ -23761,104 +22579,6 @@ } } ], - "PHB/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "PHB/USDT:USDT": [ { "tier": 1.0, @@ -25298,13 +24018,13 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, + "maxNotional": 50000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 15.0, "info": { "bracket": "2", "initialLeverage": "15", - "notionalCap": "25000", + "notionalCap": "50000", "notionalFloor": "5000", "maintMarginRatio": "0.025", "cum": "25.0" @@ -25313,65 +24033,81 @@ { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 100000.0, + "minNotional": 50000.0, + "maxNotional": 200000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "3", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "25000", + "notionalCap": "200000", + "notionalFloor": "50000", "maintMarginRatio": "0.05", - "cum": "650.0" + "cum": "1275.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "4", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "500000", + "notionalFloor": "200000", "maintMarginRatio": "0.1", - "cum": "5650.0" + "cum": "11275.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 250000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "5", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", - "notionalFloor": "250000", + "notionalFloor": "500000", "maintMarginRatio": "0.125", - "cum": "11900.0" + "cum": "23775.0" } }, { "tier": 6.0, "currency": "USDT", "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "148775.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 2000000.0, "maxNotional": 5000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "7", "initialLeverage": "1", "notionalCap": "5000000", - "notionalFloor": "1000000", + "notionalFloor": "2000000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "648775.0" } } ], @@ -25815,104 +24551,6 @@ } } ], - "SAND/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "SAND/USDT:USDT": [ { "tier": 1.0, @@ -26809,117 +25447,181 @@ } } ], - "SOL/BUSD:BUSD": [ + "SOL/USDC:USDC": [ { "tier": 1.0, - "currency": "BUSD", + "currency": "USDC", "minNotional": 0.0, - "maxNotional": 50000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 100.0, "info": { "bracket": "1", - "initialLeverage": "10", - "notionalCap": "50000", + "initialLeverage": "100", + "notionalCap": "10000", "notionalFloor": "0", - "maintMarginRatio": "0.02", + "maintMarginRatio": "0.005", "cum": "0.0" } }, { "tier": 2.0, - "currency": "BUSD", - "minNotional": 50000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, + "currency": "USDC", + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.0065, + "maxLeverage": 75.0, "info": { "bracket": "2", - "initialLeverage": "8", - "notionalCap": "100000", - "notionalFloor": "50000", - "maintMarginRatio": "0.025", - "cum": "250.0" + "initialLeverage": "75", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.0065", + "cum": "15.0" } }, { "tier": 3.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, + "currency": "USDC", + "minNotional": 50000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, "info": { "bracket": "3", - "initialLeverage": "6", - "notionalCap": "500000", - "notionalFloor": "100000", - "maintMarginRatio": "0.05", - "cum": "2750.0" + "initialLeverage": "50", + "notionalCap": "200000", + "notionalFloor": "50000", + "maintMarginRatio": "0.01", + "cum": "190.0" } }, { "tier": 4.0, - "currency": "BUSD", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "currency": "USDC", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.012, + "maxLeverage": 40.0, "info": { "bracket": "4", - "initialLeverage": "5", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.1", - "cum": "27750.0" + "initialLeverage": "40", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.012", + "cum": "590.0" } }, { "tier": 5.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, + "currency": "USDC", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "5", - "initialLeverage": "3", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.15", - "cum": "77750.0" + "initialLeverage": "25", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.02", + "cum": "4590.0" } }, { "tier": 6.0, - "currency": "BUSD", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "currency": "USDC", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.25", - "cum": "277750.0" + "initialLeverage": "20", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.025", + "cum": "9590.0" } }, { "tier": 7.0, - "currency": "BUSD", + "currency": "USDC", + "minNotional": 2000000.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "7", + "initialLeverage": "10", + "notionalCap": "5000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.05", + "cum": "59590.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", "minNotional": 5000000.0, - "maxNotional": 5500000.0, + "maxNotional": 15000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "8", + "initialLeverage": "5", + "notionalCap": "15000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.1", + "cum": "309590.0" + } + }, + { + "tier": 9.0, + "currency": "USDC", + "minNotional": 15000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "9", + "initialLeverage": "4", + "notionalCap": "20000000", + "notionalFloor": "15000000", + "maintMarginRatio": "0.125", + "cum": "684590.0" + } + }, + { + "tier": 10.0, + "currency": "USDC", + "minNotional": 20000000.0, + "maxNotional": 50000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "10", + "initialLeverage": "2", + "notionalCap": "50000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "3184590.0" + } + }, + { + "tier": 11.0, + "currency": "USDC", + "minNotional": 50000000.0, + "maxNotional": 100000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "11", "initialLeverage": "1", - "notionalCap": "5500000", - "notionalFloor": "5000000", + "notionalCap": "100000000", + "notionalFloor": "50000000", "maintMarginRatio": "0.5", - "cum": "1527750.0" + "cum": "15684590.0" } } ], @@ -26929,14 +25631,14 @@ "currency": "USDT", "minNotional": 0.0, "maxNotional": 10000.0, - "maintenanceMarginRate": 0.0065, - "maxLeverage": 75.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 100.0, "info": { "bracket": "1", - "initialLeverage": "75", + "initialLeverage": "100", "notionalCap": "10000", "notionalFloor": "0", - "maintMarginRatio": "0.0065", + "maintMarginRatio": "0.005", "cum": "0.0" } }, @@ -26945,63 +25647,63 @@ "currency": "USDT", "minNotional": 10000.0, "maxNotional": 50000.0, - "maintenanceMarginRate": 0.01, - "maxLeverage": 50.0, + "maintenanceMarginRate": 0.0065, + "maxLeverage": 75.0, "info": { "bracket": "2", - "initialLeverage": "50", + "initialLeverage": "75", "notionalCap": "50000", "notionalFloor": "10000", - "maintMarginRatio": "0.01", - "cum": "35.0" + "maintMarginRatio": "0.0065", + "cum": "15.0" } }, { "tier": 3.0, "currency": "USDT", "minNotional": 50000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.012, - "maxLeverage": 40.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 50.0, "info": { "bracket": "3", - "initialLeverage": "40", - "notionalCap": "100000", + "initialLeverage": "50", + "notionalCap": "200000", "notionalFloor": "50000", - "maintMarginRatio": "0.012", - "cum": "135.0" + "maintMarginRatio": "0.01", + "cum": "190.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 200000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 25.0, + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.012, + "maxLeverage": 40.0, "info": { "bracket": "4", - "initialLeverage": "25", - "notionalCap": "200000", - "notionalFloor": "100000", - "maintMarginRatio": "0.02", - "cum": "935.0" + "initialLeverage": "40", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.012", + "cum": "590.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 200000.0, + "minNotional": 500000.0, "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 20.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "5", - "initialLeverage": "20", + "initialLeverage": "25", "notionalCap": "1000000", - "notionalFloor": "200000", - "maintMarginRatio": "0.025", - "cum": "1935.0" + "notionalFloor": "500000", + "maintMarginRatio": "0.02", + "cum": "4590.0" } }, { @@ -27009,79 +25711,95 @@ "currency": "USDT", "minNotional": 1000000.0, "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, "info": { "bracket": "6", - "initialLeverage": "10", + "initialLeverage": "20", "notionalCap": "2000000", "notionalFloor": "1000000", - "maintMarginRatio": "0.05", - "cum": "26935.0" + "maintMarginRatio": "0.025", + "cum": "9590.0" } }, { "tier": 7.0, "currency": "USDT", "minNotional": 2000000.0, - "maxNotional": 4800000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "maxNotional": 5000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "7", - "initialLeverage": "5", - "notionalCap": "4800000", + "initialLeverage": "10", + "notionalCap": "5000000", "notionalFloor": "2000000", - "maintMarginRatio": "0.1", - "cum": "126935.0" + "maintMarginRatio": "0.05", + "cum": "59590.0" } }, { "tier": 8.0, "currency": "USDT", - "minNotional": 4800000.0, - "maxNotional": 6000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, + "minNotional": 5000000.0, + "maxNotional": 15000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, "info": { "bracket": "8", - "initialLeverage": "4", - "notionalCap": "6000000", - "notionalFloor": "4800000", - "maintMarginRatio": "0.125", - "cum": "246935.0" + "initialLeverage": "5", + "notionalCap": "15000000", + "notionalFloor": "5000000", + "maintMarginRatio": "0.1", + "cum": "309590.0" } }, { "tier": 9.0, "currency": "USDT", - "minNotional": 6000000.0, - "maxNotional": 18000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "minNotional": 15000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, "info": { "bracket": "9", - "initialLeverage": "2", - "notionalCap": "18000000", - "notionalFloor": "6000000", - "maintMarginRatio": "0.25", - "cum": "996935.0" + "initialLeverage": "4", + "notionalCap": "20000000", + "notionalFloor": "15000000", + "maintMarginRatio": "0.125", + "cum": "684590.0" } }, { "tier": 10.0, "currency": "USDT", - "minNotional": 18000000.0, - "maxNotional": 30000000.0, + "minNotional": 20000000.0, + "maxNotional": 50000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "10", + "initialLeverage": "2", + "notionalCap": "50000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "3184590.0" + } + }, + { + "tier": 11.0, + "currency": "USDT", + "minNotional": 50000000.0, + "maxNotional": 100000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "10", + "bracket": "11", "initialLeverage": "1", - "notionalCap": "30000000", - "notionalFloor": "18000000", + "notionalCap": "100000000", + "notionalFloor": "50000000", "maintMarginRatio": "0.5", - "cum": "5496935.0" + "cum": "15684590.0" } } ], @@ -28959,104 +27677,6 @@ } } ], - "TLM/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386900.0" - } - } - ], "TLM/USDT:USDT": [ { "tier": 1.0, @@ -29404,13 +28024,13 @@ "tier": 2.0, "currency": "USDT", "minNotional": 5000.0, - "maxNotional": 25000.0, + "maxNotional": 50000.0, "maintenanceMarginRate": 0.02, "maxLeverage": 25.0, "info": { "bracket": "2", "initialLeverage": "25", - "notionalCap": "25000", + "notionalCap": "50000", "notionalFloor": "5000", "maintMarginRatio": "0.02", "cum": "25.0" @@ -29419,81 +28039,97 @@ { "tier": 3.0, "currency": "USDT", - "minNotional": 25000.0, - "maxNotional": 50000.0, + "minNotional": 50000.0, + "maxNotional": 100000.0, "maintenanceMarginRate": 0.025, "maxLeverage": 20.0, "info": { "bracket": "3", "initialLeverage": "20", - "notionalCap": "50000", - "notionalFloor": "25000", + "notionalCap": "100000", + "notionalFloor": "50000", "maintMarginRatio": "0.025", - "cum": "150.0" + "cum": "275.0" } }, { "tier": 4.0, "currency": "USDT", - "minNotional": 50000.0, - "maxNotional": 100000.0, + "minNotional": 100000.0, + "maxNotional": 400000.0, "maintenanceMarginRate": 0.05, "maxLeverage": 10.0, "info": { "bracket": "4", "initialLeverage": "10", - "notionalCap": "100000", - "notionalFloor": "50000", + "notionalCap": "400000", + "notionalFloor": "100000", "maintMarginRatio": "0.05", - "cum": "1400.0" + "cum": "2775.0" } }, { "tier": 5.0, "currency": "USDT", - "minNotional": 100000.0, - "maxNotional": 250000.0, + "minNotional": 400000.0, + "maxNotional": 1000000.0, "maintenanceMarginRate": 0.1, "maxLeverage": 5.0, "info": { "bracket": "5", "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", + "notionalCap": "1000000", + "notionalFloor": "400000", "maintMarginRatio": "0.1", - "cum": "6400.0" + "cum": "22775.0" } }, { "tier": 6.0, "currency": "USDT", - "minNotional": 250000.0, - "maxNotional": 1000000.0, + "minNotional": 1000000.0, + "maxNotional": 2000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "6", - "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", + "initialLeverage": "4", + "notionalCap": "2000000", + "notionalFloor": "1000000", "maintMarginRatio": "0.125", - "cum": "12650.0" + "cum": "47775.0" } }, { "tier": 7.0, "currency": "USDT", - "minNotional": 1000000.0, - "maxNotional": 3000000.0, + "minNotional": 2000000.0, + "maxNotional": 4000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "7", + "initialLeverage": "2", + "notionalCap": "4000000", + "notionalFloor": "2000000", + "maintMarginRatio": "0.25", + "cum": "297775.0" + } + }, + { + "tier": 8.0, + "currency": "USDT", + "minNotional": 4000000.0, + "maxNotional": 8000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "7", + "bracket": "8", "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", + "notionalCap": "8000000", + "notionalFloor": "4000000", "maintMarginRatio": "0.5", - "cum": "387650.0" + "cum": "1297775.0" } } ], @@ -29611,120 +28247,6 @@ } } ], - "TRX/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 4.0, - "info": { - "bracket": "5", - "initialLeverage": "4", - "notionalCap": "1500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 1500000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, - "info": { - "bracket": "6", - "initialLeverage": "2", - "notionalCap": "3000000", - "notionalFloor": "1500000", - "maintMarginRatio": "0.25", - "cum": "199400.0" - } - }, - { - "tier": 7.0, - "currency": "BUSD", - "minNotional": 3000000.0, - "maxNotional": 4000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "7", - "initialLeverage": "1", - "notionalCap": "4000000", - "notionalFloor": "3000000", - "maintMarginRatio": "0.5", - "cum": "949400.0" - } - } - ], "TRX/USDT:USDT": [ { "tier": 1.0, @@ -30154,10 +28676,10 @@ "minNotional": 250000.0, "maxNotional": 1000000.0, "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, + "maxLeverage": 4.0, "info": { "bracket": "4", - "initialLeverage": "2", + "initialLeverage": "4", "notionalCap": "1000000", "notionalFloor": "250000", "maintMarginRatio": "0.125", @@ -30168,114 +28690,32 @@ "tier": 5.0, "currency": "USDT", "minNotional": 1000000.0, - "maxNotional": 3000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "5", - "initialLeverage": "1", - "notionalCap": "3000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.5", - "cum": "386875.0" - } - } - ], - "UNI/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 8.0, - "info": { - "bracket": "1", - "initialLeverage": "8", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 7.0, - "info": { - "bracket": "2", - "initialLeverage": "7", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.125, + "maxNotional": 1500000.0, + "maintenanceMarginRate": 0.25, "maxLeverage": 2.0, "info": { "bracket": "5", "initialLeverage": "2", - "notionalCap": "1000000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" + "notionalCap": "1500000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.25", + "cum": "136875.0" } }, { "tier": 6.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 1500000.0, + "currency": "USDT", + "minNotional": 1500000.0, + "maxNotional": 3000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { "bracket": "6", "initialLeverage": "1", - "notionalCap": "1500000", - "notionalFloor": "1000000", + "notionalCap": "3000000", + "notionalFloor": "1500000", "maintMarginRatio": "0.5", - "cum": "386900.0" + "cum": "511875.0" } } ], @@ -30799,104 +29239,6 @@ } } ], - "WAVES/BUSD:BUSD": [ - { - "tier": 1.0, - "currency": "BUSD", - "minNotional": 0.0, - "maxNotional": 5000.0, - "maintenanceMarginRate": 0.02, - "maxLeverage": 10.0, - "info": { - "bracket": "1", - "initialLeverage": "10", - "notionalCap": "5000", - "notionalFloor": "0", - "maintMarginRatio": "0.02", - "cum": "0.0" - } - }, - { - "tier": 2.0, - "currency": "BUSD", - "minNotional": 5000.0, - "maxNotional": 25000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 8.0, - "info": { - "bracket": "2", - "initialLeverage": "8", - "notionalCap": "25000", - "notionalFloor": "5000", - "maintMarginRatio": "0.025", - "cum": "25.0" - } - }, - { - "tier": 3.0, - "currency": "BUSD", - "minNotional": 25000.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 6.0, - "info": { - "bracket": "3", - "initialLeverage": "6", - "notionalCap": "100000", - "notionalFloor": "25000", - "maintMarginRatio": "0.05", - "cum": "650.0" - } - }, - { - "tier": 4.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 250000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, - "info": { - "bracket": "4", - "initialLeverage": "5", - "notionalCap": "250000", - "notionalFloor": "100000", - "maintMarginRatio": "0.1", - "cum": "5650.0" - } - }, - { - "tier": 5.0, - "currency": "BUSD", - "minNotional": 250000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.125, - "maxLeverage": 2.0, - "info": { - "bracket": "5", - "initialLeverage": "2", - "notionalCap": "500000", - "notionalFloor": "250000", - "maintMarginRatio": "0.125", - "cum": "11900.0" - } - }, - { - "tier": 6.0, - "currency": "BUSD", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.5, - "maxLeverage": 1.0, - "info": { - "bracket": "6", - "initialLeverage": "1", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.5", - "cum": "199400.0" - } - } - ], "WAVES/USDT:USDT": [ { "tier": 1.0, @@ -31125,6 +29467,120 @@ } } ], + "WIF/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "WLD/USDT:USDT": [ { "tier": 1.0, @@ -31353,6 +29809,120 @@ } } ], + "XAI/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "XEM/USDT:USDT": [ { "tier": 1.0, @@ -31727,101 +30297,165 @@ } } ], - "XRP/BUSD:BUSD": [ + "XRP/USDC:USDC": [ { "tier": 1.0, - "currency": "BUSD", + "currency": "USDC", "minNotional": 0.0, - "maxNotional": 100000.0, - "maintenanceMarginRate": 0.025, - "maxLeverage": 11.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.005, + "maxLeverage": 75.0, "info": { "bracket": "1", - "initialLeverage": "11", - "notionalCap": "100000", + "initialLeverage": "75", + "notionalCap": "5000", "notionalFloor": "0", - "maintMarginRatio": "0.025", + "maintMarginRatio": "0.005", "cum": "0.0" } }, { "tier": 2.0, - "currency": "BUSD", - "minNotional": 100000.0, - "maxNotional": 500000.0, - "maintenanceMarginRate": 0.05, - "maxLeverage": 10.0, + "currency": "USDC", + "minNotional": 5000.0, + "maxNotional": 10000.0, + "maintenanceMarginRate": 0.006, + "maxLeverage": 50.0, "info": { "bracket": "2", - "initialLeverage": "10", - "notionalCap": "500000", - "notionalFloor": "100000", - "maintMarginRatio": "0.05", - "cum": "2500.0" + "initialLeverage": "50", + "notionalCap": "10000", + "notionalFloor": "5000", + "maintMarginRatio": "0.006", + "cum": "5.0" } }, { "tier": 3.0, - "currency": "BUSD", - "minNotional": 500000.0, - "maxNotional": 1000000.0, - "maintenanceMarginRate": 0.1, - "maxLeverage": 5.0, + "currency": "USDC", + "minNotional": 10000.0, + "maxNotional": 50000.0, + "maintenanceMarginRate": 0.01, + "maxLeverage": 40.0, "info": { "bracket": "3", - "initialLeverage": "5", - "notionalCap": "1000000", - "notionalFloor": "500000", - "maintMarginRatio": "0.1", - "cum": "27500.0" + "initialLeverage": "40", + "notionalCap": "50000", + "notionalFloor": "10000", + "maintMarginRatio": "0.01", + "cum": "45.0" } }, { "tier": 4.0, - "currency": "BUSD", - "minNotional": 1000000.0, - "maxNotional": 2000000.0, - "maintenanceMarginRate": 0.15, - "maxLeverage": 3.0, + "currency": "USDC", + "minNotional": 50000.0, + "maxNotional": 750000.0, + "maintenanceMarginRate": 0.02, + "maxLeverage": 25.0, "info": { "bracket": "4", - "initialLeverage": "3", - "notionalCap": "2000000", - "notionalFloor": "1000000", - "maintMarginRatio": "0.15", - "cum": "77500.0" + "initialLeverage": "25", + "notionalCap": "750000", + "notionalFloor": "50000", + "maintMarginRatio": "0.02", + "cum": "545.0" } }, { "tier": 5.0, - "currency": "BUSD", - "minNotional": 2000000.0, - "maxNotional": 5000000.0, - "maintenanceMarginRate": 0.25, - "maxLeverage": 2.0, + "currency": "USDC", + "minNotional": 750000.0, + "maxNotional": 3000000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, "info": { "bracket": "5", - "initialLeverage": "2", - "notionalCap": "5000000", - "notionalFloor": "2000000", - "maintMarginRatio": "0.25", - "cum": "277500.0" + "initialLeverage": "10", + "notionalCap": "3000000", + "notionalFloor": "750000", + "maintMarginRatio": "0.05", + "cum": "23045.0" } }, { "tier": 6.0, - "currency": "BUSD", - "minNotional": 5000000.0, - "maxNotional": 5500000.0, + "currency": "USDC", + "minNotional": 3000000.0, + "maxNotional": 10000000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "6", + "initialLeverage": "5", + "notionalCap": "10000000", + "notionalFloor": "3000000", + "maintMarginRatio": "0.1", + "cum": "173045.0" + } + }, + { + "tier": 7.0, + "currency": "USDC", + "minNotional": 10000000.0, + "maxNotional": 12000000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "7", + "initialLeverage": "4", + "notionalCap": "12000000", + "notionalFloor": "10000000", + "maintMarginRatio": "0.125", + "cum": "423045.0" + } + }, + { + "tier": 8.0, + "currency": "USDC", + "minNotional": 12000000.0, + "maxNotional": 20000000.0, + "maintenanceMarginRate": 0.15, + "maxLeverage": 3.0, + "info": { + "bracket": "8", + "initialLeverage": "3", + "notionalCap": "20000000", + "notionalFloor": "12000000", + "maintMarginRatio": "0.15", + "cum": "723045.0" + } + }, + { + "tier": 9.0, + "currency": "USDC", + "minNotional": 20000000.0, + "maxNotional": 30000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "9", + "initialLeverage": "2", + "notionalCap": "30000000", + "notionalFloor": "20000000", + "maintMarginRatio": "0.25", + "cum": "2723045.0" + } + }, + { + "tier": 10.0, + "currency": "USDC", + "minNotional": 30000000.0, + "maxNotional": 50000000.0, "maintenanceMarginRate": 0.5, "maxLeverage": 1.0, "info": { - "bracket": "6", + "bracket": "10", "initialLeverage": "1", - "notionalCap": "5500000", - "notionalFloor": "5000000", + "notionalCap": "50000000", + "notionalFloor": "30000000", "maintMarginRatio": "0.5", - "cum": "1527500.0" + "cum": "10223045.0" } } ], @@ -32769,6 +31403,120 @@ } } ], + "ZETA/USDT:USDT": [ + { + "tier": 1.0, + "currency": "USDT", + "minNotional": 0.0, + "maxNotional": 5000.0, + "maintenanceMarginRate": 0.015, + "maxLeverage": 50.0, + "info": { + "bracket": "1", + "initialLeverage": "50", + "notionalCap": "5000", + "notionalFloor": "0", + "maintMarginRatio": "0.015", + "cum": "0.0" + } + }, + { + "tier": 2.0, + "currency": "USDT", + "minNotional": 5000.0, + "maxNotional": 25000.0, + "maintenanceMarginRate": 0.025, + "maxLeverage": 20.0, + "info": { + "bracket": "2", + "initialLeverage": "20", + "notionalCap": "25000", + "notionalFloor": "5000", + "maintMarginRatio": "0.025", + "cum": "50.0" + } + }, + { + "tier": 3.0, + "currency": "USDT", + "minNotional": 25000.0, + "maxNotional": 100000.0, + "maintenanceMarginRate": 0.05, + "maxLeverage": 10.0, + "info": { + "bracket": "3", + "initialLeverage": "10", + "notionalCap": "100000", + "notionalFloor": "25000", + "maintMarginRatio": "0.05", + "cum": "675.0" + } + }, + { + "tier": 4.0, + "currency": "USDT", + "minNotional": 100000.0, + "maxNotional": 200000.0, + "maintenanceMarginRate": 0.1, + "maxLeverage": 5.0, + "info": { + "bracket": "4", + "initialLeverage": "5", + "notionalCap": "200000", + "notionalFloor": "100000", + "maintMarginRatio": "0.1", + "cum": "5675.0" + } + }, + { + "tier": 5.0, + "currency": "USDT", + "minNotional": 200000.0, + "maxNotional": 500000.0, + "maintenanceMarginRate": 0.125, + "maxLeverage": 4.0, + "info": { + "bracket": "5", + "initialLeverage": "4", + "notionalCap": "500000", + "notionalFloor": "200000", + "maintMarginRatio": "0.125", + "cum": "10675.0" + } + }, + { + "tier": 6.0, + "currency": "USDT", + "minNotional": 500000.0, + "maxNotional": 1000000.0, + "maintenanceMarginRate": 0.25, + "maxLeverage": 2.0, + "info": { + "bracket": "6", + "initialLeverage": "2", + "notionalCap": "1000000", + "notionalFloor": "500000", + "maintMarginRatio": "0.25", + "cum": "73175.0" + } + }, + { + "tier": 7.0, + "currency": "USDT", + "minNotional": 1000000.0, + "maxNotional": 2000000.0, + "maintenanceMarginRate": 0.5, + "maxLeverage": 1.0, + "info": { + "bracket": "7", + "initialLeverage": "1", + "notionalCap": "2000000", + "notionalFloor": "1000000", + "maintMarginRatio": "0.5", + "cum": "323175.0" + } + } + ], "ZIL/USDT:USDT": [ { "tier": 1.0, From 0f9e8465873c8da64bc87ba603aab861e8d599e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Feb 2024 13:31:24 +0100 Subject: [PATCH 086/327] Update Tests data to work with new cached levtiers --- tests/conftest.py | 4 ++-- tests/exchange/test_binance.py | 4 ++-- tests/exchange/test_exchange.py | 8 ++++---- tests/exchange/test_okx.py | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0cc3a8ea0..9c81c050d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3342,7 +3342,7 @@ def leverage_tiers(): 'maintAmt': 386950.0 }, ], - "ADA/BUSD:BUSD": [ + "ADA/USDT:USDT": [ { "minNotional": 0, "maxNotional": 100000, @@ -3386,7 +3386,7 @@ def leverage_tiers(): "maintAmt": 1527500.0 }, ], - 'BNB/BUSD:BUSD': [ + 'XRP/USDT:USDT': [ { "minNotional": 0, # stake(before leverage) = 0 "maxNotional": 100000, # max stake(before leverage) = 5000 diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index c4e657ad9..625033645 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -596,10 +596,10 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog, c @pytest.mark.parametrize('pair,nominal_value,mm_ratio,amt', [ - ("BNB/BUSD:BUSD", 0.0, 0.025, 0), + ("XRP/USDT:USDT", 0.0, 0.025, 0), ("BNB/USDT:USDT", 100.0, 0.0065, 0), ("BTC/USDT:USDT", 170.30, 0.004, 0), - ("BNB/BUSD:BUSD", 999999.9, 0.1, 27500.0), + ("XRP/USDT:USDT", 999999.9, 0.1, 27500.0), ("BNB/USDT:USDT", 5000000.0, 0.15, 233035.0), ("BTC/USDT:USDT", 600000000, 0.5, 1.997038E8), ]) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 29e458cdd..fc199a7f5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4969,8 +4969,8 @@ def test_get_maintenance_ratio_and_amt_exceptions(mocker, default_conf, leverage @pytest.mark.parametrize('pair,value,mmr,maintAmt', [ - ('ADA/BUSD:BUSD', 500, 0.025, 0.0), - ('ADA/BUSD:BUSD', 20000000, 0.5, 1527500.0), + ('ADA/USDT:USDT', 500, 0.025, 0.0), + ('ADA/USDT:USDT', 20000000, 0.5, 1527500.0), ('ZEC/USDT:USDT', 500, 0.01, 0.0), ('ZEC/USDT:USDT', 20000000, 0.5, 654500.0), ]) @@ -5005,10 +5005,10 @@ def test_get_max_leverage_futures(default_conf, mocker, leverage_tiers): exchange._leverage_tiers = leverage_tiers - assert exchange.get_max_leverage("BNB/BUSD:BUSD", 1.0) == 20.0 + assert exchange.get_max_leverage("XRP/USDT:USDT", 1.0) == 20.0 assert exchange.get_max_leverage("BNB/USDT:USDT", 100.0) == 75.0 assert exchange.get_max_leverage("BTC/USDT:USDT", 170.30) == 125.0 - assert pytest.approx(exchange.get_max_leverage("BNB/BUSD:BUSD", 99999.9)) == 5.000005 + assert pytest.approx(exchange.get_max_leverage("XRP/USDT:USDT", 99999.9)) == 5.000005 assert pytest.approx(exchange.get_max_leverage("BNB/USDT:USDT", 1500)) == 33.333333333333333 assert exchange.get_max_leverage("BTC/USDT:USDT", 300000000) == 2.0 assert exchange.get_max_leverage("BTC/USDT:USDT", 600000000) == 1.0 # Last tier diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index fe9ab3c18..73f87774e 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -196,7 +196,7 @@ def test_get_max_pair_stake_amount_okx(default_conf, mocker, leverage_tiers): exchange = get_patched_exchange(mocker, default_conf, id="okx") exchange._leverage_tiers = leverage_tiers - assert exchange.get_max_pair_stake_amount('BNB/BUSD:BUSD', 1.0) == 30000000 + assert exchange.get_max_pair_stake_amount('XRP/USDT:USDT', 1.0) == 30000000 assert exchange.get_max_pair_stake_amount('BNB/USDT:USDT', 1.0) == 50000000 assert exchange.get_max_pair_stake_amount('BTC/USDT:USDT', 1.0) == 1000000000 assert exchange.get_max_pair_stake_amount('BTC/USDT:USDT', 1.0, 10.0) == 100000000 From 35e1421d5989369ae6d700cae7113e6536f7eab0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Feb 2024 13:42:25 +0100 Subject: [PATCH 087/327] Reduce whitespace --- tests/freqtradebot/test_freqtradebot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index ca6f29078..aa037fe37 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -2923,9 +2923,6 @@ def test_execute_trade_exit_custom_exit_price( } == last_msg - - - @pytest.mark.parametrize( "is_short,amount,current_rate,limit,profit_amount,profit_ratio,profit_or_loss", [ (False, 30, 2.3, 2.2, 5.685, 0.09451372, 'profit'), From cb2aaa7bbb54c14de0015e6083967817a117a42f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Feb 2024 15:00:48 +0100 Subject: [PATCH 088/327] Fix wording fully. --- freqtrade/freqai/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqai/utils.py b/freqtrade/freqai/utils.py index bc335bf20..22d75bc16 100644 --- a/freqtrade/freqai/utils.py +++ b/freqtrade/freqai/utils.py @@ -123,7 +123,7 @@ def plot_feature_importance(model: Any, pair: str, dk: FreqaiDataKitchen, elif "xgb" in str(mdl.__class__): feature_importance = mdl.feature_importances_ else: - logger.info('Model type does not support for generating feature importances.') + logger.info('Model type does not support generating feature importances.') return # Data preparation From 69611826808e8411a40e14cc1de847fc81170a99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 03:50:09 +0000 Subject: [PATCH 089/327] Bump uvicorn from 0.27.0 to 0.27.0.post1 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.27.0 to 0.27.0.post1. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.27.0...0.27.0.post1) --- updated-dependencies: - dependency-name: uvicorn dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 535d94946..cdd09c200 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ sdnotify==0.3.2 # API Server fastapi==0.109.0 pydantic==2.5.3 -uvicorn==0.27.0 +uvicorn==0.27.0.post1 pyjwt==2.8.0 aiofiles==23.2.1 psutil==5.9.8 From ab7364c62d9ef1582d8ad49e4abd41ef199717d2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 03:50:36 +0000 Subject: [PATCH 090/327] Bump aiohttp from 3.9.2 to 3.9.3 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.9.2 to 3.9.3. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.9.2...v3.9.3) --- updated-dependencies: - dependency-name: aiohttp dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 535d94946..e11834d3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas-ta==0.3.14b ccxt==4.2.25 cryptography==42.0.1 -aiohttp==3.9.2 +aiohttp==3.9.3 SQLAlchemy==2.0.25 python-telegram-bot==20.7 # can't be hard-pinned due to telegram-bot pinning httpx with ~ From 440382ae69cacb2317076533bb942d2a73145a97 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 03:51:00 +0000 Subject: [PATCH 091/327] Bump ccxt from 4.2.25 to 4.2.35 Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.2.25 to 4.2.35. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/4.2.25...4.2.35) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 535d94946..5be77a149 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.26.3 pandas==2.1.4 pandas-ta==0.3.14b -ccxt==4.2.25 +ccxt==4.2.35 cryptography==42.0.1 aiohttp==3.9.2 SQLAlchemy==2.0.25 From 5114be42cd5450b08cb531250db7fd6e756a2552 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 03:51:09 +0000 Subject: [PATCH 092/327] Bump urllib3 from 2.1.0 to 2.2.0 Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.1.0...2.2.0) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 535d94946..5dee1f9c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ httpx>=0.24.1 arrow==1.3.0 cachetools==5.3.2 requests==2.31.0 -urllib3==2.1.0 +urllib3==2.2.0 jsonschema==4.21.1 TA-Lib==0.4.28 technical==1.4.2 From 87e813a3ddc916fd13c825ec72b808f1631d273a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 03:51:20 +0000 Subject: [PATCH 093/327] Bump mkdocs-material from 9.5.6 to 9.5.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.6 to 9.5.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.6...9.5.7) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index d6d2c29cc..c7f1e1889 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.5.2 mkdocs==1.5.3 -mkdocs-material==9.5.6 +mkdocs-material==9.5.7 mdx_truly_sane_lists==1.3 pymdown-extensions==10.7 jinja2==3.1.3 From e773276ddea5f30f38c57212f9441d12cca80887 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 03:51:36 +0000 Subject: [PATCH 094/327] Bump ruff from 0.1.15 to 0.2.0 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.1.15 to 0.2.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.1.15...v0.2.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 04d4a8563..1353d4fd0 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.1.15 +ruff==0.2.0 mypy==1.8.0 pre-commit==3.6.0 pytest==7.4.4 From 667a8cc59fdf390d92ef94451aeafdce7ab6b11f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 03:59:04 +0000 Subject: [PATCH 095/327] Bump peter-evans/create-pull-request from 5 to 6 Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 5 to 6. - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - [Commits](https://github.com/peter-evans/create-pull-request/compare/v5...v6) --- updated-dependencies: - dependency-name: peter-evans/create-pull-request dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/pre-commit-update.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pre-commit-update.yml b/.github/workflows/pre-commit-update.yml index bd50a9c3c..13188af14 100644 --- a/.github/workflows/pre-commit-update.yml +++ b/.github/workflows/pre-commit-update.yml @@ -30,7 +30,7 @@ jobs: - name: Run pre-commit run: pre-commit run --all-files - - uses: peter-evans/create-pull-request@v5 + - uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.REPO_SCOPED_TOKEN }} add-paths: .pre-commit-config.yaml From a675d2b026808ed6f2a2e39b3af1efe87d3692e6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 06:02:59 +0000 Subject: [PATCH 096/327] Bump cryptography from 42.0.1 to 42.0.2 Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.1 to 42.0.2. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.1...42.0.2) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 77aa3e722..ce5068720 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==2.1.4 pandas-ta==0.3.14b ccxt==4.2.35 -cryptography==42.0.1 +cryptography==42.0.2 aiohttp==3.9.3 SQLAlchemy==2.0.25 python-telegram-bot==20.7 From 646aca7a36eb497711f59eee77c88060b7975d95 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:20:20 +0000 Subject: [PATCH 097/327] Bump fastapi from 0.109.0 to 0.109.2 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.109.0 to 0.109.2. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.109.0...0.109.2) --- updated-dependencies: - dependency-name: fastapi dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ce5068720..3daeed7a0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ orjson==3.9.12 sdnotify==0.3.2 # API Server -fastapi==0.109.0 +fastapi==0.109.2 pydantic==2.5.3 uvicorn==0.27.0.post1 pyjwt==2.8.0 From b9245aef2d865c1b1e60cdaf3f57ae68edb90550 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 08:21:44 +0000 Subject: [PATCH 098/327] Bump orjson from 3.9.12 to 3.9.13 Bumps [orjson](https://github.com/ijl/orjson) from 3.9.12 to 3.9.13. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.9.12...3.9.13) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ce5068720..19a2a4cd7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ py_find_1st==1.1.6 # Load ticker files 30% faster python-rapidjson==1.14 # Properly format api responses -orjson==3.9.12 +orjson==3.9.13 # Notify systemd sdnotify==0.3.2 From b4f030fd55037257ab7ee8edc1c9100e48c53738 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 14:18:28 +0000 Subject: [PATCH 099/327] Bump pydantic from 2.5.3 to 2.6.1 Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.5.3 to 2.6.1. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v2.5.3...v2.6.1) --- updated-dependencies: - dependency-name: pydantic dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3daeed7a0..d2813ff19 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,7 +37,7 @@ sdnotify==0.3.2 # API Server fastapi==0.109.2 -pydantic==2.5.3 +pydantic==2.6.1 uvicorn==0.27.0.post1 pyjwt==2.8.0 aiofiles==23.2.1 From 73e2e034aeada27b38faf101f03685cbb1fcb0a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Feb 2024 18:22:49 +0100 Subject: [PATCH 100/327] Remove unused argument --- tests/optimize/test_backtest_detail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 82c036e07..71cb8ff34 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -900,7 +900,7 @@ TESTS = [ @pytest.mark.parametrize("data", TESTS) -def test_backtest_results(default_conf, fee, mocker, caplog, data: BTContainer) -> None: +def test_backtest_results(default_conf, mocker, caplog, data: BTContainer) -> None: """ run functional tests """ From 4aef5676d7db4dd4b8a31b0f1e31079648000dc6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Feb 2024 18:10:11 +0100 Subject: [PATCH 101/327] update tool.ruff configuration to match 2.0 version --- pyproject.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1d8d9420d..753f44262 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -115,6 +115,8 @@ ignore = ["freqtrade/vendor/**"] line-length = 100 extend-exclude = [".env", ".venv"] target-version = "py38" + +[tool.ruff.lint] # Exclude UP036 as it's causing the "exit if < 3.9" to fail. extend-select = [ "C90", # mccabe @@ -132,16 +134,17 @@ extend-select = [ # "TCH", # flake8-type-checking "PTH", # flake8-use-pathlib ] + extend-ignore = [ "E241", # Multiple spaces after comma "E272", # Multiple spaces before keyword "E221", # Multiple spaces before operator ] -[tool.ruff.mccabe] +[tool.ruff.lint.mccabe] max-complexity = 12 -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "tests/*" = ["S"] [tool.flake8] From 8d02504072df87059db3180bc955886fa3b43f1e Mon Sep 17 00:00:00 2001 From: xmatthias <5024695+xmatthias@users.noreply.github.com> Date: Tue, 6 Feb 2024 03:03:17 +0000 Subject: [PATCH 102/327] chore: update pre-commit hooks --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 979fbe083..5a37634ff 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.1.15' + rev: 'v0.2.1' hooks: - id: ruff From e50fac34a506867246b2b49a5bc4d1d2adf2cf35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Feb 2024 06:35:46 +0100 Subject: [PATCH 103/327] Bump technical to 1.4.3 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 67b89e19b..15226b477 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ requests==2.31.0 urllib3==2.2.0 jsonschema==4.21.1 TA-Lib==0.4.28 -technical==1.4.2 +technical==1.4.3 tabulate==0.9.0 pycoingecko==3.1.0 jinja2==3.1.3 From d2e9d36dea3bb0407926ba72d6ca3267c7297046 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Feb 2024 18:15:17 +0100 Subject: [PATCH 104/327] chore: Update ordering of requirements in setup.py --- setup.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/setup.py b/setup.py index 64b30ed94..3b92b9dd7 100644 --- a/setup.py +++ b/setup.py @@ -35,21 +35,21 @@ hdf5 = [ develop = [ 'coveralls', + 'isort', 'mypy', - 'ruff', 'pre-commit', - 'pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', 'pytest-random-order', - 'isort', + 'pytest', + 'ruff', 'time-machine', 'types-cachetools', 'types-filelock', + 'types-python-dateutil' 'types-requests', 'types-tabulate', - 'types-python-dateutil' ] jupyter = [ @@ -76,8 +76,11 @@ setup( 'arrow>=1.0.0', 'cachetools', 'requests', + 'httpx>=0.24.1', 'urllib3', 'jsonschema', + 'numpy', + 'pandas', 'TA-Lib', 'pandas-ta', 'technical', @@ -86,29 +89,26 @@ setup( 'py_find_1st', 'python-rapidjson', 'orjson', - 'sdnotify', 'colorama', 'jinja2', 'questionary', 'prompt-toolkit', - 'numpy', - 'pandas', 'joblib>=1.2.0', 'rich', 'pyarrow; platform_machine != "armv7l"', 'fastapi', 'pydantic>=2.2.0', + 'pyjwt', + 'websockets', 'uvicorn', 'psutil', - 'pyjwt', - 'aiofiles', 'schedule', - 'websockets', 'janus', 'ast-comments', + 'aiofiles', 'aiohttp', 'cryptography', - 'httpx>=0.24.1', + 'sdnotify', 'python-dateutil', 'packaging', ], From 43bab85b85c5db8d0c635df5ffd473128d0939ac Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Wed, 7 Feb 2024 11:21:32 +0900 Subject: [PATCH 105/327] fix strategy-updater docs --- docs/utils.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index b4432833d..4bdb88cbd 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -990,11 +990,7 @@ options: -h, --help show this help message and exit --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] Provide a space-separated list of strategies to - backtest. Please note that timeframe needs to be set - either in config or via command line. When using this - together with `--export trades`, the strategy-name is - injected into the filename (so `backtest-data.json` - becomes `backtest-data-SampleStrategy.json` + be converted. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). From 8f9f4b40cdcad855cec449f9364a449a22cfad67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Feb 2024 07:21:16 +0100 Subject: [PATCH 106/327] Update model to new sqlalchemy version --- freqtrade/persistence/custom_data.py | 29 ++++++++++--------- .../persistence/custom_data_middleware.py | 10 ++++--- freqtrade/persistence/trade_model.py | 3 +- freqtrade/rpc/telegram.py | 8 ++--- 4 files changed, 28 insertions(+), 22 deletions(-) diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index 1f85467dd..beae8c478 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -1,14 +1,15 @@ from datetime import datetime -from typing import Optional +from typing import ClassVar, Optional -from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text, UniqueConstraint -from sqlalchemy.orm import Query, relationship +from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, select +from sqlalchemy.orm import Mapped, Query, mapped_column, relationship from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.persistence.base import _DECL_BASE +from freqtrade.persistence.base import ModelBase, SessionType +from freqtrade.util import dt_now -class CustomData(_DECL_BASE): +class CustomData(ModelBase): """ CustomData database model Keeps records of metadata as key/value store @@ -18,20 +19,22 @@ class CustomData(_DECL_BASE): - One metadata entry can only be associated with one Trade """ __tablename__ = 'trade_custom_data' + 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"),) - id = Column(Integer, primary_key=True) - ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True, default=0) + id = mapped_column(Integer, primary_key=True) + ft_trade_id = mapped_column(Integer, ForeignKey('trades.id'), index=True, default=0) trade = relationship("Trade", back_populates="custom_data") - cd_key = Column(String(255), nullable=False) - cd_type = Column(String(25), nullable=False) - cd_value = Column(Text, nullable=False) - created_at = Column(DateTime, nullable=False, default=datetime.utcnow) - updated_at = Column(DateTime, nullable=True) + cd_key: Mapped[str] = mapped_column(String(255), nullable=False) + cd_type: Mapped[str] = mapped_column(String(25), nullable=False) + cd_value: Mapped[str] = mapped_column(Text, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=dt_now) + updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) def __repr__(self): create_time = (self.created_at.strftime(DATETIME_PRINT_FORMAT) @@ -54,4 +57,4 @@ class CustomData(_DECL_BASE): if key is not None: filters.append(CustomData.cd_key.ilike(key)) - return CustomData.query.filter(*filters) + return CustomData.session.scalars(select(CustomData)) diff --git a/freqtrade/persistence/custom_data_middleware.py b/freqtrade/persistence/custom_data_middleware.py index 64e8a47fd..cf7b83abc 100644 --- a/freqtrade/persistence/custom_data_middleware.py +++ b/freqtrade/persistence/custom_data_middleware.py @@ -3,6 +3,8 @@ import logging from datetime import datetime from typing import Any, List, Optional +from sqlalchemy import select + from freqtrade.persistence.custom_data import CustomData @@ -86,8 +88,8 @@ class CustomDataWrapper: if CustomDataWrapper.use_db and value_db is not None: data_entry.cd_value = value_db - CustomData.query.session.add(data_entry) - CustomData.query.session.commit() + CustomData.session.add(data_entry) + CustomData.session.commit() elif not CustomDataWrapper.use_db: cd_index = -1 for index, data_entry in enumerate(CustomDataWrapper.custom_data): @@ -97,7 +99,7 @@ class CustomDataWrapper: if cd_index >= 0: data_entry.cd_type = value_type - data_entry.value = value + data_entry.cd_value = value data_entry.updated_at = datetime.utcnow() CustomDataWrapper.custom_data[cd_index] = data_entry @@ -108,6 +110,6 @@ class CustomDataWrapper: def get_all_custom_data() -> List[CustomData]: if CustomDataWrapper.use_db: - return CustomData.query.all() + return CustomData.session.scalars(select(CustomData)).all() else: return CustomDataWrapper.custom_data diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index a3f71ee4f..d96378bf2 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1467,7 +1467,8 @@ class Trade(ModelBase, LocalTrade): orders: Mapped[List[Order]] = relationship( "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin", innerjoin=True) # type: ignore - custom_data = relationship("CustomData", order_by="CustomData.id", cascade="all, delete-orphan") + custom_data: Mapped[List[CustomData]] = relationship( + "CustomData", order_by="CustomData.id", cascade="all, delete-orphan") # type: ignore exchange: Mapped[str] = mapped_column(String(25), nullable=False) # type: ignore pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) # type: ignore diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4bdb09f01..4d91d1bdb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1770,7 +1770,7 @@ class Telegram(RPCHandler): ) @authorized_only - def _list_custom_data(self, update: Update, context: CallbackContext) -> None: + async def _list_custom_data(self, update: Update, context: CallbackContext) -> None: """ Handler for /list_custom_data . List custom_data for specified trade (and key if supplied). @@ -1807,14 +1807,14 @@ class Telegram(RPCHandler): msg = "Message dropped because length exceeds " msg += f"maximum allowed characters: {MAX_MESSAGE_LENGTH}" logger.warning(msg) - self._send_msg(msg) + await self._send_msg(msg) else: message = f"Didn't find any custom-data entries for Trade ID: `{trade_id}`" message += f" and Key: `{key}`." if key is not None else "" - self._send_msg(message) + await self._send_msg(message) except RPCException as e: - self._send_msg(str(e)) + await self._send_msg(str(e)) async def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: From 2393a9fecf04eb284078ec1d09e6b0113138d5f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Feb 2024 19:06:41 +0100 Subject: [PATCH 107/327] Fix some minor test failures --- freqtrade/persistence/models.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 3f1661ee2..189b80fa6 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -79,8 +79,8 @@ def init_db(db_url: str) -> None: 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/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 110cd7819..45268b23b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -150,7 +150,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " "['bl_delete', 'blacklist_delete'], " "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir'], " - "['order']], ['list_custom_data']") + "['order'], ['list_custom_data']]") assert log_has(message_str, caplog) From 626c9041039212f7672dd3b5e769323de54f636f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Feb 2024 19:28:06 +0100 Subject: [PATCH 108/327] Fix some issues with types --- freqtrade/persistence/custom_data.py | 14 ++++++++------ freqtrade/persistence/custom_data_middleware.py | 8 ++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index beae8c478..42b267e95 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -1,8 +1,8 @@ from datetime import datetime -from typing import ClassVar, Optional +from typing import ClassVar, Optional, Self, Sequence from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, select -from sqlalchemy.orm import Mapped, Query, mapped_column, relationship +from sqlalchemy.orm import Mapped, mapped_column, relationship from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.persistence.base import ModelBase, SessionType @@ -45,16 +45,18 @@ class CustomData(ModelBase): f'value={self.cd_value}, trade_id={self.ft_trade_id}, created={create_time}, ' + f'updated={update_time})') - @staticmethod - def query_cd(key: Optional[str] = None, trade_id: Optional[int] = None) -> Query: + @classmethod + 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 :param trade_id: id of the Trade """ filters = [] - filters.append(CustomData.ft_trade_id == trade_id if trade_id is not None else 0) + if trade_id is not None: + filters.append(CustomData.ft_trade_id == trade_id) if key is not None: filters.append(CustomData.cd_key.ilike(key)) - return CustomData.session.scalars(select(CustomData)) + return CustomData.session.scalars(select(CustomData)).all() diff --git a/freqtrade/persistence/custom_data_middleware.py b/freqtrade/persistence/custom_data_middleware.py index cf7b83abc..2f99d9c75 100644 --- a/freqtrade/persistence/custom_data_middleware.py +++ b/freqtrade/persistence/custom_data_middleware.py @@ -37,11 +37,11 @@ class CustomDataWrapper: trade_id = 0 if CustomDataWrapper.use_db: - filtered_custom_data = CustomData.query_cd(trade_id=trade_id, key=key).all() - for index, data_entry in enumerate(filtered_custom_data): + filtered_custom_data = [] + for data_entry in CustomData.query_cd(trade_id=trade_id, key=key): if data_entry.cd_type not in CustomDataWrapper.unserialized_types: data_entry.cd_value = json.loads(data_entry.cd_value) - filtered_custom_data[index] = data_entry + filtered_custom_data.append(data_entry) return filtered_custom_data else: filtered_custom_data = [ @@ -110,6 +110,6 @@ class CustomDataWrapper: def get_all_custom_data() -> List[CustomData]: if CustomDataWrapper.use_db: - return CustomData.session.scalars(select(CustomData)).all() + return list(CustomData.query_cd()) else: return CustomDataWrapper.custom_data From 3e5a572fc61c18b5533aa6f0f5d71e7090d971b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Feb 2024 07:14:40 +0100 Subject: [PATCH 109/327] Allow int as trade-id parameter closes #9780 --- freqtrade/rpc/api_server/api_schemas.py | 2 +- freqtrade/rpc/api_server/api_v1.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index af3e84873..9919d1a05 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -397,7 +397,7 @@ class ForceEnterPayload(BaseModel): class ForceExitPayload(BaseModel): - tradeid: str + tradeid: Union[str, int] ordertype: Optional[OrderTypeValues] = None amount: Optional[float] = None diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 4f4aac32c..99fc3d451 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -215,7 +215,7 @@ def force_entry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): @router.post('/forcesell', response_model=ResultMsg, tags=['trading']) def forceexit(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None - return rpc._rpc_force_exit(payload.tradeid, ordertype, amount=payload.amount) + return rpc._rpc_force_exit(str(payload.tradeid), ordertype, amount=payload.amount) @router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) From bf1f08cd21c0b15d3b8a51d49518e4bef86e72cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Feb 2024 18:03:49 +0100 Subject: [PATCH 110/327] chore: add dependency groups for regular updates, reschedule to run at 3am UTC (hope this works now ...) --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dfbc0cee7..8c9a3f936 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,8 +10,17 @@ updates: directory: "/" schedule: interval: weekly + time: "03:00" + timezone: "Etc/UTC" open-pull-requests-limit: 15 target-branch: develop + groups: + types: + patterns: + - "types-*" + pytest: + patterns: + - "pytest*" - package-ecosystem: "github-actions" directory: "/" From 7223a6c504b5f2141a7ca841fe9a3267d007e764 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:04:19 +0000 Subject: [PATCH 111/327] Bump pre-commit/action from 3.0.0 to 3.0.1 Bumps [pre-commit/action](https://github.com/pre-commit/action) from 3.0.0 to 3.0.1. - [Release notes](https://github.com/pre-commit/action/releases) - [Commits](https://github.com/pre-commit/action/compare/v3.0.0...v3.0.1) --- updated-dependencies: - dependency-name: pre-commit/action dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8a261d0e..ba55eed04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -325,7 +325,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.10" - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 docs-check: runs-on: ubuntu-22.04 From a0c246fa9a14738b129f7f68b2dc09afc47c7736 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:05:39 +0000 Subject: [PATCH 112/327] Bump ccxt from 4.2.35 to 4.2.39 Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.2.35 to 4.2.39. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/4.2.35...4.2.39) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 15226b477..e094081af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.26.3 pandas==2.1.4 pandas-ta==0.3.14b -ccxt==4.2.35 +ccxt==4.2.39 cryptography==42.0.2 aiohttp==3.9.3 SQLAlchemy==2.0.25 From 68fd7d7ffd2ece3c93d9f7bc94e8349f58be3ef4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:05:44 +0000 Subject: [PATCH 113/327] Bump mkdocs-material from 9.5.7 to 9.5.8 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.7 to 9.5.8. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.7...9.5.8) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index c7f1e1889..ddcc951d0 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.5.2 mkdocs==1.5.3 -mkdocs-material==9.5.7 +mkdocs-material==9.5.8 mdx_truly_sane_lists==1.3 pymdown-extensions==10.7 jinja2==3.1.3 From 9f449dd34729e994f016dd16183f4eebb2cf2c2a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:06:05 +0000 Subject: [PATCH 114/327] Bump ruff from 0.2.0 to 0.2.1 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.2.0 to 0.2.1. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.2.0...v0.2.1) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1353d4fd0..155f04376 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.2.0 +ruff==0.2.1 mypy==1.8.0 pre-commit==3.6.0 pytest==7.4.4 From 01a00ad048e05297d5479e6280e2918163dee9c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:06:26 +0000 Subject: [PATCH 115/327] Bump nbconvert from 7.14.2 to 7.16.0 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.14.2 to 7.16.0. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md) - [Commits](https://github.com/jupyter/nbconvert/compare/v7.14.2...v7.16.0) --- updated-dependencies: - dependency-name: nbconvert dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1353d4fd0..7f2d00e7f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,7 +21,7 @@ isort==5.13.2 time-machine==2.13.0 # Convert jupyter notebooks to markdown documents -nbconvert==7.14.2 +nbconvert==7.16.0 # mypy types types-cachetools==5.3.0.7 From ce6b62f86571e5f97a52ccb5e6d6b51d6e9e07ce Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 8 Feb 2024 18:26:56 +0000 Subject: [PATCH 116/327] Bump numpy from 1.26.3 to 1.26.4 Bumps [numpy](https://github.com/numpy/numpy) from 1.26.3 to 1.26.4. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v1.26.3...v1.26.4) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e094081af..826f49c56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.26.3 +numpy==1.26.4 pandas==2.1.4 pandas-ta==0.3.14b From adf63a45cfea3c6f38c112cda60c67efb29edfa7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Feb 2024 19:35:16 +0100 Subject: [PATCH 117/327] Align new-config documentation examples closes #9790 --- docs/configuration.md | 2 +- docs/utils.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 202fa49bf..2fc54668a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -14,7 +14,7 @@ You can specify a different configuration file used by the bot with the `-c/--co If you used the [Quick start](docker_quickstart.md#docker-quick-start) method for installing the bot, the installation script should have already created the default configuration file (`config.json`) for you. -If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file. +If the default configuration file is not created we recommend to use `freqtrade new-config --config user_data/config.json` to generate a basic configuration file. The Freqtrade configuration file is to be written in JSON format. diff --git a/docs/utils.md b/docs/utils.md index 4bdb88cbd..202526afe 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -54,7 +54,7 @@ optional arguments: ### Create config examples ``` -$ freqtrade new-config --config config_binance.json +$ freqtrade new-config --config user_data/config_binance.json ? Do you want to enable Dry-run (simulated trades)? Yes ? Please insert your stake currency: BTC From f89147fd44d53a81120d97e3d8878d9972b1fbd9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Feb 2024 08:44:16 +0100 Subject: [PATCH 118/327] Allow limiting kraken pairs to `--pairs` selection --- freqtrade/data/converter/trade_converter_kraken.py | 7 +++++++ tests/data/test_trade_converter_kraken.py | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/freqtrade/data/converter/trade_converter_kraken.py b/freqtrade/data/converter/trade_converter_kraken.py index 5abebd6a2..71a6dd8d3 100644 --- a/freqtrade/data/converter/trade_converter_kraken.py +++ b/freqtrade/data/converter/trade_converter_kraken.py @@ -38,6 +38,13 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str): } logger.info(f"Found csv files for {', '.join(data_symbols)}.") + if pairs := config.get('pairs'): + markets = [m for m in markets if m[0] in pairs] + if not markets: + logger.info(f"No data found for pairs {', '.join(pairs)}.") + return + logger.info(f"Converting pairs: {', '.join(m[0] for m in markets)}.") + for pair, name in markets: dfs = [] # Load and combine all csv files for this pair diff --git a/tests/data/test_trade_converter_kraken.py b/tests/data/test_trade_converter_kraken.py index bb44062bf..91de303fb 100644 --- a/tests/data/test_trade_converter_kraken.py +++ b/tests/data/test_trade_converter_kraken.py @@ -34,6 +34,7 @@ def test_import_kraken_trades_from_csv(testdatadir, tmp_path, caplog, default_co import_kraken_trades_from_csv(default_conf_usdt, 'feather') assert log_has("Found csv files for BCHEUR.", caplog) + assert log_has("Converting pairs: BCH/EUR.", caplog) assert log_has_re(r"BCH/EUR: 340 trades.* 2023-01-01.* 2023-01-02.*", caplog) assert dstfile.is_file() @@ -48,3 +49,10 @@ def test_import_kraken_trades_from_csv(testdatadir, tmp_path, caplog, default_co tzinfo=timezone.utc) # ID is not filled assert len(trades.loc[trades['id'] != '']) == 0 + + caplog.clear() + default_conf_usdt['pairs'] = ['XRP/EUR'] + # Filtered to non-existing pair + import_kraken_trades_from_csv(default_conf_usdt, 'feather') + assert log_has("Found csv files for BCHEUR.", caplog) + assert log_has("No data found for pairs XRP/EUR.", caplog) From a83b5abb51ac5dba8dfb11860a79b33197aaabdd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Feb 2024 08:48:52 +0100 Subject: [PATCH 119/327] Allow wildcards for pairs filter --- freqtrade/data/converter/trade_converter_kraken.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/converter/trade_converter_kraken.py b/freqtrade/data/converter/trade_converter_kraken.py index 71a6dd8d3..ec5b265f7 100644 --- a/freqtrade/data/converter/trade_converter_kraken.py +++ b/freqtrade/data/converter/trade_converter_kraken.py @@ -8,6 +8,7 @@ from freqtrade.data.converter.trade_converter import (trades_convert_types, trades_df_remove_duplicates) from freqtrade.data.history.idatahandler import get_datahandler from freqtrade.exceptions import OperationalException +from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.resolvers import ExchangeResolver @@ -38,10 +39,11 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str): } logger.info(f"Found csv files for {', '.join(data_symbols)}.") - if pairs := config.get('pairs'): + if pairs_raw := config.get('pairs'): + pairs = expand_pairlist(pairs_raw, [m[0] for m in markets]) markets = [m for m in markets if m[0] in pairs] if not markets: - logger.info(f"No data found for pairs {', '.join(pairs)}.") + logger.info(f"No data found for pairs {', '.join(pairs_raw)}.") return logger.info(f"Converting pairs: {', '.join(m[0] for m in markets)}.") From 3aa2d0c30a0ac641ca7369b7ef1b96370d8176c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Feb 2024 09:37:59 +0100 Subject: [PATCH 120/327] Slightly improve memory-usage of kraken-convert --- freqtrade/data/converter/trade_converter_kraken.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/converter/trade_converter_kraken.py b/freqtrade/data/converter/trade_converter_kraken.py index ec5b265f7..614d97b2a 100644 --- a/freqtrade/data/converter/trade_converter_kraken.py +++ b/freqtrade/data/converter/trade_converter_kraken.py @@ -41,7 +41,7 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str): if pairs_raw := config.get('pairs'): pairs = expand_pairlist(pairs_raw, [m[0] for m in markets]) - markets = [m for m in markets if m[0] in pairs] + markets = {m for m in markets if m[0] in pairs} if not markets: logger.info(f"No data found for pairs {', '.join(pairs_raw)}.") return @@ -61,17 +61,18 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str): continue trades = pd.concat(dfs, ignore_index=True) + del dfs trades.loc[:, 'timestamp'] = trades['timestamp'] * 1e3 trades.loc[:, 'cost'] = trades['price'] * trades['amount'] for col in DEFAULT_TRADES_COLUMNS: if col not in trades.columns: - trades[col] = '' - + trades.loc[:, col] = '' trades = trades[DEFAULT_TRADES_COLUMNS] trades = trades_convert_types(trades) trades_df = trades_df_remove_duplicates(trades) + del trades logger.info(f"{pair}: {len(trades_df)} trades, from " f"{trades_df['date'].min():{DATETIME_PRINT_FORMAT}} to " f"{trades_df['date'].max():{DATETIME_PRINT_FORMAT}}") From f10c8f9c3bff2d8990b7bc88d4bb3254a6c66776 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 03:29:17 +0000 Subject: [PATCH 121/327] Bump uvicorn from 0.27.0.post1 to 0.27.1 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.27.0.post1 to 0.27.1. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.27.0.post1...0.27.1) --- updated-dependencies: - dependency-name: uvicorn dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 826f49c56..1bfd431b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ sdnotify==0.3.2 # API Server fastapi==0.109.2 pydantic==2.6.1 -uvicorn==0.27.0.post1 +uvicorn==0.27.1 pyjwt==2.8.0 aiofiles==23.2.1 psutil==5.9.8 From d5aec91a41661e1d28239639236d874a1271dd9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 03:29:27 +0000 Subject: [PATCH 122/327] Bump pre-commit from 3.6.0 to 3.6.1 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.6.0 to 3.6.1. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.6.0...v3.6.1) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c5cf7e654..0f41482de 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ coveralls==3.3.1 ruff==0.2.1 mypy==1.8.0 -pre-commit==3.6.0 +pre-commit==3.6.1 pytest==7.4.4 pytest-asyncio==0.23.4 pytest-cov==4.1.0 From 7e8e36c6be67cbccdb8d817c8786bbefdf206a07 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 03:29:40 +0000 Subject: [PATCH 123/327] Bump ccxt from 4.2.39 to 4.2.42 Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.2.39 to 4.2.42. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/4.2.39...4.2.42) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 826f49c56..bbcf13e8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.26.4 pandas==2.1.4 pandas-ta==0.3.14b -ccxt==4.2.39 +ccxt==4.2.42 cryptography==42.0.2 aiohttp==3.9.3 SQLAlchemy==2.0.25 From 7d74a4c1936b83c437471ae4c2401f305c58f96c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 03:29:45 +0000 Subject: [PATCH 124/327] Bump tqdm from 4.66.1 to 4.66.2 Bumps [tqdm](https://github.com/tqdm/tqdm) from 4.66.1 to 4.66.2. - [Release notes](https://github.com/tqdm/tqdm/releases) - [Commits](https://github.com/tqdm/tqdm/compare/v4.66.1...v4.66.2) --- updated-dependencies: - dependency-name: tqdm dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-freqai-rl.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai-rl.txt b/requirements-freqai-rl.txt index fa5e9f014..67fed9190 100644 --- a/requirements-freqai-rl.txt +++ b/requirements-freqai-rl.txt @@ -8,4 +8,4 @@ gymnasium==0.29.1; python_version < '3.12' stable_baselines3==2.2.1; python_version < '3.12' sb3_contrib>=2.0.0a9; python_version < '3.12' # Progress bar for stable-baselines3 and sb3-contrib -tqdm==4.66.1 +tqdm==4.66.2 From 903ae336794487be8cf99d1ff43c4462bb172a99 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 03:29:52 +0000 Subject: [PATCH 125/327] Bump mkdocs-material from 9.5.8 to 9.5.9 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.8 to 9.5.9. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.8...9.5.9) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index ddcc951d0..aca3da72a 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.5.2 mkdocs==1.5.3 -mkdocs-material==9.5.8 +mkdocs-material==9.5.9 mdx_truly_sane_lists==1.3 pymdown-extensions==10.7 jinja2==3.1.3 From 8ca905b45fd713cf41a0a68f63f29cb28845cb45 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 03:30:00 +0000 Subject: [PATCH 126/327] Bump python-telegram-bot from 20.7 to 20.8 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 20.7 to 20.8. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v20.7...v20.8) --- updated-dependencies: - dependency-name: python-telegram-bot dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 826f49c56..df091982f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==4.2.39 cryptography==42.0.2 aiohttp==3.9.3 SQLAlchemy==2.0.25 -python-telegram-bot==20.7 +python-telegram-bot==20.8 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.24.1 arrow==1.3.0 From 2c1ea2d2568492374d237e56c6138bf60c586f7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 03:30:19 +0000 Subject: [PATCH 127/327] Bump tensorboard from 2.15.1 to 2.15.2 Bumps [tensorboard](https://github.com/tensorflow/tensorboard) from 2.15.1 to 2.15.2. - [Release notes](https://github.com/tensorflow/tensorboard/releases) - [Changelog](https://github.com/tensorflow/tensorboard/blob/2.15.2/RELEASE.md) - [Commits](https://github.com/tensorflow/tensorboard/compare/2.15.1...2.15.2) --- updated-dependencies: - dependency-name: tensorboard dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 2d33efc3c..848b6d920 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -8,5 +8,5 @@ joblib==1.3.2 catboost==1.2.2; 'arm' not in platform_machine and python_version < '3.12' lightgbm==4.3.0 xgboost==2.0.3 -tensorboard==2.15.1 +tensorboard==2.15.2 datasieve==0.1.7 From 0ba27ddee61cd33895870758aa1434e67321693a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 05:04:43 +0000 Subject: [PATCH 128/327] Bump the pytest group with 2 updates Bumps the pytest group with 2 updates: [pytest](https://github.com/pytest-dev/pytest) and [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio). Updates `pytest` from 7.4.4 to 8.0.0 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/7.4.4...8.0.0) Updates `pytest-asyncio` from 0.23.4 to 0.23.5 - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.23.4...v0.23.5) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-major dependency-group: pytest - dependency-name: pytest-asyncio dependency-type: direct:development update-type: version-update:semver-patch dependency-group: pytest ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0f41482de..f0095bffa 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,8 +10,8 @@ coveralls==3.3.1 ruff==0.2.1 mypy==1.8.0 pre-commit==3.6.1 -pytest==7.4.4 -pytest-asyncio==0.23.4 +pytest==8.0.0 +pytest-asyncio==0.23.5 pytest-cov==4.1.0 pytest-mock==3.12.0 pytest-random-order==1.1.1 From 0740a1339358517838aa7b01f9472e251fc93006 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Feb 2024 05:18:09 +0000 Subject: [PATCH 129/327] Bump sqlalchemy from 2.0.25 to 2.0.26 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.25 to 2.0.26. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 57d570be4..c9b26eab6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==4.2.42 cryptography==42.0.2 aiohttp==3.9.3 -SQLAlchemy==2.0.25 +SQLAlchemy==2.0.26 python-telegram-bot==20.7 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.24.1 From fd737af69d958f419f6e9217e1c2e369ca5e873d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 06:29:42 +0100 Subject: [PATCH 130/327] use query.edit_message_text to simplify telegram class --- freqtrade/rpc/telegram.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f42e3ab51..f9a0635f0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1777,13 +1777,9 @@ class Telegram(RPCHandler): msg += f"\nUpdated: {datetime.now().ctime()}" if not query.message: return - chat_id = query.message.chat_id - message_id = query.message.message_id try: - await self._app.bot.edit_message_text( - chat_id=chat_id, - message_id=message_id, + await query.edit_message_text( text=msg, parse_mode=parse_mode, reply_markup=reply_markup From 72290365db735f01ca6d85f1c35b92d38ba19ca8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 06:31:39 +0100 Subject: [PATCH 131/327] telegram: ensure msg update test uses the correct edit_message_text call --- tests/rpc/test_rpc_telegram.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2e6852876..3c683d7b3 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2557,22 +2557,22 @@ async def test_telegram__send_msg(default_conf, mocker, caplog) -> None: # Test update query = MagicMock() + query.edit_message_text = AsyncMock() await telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True) - edit_message_text = telegram._app.bot.edit_message_text - assert edit_message_text.call_count == 1 - assert "Updated: " in edit_message_text.call_args_list[0][1]['text'] + assert query.edit_message_text.call_count == 1 + assert "Updated: " in query.edit_message_text.call_args_list[0][1]['text'] - telegram._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest("not modified")) + query.edit_message_text = AsyncMock(side_effect=BadRequest("not modified")) await telegram._send_msg('test', callback_path="DeadBeef", query=query) - assert telegram._app.bot.edit_message_text.call_count == 1 + assert query.edit_message_text.call_count == 1 assert not log_has_re(r"TelegramError: .*", caplog) - telegram._app.bot.edit_message_text = AsyncMock(side_effect=BadRequest("")) + query.edit_message_text = AsyncMock(side_effect=BadRequest("")) await telegram._send_msg('test2', callback_path="DeadBeef", query=query) - assert telegram._app.bot.edit_message_text.call_count == 1 + assert query.edit_message_text.call_count == 1 assert log_has_re(r"TelegramError: .*", caplog) - telegram._app.bot.edit_message_text = AsyncMock(side_effect=TelegramError("DeadBEEF")) + query.edit_message_text = AsyncMock(side_effect=TelegramError("DeadBEEF")) await telegram._send_msg('test3', callback_path="DeadBeef", query=query) assert log_has_re(r"TelegramError: DeadBEEF! Giving up.*", caplog) From f2a7312010d39bce3fac1cc423c5263984c833a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 06:34:45 +0100 Subject: [PATCH 132/327] Update sqlalchemy pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a37634ff..a1aa00f07 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - types-requests==2.31.0.20240125 - types-tabulate==0.9.0.20240106 - types-python-dateutil==2.8.19.20240106 - - SQLAlchemy==2.0.25 + - SQLAlchemy==2.0.26 # stages: [push] - repo: https://github.com/pycqa/isort From d49da763824aec1ad5fefd38aee938ef34e2a6b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 18:17:48 +0100 Subject: [PATCH 133/327] Slighlty improve docs --- docs/strategy-advanced.md | 63 ++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index b2a0d8431..69d4ced34 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -13,38 +13,46 @@ The call sequence of the methods described here is covered under [bot execution ## Storing information (Non-Persistent) -Storing information can be accomplished by creating a new dictionary within the strategy class. +!!! Warning "Deprecated" + This method of storing information is deprecated, and we do advise against using non-persistent storage. + Please use the below [Persistent Storing Information Section](#storing-information-persistent) instead. -The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables. + It's content has therefore be collapsed. -```python -class AwesomeStrategy(IStrategy): - # Create custom dictionary - custom_info = {} +??? Abstract "Storing information" + Storing information can be accomplished by creating a new dictionary within the strategy class. - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # Check if the entry already exists - if not metadata["pair"] in self.custom_info: - # Create empty entry for this pair - self.custom_info[metadata["pair"]] = {} + The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables. - if "crosstime" in self.custom_info[metadata["pair"]]: - self.custom_info[metadata["pair"]]["crosstime"] += 1 - else: - self.custom_info[metadata["pair"]]["crosstime"] = 1 -``` + ```python + class AwesomeStrategy(IStrategy): + # Create custom dictionary + custom_info = {} -!!! Warning - The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Check if the entry already exists + if not metadata["pair"] in self.custom_info: + # Create empty entry for this pair + self.custom_info[metadata["pair"]] = {} -!!! Note - If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. + if "crosstime" in self.custom_info[metadata["pair"]]: + self.custom_info[metadata["pair"]]["crosstime"] += 1 + else: + self.custom_info[metadata["pair"]]["crosstime"] = 1 + ``` + + !!! Warning + The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. + + !!! Note + If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. ## Storing information (Persistent) Storing information can also be performed in a persistent manner. Freqtrade allows storing/retrieving user custom information associated with a specific trade. -Using a trade object handle information can be stored using `trade_obj.set_custom_data(key='my_key', value=my_value)` and retrieved using `trade_obj.get_custom_data(key='my_key')`. -Each data entry is associated with a trade and a user supplied key (of type `string`). This means that this can only be used in callbacks that also provide a trade object handle. + +Using a trade object, information can be stored using `trade_obj.set_custom_data(key='my_key', value=my_value)` and retrieved using `trade_obj.get_custom_data(key='my_key')`. Each data entry is associated with a trade and a user supplied key (of type `string`). This means that this can only be used in callbacks that also provide a trade object. + For the data to be able to be stored within the database it must be serialized. This is done by converting it to a JSON formatted string. ```python @@ -69,7 +77,13 @@ class AwesomeStrategy(IStrategy): current_time: datetime, proposed_rate: float, current_order_rate: float, entry_tag: Optional[str], side: str, **kwargs) -> float: # Limit orders to use and follow SMA200 as price target for the first 10 minutes since entry trigger for BTC/USDT pair. - if pair == 'BTC/USDT' and entry_tag == 'long_sma200' and side == 'long' and (current_time - timedelta(minutes=10) > trade.open_date_utc and order.filled == 0.0: + if ( + pair == 'BTC/USDT' + and entry_tag == 'long_sma200' + and side == 'long' + and (current_time - timedelta(minutes=10)) > trade.open_date_utc + and order.filled == 0.0 + ): dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) current_candle = dataframe.iloc[-1].squeeze() # store information about entry adjustment @@ -104,8 +118,9 @@ class AwesomeStrategy(IStrategy): !!! Note It is recommended that simple data types are used `[bool, int, float, str]` to ensure no issues when serializing the data that needs to be stored. + Storing big junks of data may lead to unintended side-effects, like a database becoming big pretty fast (and as a consequence, also slow). -!!! Warning +!!! Warning "Non-serializable data" If supplied data cannot be serialized a warning is logged and the entry for the specified `key` will contain `None` as data. ## Dataframe access From 85930946195444937accc653770655788e9fd32e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 18:22:49 +0100 Subject: [PATCH 134/327] Ensure custom data access goes through the accessor functions --- freqtrade/persistence/trade_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index d96378bf2..b0b4a58dc 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1468,7 +1468,8 @@ class Trade(ModelBase, LocalTrade): "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin", innerjoin=True) # type: ignore custom_data: Mapped[List[CustomData]] = relationship( - "CustomData", order_by="CustomData.id", cascade="all, delete-orphan") # type: ignore + "CustomData", order_by="CustomData.id", cascade="all, delete-orphan", + lazy="raise") # type: ignore exchange: Mapped[str] = mapped_column(String(25), nullable=False) # type: ignore pair: Mapped[str] = mapped_column(String(25), nullable=False, index=True) # type: ignore From c67e451fe177c00f8148d0d66915e957ab15b8a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 18:24:26 +0100 Subject: [PATCH 135/327] Remove unused imports --- freqtrade/persistence/custom_data.py | 2 +- freqtrade/persistence/custom_data_middleware.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index 42b267e95..004aa51df 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import ClassVar, Optional, Self, Sequence +from typing import ClassVar, Optional, Sequence from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, select from sqlalchemy.orm import Mapped, mapped_column, relationship diff --git a/freqtrade/persistence/custom_data_middleware.py b/freqtrade/persistence/custom_data_middleware.py index 2f99d9c75..e40491b28 100644 --- a/freqtrade/persistence/custom_data_middleware.py +++ b/freqtrade/persistence/custom_data_middleware.py @@ -3,8 +3,6 @@ import logging from datetime import datetime from typing import Any, List, Optional -from sqlalchemy import select - from freqtrade.persistence.custom_data import CustomData From 83b22dedd5a6151edf67dcc0c96bcbf8207caccb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 18:25:02 +0100 Subject: [PATCH 136/327] Fix non-reset of use_db --- freqtrade/persistence/usedb_context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/persistence/usedb_context.py b/freqtrade/persistence/usedb_context.py index 795388af0..193f7021d 100644 --- a/freqtrade/persistence/usedb_context.py +++ b/freqtrade/persistence/usedb_context.py @@ -12,6 +12,7 @@ def disable_database_use(timeframe: str) -> None: PairLocks.use_db = False PairLocks.timeframe = timeframe Trade.use_db = False + CustomDataWrapper.use_db = False def enable_database_use() -> None: From 9699011cd9e607c4efacc30428c0ebe6f5048b67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 18:28:29 +0100 Subject: [PATCH 137/327] Remove pointless wrapper --- freqtrade/persistence/trade_model.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index b0b4a58dc..ea03e2c29 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1882,12 +1882,6 @@ class Trade(ModelBase, LocalTrade): return best_pair - def set_custom_data(self, key: str, value: Any) -> None: - super().set_custom_data(key=key, value=value) - - def get_custom_data(self, key: Optional[str]) -> List[CustomData]: - return super().get_custom_data(key=key) - @staticmethod def get_trading_volume(start_date: datetime = datetime.fromtimestamp(0)) -> float: """ From 7fd70b82fa61d8f1f68a8394f5b616eaff4861f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 20:05:13 +0100 Subject: [PATCH 138/327] custom_data: Simplify and fix a few things --- freqtrade/persistence/custom_data.py | 2 +- .../persistence/custom_data_middleware.py | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index 004aa51df..cfe150967 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -59,4 +59,4 @@ class CustomData(ModelBase): if key is not None: filters.append(CustomData.cd_key.ilike(key)) - return CustomData.session.scalars(select(CustomData)).all() + return CustomData.session.scalars(select(CustomData).filter(*filters)).all() diff --git a/freqtrade/persistence/custom_data_middleware.py b/freqtrade/persistence/custom_data_middleware.py index e40491b28..acc65606b 100644 --- a/freqtrade/persistence/custom_data_middleware.py +++ b/freqtrade/persistence/custom_data_middleware.py @@ -1,9 +1,9 @@ import json import logging -from datetime import datetime from typing import Any, List, Optional from freqtrade.persistence.custom_data import CustomData +from freqtrade.util import dt_now logger = logging.getLogger(__name__) @@ -30,7 +30,7 @@ class CustomDataWrapper: @staticmethod def get_custom_data(key: Optional[str] = None, - trade_id: Optional[int] = None) -> List[CustomData]: + trade_id: Optional[int] = None) -> CustomData: if trade_id is None: trade_id = 0 @@ -73,15 +73,15 @@ class CustomDataWrapper: custom_data = CustomDataWrapper.get_custom_data(key=key, trade_id=trade_id) if custom_data: data_entry = custom_data[0] - data_entry.cd_value = value - data_entry.updated_at = datetime.utcnow() + data_entry.cd_value = value_db + data_entry.updated_at = dt_now() else: data_entry = CustomData( - ft_trade_id=trade_id, - cd_key=key, - cd_type=value_type, - cd_value=value, - created_at=datetime.utcnow() + ft_trade_id=trade_id, + cd_key=key, + cd_type=value_type, + cd_value=value_db, + created_at=dt_now() ) if CustomDataWrapper.use_db and value_db is not None: @@ -97,8 +97,8 @@ class CustomDataWrapper: if cd_index >= 0: data_entry.cd_type = value_type - data_entry.cd_value = value - data_entry.updated_at = datetime.utcnow() + data_entry.cd_value = value_db + data_entry.updated_at = dt_now() CustomDataWrapper.custom_data[cd_index] = data_entry else: From b7904b8e805b57a7cd6b588c203ee981229b0b5e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 20:14:37 +0100 Subject: [PATCH 139/327] Combine custom_data classes to one file --- freqtrade/persistence/__init__.py | 2 +- freqtrade/persistence/custom_data.py | 111 ++++++++++++++++- .../persistence/custom_data_middleware.py | 113 ------------------ freqtrade/persistence/trade_model.py | 13 +- freqtrade/persistence/usedb_context.py | 2 +- 5 files changed, 118 insertions(+), 123 deletions(-) delete mode 100644 freqtrade/persistence/custom_data_middleware.py diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 5926f2ad3..d5584c22c 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa: F401 -from freqtrade.persistence.custom_data_middleware import CustomDataWrapper +from freqtrade.persistence.custom_data import CustomDataWrapper from freqtrade.persistence.key_value_store import KeyStoreKeys, KeyValueStore from freqtrade.persistence.models import init_db from freqtrade.persistence.pairlock_middleware import PairLocks diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index cfe150967..bf6056278 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -1,5 +1,7 @@ +import json +import logging from datetime import datetime -from typing import ClassVar, Optional, Sequence +from typing import Any, ClassVar, List, Optional, Sequence from sqlalchemy import DateTime, ForeignKey, Integer, String, Text, UniqueConstraint, select from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -9,6 +11,9 @@ from freqtrade.persistence.base import ModelBase, SessionType from freqtrade.util import dt_now +logger = logging.getLogger(__name__) + + class CustomData(ModelBase): """ CustomData database model @@ -60,3 +65,107 @@ class CustomData(ModelBase): filters.append(CustomData.cd_key.ilike(key)) return CustomData.session.scalars(select(CustomData).filter(*filters)).all() + + +class CustomDataWrapper: + """ + CustomData middleware class + Abstracts the database layer away so it becomes optional - which will be necessary to support + backtesting and hyperopt in the future. + """ + + use_db = True + custom_data: List[CustomData] = [] + unserialized_types = ['bool', 'float', 'int', 'str'] + + @staticmethod + def reset_custom_data() -> None: + """ + Resets all key-value pairs. Only active for backtesting mode. + """ + if not CustomDataWrapper.use_db: + CustomDataWrapper.custom_data = [] + + @staticmethod + def get_custom_data(key: Optional[str] = None, + trade_id: Optional[int] = None) -> CustomData: + if trade_id is None: + trade_id = 0 + + if CustomDataWrapper.use_db: + filtered_custom_data = [] + for data_entry in CustomData.query_cd(trade_id=trade_id, key=key): + if data_entry.cd_type not in CustomDataWrapper.unserialized_types: + data_entry.cd_value = json.loads(data_entry.cd_value) + filtered_custom_data.append(data_entry) + return filtered_custom_data + else: + filtered_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 + if (data_entry.cd_key.casefold() == key.casefold()) + ] + return filtered_custom_data + + @staticmethod + def set_custom_data(key: str, value: Any, trade_id: Optional[int] = None) -> None: + + value_type = type(value).__name__ + value_db = None + + if value_type not in CustomDataWrapper.unserialized_types: + try: + value_db = json.dumps(value) + except TypeError as e: + logger.warning(f"could not serialize {key} value due to {e}") + else: + value_db = str(value) + + if trade_id is None: + trade_id = 0 + + custom_data = CustomDataWrapper.get_custom_data(key=key, trade_id=trade_id) + if custom_data: + data_entry = custom_data[0] + data_entry.cd_value = value_db + data_entry.updated_at = dt_now() + else: + data_entry = CustomData( + ft_trade_id=trade_id, + cd_key=key, + cd_type=value_type, + cd_value=value_db, + created_at=dt_now() + ) + + if CustomDataWrapper.use_db and value_db is not None: + data_entry.cd_value = value_db + CustomData.session.add(data_entry) + CustomData.session.commit() + elif not CustomDataWrapper.use_db: + cd_index = -1 + for index, data_entry in enumerate(CustomDataWrapper.custom_data): + if data_entry.ft_trade_id == trade_id and data_entry.cd_key == key: + cd_index = index + break + + if cd_index >= 0: + data_entry.cd_type = value_type + data_entry.cd_value = value_db + data_entry.updated_at = dt_now() + + CustomDataWrapper.custom_data[cd_index] = data_entry + else: + CustomDataWrapper.custom_data.append(data_entry) + + @staticmethod + def get_all_custom_data() -> List[CustomData]: + + if CustomDataWrapper.use_db: + return list(CustomData.query_cd()) + else: + return CustomDataWrapper.custom_data diff --git a/freqtrade/persistence/custom_data_middleware.py b/freqtrade/persistence/custom_data_middleware.py deleted file mode 100644 index acc65606b..000000000 --- a/freqtrade/persistence/custom_data_middleware.py +++ /dev/null @@ -1,113 +0,0 @@ -import json -import logging -from typing import Any, List, Optional - -from freqtrade.persistence.custom_data import CustomData -from freqtrade.util import dt_now - - -logger = logging.getLogger(__name__) - - -class CustomDataWrapper: - """ - CustomData middleware class - Abstracts the database layer away so it becomes optional - which will be necessary to support - backtesting and hyperopt in the future. - """ - - use_db = True - custom_data: List[CustomData] = [] - unserialized_types = ['bool', 'float', 'int', 'str'] - - @staticmethod - def reset_custom_data() -> None: - """ - Resets all key-value pairs. Only active for backtesting mode. - """ - if not CustomDataWrapper.use_db: - CustomDataWrapper.custom_data = [] - - @staticmethod - def get_custom_data(key: Optional[str] = None, - trade_id: Optional[int] = None) -> CustomData: - if trade_id is None: - trade_id = 0 - - if CustomDataWrapper.use_db: - filtered_custom_data = [] - for data_entry in CustomData.query_cd(trade_id=trade_id, key=key): - if data_entry.cd_type not in CustomDataWrapper.unserialized_types: - data_entry.cd_value = json.loads(data_entry.cd_value) - filtered_custom_data.append(data_entry) - return filtered_custom_data - else: - filtered_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 - if (data_entry.cd_key.casefold() == key.casefold()) - ] - return filtered_custom_data - - @staticmethod - def set_custom_data(key: str, value: Any, trade_id: Optional[int] = None) -> None: - - value_type = type(value).__name__ - value_db = None - - if value_type not in CustomDataWrapper.unserialized_types: - try: - value_db = json.dumps(value) - except TypeError as e: - logger.warning(f"could not serialize {key} value due to {e}") - else: - value_db = str(value) - - if trade_id is None: - trade_id = 0 - - custom_data = CustomDataWrapper.get_custom_data(key=key, trade_id=trade_id) - if custom_data: - data_entry = custom_data[0] - data_entry.cd_value = value_db - data_entry.updated_at = dt_now() - else: - data_entry = CustomData( - ft_trade_id=trade_id, - cd_key=key, - cd_type=value_type, - cd_value=value_db, - created_at=dt_now() - ) - - if CustomDataWrapper.use_db and value_db is not None: - data_entry.cd_value = value_db - CustomData.session.add(data_entry) - CustomData.session.commit() - elif not CustomDataWrapper.use_db: - cd_index = -1 - for index, data_entry in enumerate(CustomDataWrapper.custom_data): - if data_entry.ft_trade_id == trade_id and data_entry.cd_key == key: - cd_index = index - break - - if cd_index >= 0: - data_entry.cd_type = value_type - data_entry.cd_value = value_db - data_entry.updated_at = dt_now() - - CustomDataWrapper.custom_data[cd_index] = data_entry - else: - CustomDataWrapper.custom_data.append(data_entry) - - @staticmethod - def get_all_custom_data() -> List[CustomData]: - - if CustomDataWrapper.use_db: - return list(CustomData.query_cd()) - else: - return CustomDataWrapper.custom_data diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index ea03e2c29..7487c72b3 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -23,8 +23,7 @@ from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precisi from freqtrade.leverage import interest from freqtrade.misc import safe_value_fallback from freqtrade.persistence.base import ModelBase, SessionType -from freqtrade.persistence.custom_data import CustomData -from freqtrade.persistence.custom_data_middleware import CustomDataWrapper +from freqtrade.persistence.custom_data import CustomData, CustomDataWrapper from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts @@ -345,7 +344,7 @@ class LocalTrade: id: int = 0 orders: List[Order] = [] - custom_data: List[CustomData] = [] + custom_data: List[_CustomData] = [] exchange: str = '' pair: str = '' @@ -1209,7 +1208,7 @@ class LocalTrade: def set_custom_data(self, key: str, value: Any) -> None: CustomDataWrapper.set_custom_data(key=key, value=value, trade_id=self.id) - def get_custom_data(self, key: Optional[str]) -> List[CustomData]: + def get_custom_data(self, key: Optional[str]) -> List[_CustomData]: return CustomDataWrapper.get_custom_data(key=key, trade_id=self.id) @property @@ -1467,7 +1466,7 @@ class Trade(ModelBase, LocalTrade): orders: Mapped[List[Order]] = relationship( "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin", innerjoin=True) # type: ignore - custom_data: Mapped[List[CustomData]] = relationship( + custom_data: Mapped[List[_CustomData]] = relationship( "CustomData", order_by="CustomData.id", cascade="all, delete-orphan", lazy="raise") # type: ignore @@ -1574,9 +1573,9 @@ class Trade(ModelBase, LocalTrade): Order.session.delete(order) for entry in self.custom_data: - CustomData.session.delete(entry) + _CustomData.session.delete(entry) - CustomData.session.commit() + _CustomData.session.commit() Trade.session.delete(self) Trade.commit() diff --git a/freqtrade/persistence/usedb_context.py b/freqtrade/persistence/usedb_context.py index 193f7021d..732f0b0f8 100644 --- a/freqtrade/persistence/usedb_context.py +++ b/freqtrade/persistence/usedb_context.py @@ -1,5 +1,5 @@ -from freqtrade.persistence.custom_data_middleware import CustomDataWrapper +from freqtrade.persistence.custom_data import CustomDataWrapper from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.persistence.trade_model import Trade From 8dda28351e9e6728877a169553431e3f94ad3092 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 20:25:26 +0100 Subject: [PATCH 140/327] Simplify custom_data stuff --- freqtrade/persistence/custom_data.py | 57 ++++++++++++++-------------- freqtrade/persistence/models.py | 6 +-- freqtrade/persistence/trade_model.py | 24 ++++++++++-- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index bf6056278..e8fa0d960 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -14,7 +14,7 @@ from freqtrade.util import dt_now logger = logging.getLogger(__name__) -class CustomData(ModelBase): +class _CustomData(ModelBase): """ CustomData database model Keeps records of metadata as key/value store @@ -41,6 +41,9 @@ class CustomData(ModelBase): created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=dt_now) updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) + # Empty container value - not persisted, but filled with cd_value on query + value: Any = None + def __repr__(self): create_time = (self.created_at.strftime(DATETIME_PRINT_FORMAT) if self.created_at is not None else None) @@ -52,7 +55,7 @@ class CustomData(ModelBase): @classmethod def query_cd(cls, key: Optional[str] = None, - trade_id: Optional[int] = None) -> Sequence['CustomData']: + 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 @@ -60,11 +63,11 @@ class CustomData(ModelBase): """ filters = [] if trade_id is not None: - filters.append(CustomData.ft_trade_id == trade_id) + filters.append(_CustomData.ft_trade_id == trade_id) if key is not None: - filters.append(CustomData.cd_key.ilike(key)) + filters.append(_CustomData.cd_key.ilike(key)) - return CustomData.session.scalars(select(CustomData).filter(*filters)).all() + return _CustomData.session.scalars(select(_CustomData).filter(*filters)).all() class CustomDataWrapper: @@ -75,9 +78,15 @@ class CustomDataWrapper: """ use_db = True - custom_data: List[CustomData] = [] + custom_data: List[_CustomData] = [] unserialized_types = ['bool', 'float', 'int', 'str'] + @staticmethod + def _convert_custom_data(data: _CustomData) -> _CustomData: + if data.cd_type not in CustomDataWrapper.unserialized_types: + data.value = json.loads(data.cd_value) + return data + @staticmethod def reset_custom_data() -> None: """ @@ -88,17 +97,15 @@ class CustomDataWrapper: @staticmethod def get_custom_data(key: Optional[str] = None, - trade_id: Optional[int] = None) -> CustomData: + trade_id: Optional[int] = None) -> List[_CustomData]: if trade_id is None: trade_id = 0 if CustomDataWrapper.use_db: - filtered_custom_data = [] - for data_entry in CustomData.query_cd(trade_id=trade_id, key=key): - if data_entry.cd_type not in CustomDataWrapper.unserialized_types: - data_entry.cd_value = json.loads(data_entry.cd_value) - filtered_custom_data.append(data_entry) - return filtered_custom_data + filtered_custom_data = _CustomData.session.scalars(select(_CustomData).filter( + _CustomData.ft_trade_id == trade_id, + _CustomData.cd_key.ilike(key))).all() + else: filtered_custom_data = [ data_entry for data_entry in CustomDataWrapper.custom_data @@ -109,19 +116,19 @@ class CustomDataWrapper: data_entry for data_entry in filtered_custom_data if (data_entry.cd_key.casefold() == key.casefold()) ] - return filtered_custom_data + return [CustomDataWrapper._convert_custom_data(d) for d in filtered_custom_data] @staticmethod def set_custom_data(key: str, value: Any, trade_id: Optional[int] = None) -> None: value_type = type(value).__name__ - value_db = None if value_type not in CustomDataWrapper.unserialized_types: try: value_db = json.dumps(value) except TypeError as e: logger.warning(f"could not serialize {key} value due to {e}") + return else: value_db = str(value) @@ -134,19 +141,19 @@ class CustomDataWrapper: data_entry.cd_value = value_db data_entry.updated_at = dt_now() else: - data_entry = CustomData( + data_entry = _CustomData( ft_trade_id=trade_id, cd_key=key, cd_type=value_type, cd_value=value_db, - created_at=dt_now() + created_at=dt_now(), ) + data_entry.value = value if CustomDataWrapper.use_db and value_db is not None: - data_entry.cd_value = value_db - CustomData.session.add(data_entry) - CustomData.session.commit() - elif not CustomDataWrapper.use_db: + _CustomData.session.add(data_entry) + _CustomData.session.commit() + else: cd_index = -1 for index, data_entry in enumerate(CustomDataWrapper.custom_data): if data_entry.ft_trade_id == trade_id and data_entry.cd_key == key: @@ -161,11 +168,3 @@ class CustomDataWrapper: CustomDataWrapper.custom_data[cd_index] = data_entry else: CustomDataWrapper.custom_data.append(data_entry) - - @staticmethod - def get_all_custom_data() -> List[CustomData]: - - if CustomDataWrapper.use_db: - return list(CustomData.query_cd()) - else: - return CustomDataWrapper.custom_data diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 189b80fa6..1a69b271c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -13,7 +13,7 @@ from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException from freqtrade.persistence.base import ModelBase -from freqtrade.persistence.custom_data import CustomData +from freqtrade.persistence.custom_data import _CustomData from freqtrade.persistence.key_value_store import _KeyValueStoreModel from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.pairlock import PairLock @@ -79,8 +79,8 @@ def init_db(db_url: str) -> None: 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/trade_model.py b/freqtrade/persistence/trade_model.py index 7487c72b3..55a075cc9 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -23,7 +23,7 @@ from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precisi from freqtrade.leverage import interest from freqtrade.misc import safe_value_fallback from freqtrade.persistence.base import ModelBase, SessionType -from freqtrade.persistence.custom_data import CustomData, CustomDataWrapper +from freqtrade.persistence.custom_data import CustomDataWrapper, _CustomData from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts @@ -1206,10 +1206,28 @@ class LocalTrade: ] def set_custom_data(self, key: str, value: Any) -> None: + """ + Set custom data for this trade + :param key: key of the custom data + :param value: value of the custom data (must be JSON serializable) + """ CustomDataWrapper.set_custom_data(key=key, value=value, trade_id=self.id) - def get_custom_data(self, key: Optional[str]) -> List[_CustomData]: - return CustomDataWrapper.get_custom_data(key=key, trade_id=self.id) + def get_custom_data(self, key: str) -> Optional[_CustomData]: + """ + Get custom data for this trade + :param key: key of the custom data + """ + data = CustomDataWrapper.get_custom_data(key=key, trade_id=self.id) + if data: + return data[0] + return None + + def get_all_custom_data(self) -> List[_CustomData]: + """ + Get all custom data for this trade + """ + return CustomDataWrapper.get_custom_data(trade_id=self.id) @property def nr_of_successful_entries(self) -> int: From 790c7e386a3f2146cbee39e812c7f78fd9a551ed Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 20:27:56 +0100 Subject: [PATCH 141/327] re-adjust logic for custom_data in rpc module --- freqtrade/rpc/rpc.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 0c34c19ec..34d33ecde 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1005,7 +1005,13 @@ class RPC: if trade is None: return [] # Query custom_data - custom_data = trade.get_custom_data(key=key) + custom_data = [] + if key: + data = trade.get_custom_data(key=key) + if data: + custom_data = [data] + else: + custom_data = trade.get_all_custom_data() return [ { 'id': data_entry.id, From 8364a704d6c14fc0b468c3dfce7245aeb994a7e0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 20:31:44 +0100 Subject: [PATCH 142/327] Fix a few sql gotchas --- freqtrade/persistence/custom_data.py | 1 + freqtrade/persistence/trade_model.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index e8fa0d960..bf72510be 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -24,6 +24,7 @@ class _CustomData(ModelBase): - One metadata entry can only be associated with one Trade """ __tablename__ = 'trade_custom_data' + __allow_unmapped__ = True session: ClassVar[SessionType] # Uniqueness should be ensured over pair, order_id diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 55a075cc9..1aa7cb607 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1485,7 +1485,7 @@ class Trade(ModelBase, LocalTrade): "Order", order_by="Order.id", cascade="all, delete-orphan", lazy="selectin", innerjoin=True) # type: ignore custom_data: Mapped[List[_CustomData]] = relationship( - "CustomData", order_by="CustomData.id", cascade="all, delete-orphan", + "_CustomData", cascade="all, delete-orphan", lazy="raise") # type: ignore exchange: Mapped[str] = mapped_column(String(25), nullable=False) # type: ignore From 67b910835e05b76c170f406895a0c66a784742c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 20:39:29 +0100 Subject: [PATCH 143/327] Simplify access to custom_data - users will usually only care about the value, not about the metadata. --- docs/strategy-advanced.md | 17 +++++++++++++---- freqtrade/persistence/trade_model.py | 12 +++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 69d4ced34..9f0b3c112 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -65,7 +65,7 @@ class AwesomeStrategy(IStrategy): for trade in Trade.get_open_order_trades(): fills = trade.select_filled_orders(trade.entry_side) if trade.pair == 'ETH/USDT': - trade_entry_type = trade.get_custom_data(key='entry_type').kv_value + trade_entry_type = trade.get_custom_data(key='entry_type') if trade_entry_type is None: trade_entry_type = 'breakout' if 'entry_1' in trade.enter_tag else 'dip' elif fills > 1: @@ -87,7 +87,7 @@ class AwesomeStrategy(IStrategy): dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) current_candle = dataframe.iloc[-1].squeeze() # store information about entry adjustment - existing_count = trade.get_custom_data(key='num_entry_adjustments').kv_value + existing_count = trade.get_custom_data('num_entry_adjustments', default=0) if not existing_count: existing_count = 1 else: @@ -102,8 +102,8 @@ class AwesomeStrategy(IStrategy): def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs): - entry_adjustment_count = trade.get_custom_data(key='num_entry_adjustments').kv_value - trade_entry_type = trade.get_custom_data(key='entry_type').kv_value + entry_adjustment_count = trade.get_custom_data(key='num_entry_adjustments') + trade_entry_type = trade.get_custom_data(key='entry_type') if entry_adjustment_count is None: if current_profit > 0.01 and (current_time - timedelta(minutes=100) > trade.open_date_utc): return True, 'exit_1' @@ -123,6 +123,15 @@ class AwesomeStrategy(IStrategy): !!! Warning "Non-serializable data" If supplied data cannot be serialized a warning is logged and the entry for the specified `key` will contain `None` as data. +??? Note "All attributes" + custom-data has the following accessors through the Trade object (assumed as `trade` below): + + * `trade.get_custom_data(key='something', default=0)` - Returns the actual value given in the type provided. + * `trade.get_custom_data_entry(key='something')` - Returns the entry - including metadata. The value is accessible via `.value` property. + * `trade.set_custom_data(key='something', value={'some': 'value'})` - set or update the corresponding key for this trade. Value must be serializable - and we recommend to keep the stored data relatively small. + + "value" can be any type (both in setting and receiving) - but must be json serializable. + ## Dataframe access You may access dataframe in various strategy functions by querying it from dataprovider. diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 1aa7cb607..abbc69f75 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1213,7 +1213,17 @@ class LocalTrade: """ CustomDataWrapper.set_custom_data(key=key, value=value, trade_id=self.id) - def get_custom_data(self, key: str) -> Optional[_CustomData]: + def get_custom_data(self, key: str, default: Any = None) -> Any: + """ + Get custom data for this trade + :param key: key of the custom data + """ + data = CustomDataWrapper.get_custom_data(key=key, trade_id=self.id) + if data: + return data[0] + return default + + def get_custom_data_entry(self, key: str) -> Optional[_CustomData]: """ Get custom data for this trade :param key: key of the custom data From 6a6e3aacf33da282006d56d775e301ee33d8274b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Feb 2024 20:51:44 +0100 Subject: [PATCH 144/327] Fix broken deletion --- freqtrade/persistence/custom_data.py | 4 ++++ freqtrade/persistence/trade_model.py | 3 +-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index bf72510be..0eb14738c 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -96,6 +96,10 @@ class CustomDataWrapper: if not CustomDataWrapper.use_db: CustomDataWrapper.custom_data = [] + @staticmethod + def delete_custom_data(trade_id: int) -> None: + _CustomData.session.query(_CustomData).filter(_CustomData.ft_trade_id == trade_id).delete() + @staticmethod def get_custom_data(key: Optional[str] = None, trade_id: Optional[int] = None) -> List[_CustomData]: diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index abbc69f75..a12a842c3 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1600,8 +1600,7 @@ class Trade(ModelBase, LocalTrade): for order in self.orders: Order.session.delete(order) - for entry in self.custom_data: - _CustomData.session.delete(entry) + CustomDataWrapper.delete_custom_data(trade_id=self.id) _CustomData.session.commit() Trade.session.delete(self) From 304f52ab79c899a9b6f6cfeed95f125efcac0c89 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Feb 2024 07:06:32 +0100 Subject: [PATCH 145/327] Fix some minor custom-data bugs --- freqtrade/persistence/custom_data.py | 27 +++++++++++++++++++-------- freqtrade/persistence/trade_model.py | 8 ++++---- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index 0eb14738c..f5d6587bf 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -32,7 +32,7 @@ class _CustomData(ModelBase): __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, default=0) + ft_trade_id = mapped_column(Integer, ForeignKey('trades.id'), index=True) trade = relationship("Trade", back_populates="custom_data") @@ -84,7 +84,15 @@ class CustomDataWrapper: @staticmethod def _convert_custom_data(data: _CustomData) -> _CustomData: - if data.cd_type not in CustomDataWrapper.unserialized_types: + 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': + data.value = int(data.cd_value) + elif data.cd_type == 'float': + data.value = float(data.cd_value) + else: data.value = json.loads(data.cd_value) return data @@ -101,15 +109,18 @@ class CustomDataWrapper: _CustomData.session.query(_CustomData).filter(_CustomData.ft_trade_id == trade_id).delete() @staticmethod - def get_custom_data(key: Optional[str] = None, - trade_id: Optional[int] = None) -> List[_CustomData]: + def get_custom_data(*, trade_id: int, key: Optional[str] = None) -> List[_CustomData]: if trade_id is None: trade_id = 0 if CustomDataWrapper.use_db: - filtered_custom_data = _CustomData.session.scalars(select(_CustomData).filter( + filters = [ _CustomData.ft_trade_id == trade_id, - _CustomData.cd_key.ilike(key))).all() + ] + if key is not None: + filters.append(_CustomData.cd_key.ilike(key)) + filtered_custom_data = _CustomData.session.scalars(select(_CustomData).filter( + *filters)).all() else: filtered_custom_data = [ @@ -124,7 +135,7 @@ class CustomDataWrapper: return [CustomDataWrapper._convert_custom_data(d) for d in filtered_custom_data] @staticmethod - def set_custom_data(key: str, value: Any, trade_id: Optional[int] = None) -> None: + def set_custom_data(trade_id: int, key: str, value: Any) -> None: value_type = type(value).__name__ @@ -140,7 +151,7 @@ class CustomDataWrapper: if trade_id is None: trade_id = 0 - custom_data = CustomDataWrapper.get_custom_data(key=key, trade_id=trade_id) + custom_data = CustomDataWrapper.get_custom_data(trade_id=trade_id, key=key) if custom_data: data_entry = custom_data[0] data_entry.cd_value = value_db diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index a12a842c3..d2a92a554 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -1211,16 +1211,16 @@ class LocalTrade: :param key: key of the custom data :param value: value of the custom data (must be JSON serializable) """ - CustomDataWrapper.set_custom_data(key=key, value=value, trade_id=self.id) + CustomDataWrapper.set_custom_data(trade_id=self.id, key=key, value=value) def get_custom_data(self, key: str, default: Any = None) -> Any: """ Get custom data for this trade :param key: key of the custom data """ - data = CustomDataWrapper.get_custom_data(key=key, trade_id=self.id) + data = CustomDataWrapper.get_custom_data(trade_id=self.id, key=key) if data: - return data[0] + return data[0].value return default def get_custom_data_entry(self, key: str) -> Optional[_CustomData]: @@ -1228,7 +1228,7 @@ class LocalTrade: Get custom data for this trade :param key: key of the custom data """ - data = CustomDataWrapper.get_custom_data(key=key, trade_id=self.id) + data = CustomDataWrapper.get_custom_data(trade_id=self.id, key=key) if data: return data[0] return None From 9be7759e423f9e4d5382c3176225aae8b20a1d8f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Feb 2024 07:10:11 +0100 Subject: [PATCH 146/327] Add initial test for custom_data --- tests/persistence/test_trade_custom_data.py | 34 +++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 tests/persistence/test_trade_custom_data.py diff --git a/tests/persistence/test_trade_custom_data.py b/tests/persistence/test_trade_custom_data.py new file mode 100644 index 000000000..d411a898f --- /dev/null +++ b/tests/persistence/test_trade_custom_data.py @@ -0,0 +1,34 @@ +import pytest +from sqlalchemy import select + +from freqtrade.persistence import Trade +from tests.conftest import create_mock_trades_usdt + + +@pytest.mark.usefixtures("init_persistence") +def test_trade_custom_data(fee): + create_mock_trades_usdt(fee) + + trade1 = Trade.session.scalars(select(Trade)).first() + + assert trade1.get_all_custom_data() == [] + trade1.set_custom_data('test_str', 'test_value') + trade1.set_custom_data('test_int', 1) + trade1.set_custom_data('test_float', 1.55) + trade1.set_custom_data('test_bool', True) + trade1.set_custom_data('test_dict', {'test': 'dict'}) + + assert trade1.get_custom_data('test_str') == 'test_value' + + assert trade1.get_custom_data('test_int') == 1 + assert isinstance(trade1.get_custom_data('test_int'), int) + + assert trade1.get_custom_data('test_float') == 1.55 + assert isinstance(trade1.get_custom_data('test_float'), float) + + assert trade1.get_custom_data('test_bool') is True + assert isinstance(trade1.get_custom_data('test_bool'), bool) + + assert trade1.get_custom_data('test_dict') == {'test': 'dict'} + assert isinstance(trade1.get_custom_data('test_dict'), dict) + assert len(trade1.get_all_custom_data()) == 5 From d5b21f2a32e9acc8460ea32b9030b2b67288a4bd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Feb 2024 07:16:09 +0100 Subject: [PATCH 147/327] Fix bug in backtest mode --- freqtrade/persistence/custom_data.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index f5d6587bf..3ebcd0f48 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -110,8 +110,6 @@ class CustomDataWrapper: @staticmethod def get_custom_data(*, trade_id: int, key: Optional[str] = None) -> List[_CustomData]: - if trade_id is None: - trade_id = 0 if CustomDataWrapper.use_db: filters = [ @@ -170,17 +168,6 @@ class CustomDataWrapper: _CustomData.session.add(data_entry) _CustomData.session.commit() else: - cd_index = -1 - for index, data_entry in enumerate(CustomDataWrapper.custom_data): - if data_entry.ft_trade_id == trade_id and data_entry.cd_key == key: - cd_index = index - break - - if cd_index >= 0: - data_entry.cd_type = value_type - data_entry.cd_value = value_db - data_entry.updated_at = dt_now() - - CustomDataWrapper.custom_data[cd_index] = data_entry - else: + if not custom_data: CustomDataWrapper.custom_data.append(data_entry) + # Existing data will have updated interactively. From ab062d7bb145f163b78739ee69f2f1228879e3ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Feb 2024 07:16:20 +0100 Subject: [PATCH 148/327] Add test to run in backtest mode --- tests/persistence/test_trade_custom_data.py | 23 +++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/persistence/test_trade_custom_data.py b/tests/persistence/test_trade_custom_data.py index d411a898f..12767b811 100644 --- a/tests/persistence/test_trade_custom_data.py +++ b/tests/persistence/test_trade_custom_data.py @@ -1,15 +1,23 @@ import pytest -from sqlalchemy import select -from freqtrade.persistence import Trade +from freqtrade.persistence import Trade, disable_database_use, enable_database_use +from freqtrade.persistence.custom_data import CustomDataWrapper from tests.conftest import create_mock_trades_usdt @pytest.mark.usefixtures("init_persistence") -def test_trade_custom_data(fee): - create_mock_trades_usdt(fee) +@pytest.mark.parametrize("use_db", [True, False]) +def test_trade_custom_data(fee, use_db): + if not use_db: + disable_database_use('5m') + Trade.reset_trades() + CustomDataWrapper.reset_custom_data() - trade1 = Trade.session.scalars(select(Trade)).first() + create_mock_trades_usdt(fee, use_db=use_db) + + trade1 = Trade.get_trades_proxy()[0] + if not use_db: + trade1.id = 1 assert trade1.get_all_custom_data() == [] trade1.set_custom_data('test_str', 'test_value') @@ -18,7 +26,10 @@ def test_trade_custom_data(fee): trade1.set_custom_data('test_bool', True) trade1.set_custom_data('test_dict', {'test': 'dict'}) + assert len(trade1.get_all_custom_data()) == 5 assert trade1.get_custom_data('test_str') == 'test_value' + trade1.set_custom_data('test_str', 'test_value_updated') + assert trade1.get_custom_data('test_str') == 'test_value_updated' assert trade1.get_custom_data('test_int') == 1 assert isinstance(trade1.get_custom_data('test_int'), int) @@ -31,4 +42,4 @@ def test_trade_custom_data(fee): assert trade1.get_custom_data('test_dict') == {'test': 'dict'} assert isinstance(trade1.get_custom_data('test_dict'), dict) - assert len(trade1.get_all_custom_data()) == 5 + enable_database_use() From 3d6079ae19108f09205001d485974b837e4329a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Feb 2024 17:47:43 +0100 Subject: [PATCH 149/327] Add debug output showing the pair to be converted part of #9811 --- freqtrade/data/converter/trade_converter_kraken.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/data/converter/trade_converter_kraken.py b/freqtrade/data/converter/trade_converter_kraken.py index 614d97b2a..c9848c096 100644 --- a/freqtrade/data/converter/trade_converter_kraken.py +++ b/freqtrade/data/converter/trade_converter_kraken.py @@ -48,6 +48,7 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str): logger.info(f"Converting pairs: {', '.join(m[0] for m in markets)}.") for pair, name in markets: + logger.debug(f"Converting pair {pair}, files */{name}.csv") dfs = [] # Load and combine all csv files for this pair for f in tradesdir.rglob(f"{name}.csv"): From 280737447cdac4806161fe6e0f72a3b808d68b5c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Feb 2024 07:26:23 +0100 Subject: [PATCH 150/327] Don't load empty dataframes - skip these closes #9811 --- freqtrade/data/converter/trade_converter_kraken.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/converter/trade_converter_kraken.py b/freqtrade/data/converter/trade_converter_kraken.py index c9848c096..b0fa11c25 100644 --- a/freqtrade/data/converter/trade_converter_kraken.py +++ b/freqtrade/data/converter/trade_converter_kraken.py @@ -53,7 +53,8 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str): # Load and combine all csv files for this pair for f in tradesdir.rglob(f"{name}.csv"): df = pd.read_csv(f, names=KRAKEN_CSV_TRADE_COLUMNS) - dfs.append(df) + if not df.empty: + dfs.append(df) # Load existing trades data if not dfs: From 57fd0e379abe1161390668c2dd6cd4a8c3925ce1 Mon Sep 17 00:00:00 2001 From: Robert Davey Date: Thu, 15 Feb 2024 15:57:49 +0000 Subject: [PATCH 151/327] Clarify processing_mode for RemotePairlist No default value is specified in the docs for the processing_mode, making it unclear that the default behaviour is to filter out pairs, rather than append. --- docs/includes/pairlists.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 9781edf10..5a6a2560b 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -201,7 +201,7 @@ The RemotePairList is defined in the pairlists section of the configuration sett The optional `mode` option specifies if the pairlist should be used as a `blacklist` or as a `whitelist`. The default value is "whitelist". -The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append". +The optional `processing_mode` option in the RemotePairList configuration determines how the retrieved pairlist is processed. It can have two values: "filter" or "append". The default value is "filter". In "filter" mode, the retrieved pairlist is used as a filter. Only the pairs present in both the original pairlist and the retrieved pairlist are included in the final pairlist. Other pairs are filtered out. From 86da9cb659d126a3a46a4eec2c3a25ab09761a9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Feb 2024 19:27:15 +0100 Subject: [PATCH 152/327] Simplify some pairlist conditions --- freqtrade/plugins/pairlist/VolatilityFilter.py | 3 +-- freqtrade/plugins/pairlist/rangestabilityfilter.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 800bf3664..794df5449 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -125,8 +125,7 @@ class VolatilityFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache - cached_res = self._pair_cache.get(pair, None) - if cached_res is not None: + if (cached_res := self._pair_cache.get(pair, None)) is not None: return cached_res result = False diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index f4625f572..e04772e9c 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -123,8 +123,7 @@ class RangeStabilityFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache - cached_res = self._pair_cache.get(pair, None) - if cached_res is not None: + if (cached_res := self._pair_cache.get(pair, None)) is not None: return cached_res result = True From d01e9cf2990e8bf21199b3aed465387be2da8d3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Feb 2024 19:49:47 +0100 Subject: [PATCH 153/327] Improve log message --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ff04037da..2032e437d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -702,7 +702,7 @@ class FreqtradeBot(LoggingMixin): delta = f"Delta: {bids_ask_delta}" logger.info( - f"{bids}, {asks}, {delta}, Direction: {side.value}" + f"{bids}, {asks}, {delta}, Direction: {side.value} " f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, " f"Immediate Bid Quantity: {order_book['bids'][0][1]}, " f"Immediate Ask Quantity: {order_book['asks'][0][1]}." From 6c9b9e91e8f47b94559c5ab9862b243c8165437e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Feb 2024 06:39:07 +0100 Subject: [PATCH 154/327] enhance volumpairlist range test --- tests/plugins/test_pairlist.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 09dcd0af3..55d65d3c7 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -626,8 +626,9 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t # "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}], # "BTC", "ftx", ['HOT/BTC', 'LTC/BTC', 'ETH/BTC', 'TKN/BTC', 'XRP/BTC']), ]) -def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history, - pairlists, base_currency, exchange, volumefilter_result) -> None: +def test_VolumePairList_range( + mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history, + pairlists, base_currency, exchange, volumefilter_result, time_machine) -> None: whitelist_conf['pairlists'] = pairlists whitelist_conf['stake_currency'] = base_currency whitelist_conf['exchange']['name'] = exchange @@ -686,23 +687,35 @@ def test_VolumePairList_range(mocker, whitelist_conf, shitcoinmarkets, tickers, get_tickers=tickers, markets=PropertyMock(return_value=shitcoinmarkets) ) - + start_dt = dt_now() + time_machine.move_to(start_dt) # remove ohlcv when looback_timeframe != 1d # to enforce fallback to ticker data if 'lookback_timeframe' in pairlists[0]: if pairlists[0]['lookback_timeframe'] != '1d': ohlcv_data = [] - mocker.patch.multiple( - EXMS, - refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), - ) + ohclv_mock = mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value=ohlcv_data) freqtrade.pairlists.refresh_pairlist() whitelist = freqtrade.pairlists.whitelist + assert ohclv_mock.call_count == 1 assert isinstance(whitelist, list) assert whitelist == volumefilter_result + # Test caching + ohclv_mock.reset_mock() + freqtrade.pairlists.refresh_pairlist() + assert ohclv_mock.call_count == 0 + whitelist = freqtrade.pairlists.whitelist + assert whitelist == volumefilter_result + + time_machine.move_to(start_dt + timedelta(days=2)) + ohclv_mock.reset_mock() + freqtrade.pairlists.refresh_pairlist() + assert ohclv_mock.call_count == 1 + whitelist = freqtrade.pairlists.whitelist + assert whitelist == volumefilter_result def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: From 7f7e9ec8756b2bd2b555e4b14b4ba0c5af476baa Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Feb 2024 06:42:57 +0100 Subject: [PATCH 155/327] Add additional test case for VolumePairlist in range mode --- tests/plugins/test_pairlist.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 55d65d3c7..32f6abb51 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -621,6 +621,12 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "lookback_timeframe": "1d", "lookback_period": 6, "refresh_period": 86400}], "BTC", "binance", ['LTC/BTC', 'XRP/BTC', 'ETH/BTC', 'HOT/BTC', 'NEO/BTC']), + # VolumePairlist in range mode as filter. + # TKN/BTC is removed because it doesn't have enough candles + ([{"method": "VolumePairList", "number_assets": 5}, + {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", + "lookback_timeframe": "1d", "lookback_period": 2, "refresh_period": 86400}], + "BTC", "binance", ['LTC/BTC', 'XRP/BTC', 'ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # ftx data is already in Quote currency, therefore won't require conversion # ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", # "lookback_timeframe": "1d", "lookback_period": 1, "refresh_period": 86400}], @@ -693,7 +699,7 @@ def test_VolumePairList_range( # to enforce fallback to ticker data if 'lookback_timeframe' in pairlists[0]: if pairlists[0]['lookback_timeframe'] != '1d': - ohlcv_data = [] + ohlcv_data = {} ohclv_mock = mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value=ohlcv_data) @@ -706,7 +712,8 @@ def test_VolumePairList_range( # Test caching ohclv_mock.reset_mock() freqtrade.pairlists.refresh_pairlist() - assert ohclv_mock.call_count == 0 + # in "filter" mode, caching is disabled. + assert ohclv_mock.call_count == (0 if len(pairlists) == 1 else 1) whitelist = freqtrade.pairlists.whitelist assert whitelist == volumefilter_result From a22181d721ea465ce27fca653cff56a00b66eae8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Feb 2024 18:16:55 +0100 Subject: [PATCH 156/327] Enable caching for "filter only" Volumepairlist --- freqtrade/plugins/pairlist/VolumePairList.py | 16 ++++++++++++---- tests/plugins/test_pairlist.py | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index b5525e950..671ba4db4 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -14,7 +14,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange.types import Tickers from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter -from freqtrade.util import dt_now, format_ms_time +from freqtrade.util import PeriodicCache, dt_now, format_ms_time logger = logging.getLogger(__name__) @@ -63,6 +63,7 @@ class VolumePairList(IPairList): # get timeframe in minutes and seconds self._tf_in_min = timeframe_to_minutes(self._lookback_timeframe) _tf_in_sec = self._tf_in_min * 60 + self._candle_cache = PeriodicCache(maxsize=1000, ttl=_tf_in_sec) # wether to use range lookback or not self._use_range = (self._tf_in_min > 0) & (self._lookback_period > 0) @@ -230,11 +231,18 @@ class VolumePairList(IPairList): ] # Get all candles - candles = {} - if needed_pairs: + candles = { + c: self._candle_cache.get(c, None) for c in needed_pairs + if c in self._candle_cache + } + pairs_to_download = [p for p in needed_pairs if p not in candles] + if pairs_to_download: candles = self._exchange.refresh_latest_ohlcv( - needed_pairs, since_ms=since_ms, cache=False + pairs_to_download, since_ms=since_ms, cache=False ) + for c, val in candles.items(): + self._candle_cache[c] = val + for i, p in enumerate(filtered_tickers): contract_size = self._exchange.markets[p['symbol']].get('contractSize', 1.0) or 1.0 pair_candles = candles[ diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 32f6abb51..d125f8896 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -713,7 +713,7 @@ def test_VolumePairList_range( ohclv_mock.reset_mock() freqtrade.pairlists.refresh_pairlist() # in "filter" mode, caching is disabled. - assert ohclv_mock.call_count == (0 if len(pairlists) == 1 else 1) + assert ohclv_mock.call_count == 0 whitelist = freqtrade.pairlists.whitelist assert whitelist == volumefilter_result From c1d71848490a8530b4e5ef66f21a60baa5c2e6b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Feb 2024 19:20:08 +0100 Subject: [PATCH 157/327] Adjust for ccxt exception hierarchy change caused by https://github.com/ccxt/ccxt/pull/21035 --- freqtrade/exchange/exchange.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 85a77fe5e..4c142a517 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1242,7 +1242,7 @@ class Exchange: f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' f'Tried to {side} amount {amount} at rate {limit_rate} with ' f'stop-price {stop_price_norm}. Message: {e}') from e - except (ccxt.InvalidOrder, ccxt.BadRequest) as e: + except (ccxt.InvalidOrder, ccxt.BadRequest, ccxt.OperationRejected) as e: # Errors: # `Order would trigger immediately.` raise InvalidOrderException( @@ -2685,7 +2685,7 @@ class Exchange: self._log_exchange_response('set_leverage', res) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except (ccxt.BadRequest, ccxt.InsufficientFunds) as e: + except (ccxt.BadRequest, ccxt.OperationRejected, ccxt.InsufficientFunds) as e: if not accept_fail: raise TemporaryError( f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e @@ -2727,7 +2727,7 @@ class Exchange: self._log_exchange_response('set_margin_mode', res) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except ccxt.BadRequest as e: + except (ccxt.BadRequest, ccxt.OperationRejected) as e: if not accept_fail: raise TemporaryError( f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e From c6d1c1a980ed9f587e16fa0b9df38d075b185545 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Feb 2024 19:50:56 +0100 Subject: [PATCH 158/327] Add dt_ts_none helper --- freqtrade/util/__init__.py | 5 +++-- freqtrade/util/datetime_helpers.py | 11 ++++++++++- tests/utils/test_datetime_helpers.py | 11 +++++++++-- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/freqtrade/util/__init__.py b/freqtrade/util/__init__.py index 513406fd2..f7e63d9d3 100644 --- a/freqtrade/util/__init__.py +++ b/freqtrade/util/__init__.py @@ -1,6 +1,6 @@ from freqtrade.util.datetime_helpers import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts, - dt_ts_def, dt_utc, format_date, format_ms_time, - shorten_date) + dt_ts_def, dt_ts_none, dt_utc, format_date, + format_ms_time, shorten_date) from freqtrade.util.formatters import decimals_per_coin, fmt_coin, round_value from freqtrade.util.ft_precise import FtPrecise from freqtrade.util.periodic_cache import PeriodicCache @@ -14,6 +14,7 @@ __all__ = [ 'dt_now', 'dt_ts', 'dt_ts_def', + 'dt_ts_none', 'dt_utc', 'format_date', 'format_ms_time', diff --git a/freqtrade/util/datetime_helpers.py b/freqtrade/util/datetime_helpers.py index 102c83143..973a1c51b 100644 --- a/freqtrade/util/datetime_helpers.py +++ b/freqtrade/util/datetime_helpers.py @@ -31,12 +31,21 @@ def dt_ts(dt: Optional[datetime] = None) -> int: def dt_ts_def(dt: Optional[datetime], default: int = 0) -> int: """ Return dt in ms as a timestamp in UTC. - If dt is None, return the current datetime in UTC. + If dt is None, return the given default. """ if dt: return int(dt.timestamp() * 1000) return default +def dt_ts_none(dt: Optional[datetime]) -> Optional[int]: + """ + Return dt in ms as a timestamp in UTC. + If dt is None, return the given default. + """ + if dt: + return int(dt.timestamp() * 1000) + return None + def dt_floor_day(dt: datetime) -> datetime: """Return the floor of the day for the given datetime.""" diff --git a/tests/utils/test_datetime_helpers.py b/tests/utils/test_datetime_helpers.py index b70065645..6fbe75200 100644 --- a/tests/utils/test_datetime_helpers.py +++ b/tests/utils/test_datetime_helpers.py @@ -3,8 +3,8 @@ from datetime import datetime, timedelta, timezone import pytest import time_machine -from freqtrade.util import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts, dt_ts_def, dt_utc, - format_date, format_ms_time, shorten_date) +from freqtrade.util import (dt_floor_day, dt_from_ts, dt_humanize, dt_now, dt_ts, dt_ts_def, + dt_ts_none, dt_utc, format_date, format_ms_time, shorten_date) def test_dt_now(): @@ -29,6 +29,13 @@ def test_dt_ts_def(): assert dt_ts_def(datetime(2023, 5, 5, tzinfo=timezone.utc), 123) == 1683244800000 +def test_dt_ts_none(): + assert dt_ts_none(None) is None + assert dt_ts_none(None) is None + assert dt_ts_none(datetime(2023, 5, 5, tzinfo=timezone.utc)) == 1683244800000 + assert dt_ts_none(datetime(2023, 5, 5, tzinfo=timezone.utc)) == 1683244800000 + + def test_dt_utc(): assert dt_utc(2023, 5, 5) == datetime(2023, 5, 5, tzinfo=timezone.utc) assert dt_utc(2023, 5, 5, 0, 0, 0, 555500) == datetime(2023, 5, 5, 0, 0, 0, 555500, From 0f85ef09973a0f32df6488a3873f591888364d88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Feb 2024 19:52:50 +0100 Subject: [PATCH 159/327] Simplify trade_model serializations --- freqtrade/persistence/trade_model.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index a90d9ab2d..84c11f02c 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -23,7 +23,7 @@ from freqtrade.exchange import (ROUND_DOWN, ROUND_UP, amount_to_contract_precisi from freqtrade.leverage import interest from freqtrade.misc import safe_value_fallback from freqtrade.persistence.base import ModelBase, SessionType -from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts +from freqtrade.util import FtPrecise, dt_from_ts, dt_now, dt_ts, dt_ts_none logger = logging.getLogger(__name__) @@ -224,8 +224,7 @@ class Order(ModelBase): 'amount': self.safe_amount, 'safe_price': self.safe_price, 'ft_order_side': self.ft_order_side, - 'order_filled_timestamp': int(self.order_filled_date.replace( - tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None, + '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, } @@ -625,15 +624,14 @@ class LocalTrade: 'fee_close_currency': self.fee_close_currency, 'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT), - 'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000), + 'open_timestamp': dt_ts_none(self.open_date_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': int(self.close_date.replace( - tzinfo=timezone.utc).timestamp() * 1000) 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, @@ -659,8 +657,7 @@ class LocalTrade: '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': int(self.stoploss_last_update_utc.timestamp() * 1000 - ) 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), From fb54c9ffe4a4623e62ff36aa0917bd6782391940 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Feb 2024 20:19:08 +0100 Subject: [PATCH 160/327] Add open_fill_date stuff to json schema --- freqtrade/persistence/trade_model.py | 14 ++++++++++++++ freqtrade/rpc/api_server/api_schemas.py | 2 ++ 2 files changed, 16 insertions(+) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 84c11f02c..121a0bd8a 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -460,6 +460,17 @@ class LocalTrade: return self.open_date_utc return max([self.open_date_utc, dt_last_filled]) + @property + def date_entry_fill_utc(self) -> Optional[datetime]: + """ 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])) + ): + return min(filled_date) + return None + @property def open_date_utc(self): return self.open_date.replace(tzinfo=timezone.utc) @@ -625,6 +636,9 @@ class LocalTrade: '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), diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 9919d1a05..3ea9ed4d0 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -288,6 +288,8 @@ class TradeSchema(BaseModel): open_date: str open_timestamp: int + open_fill_date: Optional[str] + open_fill_timestamp: Optional[int] open_rate: float open_rate_requested: Optional[float] = None open_trade_value: float From 1696aa391504cb2ade22a9124d17d7be27eea696 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Feb 2024 20:39:06 +0100 Subject: [PATCH 161/327] Adjust tests for new fields --- tests/persistence/test_persistence.py | 4 ++++ tests/persistence/test_trade_fromjson.py | 2 +- tests/rpc/test_rpc.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 4 ++++ 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 95db7bc0f..0e0e70ee8 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -1400,6 +1400,8 @@ def test_to_json(fee): 'is_open': None, 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), 'open_timestamp': int(trade.open_date.timestamp() * 1000), + 'open_fill_date': None, + 'open_fill_timestamp': None, 'close_date': None, 'close_timestamp': None, 'open_rate': 0.123, @@ -1486,6 +1488,8 @@ def test_to_json(fee): 'quote_currency': 'BTC', 'open_date': trade.open_date.strftime(DATETIME_PRINT_FORMAT), 'open_timestamp': int(trade.open_date.timestamp() * 1000), + 'open_fill_date': None, + 'open_fill_timestamp': None, 'close_date': trade.close_date.strftime(DATETIME_PRINT_FORMAT), 'close_timestamp': int(trade.close_date.timestamp() * 1000), 'open_rate': 0.123, diff --git a/tests/persistence/test_trade_fromjson.py b/tests/persistence/test_trade_fromjson.py index 302a81c54..988f7ed5b 100644 --- a/tests/persistence/test_trade_fromjson.py +++ b/tests/persistence/test_trade_fromjson.py @@ -223,7 +223,7 @@ def test_trade_serialize_load_back(fee): 'realized_profit_ratio', 'close_profit_pct', 'trade_duration_s', 'trade_duration', 'profit_ratio', 'profit_pct', 'profit_abs', 'stop_loss_abs', - 'initial_stop_loss_abs', + 'initial_stop_loss_abs', 'open_fill_date', 'open_fill_timestamp', 'orders', ] failed = [] diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 1f51b30df..85b105892 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -25,6 +25,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'quote_currency': 'BTC', 'open_date': ANY, 'open_timestamp': ANY, + 'open_fill_date': ANY, + 'open_fill_timestamp': ANY, 'is_open': ANY, 'fee_open': ANY, 'fee_open_cost': ANY, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index bba18bcd3..e441b127b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1165,6 +1165,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'current_rate': current_rate, 'open_date': ANY, 'open_timestamp': ANY, + 'open_fill_date': ANY, + 'open_fill_timestamp': ANY, 'open_rate': 0.123, 'pair': 'ETH/BTC', 'base_currency': 'ETH', @@ -1368,6 +1370,8 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): 'close_rate': 0.265441, 'open_date': ANY, 'open_timestamp': ANY, + 'open_fill_date': ANY, + 'open_fill_timestamp': ANY, 'open_rate': 0.245441, 'pair': 'ETH/BTC', 'base_currency': 'ETH', From fd48991fb073d5362a5c458e01f2e53d17847967 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Feb 2024 20:44:16 +0100 Subject: [PATCH 162/327] Fix duplicate parentheses --- freqtrade/persistence/trade_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 121a0bd8a..b1330b83c 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -466,7 +466,7 @@ class LocalTrade: 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])) + and len(filled_date := [o.order_filled_utc for o in orders if o.order_filled_utc]) ): return min(filled_date) return None From 60b12c1d9ecd19e8ed7ae6e559021c810c068c8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Feb 2024 20:45:16 +0100 Subject: [PATCH 163/327] Double newlines between functions ... --- freqtrade/util/datetime_helpers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/util/datetime_helpers.py b/freqtrade/util/datetime_helpers.py index 973a1c51b..66b738e8d 100644 --- a/freqtrade/util/datetime_helpers.py +++ b/freqtrade/util/datetime_helpers.py @@ -37,6 +37,7 @@ def dt_ts_def(dt: Optional[datetime], default: int = 0) -> int: return int(dt.timestamp() * 1000) return default + def dt_ts_none(dt: Optional[datetime]) -> Optional[int]: """ Return dt in ms as a timestamp in UTC. From a0b7df70d694a978dd7433244569883db8358e62 Mon Sep 17 00:00:00 2001 From: CaffeinatedTech Date: Fri, 16 Feb 2024 13:36:16 +1000 Subject: [PATCH 164/327] Added escaping to enter and exit tags on telegram performance messages. --- freqtrade/rpc/telegram.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f9a0635f0..f28b26766 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -4,6 +4,7 @@ This module manage Telegram communication """ import asyncio +import html import json import logging import re @@ -1378,7 +1379,7 @@ class Telegram(RPCHandler): output = "Entry Tag Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}.\t {trade['enter_tag']}\t" + f"{i + 1}.\t {html.escape(trade['enter_tag'])}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") @@ -1410,7 +1411,7 @@ class Telegram(RPCHandler): output = "Exit Reason Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}.\t {trade['exit_reason']}\t" + f"{i + 1}.\t {html.escape(trade['exit_reason'])}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") @@ -1442,7 +1443,7 @@ class Telegram(RPCHandler): output = "Mix Tag Performance:\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}.\t {trade['mix_tag']}\t" + f"{i + 1}.\t {html.escape(trade['mix_tag'])}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " f"({trade['count']})\n") From c0da1b6922891afc66b3071237ce3220f1421cdd Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Feb 2024 20:04:49 +0100 Subject: [PATCH 165/327] Fix edge-case when calculating cagr edge-case with leveraged trades - yielding a negative final balance. closes #9820 --- freqtrade/data/metrics.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index 7b45342bb..b37e0bb19 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -191,6 +191,9 @@ def calculate_cagr(days_passed: int, starting_balance: float, final_balance: flo :param final_balance: Final balance to calculate CAGR against :return: CAGR """ + if final_balance < 0: + # With leveraged trades, final_balance can become negative. + return 0 return (final_balance / starting_balance) ** (1 / (days_passed / 365)) - 1 From 4761bf242750e032ca427d0ad3f688cc77b4e23d Mon Sep 17 00:00:00 2001 From: CaffeinatedTech Date: Sat, 17 Feb 2024 09:12:49 +1000 Subject: [PATCH 166/327] Change enter_tag, exit_reason, mix_tag performance messages from HTML to Markdown to fix some string encoding issues. --- freqtrade/rpc/telegram.py | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f28b26766..2983eea38 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -4,7 +4,6 @@ This module manage Telegram communication """ import asyncio -import html import json import logging import re @@ -1376,21 +1375,21 @@ class Telegram(RPCHandler): pair = context.args[0] trades = self._rpc._rpc_enter_tag_performance(pair) - output = "Entry Tag Performance:\n" + output = "*Entry Tag Performance:*\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}.\t {html.escape(trade['enter_tag'])}\t" + f"{i + 1}.\t `{trade['enter_tag']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " - f"({trade['count']})\n") + f"({trade['count']})`\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - await self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2) output = stat_line else: output += stat_line - await self._send_msg(output, parse_mode=ParseMode.HTML, + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2, reload_able=True, callback_path="update_enter_tag_performance", query=update.callback_query) @@ -1408,21 +1407,21 @@ class Telegram(RPCHandler): pair = context.args[0] trades = self._rpc._rpc_exit_reason_performance(pair) - output = "Exit Reason Performance:\n" + output = "*Exit Reason Performance:*\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}.\t {html.escape(trade['exit_reason'])}\t" + f"{i + 1}\.\t `{html.escape(trade['exit_reason'])}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " - f"({trade['count']})\n") + f"({trade['count']})`\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - await self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2) output = stat_line else: output += stat_line - await self._send_msg(output, parse_mode=ParseMode.HTML, + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2, reload_able=True, callback_path="update_exit_reason_performance", query=update.callback_query) @@ -1440,21 +1439,21 @@ class Telegram(RPCHandler): pair = context.args[0] trades = self._rpc._rpc_mix_tag_performance(pair) - output = "Mix Tag Performance:\n" + output = "*Mix Tag Performance:*\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}.\t {html.escape(trade['mix_tag'])}\t" + f"{i + 1}\.\t `{trade['mix_tag']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " - f"({trade['count']})\n") + f"({trade['count']})`\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - await self._send_msg(output, parse_mode=ParseMode.HTML) + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2) output = stat_line else: output += stat_line - await self._send_msg(output, parse_mode=ParseMode.HTML, + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2, reload_able=True, callback_path="update_mix_tag_performance", query=update.callback_query) From 3f3760c0ae7fc3fee5147737907b35d92375dd6b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Feb 2024 12:02:26 +0100 Subject: [PATCH 167/327] Use Markdown V1 - update tests --- freqtrade/rpc/telegram.py | 16 ++++++++-------- tests/rpc/test_rpc_telegram.py | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2983eea38..fcc61b5e4 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1384,12 +1384,12 @@ class Telegram(RPCHandler): f"({trade['count']})`\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2) + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN) output = stat_line else: output += stat_line - await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2, + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN, reload_able=True, callback_path="update_enter_tag_performance", query=update.callback_query) @@ -1410,18 +1410,18 @@ class Telegram(RPCHandler): output = "*Exit Reason Performance:*\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}\.\t `{html.escape(trade['exit_reason'])}\t" + f"{i + 1}.\t `{trade['exit_reason']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " f"({trade['count']})`\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2) + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN) output = stat_line else: output += stat_line - await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2, + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN, reload_able=True, callback_path="update_exit_reason_performance", query=update.callback_query) @@ -1442,18 +1442,18 @@ class Telegram(RPCHandler): output = "*Mix Tag Performance:*\n" for i, trade in enumerate(trades): stat_line = ( - f"{i + 1}\.\t `{trade['mix_tag']}\t" + f"{i + 1}.\t `{trade['mix_tag']}\t" f"{fmt_coin(trade['profit_abs'], self._config['stake_currency'])} " f"({trade['profit_ratio']:.2%}) " f"({trade['count']})`\n") if len(output + stat_line) >= MAX_MESSAGE_LENGTH: - await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2) + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN) output = stat_line else: output += stat_line - await self._send_msg(output, parse_mode=ParseMode.MARKDOWN_V2, + await self._send_msg(output, parse_mode=ParseMode.MARKDOWN, reload_able=True, callback_path="update_mix_tag_performance", query=update.callback_query) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 3c683d7b3..7b1347fd6 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1507,7 +1507,7 @@ async def test_telegram_entry_tag_performance_handle( await telegram._enter_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Entry Tag Performance' in msg_mock.call_args_list[0][0][0] - assert 'TEST1\t3.987 USDT (5.00%) (1)' in msg_mock.call_args_list[0][0][0] + assert '`TEST1\t3.987 USDT (5.00%) (1)`' in msg_mock.call_args_list[0][0][0] context.args = ['XRP/USDT'] await telegram._enter_tag_performance(update=update, context=context) @@ -1538,7 +1538,7 @@ async def test_telegram_exit_reason_performance_handle( await telegram._exit_reason_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Exit Reason Performance' in msg_mock.call_args_list[0][0][0] - assert 'roi\t2.842 USDT (10.00%) (1)' in msg_mock.call_args_list[0][0][0] + assert '`roi\t2.842 USDT (10.00%) (1)`' in msg_mock.call_args_list[0][0][0] context.args = ['XRP/USDT'] await telegram._exit_reason_performance(update=update, context=context) @@ -1570,7 +1570,7 @@ async def test_telegram_mix_tag_performance_handle(default_conf_usdt, update, ti await telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0] - assert ('TEST3 roi\t2.842 USDT (10.00%) (1)' + assert ('`TEST3 roi\t2.842 USDT (10.00%) (1)`' in msg_mock.call_args_list[0][0][0]) context.args = ['XRP/USDT'] From 8033faa2f29fc58905fd7f0794d0100ad0928bc9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Feb 2024 15:14:11 +0100 Subject: [PATCH 168/327] Update pairlist cache behavior in VolumePairList --- docs/includes/pairlists.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 5a6a2560b..d1dd2cda7 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -68,7 +68,7 @@ When used in the leading position of the chain of Pairlist Handlers, the `pair_w The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists. -Filtering instances (not the first position in the list) will not apply any cache and will always use up-to-date data. +Filtering instances (not the first position in the list) will not apply any cache (beyond caching candles for the duration of the candle in advanced mode) and will always use up-to-date data. `VolumePairList` is per default based on the ticker data from exchange, as reported by the ccxt library: From bcfe7ef547871b0f1f7e1984e23935aa511ca3ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Feb 2024 16:17:32 +0100 Subject: [PATCH 169/327] Refactor ohlcv caching to exchange class --- freqtrade/exchange/exchange.py | 36 ++++++++++++++++++++ freqtrade/plugins/pairlist/VolumePairList.py | 16 ++------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 85a77fe5e..6dbb38bd8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -6,6 +6,7 @@ import asyncio import inspect import logging import signal +from collections import defaultdict from copy import deepcopy from datetime import datetime, timedelta, timezone from math import floor @@ -43,6 +44,7 @@ from freqtrade.misc import (chunks, deep_merge_dicts, file_dump_json, file_load_ from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.util import dt_from_ts, dt_now from freqtrade.util.datetime_helpers import dt_humanize, dt_ts +from freqtrade.util.periodic_cache import PeriodicCache logger = logging.getLogger(__name__) @@ -131,6 +133,7 @@ class Exchange: # Holds candles self._klines: Dict[PairWithTimeframe, DataFrame] = {} + self._expiring_candle_cache: Dict[str, PeriodicCache] = {} # Holds all open sell orders for dry_run self._dry_run_open_orders: Dict[str, Any] = {} @@ -2124,6 +2127,39 @@ class Exchange: return results_df + def refresh_ohlcv_with_cache( + self, + pairs: List[PairWithTimeframe], + since_ms: int + ) -> Dict[PairWithTimeframe, DataFrame]: + """ + Refresh ohlcv data for all pairs in needed_pairs if necessary. + Caches data with expiring per timeframe. + Should only be used for pairlists which need "on time" expirarion, and no longer cache. + """ + + timeframes = [p[1] for p in pairs] + for timeframe in timeframes: + if timeframe not in self._expiring_candle_cache: + timeframe_in_sec = timeframe_to_seconds(timeframe) + # Initialise cache + self._expiring_candle_cache[timeframe] = PeriodicCache(ttl=timeframe_in_sec, + maxsize=1000) + + # Get candles from cache + candles = { + c: self._expiring_candle_cache[c[1]].get(c, None) for c in pairs + if c in self._expiring_candle_cache[c[1]] + } + pairs_to_download = [p for p in pairs if p not in candles] + if pairs_to_download: + candles = self.refresh_latest_ohlcv( + pairs_to_download, since_ms=since_ms, cache=False + ) + for c, val in candles.items(): + self._expiring_candle_cache[c[1]][c] = val + return candles + def _now_is_time_to_refresh(self, pair: str, timeframe: str, candle_type: CandleType) -> bool: # Timeframe in seconds interval_in_sec = timeframe_to_seconds(timeframe) diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index 671ba4db4..f4d08e800 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -14,7 +14,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange.types import Tickers from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter -from freqtrade.util import PeriodicCache, dt_now, format_ms_time +from freqtrade.util import dt_now, format_ms_time logger = logging.getLogger(__name__) @@ -63,7 +63,6 @@ class VolumePairList(IPairList): # get timeframe in minutes and seconds self._tf_in_min = timeframe_to_minutes(self._lookback_timeframe) _tf_in_sec = self._tf_in_min * 60 - self._candle_cache = PeriodicCache(maxsize=1000, ttl=_tf_in_sec) # wether to use range lookback or not self._use_range = (self._tf_in_min > 0) & (self._lookback_period > 0) @@ -230,18 +229,7 @@ class VolumePairList(IPairList): if p not in self._pair_cache ] - # Get all candles - candles = { - c: self._candle_cache.get(c, None) for c in needed_pairs - if c in self._candle_cache - } - pairs_to_download = [p for p in needed_pairs if p not in candles] - if pairs_to_download: - candles = self._exchange.refresh_latest_ohlcv( - pairs_to_download, since_ms=since_ms, cache=False - ) - for c, val in candles.items(): - self._candle_cache[c] = val + candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms) for i, p in enumerate(filtered_tickers): contract_size = self._exchange.markets[p['symbol']].get('contractSize', 1.0) or 1.0 From 7b36a0fc4220ae3d7141228ad9e4a23a29a07e7d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Feb 2024 16:26:53 +0100 Subject: [PATCH 170/327] Add explicit test for ohlcv_with_cache --- tests/exchange/test_exchange.py | 60 +++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index fc199a7f5..ef41a6eb0 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2303,6 +2303,66 @@ def test_refresh_latest_ohlcv_cache(mocker, default_conf, candle_type, time_mach assert res[pair2].at[0, 'open'] +def test_refresh_ohlcv_with_cache(mocker, default_conf, time_machine) -> None: + start = datetime(2021, 8, 1, 0, 0, 0, 0, tzinfo=timezone.utc) + ohlcv = generate_test_data_raw('1h', 100, start.strftime('%Y-%m-%d')) + time_machine.move_to(start, tick=False) + pairs = [ + ('ETH/BTC', '1d', CandleType.SPOT), + ('TKN/BTC', '1d', CandleType.SPOT), + ('LTC/BTC', '1d', CandleType.SPOT), + ('LTC/BTC', '5m', CandleType.SPOT), + ('LTC/BTC', '1h', CandleType.SPOT), + ] + + ohlcv_data = { + p: ohlcv for p in pairs + } + ohlcv_mock = mocker.patch(f"{EXMS}.refresh_latest_ohlcv", return_value=ohlcv_data) + mocker.patch(f"{EXMS}.ohlcv_candle_limit", return_value=100) + exchange = get_patched_exchange(mocker, default_conf) + + assert len(exchange._expiring_candle_cache) == 0 + + res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp()) + assert ohlcv_mock.call_count == 1 + assert ohlcv_mock.call_args_list[0][0][0] == pairs + assert len(ohlcv_mock.call_args_list[0][0][0]) == 5 + + assert len(res) == 5 + # length of 3 - as we have 3 different timeframes + assert len(exchange._expiring_candle_cache) == 3 + + ohlcv_mock.reset_mock() + res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp()) + assert ohlcv_mock.call_count == 0 + + # Expire 5m cache + time_machine.move_to(start + timedelta(minutes=6), tick=False) + + ohlcv_mock.reset_mock() + res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp()) + assert ohlcv_mock.call_count == 1 + assert len(ohlcv_mock.call_args_list[0][0][0]) == 1 + + # Expire 5m and 1h cache + time_machine.move_to(start + timedelta(hours=2), tick=False) + + ohlcv_mock.reset_mock() + res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp()) + assert ohlcv_mock.call_count == 1 + assert len(ohlcv_mock.call_args_list[0][0][0]) == 2 + + # Expire all caches + time_machine.move_to(start + timedelta(days=1, hours=2), tick=False) + + ohlcv_mock.reset_mock() + res = exchange.refresh_ohlcv_with_cache(pairs, start.timestamp()) + assert ohlcv_mock.call_count == 1 + assert len(ohlcv_mock.call_args_list[0][0][0]) == 5 + assert ohlcv_mock.call_args_list[0][0][0] == pairs + + @pytest.mark.parametrize("exchange_name", EXCHANGES) async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): ohlcv = [ From 4bcf2c423a1d478bfa54b85bcdef077086ee23a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Feb 2024 16:27:43 +0100 Subject: [PATCH 171/327] Don't tick on ttl cache --- tests/utils/test_periodiccache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/test_periodiccache.py b/tests/utils/test_periodiccache.py index df05de4ef..a8931d6a2 100644 --- a/tests/utils/test_periodiccache.py +++ b/tests/utils/test_periodiccache.py @@ -5,7 +5,7 @@ from freqtrade.util import PeriodicCache def test_ttl_cache(): - with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + with time_machine.travel("2021-09-01 05:00:00 +00:00", tick=False) as t: cache = PeriodicCache(5, ttl=60) cache1h = PeriodicCache(5, ttl=3600) From 78d8a4df2ea41cab6e2c9932091267f1d9316518 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Feb 2024 16:29:52 +0100 Subject: [PATCH 172/327] Use "ohlcv_with_cache" for further pairlists --- freqtrade/plugins/pairlist/VolatilityFilter.py | 5 +---- freqtrade/plugins/pairlist/rangestabilityfilter.py | 6 +----- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 794df5449..b6ce1b9a2 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -104,10 +104,7 @@ class VolatilityFilter(IPairList): since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days)) # Get all candles - candles = {} - if needed_pairs: - candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, - cache=False) + candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) if self._enabled: for p in deepcopy(pairlist): diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index e04772e9c..f2cf4d486 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -101,11 +101,7 @@ class RangeStabilityFilter(IPairList): (p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache] since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days - 1)) - # Get all candles - candles = {} - if needed_pairs: - candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, - cache=False) + candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) if self._enabled: for p in deepcopy(pairlist): From ebd439cdd10ede780da071f0829b8b23fd9cef18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Feb 2024 16:41:10 +0100 Subject: [PATCH 173/327] Remove unused import --- freqtrade/exchange/exchange.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6dbb38bd8..ee3ca05cf 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -6,7 +6,6 @@ import asyncio import inspect import logging import signal -from collections import defaultdict from copy import deepcopy from datetime import datetime, timedelta, timezone from math import floor From a5d1ae31915f401e8b0031d99eacac4e181f516d Mon Sep 17 00:00:00 2001 From: William Wong <46506352+tar-xz@users.noreply.github.com> Date: Sun, 18 Feb 2024 03:21:50 +0800 Subject: [PATCH 174/327] docs: Update sql_cheatsheet.md --- docs/sql_cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 67c081d4c..a0c5c8da1 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -109,7 +109,7 @@ Freqtrade does not depend or install any additional database driver. Please refe The following systems have been tested and are known to work with freqtrade: * sqlite (default) -* PostgreSQL) +* PostgreSQL * MariaDB !!! Warning From 3250f42257777f076b72bf7bf8966940f9bfc618 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Feb 2024 11:21:34 +0100 Subject: [PATCH 175/327] Improve validate_exchange returns now both required and optional dependencies --- freqtrade/exchange/common.py | 1 + freqtrade/exchange/exchange_utils.py | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 72ad774b6..d04241e29 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -86,6 +86,7 @@ EXCHANGE_HAS_OPTIONAL = [ # 'fetchPositions', # Futures trading # 'fetchLeverageTiers', # Futures initialization # 'fetchMarketLeverageTiers', # Futures initialization + # 'fetchOpenOrder', 'fetchClosedOrder', # replacement for fetchOrder # 'fetchOpenOrders', 'fetchClosedOrders', # 'fetchOrders', # Refinding balance... ] diff --git a/freqtrade/exchange/exchange_utils.py b/freqtrade/exchange/exchange_utils.py index 98e05bf7a..f8da47fee 100644 --- a/freqtrade/exchange/exchange_utils.py +++ b/freqtrade/exchange/exchange_utils.py @@ -40,21 +40,30 @@ def available_exchanges(ccxt_module: Optional[CcxtModuleType] = None) -> List[st def validate_exchange(exchange: str) -> Tuple[bool, str]: + """ + returns: can_use, reason + with Reason including both missing and missing_opt + """ ex_mod = getattr(ccxt, exchange.lower())() + result = True + reason = '' if not ex_mod or not ex_mod.has: return False, '' missing = [k for k in EXCHANGE_HAS_REQUIRED if ex_mod.has.get(k) is not True] if missing: - return False, f"missing: {', '.join(missing)}" + result = False + reason += f"missing: {', '.join(missing)}" missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)] if exchange.lower() in BAD_EXCHANGES: - return False, BAD_EXCHANGES.get(exchange.lower(), '') - if missing_opt: - return True, f"missing opt: {', '.join(missing_opt)}" + result = False + reason = BAD_EXCHANGES.get(exchange.lower(), '') - return True, '' + if missing_opt: + reason += f"{'. ' if reason else ''}missing opt: {', '.join(missing_opt)}. " + + return result, reason def _build_exchange_list_entry( From e06b70eb0530e81a8e6bf503a94c4bef45884a9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Feb 2024 11:40:50 +0100 Subject: [PATCH 176/327] Add log message for Bybit accout type --- freqtrade/exchange/bybit.py | 16 +++++++++++++--- tests/exchange/test_bybit.py | 21 ++++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index e7c463140..259858802 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -25,6 +25,7 @@ class Bybit(Exchange): officially supported by the Freqtrade development team. So some features may still not work as expected. """ + unified_account = False _ft_has: Dict = { "ohlcv_candle_limit": 1000, @@ -82,9 +83,18 @@ class Bybit(Exchange): Must be overridden in child methods if required. """ try: - if self.trading_mode == TradingMode.FUTURES and not self._config['dry_run']: - position_mode = self._api.set_position_mode(False) - self._log_exchange_response('set_position_mode', position_mode) + if not self._config['dry_run']: + if self.trading_mode == TradingMode.FUTURES: + position_mode = self._api.set_position_mode(False) + self._log_exchange_response('set_position_mode', position_mode) + is_unified = self._api.is_unified_enabled() + # Returns a tuple of bools, first for margin, second for Account + if is_unified and len(is_unified) > 1 and is_unified[1]: + self.unified_account = True + logger.info("Bybit: Unified account.") + else: + self.unified_account = False + logger.info("Bybit: Standard account.") except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index f7383934b..74a490aa9 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -3,18 +3,33 @@ from unittest.mock import MagicMock from freqtrade.enums.marginmode import MarginMode from freqtrade.enums.tradingmode import TradingMode -from tests.conftest import EXMS, get_mock_coro, get_patched_exchange +from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers -def test_additional_exchange_init_bybit(default_conf, mocker): +def test_additional_exchange_init_bybit(default_conf, mocker, caplog): default_conf['dry_run'] = False default_conf['trading_mode'] = TradingMode.FUTURES default_conf['margin_mode'] = MarginMode.ISOLATED api_mock = MagicMock() api_mock.set_position_mode = MagicMock(return_value={"dualSidePosition": False}) - get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) + api_mock.is_unified_enabled = MagicMock(return_value=[False, False]) + + exchange = get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) assert api_mock.set_position_mode.call_count == 1 + assert api_mock.is_unified_enabled.call_count == 1 + assert exchange.unified_account is False + + assert log_has("Bybit: Standard account.", caplog) + + api_mock.set_position_mode.reset_mock() + api_mock.is_unified_enabled = MagicMock(return_value=[False, True]) + exchange = get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) + assert api_mock.set_position_mode.call_count == 1 + assert api_mock.is_unified_enabled.call_count == 1 + assert exchange.unified_account is True + + assert log_has("Bybit: Unified account.", caplog) ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'bybit', "additional_exchange_init", "set_position_mode") From 583b2fc690f2461126d7b14b6af560b87537e928 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Feb 2024 11:44:54 +0100 Subject: [PATCH 177/327] Fail if unified account is detected. --- freqtrade/exchange/bybit.py | 2 ++ tests/exchange/test_bybit.py | 15 ++++++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 259858802..63047066a 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -92,6 +92,8 @@ class Bybit(Exchange): if is_unified and len(is_unified) > 1 and is_unified[1]: self.unified_account = True logger.info("Bybit: Unified account.") + raise OperationalException("Bybit: Unified account is not supported. " + "Please use a standard (sub)account.") else: self.unified_account = False logger.info("Bybit: Standard account.") diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index 74a490aa9..fb7d7a120 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -1,8 +1,11 @@ from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock +import pytest + from freqtrade.enums.marginmode import MarginMode from freqtrade.enums.tradingmode import TradingMode +from freqtrade.exceptions import OperationalException from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -24,12 +27,14 @@ def test_additional_exchange_init_bybit(default_conf, mocker, caplog): api_mock.set_position_mode.reset_mock() api_mock.is_unified_enabled = MagicMock(return_value=[False, True]) - exchange = get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) - assert api_mock.set_position_mode.call_count == 1 - assert api_mock.is_unified_enabled.call_count == 1 - assert exchange.unified_account is True - + with pytest.raises(OperationalException, match=r"Bybit: Unified account is not supported.*"): + get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) assert log_has("Bybit: Unified account.", caplog) + # exchange = get_patched_exchange(mocker, default_conf, id="bybit", api_mock=api_mock) + # assert api_mock.set_position_mode.call_count == 1 + # assert api_mock.is_unified_enabled.call_count == 1 + # assert exchange.unified_account is True + ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'bybit', "additional_exchange_init", "set_position_mode") From 61e09ac719f9c1bffb2cb082bb45dd75657154fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Feb 2024 16:07:03 +0100 Subject: [PATCH 178/327] Update telegram help with new wording --- freqtrade/rpc/telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fcc61b5e4..904b1fdbc 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1364,7 +1364,7 @@ class Telegram(RPCHandler): @authorized_only async def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None: """ - Handler for /buys PAIR . + Handler for /entries PAIR . Shows a performance statistic from finished trades :param bot: telegram bot :param update: message update @@ -1396,7 +1396,7 @@ class Telegram(RPCHandler): @authorized_only async def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None: """ - Handler for /sells. + Handler for /exits. Shows a performance statistic from finished trades :param bot: telegram bot :param update: message update @@ -1676,8 +1676,8 @@ class Telegram(RPCHandler): " *table :* `will display trades in a table`\n" " `pending buy orders are marked with an asterisk (*)`\n" " `pending sell orders are marked with a double asterisk (**)`\n" - "*/buys :* `Shows the enter_tag performance`\n" - "*/sells :* `Shows the exit reason performance`\n" + "*/entries :* `Shows the enter_tag performance`\n" + "*/exits :* `Shows the exit reason performance`\n" "*/mix_tags :* `Shows combined entry tag + exit reason performance`\n" "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n" "*/profit []:* `Lists cumulative profit from all finished trades, " From 69a0f4c465fa4f45ef8d4271de85839897c70f05 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Feb 2024 16:09:59 +0100 Subject: [PATCH 179/327] Fix bybit spot live tests --- tests/exchange_online/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange_online/conftest.py b/tests/exchange_online/conftest.py index a613ae586..f8cd8f413 100644 --- a/tests/exchange_online/conftest.py +++ b/tests/exchange_online/conftest.py @@ -324,7 +324,8 @@ def get_futures_exchange(exchange_name, exchange_conf, class_mocker): @pytest.fixture(params=EXCHANGES, scope="class") -def exchange(request, exchange_conf): +def exchange(request, exchange_conf, class_mocker): + class_mocker.patch('freqtrade.exchange.bybit.Bybit.additional_exchange_init') yield from get_exchange(request.param, exchange_conf) From 4c3879cb57b0aaad66e7b1767fe6c0b58de16fc3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 03:11:21 +0000 Subject: [PATCH 180/327] Bump the types group with 1 update Bumps the types group with 1 update: [types-requests](https://github.com/python/typeshed). Updates `types-requests` from 2.31.0.20240125 to 2.31.0.20240218 - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch dependency-group: types ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f0095bffa..bee5140f6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,6 +26,6 @@ nbconvert==7.16.0 # mypy types types-cachetools==5.3.0.7 types-filelock==3.2.7 -types-requests==2.31.0.20240125 +types-requests==2.31.0.20240218 types-tabulate==0.9.0.20240106 types-python-dateutil==2.8.19.20240106 From 0979d0b6e464b15c67e79d10530b846d1c1e65d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 03:11:53 +0000 Subject: [PATCH 181/327] Bump the pytest group with 1 update Bumps the pytest group with 1 update: [pytest](https://github.com/pytest-dev/pytest). Updates `pytest` from 8.0.0 to 8.0.1 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.0...8.0.1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-patch dependency-group: pytest ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f0095bffa..a70af1fb9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ coveralls==3.3.1 ruff==0.2.1 mypy==1.8.0 pre-commit==3.6.1 -pytest==8.0.0 +pytest==8.0.1 pytest-asyncio==0.23.5 pytest-cov==4.1.0 pytest-mock==3.12.0 From 8675f86d14af7021eb6386ad0d997d07bbc9dfb6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 03:11:57 +0000 Subject: [PATCH 182/327] Bump urllib3 from 2.2.0 to 2.2.1 Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.2.0 to 2.2.1. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/2.2.0...2.2.1) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c30036e5a..72e342523 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ httpx>=0.24.1 arrow==1.3.0 cachetools==5.3.2 requests==2.31.0 -urllib3==2.2.0 +urllib3==2.2.1 jsonschema==4.21.1 TA-Lib==0.4.28 technical==1.4.3 From f361824b15b5e7bd7c83774ac420c5f825a1d7a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 03:12:05 +0000 Subject: [PATCH 183/327] Bump cryptography from 42.0.2 to 42.0.3 Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.2 to 42.0.3. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.2...42.0.3) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c30036e5a..52ae42c60 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==2.1.4 pandas-ta==0.3.14b ccxt==4.2.42 -cryptography==42.0.2 +cryptography==42.0.3 aiohttp==3.9.3 SQLAlchemy==2.0.26 python-telegram-bot==20.8 From c966f83147da6f4aebd394cfd90fc097b4213b80 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 03:12:17 +0000 Subject: [PATCH 184/327] Bump plotly from 5.18.0 to 5.19.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 5.18.0 to 5.19.0. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v5.18.0...v5.19.0) --- updated-dependencies: - dependency-name: plotly dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 8900bf1f9..af746ef98 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,4 +1,4 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.18.0 +plotly==5.19.0 From 4241db2fe25b7fc37282ec8e47acc08aed10b4b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 03:12:37 +0000 Subject: [PATCH 185/327] Bump scikit-learn from 1.4.0 to 1.4.1.post1 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 1.4.0 to 1.4.1.post1. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/1.4.0...1.4.1.post1) --- updated-dependencies: - dependency-name: scikit-learn dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- requirements-hyperopt.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 848b6d920..3719f7d57 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt # Required for freqai -scikit-learn==1.4.0 +scikit-learn==1.4.1.post1 joblib==1.3.2 catboost==1.2.2; 'arm' not in platform_machine and python_version < '3.12' lightgbm==4.3.0 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index b961b3b04..5347adf9c 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,6 +3,6 @@ # Required for hyperopt scipy==1.12.0 -scikit-learn==1.4.0 +scikit-learn==1.4.1.post1 ft-scikit-optimize==0.9.2 filelock==3.13.1 From edb5431a778e367c029d4f163f23288a8ee8cee6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 03:13:07 +0000 Subject: [PATCH 186/327] Bump orjson from 3.9.13 to 3.9.14 Bumps [orjson](https://github.com/ijl/orjson) from 3.9.13 to 3.9.14. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.9.13...3.9.14) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c30036e5a..73688c870 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ py_find_1st==1.1.6 # Load ticker files 30% faster python-rapidjson==1.14 # Properly format api responses -orjson==3.9.13 +orjson==3.9.14 # Notify systemd sdnotify==0.3.2 From 549b9f62fdc234c6dc43e7c0875a8d5c8438a905 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 03:13:12 +0000 Subject: [PATCH 187/327] Bump tensorboard from 2.15.2 to 2.16.2 Bumps [tensorboard](https://github.com/tensorflow/tensorboard) from 2.15.2 to 2.16.2. - [Release notes](https://github.com/tensorflow/tensorboard/releases) - [Changelog](https://github.com/tensorflow/tensorboard/blob/master/RELEASE.md) - [Commits](https://github.com/tensorflow/tensorboard/compare/2.15.2...2.16.2) --- updated-dependencies: - dependency-name: tensorboard dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 848b6d920..1df9ad416 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -8,5 +8,5 @@ joblib==1.3.2 catboost==1.2.2; 'arm' not in platform_machine and python_version < '3.12' lightgbm==4.3.0 xgboost==2.0.3 -tensorboard==2.15.2 +tensorboard==2.16.2 datasieve==0.1.7 From 381576b8f148517bcc9fc74023599122b2c3a156 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 05:30:18 +0000 Subject: [PATCH 188/327] Bump pre-commit from 3.6.1 to 3.6.2 Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 3.6.1 to 3.6.2. - [Release notes](https://github.com/pre-commit/pre-commit/releases) - [Changelog](https://github.com/pre-commit/pre-commit/blob/main/CHANGELOG.md) - [Commits](https://github.com/pre-commit/pre-commit/compare/v3.6.1...v3.6.2) --- updated-dependencies: - dependency-name: pre-commit dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index a70af1fb9..13d699a16 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ coveralls==3.3.1 ruff==0.2.1 mypy==1.8.0 -pre-commit==3.6.1 +pre-commit==3.6.2 pytest==8.0.1 pytest-asyncio==0.23.5 pytest-cov==4.1.0 From 6aa1bbf574f417d122475bdf97a8ed057b484e6d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 05:31:00 +0000 Subject: [PATCH 189/327] Bump sqlalchemy from 2.0.26 to 2.0.27 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 2.0.26 to 2.0.27. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/main/CHANGES.rst) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) --- updated-dependencies: - dependency-name: sqlalchemy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d18950d73..09933bffa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==4.2.42 cryptography==42.0.3 aiohttp==3.9.3 -SQLAlchemy==2.0.26 +SQLAlchemy==2.0.27 python-telegram-bot==20.8 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.24.1 From 66f48391014a3005b321635e8b8a801b3f51c991 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Feb 2024 07:02:37 +0100 Subject: [PATCH 190/327] Further increase test coverate of max_drawdown --- tests/data/test_btanalysis.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index c1b007e77..554ee261a 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -455,6 +455,13 @@ def test_calculate_max_drawdown2(): with pytest.raises(ValueError, match='No losing trade, therefore no drawdown.'): calculate_max_drawdown(df, date_col='open_date', value_col='profit') + df1 = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date']) + df1.loc[:, 'profit'] = df1['profit'] * -1 + # No winning trade ... + drawdown, hdate, ldate, hval, lval, drawdown_rel = calculate_max_drawdown( + df1, date_col='open_date', value_col='profit') + assert drawdown == 0.043965 + @pytest.mark.parametrize('profits,relative,highd,lowd,result,result_rel', [ ([0.0, -500.0, 500.0, 10000.0, -1000.0], False, 3, 4, 1000.0, 0.090909), From 39941a7ac04da6b8aa9fabe947ed2ca266ef1b27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Feb 2024 07:09:23 +0100 Subject: [PATCH 191/327] Improve formatting in drawdown calc --- freqtrade/data/metrics.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/metrics.py b/freqtrade/data/metrics.py index b37e0bb19..738129939 100644 --- a/freqtrade/data/metrics.py +++ b/freqtrade/data/metrics.py @@ -143,8 +143,10 @@ def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date' starting_balance=starting_balance ) - idxmin = max_drawdown_df['drawdown_relative'].idxmax() if relative \ - else max_drawdown_df['drawdown'].idxmin() + idxmin = ( + max_drawdown_df['drawdown_relative'].idxmax() + if relative else max_drawdown_df['drawdown'].idxmin() + ) if idxmin == 0: raise ValueError("No losing trade, therefore no drawdown.") high_date = profit_results.loc[max_drawdown_df.iloc[:idxmin]['high_value'].idxmax(), date_col] From a200b5524b88ccdb3e9f5c3a52c93c3cff367521 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Feb 2024 07:18:56 +0100 Subject: [PATCH 192/327] Update sqlalchemy in pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1aa00f07..f843b6ebe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: - types-requests==2.31.0.20240125 - types-tabulate==0.9.0.20240106 - types-python-dateutil==2.8.19.20240106 - - SQLAlchemy==2.0.26 + - SQLAlchemy==2.0.27 # stages: [push] - repo: https://github.com/pycqa/isort From 82876570a339882987cd441a7a0b459ae0569efd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Feb 2024 07:14:01 +0000 Subject: [PATCH 193/327] Bump ruff from 0.2.1 to 0.2.2 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.2.1 to 0.2.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.2.1...v0.2.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 13d699a16..c606f219d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.2.1 +ruff==0.2.2 mypy==1.8.0 pre-commit==3.6.2 pytest==8.0.1 From 434b8a423cda08ada2d580ee58ab70204f25f772 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Feb 2024 09:33:09 +0100 Subject: [PATCH 194/327] bump types-requests pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1aa00f07..b73c2e5cf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: additional_dependencies: - types-cachetools==5.3.0.7 - types-filelock==3.2.7 - - types-requests==2.31.0.20240125 + - types-requests==2.31.0.20240218 - types-tabulate==0.9.0.20240106 - types-python-dateutil==2.8.19.20240106 - SQLAlchemy==2.0.26 From 00bde70f73e9363fdfe3e0f6b305b69cb959a6c0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Feb 2024 19:14:44 +0100 Subject: [PATCH 195/327] Fix / improve styling in test class --- tests/rpc/test_rpc_apiserver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e441b127b..1e008d98e 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -180,7 +180,9 @@ def test_api_auth(): def test_api_ws_auth(botclient): ftbot, client = botclient - def url(token): return f"/api/v1/message/ws?token={token}" + + def url(token): + return f"/api/v1/message/ws?token={token}" bad_token = "bad-ws_token" with pytest.raises(WebSocketDisconnect): From f6e2030bf2c22ca4bf775df8efee65ea81aeff93 Mon Sep 17 00:00:00 2001 From: xmatthias <5024695+xmatthias@users.noreply.github.com> Date: Tue, 20 Feb 2024 03:03:46 +0000 Subject: [PATCH 196/327] chore: update pre-commit hooks --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f9865be7b..842c87976 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.2.1' + rev: 'v0.2.2' hooks: - id: ruff From 65af7750e6cbb5ce9c026036d2dee1bf0ef2a20e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Feb 2024 13:00:43 +0100 Subject: [PATCH 197/327] Add fetch_order_emulated to support exchanges without proper fetch_order method --- freqtrade/exchange/exchange.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 1df51ed90..2872e603e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1260,11 +1260,43 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def fetch_order_emulated(self, order_id: str, pair: str, params: Dict) -> Dict: + """ + Emulated fetch_order if the exchange doesn't support fetch_order, but requires separate + calls for open and closed orders. + """ + try: + order = self._api.fetch_open_order(order_id, pair, params=params) + self._log_exchange_response('fetch_open_order', order) + order = self._order_contracts_to_amount(order) + return order + except ccxt.OrderNotFound: + try: + order = self._api.fetch_closed_order(order_id, pair, params=params) + self._log_exchange_response('fetch_closed_order', order) + order = self._order_contracts_to_amount(order) + return order + except ccxt.OrderNotFound as e: + raise RetryableOrderError( + f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e + except ccxt.InvalidOrder as e: + raise InvalidOrderException( + f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) def fetch_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: if self._config['dry_run']: return self.fetch_dry_run_order(order_id) try: + if not self.exchange_has('fetchOrder'): + return self.fetch_order_emulated(order_id, pair, params) order = self._api.fetch_order(order_id, pair, params=params) self._log_exchange_response('fetch_order', order) order = self._order_contracts_to_amount(order) From 3497f7946e89608551f3873e2eb5ecad93aa850e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Feb 2024 13:09:40 +0100 Subject: [PATCH 198/327] Add test for fetch_order_emulated --- tests/exchange/test_exchange.py | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ef41a6eb0..077f1f8f0 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3391,6 +3391,72 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog): order_id='_', pair='TKN/BTC') +@pytest.mark.usefixtures("init_persistence") +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_fetch_order_emulated(default_conf, mocker, exchange_name, caplog): + default_conf['dry_run'] = True + default_conf['exchange']['log_responses'] = True + order = MagicMock() + order.myid = 123 + order.symbol = 'TKN/BTC' + + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + mocker.patch(f'{EXMS}.exchange_has', return_value=False) + exchange._dry_run_open_orders['X'] = order + # Dry run - regular fetch_order behavior + assert exchange.fetch_order('X', 'TKN/BTC').myid == 123 + + with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): + exchange.fetch_order('Y', 'TKN/BTC') + + default_conf['dry_run'] = False + mocker.patch(f'{EXMS}.exchange_has', return_value=False) + api_mock = MagicMock() + api_mock.fetch_open_order = MagicMock(return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}) + api_mock.fetch_closed_order = MagicMock(return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + assert exchange.fetch_order( + 'X', 'TKN/BTC') == {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'} + assert log_has( + ("API fetch_open_order: {\'id\': \'123\', \'amount\': 2, \'symbol\': \'TKN/BTC\'}" + ), + caplog + ) + assert api_mock.fetch_open_order.call_count == 1 + assert api_mock.fetch_closed_order.call_count == 0 + caplog.clear() + + # open_order doesn't find order + api_mock.fetch_open_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found")) + api_mock.fetch_closed_order = MagicMock(return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + assert exchange.fetch_order( + 'X', 'TKN/BTC') == {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'} + assert log_has( + ("API fetch_closed_order: {\'id\': \'123\', \'amount\': 2, \'symbol\': \'TKN/BTC\'}" + ), + caplog + ) + assert api_mock.fetch_open_order.call_count == 1 + assert api_mock.fetch_closed_order.call_count == 1 + caplog.clear() + + with pytest.raises(InvalidOrderException): + api_mock.fetch_open_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + api_mock.fetch_closed_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.fetch_order(order_id='_', pair='TKN/BTC') + assert api_mock.fetch_open_order.call_count == 1 + + api_mock.fetch_open_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + 'fetch_order_emulated', 'fetch_open_order', + retries=1, + order_id='_', pair='TKN/BTC', params={}) + + @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_stoploss_order(default_conf, mocker, exchange_name): From f53c019d2afa80f33b96e2fad61290d9c01a46d8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Feb 2024 15:14:07 +0100 Subject: [PATCH 199/327] Update "exchange_has" validation with new fallbacks --- freqtrade/exchange/common.py | 15 ++++++++------- freqtrade/exchange/exchange_utils.py | 6 +++++- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index d04241e29..06ae21001 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -60,16 +60,17 @@ SUPPORTED_EXCHANGES = [ 'okx', ] -EXCHANGE_HAS_REQUIRED = [ +# either the main, or replacement methods (array) is required +EXCHANGE_HAS_REQUIRED = { # Required / private - 'fetchOrder', - 'cancelOrder', - 'createOrder', - 'fetchBalance', + 'fetchOrder': ['fetchOpenOrder', 'fetchClosedOrder'], + 'cancelOrder': [], + 'createOrder': [], + 'fetchBalance': [], # Public endpoints - 'fetchOHLCV', -] + 'fetchOHLCV': [], +} EXCHANGE_HAS_OPTIONAL = [ # Private diff --git a/freqtrade/exchange/exchange_utils.py b/freqtrade/exchange/exchange_utils.py index f8da47fee..f4dc3a721 100644 --- a/freqtrade/exchange/exchange_utils.py +++ b/freqtrade/exchange/exchange_utils.py @@ -49,7 +49,11 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]: reason = '' if not ex_mod or not ex_mod.has: return False, '' - missing = [k for k in EXCHANGE_HAS_REQUIRED if ex_mod.has.get(k) is not True] + missing = [ + k for k, v in EXCHANGE_HAS_REQUIRED.items() + if ex_mod.has.get(k) is not True + and not (all(ex_mod.has.get(x) for x in v)) + ] if missing: result = False reason += f"missing: {', '.join(missing)}" From 411f60647649ddb0833180dc5df7554939462005 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Feb 2024 15:39:13 +0100 Subject: [PATCH 200/327] Fix some tests due to new method --- tests/exchange/test_bybit.py | 1 + tests/exchange/test_exchange.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_bybit.py b/tests/exchange/test_bybit.py index fb7d7a120..556547d88 100644 --- a/tests/exchange/test_bybit.py +++ b/tests/exchange/test_bybit.py @@ -131,6 +131,7 @@ def test_bybit_fetch_order_canceled_empty(default_conf_usdt, mocker): 'amount': 20.0, }) + mocker.patch(f"{EXMS}.exchange_has", return_value=True) exchange = get_patched_exchange(mocker, default_conf_usdt, api_mock, id='bybit') res = exchange.fetch_order('123', 'BTC/USDT') diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 077f1f8f0..5c4879a32 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3237,6 +3237,7 @@ def test_is_cancel_order_result_suitable(mocker, default_conf, exchange_name, or def test_cancel_order_with_result(default_conf, mocker, exchange_name, corder, call_corder, call_forder): default_conf['dry_run'] = False + mocker.patch(f"{EXMS}.exchange_has", return_value=True) api_mock = MagicMock() api_mock.cancel_order = MagicMock(return_value=corder) api_mock.fetch_order = MagicMock(return_value={}) @@ -3250,6 +3251,7 @@ def test_cancel_order_with_result(default_conf, mocker, exchange_name, corder, @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, caplog): default_conf['dry_run'] = False + mocker.patch(f"{EXMS}.exchange_has", return_value=True) api_mock = MagicMock() api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) @@ -3347,6 +3349,7 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog): order.myid = 123 order.symbol = 'TKN/BTC' + mocker.patch(f"{EXMS}.exchange_has", return_value=True) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._dry_run_open_orders['X'] = order assert exchange.fetch_order('X', 'TKN/BTC').myid == 123 @@ -3412,8 +3415,10 @@ def test_fetch_order_emulated(default_conf, mocker, exchange_name, caplog): default_conf['dry_run'] = False mocker.patch(f'{EXMS}.exchange_has', return_value=False) api_mock = MagicMock() - api_mock.fetch_open_order = MagicMock(return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}) - api_mock.fetch_closed_order = MagicMock(return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}) + api_mock.fetch_open_order = MagicMock( + return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}) + api_mock.fetch_closed_order = MagicMock( + return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert exchange.fetch_order( 'X', 'TKN/BTC') == {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'} @@ -3428,7 +3433,8 @@ def test_fetch_order_emulated(default_conf, mocker, exchange_name, caplog): # open_order doesn't find order api_mock.fetch_open_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found")) - api_mock.fetch_closed_order = MagicMock(return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}) + api_mock.fetch_closed_order = MagicMock( + return_value={'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert exchange.fetch_order( 'X', 'TKN/BTC') == {'id': '123', 'amount': 2, 'symbol': 'TKN/BTC'} @@ -3461,6 +3467,7 @@ def test_fetch_order_emulated(default_conf, mocker, exchange_name, caplog): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_stoploss_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = True + mocker.patch(f"{EXMS}.exchange_has", return_value=True) order = MagicMock() order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) From b3ba2cee1744141fd0ef252ae731d27a9e65a8df Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Feb 2024 06:17:13 +0100 Subject: [PATCH 201/327] Bump ccxt to 4.2.47 --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 17ebe8867..1efe0b7da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.26.4 pandas==2.1.4 pandas-ta==0.3.14b -ccxt==4.2.42 +ccxt==4.2.47 cryptography==42.0.3 aiohttp==3.9.3 SQLAlchemy==2.0.27 diff --git a/setup.py b/setup.py index 3b92b9dd7..38f0f9a78 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ setup( ], install_requires=[ # from requirements.txt - 'ccxt>=4.2.15', + 'ccxt>=4.2.47', 'SQLAlchemy>=2.0.6', 'python-telegram-bot>=20.1', 'arrow>=1.0.0', From 0199e7d3d8dfe0046be307eabe70b58a6ef1e680 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Feb 2024 06:30:10 +0100 Subject: [PATCH 202/327] Add type-hint to exchange_has dict --- freqtrade/exchange/common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 06ae21001..8909ef5ff 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -2,7 +2,7 @@ import asyncio import logging import time from functools import wraps -from typing import Any, Callable, Optional, TypeVar, cast, overload +from typing import Any, Callable, Dict, List, Optional, TypeVar, cast, overload from freqtrade.constants import ExchangeConfig from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError @@ -61,7 +61,7 @@ SUPPORTED_EXCHANGES = [ ] # either the main, or replacement methods (array) is required -EXCHANGE_HAS_REQUIRED = { +EXCHANGE_HAS_REQUIRED: Dict[str, List[str]] = { # Required / private 'fetchOrder': ['fetchOpenOrder', 'fetchClosedOrder'], 'cancelOrder': [], From e8ca9ce39b23172baa4ecc8e954b1a5858eed25d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Feb 2024 20:00:45 +0100 Subject: [PATCH 203/327] Add testconfirming correct functioning --- tests/persistence/test_trade_custom_data.py | 40 ++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/persistence/test_trade_custom_data.py b/tests/persistence/test_trade_custom_data.py index 12767b811..cc86342d2 100644 --- a/tests/persistence/test_trade_custom_data.py +++ b/tests/persistence/test_trade_custom_data.py @@ -2,7 +2,7 @@ import pytest from freqtrade.persistence import Trade, disable_database_use, enable_database_use from freqtrade.persistence.custom_data import CustomDataWrapper -from tests.conftest import create_mock_trades_usdt +from tests.conftest import EXMS, create_mock_trades_usdt, get_patched_freqtradebot @pytest.mark.usefixtures("init_persistence") @@ -43,3 +43,41 @@ def test_trade_custom_data(fee, use_db): assert trade1.get_custom_data('test_dict') == {'test': 'dict'} assert isinstance(trade1.get_custom_data('test_dict'), dict) enable_database_use() + + +def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee): + + mocker.patch(f'{EXMS}.get_rate', return_value=0.50) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=None) + default_conf_usdt["minimal_roi"] = { + "0": 100 + } + + freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) + create_mock_trades_usdt(fee) + + trade1 = Trade.get_trades_proxy(pair='ADA/USDT')[0] + trade1.set_custom_data('test_str', 'test_value') + trade1.set_custom_data('test_int', 1) + + def custom_exit(pair, trade, **kwargs): + + if pair == 'ADA/USDT': + custom_val = trade.get_custom_data('test_str') + custom_val_i = trade.get_custom_data('test_int') + + return f"{custom_val}_{custom_val_i}" + + freqtrade.strategy.custom_exit = custom_exit + ff_spy = mocker.spy(freqtrade.strategy, 'custom_exit') + trades = Trade.get_open_trades() + freqtrade.exit_positions(trades) + Trade.commit() + + trade_after = Trade.get_trades_proxy(pair='ADA/USDT')[0] + assert trade_after.get_custom_data('test_str') == 'test_value' + assert trade_after.get_custom_data('test_int') == 1 + # 2 open pairs eligible for exit + assert ff_spy.call_count == 2 + + assert trade_after.exit_reason == 'test_value_1' From c511d65d2e10cc858591c36c2a1fc99673538eec Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Feb 2024 20:25:12 +0100 Subject: [PATCH 204/327] Add backtesting test --- tests/persistence/test_trade_custom_data.py | 83 ++++++++++++++++++++- 1 file changed, 79 insertions(+), 4 deletions(-) diff --git a/tests/persistence/test_trade_custom_data.py b/tests/persistence/test_trade_custom_data.py index cc86342d2..fe73f5f6b 100644 --- a/tests/persistence/test_trade_custom_data.py +++ b/tests/persistence/test_trade_custom_data.py @@ -1,8 +1,14 @@ +from copy import deepcopy +from unittest.mock import MagicMock + import pytest +from freqtrade.data.history.history_utils import get_timerange +from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade, disable_database_use, enable_database_use from freqtrade.persistence.custom_data import CustomDataWrapper -from tests.conftest import EXMS, create_mock_trades_usdt, get_patched_freqtradebot +from tests.conftest import (EXMS, create_mock_trades_usdt, generate_test_data, + get_patched_freqtradebot, patch_exchange) @pytest.mark.usefixtures("init_persistence") @@ -49,9 +55,7 @@ def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee): mocker.patch(f'{EXMS}.get_rate', return_value=0.50) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=None) - default_conf_usdt["minimal_roi"] = { - "0": 100 - } + default_conf_usdt["minimal_roi"] = {"0": 100} freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt) create_mock_trades_usdt(fee) @@ -81,3 +85,74 @@ def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee): assert ff_spy.call_count == 2 assert trade_after.exit_reason == 'test_value_1' + + +def test_trade_custom_data_strategy_backtest_compat(mocker, default_conf_usdt, fee): + + mocker.patch(f'{EXMS}.get_fee', fee) + mocker.patch(f"{EXMS}.get_min_pair_stake_amount", return_value=10) + mocker.patch(f"{EXMS}.get_max_pair_stake_amount", return_value=float('inf')) + mocker.patch(f"{EXMS}.get_max_leverage", return_value=10) + mocker.patch(f"{EXMS}.get_maintenance_ratio_and_amt", return_value=(0.1, 0.1)) + mocker.patch('freqtrade.optimize.backtesting.Backtesting._run_funding_fees') + + patch_exchange(mocker) + default_conf_usdt.update({ + "stake_amount": 100.0, + "max_open_trades": 2, + "dry_run_wallet": 1000.0, + "strategy": "StrategyTestV3", + "trading_mode": "futures", + "margin_mode": "isolated", + "stoploss": -2, + "minimal_roi": {"0": 100}, + }) + default_conf_usdt['pairlists'] = [{'method': 'StaticPairList', 'allow_inactive': True}] + backtesting = Backtesting(default_conf_usdt) + + df = generate_test_data(default_conf_usdt['timeframe'], 100, '2022-01-01 00:00:00+00:00') + + pair_exp = 'XRP/USDT:USDT' + + def custom_exit(pair, trade, **kwargs): + custom_val = trade.get_custom_data('test_str') + custom_val_i = trade.get_custom_data('test_int', 0) + + if pair == pair_exp: + trade.set_custom_data('test_str', 'test_value') + trade.set_custom_data('test_int', custom_val_i + 1) + + if custom_val_i >= 2: + return f"{custom_val}_{custom_val_i}" + + backtesting._set_strategy(backtesting.strategylist[0]) + processed = backtesting.strategy.advise_all_indicators({ + pair_exp: df, + 'BTC/USDT:USDT': df, + }) + + def fun(dataframe, *args, **kwargs): + dataframe.loc[dataframe.index == 50, 'enter_long'] = 1 + return dataframe + + backtesting.strategy.advise_entry = fun + backtesting.strategy.leverage = MagicMock(return_value=1) + backtesting.strategy.custom_exit = custom_exit + ff_spy = mocker.spy(backtesting.strategy, 'custom_exit') + + min_date, max_date = get_timerange(processed) + + result = backtesting.backtest( + processed=deepcopy(processed), + start_date=min_date, + end_date=max_date, + ) + results = result['results'] + assert not results.empty + assert len(results) == 2 + assert results['pair'][0] == pair_exp + assert results['pair'][1] == 'BTC/USDT:USDT' + assert results['exit_reason'][0] == 'test_value_2' + assert results['exit_reason'][1] == 'exit_signal' + + assert ff_spy.call_count == 7 From 4bbb3174b21771f2336f64ccf73c54dec626460d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Feb 2024 06:52:22 +0100 Subject: [PATCH 205/327] re-enable use_database after bt test --- tests/persistence/test_trade_custom_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/persistence/test_trade_custom_data.py b/tests/persistence/test_trade_custom_data.py index fe73f5f6b..bf1d73e1d 100644 --- a/tests/persistence/test_trade_custom_data.py +++ b/tests/persistence/test_trade_custom_data.py @@ -156,3 +156,4 @@ def test_trade_custom_data_strategy_backtest_compat(mocker, default_conf_usdt, f assert results['exit_reason'][1] == 'exit_signal' assert ff_spy.call_count == 7 + enable_database_use() From c013c76e64caa9e00330394fc597015607e8d69a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 20:49:18 +0000 Subject: [PATCH 206/327] Bump cryptography from 42.0.3 to 42.0.4 Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.3 to 42.0.4. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.3...42.0.4) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1efe0b7da..1c5bf8ba5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==2.1.4 pandas-ta==0.3.14b ccxt==4.2.47 -cryptography==42.0.3 +cryptography==42.0.4 aiohttp==3.9.3 SQLAlchemy==2.0.27 python-telegram-bot==20.8 From 4e87169a0ce074eba51007469234a8cb92502136 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Feb 2024 05:55:05 +0100 Subject: [PATCH 207/327] Use set to reduce iterations --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2872e603e..81d3973ba 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2169,7 +2169,7 @@ class Exchange: Should only be used for pairlists which need "on time" expirarion, and no longer cache. """ - timeframes = [p[1] for p in pairs] + timeframes = {p[1] for p in pairs} for timeframe in timeframes: if timeframe not in self._expiring_candle_cache: timeframe_in_sec = timeframe_to_seconds(timeframe) From 3680e199ce56672ed36e0b599deb6ba189861953 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Feb 2024 06:15:26 +0100 Subject: [PATCH 208/327] Fix range-stability filter downloading too little data --- freqtrade/plugins/pairlist/rangestabilityfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index f2cf4d486..49fba59b9 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -100,7 +100,7 @@ class RangeStabilityFilter(IPairList): needed_pairs: ListPairsWithTimeframes = [ (p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache] - since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days - 1)) + since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days + 1)) candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) if self._enabled: From 4cfd5d004ea10bb8866c68fa4709cbcbdd149969 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Feb 2024 06:38:28 +0100 Subject: [PATCH 209/327] Improve ohlcv pair cache with since_ms avoids some rare bugs in more complex configurations. --- freqtrade/exchange/exchange.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 81d3973ba..f896c7c51 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2171,16 +2171,16 @@ class Exchange: timeframes = {p[1] for p in pairs} for timeframe in timeframes: - if timeframe not in self._expiring_candle_cache: + if (timeframe, since_ms) not in self._expiring_candle_cache: timeframe_in_sec = timeframe_to_seconds(timeframe) # Initialise cache - self._expiring_candle_cache[timeframe] = PeriodicCache(ttl=timeframe_in_sec, - maxsize=1000) + self._expiring_candle_cache[(timeframe, since_ms)] = PeriodicCache( + ttl=timeframe_in_sec, maxsize=1000) # Get candles from cache candles = { - c: self._expiring_candle_cache[c[1]].get(c, None) for c in pairs - if c in self._expiring_candle_cache[c[1]] + c: self._expiring_candle_cache[(c[1], since_ms)].get(c, None) for c in pairs + if c in self._expiring_candle_cache[(c[1], since_ms)] } pairs_to_download = [p for p in pairs if p not in candles] if pairs_to_download: @@ -2188,7 +2188,7 @@ class Exchange: pairs_to_download, since_ms=since_ms, cache=False ) for c, val in candles.items(): - self._expiring_candle_cache[c[1]][c] = val + self._expiring_candle_cache[(c[1], since_ms)][c] = val return candles def _now_is_time_to_refresh(self, pair: str, timeframe: str, candle_type: CandleType) -> bool: From db83b0cdb0098d0f2d0a4b19443631a42a2d2abc Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Feb 2024 07:09:47 +0100 Subject: [PATCH 210/327] Update typehint for candle_cache --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f896c7c51..7f7fccca8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -132,7 +132,7 @@ class Exchange: # Holds candles self._klines: Dict[PairWithTimeframe, DataFrame] = {} - self._expiring_candle_cache: Dict[str, PeriodicCache] = {} + self._expiring_candle_cache: Dict[Tuple[str, int], PeriodicCache] = {} # Holds all open sell orders for dry_run self._dry_run_open_orders: Dict[str, Any] = {} From e30fa3a4459c3ed154b0b3f81f4e083721870ca6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Feb 2024 18:17:58 +0100 Subject: [PATCH 211/327] Remove duplicate pairlistmanager init --- freqtrade/freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2032e437d..4c6c4c1d4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -82,7 +82,6 @@ class FreqtradeBot(LoggingMixin): PairLocks.timeframe = self.config['timeframe'] - self.pairlists = PairListManager(self.exchange, self.config) self.trading_mode: TradingMode = self.config.get('trading_mode', TradingMode.SPOT) self.last_process: Optional[datetime] = None From c7fff45bef14cb8c3009378aa141cf2570334e3f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Feb 2024 19:01:33 +0100 Subject: [PATCH 212/327] Fix test leakage --- tests/persistence/test_trade_custom_data.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/persistence/test_trade_custom_data.py b/tests/persistence/test_trade_custom_data.py index bf1d73e1d..15241aa93 100644 --- a/tests/persistence/test_trade_custom_data.py +++ b/tests/persistence/test_trade_custom_data.py @@ -48,7 +48,8 @@ def test_trade_custom_data(fee, use_db): assert trade1.get_custom_data('test_dict') == {'test': 'dict'} assert isinstance(trade1.get_custom_data('test_dict'), dict) - enable_database_use() + if not use_db: + enable_database_use() def test_trade_custom_data_strategy_compat(mocker, default_conf_usdt, fee): @@ -156,4 +157,4 @@ def test_trade_custom_data_strategy_backtest_compat(mocker, default_conf_usdt, f assert results['exit_reason'][1] == 'exit_signal' assert ff_spy.call_count == 7 - enable_database_use() + Backtesting.cleanup() From f4ad1e802007bc11518ab133ac4773822815891d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Feb 2024 19:02:04 +0100 Subject: [PATCH 213/327] chore: Remove wrong typehint --- tests/optimize/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index 4d257addc..cb8a6b5f7 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -30,7 +30,7 @@ def hyperopt_conf(default_conf): @pytest.fixture(autouse=True) -def backtesting_cleanup() -> None: +def backtesting_cleanup(): yield None Backtesting.cleanup() From 0acde289023a32fe9c3137ff8aaf0192171a9d56 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Feb 2024 19:28:02 +0100 Subject: [PATCH 214/327] Remove pointless comment --- freqtrade/plugins/pairlist/VolatilityFilter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index b6ce1b9a2..ef72486e1 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -103,7 +103,6 @@ class VolatilityFilter(IPairList): (p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache] since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days)) - # Get all candles candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) if self._enabled: From 9ac7149c47c9e07fa09b0c2ae73f12b2a1522370 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Feb 2024 22:14:29 +0100 Subject: [PATCH 215/327] Add check to verify that "since" is properly respected. --- tests/exchange_online/test_ccxt_compat.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/exchange_online/test_ccxt_compat.py b/tests/exchange_online/test_ccxt_compat.py index f95f4c000..370bc8184 100644 --- a/tests/exchange_online/test_ccxt_compat.py +++ b/tests/exchange_online/test_ccxt_compat.py @@ -12,6 +12,7 @@ import pytest from freqtrade.enums import CandleType from freqtrade.exchange import timeframe_to_minutes, timeframe_to_prev_date from freqtrade.exchange.exchange import timeframe_to_msecs +from freqtrade.util import dt_floor_day, dt_now, dt_ts from tests.exchange_online.conftest import EXCHANGE_FIXTURE_TYPE, EXCHANGES @@ -187,6 +188,25 @@ class TestCCXTExchange: now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2)) assert exch.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) + def test_ccxt_fetch_ohlcv_startdate(self, exchange: EXCHANGE_FIXTURE_TYPE): + """ + Test that pair data starts at the provided startdate + """ + exch, exchangename = exchange + pair = EXCHANGES[exchangename]['pair'] + timeframe = '1d' + + pair_tf = (pair, timeframe, CandleType.SPOT) + # last 5 days ... + since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=6)) + ohlcv = exch.refresh_latest_ohlcv([pair_tf], since_ms=since_ms) + assert isinstance(ohlcv, dict) + assert len(ohlcv[pair_tf]) == len(exch.klines(pair_tf)) + # Check if last-timeframe is within the last 2 intervals + now = datetime.now(timezone.utc) - timedelta(minutes=(timeframe_to_minutes(timeframe) * 2)) + assert exch.klines(pair_tf).iloc[-1]['date'] >= timeframe_to_prev_date(timeframe, now) + assert exch.klines(pair_tf)['date'].astype(int).iloc[0] // 1e6 == since_ms + def ccxt__async_get_candle_history( self, exchange, exchangename, pair, timeframe, candle_type, factor=0.9): From 6307e1630498e015e8df03fdc21a55e583160414 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Feb 2024 06:45:28 +0100 Subject: [PATCH 216/327] Properly format notification date --- freqtrade/rpc/telegram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index d0e12cc4a..4f4ea17d3 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -33,7 +33,7 @@ from freqtrade.misc import chunks, plural from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException, RPCHandler from freqtrade.rpc.rpc_types import RPCEntryMsg, RPCExitMsg, RPCOrderMsg, RPCSendMsg -from freqtrade.util import dt_humanize, fmt_coin, round_value +from freqtrade.util import dt_humanize, fmt_coin, format_date, round_value MAX_MESSAGE_LENGTH = MessageLimit.MAX_TEXT_LENGTH @@ -1797,8 +1797,8 @@ class Telegram(RPCHandler): f"*Trade ID:* `{result['ft_trade_id']}`", f"*Type:* `{result['cd_type']}`", f"*Value:* `{result['cd_value']}`", - f"*Create Date:* `{result['created_at']}`", - f"*Update Date:* `{result['updated_at']}`" + f"*Create Date:* `{format_date(result['created_at'])}`", + f"*Update Date:* `{format_date(result['updated_at'])}`" ] # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line])) From c663016b47ed29641c31344cf5f1a185d38bc18f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 08:06:06 +0100 Subject: [PATCH 217/327] Improve some type safety --- freqtrade/freqtradebot.py | 8 ++++---- freqtrade/persistence/trade_model.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4c6c4c1d4..3f23f43ae 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -986,7 +986,7 @@ class FreqtradeBot(LoggingMixin): return enter_limit_requested, stake_amount, leverage - def _notify_enter(self, trade: Trade, order: Order, order_type: str, + def _notify_enter(self, trade: Trade, order: Order, order_type: Optional[str], fill: bool = False, sub_trade: bool = False) -> None: """ Sends rpc notification when a entry order occurred. @@ -1010,7 +1010,7 @@ class FreqtradeBot(LoggingMixin): 'direction': 'Short' if trade.is_short else 'Long', 'limit': open_rate, # Deprecated (?) 'open_rate': open_rate, - 'order_type': order_type, + 'order_type': order_type or 'unknown', 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], 'base_currency': self.exchange.get_pair_base_currency(trade.pair), @@ -1775,7 +1775,7 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False, + def _notify_exit(self, trade: Trade, order_type: Optional[str], fill: bool = False, sub_trade: bool = False, order: Optional[Order] = None) -> None: """ Sends rpc notification when a sell occurred. @@ -1807,7 +1807,7 @@ class FreqtradeBot(LoggingMixin): 'gain': gain, 'limit': order_rate, # Deprecated 'order_rate': order_rate, - 'order_type': order_type, + 'order_type': order_type or 'unknown', 'amount': amount, 'open_rate': trade.open_rate, 'close_rate': order_rate, diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index b1330b83c..407affe72 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -73,8 +73,7 @@ class Order(ModelBase): order_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True) status: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) symbol: Mapped[Optional[str]] = mapped_column(String(25), nullable=True) - # TODO: type: order_type type is Optional[str] - order_type: Mapped[str] = mapped_column(String(50), nullable=True) + order_type: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) side: Mapped[str] = mapped_column(String(25), nullable=True) price: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) average: Mapped[Optional[float]] = mapped_column(Float(), nullable=True) @@ -815,6 +814,7 @@ class LocalTrade: order.funding_fee = self.funding_fee_running # Reset running funding fees self.funding_fee_running = 0.0 + order_type = order.order_type.upper() if order.order_type else None if order.ft_order_side == self.entry_side: # Update open rate and actual amount @@ -822,20 +822,20 @@ 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.order_type.upper()}_{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.order_type.upper()}_{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', ): 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.order_type.upper()} is hit for {self}.') + logger.info(f'{order_type} is hit for {self}.') else: raise ValueError(f'Unknown order type: {order.order_type}') From 7ddaa09a2380dc2df81d0a570379a2808afa655f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 08:23:55 +0100 Subject: [PATCH 218/327] Refactor VolatilityFilter --- freqtrade/plugins/pairlist/VolatilityFilter.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index ef72486e1..f18af2c97 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -105,13 +105,13 @@ class VolatilityFilter(IPairList): since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days)) candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) - if self._enabled: - for p in deepcopy(pairlist): - daily_candles = candles[(p, '1d', self._def_candletype)] if ( - p, '1d', self._def_candletype) in candles else None - if not self._validate_pair_loc(p, daily_candles): - pairlist.remove(p) - return pairlist + resulting_pairlist: List[str] = [] + for p in pairlist: + daily_candles = candles[(p, '1d', self._def_candletype)] if ( + p, '1d', self._def_candletype) in candles else None + if self._validate_pair_loc(p, daily_candles): + resulting_pairlist.append(p) + return resulting_pairlist def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: """ From 0bf73cc64b66cee16f9d0de95e10fdc553e989c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 13:11:43 +0100 Subject: [PATCH 219/327] Voliatilityfilter - sorting --- .../plugins/pairlist/VolatilityFilter.py | 62 +++++++++++++------ 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index f18af2c97..70b48f7d2 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -37,6 +37,7 @@ class VolatilityFilter(IPairList): self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize) self._refresh_period = pairlistconfig.get('refresh_period', 1440) self._def_candletype = self._config['candle_type_def'] + self._sort_direction: Optional[str] = pairlistconfig.get('sort_direction', None) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) @@ -89,6 +90,13 @@ class VolatilityFilter(IPairList): "description": "Maximum Volatility", "help": "Maximum volatility a pair must have to be considered.", }, + "sort_direction": { + "type": "option", + "default": None, + "options": [None, "asc", "desc"], + "description": "Sort pairlist", + "help": "Sort Pairlist", + }, **IPairList.refresh_period_parameter() } @@ -106,14 +114,34 @@ class VolatilityFilter(IPairList): candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) resulting_pairlist: List[str] = [] + volatilitys: Dict[str, float] = {} for p in pairlist: daily_candles = candles[(p, '1d', self._def_candletype)] if ( p, '1d', self._def_candletype) in candles else None - if self._validate_pair_loc(p, daily_candles): - resulting_pairlist.append(p) + + if daily_candles is not None and not daily_candles.empty: + volatility_avg = self._calculate_volatility(deepcopy(daily_candles)) + + if self._validate_pair_loc(p, volatility_avg): + resulting_pairlist.append(p) + if self._sort_direction: + volatilitys[p] = volatility_avg if not np.isnan(volatility_avg) else 0 + + if self._sort_direction: + resulting_pairlist = sorted(resulting_pairlist, + key=lambda p: volatilitys[p], + reverse=self._sort_direction == 'desc') return resulting_pairlist - def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: + def _calculate_volatility(self, daily_candles: DataFrame) -> float: + returns = (np.log(daily_candles["close"].shift(1) / daily_candles["close"])) + returns.fillna(0, inplace=True) + + volatility_series = returns.rolling(window=self._days).std() * np.sqrt(self._days) + volatility_avg = volatility_series.mean() + return volatility_avg + + def _validate_pair_loc(self, pair: str, volatility_avg: float) -> bool: """ Validate trading range :param pair: Pair that's currently validated @@ -125,23 +153,17 @@ class VolatilityFilter(IPairList): return cached_res result = False - if daily_candles is not None and not daily_candles.empty: - returns = (np.log(daily_candles["close"].shift(1) / daily_candles["close"])) - returns.fillna(0, inplace=True) - volatility_series = returns.rolling(window=self._days).std() * np.sqrt(self._days) - volatility_avg = volatility_series.mean() - - if self._min_volatility <= volatility_avg <= self._max_volatility: - result = True - else: - self.log_once(f"Removed {pair} from whitelist, because volatility " - f"over {self._days} {plural(self._days, 'day')} " - f"is: {volatility_avg:.3f} " - f"which is not in the configured range of " - f"{self._min_volatility}-{self._max_volatility}.", - logger.info) - result = False - self._pair_cache[pair] = result + if self._min_volatility <= volatility_avg <= self._max_volatility: + result = True + else: + self.log_once(f"Removed {pair} from whitelist, because volatility " + f"over {self._days} {plural(self._days, 'day')} " + f"is: {volatility_avg:.3f} " + f"which is not in the configured range of " + f"{self._min_volatility}-{self._max_volatility}.", + logger.info) + result = False + self._pair_cache[pair] = result return result From 38ca58c728a752bf3eaa9164f1a724ea07807a95 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 13:12:38 +0100 Subject: [PATCH 220/327] Add verification for volatilityfilter --- freqtrade/plugins/pairlist/VolatilityFilter.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 70b48f7d2..ff1525d70 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -47,6 +47,9 @@ class VolatilityFilter(IPairList): if self._days > candle_limit: raise OperationalException("VolatilityFilter requires lookback_days to not " f"exceed exchange max request size ({candle_limit})") + if self._sort_direction not in [None, 'asc', 'desc']: + raise OperationalException("VolatilityFilter requires sort_direction to be " + "either None (undefined), 'asc' or 'desc'") @property def needstickers(self) -> bool: From eaf70428c161a790a7e72fdf0549a78591a186f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 13:20:59 +0100 Subject: [PATCH 221/327] Improve volatility tests --- tests/plugins/test_pairlist.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index d125f8896..64203d9a6 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -748,6 +748,32 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: assert log_has("PerformanceFilter is not available in this mode.", caplog) +def test_VolatilityFilter_error(mocker, whitelist_conf) -> None: + volatility_filter = {"method": "VolatilityFilter", "lookback_days": -1} + whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, volatility_filter] + + mocker.patch(f'{EXMS}.exchange_has', MagicMock(return_value=True)) + exchange_mock = MagicMock() + exchange_mock.ohlcv_candle_limit = MagicMock(return_value=1000) + + with pytest.raises(OperationalException, + match=r"VolatilityFilter requires lookback_days to be >= 1*"): + PairListManager(exchange_mock, whitelist_conf, MagicMock()) + + volatility_filter = {"method": "VolatilityFilter", "lookback_days": 2000} + whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, volatility_filter] + with pytest.raises(OperationalException, + match=r"VolatilityFilter requires lookback_days to not exceed exchange max"): + PairListManager(exchange_mock, whitelist_conf, MagicMock()) + + volatility_filter = {"method": "VolatilityFilter", "sort_direction": "Random"} + whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, volatility_filter] + with pytest.raises(OperationalException, + match=r"VolatilityFilter requires sort_direction to be either " + r"None .*'asc'.*'desc'"): + PairListManager(exchange_mock, whitelist_conf, MagicMock()) + + def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None: whitelist_conf['pairlists'] = [ {"method": "StaticPairList"}, From 31e254313425892abb6a51ccd205185b85e39693 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 13:30:38 +0100 Subject: [PATCH 222/327] Enhance generate_test_data with parametrizable random seed --- tests/conftest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 9c81c050d..c1c35fc9d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -142,8 +142,8 @@ def generate_trades_history(n_rows, start_date: Optional[datetime] = None, days= return df -def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): - np.random.seed(42) +def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05', random_seed=42): + np.random.seed(random_seed) base = np.random.normal(20, 2, size=size) if timeframe == '1y': @@ -174,9 +174,9 @@ def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): return df -def generate_test_data_raw(timeframe: str, size: int, start: str = '2020-07-05'): +def generate_test_data_raw(timeframe: str, size: int, start: str = '2020-07-05', random_seed=42): """ Generates data in the ohlcv format used by ccxt """ - df = generate_test_data(timeframe, size, start) + df = generate_test_data(timeframe, size, start, random_seed) df['date'] = df.loc[:, 'date'].view(np.int64) // 1000 // 1000 return list(list(x) for x in zip(*(df[x].values.tolist() for x in df.columns))) From 91ba4f642425e239d77acb4ef4a9f211dabe992d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 13:31:26 +0100 Subject: [PATCH 223/327] Add test for volatilityFilter sorting --- tests/plugins/test_pairlist.py | 35 +++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 64203d9a6..f16f5dce9 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -19,7 +19,7 @@ from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist, from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver from freqtrade.util.datetime_helpers import dt_now -from tests.conftest import (EXMS, create_mock_trades_usdt, get_patched_exchange, +from tests.conftest import (EXMS, create_mock_trades_usdt, generate_test_data, get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re, num_log_has) @@ -774,6 +774,39 @@ def test_VolatilityFilter_error(mocker, whitelist_conf) -> None: PairListManager(exchange_mock, whitelist_conf, MagicMock()) +@pytest.mark.parametrize('sort_direction', ['asc', 'desc']) +def test_VolatilityFilter_sort(mocker, whitelist_conf, time_machine, sort_direction) -> None: + volatility_filter = {"method": "VolatilityFilter", "sort_direction": sort_direction} + whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, volatility_filter] + + df1 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=42) + df2 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=1) + assert not df1.equals(df2) + time_machine.move_to('2022-01-15 00:00:00+00:00') + + ohlcv_data = { + ('ETH/BTC', '1d', CandleType.SPOT): df1, + ('TKN/BTC', '1d', CandleType.SPOT): df2, + + } + mocker.patch.multiple( + EXMS, + exchange_has=MagicMock(return_value=True), + refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), + ) + + exchange = get_patched_exchange(mocker, whitelist_conf) + exchange.ohlcv_candle_limit = MagicMock(return_value=1000) + plm = PairListManager(exchange, whitelist_conf, MagicMock()) + + assert exchange.ohlcv_candle_limit.call_count == 1 + plm.refresh_pairlist() + assert exchange.ohlcv_candle_limit.call_count == 1 + assert plm.whitelist == ( + ['ETH/BTC', 'TKN/BTC'] if sort_direction == 'asc' else ['TKN/BTC', 'ETH/BTC'] + ) + + def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None: whitelist_conf['pairlists'] = [ {"method": "StaticPairList"}, From 866ff55d840d747e51995379d677e81735a6fad2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 13:34:42 +0100 Subject: [PATCH 224/327] document sort_direction mode --- docs/includes/pairlists.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index d1dd2cda7..844e30ff9 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -460,7 +460,7 @@ Volatility is the degree of historical variation of a pairs over time, it is mea This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. -This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs. +This filter can be used to narrow down your pairs to a certain volatility or avoid very volatile pairs. In the below example: If the volatility over the last 10 days is not in the range of 0.05-0.50, remove the pair from the whitelist. The filter is applied every 24h. @@ -477,6 +477,9 @@ If the volatility over the last 10 days is not in the range of 0.05-0.50, remove ] ``` +Adding `"sort_direction": "asc"` or `"sort_direction": "desc"` enables sorting mode for this pairlist. +When sorting, caching will be applied at the candle level - ignoring `refresh_period` (the candle's won't change anyway). + ### Full example of Pairlist Handlers The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#pricefilter), filtering all assets where 1 price unit is > 1%. Then the [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) is applied and pairs are finally shuffled with the random seed set to some predefined value. From 88a2995b4c66baa0790493a1c291113bfc70354f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 13:39:46 +0100 Subject: [PATCH 225/327] Fix wrong typehint --- freqtrade/plugins/pairlist/VolatilityFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index ff1525d70..d71d13a4d 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -148,7 +148,7 @@ class VolatilityFilter(IPairList): """ Validate trading range :param pair: Pair that's currently validated - :param daily_candles: Downloaded daily candles + :param volatility_avg: Average volatility :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache From 7af46628f8386bb6061090d2d04739906b675a9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 13:50:54 +0100 Subject: [PATCH 226/327] Simplify rangeStability Filter --- .../plugins/pairlist/rangestabilityfilter.py | 81 ++++++++++--------- 1 file changed, 45 insertions(+), 36 deletions(-) diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index 49fba59b9..d66ea92ec 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -2,9 +2,8 @@ Rate of change pairlist filter """ import logging -from copy import deepcopy from datetime import timedelta -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from cachetools import TTLCache from pandas import DataFrame @@ -103,45 +102,55 @@ class RangeStabilityFilter(IPairList): since_ms = dt_ts(dt_floor_day(dt_now()) - timedelta(days=self._days + 1)) candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) - if self._enabled: - for p in deepcopy(pairlist): - daily_candles = candles[(p, '1d', self._def_candletype)] if ( - p, '1d', self._def_candletype) in candles else None - if not self._validate_pair_loc(p, daily_candles): - pairlist.remove(p) - return pairlist + resulting_pairlist: List[str] = [] - def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: - """ - Validate trading range - :param pair: Pair that's currently validated - :param daily_candles: Downloaded daily candles - :return: True if the pair can stay, false if it should be removed - """ + for p in pairlist: + daily_candles = candles.get((p, '1d', self._def_candletype), None) + + pct_change = self._calculate_rate_of_change(p, daily_candles) + + if pct_change is not None and self._validate_pair_loc(p, pct_change): + resulting_pairlist.append(p) + else: + self.log_once(f"Removed {p} from whitelist, no candles found.", logger.info) + + return resulting_pairlist + + def _calculate_rate_of_change(self, pair: str, daily_candles: DataFrame) -> float: # Check symbol in cache - if (cached_res := self._pair_cache.get(pair, None)) is not None: - return cached_res - - result = True + if (pct_change := self._pair_cache.get(pair, None)) is not None: + return pct_change if daily_candles is not None and not daily_candles.empty: + highest_high = daily_candles['high'].max() lowest_low = daily_candles['low'].min() pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0 - if pct_change < self._min_rate_of_change: - self.log_once(f"Removed {pair} from whitelist, because rate of change " - f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " - f"which is below the threshold of {self._min_rate_of_change}.", - logger.info) - result = False - if self._max_rate_of_change: - if pct_change > self._max_rate_of_change: - self.log_once( - f"Removed {pair} from whitelist, because rate of change " - f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " - f"which is above the threshold of {self._max_rate_of_change}.", - logger.info) - result = False - self._pair_cache[pair] = result + self._pair_cache[pair] = pct_change + return pct_change else: - self.log_once(f"Removed {pair} from whitelist, no candles found.", logger.info) + return None + + def _validate_pair_loc(self, pair: str, pct_change: float) -> bool: + """ + Validate trading range + :param pair: Pair that's currently validated + :param pct_change: Rate of change + :return: True if the pair can stay, false if it should be removed + """ + + result = True + if pct_change < self._min_rate_of_change: + self.log_once(f"Removed {pair} from whitelist, because rate of change " + f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " + f"which is below the threshold of {self._min_rate_of_change}.", + logger.info) + result = False + if self._max_rate_of_change: + if pct_change > self._max_rate_of_change: + self.log_once( + f"Removed {pair} from whitelist, because rate of change " + f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " + f"which is above the threshold of {self._max_rate_of_change}.", + logger.info) + result = False return result From 3677953d90e6b93e98bb9f6294a5959428750a46 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 13:54:52 +0100 Subject: [PATCH 227/327] Properly cache volatility-average --- docs/includes/pairlists.md | 1 - .../plugins/pairlist/VolatilityFilter.py | 42 +++++++++---------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 844e30ff9..51c38fcce 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -478,7 +478,6 @@ If the volatility over the last 10 days is not in the range of 0.05-0.50, remove ``` Adding `"sort_direction": "asc"` or `"sort_direction": "desc"` enables sorting mode for this pairlist. -When sorting, caching will be applied at the candle level - ignoring `refresh_period` (the candle's won't change anyway). ### Full example of Pairlist Handlers diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index d71d13a4d..36f24af4b 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -119,16 +119,14 @@ class VolatilityFilter(IPairList): resulting_pairlist: List[str] = [] volatilitys: Dict[str, float] = {} for p in pairlist: - daily_candles = candles[(p, '1d', self._def_candletype)] if ( - p, '1d', self._def_candletype) in candles else None + daily_candles = candles.get((p, '1d', self._def_candletype), None) - if daily_candles is not None and not daily_candles.empty: - volatility_avg = self._calculate_volatility(deepcopy(daily_candles)) + volatility_avg = self._calculate_volatility(p, daily_candles) - if self._validate_pair_loc(p, volatility_avg): - resulting_pairlist.append(p) - if self._sort_direction: - volatilitys[p] = volatility_avg if not np.isnan(volatility_avg) else 0 + if volatility_avg is not None and self._validate_pair_loc(p, volatility_avg): + resulting_pairlist.append(p) + if self._sort_direction: + volatilitys[p] = volatility_avg if not np.isnan(volatility_avg) else 0 if self._sort_direction: resulting_pairlist = sorted(resulting_pairlist, @@ -136,13 +134,22 @@ class VolatilityFilter(IPairList): reverse=self._sort_direction == 'desc') return resulting_pairlist - def _calculate_volatility(self, daily_candles: DataFrame) -> float: - returns = (np.log(daily_candles["close"].shift(1) / daily_candles["close"])) - returns.fillna(0, inplace=True) + def _calculate_volatility(self, pair: str, daily_candles: DataFrame) -> float: + # Check symbol in cache + if (volatility_avg := self._pair_cache.get(pair, None)) is not None: + return volatility_avg - volatility_series = returns.rolling(window=self._days).std() * np.sqrt(self._days) - volatility_avg = volatility_series.mean() - return volatility_avg + if daily_candles is not None and not daily_candles.empty: + returns = (np.log(daily_candles["close"].shift(1) / daily_candles["close"])) + returns.fillna(0, inplace=True) + + volatility_series = returns.rolling(window=self._days).std() * np.sqrt(self._days) + volatility_avg = volatility_series.mean() + self._pair_cache[pair] = volatility_avg + + return volatility_avg + else: + return None def _validate_pair_loc(self, pair: str, volatility_avg: float) -> bool: """ @@ -151,11 +158,6 @@ class VolatilityFilter(IPairList): :param volatility_avg: Average volatility :return: True if the pair can stay, false if it should be removed """ - # Check symbol in cache - if (cached_res := self._pair_cache.get(pair, None)) is not None: - return cached_res - - result = False if self._min_volatility <= volatility_avg <= self._max_volatility: result = True @@ -167,6 +169,4 @@ class VolatilityFilter(IPairList): f"{self._min_volatility}-{self._max_volatility}.", logger.info) result = False - self._pair_cache[pair] = result - return result From 81de29a1e3b9f28dc6c77bd4c8f28d1fe66cbc83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 14:00:50 +0100 Subject: [PATCH 228/327] Improve conditions for removal of pairs --- freqtrade/plugins/pairlist/VolatilityFilter.py | 8 ++++++-- freqtrade/plugins/pairlist/rangestabilityfilter.py | 5 +++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 36f24af4b..224dfcca8 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -123,8 +123,12 @@ class VolatilityFilter(IPairList): volatility_avg = self._calculate_volatility(p, daily_candles) - if volatility_avg is not None and self._validate_pair_loc(p, volatility_avg): - resulting_pairlist.append(p) + if volatility_avg is not None: + if self._validate_pair_loc(p, volatility_avg): + resulting_pairlist.append(p) + else: + self.log_once(f"Removed {p} from whitelist, no candles found.", logger.info) + if self._sort_direction: volatilitys[p] = volatility_avg if not np.isnan(volatility_avg) else 0 diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index d66ea92ec..ff0ec80e4 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -109,8 +109,9 @@ class RangeStabilityFilter(IPairList): pct_change = self._calculate_rate_of_change(p, daily_candles) - if pct_change is not None and self._validate_pair_loc(p, pct_change): - resulting_pairlist.append(p) + if pct_change is not None: + if self._validate_pair_loc(p, pct_change): + resulting_pairlist.append(p) else: self.log_once(f"Removed {p} from whitelist, no candles found.", logger.info) From 6a313aa9e38210823e9372acc07d301cda138dfb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 14:03:26 +0100 Subject: [PATCH 229/327] Improve help wording --- freqtrade/plugins/pairlist/VolatilityFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 224dfcca8..301a92b7d 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -98,7 +98,7 @@ class VolatilityFilter(IPairList): "default": None, "options": [None, "asc", "desc"], "description": "Sort pairlist", - "help": "Sort Pairlist", + "help": "Sort Pairlist ascending or descending by volatility.", }, **IPairList.refresh_period_parameter() } From 9dd59672756fe2a9f85ac5d6dcc34222fa8b4e9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 14:03:50 +0100 Subject: [PATCH 230/327] Add sorting capabilities to rangeStabilityFilter --- .../plugins/pairlist/rangestabilityfilter.py | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index ff0ec80e4..0bd35997c 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -3,7 +3,7 @@ Rate of change pairlist filter """ import logging from datetime import timedelta -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional from cachetools import TTLCache from pandas import DataFrame @@ -31,6 +31,7 @@ class RangeStabilityFilter(IPairList): self._max_rate_of_change = pairlistconfig.get('max_rate_of_change') self._refresh_period = pairlistconfig.get('refresh_period', 86400) self._def_candletype = self._config['candle_type_def'] + self._sort_direction: Optional[str] = pairlistconfig.get('sort_direction', None) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) @@ -40,7 +41,9 @@ class RangeStabilityFilter(IPairList): if self._days > candle_limit: raise OperationalException("RangeStabilityFilter requires lookback_days to not " f"exceed exchange max request size ({candle_limit})") - + if self._sort_direction not in [None, 'asc', 'desc']: + raise OperationalException("RangeStabilityFilter requires sort_direction to be " + "either None (undefined), 'asc' or 'desc'") @property def needstickers(self) -> bool: """ @@ -86,6 +89,13 @@ class RangeStabilityFilter(IPairList): "description": "Maximum Rate of Change", "help": "Maximum rate of change to filter pairs.", }, + "sort_direction": { + "type": "option", + "default": None, + "options": [None, "asc", "desc"], + "description": "Sort pairlist", + "help": "Sort Pairlist ascending or descending by rate of change.", + }, **IPairList.refresh_period_parameter() } @@ -103,6 +113,7 @@ class RangeStabilityFilter(IPairList): candles = self._exchange.refresh_ohlcv_with_cache(needed_pairs, since_ms=since_ms) resulting_pairlist: List[str] = [] + pct_changes: Dict[str, float] = {} for p in pairlist: daily_candles = candles.get((p, '1d', self._def_candletype), None) @@ -112,9 +123,14 @@ class RangeStabilityFilter(IPairList): if pct_change is not None: if self._validate_pair_loc(p, pct_change): resulting_pairlist.append(p) + pct_changes[p] = pct_change else: self.log_once(f"Removed {p} from whitelist, no candles found.", logger.info) + if self._sort_direction: + resulting_pairlist = sorted(resulting_pairlist, + key=lambda p: pct_changes[p], + reverse=self._sort_direction == 'desc') return resulting_pairlist def _calculate_rate_of_change(self, pair: str, daily_candles: DataFrame) -> float: From 2704f6e758e9a0a926bd2599f329dc51834e2cd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 14:05:25 +0100 Subject: [PATCH 231/327] Improve test --- tests/plugins/test_pairlist.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index f16f5dce9..43a99df33 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -789,10 +789,11 @@ def test_VolatilityFilter_sort(mocker, whitelist_conf, time_machine, sort_direct ('TKN/BTC', '1d', CandleType.SPOT): df2, } + ohlcv_mock = MagicMock(return_value=ohlcv_data) mocker.patch.multiple( EXMS, exchange_has=MagicMock(return_value=True), - refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), + refresh_latest_ohlcv=ohlcv_mock, ) exchange = get_patched_exchange(mocker, whitelist_conf) @@ -801,11 +802,16 @@ def test_VolatilityFilter_sort(mocker, whitelist_conf, time_machine, sort_direct assert exchange.ohlcv_candle_limit.call_count == 1 plm.refresh_pairlist() + assert ohlcv_mock.call_count == 1 assert exchange.ohlcv_candle_limit.call_count == 1 assert plm.whitelist == ( ['ETH/BTC', 'TKN/BTC'] if sort_direction == 'asc' else ['TKN/BTC', 'ETH/BTC'] ) + plm.refresh_pairlist() + assert exchange.ohlcv_candle_limit.call_count == 1 + assert ohlcv_mock.call_count == 1 + def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None: whitelist_conf['pairlists'] = [ From b972ee78ec34bff99d1b111452201be45bfa3a45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 14:41:06 +0100 Subject: [PATCH 232/327] Enhance rangeStability test --- tests/plugins/test_pairlist.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 43a99df33..6cf331c86 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -1160,6 +1160,13 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): match='RangeStabilityFilter requires lookback_days to be >= 1'): get_patched_freqtradebot(mocker, default_conf) + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'RangeStabilityFilter', 'sort_direction': 'something'}] + + with pytest.raises(OperationalException, + match='RangeStabilityFilter requires sort_direction to be either None.*'): + get_patched_freqtradebot(mocker, default_conf) + @pytest.mark.parametrize('min_rate_of_change,max_rate_of_change,expected_length', [ (0.01, 0.99, 5), From e82d9e2f5568c1f2847fb2cbf7cfe5d8cf944973 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 14:45:15 +0100 Subject: [PATCH 233/327] Test volatilityfilter with more pairs --- tests/plugins/test_pairlist.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 6cf331c86..5d7497273 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -775,18 +775,29 @@ def test_VolatilityFilter_error(mocker, whitelist_conf) -> None: @pytest.mark.parametrize('sort_direction', ['asc', 'desc']) -def test_VolatilityFilter_sort(mocker, whitelist_conf, time_machine, sort_direction) -> None: - volatility_filter = {"method": "VolatilityFilter", "sort_direction": sort_direction} - whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, volatility_filter] +def test_VolatilityFilter_sort( + mocker, whitelist_conf, tickers, time_machine, sort_direction) -> None: + whitelist_conf['pairlists'] = [ + {'method': 'VolumePairList', 'number_assets': 10}, + {"method": "VolatilityFilter", "sort_direction": sort_direction}] df1 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=42) - df2 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=1) + df2 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=2) + df3 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=3) + df4 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=4) + df5 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=5) + df6 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=6) + assert not df1.equals(df2) time_machine.move_to('2022-01-15 00:00:00+00:00') ohlcv_data = { ('ETH/BTC', '1d', CandleType.SPOT): df1, ('TKN/BTC', '1d', CandleType.SPOT): df2, + ('LTC/BTC', '1d', CandleType.SPOT): df3, + ('XRP/BTC', '1d', CandleType.SPOT): df4, + ('HOT/BTC', '1d', CandleType.SPOT): df5, + ('BLK/BTC', '1d', CandleType.SPOT): df6, } ohlcv_mock = MagicMock(return_value=ohlcv_data) @@ -794,22 +805,25 @@ def test_VolatilityFilter_sort(mocker, whitelist_conf, time_machine, sort_direct EXMS, exchange_has=MagicMock(return_value=True), refresh_latest_ohlcv=ohlcv_mock, + get_tickers=tickers + ) exchange = get_patched_exchange(mocker, whitelist_conf) exchange.ohlcv_candle_limit = MagicMock(return_value=1000) plm = PairListManager(exchange, whitelist_conf, MagicMock()) - assert exchange.ohlcv_candle_limit.call_count == 1 + assert exchange.ohlcv_candle_limit.call_count == 2 plm.refresh_pairlist() assert ohlcv_mock.call_count == 1 - assert exchange.ohlcv_candle_limit.call_count == 1 + assert exchange.ohlcv_candle_limit.call_count == 2 assert plm.whitelist == ( - ['ETH/BTC', 'TKN/BTC'] if sort_direction == 'asc' else ['TKN/BTC', 'ETH/BTC'] + ['XRP/BTC', 'ETH/BTC', 'LTC/BTC', 'TKN/BTC'] if sort_direction == 'asc' + else ['TKN/BTC', 'LTC/BTC', 'ETH/BTC', 'XRP/BTC'] ) plm.refresh_pairlist() - assert exchange.ohlcv_candle_limit.call_count == 1 + assert exchange.ohlcv_candle_limit.call_count == 2 assert ohlcv_mock.call_count == 1 From 67152ad48a1fbf7a621acb779fe4b7aa2b200cac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 14:56:42 +0100 Subject: [PATCH 234/327] Improve and parametrize pairlist tests --- .../plugins/pairlist/VolatilityFilter.py | 1 - .../plugins/pairlist/rangestabilityfilter.py | 1 + tests/plugins/test_pairlist.py | 35 ++++++++++++++----- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index 301a92b7d..ca375fcda 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -3,7 +3,6 @@ Volatility pairlist filter """ import logging import sys -from copy import deepcopy from datetime import timedelta from typing import Any, Dict, List, Optional diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index 0bd35997c..730bb3d78 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -44,6 +44,7 @@ class RangeStabilityFilter(IPairList): if self._sort_direction not in [None, 'asc', 'desc']: raise OperationalException("RangeStabilityFilter requires sort_direction to be " "either None (undefined), 'asc' or 'desc'") + @property def needstickers(self) -> bool: """ diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 5d7497273..57affc731 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -774,12 +774,34 @@ def test_VolatilityFilter_error(mocker, whitelist_conf) -> None: PairListManager(exchange_mock, whitelist_conf, MagicMock()) -@pytest.mark.parametrize('sort_direction', ['asc', 'desc']) -def test_VolatilityFilter_sort( - mocker, whitelist_conf, tickers, time_machine, sort_direction) -> None: +@pytest.mark.parametrize('pairlist,expected_pairlist', [ + ({"method": "VolatilityFilter", "sort_direction": "asc"}, + ['XRP/BTC', 'ETH/BTC', 'LTC/BTC', 'TKN/BTC']), + ({"method": "VolatilityFilter", "sort_direction": "desc"}, + ['TKN/BTC', 'LTC/BTC', 'ETH/BTC', 'XRP/BTC']), + ({"method": "VolatilityFilter", "sort_direction": "desc", 'min_volatility': 0.4}, + ['TKN/BTC', 'LTC/BTC', 'ETH/BTC']), + ({"method": "VolatilityFilter", "sort_direction": "asc", 'min_volatility': 0.4}, + ['ETH/BTC', 'LTC/BTC', 'TKN/BTC']), + ({"method": "VolatilityFilter", "sort_direction": "desc", 'max_volatility': 0.5}, + ['LTC/BTC', 'ETH/BTC', 'XRP/BTC']), + ({"method": "VolatilityFilter", "sort_direction": "asc", 'max_volatility': 0.5}, + ['XRP/BTC', 'ETH/BTC', 'LTC/BTC']), + ({"method": "RangeStabilityFilter", "sort_direction": "asc"}, + ['ETH/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), + ({"method": "RangeStabilityFilter", "sort_direction": "desc"}, + ['TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'ETH/BTC']), + ({"method": "RangeStabilityFilter", "sort_direction": "asc", 'min_rate_of_change': 0.4}, + ['XRP/BTC', 'LTC/BTC', 'TKN/BTC']), + ({"method": "RangeStabilityFilter", "sort_direction": "desc", 'min_rate_of_change': 0.4}, + ['TKN/BTC', 'LTC/BTC', 'XRP/BTC']), +]) +def test_VolatilityFilter_RangeStabilityFilter_sort( + mocker, whitelist_conf, tickers, time_machine, pairlist, expected_pairlist) -> None: whitelist_conf['pairlists'] = [ {'method': 'VolumePairList', 'number_assets': 10}, - {"method": "VolatilityFilter", "sort_direction": sort_direction}] + pairlist + ] df1 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=42) df2 = generate_test_data('1d', 10, '2022-01-05 00:00:00+00:00', random_seed=2) @@ -817,10 +839,7 @@ def test_VolatilityFilter_sort( plm.refresh_pairlist() assert ohlcv_mock.call_count == 1 assert exchange.ohlcv_candle_limit.call_count == 2 - assert plm.whitelist == ( - ['XRP/BTC', 'ETH/BTC', 'LTC/BTC', 'TKN/BTC'] if sort_direction == 'asc' - else ['TKN/BTC', 'LTC/BTC', 'ETH/BTC', 'XRP/BTC'] - ) + assert plm.whitelist == expected_pairlist plm.refresh_pairlist() assert exchange.ohlcv_candle_limit.call_count == 2 From 817ad6440280ed2fd417307766676c1a89ab8577 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 15:00:28 +0100 Subject: [PATCH 235/327] Add docs for rangeStability sorting --- docs/includes/pairlists.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 51c38fcce..960f2d210 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -450,6 +450,8 @@ If the trading range over the last 10 days is <1% or >99%, remove the pair from ] ``` +Adding `"sort_direction": "asc"` or `"sort_direction": "desc"` enables sorting for this pairlist. + !!! Tip This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit. Additionally, it can also be used to automatically remove pairs with extreme high/low variance over a given amount of time. From e80ad309f1b7b36ac23e5a7d09cb04c21f8f6465 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Feb 2024 15:04:54 +0100 Subject: [PATCH 236/327] Improve type safety, refactor volatilityfilter --- freqtrade/plugins/pairlist/VolatilityFilter.py | 10 +++++----- freqtrade/plugins/pairlist/rangestabilityfilter.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/plugins/pairlist/VolatilityFilter.py b/freqtrade/plugins/pairlist/VolatilityFilter.py index ca375fcda..cdd171e91 100644 --- a/freqtrade/plugins/pairlist/VolatilityFilter.py +++ b/freqtrade/plugins/pairlist/VolatilityFilter.py @@ -95,7 +95,7 @@ class VolatilityFilter(IPairList): "sort_direction": { "type": "option", "default": None, - "options": [None, "asc", "desc"], + "options": ["", "asc", "desc"], "description": "Sort pairlist", "help": "Sort Pairlist ascending or descending by volatility.", }, @@ -125,19 +125,19 @@ class VolatilityFilter(IPairList): if volatility_avg is not None: if self._validate_pair_loc(p, volatility_avg): resulting_pairlist.append(p) + volatilitys[p] = ( + volatility_avg if volatility_avg and not np.isnan(volatility_avg) else 0 + ) else: self.log_once(f"Removed {p} from whitelist, no candles found.", logger.info) - if self._sort_direction: - volatilitys[p] = volatility_avg if not np.isnan(volatility_avg) else 0 - if self._sort_direction: resulting_pairlist = sorted(resulting_pairlist, key=lambda p: volatilitys[p], reverse=self._sort_direction == 'desc') return resulting_pairlist - def _calculate_volatility(self, pair: str, daily_candles: DataFrame) -> float: + def _calculate_volatility(self, pair: str, daily_candles: DataFrame) -> Optional[float]: # Check symbol in cache if (volatility_avg := self._pair_cache.get(pair, None)) is not None: return volatility_avg diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index 730bb3d78..0480f60d0 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -93,7 +93,7 @@ class RangeStabilityFilter(IPairList): "sort_direction": { "type": "option", "default": None, - "options": [None, "asc", "desc"], + "options": ["", "asc", "desc"], "description": "Sort pairlist", "help": "Sort Pairlist ascending or descending by rate of change.", }, @@ -134,7 +134,7 @@ class RangeStabilityFilter(IPairList): reverse=self._sort_direction == 'desc') return resulting_pairlist - def _calculate_rate_of_change(self, pair: str, daily_candles: DataFrame) -> float: + def _calculate_rate_of_change(self, pair: str, daily_candles: DataFrame) -> Optional[float]: # Check symbol in cache if (pct_change := self._pair_cache.get(pair, None)) is not None: return pct_change From f164b893519522f762969b647e6e14fc38343cbc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Feb 2024 08:57:05 +0100 Subject: [PATCH 237/327] Ensure pytz is updated regularily by pinning it --- requirements.txt | 1 + setup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 1c5bf8ba5..c227ae732 100644 --- a/requirements.txt +++ b/requirements.txt @@ -50,6 +50,7 @@ questionary==2.0.1 prompt-toolkit==3.0.36 # Extensions to datetime library python-dateutil==2.8.2 +pytz==2024.1 #Futures schedule==1.2.1 diff --git a/setup.py b/setup.py index 38f0f9a78..dea1966fa 100644 --- a/setup.py +++ b/setup.py @@ -110,6 +110,7 @@ setup( 'cryptography', 'sdnotify', 'python-dateutil', + 'pytz', 'packaging', ], extras_require={ From 3e0755b8ad480bdbe1f8d1f12ef551422590e12b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Feb 2024 09:02:17 +0100 Subject: [PATCH 238/327] Enhance funding-fee call test to account for different timezones --- tests/freqtradebot/test_freqtradebot.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/freqtradebot/test_freqtradebot.py b/tests/freqtradebot/test_freqtradebot.py index aa037fe37..1891c2332 100644 --- a/tests/freqtradebot/test_freqtradebot.py +++ b/tests/freqtradebot/test_freqtradebot.py @@ -4680,9 +4680,14 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: ('futures', 17, "2021-08-31 23:59:59", "2021-09-01 08:01:07"), ('futures', 17, "2021-08-31 23:59:58", "2021-09-01 08:01:07"), ]) +@pytest.mark.parametrize('tzoffset', [ + '+00:00', + '+01:00', + '-02:00', +]) def test_update_funding_fees_schedule(mocker, default_conf, trading_mode, calls, time_machine, - t1, t2): - time_machine.move_to(f"{t1} +00:00", tick=False) + t1, t2, tzoffset): + time_machine.move_to(f"{t1} {tzoffset}", tick=False) patch_RPCManager(mocker) patch_exchange(mocker) @@ -4691,7 +4696,7 @@ def test_update_funding_fees_schedule(mocker, default_conf, trading_mode, calls, default_conf['margin_mode'] = 'isolated' freqtrade = get_patched_freqtradebot(mocker, default_conf) - time_machine.move_to(f"{t2} +00:00", tick=False) + time_machine.move_to(f"{t2} {tzoffset}", tick=False) # Check schedule jobs in debugging with freqtrade._schedule.jobs freqtrade._schedule.run_pending() From aad327b1fe4aa6abad07d43a32e1d04b86d0723f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Feb 2024 09:02:58 +0100 Subject: [PATCH 239/327] Update comment around funding fees --- freqtrade/freqtradebot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3f23f43ae..974f8124e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -128,8 +128,9 @@ class FreqtradeBot(LoggingMixin): self.update_funding_fees() self.wallets.update() - # TODO: This would be more efficient if scheduled in utc time, and performed at each - # TODO: funding interval, specified by funding_fee_times on the exchange classes + # This would be more efficient if scheduled in utc time, and performed at each + # funding interval, specified by funding_fee_times on the exchange classes + # However, this reduces the precision - and might therefore lead to problems. for time_slot in range(0, 24): for minutes in [1, 31]: t = str(time(time_slot, minutes, 2)) From e2d3774b07a12e0ad04ae8f3607486e709372681 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Feb 2024 09:07:53 +0100 Subject: [PATCH 240/327] Clearer wallets variable/parameter wording --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/wallets.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 493c7567f..8d16122ea 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -201,7 +201,7 @@ class Backtesting: self.prepare_backtest(False) - self.wallets = Wallets(self.config, self.exchange, log=False) + self.wallets = Wallets(self.config, self.exchange, is_backtest=True) self.progress = BTProgress() self.abort = False diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 0f41114ed..0d22feb36 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -36,9 +36,9 @@ class PositionWallet(NamedTuple): class Wallets: - def __init__(self, config: Config, exchange: Exchange, log: bool = True) -> None: + def __init__(self, config: Config, exchange: Exchange, is_backtest: bool = False) -> None: self._config = config - self._log = log + self._is_backtest = is_backtest self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self._positions: Dict[str, PositionWallet] = {} @@ -78,11 +78,11 @@ class Wallets: _wallets = {} _positions = {} open_trades = Trade.get_trades_proxy(is_open=True) - # If not backtesting... - # TODO: potentially remove the ._log workaround to determine backtest mode. - if self._log: + if not self._is_backtest: + # Live / Dry-run mode tot_profit = Trade.get_total_closed_profit() else: + # Backtest mode tot_profit = LocalTrade.total_profit tot_profit += sum(trade.realized_profit for trade in open_trades) tot_in_trades = sum(trade.stake_amount for trade in open_trades) @@ -177,7 +177,7 @@ class Wallets: self._update_live() else: self._update_dry() - if self._log: + if not self._is_backtest: logger.info('Wallets synced.') self._last_wallet_refresh = dt_now() @@ -341,19 +341,19 @@ class Wallets: max_allowed_stake = min(max_allowed_stake, max_stake_amount - trade_amount) if min_stake_amount is not None and min_stake_amount > max_allowed_stake: - if self._log: + if not self._is_backtest: logger.warning("Minimum stake amount > available balance. " f"{min_stake_amount} > {max_allowed_stake}") return 0 if min_stake_amount is not None and stake_amount < min_stake_amount: - if self._log: + if not self._is_backtest: logger.info( f"Stake amount for pair {pair} is too small " f"({stake_amount} < {min_stake_amount}), adjusting to {min_stake_amount}." ) if stake_amount * 1.3 < min_stake_amount: # Top-cap stake-amount adjustments to +30%. - if self._log: + if not self._is_backtest: logger.info( f"Adjusted stake amount for pair {pair} is more than 30% bigger than " f"the desired stake amount of ({stake_amount:.8f} * 1.3 = " @@ -363,7 +363,7 @@ class Wallets: stake_amount = min_stake_amount if stake_amount > max_allowed_stake: - if self._log: + if not self._is_backtest: logger.info( f"Stake amount for pair {pair} is too big " f"({stake_amount} > {max_allowed_stake}), adjusting to {max_allowed_stake}." From d80ed7e33c7bacda632d50075a8bf6a852199e0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Feb 2024 15:17:42 +0100 Subject: [PATCH 241/327] Bump Docker Python version from 3.11.7 to 3.11.8 --- Dockerfile | 2 +- docker/Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index e5a33df87..a1205f219 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11.7-slim-bookworm as base +FROM python:3.11.8-slim-bookworm as base # Setup env ENV LANG C.UTF-8 diff --git a/docker/Dockerfile.armhf b/docker/Dockerfile.armhf index 4cb8f5fea..1165f305c 100644 --- a/docker/Dockerfile.armhf +++ b/docker/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM python:3.11.7-slim-bookworm as base +FROM python:3.11.8-slim-bookworm as base # Setup env ENV LANG C.UTF-8 From c06e4ee89ce91e8b4aa76e2f09c44549479dfeda Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 03:54:50 +0000 Subject: [PATCH 242/327] Bump the pytest group with 1 update Bumps the pytest group with 1 update: [pytest](https://github.com/pytest-dev/pytest). Updates `pytest` from 8.0.1 to 8.0.2 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.1...8.0.2) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-patch dependency-group: pytest ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e0993988a..43c06212a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ coveralls==3.3.1 ruff==0.2.2 mypy==1.8.0 pre-commit==3.6.2 -pytest==8.0.1 +pytest==8.0.2 pytest-asyncio==0.23.5 pytest-cov==4.1.0 pytest-mock==3.12.0 From 9805cd768b3cb438867581925b56d0bdcdf4dc34 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 03:54:59 +0000 Subject: [PATCH 243/327] Bump orjson from 3.9.14 to 3.9.15 Bumps [orjson](https://github.com/ijl/orjson) from 3.9.14 to 3.9.15. - [Release notes](https://github.com/ijl/orjson/releases) - [Changelog](https://github.com/ijl/orjson/blob/master/CHANGELOG.md) - [Commits](https://github.com/ijl/orjson/compare/3.9.14...3.9.15) --- updated-dependencies: - dependency-name: orjson dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c227ae732..4891e6556 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ py_find_1st==1.1.6 # Load ticker files 30% faster python-rapidjson==1.14 # Properly format api responses -orjson==3.9.14 +orjson==3.9.15 # Notify systemd sdnotify==0.3.2 From c2998f3d7545ae031f590f1d240927c0e218ad8e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 03:55:06 +0000 Subject: [PATCH 244/327] Bump pydantic from 2.6.1 to 2.6.2 Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.6.1 to 2.6.2. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v2.6.1...v2.6.2) --- updated-dependencies: - dependency-name: pydantic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c227ae732..ad7a3640e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,7 +37,7 @@ sdnotify==0.3.2 # API Server fastapi==0.109.2 -pydantic==2.6.1 +pydantic==2.6.2 uvicorn==0.27.1 pyjwt==2.8.0 aiofiles==23.2.1 From 313de7b8a0041923f07a46b9011b16baf27e5a79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 03:55:12 +0000 Subject: [PATCH 245/327] Bump mkdocs-material from 9.5.9 to 9.5.11 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.9 to 9.5.11. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.9...9.5.11) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index aca3da72a..cbb81b6b2 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.5.2 mkdocs==1.5.3 -mkdocs-material==9.5.9 +mkdocs-material==9.5.11 mdx_truly_sane_lists==1.3 pymdown-extensions==10.7 jinja2==3.1.3 From e2a486f85eba0f7206a22d13235b738bf8d40553 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 03:55:27 +0000 Subject: [PATCH 246/327] Bump nbconvert from 7.16.0 to 7.16.1 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.16.0 to 7.16.1. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md) - [Commits](https://github.com/jupyter/nbconvert/compare/v7.16.0...v7.16.1) --- updated-dependencies: - dependency-name: nbconvert dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e0993988a..71c590c02 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,7 +21,7 @@ isort==5.13.2 time-machine==2.13.0 # Convert jupyter notebooks to markdown documents -nbconvert==7.16.0 +nbconvert==7.16.1 # mypy types types-cachetools==5.3.0.7 From 4ff888488b42c442efb13293537b634288363454 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 03:55:48 +0000 Subject: [PATCH 247/327] Bump cryptography from 42.0.4 to 42.0.5 Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.4 to 42.0.5. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/42.0.4...42.0.5) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c227ae732..5ac1e2d1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==2.1.4 pandas-ta==0.3.14b ccxt==4.2.47 -cryptography==42.0.4 +cryptography==42.0.5 aiohttp==3.9.3 SQLAlchemy==2.0.27 python-telegram-bot==20.8 From 080d9b55f8abcc400265ccef71dbf5ca6f71c72e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 05:29:59 +0000 Subject: [PATCH 248/327] Bump fastapi from 0.109.2 to 0.110.0 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.109.2 to 0.110.0. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.109.2...0.110.0) --- updated-dependencies: - dependency-name: fastapi dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a08fe7b2a..c7f1db813 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,7 +36,7 @@ orjson==3.9.15 sdnotify==0.3.2 # API Server -fastapi==0.109.2 +fastapi==0.110.0 pydantic==2.6.2 uvicorn==0.27.1 pyjwt==2.8.0 From d9f4c62f15cdce834f218f94eb43191686e65417 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Feb 2024 07:07:48 +0100 Subject: [PATCH 249/327] Add warning about excessive use of position_adjustment --- docs/strategy-callbacks.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 2292b7ed0..0324e0915 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -797,6 +797,11 @@ Returning a value more than the above (so remaining stake_amount would become ne While `/stopentry` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades. +!!! Danger "Performance with many position adjustments" + Position adjustments can be a good approach to increase a strategy's output - but it can also have drawbacks if using this feature extensively. + Each of the orders will be attached to the trade object for the duration of the trade - hence increasing memory usage. + Trades with long duration and 10s or even 100ds of position adjustments are therefore not recommended, and should be closed at regular intervals to not affect performance. + !!! Warning "Backtesting" During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected. This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle. From d5c01f7480769dfb66ad06ec69babc291002cfa8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 07:47:19 +0000 Subject: [PATCH 250/327] Bump ccxt from 4.2.47 to 4.2.51 Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.2.47 to 4.2.51. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/4.2.47...4.2.51) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f147245ac..94f63d033 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.26.4 pandas==2.1.4 pandas-ta==0.3.14b -ccxt==4.2.47 +ccxt==4.2.51 cryptography==42.0.5 aiohttp==3.9.3 SQLAlchemy==2.0.27 From 5402d276d61efb102d215d6fc1a84b61cd1eca59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Feb 2024 18:03:34 +0100 Subject: [PATCH 251/327] Add header to warning box, reorder Boxes --- docs/strategy-callbacks.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 0324e0915..2f04e906e 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -791,21 +791,21 @@ Returning a value more than the above (so remaining stake_amount would become ne If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that. Using 'unlimited' stake amount with DCA orders requires you to also implement the `custom_stake_amount()` callback to avoid allocating all funds to the initial order. -!!! Warning +!!! Warning "Stoploss calculation" Stoploss is still calculated from the initial opening price, not averaged price. Regular stoploss rules still apply (cannot move down). While `/stopentry` command stops the bot from entering new trades, the position adjustment feature will continue buying new orders on existing trades. -!!! Danger "Performance with many position adjustments" - Position adjustments can be a good approach to increase a strategy's output - but it can also have drawbacks if using this feature extensively. - Each of the orders will be attached to the trade object for the duration of the trade - hence increasing memory usage. - Trades with long duration and 10s or even 100ds of position adjustments are therefore not recommended, and should be closed at regular intervals to not affect performance. - !!! Warning "Backtesting" During backtesting this callback is called for each candle in `timeframe` or `timeframe_detail`, so run-time performance will be affected. This can also cause deviating results between live and backtesting, since backtesting can adjust the trade only once per candle, whereas live could adjust the trade multiple times per candle. +!!! Warning "Performance with many position adjustments" + Position adjustments can be a good approach to increase a strategy's output - but it can also have drawbacks if using this feature extensively. + Each of the orders will be attached to the trade object for the duration of the trade - hence increasing memory usage. + Trades with long duration and 10s or even 100ds of position adjustments are therefore not recommended, and should be closed at regular intervals to not affect performance. + ``` python from freqtrade.persistence import Trade From 01266ed7eb90313c6f7db6f5c77405c300e2597c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Feb 2024 19:02:11 +0100 Subject: [PATCH 252/327] Align test results ... --- tests/optimize/test_backtesting_adjust_position.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index 7f7bbb29f..ce2b73d02 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -76,7 +76,7 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> 'leverage': [1.0, 1.0], 'is_short': [False, False], 'open_timestamp': [1517251200000, 1517283000000], - 'close_timestamp': [1517265300000, 1517285400000], + 'close_timestamp': [1517265200000, 1517285400000], }) pd.testing.assert_frame_equal(results.drop(columns=['orders']), expected) data_pair = processed[pair] From d6b01a6ffe476797084ac82a1366a5147b2f9975 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Feb 2024 19:17:50 +0100 Subject: [PATCH 253/327] Assert for exact equality --- .../optimize/test_backtesting_adjust_position.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/optimize/test_backtesting_adjust_position.py b/tests/optimize/test_backtesting_adjust_position.py index ce2b73d02..2a158acf3 100644 --- a/tests/optimize/test_backtesting_adjust_position.py +++ b/tests/optimize/test_backtesting_adjust_position.py @@ -57,28 +57,30 @@ def test_backtest_position_adjustment(default_conf, fee, mocker, testdatadir) -> ), 'close_date': pd.to_datetime([dt_utc(2018, 1, 29, 22, 00, 0), dt_utc(2018, 1, 30, 4, 10, 0)], utc=True), - 'open_rate': [0.10401764894444211, 0.10302485], - 'close_rate': [0.10453904066847439, 0.103541], + 'open_rate': [0.10401764891917063, 0.10302485], + 'close_rate': [0.10453904064307624, 0.10354126528822055], 'fee_open': [0.0025, 0.0025], 'fee_close': [0.0025, 0.0025], 'trade_duration': [200, 40], 'profit_ratio': [0.0, 0.0], 'profit_abs': [0.0, 0.0], 'exit_reason': [ExitType.ROI.value, ExitType.ROI.value], - 'initial_stop_loss_abs': [0.0940005, 0.09272236], + 'initial_stop_loss_abs': [0.0940005, 0.092722365], 'initial_stop_loss_ratio': [-0.1, -0.1], - 'stop_loss_abs': [0.0940005, 0.09272236], + 'stop_loss_abs': [0.0940005, 0.092722365], 'stop_loss_ratio': [-0.1, -0.1], 'min_rate': [0.10370188, 0.10300000000000001], - 'max_rate': [0.10481985, 0.1038888], + 'max_rate': [0.10481985, 0.10388887000000001], 'is_open': [False, False], 'enter_tag': ['', ''], 'leverage': [1.0, 1.0], 'is_short': [False, False], 'open_timestamp': [1517251200000, 1517283000000], - 'close_timestamp': [1517265200000, 1517285400000], + 'close_timestamp': [1517263200000, 1517285400000], }) - pd.testing.assert_frame_equal(results.drop(columns=['orders']), expected) + results_no = results.drop(columns=['orders']) + pd.testing.assert_frame_equal(results_no, expected, check_exact=True) + data_pair = processed[pair] assert len(results.iloc[0]['orders']) == 6 assert len(results.iloc[1]['orders']) == 2 From bd7edfba977d5749d3ac3edab3c0a787b072b41d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 12:27:08 +0000 Subject: [PATCH 254/327] Bump pandas from 2.1.4 to 2.2.1 Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.1.4 to 2.2.1. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Commits](https://github.com/pandas-dev/pandas/compare/v2.1.4...v2.2.1) --- updated-dependencies: - dependency-name: pandas dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 94f63d033..0477751cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.26.4 -pandas==2.1.4 +pandas==2.2.1 pandas-ta==0.3.14b ccxt==4.2.51 From b1015172c7b5f690b821bdf7010f45ecaa827172 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Feb 2024 05:55:26 +0100 Subject: [PATCH 255/327] Update test for fixed pandas behavior --- tests/strategy/test_interface.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 790f5d255..645cae887 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1022,22 +1022,22 @@ def test_auto_hyperopt_interface_loadparams(default_conf, mocker, caplog): @pytest.mark.parametrize('function,raises', [ - ('populate_entry_trend', True), + ('populate_entry_trend', False), ('advise_entry', False), - ('populate_exit_trend', True), + ('populate_exit_trend', False), ('advise_exit', False), ]) -def test_pandas_warning_direct(ohlcv_history, function, raises): +def test_pandas_warning_direct(ohlcv_history, function, raises, recwarn): df = _STRATEGY.populate_indicators(ohlcv_history, {'pair': 'ETH/BTC'}) if raises: - with pytest.warns(FutureWarning): - # Test for Future warning - # FutureWarning: Setting an item of incompatible dtype is - # deprecated and will raise in a future error of pandas - # https://github.com/pandas-dev/pandas/issues/56503 - getattr(_STRATEGY, function)(df, {'pair': 'ETH/BTC'}) + assert len(recwarn) == 1 + # https://github.com/pandas-dev/pandas/issues/56503 + # Fixed in 2.2.x + getattr(_STRATEGY, function)(df, {'pair': 'ETH/BTC'}) else: + assert len(recwarn) == 0 + getattr(_STRATEGY, function)(df, {'pair': 'ETH/BTC'}) From 5912d87b658995665c5c2eb12e61d329e0adf44d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Feb 2024 06:05:01 +0100 Subject: [PATCH 256/327] Pandas - update view to astype --- freqtrade/data/history/jsondatahandler.py | 2 +- freqtrade/rpc/rpc.py | 2 +- tests/conftest.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 9a02a7769..baa0c10a5 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -37,7 +37,7 @@ class JsonDataHandler(IDataHandler): self.create_dir_if_needed(filename) _data = data.copy() # Convert date to int - _data['date'] = _data['date'].view(np.int64) // 1000 // 1000 + _data['date'] = _data['date'].astype(np.int64) // 1000 // 1000 # Reset index, select only appropriate columns and save as json _data.reset_index(drop=True).loc[:, self._columns].to_json( diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2317ee1a9..6e8447d29 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1155,7 +1155,7 @@ class RPC: } if has_content: - dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000 + dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000 # Move signal close to separate column when signal for easy plotting for sig_type in signals.keys(): if sig_type in dataframe.columns: diff --git a/tests/conftest.py b/tests/conftest.py index 9c81c050d..a322bfd77 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -177,7 +177,7 @@ def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): def generate_test_data_raw(timeframe: str, size: int, start: str = '2020-07-05'): """ Generates data in the ohlcv format used by ccxt """ df = generate_test_data(timeframe, size, start) - df['date'] = df.loc[:, 'date'].view(np.int64) // 1000 // 1000 + df['date'] = df.loc[:, 'date'].astype(np.int64) // 1000 // 1000 return list(list(x) for x in zip(*(df[x].values.tolist() for x in df.columns))) From ce2f4f89c4bbc7b6b7ae028af477dfa28862d424 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Feb 2024 06:07:21 +0100 Subject: [PATCH 257/327] update pandas deprecation to_hdf --- freqtrade/data/history/hdf5datahandler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index d22fd9e31..b118bd7e0 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -35,7 +35,7 @@ class HDF5DataHandler(IDataHandler): self.create_dir_if_needed(filename) _data.loc[:, self._columns].to_hdf( - filename, key, mode='a', complevel=9, complib='blosc', + filename, key=key, mode='a', complevel=9, complib='blosc', format='table', data_columns=['date'] ) @@ -110,7 +110,7 @@ class HDF5DataHandler(IDataHandler): key = self._pair_trades_key(pair) data.to_hdf( - self._pair_trades_filename(self._datadir, pair), key, + self._pair_trades_filename(self._datadir, pair), key=key, mode='a', complevel=9, complib='blosc', format='table', data_columns=['timestamp'] ) From 0021e2c2052a6643d53d59bfaa1bc9df88a9ee12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Feb 2024 06:08:25 +0100 Subject: [PATCH 258/327] fillna needs explicit type. --- freqtrade/optimize/analysis/lookahead_helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/analysis/lookahead_helpers.py b/freqtrade/optimize/analysis/lookahead_helpers.py index 1d2b9db48..00f83a46b 100644 --- a/freqtrade/optimize/analysis/lookahead_helpers.py +++ b/freqtrade/optimize/analysis/lookahead_helpers.py @@ -107,9 +107,9 @@ class LookaheadAnalysisSubFunctions: csv_df = add_or_update_row(csv_df, new_row_data) # Fill NaN values with a default value (e.g., 0) - csv_df['total_signals'] = csv_df['total_signals'].fillna(0) - csv_df['biased_entry_signals'] = csv_df['biased_entry_signals'].fillna(0) - csv_df['biased_exit_signals'] = csv_df['biased_exit_signals'].fillna(0) + csv_df['total_signals'] = csv_df['total_signals'].astype(int).fillna(0) + csv_df['biased_entry_signals'] = csv_df['biased_entry_signals'].astype(int).fillna(0) + csv_df['biased_exit_signals'] = csv_df['biased_exit_signals'].astype(int).fillna(0) # Convert columns to integers csv_df['total_signals'] = csv_df['total_signals'].astype(int) From c0e9726f493adde27b42c60d7965db78019c4161 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Feb 2024 06:08:48 +0100 Subject: [PATCH 259/327] don't use "1M" - but be explicit in the intend --- freqtrade/optimize/optimize_reports/optimize_reports.py | 2 +- tests/optimize/test_optimize_reports.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports/optimize_reports.py b/freqtrade/optimize/optimize_reports/optimize_reports.py index 47a13dcd8..47aab2a62 100644 --- a/freqtrade/optimize/optimize_reports/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports/optimize_reports.py @@ -215,7 +215,7 @@ def _get_resample_from_period(period: str) -> str: # Weekly defaulting to Monday. return '1W-MON' if period == 'month': - return '1M' + return '1ME' raise ValueError(f"Period {period} is not supported.") diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 0f190f3f5..e3603849d 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -498,7 +498,7 @@ def test__get_resample_from_period(): assert _get_resample_from_period('day') == '1d' assert _get_resample_from_period('week') == '1W-MON' - assert _get_resample_from_period('month') == '1M' + assert _get_resample_from_period('month') == '1ME' with pytest.raises(ValueError, match=r"Period noooo is not supported."): _get_resample_from_period('noooo') From 883f27d99e4150ada229b3e8b41a20087442ba75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Feb 2024 07:01:16 +0100 Subject: [PATCH 260/327] Version bump to 2024.3-dev --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 7c699d643..fa5d9214e 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2024.2-dev' +__version__ = '2024.3-dev' if 'dev' in __version__: from pathlib import Path From e988995d71a512e2485f3663c9a876c9c21855d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Feb 2024 07:22:40 +0100 Subject: [PATCH 261/327] Handle NaN funding fees closes #9831 --- freqtrade/exchange/exchange.py | 5 +++-- tests/exchange/test_exchange.py | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7f7fccca8..d1ac47df4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -8,7 +8,7 @@ import logging import signal from copy import deepcopy from datetime import datetime, timedelta, timezone -from math import floor +from math import floor, isnan from threading import Lock from typing import Any, Coroutine, Dict, List, Literal, Optional, Tuple, Union @@ -2916,7 +2916,8 @@ class Exchange: if not df.empty: df1 = df[(df['date'] >= open_date) & (df['date'] <= close_date)] fees = sum(df1['open_fund'] * df1['open_mark'] * amount) - + if isnan(fees): + fees = 0.0 # Negate fees for longs as funding_fees expects it this way based on live endpoints. return fees if is_short else -fees diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5c4879a32..168cf512d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -7,6 +7,7 @@ from unittest.mock import MagicMock, Mock, PropertyMock, patch import ccxt import pytest +from numpy import NaN from pandas import DataFrame from freqtrade.enums import CandleType, MarginMode, RunMode, TradingMode @@ -4203,6 +4204,7 @@ def test_get_max_leverage_from_margin(default_conf, mocker, pair, nominal_value, (10, 0.0001, 2.0, 1.0, 0.002, 0.002), (10, 0.0002, 2.0, 0.01, 0.004, 0.00004), (10, 0.0002, 2.5, None, 0.005, None), + (10, 0.0002, NaN, None, 0.0, None), ]) def test_calculate_funding_fees( default_conf, From cdfeae9f904a1f615723ad3ba2136acc30061b52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Feb 2024 07:31:22 +0100 Subject: [PATCH 262/327] Update merge to "left" to avoid creating non-existing mark candles --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d1ac47df4..d17b442ab 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -2887,7 +2887,7 @@ class Exchange: else: # Fill up missing funding_rate candles with fallback value combined = mark_rates.merge( - funding_rates, on='date', how="outer", suffixes=["_mark", "_fund"] + funding_rates, on='date', how="left", suffixes=["_mark", "_fund"] ) combined['open_fund'] = combined['open_fund'].fillna(futures_funding_rate) return combined diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 168cf512d..34d4ca4c6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4314,8 +4314,8 @@ def test_combine_funding_and_mark( assert len(df) == 1 # Empty funding rates - funding_rates = DataFrame([], columns=['date', 'open']) - df = exchange.combine_funding_and_mark(funding_rates, mark_rates, futures_funding_rate) + funding_rates2 = DataFrame([], columns=['date', 'open']) + df = exchange.combine_funding_and_mark(funding_rates2, mark_rates, futures_funding_rate) if futures_funding_rate is not None: assert len(df) == 3 assert df.iloc[0]['open_fund'] == futures_funding_rate @@ -4324,6 +4324,12 @@ def test_combine_funding_and_mark( else: assert len(df) == 0 + # Empty mark candles + mark_candles = DataFrame([], columns=['date', 'open']) + df = exchange.combine_funding_and_mark(funding_rates, mark_candles, futures_funding_rate) + + assert len(df) == 0 + @pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [ ('binance', 0, 2, "2021-09-01 01:00:00", "2021-09-01 04:00:00", 30.0, 0.0), From 46e616f9975b99de2fe7fb203c58ff0111212e1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Mar 2024 19:32:18 +0100 Subject: [PATCH 263/327] Remove defaults for converter - they're always provided and necessary. --- freqtrade/commands/data_commands.py | 3 ++- freqtrade/data/converter/trade_converter.py | 8 ++++---- freqtrade/data/history/history_utils.py | 1 + 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 33069885a..d3600e3ef 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -8,7 +8,7 @@ from freqtrade.constants import DATETIME_PRINT_FORMAT, DL_DATA_TIMEFRAMES, Confi from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format, convert_trades_to_ohlcv) from freqtrade.data.history import download_data_main -from freqtrade.enums import RunMode, TradingMode +from freqtrade.enums import CandleType, RunMode, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.resolvers import ExchangeResolver @@ -69,6 +69,7 @@ def start_convert_trades(args: Dict[str, Any]) -> None: datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), data_format_ohlcv=config['dataformat_ohlcv'], data_format_trades=config['dataformat_trades'], + candle_type=config.get('candle_type_def', CandleType.SPOT) ) diff --git a/freqtrade/data/converter/trade_converter.py b/freqtrade/data/converter/trade_converter.py index bd4efb77e..117f65bc6 100644 --- a/freqtrade/data/converter/trade_converter.py +++ b/freqtrade/data/converter/trade_converter.py @@ -88,10 +88,10 @@ def convert_trades_to_ohlcv( timeframes: List[str], datadir: Path, timerange: TimeRange, - erase: bool = False, - data_format_ohlcv: str = 'feather', - data_format_trades: str = 'feather', - candle_type: CandleType = CandleType.SPOT + erase: bool, + data_format_ohlcv: str, + data_format_trades: str, + candle_type: CandleType, ) -> None: """ Convert stored trades data to ohlcv data diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index ff6c2561d..27e229973 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -529,6 +529,7 @@ def download_data_main(config: Config) -> None: datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), data_format_ohlcv=config['dataformat_ohlcv'], data_format_trades=config['dataformat_trades'], + candle_type=config.get('candle_type_def', CandleType.SPOT), ) else: if not exchange.get_option('ohlcv_has_history', True): From 5dee60921f41cc4efd562c6047ea8cc97a0ff8e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Mar 2024 19:42:33 +0100 Subject: [PATCH 264/327] Fix test for convert_trades_to_ohlcv --- tests/data/test_converter.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 08fc785aa..2202ada44 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -542,7 +542,9 @@ def test_convert_trades_to_ohlcv(testdatadir, tmp_path, caplog): convert_trades_to_ohlcv([pair], timeframes=['1m', '5m'], data_format_trades='jsongz', - datadir=tmp_path, timerange=tr, erase=True) + datadir=tmp_path, timerange=tr, erase=True, + data_format_ohlcv='feather', + candle_type=CandleType.SPOT) assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog) # Load new data @@ -556,5 +558,7 @@ def test_convert_trades_to_ohlcv(testdatadir, tmp_path, caplog): convert_trades_to_ohlcv(['NoDatapair'], timeframes=['1m', '5m'], data_format_trades='jsongz', - datadir=tmp_path, timerange=tr, erase=True) + datadir=tmp_path, timerange=tr, erase=True, + data_format_ohlcv='feather', + candle_type=CandleType.SPOT) assert log_has(msg, caplog) From bdd63aa1d661be6e54500329a0f50fc614d2f3e4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 1 Mar 2024 20:17:43 +0100 Subject: [PATCH 265/327] FIx futures trades pair download directory --- freqtrade/data/history/idatahandler.py | 5 +++++ tests/commands/test_commands.py | 5 ----- tests/data/test_download_data.py | 4 ---- tests/data/test_history.py | 1 + 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 47c2dd838..01c244f38 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -266,6 +266,11 @@ class IDataHandler(ABC): @classmethod def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path: pair_s = misc.pair_to_filename(pair) + if ':' in pair: + # Futures pair ... + # TODO: this should not rely on ";" in the pairname. + datadir = datadir.joinpath('futures') + filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}') return filename diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index cdad46407..1ab9d2202 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -820,11 +820,6 @@ def test_download_data_trades(mocker): "--trading-mode", "futures", "--dl-trades" ] - with pytest.raises(OperationalException, - match="Trade download not supported for futures."): - pargs = get_args(args) - pargs['config'] = None - start_download_data(pargs) def test_download_data_data_invalid(mocker): diff --git a/tests/data/test_download_data.py b/tests/data/test_download_data.py index 97640d01c..1518b28f3 100644 --- a/tests/data/test_download_data.py +++ b/tests/data/test_download_data.py @@ -78,10 +78,6 @@ def test_download_data_main_trades(mocker): "trading_mode": "futures", }) - with pytest.raises(OperationalException, - match="Trade download not supported for futures."): - download_data_main(config) - def test_download_data_main_data_invalid(mocker): patch_exchange(mocker, id="kraken") diff --git a/tests/data/test_history.py b/tests/data/test_history.py index a48d34aee..106babd63 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -170,6 +170,7 @@ def test_json_pair_data_filename(pair, timeframe, expected_result, candle_type): @pytest.mark.parametrize("pair,expected_result", [ ("ETH/BTC", 'freqtrade/hello/world/ETH_BTC-trades.json'), + ("ETH/USDT:USDT", 'freqtrade/hello/world/futures/ETH_USDT_USDT-trades.json'), ("Fabric Token/ETH", 'freqtrade/hello/world/Fabric_Token_ETH-trades.json'), ("ETHH20", 'freqtrade/hello/world/ETHH20-trades.json'), (".XBTBON2H", 'freqtrade/hello/world/_XBTBON2H-trades.json'), From 75c84bfe654497f9d64f193cfc26591c99118e76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Mar 2024 08:23:38 +0100 Subject: [PATCH 266/327] Only show a subset of list-exchanges output in the docs (it's potentially missleading, and changes all the time - so was probably outdated). --- docs/utils.md | 218 +++++++------------------------------------------- 1 file changed, 30 insertions(+), 188 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index 202526afe..ea52737d6 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -219,207 +219,49 @@ optional arguments: -a, --all Print all exchanges known to the ccxt library. ``` -* Example: see exchanges available for the bot: +Example: see exchanges available for the bot: + ``` $ freqtrade list-exchanges Exchanges available for Freqtrade: -Exchange name Valid reason ---------------- ------- -------------------------------------------- -aax True -ascendex True missing opt: fetchMyTrades -bequant True -bibox True -bigone True -binance True -binanceus True -bitbank True missing opt: fetchTickers -bitcoincom True -bitfinex True -bitforex True missing opt: fetchMyTrades, fetchTickers -bitget True -bithumb True missing opt: fetchMyTrades -bitkk True missing opt: fetchMyTrades -bitmart True -bitmax True missing opt: fetchMyTrades -bitpanda True -bitvavo True -bitz True missing opt: fetchMyTrades -btcalpha True missing opt: fetchTicker, fetchTickers -btcmarkets True missing opt: fetchTickers -buda True missing opt: fetchMyTrades, fetchTickers -bw True missing opt: fetchMyTrades, fetchL2OrderBook -bybit True -bytetrade True -cdax True -cex True missing opt: fetchMyTrades -coinbaseprime True missing opt: fetchTickers -coinbasepro True missing opt: fetchTickers -coinex True -crex24 True -deribit True -digifinex True -equos True missing opt: fetchTicker, fetchTickers -eterbase True -fcoin True missing opt: fetchMyTrades, fetchTickers -fcoinjp True missing opt: fetchMyTrades, fetchTickers -gateio True -gemini True -gopax True -hbtc True -hitbtc True -huobijp True -huobipro True -idex True -kraken True -kucoin True -lbank True missing opt: fetchMyTrades -mercado True missing opt: fetchTickers -ndax True missing opt: fetchTickers -novadax True -okcoin True -okex True -probit True -qtrade True -stex True -timex True -upbit True missing opt: fetchMyTrades -vcc True -zb True missing opt: fetchMyTrades - +Exchange name Supported Markets Reason +------------------ ----------- ---------------------- ------------------------------------------------------------------------ +binance Official spot, isolated futures +bitmart Official spot +bybit spot, isolated futures +gate Official spot, isolated futures +htx Official spot +huobi spot +kraken Official spot +okx Official spot, isolated futures ``` +!!! info "" + Output reduced for clarity - supported and available exchanges may change over time. + !!! Note "missing opt exchanges" Values with "missing opt:" might need special configuration (e.g. using orderbook if `fetchTickers` is missing) - but should in theory work (although we cannot guarantee they will). -* Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade): +Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade) + ``` $ freqtrade list-exchanges -a All exchanges supported by the ccxt library: -Exchange name Valid reason ------------------- ------- --------------------------------------------------------------------------------------- -aax True -aofex False missing: fetchOrder -ascendex True missing opt: fetchMyTrades -bequant True -bibox True -bigone True -binance True -binanceus True -bit2c False missing: fetchOrder, fetchOHLCV -bitbank True missing opt: fetchTickers -bitbay False missing: fetchOrder -bitcoincom True -bitfinex True -bitfinex2 False missing: fetchOrder -bitflyer False missing: fetchOrder, fetchOHLCV -bitforex True missing opt: fetchMyTrades, fetchTickers -bitget True -bithumb True missing opt: fetchMyTrades -bitkk True missing opt: fetchMyTrades -bitmart True -bitmax True missing opt: fetchMyTrades -bitmex False Various reasons. -bitpanda True -bitso False missing: fetchOHLCV -bitstamp True missing opt: fetchTickers -bitstamp1 False missing: fetchOrder, fetchOHLCV -bitvavo True -bitz True missing opt: fetchMyTrades -bl3p False missing: fetchOrder, fetchOHLCV -bleutrade False missing: fetchOrder -braziliex False missing: fetchOHLCV -btcalpha True missing opt: fetchTicker, fetchTickers -btcbox False missing: fetchOHLCV -btcmarkets True missing opt: fetchTickers -btctradeua False missing: fetchOrder, fetchOHLCV -btcturk False missing: fetchOrder -buda True missing opt: fetchMyTrades, fetchTickers -bw True missing opt: fetchMyTrades, fetchL2OrderBook -bybit True -bytetrade True -cdax True -cex True missing opt: fetchMyTrades -chilebit False missing: fetchOrder, fetchOHLCV -coinbase False missing: fetchOrder, cancelOrder, createOrder, fetchOHLCV -coinbaseprime True missing opt: fetchTickers -coinbasepro True missing opt: fetchTickers -coincheck False missing: fetchOrder, fetchOHLCV -coinegg False missing: fetchOHLCV -coinex True -coinfalcon False missing: fetchOHLCV -coinfloor False missing: fetchOrder, fetchOHLCV -coingi False missing: fetchOrder, fetchOHLCV -coinmarketcap False missing: fetchOrder, cancelOrder, createOrder, fetchBalance, fetchOHLCV -coinmate False missing: fetchOHLCV -coinone False missing: fetchOHLCV -coinspot False missing: fetchOrder, cancelOrder, fetchOHLCV -crex24 True -currencycom False missing: fetchOrder -delta False missing: fetchOrder -deribit True -digifinex True -equos True missing opt: fetchTicker, fetchTickers -eterbase True -exmo False missing: fetchOrder -exx False missing: fetchOHLCV -fcoin True missing opt: fetchMyTrades, fetchTickers -fcoinjp True missing opt: fetchMyTrades, fetchTickers -flowbtc False missing: fetchOrder, fetchOHLCV -foxbit False missing: fetchOrder, fetchOHLCV -gateio True -gemini True -gopax True -hbtc True -hitbtc True -hollaex False missing: fetchOrder -huobijp True -huobipro True -idex True -independentreserve False missing: fetchOHLCV -indodax False missing: fetchOHLCV -itbit False missing: fetchOHLCV -kraken True -kucoin True -kuna False missing: fetchOHLCV -lakebtc False missing: fetchOrder, fetchOHLCV -latoken False missing: fetchOrder, fetchOHLCV -lbank True missing opt: fetchMyTrades -liquid False missing: fetchOHLCV -luno False missing: fetchOHLCV -lykke False missing: fetchOHLCV -mercado True missing opt: fetchTickers -mixcoins False missing: fetchOrder, fetchOHLCV -ndax True missing opt: fetchTickers -novadax True -oceanex False missing: fetchOHLCV -okcoin True -okex True -paymium False missing: fetchOrder, fetchOHLCV -phemex False Does not provide history. -poloniex False missing: fetchOrder -probit True -qtrade True -rightbtc False missing: fetchOrder -ripio False missing: fetchOHLCV -southxchange False missing: fetchOrder, fetchOHLCV -stex True -surbitcoin False missing: fetchOrder, fetchOHLCV -therock False missing: fetchOHLCV -tidebit False missing: fetchOrder -tidex False missing: fetchOHLCV -timex True -upbit True missing opt: fetchMyTrades -vbtc False missing: fetchOrder, fetchOHLCV -vcc True -wavesexchange False missing: fetchOrder -whitebit False missing: fetchOrder, cancelOrder, createOrder, fetchBalance -xbtce False missing: fetchOrder, fetchOHLCV -xena False missing: fetchOrder -yobit False missing: fetchOHLCV -zaif False missing: fetchOrder, fetchOHLCV -zb True missing opt: fetchMyTrades +Exchange name Valid Supported Markets Reason +------------------ ------- ----------- ---------------------- --------------------------------------------------------------------------------- +binance True Official spot, isolated futures +bitflyer False spot missing: fetchOrder. missing opt: fetchTickers. +bitmart True Official spot +bybit True spot, isolated futures +gate True Official spot, isolated futures +htx True Official spot +kraken True Official spot +okx True Official spot, isolated futures ``` +!!! info "" + Reduced output - supported and available exchanges may change over time. + ## List Timeframes Use the `list-timeframes` subcommand to see the list of timeframes available for the exchange. From f8cc2a6e74ee5de6db16b3d234aea956df6daded Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Mar 2024 12:01:01 +0100 Subject: [PATCH 267/327] Fix typo in doc header --- docs/updating.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/updating.md b/docs/updating.md index 1e5dc8ffe..5841d205a 100644 --- a/docs/updating.md +++ b/docs/updating.md @@ -6,7 +6,7 @@ To update your freqtrade installation, please use one of the below methods, corr Breaking changes / changed behavior will be documented in the changelog that is posted alongside every release. For the develop branch, please follow PR's to avoid being surprised by changes. -## docker +## Docker !!! Note "Legacy installations using the `master` image" We're switching from master to stable for the release Images - please adjust your docker-file and replace `freqtradeorg/freqtrade:master` with `freqtradeorg/freqtrade:stable` From b6040e270fa253fc089230decf37d7cce98d61a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Mar 2024 13:08:58 +0100 Subject: [PATCH 268/327] Update data handlers to accept trading_mode for trade data related functions --- freqtrade/data/history/featherdatahandler.py | 14 ++++++---- freqtrade/data/history/hdf5datahandler.py | 14 ++++++---- freqtrade/data/history/idatahandler.py | 29 ++++++++++++++------ freqtrade/data/history/jsondatahandler.py | 14 ++++++---- freqtrade/data/history/parquetdatahandler.py | 16 +++++++---- 5 files changed, 57 insertions(+), 30 deletions(-) diff --git a/freqtrade/data/history/featherdatahandler.py b/freqtrade/data/history/featherdatahandler.py index 44d337836..6d57dbed7 100644 --- a/freqtrade/data/history/featherdatahandler.py +++ b/freqtrade/data/history/featherdatahandler.py @@ -5,7 +5,7 @@ from pandas import DataFrame, read_feather, to_datetime from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS -from freqtrade.enums import CandleType +from freqtrade.enums import CandleType, TradingMode from .idatahandler import IDataHandler @@ -82,14 +82,15 @@ class FeatherDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_store(self, pair: str, data: DataFrame) -> None: + def _trades_store(self, pair: str, data: DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) self.create_dir_if_needed(filename) data.reset_index(drop=True).to_feather(filename, compression_level=9, compression='lz4') @@ -102,15 +103,18 @@ class FeatherDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame: + def _trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> DataFrame: """ Load a pair from file, either .json.gz or .json # TODO: respect timerange ... :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: Dataframe containing trades """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) if not filename.exists(): return DataFrame(columns=DEFAULT_TRADES_COLUMNS) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index b118bd7e0..cb2cdd884 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -6,7 +6,7 @@ import pandas as pd from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS -from freqtrade.enums import CandleType +from freqtrade.enums import CandleType, TradingMode from .idatahandler import IDataHandler @@ -100,17 +100,18 @@ class HDF5DataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_store(self, pair: str, data: pd.DataFrame) -> None: + def _trades_store(self, pair: str, data: pd.DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ key = self._pair_trades_key(pair) data.to_hdf( - self._pair_trades_filename(self._datadir, pair), key=key, + self._pair_trades_filename(self._datadir, pair, trading_mode), key=key, mode='a', complevel=9, complib='blosc', format='table', data_columns=['timestamp'] ) @@ -124,15 +125,18 @@ class HDF5DataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> pd.DataFrame: + def _trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> pd.DataFrame: """ Load a pair from h5 file. :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: Dataframe containing trades """ key = self._pair_trades_key(pair) - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) if not filename.exists(): return pd.DataFrame(columns=DEFAULT_TRADES_COLUMNS) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 01c244f38..bcb31a7c8 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -172,12 +172,13 @@ class IDataHandler(ABC): return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match] @abstractmethod - def _trades_store(self, pair: str, data: DataFrame) -> None: + def _trades_store(self, pair: str, data: DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ @abstractmethod @@ -190,45 +191,55 @@ class IDataHandler(ABC): """ @abstractmethod - def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame: + def _trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> DataFrame: """ Load a pair from file, either .json.gz or .json :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: Dataframe containing trades """ - def trades_store(self, pair: str, data: DataFrame) -> None: + def trades_store(self, pair: str, data: DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ # Filter on expected columns (will remove the actual date column). - self._trades_store(pair, data[DEFAULT_TRADES_COLUMNS]) + self._trades_store(pair, data[DEFAULT_TRADES_COLUMNS], trading_mode) - def trades_purge(self, pair: str) -> bool: + def trades_purge(self, pair: str, trading_mode: TradingMode) -> bool: """ Remove data for this pair :param pair: Delete data for this pair. + :param trading_mode: Trading mode to use (used to determine the filename) :return: True when deleted, false if file did not exist. """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) if filename.exists(): filename.unlink() return True return False - def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame: + def trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> DataFrame: """ Load a pair from file, either .json.gz or .json Removes duplicates in the process. :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: List of trades """ - trades = trades_df_remove_duplicates(self._trades_load(pair, timerange=timerange)) + trades = trades_df_remove_duplicates( + self._trades_load(pair, trading_mode, timerange=timerange) + ) trades = trades_convert_types(trades) return trades @@ -264,7 +275,7 @@ class IDataHandler(ABC): return filename @classmethod - def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path: + def _pair_trades_filename(cls, datadir: Path, pair: str, trading_mode: TradingMode) -> Path: pair_s = misc.pair_to_filename(pair) if ':' in pair: # Futures pair ... diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index baa0c10a5..2d0333fed 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -8,7 +8,7 @@ from freqtrade import misc from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS from freqtrade.data.converter import trades_dict_to_list, trades_list_to_df -from freqtrade.enums import CandleType +from freqtrade.enums import CandleType, TradingMode from .idatahandler import IDataHandler @@ -94,14 +94,15 @@ class JsonDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_store(self, pair: str, data: DataFrame) -> None: + def _trades_store(self, pair: str, data: DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) trades = data.values.tolist() misc.file_dump_json(filename, trades, is_zip=self._use_zip) @@ -114,15 +115,18 @@ class JsonDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> DataFrame: + def _trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> DataFrame: """ Load a pair from file, either .json.gz or .json # TODO: respect timerange ... :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: Dataframe containing trades """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) tradesdata = misc.file_load_json(filename) if not tradesdata: diff --git a/freqtrade/data/history/parquetdatahandler.py b/freqtrade/data/history/parquetdatahandler.py index c0b0cad63..01becdc84 100644 --- a/freqtrade/data/history/parquetdatahandler.py +++ b/freqtrade/data/history/parquetdatahandler.py @@ -4,8 +4,8 @@ from typing import Optional from pandas import DataFrame, read_parquet, to_datetime from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList -from freqtrade.enums import CandleType +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS +from freqtrade.enums import CandleType, TradingMode from .idatahandler import IDataHandler @@ -81,14 +81,15 @@ class ParquetDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_store(self, pair: str, data: DataFrame) -> None: + def _trades_store(self, pair: str, data: DataFrame, trading_mode: TradingMode) -> None: """ Store trades data (list of Dicts) to file :param pair: Pair - used for filename :param data: Dataframe containing trades column sequence as in DEFAULT_TRADES_COLUMNS + :param trading_mode: Trading mode to use (used to determine the filename) """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) self.create_dir_if_needed(filename) data.reset_index(drop=True).to_parquet(filename) @@ -101,15 +102,18 @@ class ParquetDataHandler(IDataHandler): """ raise NotImplementedError() - def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList: + def _trades_load( + self, pair: str, trading_mode: TradingMode, timerange: Optional[TimeRange] = None + ) -> DataFrame: """ Load a pair from file, either .json.gz or .json # TODO: respect timerange ... :param pair: Load trades for this pair + :param trading_mode: Trading mode to use (used to determine the filename) :param timerange: Timerange to load trades for - currently not implemented :return: List of trades """ - filename = self._pair_trades_filename(self._datadir, pair) + filename = self._pair_trades_filename(self._datadir, pair, trading_mode) if not filename.exists(): return DataFrame(columns=DEFAULT_TRADES_COLUMNS) From 43103f51e55aada44c81a8a23fd29d886a3e3244 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Mar 2024 13:10:57 +0100 Subject: [PATCH 269/327] Update functions that use datahandler trade methods --- freqtrade/data/converter/trade_converter.py | 15 ++++++------ .../data/converter/trade_converter_kraken.py | 3 ++- freqtrade/data/history/history_utils.py | 24 ++++++++++++------- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/freqtrade/data/converter/trade_converter.py b/freqtrade/data/converter/trade_converter.py index 117f65bc6..0e5050a05 100644 --- a/freqtrade/data/converter/trade_converter.py +++ b/freqtrade/data/converter/trade_converter.py @@ -11,7 +11,7 @@ from pandas import DataFrame, to_datetime from freqtrade.configuration import TimeRange from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TRADES_DTYPES, Config, TradeList) -from freqtrade.enums import CandleType +from freqtrade.enums import CandleType, TradingMode from freqtrade.exceptions import OperationalException @@ -104,9 +104,9 @@ def convert_trades_to_ohlcv( logger.info(f"About to convert pairs: '{', '.join(pairs)}', " f"intervals: '{', '.join(timeframes)}' to {datadir}") - + trading_mode = TradingMode.FUTURES if candle_type != CandleType.SPOT else TradingMode.SPOT for pair in pairs: - trades = data_handler_trades.trades_load(pair) + trades = data_handler_trades.trades_load(pair, trading_mode) for timeframe in timeframes: if erase: if data_handler_ohlcv.ohlcv_purge(pair, timeframe, candle_type=candle_type): @@ -144,11 +144,12 @@ def convert_trades_format(config: Config, convert_from: str, convert_to: str, er if 'pairs' not in config: config['pairs'] = src.trades_get_pairs(config['datadir']) logger.info(f"Converting trades for {config['pairs']}") - + trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT) for pair in config['pairs']: - data = src.trades_load(pair=pair) + data = src.trades_load(pair, trading_mode) logger.info(f"Converting {len(data)} trades for {pair}") - trg.trades_store(pair, data) + trg.trades_store(pair, data, trading_mode) + if erase and convert_from != convert_to: logger.info(f"Deleting source Trade data for {pair}.") - src.trades_purge(pair=pair) + src.trades_purge(pair, trading_mode) diff --git a/freqtrade/data/converter/trade_converter_kraken.py b/freqtrade/data/converter/trade_converter_kraken.py index b0fa11c25..80bd917af 100644 --- a/freqtrade/data/converter/trade_converter_kraken.py +++ b/freqtrade/data/converter/trade_converter_kraken.py @@ -7,6 +7,7 @@ from freqtrade.constants import DATETIME_PRINT_FORMAT, DEFAULT_TRADES_COLUMNS, C from freqtrade.data.converter.trade_converter import (trades_convert_types, trades_df_remove_duplicates) from freqtrade.data.history.idatahandler import get_datahandler +from freqtrade.enums import TradingMode from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.resolvers import ExchangeResolver @@ -79,4 +80,4 @@ def import_kraken_trades_from_csv(config: Config, convert_to: str): f"{trades_df['date'].min():{DATETIME_PRINT_FORMAT}} to " f"{trades_df['date'].max():{DATETIME_PRINT_FORMAT}}") - data_handler.trades_store(pair, trades_df) + data_handler.trades_store(pair, trades_df, TradingMode.SPOT) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 27e229973..3f9468f7a 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -13,7 +13,7 @@ from freqtrade.data.converter import (clean_ohlcv_dataframe, convert_trades_to_o ohlcv_to_dataframe, trades_df_remove_duplicates, trades_list_to_df) from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler -from freqtrade.enums import CandleType +from freqtrade.enums import CandleType, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist @@ -333,7 +333,8 @@ def _download_trades_history(exchange: Exchange, pair: str, *, new_pairs_days: int = 30, timerange: Optional[TimeRange] = None, - data_handler: IDataHandler + data_handler: IDataHandler, + trading_mode: TradingMode, ) -> bool: """ Download trade history from the exchange. @@ -349,7 +350,7 @@ def _download_trades_history(exchange: Exchange, if timerange.stoptype == 'date': until = timerange.stopts * 1000 - trades = data_handler.trades_load(pair) + trades = data_handler.trades_load(pair, trading_mode) # TradesList columns are defined in constants.DEFAULT_TRADES_COLUMNS # DEFAULT_TRADES_COLUMNS: 0 -> timestamp @@ -388,7 +389,7 @@ def _download_trades_history(exchange: Exchange, trades = concat([trades, new_trades_df], axis=0) # Remove duplicates to make sure we're not storing data we don't need trades = trades_df_remove_duplicates(trades) - data_handler.trades_store(pair, data=trades) + data_handler.trades_store(pair, trades, trading_mode) logger.debug("New Start: %s", 'None' if trades.empty else f"{trades.iloc[0]['date']:{DATETIME_PRINT_FORMAT}}") @@ -405,8 +406,10 @@ def _download_trades_history(exchange: Exchange, def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path, - timerange: TimeRange, new_pairs_days: int = 30, - erase: bool = False, data_format: str = 'feather') -> List[str]: + timerange: TimeRange, trading_mode: TradingMode, + new_pairs_days: int = 30, + erase: bool = False, data_format: str = 'feather', + ) -> List[str]: """ Refresh stored trades data for backtesting and hyperopt operations. Used by freqtrade download-data subcommand. @@ -421,7 +424,7 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: continue if erase: - if data_handler.trades_purge(pair): + if data_handler.trades_purge(pair, trading_mode): logger.info(f'Deleting existing data for pair {pair}.') logger.info(f'Downloading trades for pair {pair}.') @@ -429,7 +432,8 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: pair=pair, new_pairs_days=new_pairs_days, timerange=timerange, - data_handler=data_handler) + data_handler=data_handler, + trading_mode=trading_mode) return pairs_not_available @@ -521,7 +525,9 @@ def download_data_main(config: Config) -> None: pairs_not_available = refresh_backtest_trades_data( exchange, pairs=expanded_pairs, datadir=config['datadir'], timerange=timerange, new_pairs_days=config['new_pairs_days'], - erase=bool(config.get('erase')), data_format=config['dataformat_trades']) + erase=bool(config.get('erase')), data_format=config['dataformat_trades'], + trading_mode=config.get('trading_mode', TradingMode.SPOT), + ) # Convert downloaded trade data to different timeframes convert_trades_to_ohlcv( From 66e43f2fe86e9948ce800e61b8362788e742c8f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Mar 2024 13:17:45 +0100 Subject: [PATCH 270/327] Adjust tests for new arguments --- tests/data/test_datahandler.py | 24 ++++++------- tests/data/test_history.py | 41 ++++++++++++----------- tests/data/test_trade_converter_kraken.py | 3 +- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/tests/data/test_datahandler.py b/tests/data/test_datahandler.py index a0a37c393..1217c35ad 100644 --- a/tests/data/test_datahandler.py +++ b/tests/data/test_datahandler.py @@ -261,11 +261,11 @@ def test_datahandler_trades_not_supported(datahandler, testdatadir, ): def test_jsondatahandler_trades_load(testdatadir, caplog): dh = JsonGzDataHandler(testdatadir) logmsg = "Old trades format detected - converting" - dh.trades_load('XRP/ETH') + dh.trades_load('XRP/ETH', TradingMode.SPOT) assert not log_has(logmsg, caplog) # Test conversation is happening - dh.trades_load('XRP/OLD') + dh.trades_load('XRP/OLD', TradingMode.SPOT) assert log_has(logmsg, caplog) @@ -300,16 +300,16 @@ def test_datahandler_trades_get_pairs(testdatadir, datahandler, expected): def test_hdf5datahandler_trades_load(testdatadir): dh = get_datahandler(testdatadir, 'hdf5') - trades = dh.trades_load('XRP/ETH') + trades = dh.trades_load('XRP/ETH', TradingMode.SPOT) assert isinstance(trades, DataFrame) - trades1 = dh.trades_load('UNITTEST/NONEXIST') + trades1 = dh.trades_load('UNITTEST/NONEXIST', TradingMode.SPOT) assert isinstance(trades1, DataFrame) assert trades1.empty # data goes from 2019-10-11 - 2019-10-13 timerange = TimeRange.parse_timerange('20191011-20191012') - trades2 = dh._trades_load('XRP/ETH', timerange) + trades2 = dh._trades_load('XRP/ETH', TradingMode.SPOT, timerange) assert len(trades) > len(trades2) # Check that ID is None (If it's nan, it's wrong) assert trades2.iloc[0]['type'] is None @@ -451,13 +451,13 @@ def test_hdf5datahandler_ohlcv_purge(mocker, testdatadir): @pytest.mark.parametrize('datahandler', ['jsongz', 'hdf5', 'feather', 'parquet']) def test_datahandler_trades_load(testdatadir, datahandler): dh = get_datahandler(testdatadir, datahandler) - trades = dh.trades_load('XRP/ETH') + trades = dh.trades_load('XRP/ETH', TradingMode.SPOT) assert isinstance(trades, DataFrame) assert trades.iloc[0]['timestamp'] == 1570752011620 assert trades.iloc[0]['date'] == Timestamp('2019-10-11 00:00:11.620000+0000') assert trades.iloc[-1]['cost'] == 0.1986231 - trades1 = dh.trades_load('UNITTEST/NONEXIST') + trades1 = dh.trades_load('UNITTEST/NONEXIST', TradingMode.SPOT) assert isinstance(trades, DataFrame) assert trades1.empty @@ -465,15 +465,15 @@ def test_datahandler_trades_load(testdatadir, datahandler): @pytest.mark.parametrize('datahandler', ['jsongz', 'hdf5', 'feather', 'parquet']) def test_datahandler_trades_store(testdatadir, tmp_path, datahandler): dh = get_datahandler(testdatadir, datahandler) - trades = dh.trades_load('XRP/ETH') + trades = dh.trades_load('XRP/ETH', TradingMode.SPOT) dh1 = get_datahandler(tmp_path, datahandler) - dh1.trades_store('XRP/NEW', trades) + dh1.trades_store('XRP/NEW', trades, TradingMode.SPOT) file = tmp_path / f'XRP_NEW-trades.{dh1._get_file_extension()}' assert file.is_file() # Load trades back - trades_new = dh1.trades_load('XRP/NEW') + trades_new = dh1.trades_load('XRP/NEW', TradingMode.SPOT) assert_frame_equal(trades, trades_new, check_exact=True) assert len(trades_new) == len(trades) @@ -483,11 +483,11 @@ def test_datahandler_trades_purge(mocker, testdatadir, datahandler): mocker.patch.object(Path, "exists", MagicMock(return_value=False)) unlinkmock = mocker.patch.object(Path, "unlink", MagicMock()) dh = get_datahandler(testdatadir, datahandler) - assert not dh.trades_purge('UNITTEST/NONEXIST') + assert not dh.trades_purge('UNITTEST/NONEXIST', TradingMode.SPOT) assert unlinkmock.call_count == 0 mocker.patch.object(Path, "exists", MagicMock(return_value=True)) - assert dh.trades_purge('UNITTEST/NONEXIST') + assert dh.trades_purge('UNITTEST/NONEXIST', TradingMode.SPOT) assert unlinkmock.call_count == 1 diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 106babd63..a3fe492b7 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -23,7 +23,7 @@ from freqtrade.data.history.history_utils import (_download_pair_history, _downl validate_backtest_data) from freqtrade.data.history.idatahandler import get_datahandler from freqtrade.data.history.jsondatahandler import JsonDataHandler, JsonGzDataHandler -from freqtrade.enums import CandleType +from freqtrade.enums import CandleType, TradingMode from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.resolvers import StrategyResolver @@ -168,21 +168,21 @@ def test_json_pair_data_filename(pair, timeframe, expected_result, candle_type): assert fn == Path(expected_result + '.gz') -@pytest.mark.parametrize("pair,expected_result", [ - ("ETH/BTC", 'freqtrade/hello/world/ETH_BTC-trades.json'), - ("ETH/USDT:USDT", 'freqtrade/hello/world/futures/ETH_USDT_USDT-trades.json'), - ("Fabric Token/ETH", 'freqtrade/hello/world/Fabric_Token_ETH-trades.json'), - ("ETHH20", 'freqtrade/hello/world/ETHH20-trades.json'), - (".XBTBON2H", 'freqtrade/hello/world/_XBTBON2H-trades.json'), - ("ETHUSD.d", 'freqtrade/hello/world/ETHUSD_d-trades.json'), - ("ACC_OLD_BTC", 'freqtrade/hello/world/ACC_OLD_BTC-trades.json'), +@pytest.mark.parametrize("pair,trading_mode,expected_result", [ + ("ETH/BTC", '', 'freqtrade/hello/world/ETH_BTC-trades.json'), + ("ETH/USDT:USDT", 'futures', 'freqtrade/hello/world/futures/ETH_USDT_USDT-trades.json'), + ("Fabric Token/ETH", '', 'freqtrade/hello/world/Fabric_Token_ETH-trades.json'), + ("ETHH20", '', 'freqtrade/hello/world/ETHH20-trades.json'), + (".XBTBON2H", '', 'freqtrade/hello/world/_XBTBON2H-trades.json'), + ("ETHUSD.d", '', 'freqtrade/hello/world/ETHUSD_d-trades.json'), + ("ACC_OLD_BTC", '', 'freqtrade/hello/world/ACC_OLD_BTC-trades.json'), ]) -def test_json_pair_trades_filename(pair, expected_result): - fn = JsonDataHandler._pair_trades_filename(Path('freqtrade/hello/world'), pair) +def test_json_pair_trades_filename(pair, trading_mode, expected_result): + fn = JsonDataHandler._pair_trades_filename(Path('freqtrade/hello/world'), pair, trading_mode) assert isinstance(fn, Path) assert fn == Path(expected_result) - fn = JsonGzDataHandler._pair_trades_filename(Path('freqtrade/hello/world'), pair) + fn = JsonGzDataHandler._pair_trades_filename(Path('freqtrade/hello/world'), pair, trading_mode) assert isinstance(fn, Path) assert fn == Path(expected_result + '.gz') @@ -560,7 +560,8 @@ def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, tes unavailable_pairs = refresh_backtest_trades_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC", "XRP/ETH"], datadir=testdatadir, - timerange=timerange, erase=True + timerange=timerange, erase=True, + trading_mode=TradingMode.SPOT, ) assert dl_mock.call_count == 2 @@ -585,7 +586,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad assert not file1.is_file() assert _download_trades_history(data_handler=data_handler, exchange=exchange, - pair='ETH/BTC') + pair='ETH/BTC', trading_mode=TradingMode.SPOT) assert log_has("Current Amount of trades: 0", caplog) assert log_has("New Amount of trades: 6", caplog) assert ght_mock.call_count == 1 @@ -598,8 +599,9 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad since_time = int(trades_history[-3][0] // 1000) since_time2 = int(trades_history[-1][0] // 1000) timerange = TimeRange('date', None, since_time, 0) - assert _download_trades_history(data_handler=data_handler, exchange=exchange, - pair='ETH/BTC', timerange=timerange) + assert _download_trades_history( + data_handler=data_handler, exchange=exchange, pair='ETH/BTC', + timerange=timerange, trading_mode=TradingMode.SPOT) assert ght_mock.call_count == 1 # Check this in seconds - since we had to convert to seconds above too. @@ -612,7 +614,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad caplog.clear() assert not _download_trades_history(data_handler=data_handler, exchange=exchange, - pair='ETH/BTC') + pair='ETH/BTC', trading_mode=TradingMode.SPOT) assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog) file2 = tmp_path / 'XRP_ETH-trades.json.gz' @@ -624,8 +626,9 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad since_time = int(trades_history[0][0] // 1000) - 500 timerange = TimeRange('date', None, since_time, 0) - assert _download_trades_history(data_handler=data_handler, exchange=exchange, - pair='XRP/ETH', timerange=timerange) + assert _download_trades_history( + data_handler=data_handler, exchange=exchange, pair='XRP/ETH', + timerange=timerange, trading_mode=TradingMode.SPOT) assert ght_mock.call_count == 1 diff --git a/tests/data/test_trade_converter_kraken.py b/tests/data/test_trade_converter_kraken.py index 91de303fb..ba9221e0a 100644 --- a/tests/data/test_trade_converter_kraken.py +++ b/tests/data/test_trade_converter_kraken.py @@ -6,6 +6,7 @@ import pytest from freqtrade.data.converter.trade_converter_kraken import import_kraken_trades_from_csv from freqtrade.data.history.idatahandler import get_datahandler +from freqtrade.enums import TradingMode from freqtrade.exceptions import OperationalException from tests.conftest import EXMS, log_has, log_has_re, patch_exchange @@ -40,7 +41,7 @@ def test_import_kraken_trades_from_csv(testdatadir, tmp_path, caplog, default_co assert dstfile.is_file() dh = get_datahandler(tmp_path, 'feather') - trades = dh.trades_load('BCH_EUR') + trades = dh.trades_load('BCH_EUR', TradingMode.SPOT) assert len(trades) == 340 assert trades['date'].min().to_pydatetime() == datetime(2023, 1, 1, 0, 3, 56, From 5e7868a28dbadf96cefd27f280cd93016e0de6d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Mar 2024 13:22:41 +0100 Subject: [PATCH 271/327] Remove block from download-trades for futures --- freqtrade/data/history/history_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 3f9468f7a..208859cd3 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -520,8 +520,6 @@ def download_data_main(config: Config) -> None: # Start downloading try: if config.get('download_trades'): - if config.get('trading_mode') == 'futures': - raise OperationalException("Trade download not supported for futures.") pairs_not_available = refresh_backtest_trades_data( exchange, pairs=expanded_pairs, datadir=config['datadir'], timerange=timerange, new_pairs_days=config['new_pairs_days'], From 09d763b604453ac702bc2d6e5d9adfbc3947fd69 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Mar 2024 13:23:03 +0100 Subject: [PATCH 272/327] convert-trades should do proper pair expansion to support regex in pairlists --- freqtrade/commands/data_commands.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index d3600e3ef..b183d403b 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -11,6 +11,7 @@ from freqtrade.data.history import download_data_main from freqtrade.enums import CandleType, RunMode, TradingMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes +from freqtrade.plugins.pairlist.pairlist_helpers import dynamic_expand_pairlist from freqtrade.resolvers import ExchangeResolver from freqtrade.util.migrations import migrate_data @@ -62,10 +63,17 @@ def start_convert_trades(args: Dict[str, Any]) -> None: for timeframe in config['timeframes']: exchange.validate_timeframes(timeframe) + available_pairs = [ + p for p in exchange.get_markets( + tradable_only=True, active_only=not config.get('include_inactive') + ).keys() + ] + + expanded_pairs = dynamic_expand_pairlist(config, available_pairs) # Convert downloaded trade data to different timeframes convert_trades_to_ohlcv( - pairs=config.get('pairs', []), timeframes=config['timeframes'], + pairs=expanded_pairs, timeframes=config['timeframes'], datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), data_format_ohlcv=config['dataformat_ohlcv'], data_format_trades=config['dataformat_trades'], From 99da6f70c2959f75e66850237086f20b8de007c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Mar 2024 13:27:54 +0100 Subject: [PATCH 273/327] Fix failing test due to new approach for convert-trades --- tests/commands/test_commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 1ab9d2202..2252ff9f4 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -837,10 +837,11 @@ def test_download_data_data_invalid(mocker): start_download_data(pargs) -def test_start_convert_trades(mocker, caplog): +def test_start_convert_trades(mocker): convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv', MagicMock(return_value=[])) patch_exchange(mocker) + mocker.patch(f'{EXMS}.get_markets') mocker.patch(f'{EXMS}.markets', PropertyMock(return_value={})) args = [ "trades-to-ohlcv", From 7ed7ed4081a1dedaecd025678268a71e0543f980 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Mar 2024 15:38:36 +0100 Subject: [PATCH 274/327] Accept trading-mode for trades-to-ohlcv command --- freqtrade/commands/arguments.py | 3 ++- freqtrade/data/converter/trade_converter.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index f72164675..191f07910 100755 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -69,7 +69,8 @@ ARGS_CONVERT_DATA_TRADES = ["pairs", "format_from_trades", "format_to", "erase", ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase", "exchange"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "trading_mode", "candle_types"] -ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"] +ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades", + "trading_mode"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode", "show_timerange"] diff --git a/freqtrade/data/converter/trade_converter.py b/freqtrade/data/converter/trade_converter.py index 0e5050a05..1c8327ec3 100644 --- a/freqtrade/data/converter/trade_converter.py +++ b/freqtrade/data/converter/trade_converter.py @@ -99,8 +99,6 @@ def convert_trades_to_ohlcv( from freqtrade.data.history.idatahandler import get_datahandler data_handler_trades = get_datahandler(datadir, data_format=data_format_trades) data_handler_ohlcv = get_datahandler(datadir, data_format=data_format_ohlcv) - if not pairs: - pairs = data_handler_trades.trades_get_pairs(datadir) logger.info(f"About to convert pairs: '{', '.join(pairs)}', " f"intervals: '{', '.join(timeframes)}' to {datadir}") From fcb16098d86779b17cdc889553d653547b5b3a51 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 2 Mar 2024 15:40:43 +0100 Subject: [PATCH 275/327] Reduce Error level when converting trades --- freqtrade/data/converter/trade_converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/converter/trade_converter.py b/freqtrade/data/converter/trade_converter.py index 1c8327ec3..682430994 100644 --- a/freqtrade/data/converter/trade_converter.py +++ b/freqtrade/data/converter/trade_converter.py @@ -114,7 +114,7 @@ def convert_trades_to_ohlcv( # Store ohlcv data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv, candle_type=candle_type) except ValueError: - logger.exception(f'Could not convert {pair} to OHLCV.') + logger.warning(f'Could not convert {pair} to OHLCV.') def convert_trades_format(config: Config, convert_from: str, convert_to: str, erase: bool): From b8a1089592d2b6087fbd6dace25b2211d39cc93d Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 3 Mar 2024 12:23:18 +0100 Subject: [PATCH 276/327] fix: try plotting as much info in xgboost tensorboard as possible --- freqtrade/freqai/tensorboard/tensorboard.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqai/tensorboard/tensorboard.py b/freqtrade/freqai/tensorboard/tensorboard.py index 46bf8dc61..3ac58a117 100644 --- a/freqtrade/freqai/tensorboard/tensorboard.py +++ b/freqtrade/freqai/tensorboard/tensorboard.py @@ -46,10 +46,8 @@ class TensorBoardCallback(BaseTensorBoardCallback): for data, metric in evals_log.items(): for metric_name, log in metric.items(): score = log[-1][0] if isinstance(log[-1], tuple) else log[-1] - if data == "train": - self.writer.add_scalar("train_loss", score, epoch) - else: - self.writer.add_scalar("valid_loss", score, epoch) + key = self._get_key(data, metric_name) + self.writer.add_scalar(f"{key}_loss", score, epoch) return False From 093a093bd513111dce24c200fc2636cb7e6185c1 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 3 Mar 2024 12:38:51 +0100 Subject: [PATCH 277/327] fix: use data metric_name directly --- freqtrade/freqai/tensorboard/tensorboard.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqai/tensorboard/tensorboard.py b/freqtrade/freqai/tensorboard/tensorboard.py index 3ac58a117..d91c70c81 100644 --- a/freqtrade/freqai/tensorboard/tensorboard.py +++ b/freqtrade/freqai/tensorboard/tensorboard.py @@ -46,8 +46,7 @@ class TensorBoardCallback(BaseTensorBoardCallback): for data, metric in evals_log.items(): for metric_name, log in metric.items(): score = log[-1][0] if isinstance(log[-1], tuple) else log[-1] - key = self._get_key(data, metric_name) - self.writer.add_scalar(f"{key}_loss", score, epoch) + self.writer.add_scalar(f"{data}-{metric_name}", score, epoch) return False From 1176c16b93bd8b4d8375236b7a9011880b9f306c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Mar 2024 12:41:51 +0100 Subject: [PATCH 278/327] REmove unnecessary assignment --- freqtrade/optimize/backtesting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4b217bcf6..7147ad14f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -337,7 +337,6 @@ class Backtesting: self.disable_database_use() PairLocks.reset_locks() Trade.reset_trades() - CustomDataWrapper.use_db = False CustomDataWrapper.reset_custom_data() self.rejected_trades = 0 self.timedout_entry_orders = 0 From 30b4f271522f233fe50ecd9e8b67e6120446d581 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Mar 2024 12:46:46 +0100 Subject: [PATCH 279/327] Cleanup some nitpicks --- freqtrade/persistence/custom_data.py | 1 + freqtrade/persistence/trade_model.py | 4 +--- tests/persistence/test_persistence.py | 1 + 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/custom_data.py b/freqtrade/persistence/custom_data.py index 3ebcd0f48..81a9e7ad6 100644 --- a/freqtrade/persistence/custom_data.py +++ b/freqtrade/persistence/custom_data.py @@ -107,6 +107,7 @@ class CustomDataWrapper: @staticmethod def delete_custom_data(trade_id: int) -> None: _CustomData.session.query(_CustomData).filter(_CustomData.ft_trade_id == trade_id).delete() + _CustomData.session.commit() @staticmethod def get_custom_data(*, trade_id: int, key: Optional[str] = None) -> List[_CustomData]: diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 06a6e818d..e74bc1f48 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -343,7 +343,6 @@ class LocalTrade: id: int = 0 orders: List[Order] = [] - custom_data: List[_CustomData] = [] exchange: str = '' pair: str = '' @@ -1507,7 +1506,7 @@ class Trade(ModelBase, LocalTrade): innerjoin=True) # type: ignore custom_data: Mapped[List[_CustomData]] = relationship( "_CustomData", cascade="all, delete-orphan", - lazy="raise") # type: ignore + 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 @@ -1613,7 +1612,6 @@ class Trade(ModelBase, LocalTrade): CustomDataWrapper.delete_custom_data(trade_id=self.id) - _CustomData.session.commit() Trade.session.delete(self) Trade.commit() diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 0e0e70ee8..18f28da2b 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -2099,6 +2099,7 @@ def test_Trade_object_idem(): 'get_mix_tag_performance', 'get_trading_volume', 'validate_string_len', + 'custom_data' ) EXCLUDES2 = ('trades', 'trades_open', 'bt_trades_open_pp', 'bt_open_open_trade_count', 'total_profit', 'from_json',) From c1ae110080e4b966efe1c5adf60b013b1b6a2e5b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Mar 2024 12:56:42 +0100 Subject: [PATCH 280/327] Improve documentation --- docs/strategy-advanced.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 9f0b3c112..36185676c 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -14,10 +14,10 @@ The call sequence of the methods described here is covered under [bot execution ## Storing information (Non-Persistent) !!! Warning "Deprecated" - This method of storing information is deprecated, and we do advise against using non-persistent storage. + This method of storing information is deprecated and we do advise against using non-persistent storage. Please use the below [Persistent Storing Information Section](#storing-information-persistent) instead. - It's content has therefore be collapsed. + It's content has therefore been collapsed. ??? Abstract "Storing information" Storing information can be accomplished by creating a new dictionary within the strategy class. @@ -49,11 +49,12 @@ The call sequence of the methods described here is covered under [bot execution ## Storing information (Persistent) -Storing information can also be performed in a persistent manner. Freqtrade allows storing/retrieving user custom information associated with a specific trade. +Freqtrade allows storing/retrieving user custom information associated with a specific trade in the database. -Using a trade object, information can be stored using `trade_obj.set_custom_data(key='my_key', value=my_value)` and retrieved using `trade_obj.get_custom_data(key='my_key')`. Each data entry is associated with a trade and a user supplied key (of type `string`). This means that this can only be used in callbacks that also provide a trade object. +Using a trade object, information can be stored using `trade.set_custom_data(key='my_key', value=my_value)` and retrieved using `trade.get_custom_data(key='my_key')`. Each data entry is associated with a trade and a user supplied key (of type `string`). This means that this can only be used in callbacks that also provide a trade object. -For the data to be able to be stored within the database it must be serialized. This is done by converting it to a JSON formatted string. +For the data to be able to be stored within the database, freqtrade must serialized the data. This is done by converting the data to a JSON formatted string. +Freqtrade will attempt to reverse this action on retrieval, so from a strategy perspective, this should not be relevant. ```python from freqtrade.persistence import Trade @@ -116,9 +117,11 @@ class AwesomeStrategy(IStrategy): return False, None ``` +The above is a simple example - there are simpler ways to retrieve trade data like entry-adjustments. + !!! Note It is recommended that simple data types are used `[bool, int, float, str]` to ensure no issues when serializing the data that needs to be stored. - Storing big junks of data may lead to unintended side-effects, like a database becoming big pretty fast (and as a consequence, also slow). + Storing big junks of data may lead to unintended side-effects, like a database becoming big (and as a consequence, also slow). !!! Warning "Non-serializable data" If supplied data cannot be serialized a warning is logged and the entry for the specified `key` will contain `None` as data. From ceb461a25285d9a0ce7fc44c6ceac0b1cccb3552 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Mar 2024 12:58:10 +0100 Subject: [PATCH 281/327] Switch sequence of information documentation --- docs/strategy-advanced.md | 72 +++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 36185676c..debd5bc1b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -11,42 +11,6 @@ The call sequence of the methods described here is covered under [bot execution !!! Tip Start off with a strategy template containing all available callback methods by running `freqtrade new-strategy --strategy MyAwesomeStrategy --template advanced` -## Storing information (Non-Persistent) - -!!! Warning "Deprecated" - This method of storing information is deprecated and we do advise against using non-persistent storage. - Please use the below [Persistent Storing Information Section](#storing-information-persistent) instead. - - It's content has therefore been collapsed. - -??? Abstract "Storing information" - Storing information can be accomplished by creating a new dictionary within the strategy class. - - The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables. - - ```python - class AwesomeStrategy(IStrategy): - # Create custom dictionary - custom_info = {} - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # Check if the entry already exists - if not metadata["pair"] in self.custom_info: - # Create empty entry for this pair - self.custom_info[metadata["pair"]] = {} - - if "crosstime" in self.custom_info[metadata["pair"]]: - self.custom_info[metadata["pair"]]["crosstime"] += 1 - else: - self.custom_info[metadata["pair"]]["crosstime"] = 1 - ``` - - !!! Warning - The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. - - !!! Note - If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. - ## Storing information (Persistent) Freqtrade allows storing/retrieving user custom information associated with a specific trade in the database. @@ -135,6 +99,42 @@ The above is a simple example - there are simpler ways to retrieve trade data li "value" can be any type (both in setting and receiving) - but must be json serializable. +## Storing information (Non-Persistent) + +!!! Warning "Deprecated" + This method of storing information is deprecated and we do advise against using non-persistent storage. + Please use [Persistent Storage](#storing-information-persistent) instead. + + It's content has therefore been collapsed. + +??? Abstract "Storing information" + Storing information can be accomplished by creating a new dictionary within the strategy class. + + The name of the variable can be chosen at will, but should be prefixed with `custom_` to avoid naming collisions with predefined strategy variables. + + ```python + class AwesomeStrategy(IStrategy): + # Create custom dictionary + custom_info = {} + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Check if the entry already exists + if not metadata["pair"] in self.custom_info: + # Create empty entry for this pair + self.custom_info[metadata["pair"]] = {} + + if "crosstime" in self.custom_info[metadata["pair"]]: + self.custom_info[metadata["pair"]]["crosstime"] += 1 + else: + self.custom_info[metadata["pair"]]["crosstime"] = 1 + ``` + + !!! Warning + The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. + + !!! Note + If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. + ## Dataframe access You may access dataframe in various strategy functions by querying it from dataprovider. From 265a7123dad8a68a8df6d0e258fc41bdc9d81211 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Mar 2024 13:10:50 +0100 Subject: [PATCH 282/327] Add explicit test for telegram functionality of list-custom-data --- tests/rpc/test_rpc_telegram.py | 46 ++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 29a2b2723..3bd372b19 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2657,3 +2657,49 @@ async def test_change_market_direction(default_conf, mocker, update) -> None: context.args = ["invalid"] await telegram._changemarketdir(update, context) assert telegram._rpc._freqtrade.strategy.market_direction == MarketDirection.LONG + + +async def test_telegram_list_custom_data(default_conf_usdt, update, ticker, fee, mocker) -> None: + + mocker.patch.multiple( + EXMS, + fetch_ticker=ticker, + get_fee=fee, + ) + telegram, _freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf_usdt) + + # Create some test data + create_mock_trades_usdt(fee) + # No trade id + context = MagicMock() + await telegram._list_custom_data(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Trade-id not set.' in msg_mock.call_args_list[0][0][0] + msg_mock.reset_mock() + + # + context.args = ['1'] + await telegram._list_custom_data(update=update, context=context) + assert msg_mock.call_count == 1 + assert ( + "Didn't find any custom-data entries for Trade ID: `1`" in msg_mock.call_args_list[0][0][0] + ) + msg_mock.reset_mock() + + # Add some custom data + trade1 = Trade.get_trades_proxy()[0] + trade1.set_custom_data('test_int', 1) + trade1.set_custom_data('test_dict', {'test': 'dict'}) + Trade.commit() + context.args = [f"{trade1.id}"] + await telegram._list_custom_data(update=update, context=context) + assert msg_mock.call_count == 3 + assert "Found custom-data entries: " in msg_mock.call_args_list[0][0][0] + assert ( + "*Key:* `test_int`\n*ID:* `1`\n*Trade ID:* `1`\n*Type:* `int`\n" + "*Value:* `1`\n*Create Date:*") in msg_mock.call_args_list[1][0][0] + assert ( + '*Key:* `test_dict`\n*ID:* `2`\n*Trade ID:* `1`\n*Type:* `dict`\n' + '*Value:* `{"test": "dict"}`\n*Create Date:* `') in msg_mock.call_args_list[2][0][0] + + msg_mock.reset_mock() From ed8469f23ac32fe0ca6e6d6eb06b9cd7e4f07a70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Mar 2024 13:12:42 +0100 Subject: [PATCH 283/327] use trading_mode to determine trades file location --- freqtrade/data/history/idatahandler.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index bcb31a7c8..fbaded640 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -277,9 +277,8 @@ class IDataHandler(ABC): @classmethod def _pair_trades_filename(cls, datadir: Path, pair: str, trading_mode: TradingMode) -> Path: pair_s = misc.pair_to_filename(pair) - if ':' in pair: + if trading_mode == TradingMode.FUTURES: # Futures pair ... - # TODO: this should not rely on ";" in the pairname. datadir = datadir.joinpath('futures') filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}') From 255ea88638957d19fcd439987ae32d383243f810 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Mar 2024 15:24:26 +0100 Subject: [PATCH 284/327] Add to telegram documentation --- docs/telegram-usage.md | 1 + freqtrade/rpc/telegram.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index e4dc02c76..2709baf9a 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -181,6 +181,7 @@ official commands. You can ask at any moment for help with `/help`. | `/locks` | Show currently locked pairs. | `/unlock ` | Remove the lock for this pair (or for this lock id). | `/marketdir [long | short | even | none]` | Updates the user managed variable that represents the current market direction. If no direction is provided, the currently set direction will be displayed. +| `/list_custom_data [key]` | List custom_data for Trade ID & Key combination. If no Key is supplied it will list all key-value pairs found for that Trade ID. | **Modify Trade states** | | `/forceexit | /fx ` | Instantly exits the given trade (Ignoring `minimum_roi`). | `/forceexit all | /fx all` | Instantly exits all open trades (Ignoring `minimum_roi`). diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4f4ea17d3..f7e7362ef 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1668,6 +1668,8 @@ class Telegram(RPCHandler): "*/marketdir [long | short | even | none]:* `Updates the user managed variable " "that represents the current market direction. If no direction is provided `" "`the currently set market direction will be output.` \n" + "*/list_custom_data :* `List custom_data for Trade ID & Key combo.`\n" + "`If no Key is supplied it will list all key-value pairs found for that Trade ID.`" "_Statistics_\n" "------------\n" @@ -1691,8 +1693,6 @@ class Telegram(RPCHandler): "Avg. holding durations for buys and sells.`\n" "*/help:* `This help message`\n" "*/version:* `Show version`\n" - "*/list_custom_data :* `List custom_data for Trade ID & Key combo.`\n" - "`If no Key is supplied it will list all key-value pairs found for that Trade ID.`" ) await self._send_msg(message, parse_mode=ParseMode.MARKDOWN) From a948796ef74449952a4f57268304f616e8a6aa33 Mon Sep 17 00:00:00 2001 From: robcaulk Date: Sun, 3 Mar 2024 15:47:19 +0100 Subject: [PATCH 285/327] fix: manually add train eval since xgboost does not expose this information by default --- .../freqai/prediction_models/XGBoostRegressor.py | 11 +++++++++-- freqtrade/freqai/tensorboard/tensorboard.py | 7 ++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqai/prediction_models/XGBoostRegressor.py b/freqtrade/freqai/prediction_models/XGBoostRegressor.py index f1a2474da..f3de6653b 100644 --- a/freqtrade/freqai/prediction_models/XGBoostRegressor.py +++ b/freqtrade/freqai/prediction_models/XGBoostRegressor.py @@ -36,8 +36,15 @@ class XGBoostRegressor(BaseRegressionModel): eval_set = None eval_weights = None else: - eval_set = [(data_dictionary["test_features"], data_dictionary["test_labels"])] - eval_weights = [data_dictionary['test_weights']] + eval_set = [ + (data_dictionary["test_features"], + data_dictionary["test_labels"]), + (X, y) + ] + eval_weights = [ + data_dictionary['test_weights'], + data_dictionary['train_weights'] + ] sample_weight = data_dictionary["train_weights"] diff --git a/freqtrade/freqai/tensorboard/tensorboard.py b/freqtrade/freqai/tensorboard/tensorboard.py index d91c70c81..3ad896108 100644 --- a/freqtrade/freqai/tensorboard/tensorboard.py +++ b/freqtrade/freqai/tensorboard/tensorboard.py @@ -43,10 +43,11 @@ class TensorBoardCallback(BaseTensorBoardCallback): if not evals_log: return False - for data, metric in evals_log.items(): - for metric_name, log in metric.items(): + evals = ["validation", "train"] + for metric, eval in zip(evals_log.items(), evals): + for metric_name, log in metric[1].items(): score = log[-1][0] if isinstance(log[-1], tuple) else log[-1] - self.writer.add_scalar(f"{data}-{metric_name}", score, epoch) + self.writer.add_scalar(f"{eval}-{metric_name}", score, epoch) return False From 07bf19a990ad8342014c558f81a729a584b49585 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:17:13 +0000 Subject: [PATCH 286/327] Bump pypa/gh-action-pypi-publish from 1.8.11 to 1.8.12 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.11 to 1.8.12. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.11...v1.8.12) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ba55eed04..e0587525e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -482,12 +482,12 @@ jobs: path: dist - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@v1.8.12 with: repository-url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@v1.8.12 deploy-docker: From 7ca3032d515bdf2bf5d6b48440c16f053216629d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:22:32 +0000 Subject: [PATCH 287/327] Bump the pytest group with 1 update Bumps the pytest group with 1 update: [pytest](https://github.com/pytest-dev/pytest). Updates `pytest` from 8.0.2 to 8.1.0 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.0.2...8.1.0) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-minor dependency-group: pytest ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 77d981087..1014648ce 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ coveralls==3.3.1 ruff==0.2.2 mypy==1.8.0 pre-commit==3.6.2 -pytest==8.0.2 +pytest==8.1.0 pytest-asyncio==0.23.5 pytest-cov==4.1.0 pytest-mock==3.12.0 From 21709204eb8fef248870704cb1a1793205e641bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:22:37 +0000 Subject: [PATCH 288/327] Bump time-machine from 2.13.0 to 2.14.0 Bumps [time-machine](https://github.com/adamchainz/time-machine) from 2.13.0 to 2.14.0. - [Changelog](https://github.com/adamchainz/time-machine/blob/main/CHANGELOG.rst) - [Commits](https://github.com/adamchainz/time-machine/compare/2.13.0...2.14.0) --- updated-dependencies: - dependency-name: time-machine dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 77d981087..a05aa478f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -18,7 +18,7 @@ pytest-random-order==1.1.1 pytest-xdist==3.5.0 isort==5.13.2 # For datetime mocking -time-machine==2.13.0 +time-machine==2.14.0 # Convert jupyter notebooks to markdown documents nbconvert==7.16.1 From 062376f5735987e4b57f18e860b9685c41ed4682 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:22:41 +0000 Subject: [PATCH 289/327] Bump mkdocs-material from 9.5.11 to 9.5.12 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.11 to 9.5.12. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.11...9.5.12) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index cbb81b6b2..55a2c11aa 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.5.2 mkdocs==1.5.3 -mkdocs-material==9.5.11 +mkdocs-material==9.5.12 mdx_truly_sane_lists==1.3 pymdown-extensions==10.7 jinja2==3.1.3 From c3f9b16c8456551f7c700e017479379794c58e73 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:22:46 +0000 Subject: [PATCH 290/327] Bump rich from 13.7.0 to 13.7.1 Bumps [rich](https://github.com/Textualize/rich) from 13.7.0 to 13.7.1. - [Release notes](https://github.com/Textualize/rich/releases) - [Changelog](https://github.com/Textualize/rich/blob/master/CHANGELOG.md) - [Commits](https://github.com/Textualize/rich/compare/v13.7.0...v13.7.1) --- updated-dependencies: - dependency-name: rich dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0477751cd..90166b267 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,7 +21,7 @@ pycoingecko==3.1.0 jinja2==3.1.3 tables==3.9.1 joblib==1.3.2 -rich==13.7.0 +rich==13.7.1 pyarrow==15.0.0; platform_machine != 'armv7l' # find first, C search in arrays From b8c16fb889244a0445b39f014735046bcad2cd7b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:23:02 +0000 Subject: [PATCH 291/327] Bump python-dateutil from 2.8.2 to 2.9.0.post0 Bumps [python-dateutil](https://github.com/dateutil/dateutil) from 2.8.2 to 2.9.0.post0. - [Release notes](https://github.com/dateutil/dateutil/releases) - [Changelog](https://github.com/dateutil/dateutil/blob/master/NEWS) - [Commits](https://github.com/dateutil/dateutil/compare/2.8.2...2.9.0.post0) --- updated-dependencies: - dependency-name: python-dateutil dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0477751cd..449b5be28 100644 --- a/requirements.txt +++ b/requirements.txt @@ -49,7 +49,7 @@ colorama==0.4.6 questionary==2.0.1 prompt-toolkit==3.0.36 # Extensions to datetime library -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 pytz==2024.1 #Futures From ec17b5523c98c3c167473838424978151707b14c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:23:10 +0000 Subject: [PATCH 292/327] Bump cachetools from 5.3.2 to 5.3.3 Bumps [cachetools](https://github.com/tkem/cachetools) from 5.3.2 to 5.3.3. - [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tkem/cachetools/compare/v5.3.2...v5.3.3) --- updated-dependencies: - dependency-name: cachetools dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0477751cd..e94313984 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ python-telegram-bot==20.8 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.24.1 arrow==1.3.0 -cachetools==5.3.2 +cachetools==5.3.3 requests==2.31.0 urllib3==2.2.1 jsonschema==4.21.1 From d6ae63ac484cefccbef885219000eefa8326a7da Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:23:16 +0000 Subject: [PATCH 293/327] Bump python-rapidjson from 1.14 to 1.16 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 1.14 to 1.16. - [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst) - [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v1.14...v1.16) --- updated-dependencies: - dependency-name: python-rapidjson dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0477751cd..86deabc3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ pyarrow==15.0.0; platform_machine != 'armv7l' py_find_1st==1.1.6 # Load ticker files 30% faster -python-rapidjson==1.14 +python-rapidjson==1.16 # Properly format api responses orjson==3.9.15 From 8c0ba2a69aff9acac58669880b23e98c08a22cae Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 03:23:27 +0000 Subject: [PATCH 294/327] Bump ccxt from 4.2.51 to 4.2.58 Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.2.51 to 4.2.58. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/4.2.51...4.2.58) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0477751cd..8e862275a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.26.4 pandas==2.2.1 pandas-ta==0.3.14b -ccxt==4.2.51 +ccxt==4.2.58 cryptography==42.0.5 aiohttp==3.9.3 SQLAlchemy==2.0.27 From 9ad60643f5d7df7c40585f870fd244462e824172 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 05:46:23 +0000 Subject: [PATCH 295/327] Bump ruff from 0.2.2 to 0.3.0 Bumps [ruff](https://github.com/astral-sh/ruff) from 0.2.2 to 0.3.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/v0.2.2...v0.3.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 31c8a4f8c..562841375 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.2.2 +ruff==0.3.0 mypy==1.8.0 pre-commit==3.6.2 pytest==8.1.0 From 99c8be4c30e931d64bc96f3f471daa953aff2e93 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 07:59:10 +0000 Subject: [PATCH 296/327] Bump pydantic from 2.6.2 to 2.6.3 Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.6.2 to 2.6.3. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v2.6.2...v2.6.3) --- updated-dependencies: - dependency-name: pydantic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 36ca1b76f..44e8e2ccb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,7 +37,7 @@ sdnotify==0.3.2 # API Server fastapi==0.110.0 -pydantic==2.6.2 +pydantic==2.6.3 uvicorn==0.27.1 pyjwt==2.8.0 aiofiles==23.2.1 From 510863f939b30152f5f77292cc95f65f179395ad Mon Sep 17 00:00:00 2001 From: xmatthias <5024695+xmatthias@users.noreply.github.com> Date: Tue, 5 Mar 2024 03:03:38 +0000 Subject: [PATCH 297/327] chore: update pre-commit hooks --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 842c87976..23fa54326 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,7 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit # Ruff version. - rev: 'v0.2.2' + rev: 'v0.3.0' hooks: - id: ruff From 3e6e534e76ac313f5d22d828bca88097cdb11f6d Mon Sep 17 00:00:00 2001 From: cuinix <915115094@qq.com> Date: Thu, 7 Mar 2024 13:57:25 +0800 Subject: [PATCH 298/327] fix some typos in docs Signed-off-by: cuinix <915115094@qq.com> --- docs/advanced-backtesting.md | 4 ++-- docs/freqai-parameter-table.md | 2 +- docs/freqai-reinforcement-learning.md | 2 +- docs/telegram-usage.md | 2 +- docs/webhook-config.md | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/advanced-backtesting.md b/docs/advanced-backtesting.md index 3926fb5b1..e91842d64 100644 --- a/docs/advanced-backtesting.md +++ b/docs/advanced-backtesting.md @@ -109,12 +109,12 @@ automatically accessible by including them on the indicator-list, and these incl - **open_date :** trade open datetime - **close_date :** trade close datetime - **min_rate :** minimum price seen throughout the position -- **max_rate :** maxiumum price seen throughout the position +- **max_rate :** maximum price seen throughout the position - **open :** signal candle open price - **close :** signal candle close price - **high :** signal candle high price - **low :** signal candle low price -- **volume :** signal candle volumne +- **volume :** signal candle volume - **profit_ratio :** trade profit ratio - **profit_abs :** absolute profit return of the trade diff --git a/docs/freqai-parameter-table.md b/docs/freqai-parameter-table.md index 905ea479a..055b7b45d 100644 --- a/docs/freqai-parameter-table.md +++ b/docs/freqai-parameter-table.md @@ -75,7 +75,7 @@ Mandatory parameters are marked as **Required** and have to be set in one of the | `rl_config` | A dictionary containing the control parameters for a Reinforcement Learning model.
**Datatype:** Dictionary. | `train_cycles` | Training time steps will be set based on the `train_cycles * number of training data points.
**Datatype:** Integer. | `max_trade_duration_candles`| Guides the agent training to keep trades below desired length. Example usage shown in `prediction_models/ReinforcementLearner.py` within the customizable `calculate_reward()` function.
**Datatype:** int. -| `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentaiton. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website)
**Datatype:** string. +| `model_type` | Model string from stable_baselines3 or SBcontrib. Available strings include: `'TRPO', 'ARS', 'RecurrentPPO', 'MaskablePPO', 'PPO', 'A2C', 'DQN'`. User should ensure that `model_training_parameters` match those available to the corresponding stable_baselines3 model by visiting their documentation. [PPO doc](https://stable-baselines3.readthedocs.io/en/master/modules/ppo.html) (external website)
**Datatype:** string. | `policy_type` | One of the available policy types from stable_baselines3
**Datatype:** string. | `max_training_drawdown_pct` | The maximum drawdown that the agent is allowed to experience during training.
**Datatype:** float.
Default: 0.8 | `cpu_count` | Number of threads/cpus to dedicate to the Reinforcement Learning training process (depending on if `ReinforcementLearning_multiproc` is selected or not). Recommended to leave this untouched, by default, this value is set to the total number of physical cores minus 1.
**Datatype:** int. diff --git a/docs/freqai-reinforcement-learning.md b/docs/freqai-reinforcement-learning.md index c5cda3bc3..3b75e6b71 100644 --- a/docs/freqai-reinforcement-learning.md +++ b/docs/freqai-reinforcement-learning.md @@ -142,7 +142,7 @@ Parameter details can be found [here](freqai-parameter-table.md), but in general As you begin to modify the strategy and the prediction model, you will quickly realize some important differences between the Reinforcement Learner and the Regressors/Classifiers. Firstly, the strategy does not set a target value (no labels!). Instead, you set the `calculate_reward()` function inside the `MyRLEnv` class (see below). A default `calculate_reward()` is provided inside `prediction_models/ReinforcementLearner.py` to demonstrate the necessary building blocks for creating rewards, but this is *not* designed for production. Users *must* create their own custom reinforcement learning model class or use a pre-built one from outside the Freqtrade source code and save it to `user_data/freqaimodels`. It is inside the `calculate_reward()` where creative theories about the market can be expressed. For example, you can reward your agent when it makes a winning trade, and penalize the agent when it makes a losing trade. Or perhaps, you wish to reward the agent for entering trades, and penalize the agent for sitting in trades too long. Below we show examples of how these rewards are all calculated: !!! note "Hint" - The best reward functions are ones that are continuously differentiable, and well scaled. In other words, adding a single large negative penalty to a rare event is not a good idea, and the neural net will not be able to learn that function. Instead, it is better to add a small negative penalty to a common event. This will help the agent learn faster. Not only this, but you can help improve the continuity of your rewards/penalties by having them scale with severity according to some linear/exponential functions. In other words, you'd slowly scale the penalty as the duration of the trade increases. This is better than a single large penalty occuring at a single point in time. + The best reward functions are ones that are continuously differentiable, and well scaled. In other words, adding a single large negative penalty to a rare event is not a good idea, and the neural net will not be able to learn that function. Instead, it is better to add a small negative penalty to a common event. This will help the agent learn faster. Not only this, but you can help improve the continuity of your rewards/penalties by having them scale with severity according to some linear/exponential functions. In other words, you'd slowly scale the penalty as the duration of the trade increases. This is better than a single large penalty occurring at a single point in time. ```python from freqtrade.freqai.prediction_models.ReinforcementLearner import ReinforcementLearner diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index e4dc02c76..76023ad4e 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -59,7 +59,7 @@ For the Freqtrade configuration, you can then use the the full value (including "chat_id": "-1001332619709" ``` !!! Warning "Using telegram groups" - When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasent surprises. + When using telegram groups, you're giving every member of the telegram group access to your freqtrade bot and to all commands possible via telegram. Please make sure that you can trust everyone in the telegram group to avoid unpleasant surprises. ## Control telegram noise diff --git a/docs/webhook-config.md b/docs/webhook-config.md index b4044655c..9125ff361 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -65,7 +65,7 @@ You can set the POST body format to Form-Encoded (default), JSON-Encoded, or raw The result would be a POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel. -When using the Form-Encoded or JSON-Encoded configuration you can configure any number of payload values, and both the key and value will be ouput in the POST request. However, when using the raw data format you can only configure one value and it **must** be named `"data"`. In this instance the data key will not be output in the POST request, only the value. For example: +When using the Form-Encoded or JSON-Encoded configuration you can configure any number of payload values, and both the key and value will be output in the POST request. However, when using the raw data format you can only configure one value and it **must** be named `"data"`. In this instance the data key will not be output in the POST request, only the value. For example: ```json "webhook": { From 865ebc314326cf32af48472d0603416814771f89 Mon Sep 17 00:00:00 2001 From: Alberto Date: Thu, 7 Mar 2024 17:05:13 +0000 Subject: [PATCH 299/327] update status table to show total amounts in stake currency Signed-off-by: Alberto --- freqtrade/rpc/rpc.py | 6 ++++++ tests/rpc/test_rpc.py | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 6e8447d29..7642d1697 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -291,6 +291,10 @@ class RPC: profit_str += f" ({fiat_profit:.2f})" fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \ else fiat_profit_sum + fiat_profit + else: + profit_str += f" ({trade_profit:.2f})" + fiat_profit_sum = trade_profit if isnan(fiat_profit_sum) \ + else fiat_profit_sum + trade_profit active_attempt_side_symbols = [ '*' if (oo and oo.ft_order_side == trade.entry_side) else '**' @@ -317,6 +321,8 @@ class RPC: profitcol = "Profit" if self._fiat_converter: profitcol += " (" + fiat_display_currency + ")" + else: + profitcol += " (" + stake_currency + ")" columns = [ 'ID L/S' if nonspot else 'ID', diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 85b105892..bc1fc6227 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -223,8 +223,8 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert "Pair" in headers assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] - assert '0.00' == result[0][3] - assert isnan(fiat_profit_sum) + assert '0.00 (0.00)' == result[0][3] + assert '0.00' == f'{fiat_profit_sum:.2f}' mocker.patch(f'{EXMS}._dry_is_price_crossed', return_value=True) freqtradebot.process() @@ -234,8 +234,8 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert "Pair" in headers assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] - assert '-0.41%' == result[0][3] - assert isnan(fiat_profit_sum) + assert '-0.41% (-0.00)' == result[0][3] + assert '-0.00' == f'{fiat_profit_sum:.2f}' # Test with fiat convert rpc._fiat_converter = CryptoToFiatConverter() From b690325f2260793b551f9e1c30acf721be2e00be Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 8 Mar 2024 06:39:34 +0100 Subject: [PATCH 300/327] Remove typo in change-dir notebook closes #9916 --- docs/strategy_analysis_example.md | 2 +- freqtrade/templates/strategy_analysis_example.ipynb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 846c53238..22828b899 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -19,7 +19,7 @@ from pathlib import Path project_root = "somedir/freqtrade" i=0 try: - os.chdirdir(project_root) + os.chdir(project_root) assert Path('LICENSE').is_file() except: while i<4 and (not Path('LICENSE').is_file()): diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 0b30dbd54..8d4459a3c 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -35,7 +35,7 @@ "project_root = \"somedir/freqtrade\"\n", "i=0\n", "try:\n", - " os.chdirdir(project_root)\n", + " os.chdir(project_root)\n", " assert Path('LICENSE').is_file()\n", "except:\n", " while i<4 and (not Path('LICENSE').is_file()):\n", @@ -181,7 +181,7 @@ "\n", "# if backtest_dir points to a directory, it'll automatically load the last backtest file.\n", "backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n", - "# backtest_dir can also point to a specific file \n", + "# backtest_dir can also point to a specific file\n", "# backtest_dir = config[\"user_data_dir\"] / \"backtest_results/backtest-result-2020-07-01_20-04-22.json\"" ] }, From 2cfe9939517d1c7ea1bcee541f458acc70ea349d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 8 Mar 2024 07:10:41 +0100 Subject: [PATCH 301/327] Fix condition for min-stake in position-adjust mode closes #9915 --- freqtrade/freqtradebot.py | 2 +- freqtrade/rpc/rpc.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 974f8124e..8ad151108 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -962,7 +962,7 @@ class FreqtradeBot(LoggingMixin): # edge-case for now. min_stake_amount = self.exchange.get_min_pair_stake_amount( pair, enter_limit_requested, - self.strategy.stoploss if not mode != 'pos_adjust' else 0.0, + self.strategy.stoploss if not mode == 'pos_adjust' else 0.0, leverage) max_stake_amount = self.exchange.get_max_pair_stake_amount( pair, enter_limit_requested, leverage) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 47646923d..8d91fc92c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -927,6 +927,7 @@ class RPC: is_short=is_short, enter_tag=enter_tag, leverage_=leverage, + mode='pos_adjust' if trade else 'initial' ): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() From acbb485302aeacaf49a07809601d726cd453ab9e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 15:01:13 +0100 Subject: [PATCH 302/327] Add bot start and bot-startup to health endpoint --- freqtrade/rpc/api_server/api_schemas.py | 4 +++ freqtrade/rpc/rpc.py | 43 ++++++++++++++++++------- tests/rpc/test_rpc.py | 2 ++ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 3ea9ed4d0..af8d8ddf4 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -559,3 +559,7 @@ class SysInfo(BaseModel): class Health(BaseModel): last_process: Optional[datetime] = None last_process_ts: Optional[int] = None + bot_start: Optional[datetime] = None + bot_start_ts: Optional[int] = None + bot_startup: Optional[datetime] = None + bot_startup_ts: Optional[int] = None diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4487814c5..cd30d5be8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1366,19 +1366,40 @@ class RPC: def health(self) -> Dict[str, Optional[Union[str, int]]]: last_p = self._freqtrade.last_process - if last_p is None: - return { - "last_process": None, - "last_process_loc": None, - "last_process_ts": None, - } - - return { - "last_process": str(last_p), - "last_process_loc": format_date(last_p.astimezone(tzlocal())), - "last_process_ts": int(last_p.timestamp()), + res = { + "last_process": None, + "last_process_loc": None, + "last_process_ts": None, + "bot_start": None, + "bot_start_loc": None, + "bot_start_ts": None, + "bot_startup": None, + "bot_startup_loc": None, + "bot_startup_ts": None, } + if last_p is not None: + res.update({ + "last_process": str(last_p), + "last_process_loc": format_date(last_p.astimezone(tzlocal())), + "last_process_ts": int(last_p.timestamp()), + }) + + if (bot_start := KeyValueStore.get_datetime_value(KeyStoreKeys.BOT_START_TIME)): + res.update({ + "bot_start": str(bot_start), + "bot_start_loc": format_date(bot_start.astimezone(tzlocal())), + "bot_start_ts": int(bot_start.timestamp()), + }) + if (bot_startup := KeyValueStore.get_datetime_value(KeyStoreKeys.STARTUP_TIME)): + res.update({ + "bot_startup": str(bot_startup), + "bot_startup_loc": format_date(bot_startup.astimezone(tzlocal())), + "bot_startup_ts": int(bot_startup.timestamp()), + }) + + return res + def _update_market_direction(self, direction: MarketDirection) -> None: self._freqtrade.strategy.market_direction = direction diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index bc1fc6227..66a750f1f 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -10,6 +10,7 @@ from freqtrade.edge import PairInfo from freqtrade.enums import SignalDirection, State, TradingMode from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Order, Trade +from freqtrade.persistence.key_value_store import set_startup_time from freqtrade.persistence.pairlock_middleware import PairLocks from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -1298,6 +1299,7 @@ def test_rpc_health(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) freqtradebot = get_patched_freqtradebot(mocker, default_conf) + set_startup_time() rpc = RPC(freqtradebot) result = rpc.health() assert result['last_process'] is None From f8cbf138ee659238f727fc988b4fbcc414293d82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 15:47:01 +0100 Subject: [PATCH 303/327] Add Initial bot start and current bot start to /health telegram msg --- freqtrade/rpc/telegram.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f7e7362ef..2d59e1f16 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1704,7 +1704,9 @@ class Telegram(RPCHandler): Shows the last process timestamp """ health = self._rpc.health() - message = f"Last process: `{health['last_process_loc']}`" + message = f"Last process: `{health['last_process_loc']}`\n" + message += f"Initial bot Start: `{health['bot_start_loc']}`\n" + message += f"Current bot Start: `{health['bot_startup_loc']}`" await self._send_msg(message) @authorized_only From 1b608a162ef8cefd8c8e9803ef8d93bf4ca151ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 16:10:33 +0100 Subject: [PATCH 304/327] Add type-hint for result dictionary --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index cd30d5be8..8bb7f754f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1366,7 +1366,7 @@ class RPC: def health(self) -> Dict[str, Optional[Union[str, int]]]: last_p = self._freqtrade.last_process - res = { + res: Dict[str, Union[None, str, int]] = { "last_process": None, "last_process_loc": None, "last_process_ts": None, From 29f90cbd048352461728ff04c6870216d6e09078 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 17:34:06 +0100 Subject: [PATCH 305/327] Run CI on macos-14 (M1) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0587525e..bb1058afb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,7 +124,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ "macos-latest", "macos-13" ] + os: [ "macos-latest", "macos-13", "macos-14" ] python-version: ["3.9", "3.10", "3.11", "3.12"] steps: From 7cceddb3df169c3bbc598329c94d924d822de3b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 17:35:14 +0100 Subject: [PATCH 306/327] Improve wording on /health message --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2d59e1f16..f99149c01 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1705,8 +1705,8 @@ class Telegram(RPCHandler): """ health = self._rpc.health() message = f"Last process: `{health['last_process_loc']}`\n" - message += f"Initial bot Start: `{health['bot_start_loc']}`\n" - message += f"Current bot Start: `{health['bot_startup_loc']}`" + message += f"Initial bot start: `{health['bot_start_loc']}`\n" + message += f"Last bot restart: `{health['bot_startup_loc']}`" await self._send_msg(message) @authorized_only From 86db8883862aa7e6d44ec9d994ffcc1cd3f234c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 17:41:10 +0100 Subject: [PATCH 307/327] Install libomp from brew for macos closes #9874 --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 6bf85edab..68374a689 100755 --- a/setup.sh +++ b/setup.sh @@ -161,7 +161,7 @@ function install_macos() { /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" fi - brew install gettext + brew install gettext libomp #Gets number after decimal in python version version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g') From cc3b2000eb1f13dd22e1cfbb370d3993a65319ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 17:55:22 +0100 Subject: [PATCH 308/327] Avoid fully patching torch on M1 macs --- tests/freqai/conftest.py | 7 ++++++- tests/freqai/test_freqai_interface.py | 10 ++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/freqai/conftest.py b/tests/freqai/conftest.py index 81d72d92a..55f0296a3 100644 --- a/tests/freqai/conftest.py +++ b/tests/freqai/conftest.py @@ -25,10 +25,15 @@ def is_mac() -> bool: return "Darwin" in machine +def is_arm() -> bool: + machine = platform.machine() + return "arm" in machine or "aarch64" in machine + + @pytest.fixture(autouse=True) def patch_torch_initlogs(mocker) -> None: - if is_mac(): + if is_mac() and not is_arm(): # Mock torch import completely import sys import types diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 58648d97f..2a71e8af6 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -1,5 +1,4 @@ import logging -import platform import shutil from pathlib import Path from unittest.mock import MagicMock @@ -15,13 +14,8 @@ from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import Trade from freqtrade.plugins.pairlistmanager import PairListManager from tests.conftest import EXMS, create_mock_trades, get_patched_exchange, log_has_re -from tests.freqai.conftest import (get_patched_freqai_strategy, is_mac, is_py12, make_rl_config, - mock_pytorch_mlp_model_training_parameters) - - -def is_arm() -> bool: - machine = platform.machine() - return "arm" in machine or "aarch64" in machine +from tests.freqai.conftest import (get_patched_freqai_strategy, is_arm, is_mac, is_py12, + make_rl_config, mock_pytorch_mlp_model_training_parameters) def can_run_model(model: str) -> None: From 971a81e15d5191a9b72a7e681bde3258688f7ffe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 18:15:47 +0100 Subject: [PATCH 309/327] Bump catboost to 1.2.3, remove 3.12 restriction --- requirements-freqai.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-freqai.txt b/requirements-freqai.txt index 0532562da..31366efa7 100644 --- a/requirements-freqai.txt +++ b/requirements-freqai.txt @@ -5,7 +5,7 @@ # Required for freqai scikit-learn==1.4.1.post1 joblib==1.3.2 -catboost==1.2.2; 'arm' not in platform_machine and python_version < '3.12' +catboost==1.2.3; 'arm' not in platform_machine lightgbm==4.3.0 xgboost==2.0.3 tensorboard==2.16.2 From c5f2a69d9c9836b2cf905a70fbccd5dc08c69eee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 18:16:22 +0100 Subject: [PATCH 310/327] Allow running catboost tests on 3.12 --- tests/freqai/test_freqai_interface.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 58648d97f..cceda8e8e 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -27,9 +27,6 @@ def is_arm() -> bool: def can_run_model(model: str) -> None: is_pytorch_model = 'Reinforcement' in model or 'PyTorch' in model - if is_py12() and ("Catboost" in model or is_pytorch_model): - pytest.skip("Model not supported on python 3.12 yet.") - if is_arm() and "Catboost" in model: pytest.skip("CatBoost is not supported on ARM.") From edc74ae2e460bb2c6bf051c11cb4a58d122d90d2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 18:23:35 +0100 Subject: [PATCH 311/327] Split macos Installation into 2 separate actions --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb1058afb..fc3886b32 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,7 @@ jobs: run: | cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. - - name: Installation - macOS + - name: Installation - macOS (Brew) run: | # brew update # TODO: Should be the brew upgrade @@ -177,6 +177,9 @@ jobs: rm /usr/local/bin/python3.12-config || true brew install hdf5 c-blosc libomp + + - name: Installation (python) + run: | python -m pip install --upgrade pip wheel export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH export TA_LIBRARY_PATH=${HOME}/dependencies/lib From cb1f49e81ce142f830f5b0dd0f359a0171fea4b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 19:25:17 +0100 Subject: [PATCH 312/327] Don't run torch models on 3.12 yet --- tests/freqai/test_freqai_interface.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index cceda8e8e..e3c286fcd 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -27,6 +27,9 @@ def is_arm() -> bool: def can_run_model(model: str) -> None: is_pytorch_model = 'Reinforcement' in model or 'PyTorch' in model + if is_py12() and is_pytorch_model: + pytest.skip("Model not supported on python 3.12 yet.") + if is_arm() and "Catboost" in model: pytest.skip("CatBoost is not supported on ARM.") From 0bd50a6e2479e68caeed95f269f3a80447b65f75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 19:44:40 +0100 Subject: [PATCH 313/327] Don't disable tensorboard on mac ARM --- tests/freqai/test_freqai_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/freqai/test_freqai_interface.py b/tests/freqai/test_freqai_interface.py index 2a71e8af6..fb7af5853 100644 --- a/tests/freqai/test_freqai_interface.py +++ b/tests/freqai/test_freqai_interface.py @@ -237,7 +237,7 @@ def test_extract_data_and_train_model_Classifiers(mocker, freqai_conf, model): def test_start_backtesting(mocker, freqai_conf, model, num_files, strat, caplog): can_run_model(model) test_tb = True - if is_mac(): + if is_mac() and not is_arm(): test_tb = False freqai_conf.get("freqai", {}).update({"save_backtest_models": True}) From 4e94178169f910c7a665f689d3a625f7d7b43449 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Mar 2024 20:10:40 +0100 Subject: [PATCH 314/327] exclude python 3.9 on Macos 14 --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc3886b32..44f489346 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,6 +126,9 @@ jobs: matrix: os: [ "macos-latest", "macos-13", "macos-14" ] python-version: ["3.9", "3.10", "3.11", "3.12"] + exclude: + - os: "macos-14" + python-version: "3.9" steps: - uses: actions/checkout@v4 From 518b6eb56577385c3b358e35c9e714a4f7d18451 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 10 Mar 2024 19:31:43 +0100 Subject: [PATCH 315/327] use dt_ts to simplify exchange date math --- freqtrade/exchange/exchange.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d17b442ab..482ac598f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -659,7 +659,7 @@ class Exchange: candle_limit = self.ohlcv_candle_limit( timeframe, self._config['candle_type_def'], - int(date_minus_candles(timeframe, startup_candles).timestamp() * 1000) + dt_ts(date_minus_candles(timeframe, startup_candles)) if timeframe else None) # Require one more candle - to account for the still open candle. candle_count = startup_candles + 1 @@ -2043,7 +2043,7 @@ class Exchange: timeframe, candle_type, since_ms) move_to = one_call * self.required_candle_call_count now = timeframe_to_next_date(timeframe) - since_ms = int((now - timedelta(seconds=move_to // 1000)).timestamp() * 1000) + since_ms = dt_ts(now - timedelta(seconds=move_to // 1000)) if since_ms: return self._async_get_historic_ohlcv( @@ -2503,7 +2503,7 @@ class Exchange: ) if type(since) is datetime: - since = int(since.timestamp()) * 1000 # * 1000 for ms + since = dt_ts(since) try: funding_history = self._api.fetch_funding_history( @@ -2833,7 +2833,7 @@ class Exchange: if not close_date: close_date = datetime.now(timezone.utc) - since_ms = int(timeframe_to_prev_date(timeframe, open_date).timestamp()) * 1000 + since_ms = dt_ts(timeframe_to_prev_date(timeframe, open_date)) mark_comb: PairWithTimeframe = (pair, timeframe, mark_price_type) funding_comb: PairWithTimeframe = (pair, timeframe_ff, CandleType.FUNDING_RATE) From 60b9d9448ac925b4ca7b38d58e8cbe8d094ccab8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:37:19 +0000 Subject: [PATCH 316/327] Bump the types group with 2 updates Bumps the types group with 2 updates: [types-requests](https://github.com/python/typeshed) and [types-python-dateutil](https://github.com/python/typeshed). Updates `types-requests` from 2.31.0.20240218 to 2.31.0.20240311 - [Commits](https://github.com/python/typeshed/commits) Updates `types-python-dateutil` from 2.8.19.20240106 to 2.8.19.20240311 - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch dependency-group: types - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch dependency-group: types ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 562841375..b7d2ae079 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,6 +26,6 @@ nbconvert==7.16.1 # mypy types types-cachetools==5.3.0.7 types-filelock==3.2.7 -types-requests==2.31.0.20240218 +types-requests==2.31.0.20240311 types-tabulate==0.9.0.20240106 -types-python-dateutil==2.8.19.20240106 +types-python-dateutil==2.8.19.20240311 From 80560a389c0a3cfd2f58226085c1453a1eac3634 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:39:48 +0000 Subject: [PATCH 317/327] Bump mypy from 1.8.0 to 1.9.0 Bumps [mypy](https://github.com/python/mypy) from 1.8.0 to 1.9.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.8.0...1.9.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 562841375..455ce8ccd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ coveralls==3.3.1 ruff==0.3.0 -mypy==1.8.0 +mypy==1.9.0 pre-commit==3.6.2 pytest==8.1.0 pytest-asyncio==0.23.5 From 6d2f454d8cccefadad204e42a35219f0f4d28cfc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:40:01 +0000 Subject: [PATCH 318/327] Bump pymdown-extensions from 10.7 to 10.7.1 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 10.7 to 10.7.1. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/10.7...10.7.1) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 55a2c11aa..be276085e 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -2,5 +2,5 @@ markdown==3.5.2 mkdocs==1.5.3 mkdocs-material==9.5.12 mdx_truly_sane_lists==1.3 -pymdown-extensions==10.7 +pymdown-extensions==10.7.1 jinja2==3.1.3 From e1fdb8dec9bb5e6584bd0010880dbefa97739632 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:40:06 +0000 Subject: [PATCH 319/327] Bump nbconvert from 7.16.1 to 7.16.2 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 7.16.1 to 7.16.2. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Changelog](https://github.com/jupyter/nbconvert/blob/main/CHANGELOG.md) - [Commits](https://github.com/jupyter/nbconvert/compare/v7.16.1...v7.16.2) --- updated-dependencies: - dependency-name: nbconvert dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 562841375..3e56e6633 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,7 +21,7 @@ isort==5.13.2 time-machine==2.14.0 # Convert jupyter notebooks to markdown documents -nbconvert==7.16.1 +nbconvert==7.16.2 # mypy types types-cachetools==5.3.0.7 From ab6a5d75bcab19b6d87c7d40cb22bb5ce2aafed6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:40:15 +0000 Subject: [PATCH 320/327] Bump python-telegram-bot from 20.8 to 21.0.1 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 20.8 to 21.0.1. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v20.8...v21.0.1) --- updated-dependencies: - dependency-name: python-telegram-bot dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 44e8e2ccb..1a7ce1f71 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ ccxt==4.2.58 cryptography==42.0.5 aiohttp==3.9.3 SQLAlchemy==2.0.27 -python-telegram-bot==20.8 +python-telegram-bot==21.0.1 # can't be hard-pinned due to telegram-bot pinning httpx with ~ httpx>=0.24.1 arrow==1.3.0 From 9b44d1d8cb0a8dcdd9951428073221bc0012b070 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:40:29 +0000 Subject: [PATCH 321/327] Bump packaging from 23.2 to 24.0 Bumps [packaging](https://github.com/pypa/packaging) from 23.2 to 24.0. - [Release notes](https://github.com/pypa/packaging/releases) - [Changelog](https://github.com/pypa/packaging/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pypa/packaging/compare/23.2...24.0) --- updated-dependencies: - dependency-name: packaging dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 44e8e2ccb..c39d5b254 100644 --- a/requirements.txt +++ b/requirements.txt @@ -60,4 +60,4 @@ websockets==12.0 janus==1.0.0 ast-comments==1.2.1 -packaging==23.2 +packaging==24.0 From 23d226d372e5d3807675b1343ea19c35323655b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 03:46:55 +0000 Subject: [PATCH 322/327] Bump pypa/gh-action-pypi-publish from 1.8.12 to 1.8.14 Bumps [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) from 1.8.12 to 1.8.14. - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/v1.8.12...v1.8.14) --- updated-dependencies: - dependency-name: pypa/gh-action-pypi-publish dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44f489346..a268acf80 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -488,12 +488,12 @@ jobs: path: dist - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.8.12 + uses: pypa/gh-action-pypi-publish@v1.8.14 with: repository-url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.8.12 + uses: pypa/gh-action-pypi-publish@v1.8.14 deploy-docker: From c78480c4941878c5aaa00a848d557a08a432665f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Mar 2024 06:28:11 +0100 Subject: [PATCH 323/327] Bump types in pre-commit file --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 23fa54326..facc774f3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,9 +16,9 @@ repos: additional_dependencies: - types-cachetools==5.3.0.7 - types-filelock==3.2.7 - - types-requests==2.31.0.20240218 + - types-requests==2.31.0.20240311 - types-tabulate==0.9.0.20240106 - - types-python-dateutil==2.8.19.20240106 + - types-python-dateutil==2.8.19.20240311 - SQLAlchemy==2.0.27 # stages: [push] From 1c91675c5840a442bad137d3d49548d343c1e075 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 06:14:04 +0000 Subject: [PATCH 324/327] Bump the pytest group with 2 updates Bumps the pytest group with 2 updates: [pytest](https://github.com/pytest-dev/pytest) and [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio). Updates `pytest` from 8.1.0 to 8.1.1 - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.1.0...8.1.1) Updates `pytest-asyncio` from 0.23.5 to 0.23.5.post1 - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.23.5...v0.23.5.post1) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:development update-type: version-update:semver-patch dependency-group: pytest - dependency-name: pytest-asyncio dependency-type: direct:development update-type: version-update:semver-patch dependency-group: pytest ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 455ce8ccd..dd41a80db 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,8 +10,8 @@ coveralls==3.3.1 ruff==0.3.0 mypy==1.9.0 pre-commit==3.6.2 -pytest==8.1.0 -pytest-asyncio==0.23.5 +pytest==8.1.1 +pytest-asyncio==0.23.5.post1 pytest-cov==4.1.0 pytest-mock==3.12.0 pytest-random-order==1.1.1 From 33556f3c2c5343e7bb06fe148b0605fd4bb6ed30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 07:11:44 +0000 Subject: [PATCH 325/327] Bump mkdocs-material from 9.5.12 to 9.5.13 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 9.5.12 to 9.5.13. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/9.5.12...9.5.13) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index be276085e..33d58fdbb 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.5.2 mkdocs==1.5.3 -mkdocs-material==9.5.12 +mkdocs-material==9.5.13 mdx_truly_sane_lists==1.3 pymdown-extensions==10.7.1 jinja2==3.1.3 From 018d10b3461acb2708d4ff84380d42133d390142 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 07:33:37 +0000 Subject: [PATCH 326/327] Bump ccxt from 4.2.58 to 4.2.66 Bumps [ccxt](https://github.com/ccxt/ccxt) from 4.2.58 to 4.2.66. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/4.2.58...4.2.66) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1a7ce1f71..d9528f7ec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.26.4 pandas==2.2.1 pandas-ta==0.3.14b -ccxt==4.2.58 +ccxt==4.2.66 cryptography==42.0.5 aiohttp==3.9.3 SQLAlchemy==2.0.27 From 2a8c6a6d0eedd0068b44b61fbd9531bf67419a5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:21:40 +0000 Subject: [PATCH 327/327] Bump uvicorn from 0.27.1 to 0.28.0 Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.27.1 to 0.28.0. - [Release notes](https://github.com/encode/uvicorn/releases) - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md) - [Commits](https://github.com/encode/uvicorn/compare/0.27.1...0.28.0) --- updated-dependencies: - dependency-name: uvicorn dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8cecda963..dcc946696 100644 --- a/requirements.txt +++ b/requirements.txt @@ -38,7 +38,7 @@ sdnotify==0.3.2 # API Server fastapi==0.110.0 pydantic==2.6.3 -uvicorn==0.27.1 +uvicorn==0.28.0 pyjwt==2.8.0 aiofiles==23.2.1 psutil==5.9.8