From b7891485b35d52223793cab10d9fd3859f0f2726 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 3 Aug 2021 12:55:22 -0600 Subject: [PATCH 01/59] Created FundingFee class and added funding_fee to LocalTrade and freqtradebot --- freqtrade/freqtradebot.py | 19 +++++- freqtrade/leverage/__init__.py | 1 - freqtrade/leverage/funding_fee.py | 80 ++++++++++++++++++++++++ freqtrade/persistence/migrations.py | 20 ++++-- freqtrade/persistence/models.py | 94 ++++++++++++++++++++++------- requirements.txt | 3 + tests/leverage/test_leverage.py | 2 +- tests/rpc/test_rpc.py | 16 +++-- tests/test_persistence.py | 25 +++++++- 9 files changed, 223 insertions(+), 37 deletions(-) create mode 100644 freqtrade/leverage/funding_fee.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 53ca2764b..4659a634c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,10 +16,11 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.enums import RPCMessageType, SellType, State +from freqtrade.enums import RPCMessageType, SellType, State, TradingMode from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds +from freqtrade.leverage.funding_fee import FundingFee from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db @@ -102,6 +103,11 @@ class FreqtradeBot(LoggingMixin): self._sell_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) + self.trading_mode = TradingMode.SPOT + if self.trading_mode == TradingMode.FUTURES: + self.funding_fee = FundingFee() + self.funding_fee.start() + def notify_status(self, msg: str) -> None: """ Public method for users of this class (worker, etc.) to send notifications @@ -559,6 +565,10 @@ class FreqtradeBot(LoggingMixin): amount = safe_value_fallback(order, 'filled', 'amount') buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') + funding_fee = (self.funding_fee.initial_funding_fee(amount) + if self.trading_mode == TradingMode.FUTURES + else None) + # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade( @@ -576,10 +586,15 @@ class FreqtradeBot(LoggingMixin): open_order_id=order_id, strategy=self.strategy.get_strategy_name(), buy_tag=buy_tag, - timeframe=timeframe_to_minutes(self.config['timeframe']) + timeframe=timeframe_to_minutes(self.config['timeframe']), + funding_fee=funding_fee, + trading_mode=self.trading_mode ) trade.orders.append(order_obj) + if self.trading_mode == TradingMode.FUTURES: + self.funding_fee.add_new_trade(trade) + # Update fees if order is closed if order_status == 'closed': self.update_trade_state(trade, order_id, order) diff --git a/freqtrade/leverage/__init__.py b/freqtrade/leverage/__init__.py index ae78f4722..9186b160e 100644 --- a/freqtrade/leverage/__init__.py +++ b/freqtrade/leverage/__init__.py @@ -1,2 +1 @@ # flake8: noqa: F401 -from freqtrade.leverage.interest import interest diff --git a/freqtrade/leverage/funding_fee.py b/freqtrade/leverage/funding_fee.py new file mode 100644 index 000000000..738fa1344 --- /dev/null +++ b/freqtrade/leverage/funding_fee.py @@ -0,0 +1,80 @@ +from datetime import datetime, timedelta +from typing import List + +import schedule + +from freqtrade.persistence import Trade + + +class FundingFee: + + trades: List[Trade] + # Binance + begin_times = [ + # TODO-lev: Make these UTC time + "23:59:45", + "07:59:45", + "15:59:45", + ] + + # FTX + # begin_times = every hour + + def _is_time_between(self, begin_time, end_time): + # If check time is not given, default to current UTC time + check_time = datetime.utcnow().time() + if begin_time < end_time: + return check_time >= begin_time and check_time <= end_time + else: # crosses midnight + return check_time >= begin_time or check_time <= end_time + + def _apply_funding_fees(self, num_of: int = 1): + if num_of == 0: + return + for trade in self.trades: + trade.adjust_funding_fee(self._calculate(trade.amount) * num_of) + + def _calculate(self, amount): + # TODO-futures: implement + # TODO-futures: Check how other exchages do it and adjust accordingly + # https://www.binance.com/en/support/faq/360033525031 + # mark_price = + # contract_size = maybe trade.amount + # funding_rate = # https://www.binance.com/en/futures/funding-history/0 + # nominal_value = mark_price * contract_size + # adjustment = nominal_value * funding_rate + # return adjustment + + # FTX - paid in USD(always) + # position size * TWAP of((future - index) / index) / 24 + # https: // help.ftx.com/hc/en-us/articles/360027946571-Funding + return + + def initial_funding_fee(self, amount) -> float: + # A funding fee interval is applied immediately if within 30s of an iterval + # May only exist on binance + for begin_string in self.begin_times: + begin_time = datetime.strptime(begin_string, "%H:%M:%S") + end_time = (begin_time + timedelta(seconds=30)) + if self._is_time_between(begin_time.time(), end_time.time()): + return self._calculate(amount) + return 0.0 + + def start(self): + for interval in self.begin_times: + schedule.every().day.at(interval).do(self._apply_funding_fees()) + + # https://stackoverflow.com/a/30393162/6331353 + # TODO-futures: Put schedule.run_pending() somewhere in the bot_loop + + def reboot(self): + # TODO-futures Find out how many begin_times have passed since last funding_fee added + amount_missed = 0 + self.apply_funding_fees(num_of=amount_missed) + self.start() + + def add_new_trade(self, trade): + self.trades.append(trade) + + def remove_trade(self, trade): + self.trades.remove(trade) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index c81a4156c..f4deef45b 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -49,11 +49,21 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col strategy = get_column_def(cols, 'strategy', 'null') buy_tag = get_column_def(cols, 'buy_tag', 'null') + trading_mode = get_column_def(cols, 'trading_mode', 'null') + + # Leverage Properties leverage = get_column_def(cols, 'leverage', '1.0') - interest_rate = get_column_def(cols, 'interest_rate', '0.0') isolated_liq = get_column_def(cols, 'isolated_liq', 'null') # sqlite does not support literals for booleans is_short = get_column_def(cols, 'is_short', '0') + + # Margin Properties + interest_rate = get_column_def(cols, 'interest_rate', '0.0') + + # Futures properties + funding_fee = get_column_def(cols, 'funding_fee', '0.0') + last_funding_adjustment = get_column_def(cols, 'last_funding_adjustment', 'null') + # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') @@ -91,7 +101,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, timeframe, open_trade_value, close_profit_abs, - leverage, interest_rate, isolated_liq, is_short + trading_mode, leverage, isolated_liq, is_short, + interest_rate, funding_fee, last_funding_adjustment ) select id, lower(exchange), pair, is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost, @@ -108,8 +119,9 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {sell_order_status} sell_order_status, {strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, - {leverage} leverage, {interest_rate} interest_rate, - {isolated_liq} isolated_liq, {is_short} is_short + {trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq, + {is_short} is_short, {interest_rate} interest_rate, + {funding_fee} funding_fee, {last_funding_adjustment} last_funding_adjustment from {table_back_name} """)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b73611c1b..72d2fafc9 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,11 +2,11 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from decimal import Decimal from typing import Any, Dict, List, Optional -from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, +from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker @@ -14,9 +14,9 @@ from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES -from freqtrade.enums import SellType +from freqtrade.enums import SellType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.leverage import interest +from freqtrade.leverage.interest import interest from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -57,7 +57,7 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: f"is no valid database URL! (See {_SQL_DOCS_URL})") # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope - # Scoped sessions proxy requests to the appropriate thread-local session. + # Scoped sessions proxy reque sts to the appropriate thread-local session. # We should use the scoped_session object - not a seperately initialized version Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True)) Trade.query = Trade._session.query_property() @@ -93,6 +93,12 @@ def clean_dry_run_db() -> None: Trade.commit() +def hour_rounder(t): + # Rounds to nearest hour by adding a timedelta hour if minute >= 30 + return ( + t.replace(second=0, microsecond=0, minute=0, hour=t.hour) + timedelta(hours=t.minute//30)) + + class Order(_DECL_BASE): """ Order database model @@ -265,14 +271,20 @@ class LocalTrade(): buy_tag: Optional[str] = None timeframe: Optional[int] = None + trading_mode: TradingMode = TradingMode.SPOT + # Leverage trading properties - is_short: bool = False isolated_liq: Optional[float] = None + is_short: bool = False leverage: float = 1.0 # Margin trading properties interest_rate: float = 0.0 + # Futures properties + funding_fee: Optional[float] = None + last_funding_adjustment: Optional[datetime] = None + @property def has_no_leverage(self) -> bool: """Returns true if this is a non-leverage, non-short trade""" @@ -438,7 +450,10 @@ class LocalTrade(): 'interest_rate': self.interest_rate, 'isolated_liq': self.isolated_liq, 'is_short': self.is_short, - + 'trading_mode': self.trading_mode, + 'funding_fee': self.funding_fee, + 'last_funding_adjustment': (self.last_funding_adjustment.strftime(DATETIME_PRINT_FORMAT) + if self.last_funding_adjustment else None), 'open_order_id': self.open_order_id, } @@ -516,6 +531,10 @@ class LocalTrade(): f"Trailing stoploss saved us: " f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") + def adjust_funding_fee(self, adjustment): + self.funding_fee = self.funding_fee + adjustment + self.last_funding_adjustment = datetime.utcnow() + def update(self, order: Dict) -> None: """ Updates this entity with amount and actual open/close rates. @@ -654,8 +673,20 @@ class LocalTrade(): rate = Decimal(interest_rate or self.interest_rate) borrowed = Decimal(self.borrowed) + # TODO-lev: Pass trading mode to interest maybe return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours) + def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None, + fee: Optional[float] = None) -> Decimal: + + close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore + fees = close_trade * Decimal(fee or self.fee_close) + + if self.is_short: + return close_trade + fees + else: + return close_trade - fees + def calc_close_trade_value(self, rate: Optional[float] = None, fee: Optional[float] = None, interest_rate: Optional[float] = None) -> float: @@ -672,20 +703,32 @@ class LocalTrade(): if rate is None and not self.close_rate: return 0.0 - interest = self.calculate_interest(interest_rate) - if self.is_short: - amount = Decimal(self.amount) + Decimal(interest) - else: - # Currency already owned for longs, no need to purchase - amount = Decimal(self.amount) + amount = Decimal(self.amount) + trading_mode = self.trading_mode or TradingMode.SPOT - close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore - fees = close_trade * Decimal(fee or self.fee_close) + if trading_mode == TradingMode.SPOT: + return float(self._calc_base_close(amount, rate, fee)) - if self.is_short: - return float(close_trade + fees) + elif (trading_mode == TradingMode.MARGIN): + + total_interest = self.calculate_interest(interest_rate) + + if self.is_short: + amount = amount + total_interest + return float(self._calc_base_close(amount, rate, fee)) + else: + # Currency already owned for longs, no need to purchase + return float(self._calc_base_close(amount, rate, fee) - total_interest) + + elif (trading_mode == TradingMode.FUTURES): + funding_fee = self.funding_fee or 0.0 + if self.is_short: + return float(self._calc_base_close(amount, rate, fee)) + funding_fee + else: + return float(self._calc_base_close(amount, rate, fee)) - funding_fee else: - return float(close_trade - fees - interest) + raise OperationalException( + f"{self.trading_mode.value} trading is not yet available using freqtrade") def calc_profit(self, rate: Optional[float] = None, fee: Optional[float] = None, @@ -893,14 +936,19 @@ class Trade(_DECL_BASE, LocalTrade): buy_tag = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) - # Leverage trading properties - leverage = Column(Float, nullable=True, default=1.0) - is_short = Column(Boolean, nullable=False, default=False) - isolated_liq = Column(Float, nullable=True) + trading_mode = Column(Enum(TradingMode)) - # Margin Trading Properties + leverage = Column(Float, nullable=True, default=1.0) + isolated_liq = Column(Float, nullable=True) + is_short = Column(Boolean, nullable=False, default=False) + + # Margin properties interest_rate = Column(Float, nullable=False, default=0.0) + # Futures properties + funding_fee = Column(Float, nullable=True, default=None) + last_funding_adjustment = Column(DateTime, nullable=True) + def __init__(self, **kwargs): super().__init__(**kwargs) self.recalc_open_trade_value() diff --git a/requirements.txt b/requirements.txt index f77edddfe..73a4a9cb3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,3 +41,6 @@ colorama==0.4.4 # Building config files interactively questionary==1.10.0 prompt-toolkit==3.0.20 + +#Futures +schedule==1.1.0 diff --git a/tests/leverage/test_leverage.py b/tests/leverage/test_leverage.py index 7b7ca0f9b..9a6e99806 100644 --- a/tests/leverage/test_leverage.py +++ b/tests/leverage/test_leverage.py @@ -3,7 +3,7 @@ from math import isclose import pytest -from freqtrade.leverage import interest +from freqtrade.leverage.interest import interest ten_mins = Decimal(1/6) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 56e64db69..d649581a6 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -8,7 +8,7 @@ import pytest from numpy import isnan from freqtrade.edge import PairInfo -from freqtrade.enums import State +from freqtrade.enums import State, TradingMode from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade from freqtrade.persistence.pairlock_middleware import PairLocks @@ -108,10 +108,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', - 'leverage': 1.0, - 'interest_rate': 0.0, + 'trading_mode': TradingMode.SPOT, 'isolated_liq': None, 'is_short': False, + 'leverage': 1.0, + 'interest_rate': 0.0, + 'funding_fee': None, + 'last_funding_adjustment': None, } mocker.patch('freqtrade.exchange.Exchange.get_rate', @@ -179,10 +182,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', - 'leverage': 1.0, - 'interest_rate': 0.0, + 'trading_mode': TradingMode.SPOT, 'isolated_liq': None, 'is_short': False, + 'leverage': 1.0, + 'interest_rate': 0.0, + 'funding_fee': None, + 'last_funding_adjustment': None, } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 911d7d6c2..a33f2c1b0 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -11,6 +11,7 @@ import pytest from sqlalchemy import create_engine, inspect, text from freqtrade import constants +from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re @@ -90,7 +91,7 @@ def test_enter_exit_side(fee): @pytest.mark.usefixtures("init_persistence") -def test__set_stop_loss_isolated_liq(fee): +def test_set_stop_loss_isolated_liq(fee): trade = Trade( id=2, pair='ADA/USDT', @@ -236,6 +237,7 @@ def test_interest(market_buy_order_usdt, fee): exchange='binance', leverage=3.0, interest_rate=0.0005, + trading_mode=TradingMode.MARGIN ) # 10min, 3x leverage @@ -548,6 +550,7 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca is_short=True, leverage=3.0, interest_rate=0.0005, + trading_mode=TradingMode.MARGIN ) trade.open_order_id = 'something' trade.update(limit_sell_order_usdt) @@ -639,6 +642,7 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt assert trade.calc_profit() == 5.685 assert trade.calc_profit_ratio() == round(0.0945137157107232, 8) # 3x leverage, binance + trade.trading_mode = TradingMode.MARGIN trade.leverage = 3 trade.exchange = "binance" assert trade._calc_open_trade_value() == 60.15 @@ -796,12 +800,19 @@ def test_calc_open_trade_value(limit_buy_order_usdt, fee): # Get the open rate price with the standard fee rate assert trade._calc_open_trade_value() == 60.15 + + # Margin + trade.trading_mode = TradingMode.MARGIN trade.is_short = True trade.recalc_open_trade_value() assert trade._calc_open_trade_value() == 59.85 + + # 3x short margin leverage trade.leverage = 3 trade.exchange = "binance" assert trade._calc_open_trade_value() == 59.85 + + # 3x long margin leverage trade.is_short = False trade.recalc_open_trade_value() assert trade._calc_open_trade_value() == 60.15 @@ -838,6 +849,7 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee assert trade.calc_close_trade_value(fee=0.005) == 65.67 # 3x leverage binance + trade.trading_mode = TradingMode.MARGIN trade.leverage = 3.0 assert round(trade.calc_close_trade_value(rate=2.5), 8) == 74.81166667 assert round(trade.calc_close_trade_value(rate=2.5, fee=0.003), 8) == 74.77416667 @@ -1037,6 +1049,8 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): trade.open_trade_value = 0.0 trade.open_trade_value = trade._calc_open_trade_value() + # Margin + trade.trading_mode = TradingMode.MARGIN # 3x leverage, long ################################################### trade.leverage = 3.0 # Higher than open rate - 2.1 quote @@ -1139,6 +1153,8 @@ def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee): assert trade.calc_profit_ratio(fee=0.003) == 0.0 trade.open_trade_value = trade._calc_open_trade_value() + # Margin + trade.trading_mode = TradingMode.MARGIN # 3x leverage, long ################################################### trade.leverage = 3.0 # 2.1 quote - Higher than open rate @@ -1707,6 +1723,9 @@ def test_to_json(default_conf, fee): 'interest_rate': None, 'isolated_liq': None, 'is_short': None, + 'trading_mode': None, + 'funding_fee': None, + 'last_funding_adjustment': None } # Simulate dry_run entries @@ -1778,6 +1797,9 @@ def test_to_json(default_conf, fee): 'interest_rate': None, 'isolated_liq': None, 'is_short': None, + 'trading_mode': None, + 'funding_fee': None, + 'last_funding_adjustment': None } @@ -2197,6 +2219,7 @@ def test_Trade_object_idem(): 'get_open_trades_without_assigned_fees', 'get_open_order_trades', 'get_trades', + 'last_funding_adjustment' ) # Parent (LocalTrade) should have the same attributes From 194bb24a5537f60d399bcf486e13a3dfee77538e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 25 Aug 2021 12:59:25 -0600 Subject: [PATCH 02/59] Miscellaneous funding fee changes. Abandoning for a new method of tracking funding fee --- freqtrade/exchange/binance.py | 16 ++++++++++- freqtrade/exchange/exchange.py | 14 ++++++++++ freqtrade/exchange/ftx.py | 17 +++++++++++- freqtrade/leverage/funding_fee.py | 44 ++++++++++++++++++------------- 4 files changed, 71 insertions(+), 20 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 0c470cb24..bed07ca89 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,6 +1,6 @@ """ Binance exchange subclass """ import logging -from typing import Dict +from typing import Dict, Optional import ccxt @@ -89,3 +89,17 @@ class Binance(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + # https://www.binance.com/en/support/faq/360033525031 + def get_funding_fee( + self, + contract_size: float, + mark_price: float, + rate: Optional[float], + # index_price: float, + # interest_rate: float + ): + assert isinstance(rate, float) + nominal_value = mark_price * contract_size + adjustment = nominal_value * rate + return adjustment diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ecf3302d8..0040fa6b9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1516,6 +1516,20 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) + def fetch_funding_rates(self): + return self._api.fetch_funding_rates() + + # https://www.binance.com/en/support/faq/360033525031 + def get_funding_fee( + self, + contract_size: float, + mark_price: float, + rate: Optional[float], + # index_price: float, + # interest_rate: float + ): + raise OperationalException(f"{self.name} has not implemented get_funding_rate") + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 6cd549d60..77b864ac7 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,6 +1,6 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict +from typing import Any, Dict, Optional import ccxt @@ -152,3 +152,18 @@ class Ftx(Exchange): if order['type'] == 'stop': return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] + + # https://help.ftx.com/hc/en-us/articles/360027946571-Funding + def get_funding_fee( + self, + contract_size: float, + mark_price: float, + rate: Optional[float], + # index_price: float, + # interest_rate: float + ): + """ + Always paid in USD on FTX # TODO: How do we account for this + """ + (contract_size * mark_price) / 24 + return diff --git a/freqtrade/leverage/funding_fee.py b/freqtrade/leverage/funding_fee.py index 738fa1344..209019075 100644 --- a/freqtrade/leverage/funding_fee.py +++ b/freqtrade/leverage/funding_fee.py @@ -3,6 +3,7 @@ from typing import List import schedule +from freqtrade.exchange import Exchange from freqtrade.persistence import Trade @@ -16,10 +17,14 @@ class FundingFee: "07:59:45", "15:59:45", ] + exchange: Exchange # FTX # begin_times = every hour + def __init__(self, exchange: Exchange): + self.exchange = exchange + def _is_time_between(self, begin_time, end_time): # If check time is not given, default to current UTC time check_time = datetime.utcnow().time() @@ -28,27 +33,30 @@ class FundingFee: else: # crosses midnight return check_time >= begin_time or check_time <= end_time - def _apply_funding_fees(self, num_of: int = 1): - if num_of == 0: - return + def _apply_current_funding_fees(self): + funding_rates = self.exchange.fetch_funding_rates() + for trade in self.trades: - trade.adjust_funding_fee(self._calculate(trade.amount) * num_of) + funding_rate = funding_rates[trade.pair] + self._apply_fee_to_trade(funding_rate, trade) - def _calculate(self, amount): - # TODO-futures: implement - # TODO-futures: Check how other exchages do it and adjust accordingly - # https://www.binance.com/en/support/faq/360033525031 - # mark_price = - # contract_size = maybe trade.amount - # funding_rate = # https://www.binance.com/en/futures/funding-history/0 - # nominal_value = mark_price * contract_size - # adjustment = nominal_value * funding_rate - # return adjustment + def _apply_fee_to_trade(self, funding_rate: dict, trade: Trade): - # FTX - paid in USD(always) - # position size * TWAP of((future - index) / index) / 24 - # https: // help.ftx.com/hc/en-us/articles/360027946571-Funding - return + amount = trade.amount + mark_price = funding_rate['markPrice'] + rate = funding_rate['fundingRate'] + # index_price = funding_rate['indexPrice'] + # interest_rate = funding_rate['interestRate'] + + funding_fee = self.exchange.get_funding_fee( + amount, + mark_price, + rate, + # interest_rate + # index_price, + ) + + trade.adjust_funding_fee(funding_fee) def initial_funding_fee(self, amount) -> float: # A funding fee interval is applied immediately if within 30s of an iterval From b854350e8d49e4b9bfd239c0a5e7ff612ac5076b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 25 Aug 2021 22:09:32 -0600 Subject: [PATCH 03/59] Changed funding fee implementation --- freqtrade/exchange/binance.py | 16 +----- freqtrade/exchange/exchange.py | 29 +++++----- freqtrade/exchange/ftx.py | 17 +----- freqtrade/freqtradebot.py | 7 ++- freqtrade/leverage/__init__.py | 1 + freqtrade/leverage/funding_fee.py | 88 ------------------------------- freqtrade/optimize/backtesting.py | 2 +- tests/rpc/test_rpc.py | 16 ++---- 8 files changed, 30 insertions(+), 146 deletions(-) delete mode 100644 freqtrade/leverage/funding_fee.py diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index bed07ca89..0c470cb24 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,6 +1,6 @@ """ Binance exchange subclass """ import logging -from typing import Dict, Optional +from typing import Dict import ccxt @@ -89,17 +89,3 @@ class Binance(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e - - # https://www.binance.com/en/support/faq/360033525031 - def get_funding_fee( - self, - contract_size: float, - mark_price: float, - rate: Optional[float], - # index_price: float, - # interest_rate: float - ): - assert isinstance(rate, float) - nominal_value = mark_price * contract_size - adjustment = nominal_value * rate - return adjustment diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0040fa6b9..168dcd575 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1516,19 +1516,22 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - def fetch_funding_rates(self): - return self._api.fetch_funding_rates() - - # https://www.binance.com/en/support/faq/360033525031 - def get_funding_fee( - self, - contract_size: float, - mark_price: float, - rate: Optional[float], - # index_price: float, - # interest_rate: float - ): - raise OperationalException(f"{self.name} has not implemented get_funding_rate") + def get_funding_fees(self, pair: str, since: datetime): + try: + funding_history = self._api.fetch_funding_history( + pair=pair, + since=since + ) + # TODO: sum all the funding fees in funding_history together + funding_fees = funding_history + return funding_fees + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get funding fees due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 77b864ac7..6cd549d60 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,6 +1,6 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict, Optional +from typing import Any, Dict import ccxt @@ -152,18 +152,3 @@ class Ftx(Exchange): if order['type'] == 'stop': return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] - - # https://help.ftx.com/hc/en-us/articles/360027946571-Funding - def get_funding_fee( - self, - contract_size: float, - mark_price: float, - rate: Optional[float], - # index_price: float, - # interest_rate: float - ): - """ - Always paid in USD on FTX # TODO: How do we account for this - """ - (contract_size * mark_price) / 24 - return diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4659a634c..7b0a521bf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -103,7 +103,7 @@ class FreqtradeBot(LoggingMixin): self._sell_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) - self.trading_mode = TradingMode.SPOT + self.trading_mode = self.config['trading_mode'] if self.trading_mode == TradingMode.FUTURES: self.funding_fee = FundingFee() self.funding_fee.start() @@ -243,6 +243,10 @@ class FreqtradeBot(LoggingMixin): open_trades = len(Trade.get_open_trades()) return max(0, self.config['max_open_trades'] - open_trades) + def get_funding_fees(): + if self.trading_mode == TradingMode.FUTURES: + return + def update_open_orders(self): """ Updates open orders based on order list kept in the database. @@ -258,7 +262,6 @@ class FreqtradeBot(LoggingMixin): try: fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, order.ft_order_side == 'stoploss') - self.update_trade_state(order.trade, order.order_id, fo) except ExchangeError as e: diff --git a/freqtrade/leverage/__init__.py b/freqtrade/leverage/__init__.py index 9186b160e..ae78f4722 100644 --- a/freqtrade/leverage/__init__.py +++ b/freqtrade/leverage/__init__.py @@ -1 +1,2 @@ # flake8: noqa: F401 +from freqtrade.leverage.interest import interest diff --git a/freqtrade/leverage/funding_fee.py b/freqtrade/leverage/funding_fee.py deleted file mode 100644 index 209019075..000000000 --- a/freqtrade/leverage/funding_fee.py +++ /dev/null @@ -1,88 +0,0 @@ -from datetime import datetime, timedelta -from typing import List - -import schedule - -from freqtrade.exchange import Exchange -from freqtrade.persistence import Trade - - -class FundingFee: - - trades: List[Trade] - # Binance - begin_times = [ - # TODO-lev: Make these UTC time - "23:59:45", - "07:59:45", - "15:59:45", - ] - exchange: Exchange - - # FTX - # begin_times = every hour - - def __init__(self, exchange: Exchange): - self.exchange = exchange - - def _is_time_between(self, begin_time, end_time): - # If check time is not given, default to current UTC time - check_time = datetime.utcnow().time() - if begin_time < end_time: - return check_time >= begin_time and check_time <= end_time - else: # crosses midnight - return check_time >= begin_time or check_time <= end_time - - def _apply_current_funding_fees(self): - funding_rates = self.exchange.fetch_funding_rates() - - for trade in self.trades: - funding_rate = funding_rates[trade.pair] - self._apply_fee_to_trade(funding_rate, trade) - - def _apply_fee_to_trade(self, funding_rate: dict, trade: Trade): - - amount = trade.amount - mark_price = funding_rate['markPrice'] - rate = funding_rate['fundingRate'] - # index_price = funding_rate['indexPrice'] - # interest_rate = funding_rate['interestRate'] - - funding_fee = self.exchange.get_funding_fee( - amount, - mark_price, - rate, - # interest_rate - # index_price, - ) - - trade.adjust_funding_fee(funding_fee) - - def initial_funding_fee(self, amount) -> float: - # A funding fee interval is applied immediately if within 30s of an iterval - # May only exist on binance - for begin_string in self.begin_times: - begin_time = datetime.strptime(begin_string, "%H:%M:%S") - end_time = (begin_time + timedelta(seconds=30)) - if self._is_time_between(begin_time.time(), end_time.time()): - return self._calculate(amount) - return 0.0 - - def start(self): - for interval in self.begin_times: - schedule.every().day.at(interval).do(self._apply_funding_fees()) - - # https://stackoverflow.com/a/30393162/6331353 - # TODO-futures: Put schedule.run_pending() somewhere in the bot_loop - - def reboot(self): - # TODO-futures Find out how many begin_times have passed since last funding_fee added - amount_missed = 0 - self.apply_funding_fees(num_of=amount_missed) - self.start() - - def add_new_trade(self, trade): - self.trades.append(trade) - - def remove_trade(self, trade): - self.trades.remove(trade) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 99d4c60d0..084142646 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -386,7 +386,7 @@ class Backtesting: detail_data = detail_data.loc[ (detail_data['date'] >= sell_candle_time) & (detail_data['date'] < sell_candle_end) - ] + ] if len(detail_data) == 0: # Fall back to "regular" data if no detail data was found for this candle return self._get_sell_trade_entry_for_candle(trade, sell_row) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index d649581a6..56e64db69 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -8,7 +8,7 @@ import pytest from numpy import isnan from freqtrade.edge import PairInfo -from freqtrade.enums import State, TradingMode +from freqtrade.enums import State from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade from freqtrade.persistence.pairlock_middleware import PairLocks @@ -108,13 +108,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', - 'trading_mode': TradingMode.SPOT, - 'isolated_liq': None, - 'is_short': False, 'leverage': 1.0, 'interest_rate': 0.0, - 'funding_fee': None, - 'last_funding_adjustment': None, + 'isolated_liq': None, + 'is_short': False, } mocker.patch('freqtrade.exchange.Exchange.get_rate', @@ -182,13 +179,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', - 'trading_mode': TradingMode.SPOT, - 'isolated_liq': None, - 'is_short': False, 'leverage': 1.0, 'interest_rate': 0.0, - 'funding_fee': None, - 'last_funding_adjustment': None, + 'isolated_liq': None, + 'is_short': False, } From d6d5bae2a12bf3052cf80cb3ad1899f5444b6354 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 25 Aug 2021 23:01:07 -0600 Subject: [PATCH 04/59] New funding fee methods --- freqtrade/freqtradebot.py | 23 ++++++----------- freqtrade/persistence/migrations.py | 7 +++--- freqtrade/persistence/models.py | 39 ++++++++--------------------- tests/leverage/test_leverage.py | 2 +- tests/rpc/test_rpc.py | 6 ++++- tests/test_persistence.py | 7 ++---- 6 files changed, 30 insertions(+), 54 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7b0a521bf..69b669f63 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -20,7 +20,6 @@ from freqtrade.enums import RPCMessageType, SellType, State, TradingMode from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds -from freqtrade.leverage.funding_fee import FundingFee from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db @@ -103,10 +102,10 @@ class FreqtradeBot(LoggingMixin): self._sell_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) - self.trading_mode = self.config['trading_mode'] - if self.trading_mode == TradingMode.FUTURES: - self.funding_fee = FundingFee() - self.funding_fee.start() + if 'trading_mode' in self.config: + self.trading_mode = self.config['trading_mode'] + else: + self.trading_mode = TradingMode.SPOT def notify_status(self, msg: str) -> None: """ @@ -243,9 +242,10 @@ class FreqtradeBot(LoggingMixin): open_trades = len(Trade.get_open_trades()) return max(0, self.config['max_open_trades'] - open_trades) - def get_funding_fees(): + def add_funding_fees(self, trade: Trade): if self.trading_mode == TradingMode.FUTURES: - return + funding_fees = self.exchange.get_funding_fees(trade.pair, trade.open_date) + trade.funding_fees = funding_fees def update_open_orders(self): """ @@ -262,6 +262,7 @@ class FreqtradeBot(LoggingMixin): try: fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, order.ft_order_side == 'stoploss') + self.update_trade_state(order.trade, order.order_id, fo) except ExchangeError as e: @@ -568,10 +569,6 @@ class FreqtradeBot(LoggingMixin): amount = safe_value_fallback(order, 'filled', 'amount') buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') - funding_fee = (self.funding_fee.initial_funding_fee(amount) - if self.trading_mode == TradingMode.FUTURES - else None) - # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade( @@ -590,14 +587,10 @@ class FreqtradeBot(LoggingMixin): strategy=self.strategy.get_strategy_name(), buy_tag=buy_tag, timeframe=timeframe_to_minutes(self.config['timeframe']), - funding_fee=funding_fee, trading_mode=self.trading_mode ) trade.orders.append(order_obj) - if self.trading_mode == TradingMode.FUTURES: - self.funding_fee.add_new_trade(trade) - # Update fees if order is closed if order_status == 'closed': self.update_trade_state(trade, order_id, order) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index f4deef45b..ec6f10e3f 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -61,8 +61,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col interest_rate = get_column_def(cols, 'interest_rate', '0.0') # Futures properties - funding_fee = get_column_def(cols, 'funding_fee', '0.0') - last_funding_adjustment = get_column_def(cols, 'last_funding_adjustment', 'null') + funding_fees = get_column_def(cols, 'funding_fees', '0.0') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): @@ -102,7 +101,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, timeframe, open_trade_value, close_profit_abs, trading_mode, leverage, isolated_liq, is_short, - interest_rate, funding_fee, last_funding_adjustment + interest_rate, funding_fees ) select id, lower(exchange), pair, is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost, @@ -121,7 +120,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {trading_mode} trading_mode, {leverage} leverage, {isolated_liq} isolated_liq, {is_short} is_short, {interest_rate} interest_rate, - {funding_fee} funding_fee, {last_funding_adjustment} last_funding_adjustment + {funding_fees} funding_fees from {table_back_name} """)) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 72d2fafc9..eabc36509 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone from decimal import Decimal from typing import Any, Dict, List, Optional @@ -16,7 +16,7 @@ from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.enums import SellType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.leverage.interest import interest +from freqtrade.leverage import interest from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -57,7 +57,7 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: f"is no valid database URL! (See {_SQL_DOCS_URL})") # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope - # Scoped sessions proxy reque sts to the appropriate thread-local session. + # Scoped sessions proxy requests to the appropriate thread-local session. # We should use the scoped_session object - not a seperately initialized version Trade._session = scoped_session(sessionmaker(bind=engine, autoflush=True)) Trade.query = Trade._session.query_property() @@ -93,12 +93,6 @@ def clean_dry_run_db() -> None: Trade.commit() -def hour_rounder(t): - # Rounds to nearest hour by adding a timedelta hour if minute >= 30 - return ( - t.replace(second=0, microsecond=0, minute=0, hour=t.hour) + timedelta(hours=t.minute//30)) - - class Order(_DECL_BASE): """ Order database model @@ -282,8 +276,7 @@ class LocalTrade(): interest_rate: float = 0.0 # Futures properties - funding_fee: Optional[float] = None - last_funding_adjustment: Optional[datetime] = None + funding_fees: Optional[float] = None @property def has_no_leverage(self) -> bool: @@ -451,9 +444,7 @@ class LocalTrade(): 'isolated_liq': self.isolated_liq, 'is_short': self.is_short, 'trading_mode': self.trading_mode, - 'funding_fee': self.funding_fee, - 'last_funding_adjustment': (self.last_funding_adjustment.strftime(DATETIME_PRINT_FORMAT) - if self.last_funding_adjustment else None), + 'funding_fees': self.funding_fees, 'open_order_id': self.open_order_id, } @@ -531,10 +522,6 @@ class LocalTrade(): f"Trailing stoploss saved us: " f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.") - def adjust_funding_fee(self, adjustment): - self.funding_fee = self.funding_fee + adjustment - self.last_funding_adjustment = datetime.utcnow() - def update(self, order: Dict) -> None: """ Updates this entity with amount and actual open/close rates. @@ -673,7 +660,6 @@ class LocalTrade(): rate = Decimal(interest_rate or self.interest_rate) borrowed = Decimal(self.borrowed) - # TODO-lev: Pass trading mode to interest maybe return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours) def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None, @@ -721,11 +707,8 @@ class LocalTrade(): return float(self._calc_base_close(amount, rate, fee) - total_interest) elif (trading_mode == TradingMode.FUTURES): - funding_fee = self.funding_fee or 0.0 - if self.is_short: - return float(self._calc_base_close(amount, rate, fee)) + funding_fee - else: - return float(self._calc_base_close(amount, rate, fee)) - funding_fee + funding_fees = self.funding_fees or 0.0 + return float(self._calc_base_close(amount, rate, fee)) + funding_fees else: raise OperationalException( f"{self.trading_mode.value} trading is not yet available using freqtrade") @@ -938,16 +921,16 @@ class Trade(_DECL_BASE, LocalTrade): trading_mode = Column(Enum(TradingMode)) + # Leverage trading properties leverage = Column(Float, nullable=True, default=1.0) - isolated_liq = Column(Float, nullable=True) is_short = Column(Boolean, nullable=False, default=False) + isolated_liq = Column(Float, nullable=True) - # Margin properties + # Margin Trading Properties interest_rate = Column(Float, nullable=False, default=0.0) # Futures properties - funding_fee = Column(Float, nullable=True, default=None) - last_funding_adjustment = Column(DateTime, nullable=True) + funding_fees = Column(Float, nullable=True, default=None) def __init__(self, **kwargs): super().__init__(**kwargs) diff --git a/tests/leverage/test_leverage.py b/tests/leverage/test_leverage.py index 9a6e99806..7b7ca0f9b 100644 --- a/tests/leverage/test_leverage.py +++ b/tests/leverage/test_leverage.py @@ -3,7 +3,7 @@ from math import isclose import pytest -from freqtrade.leverage.interest import interest +from freqtrade.leverage import interest ten_mins = Decimal(1/6) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 56e64db69..d78f40a96 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -8,7 +8,7 @@ import pytest from numpy import isnan from freqtrade.edge import PairInfo -from freqtrade.enums import State +from freqtrade.enums import State, TradingMode from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade from freqtrade.persistence.pairlock_middleware import PairLocks @@ -112,6 +112,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'interest_rate': 0.0, 'isolated_liq': None, 'is_short': False, + 'funding_fees': None, + 'trading_mode': TradingMode.SPOT } mocker.patch('freqtrade.exchange.Exchange.get_rate', @@ -183,6 +185,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'interest_rate': 0.0, 'isolated_liq': None, 'is_short': False, + 'funding_fees': None, + 'trading_mode': TradingMode.SPOT } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index a33f2c1b0..062aa65fe 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1724,8 +1724,7 @@ def test_to_json(default_conf, fee): 'isolated_liq': None, 'is_short': None, 'trading_mode': None, - 'funding_fee': None, - 'last_funding_adjustment': None + 'funding_fees': None, } # Simulate dry_run entries @@ -1798,8 +1797,7 @@ def test_to_json(default_conf, fee): 'isolated_liq': None, 'is_short': None, 'trading_mode': None, - 'funding_fee': None, - 'last_funding_adjustment': None + 'funding_fees': None, } @@ -2219,7 +2217,6 @@ def test_Trade_object_idem(): 'get_open_trades_without_assigned_fees', 'get_open_order_trades', 'get_trades', - 'last_funding_adjustment' ) # Parent (LocalTrade) should have the same attributes From 92e630eb696a97217a7b4246f8bee6bb71408c32 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 1 Sep 2021 20:34:01 -0600 Subject: [PATCH 05/59] Added get_funding_fees method to exchange --- freqtrade/exchange/exchange.py | 23 ++++++++--- tests/exchange/test_exchange.py | 68 +++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 168dcd575..67eb0ad15 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -9,7 +9,7 @@ import logging from copy import deepcopy from datetime import datetime, timezone from math import ceil -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union import arrow import ccxt @@ -1516,15 +1516,28 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - def get_funding_fees(self, pair: str, since: datetime): + @retrier + def get_funding_fees(self, pair: str, since: Union[datetime, int]) -> float: + """ + Returns the sum of all funding fees that were exchanged for a pair within a timeframe + :param pair: (e.g. ADA/USDT) + :param since: The earliest time of consideration for calculating funding fees, + in unix time or as a datetime + """ + + if not self.exchange_has("fetchFundingHistory"): + raise OperationalException( + f"fetch_funding_history() has not been implemented on ccxt.{self.name}") + + if type(since) is datetime: + since = int(since.strftime('%s')) + try: funding_history = self._api.fetch_funding_history( pair=pair, since=since ) - # TODO: sum all the funding fees in funding_history together - funding_fees = funding_history - return funding_fees + return sum(fee['amount'] for fee in funding_history) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 42da5dddc..e2a6639a3 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2926,3 +2926,71 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected + + +@pytest.mark.parametrize("exchange_name", ['binance', 'ftx']) +def test_get_funding_fees(default_conf, mocker, exchange_name): + api_mock = MagicMock() + api_mock.fetch_funding_history = MagicMock(return_value=[ + { + 'amount': 0.14542341, + 'code': 'USDT', + 'datetime': '2021-09-01T08:00:01.000Z', + 'id': '485478', + 'info': {'asset': 'USDT', + 'income': '0.14542341', + 'incomeType': 'FUNDING_FEE', + 'info': 'FUNDING_FEE', + 'symbol': 'XRPUSDT', + 'time': '1630512001000', + 'tradeId': '', + 'tranId': '4854789484855218760'}, + 'symbol': 'XRP/USDT', + 'timestamp': 1630512001000 + }, + { + 'amount': -0.14642341, + 'code': 'USDT', + 'datetime': '2021-09-01T16:00:01.000Z', + 'id': '485479', + 'info': {'asset': 'USDT', + 'income': '-0.14642341', + 'incomeType': 'FUNDING_FEE', + 'info': 'FUNDING_FEE', + 'symbol': 'XRPUSDT', + 'time': '1630512001000', + 'tradeId': '', + 'tranId': '4854789484855218760'}, + 'symbol': 'XRP/USDT', + 'timestamp': 1630512001000 + } + ]) + type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) + + # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') + unix_time = int(date_time.strftime('%s')) + expected_fees = -0.001 # 0.14542341 + -0.14642341 + fees_from_datetime = exchange.get_funding_fees( + pair='XRP/USDT', + since=date_time + ) + fees_from_unix_time = exchange.get_funding_fees( + pair='XRP/USDT', + since=unix_time + ) + + assert(isclose(expected_fees, fees_from_datetime)) + assert(isclose(expected_fees, fees_from_unix_time)) + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + exchange_name, + "get_funding_fees", + "fetch_funding_history", + pair="XRP/USDT", + since=unix_time + ) From f5248be043afa27f6264ec24848ed882a0ea9bca Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 6 Sep 2021 02:24:15 -0600 Subject: [PATCH 06/59] Changed funding fee tracking method, need to get funding_rate and open prices at multiple candles --- freqtrade/exchange/binance.py | 2 +- freqtrade/exchange/exchange.py | 32 ++------ freqtrade/exchange/ftx.py | 2 +- freqtrade/freqtradebot.py | 5 -- freqtrade/leverage/__init__.py | 1 + freqtrade/leverage/funding_fees.py | 74 +++++++++++++++++ freqtrade/persistence/models.py | 13 ++- tests/exchange/test_exchange.py | 126 ++++++++++++++--------------- 8 files changed, 157 insertions(+), 98 deletions(-) create mode 100644 freqtrade/leverage/funding_fees.py diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 0c470cb24..ba4f510d3 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,6 +1,6 @@ """ Binance exchange subclass """ import logging -from typing import Dict +from typing import Dict, Optional import ccxt diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 67eb0ad15..d82c20599 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -9,7 +9,7 @@ import logging from copy import deepcopy from datetime import datetime, timezone from math import ceil -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt @@ -361,7 +361,7 @@ class Exchange: raise OperationalException( 'Could not load markets, therefore cannot start. ' 'Please investigate the above error for more details.' - ) + ) quote_currencies = self.get_quote_currencies() if stake_currency not in quote_currencies: raise OperationalException( @@ -1516,35 +1516,13 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - @retrier - def get_funding_fees(self, pair: str, since: Union[datetime, int]) -> float: - """ - Returns the sum of all funding fees that were exchanged for a pair within a timeframe - :param pair: (e.g. ADA/USDT) - :param since: The earliest time of consideration for calculating funding fees, - in unix time or as a datetime - """ - + # https://www.binance.com/en/support/faq/360033525031 + def fetch_funding_rate(self): if not self.exchange_has("fetchFundingHistory"): raise OperationalException( f"fetch_funding_history() has not been implemented on ccxt.{self.name}") - if type(since) is datetime: - since = int(since.strftime('%s')) - - try: - funding_history = self._api.fetch_funding_history( - pair=pair, - since=since - ) - return sum(fee['amount'] for fee in funding_history) - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError( - f'Could not get funding fees due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e + return self._api.fetch_funding_rates() def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 6cd549d60..f1d633ca9 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,6 +1,6 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict +from typing import Any, Dict, Optional import ccxt diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 69b669f63..a6793a79a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -242,11 +242,6 @@ class FreqtradeBot(LoggingMixin): open_trades = len(Trade.get_open_trades()) return max(0, self.config['max_open_trades'] - open_trades) - def add_funding_fees(self, trade: Trade): - if self.trading_mode == TradingMode.FUTURES: - funding_fees = self.exchange.get_funding_fees(trade.pair, trade.open_date) - trade.funding_fees = funding_fees - def update_open_orders(self): """ Updates open orders based on order list kept in the database. diff --git a/freqtrade/leverage/__init__.py b/freqtrade/leverage/__init__.py index ae78f4722..54cd37481 100644 --- a/freqtrade/leverage/__init__.py +++ b/freqtrade/leverage/__init__.py @@ -1,2 +1,3 @@ # flake8: noqa: F401 +from freqtrade.leverage.funding_fees import funding_fee from freqtrade.leverage.interest import interest diff --git a/freqtrade/leverage/funding_fees.py b/freqtrade/leverage/funding_fees.py new file mode 100644 index 000000000..754d3ec96 --- /dev/null +++ b/freqtrade/leverage/funding_fees.py @@ -0,0 +1,74 @@ +from datetime import datetime +from typing import Optional + +from freqtrade.exceptions import OperationalException + + +def funding_fees( + exchange_name: str, + pair: str, + contract_size: float, + open_date: datetime, + close_date: datetime + # index_price: float, + # interest_rate: float +): + """ + Equation to calculate funding_fees on futures trades + + :param exchange_name: The exchanged being trading on + :param borrowed: The amount of currency being borrowed + :param rate: The rate of interest + :param hours: The time in hours that the currency has been borrowed for + + Raises: + OperationalException: Raised if freqtrade does + not support margin trading for this exchange + + Returns: The amount of interest owed (currency matches borrowed) + """ + exchange_name = exchange_name.lower() + # fees = 0 + if exchange_name == "binance": + for timeslot in ["23:59:45", "07:59:45", "15:59:45"]: + # for each day in close_date - open_date + # mark_price = mark_price at this time + # rate = rate at this time + # fees = fees + funding_fee(exchange_name, contract_size, mark_price, rate) + # return fees + return + elif exchange_name == "kraken": + raise OperationalException("Funding_fees has not been implemented for Kraken") + elif exchange_name == "ftx": + # for timeslot in every hour since open_date: + # mark_price = mark_price at this time + # fees = fees + funding_fee(exchange_name, contract_size, mark_price, rate) + return + else: + raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + + +def funding_fee( + exchange_name: str, + contract_size: float, + mark_price: float, + rate: Optional[float], + # index_price: float, + # interest_rate: float +): + """ + Calculates a single funding fee + """ + if exchange_name == "binance": + assert isinstance(rate, float) + nominal_value = mark_price * contract_size + adjustment = nominal_value * rate + return adjustment + elif exchange_name == "kraken": + raise OperationalException("Funding fee has not been implemented for kraken") + elif exchange_name == "ftx": + """ + Always paid in USD on FTX # TODO: How do we account for this + """ + (contract_size * mark_price) / 24 + return diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index eabc36509..1bbc0d296 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -16,7 +16,7 @@ from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.enums import SellType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.leverage import interest +from freqtrade.leverage import funding_fees, interest from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -707,6 +707,7 @@ class LocalTrade(): return float(self._calc_base_close(amount, rate, fee) - total_interest) elif (trading_mode == TradingMode.FUTURES): + self.add_funding_fees() funding_fees = self.funding_fees or 0.0 return float(self._calc_base_close(amount, rate, fee)) + funding_fees else: @@ -785,6 +786,16 @@ class LocalTrade(): else: return None + def add_funding_fees(self): + if self.trading_mode == TradingMode.FUTURES: + self.funding_fees = funding_fees( + self.exchange, + self.pair, + self.amount, + self.open_date_utc, + self.close_date_utc + ) + @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e2a6639a3..8e4a099c5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2928,69 +2928,69 @@ def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected -@pytest.mark.parametrize("exchange_name", ['binance', 'ftx']) -def test_get_funding_fees(default_conf, mocker, exchange_name): - api_mock = MagicMock() - api_mock.fetch_funding_history = MagicMock(return_value=[ - { - 'amount': 0.14542341, - 'code': 'USDT', - 'datetime': '2021-09-01T08:00:01.000Z', - 'id': '485478', - 'info': {'asset': 'USDT', - 'income': '0.14542341', - 'incomeType': 'FUNDING_FEE', - 'info': 'FUNDING_FEE', - 'symbol': 'XRPUSDT', - 'time': '1630512001000', - 'tradeId': '', - 'tranId': '4854789484855218760'}, - 'symbol': 'XRP/USDT', - 'timestamp': 1630512001000 - }, - { - 'amount': -0.14642341, - 'code': 'USDT', - 'datetime': '2021-09-01T16:00:01.000Z', - 'id': '485479', - 'info': {'asset': 'USDT', - 'income': '-0.14642341', - 'incomeType': 'FUNDING_FEE', - 'info': 'FUNDING_FEE', - 'symbol': 'XRPUSDT', - 'time': '1630512001000', - 'tradeId': '', - 'tranId': '4854789484855218760'}, - 'symbol': 'XRP/USDT', - 'timestamp': 1630512001000 - } - ]) - type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) +# @pytest.mark.parametrize("exchange_name", ['binance', 'ftx']) +# def test_get_funding_fees(default_conf, mocker, exchange_name): +# api_mock = MagicMock() +# api_mock.fetch_funding_history = MagicMock(return_value=[ +# { +# 'amount': 0.14542341, +# 'code': 'USDT', +# 'datetime': '2021-09-01T08:00:01.000Z', +# 'id': '485478', +# 'info': {'asset': 'USDT', +# 'income': '0.14542341', +# 'incomeType': 'FUNDING_FEE', +# 'info': 'FUNDING_FEE', +# 'symbol': 'XRPUSDT', +# 'time': '1630512001000', +# 'tradeId': '', +# 'tranId': '4854789484855218760'}, +# 'symbol': 'XRP/USDT', +# 'timestamp': 1630512001000 +# }, +# { +# 'amount': -0.14642341, +# 'code': 'USDT', +# 'datetime': '2021-09-01T16:00:01.000Z', +# 'id': '485479', +# 'info': {'asset': 'USDT', +# 'income': '-0.14642341', +# 'incomeType': 'FUNDING_FEE', +# 'info': 'FUNDING_FEE', +# 'symbol': 'XRPUSDT', +# 'time': '1630512001000', +# 'tradeId': '', +# 'tranId': '4854789484855218760'}, +# 'symbol': 'XRP/USDT', +# 'timestamp': 1630512001000 +# } +# ]) +# type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) - # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') - unix_time = int(date_time.strftime('%s')) - expected_fees = -0.001 # 0.14542341 + -0.14642341 - fees_from_datetime = exchange.get_funding_fees( - pair='XRP/USDT', - since=date_time - ) - fees_from_unix_time = exchange.get_funding_fees( - pair='XRP/USDT', - since=unix_time - ) +# # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) +# exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) +# date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') +# unix_time = int(date_time.strftime('%s')) +# expected_fees = -0.001 # 0.14542341 + -0.14642341 +# fees_from_datetime = exchange.get_funding_fees( +# pair='XRP/USDT', +# since=date_time +# ) +# fees_from_unix_time = exchange.get_funding_fees( +# pair='XRP/USDT', +# since=unix_time +# ) - assert(isclose(expected_fees, fees_from_datetime)) - assert(isclose(expected_fees, fees_from_unix_time)) +# assert(isclose(expected_fees, fees_from_datetime)) +# assert(isclose(expected_fees, fees_from_unix_time)) - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - exchange_name, - "get_funding_fees", - "fetch_funding_history", - pair="XRP/USDT", - since=unix_time - ) +# ccxt_exceptionhandlers( +# mocker, +# default_conf, +# api_mock, +# exchange_name, +# "get_funding_fees", +# "fetch_funding_history", +# pair="XRP/USDT", +# since=unix_time +# ) From baaf516aa6d196137051be3fc1d8260ce9171979 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 13:41:32 -0600 Subject: [PATCH 07/59] Added funding_times property to exchange --- freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/binance.py | 7 ++++--- freqtrade/exchange/exchange.py | 12 +++++++++++- freqtrade/exchange/ftx.py | 7 ++++--- freqtrade/exchange/kraken.py | 7 ++++--- 5 files changed, 24 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index b0c88a51a..138c02647 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -8,7 +8,7 @@ from freqtrade.exchange.binance import Binance from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.coinbasepro import Coinbasepro -from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, +from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, hours_to_time, is_exchange_known_ccxt, is_exchange_officially_supported, market_is_active, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index ba4f510d3..9be06e94d 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,12 +1,12 @@ """ Binance exchange subclass """ import logging -from typing import Dict, Optional +from typing import Dict, List import ccxt - +from datetime import time from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) -from freqtrade.exchange import Exchange +from freqtrade.exchange import Exchange, hours_to_time from freqtrade.exchange.common import retrier @@ -23,6 +23,7 @@ class Binance(Exchange): "trades_pagination_arg": "fromId", "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } + funding_fee_times: List[time] = hours_to_time([0, 8, 16]) def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: """ diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d82c20599..22f6f029d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -7,7 +7,7 @@ import http import inspect import logging from copy import deepcopy -from datetime import datetime, timezone +from datetime import datetime, time, timezone from math import ceil from typing import Any, Dict, List, Optional, Tuple @@ -69,6 +69,7 @@ class Exchange: "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) } _ft_has: Dict = {} + funding_fee_times: List[time] = [] def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ @@ -1525,6 +1526,15 @@ class Exchange: return self._api.fetch_funding_rates() +def hours_to_time(hours: List[int]) -> List[time]: + ''' + :param hours: a list of hours as a time of day (e.g. [1, 16] is 01:00 and 16:00 o'clock) + :return: a list of datetime time objects that correspond to the hours in hours + ''' + # TODO-lev: These must be utc time + return [datetime.strptime(str(t), '%H').time() for t in hours] + + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f1d633ca9..6f5c28e58 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,12 +1,12 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict, Optional +from typing import Any, Dict, List import ccxt - +from datetime import time from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) -from freqtrade.exchange import Exchange +from freqtrade.exchange import Exchange, hours_to_time from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier from freqtrade.misc import safe_value_fallback2 @@ -20,6 +20,7 @@ class Ftx(Exchange): "stoploss_on_exchange": True, "ohlcv_candle_limit": 1500, } + funding_fee_times: List[time] = hours_to_time(list(range(0, 23))) def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 1b069aa6c..d69ac9e33 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -1,12 +1,12 @@ """ Kraken exchange subclass """ import logging -from typing import Any, Dict +from typing import Any, Dict, List import ccxt - +from datetime import time from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) -from freqtrade.exchange import Exchange +from freqtrade.exchange import Exchange, hours_to_time from freqtrade.exchange.common import retrier @@ -22,6 +22,7 @@ class Kraken(Exchange): "trades_pagination": "id", "trades_pagination_arg": "since", } + funding_fee_times: List[time] = hours_to_time([0, 4, 8, 12, 16, 20]) def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ From af4a6effb7349502d84925c5e75af4ed84063fb9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 13:43:28 -0600 Subject: [PATCH 08/59] added pair to fetch_funding_rate --- 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 22f6f029d..bfb6494e1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1518,7 +1518,7 @@ class Exchange: until=until, from_id=from_id)) # https://www.binance.com/en/support/faq/360033525031 - def fetch_funding_rate(self): + def fetch_funding_rate(self, pair): if not self.exchange_has("fetchFundingHistory"): raise OperationalException( f"fetch_funding_history() has not been implemented on ccxt.{self.name}") From 2f4b566d99d176865a9b9101e471fd76867a0415 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 13:46:52 -0600 Subject: [PATCH 09/59] reverted back exchange.get_funding_fees method --- freqtrade/exchange/exchange.py | 32 +++++++- tests/exchange/test_exchange.py | 126 ++++++++++++++++---------------- 2 files changed, 94 insertions(+), 64 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bfb6494e1..358fab6c4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -9,7 +9,7 @@ import logging from copy import deepcopy from datetime import datetime, time, timezone from math import ceil -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple, Union import arrow import ccxt @@ -1525,6 +1525,36 @@ class Exchange: return self._api.fetch_funding_rates() + @retrier + def get_funding_fees(self, pair: str, since: Union[datetime, int]) -> float: + """ + Returns the sum of all funding fees that were exchanged for a pair within a timeframe + :param pair: (e.g. ADA/USDT) + :param since: The earliest time of consideration for calculating funding fees, + in unix time or as a datetime + """ + + if not self.exchange_has("fetchFundingHistory"): + raise OperationalException( + f"fetch_funding_history() has not been implemented on ccxt.{self.name}") + + if type(since) is datetime: + since = int(since.strftime('%s')) + + try: + funding_history = self._api.fetch_funding_history( + pair=pair, + since=since + ) + return sum(fee['amount'] for fee in funding_history) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get funding fees due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + def hours_to_time(hours: List[int]) -> List[time]: ''' diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8e4a099c5..e2a6639a3 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2928,69 +2928,69 @@ def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected -# @pytest.mark.parametrize("exchange_name", ['binance', 'ftx']) -# def test_get_funding_fees(default_conf, mocker, exchange_name): -# api_mock = MagicMock() -# api_mock.fetch_funding_history = MagicMock(return_value=[ -# { -# 'amount': 0.14542341, -# 'code': 'USDT', -# 'datetime': '2021-09-01T08:00:01.000Z', -# 'id': '485478', -# 'info': {'asset': 'USDT', -# 'income': '0.14542341', -# 'incomeType': 'FUNDING_FEE', -# 'info': 'FUNDING_FEE', -# 'symbol': 'XRPUSDT', -# 'time': '1630512001000', -# 'tradeId': '', -# 'tranId': '4854789484855218760'}, -# 'symbol': 'XRP/USDT', -# 'timestamp': 1630512001000 -# }, -# { -# 'amount': -0.14642341, -# 'code': 'USDT', -# 'datetime': '2021-09-01T16:00:01.000Z', -# 'id': '485479', -# 'info': {'asset': 'USDT', -# 'income': '-0.14642341', -# 'incomeType': 'FUNDING_FEE', -# 'info': 'FUNDING_FEE', -# 'symbol': 'XRPUSDT', -# 'time': '1630512001000', -# 'tradeId': '', -# 'tranId': '4854789484855218760'}, -# 'symbol': 'XRP/USDT', -# 'timestamp': 1630512001000 -# } -# ]) -# type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) +@pytest.mark.parametrize("exchange_name", ['binance', 'ftx']) +def test_get_funding_fees(default_conf, mocker, exchange_name): + api_mock = MagicMock() + api_mock.fetch_funding_history = MagicMock(return_value=[ + { + 'amount': 0.14542341, + 'code': 'USDT', + 'datetime': '2021-09-01T08:00:01.000Z', + 'id': '485478', + 'info': {'asset': 'USDT', + 'income': '0.14542341', + 'incomeType': 'FUNDING_FEE', + 'info': 'FUNDING_FEE', + 'symbol': 'XRPUSDT', + 'time': '1630512001000', + 'tradeId': '', + 'tranId': '4854789484855218760'}, + 'symbol': 'XRP/USDT', + 'timestamp': 1630512001000 + }, + { + 'amount': -0.14642341, + 'code': 'USDT', + 'datetime': '2021-09-01T16:00:01.000Z', + 'id': '485479', + 'info': {'asset': 'USDT', + 'income': '-0.14642341', + 'incomeType': 'FUNDING_FEE', + 'info': 'FUNDING_FEE', + 'symbol': 'XRPUSDT', + 'time': '1630512001000', + 'tradeId': '', + 'tranId': '4854789484855218760'}, + 'symbol': 'XRP/USDT', + 'timestamp': 1630512001000 + } + ]) + type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) -# # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) -# exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) -# date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') -# unix_time = int(date_time.strftime('%s')) -# expected_fees = -0.001 # 0.14542341 + -0.14642341 -# fees_from_datetime = exchange.get_funding_fees( -# pair='XRP/USDT', -# since=date_time -# ) -# fees_from_unix_time = exchange.get_funding_fees( -# pair='XRP/USDT', -# since=unix_time -# ) + # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') + unix_time = int(date_time.strftime('%s')) + expected_fees = -0.001 # 0.14542341 + -0.14642341 + fees_from_datetime = exchange.get_funding_fees( + pair='XRP/USDT', + since=date_time + ) + fees_from_unix_time = exchange.get_funding_fees( + pair='XRP/USDT', + since=unix_time + ) -# assert(isclose(expected_fees, fees_from_datetime)) -# assert(isclose(expected_fees, fees_from_unix_time)) + assert(isclose(expected_fees, fees_from_datetime)) + assert(isclose(expected_fees, fees_from_unix_time)) -# ccxt_exceptionhandlers( -# mocker, -# default_conf, -# api_mock, -# exchange_name, -# "get_funding_fees", -# "fetch_funding_history", -# pair="XRP/USDT", -# since=unix_time -# ) + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + exchange_name, + "get_funding_fees", + "fetch_funding_history", + pair="XRP/USDT", + since=unix_time + ) From 8bcd444775f187814d537da38303d282aba4a9ac Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 13:56:58 -0600 Subject: [PATCH 10/59] real-time updates to funding-fee in freqtradebot --- freqtrade/freqtradebot.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a6793a79a..02f8b27cb 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,6 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback +import schedule from datetime import datetime, timezone from math import isclose from threading import Lock @@ -107,6 +108,11 @@ class FreqtradeBot(LoggingMixin): else: self.trading_mode = TradingMode.SPOT + if self.trading_mode == TradingMode.FUTURES: + for time_slot in self.exchange.funding_fee_times: + schedule.every().day.at(time_slot).do(self.update_funding_fees()) + self.wallets.update() + def notify_status(self, msg: str) -> None: """ Public method for users of this class (worker, etc.) to send notifications @@ -242,6 +248,12 @@ class FreqtradeBot(LoggingMixin): open_trades = len(Trade.get_open_trades()) return max(0, self.config['max_open_trades'] - open_trades) + def update_funding_fees(self): + if self.trading_mode == TradingMode.FUTURES: + for trade in Trade.get_open_trades(): + funding_fees = self.exchange.get_funding_fees(trade.pair, trade.open_date) + trade.funding_fees = funding_fees + def update_open_orders(self): """ Updates open orders based on order list kept in the database. @@ -264,6 +276,9 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Error updating Order {order.order_id} due to {e}") + if self.trading_mode == TradingMode.FUTURES: + schedule.run_pending() + def update_closed_trades_without_assigned_fees(self): """ Update closed trades without close fees assigned. @@ -566,6 +581,12 @@ class FreqtradeBot(LoggingMixin): # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') + open_date = datetime.utcnow() + if self.trading_mode == TradingMode.FUTURES: + funding_fees = self.exchange.get_funding_fees(pair, open_date) + else: + funding_fees = 0.0 + trade = Trade( pair=pair, stake_amount=stake_amount, @@ -576,13 +597,14 @@ class FreqtradeBot(LoggingMixin): fee_close=fee, open_rate=buy_limit_filled_price, open_rate_requested=buy_limit_requested, - open_date=datetime.utcnow(), + open_date=open_date, exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), buy_tag=buy_tag, timeframe=timeframe_to_minutes(self.config['timeframe']), - trading_mode=self.trading_mode + trading_mode=self.trading_mode, + funding_fees=funding_fees ) trade.orders.append(order_obj) From cdefd15b283bfc7e15bcb17cf3d0eac6d84a3e88 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 14:50:30 -0600 Subject: [PATCH 11/59] separated hours_to_time to utils folder --- freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/binance.py | 4 ++-- freqtrade/exchange/exchange.py | 9 --------- freqtrade/exchange/ftx.py | 4 ++-- freqtrade/exchange/kraken.py | 4 ++-- freqtrade/utils/__init__.py | 2 ++ freqtrade/utils/hours_to_time.py | 11 +++++++++++ 7 files changed, 20 insertions(+), 16 deletions(-) create mode 100644 freqtrade/utils/__init__.py create mode 100644 freqtrade/utils/hours_to_time.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 138c02647..b0c88a51a 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -8,7 +8,7 @@ from freqtrade.exchange.binance import Binance from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.coinbasepro import Coinbasepro -from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, hours_to_time, +from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, is_exchange_known_ccxt, is_exchange_officially_supported, market_is_active, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 9be06e94d..cb18b7f8e 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -6,9 +6,9 @@ import ccxt from datetime import time from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) -from freqtrade.exchange import Exchange, hours_to_time +from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier - +from freqtrade.utils import hours_to_time logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 358fab6c4..df1bf28f3 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1556,15 +1556,6 @@ class Exchange: raise OperationalException(e) from e -def hours_to_time(hours: List[int]) -> List[time]: - ''' - :param hours: a list of hours as a time of day (e.g. [1, 16] is 01:00 and 16:00 o'clock) - :return: a list of datetime time objects that correspond to the hours in hours - ''' - # TODO-lev: These must be utc time - return [datetime.strptime(str(t), '%H').time() for t in hours] - - def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 6f5c28e58..5b7a9ffeb 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -6,10 +6,10 @@ import ccxt from datetime import time from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) -from freqtrade.exchange import Exchange, hours_to_time +from freqtrade.exchange import Exchange from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier from freqtrade.misc import safe_value_fallback2 - +from freqtrade.utils import hours_to_time logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index d69ac9e33..6aaf00214 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -6,9 +6,9 @@ import ccxt from datetime import time from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) -from freqtrade.exchange import Exchange, hours_to_time +from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier - +from freqtrade.utils import hours_to_time logger = logging.getLogger(__name__) diff --git a/freqtrade/utils/__init__.py b/freqtrade/utils/__init__.py new file mode 100644 index 000000000..e6e76c589 --- /dev/null +++ b/freqtrade/utils/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +from freqtrade.utils.hours_to_time import hours_to_time diff --git a/freqtrade/utils/hours_to_time.py b/freqtrade/utils/hours_to_time.py new file mode 100644 index 000000000..139fd83a1 --- /dev/null +++ b/freqtrade/utils/hours_to_time.py @@ -0,0 +1,11 @@ +from datetime import datetime, time +from typing import List + + +def hours_to_time(hours: List[int]) -> List[time]: + ''' + :param hours: a list of hours as a time of day (e.g. [1, 16] is 01:00 and 16:00 o'clock) + :return: a list of datetime time objects that correspond to the hours in hours + ''' + # TODO-lev: These must be utc time + return [datetime.strptime(str(t), '%H').time() for t in hours] From 36b8c87fb6d535d63a6bbbf752fe80a54d54b704 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 19:31:04 -0600 Subject: [PATCH 12/59] Added funding fee calculation methods to exchange classes --- freqtrade/exchange/binance.py | 22 +++++++++++++++++++++- freqtrade/exchange/exchange.py | 19 ++++++++++++++++++- freqtrade/exchange/ftx.py | 20 +++++++++++++++++++- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index cb18b7f8e..8c2713c72 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,6 +1,6 @@ """ Binance exchange subclass """ import logging -from typing import Dict, List +from typing import Dict, List, Optional import ccxt from datetime import time @@ -90,3 +90,23 @@ class Binance(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + def _get_funding_fee( + self, + contract_size: float, + mark_price: float, + funding_rate: Optional[float], + ) -> float: + """ + Calculates a single funding fee + :param contract_size: The amount/quanity + :param mark_price: The price of the asset that the contract is based off of + :param funding_rate: the interest rate and the premium + - interest rate: 0.03% daily, BNBUSDT, LINKUSDT, and LTCUSDT are 0% + - premium: varies by price difference between the perpetual contract and mark price + """ + if funding_rate is None: + raise OperationalException("Funding rate cannot be None for Binance._get_funding_fee") + nominal_value = mark_price * contract_size + adjustment = nominal_value * funding_rate + return adjustment diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index df1bf28f3..cd41f2b13 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1526,7 +1526,7 @@ class Exchange: return self._api.fetch_funding_rates() @retrier - def get_funding_fees(self, pair: str, since: Union[datetime, int]) -> float: + def get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float: """ Returns the sum of all funding fees that were exchanged for a pair within a timeframe :param pair: (e.g. ADA/USDT) @@ -1555,6 +1555,23 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def _get_funding_fee( + self, + contract_size: float, + mark_price: float, + funding_rate: Optional[float], + # index_price: float, + # interest_rate: float) + ) -> float: + """ + Calculates a single funding fee + :param contract_size: The amount/quanity + :param mark_price: The price of the asset that the contract is based off of + :param funding_rate: the interest rate and the premium + - premium: varies by price difference between the perpetual contract and mark price + """ + raise OperationalException(f"Funding fee has not been implemented for {self.name}") + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 5b7a9ffeb..c442924fa 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,6 +1,6 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional import ccxt from datetime import time @@ -153,3 +153,21 @@ class Ftx(Exchange): if order['type'] == 'stop': return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] + + def _get_funding_fee( + self, + contract_size: float, + mark_price: float, + funding_rate: Optional[float], + # index_price: float, + # interest_rate: float) + ): + """ + Calculates a single funding fee + Always paid in USD on FTX # TODO: How do we account for this + :param contract_size: The amount/quanity + :param mark_price: The price of the asset that the contract is based off of + :param funding_rate: Must be None on ftx + """ + (contract_size * mark_price) / 24 + return From 3eb0e6ac09c3093b753d941680a58a846dfd0fc8 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 19:31:27 -0600 Subject: [PATCH 13/59] removed leverage/funding_fees --- freqtrade/leverage/funding_fees.py | 74 ------------------------------ 1 file changed, 74 deletions(-) delete mode 100644 freqtrade/leverage/funding_fees.py diff --git a/freqtrade/leverage/funding_fees.py b/freqtrade/leverage/funding_fees.py deleted file mode 100644 index 754d3ec96..000000000 --- a/freqtrade/leverage/funding_fees.py +++ /dev/null @@ -1,74 +0,0 @@ -from datetime import datetime -from typing import Optional - -from freqtrade.exceptions import OperationalException - - -def funding_fees( - exchange_name: str, - pair: str, - contract_size: float, - open_date: datetime, - close_date: datetime - # index_price: float, - # interest_rate: float -): - """ - Equation to calculate funding_fees on futures trades - - :param exchange_name: The exchanged being trading on - :param borrowed: The amount of currency being borrowed - :param rate: The rate of interest - :param hours: The time in hours that the currency has been borrowed for - - Raises: - OperationalException: Raised if freqtrade does - not support margin trading for this exchange - - Returns: The amount of interest owed (currency matches borrowed) - """ - exchange_name = exchange_name.lower() - # fees = 0 - if exchange_name == "binance": - for timeslot in ["23:59:45", "07:59:45", "15:59:45"]: - # for each day in close_date - open_date - # mark_price = mark_price at this time - # rate = rate at this time - # fees = fees + funding_fee(exchange_name, contract_size, mark_price, rate) - # return fees - return - elif exchange_name == "kraken": - raise OperationalException("Funding_fees has not been implemented for Kraken") - elif exchange_name == "ftx": - # for timeslot in every hour since open_date: - # mark_price = mark_price at this time - # fees = fees + funding_fee(exchange_name, contract_size, mark_price, rate) - return - else: - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") - - -def funding_fee( - exchange_name: str, - contract_size: float, - mark_price: float, - rate: Optional[float], - # index_price: float, - # interest_rate: float -): - """ - Calculates a single funding fee - """ - if exchange_name == "binance": - assert isinstance(rate, float) - nominal_value = mark_price * contract_size - adjustment = nominal_value * rate - return adjustment - elif exchange_name == "kraken": - raise OperationalException("Funding fee has not been implemented for kraken") - elif exchange_name == "ftx": - """ - Always paid in USD on FTX # TODO: How do we account for this - """ - (contract_size * mark_price) / 24 - return From d559b6d6c685c451e48ca57f7b47f4a6d62f45d3 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 19:34:54 -0600 Subject: [PATCH 14/59] changed add_funding_fees template --- freqtrade/persistence/models.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 1bbc0d296..e15d31d6c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -16,7 +16,7 @@ from sqlalchemy.sql.schema import UniqueConstraint from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.enums import SellType, TradingMode from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.leverage import funding_fees, interest +from freqtrade.leverage import interest from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -788,13 +788,16 @@ class LocalTrade(): def add_funding_fees(self): if self.trading_mode == TradingMode.FUTURES: - self.funding_fees = funding_fees( - self.exchange, - self.pair, - self.amount, - self.open_date_utc, - self.close_date_utc - ) + # TODO-lev: Calculate this correctly and add it + # if self.config['runmode'].value in ('backtest', 'hyperopt'): + # self.funding_fees = getattr(Exchange, self.exchange).calculate_funding_fees( + # self.exchange, + # self.pair, + # self.amount, + # self.open_date_utc, + # self.close_date_utc + # ) + return @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, From d54117990b1f1ddcd3043e42c5a7c1159194696e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 9 Sep 2021 01:19:24 -0600 Subject: [PATCH 15/59] Added funding_fee method headers to exchange, and implemented some of the methods --- freqtrade/exchange/binance.py | 6 ++-- freqtrade/exchange/exchange.py | 58 +++++++++++++++++++++++++++++++-- freqtrade/exchange/ftx.py | 13 +++----- freqtrade/exchange/kraken.py | 6 ++-- freqtrade/freqtradebot.py | 9 +++-- freqtrade/leverage/__init__.py | 1 - tests/exchange/test_exchange.py | 6 ++-- tests/rpc/test_rpc.py | 4 +-- 8 files changed, 78 insertions(+), 25 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 8c2713c72..aa18634cf 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -3,12 +3,12 @@ import logging from typing import Dict, List, Optional import ccxt -from datetime import time + from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier -from freqtrade.utils import hours_to_time + logger = logging.getLogger(__name__) @@ -23,7 +23,7 @@ class Binance(Exchange): "trades_pagination_arg": "fromId", "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } - funding_fee_times: List[time] = hours_to_time([0, 8, 16]) + funding_fee_times: List[int] = [0, 8, 16] # hours of the day def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: """ diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cd41f2b13..c9a932bff 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -7,7 +7,7 @@ import http import inspect import logging from copy import deepcopy -from datetime import datetime, time, timezone +from datetime import datetime, timedelta, timezone from math import ceil from typing import Any, Dict, List, Optional, Tuple, Union @@ -69,7 +69,7 @@ class Exchange: "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) } _ft_has: Dict = {} - funding_fee_times: List[time] = [] + funding_fee_times: List[int] = [] # hours of the day def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ @@ -1555,6 +1555,21 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def get_mark_price(self, pair: str, when: datetime): + """ + Get's the value of the underlying asset for a futures contract + at a specific date and time in the past + """ + # TODO-lev: implement + raise OperationalException(f"get_mark_price has not been implemented for {self.name}") + + def get_funding_rate(self, pair: str, when: datetime): + """ + Get's the funding_rate for a pair at a specific date and time in the past + """ + # TODO-lev: implement + raise OperationalException(f"get_funding_rate has not been implemented for {self.name}") + def _get_funding_fee( self, contract_size: float, @@ -1572,6 +1587,45 @@ class Exchange: """ raise OperationalException(f"Funding fee has not been implemented for {self.name}") + def get_funding_fee_dates(self, open_date: datetime, close_date: datetime): + """ + Get's the date and time of every funding fee that happened between two datetimes + """ + open_date = datetime(open_date.year, open_date.month, open_date.day, open_date.hour) + close_date = datetime(close_date.year, close_date.month, close_date.day, close_date.hour) + + results = [] + date_iterator = open_date + while date_iterator < close_date: + date_iterator += timedelta(hours=1) + if date_iterator.hour in self.funding_fee_times: + results.append(date_iterator) + + return results + + def calculate_funding_fees( + self, + pair: str, + amount: float, + open_date: datetime, + close_date: datetime + ) -> float: + """ + calculates the sum of all funding fees that occurred for a pair during a futures trade + :param pair: The quote/base pair of the trade + :param amount: The quantity of the trade + :param open_date: The date and time that the trade started + :param close_date: The date and time that the trade ended + """ + + fees: float = 0 + for date in self.get_funding_fee_dates(open_date, close_date): + funding_rate = self.get_funding_rate(pair, date) + mark_price = self.get_mark_price(pair, date) + fees += self._get_funding_fee(amount, mark_price, funding_rate) + + return fees + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index c442924fa..42d7ce050 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -3,13 +3,13 @@ import logging from typing import Any, Dict, List, Optional import ccxt -from datetime import time + from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier from freqtrade.misc import safe_value_fallback2 -from freqtrade.utils import hours_to_time + logger = logging.getLogger(__name__) @@ -20,7 +20,7 @@ class Ftx(Exchange): "stoploss_on_exchange": True, "ohlcv_candle_limit": 1500, } - funding_fee_times: List[time] = hours_to_time(list(range(0, 23))) + funding_fee_times: List[int] = list(range(0, 23)) def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ @@ -159,9 +159,7 @@ class Ftx(Exchange): contract_size: float, mark_price: float, funding_rate: Optional[float], - # index_price: float, - # interest_rate: float) - ): + ) -> float: """ Calculates a single funding fee Always paid in USD on FTX # TODO: How do we account for this @@ -169,5 +167,4 @@ class Ftx(Exchange): :param mark_price: The price of the asset that the contract is based off of :param funding_rate: Must be None on ftx """ - (contract_size * mark_price) / 24 - return + return (contract_size * mark_price) / 24 diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 6aaf00214..a83b9f9cb 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -3,12 +3,12 @@ import logging from typing import Any, Dict, List import ccxt -from datetime import time + from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier -from freqtrade.utils import hours_to_time + logger = logging.getLogger(__name__) @@ -22,7 +22,7 @@ class Kraken(Exchange): "trades_pagination": "id", "trades_pagination_arg": "since", } - funding_fee_times: List[time] = hours_to_time([0, 4, 8, 12, 16, 20]) + funding_fee_times: List[int] = [0, 4, 8, 12, 16, 20] # hours of the day def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 02f8b27cb..574ade803 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,13 +4,13 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -import schedule from datetime import datetime, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional import arrow +import schedule from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency @@ -251,7 +251,10 @@ class FreqtradeBot(LoggingMixin): def update_funding_fees(self): if self.trading_mode == TradingMode.FUTURES: for trade in Trade.get_open_trades(): - funding_fees = self.exchange.get_funding_fees(trade.pair, trade.open_date) + funding_fees = self.exchange.get_funding_fees_from_exchange( + trade.pair, + trade.open_date + ) trade.funding_fees = funding_fees def update_open_orders(self): @@ -583,7 +586,7 @@ class FreqtradeBot(LoggingMixin): fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') open_date = datetime.utcnow() if self.trading_mode == TradingMode.FUTURES: - funding_fees = self.exchange.get_funding_fees(pair, open_date) + funding_fees = self.exchange.get_funding_fees_from_exchange(pair, open_date) else: funding_fees = 0.0 diff --git a/freqtrade/leverage/__init__.py b/freqtrade/leverage/__init__.py index 54cd37481..ae78f4722 100644 --- a/freqtrade/leverage/__init__.py +++ b/freqtrade/leverage/__init__.py @@ -1,3 +1,2 @@ # flake8: noqa: F401 -from freqtrade.leverage.funding_fees import funding_fee from freqtrade.leverage.interest import interest diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e2a6639a3..1d23482fc 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2972,11 +2972,11 @@ def test_get_funding_fees(default_conf, mocker, exchange_name): date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') unix_time = int(date_time.strftime('%s')) expected_fees = -0.001 # 0.14542341 + -0.14642341 - fees_from_datetime = exchange.get_funding_fees( + fees_from_datetime = exchange.get_funding_fees_from_exchange( pair='XRP/USDT', since=date_time ) - fees_from_unix_time = exchange.get_funding_fees( + fees_from_unix_time = exchange.get_funding_fees_from_exchange( pair='XRP/USDT', since=unix_time ) @@ -2989,7 +2989,7 @@ def test_get_funding_fees(default_conf, mocker, exchange_name): default_conf, api_mock, exchange_name, - "get_funding_fees", + "get_funding_fees_from_exchange", "fetch_funding_history", pair="XRP/USDT", since=unix_time diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index d78f40a96..586fadff8 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -112,7 +112,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'interest_rate': 0.0, 'isolated_liq': None, 'is_short': False, - 'funding_fees': None, + 'funding_fees': 0.0, 'trading_mode': TradingMode.SPOT } @@ -185,7 +185,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'interest_rate': 0.0, 'isolated_liq': None, 'is_short': False, - 'funding_fees': None, + 'funding_fees': 0.0, 'trading_mode': TradingMode.SPOT } From dfb9937436a8dd5ad9c98e2cdfb9bf1437029bf5 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 9 Sep 2021 01:43:05 -0600 Subject: [PATCH 16/59] Added tests and docstring to exchange funding_fee methods, removed utils --- freqtrade/exchange/binance.py | 8 ++++ freqtrade/exchange/exchange.py | 12 ++--- freqtrade/exchange/ftx.py | 14 ++++-- freqtrade/leverage/funding_fees.py | 75 ++++++++++++++++++++++++++++++ freqtrade/utils/__init__.py | 2 - freqtrade/utils/hours_to_time.py | 11 ----- tests/exchange/test_binance.py | 8 ++++ tests/exchange/test_exchange.py | 12 +++++ tests/exchange/test_ftx.py | 16 +++++++ 9 files changed, 134 insertions(+), 24 deletions(-) create mode 100644 freqtrade/leverage/funding_fees.py delete mode 100644 freqtrade/utils/__init__.py delete mode 100644 freqtrade/utils/hours_to_time.py diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index aa18634cf..4161b627d 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,5 +1,6 @@ """ Binance exchange subclass """ import logging +from datetime import datetime from typing import Dict, List, Optional import ccxt @@ -91,6 +92,13 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e + def _get_funding_rate(self, pair: str, when: datetime) -> Optional[float]: + """ + Get's the funding_rate for a pair at a specific date and time in the past + """ + # TODO-lev: implement + raise OperationalException("_get_funding_rate has not been implement on binance") + def _get_funding_fee( self, contract_size: float, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c9a932bff..3236ee8f8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1555,7 +1555,7 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def get_mark_price(self, pair: str, when: datetime): + def _get_mark_price(self, pair: str, when: datetime): """ Get's the value of the underlying asset for a futures contract at a specific date and time in the past @@ -1563,7 +1563,7 @@ class Exchange: # TODO-lev: implement raise OperationalException(f"get_mark_price has not been implemented for {self.name}") - def get_funding_rate(self, pair: str, when: datetime): + def _get_funding_rate(self, pair: str, when: datetime): """ Get's the funding_rate for a pair at a specific date and time in the past """ @@ -1587,7 +1587,7 @@ class Exchange: """ raise OperationalException(f"Funding fee has not been implemented for {self.name}") - def get_funding_fee_dates(self, open_date: datetime, close_date: datetime): + def _get_funding_fee_dates(self, open_date: datetime, close_date: datetime): """ Get's the date and time of every funding fee that happened between two datetimes """ @@ -1619,9 +1619,9 @@ class Exchange: """ fees: float = 0 - for date in self.get_funding_fee_dates(open_date, close_date): - funding_rate = self.get_funding_rate(pair, date) - mark_price = self.get_mark_price(pair, date) + for date in self._get_funding_fee_dates(open_date, close_date): + funding_rate = self._get_funding_rate(pair, date) + mark_price = self._get_mark_price(pair, date) fees += self._get_funding_fee(amount, mark_price, funding_rate) return fees diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 42d7ce050..11af26b32 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -3,7 +3,7 @@ import logging from typing import Any, Dict, List, Optional import ccxt - +from datetime import datetime from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -154,6 +154,10 @@ class Ftx(Exchange): return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] + def _get_funding_rate(self, pair: str, when: datetime) -> Optional[float]: + """FTX doesn't use this""" + return None + def _get_funding_fee( self, contract_size: float, @@ -162,9 +166,9 @@ class Ftx(Exchange): ) -> float: """ Calculates a single funding fee - Always paid in USD on FTX # TODO: How do we account for this - :param contract_size: The amount/quanity - :param mark_price: The price of the asset that the contract is based off of - :param funding_rate: Must be None on ftx + Always paid in USD on FTX # TODO: How do we account for this + : param contract_size: The amount/quanity + : param mark_price: The price of the asset that the contract is based off of + : param funding_rate: Must be None on ftx """ return (contract_size * mark_price) / 24 diff --git a/freqtrade/leverage/funding_fees.py b/freqtrade/leverage/funding_fees.py new file mode 100644 index 000000000..e6e9e9f0d --- /dev/null +++ b/freqtrade/leverage/funding_fees.py @@ -0,0 +1,75 @@ +from datetime import datetime, time +from typing import Optional + +from freqtrade.exceptions import OperationalException + + +def funding_fees( + exchange_name: str, + pair: str, + contract_size: float, + open_date: datetime, + close_date: datetime, + funding_times: [time] + # index_price: float, + # interest_rate: float +): + """ + Equation to calculate funding_fees on futures trades + + :param exchange_name: The exchanged being trading on + :param borrowed: The amount of currency being borrowed + :param rate: The rate of interest + :param hours: The time in hours that the currency has been borrowed for + + Raises: + OperationalException: Raised if freqtrade does + not support margin trading for this exchange + + Returns: The amount of interest owed (currency matches borrowed) + """ + exchange_name = exchange_name.lower() + # fees = 0 + if exchange_name == "binance": + for timeslot in funding_times: + # for each day in close_date - open_date + # mark_price = mark_price at this time + # rate = rate at this time + # fees = fees + funding_fee(exchange_name, contract_size, mark_price, rate) + # return fees + return + elif exchange_name == "kraken": + raise OperationalException("Funding_fees has not been implemented for Kraken") + elif exchange_name == "ftx": + # for timeslot in every hour since open_date: + # mark_price = mark_price at this time + # fees = fees + funding_fee(exchange_name, contract_size, mark_price, rate) + return + else: + raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + + +def funding_fee( + exchange_name: str, + contract_size: float, + mark_price: float, + rate: Optional[float], + # index_price: float, + # interest_rate: float +): + """ + Calculates a single funding fee + """ + if exchange_name == "binance": + assert isinstance(rate, float) + nominal_value = mark_price * contract_size + adjustment = nominal_value * rate + return adjustment + elif exchange_name == "kraken": + raise OperationalException("Funding fee has not been implemented for kraken") + elif exchange_name == "ftx": + """ + Always paid in USD on FTX # TODO: How do we account for this + """ + (contract_size * mark_price) / 24 + return diff --git a/freqtrade/utils/__init__.py b/freqtrade/utils/__init__.py deleted file mode 100644 index e6e76c589..000000000 --- a/freqtrade/utils/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# flake8: noqa: F401 -from freqtrade.utils.hours_to_time import hours_to_time diff --git a/freqtrade/utils/hours_to_time.py b/freqtrade/utils/hours_to_time.py deleted file mode 100644 index 139fd83a1..000000000 --- a/freqtrade/utils/hours_to_time.py +++ /dev/null @@ -1,11 +0,0 @@ -from datetime import datetime, time -from typing import List - - -def hours_to_time(hours: List[int]) -> List[time]: - ''' - :param hours: a list of hours as a time of day (e.g. [1, 16] is 01:00 and 16:00 o'clock) - :return: a list of datetime time objects that correspond to the hours in hours - ''' - # TODO-lev: These must be utc time - return [datetime.strptime(str(t), '%H').time() for t in hours] diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index f2b508761..6e51dd22d 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -105,3 +105,11 @@ def test_stoploss_adjust_binance(mocker, default_conf): # Test with invalid order case order['type'] = 'stop_loss' assert not exchange.stoploss_adjust(1501, order) + + +def test_get_funding_rate(): + return + + +def test__get_funding_fee(): + return diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1d23482fc..dc8e9ca2f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2994,3 +2994,15 @@ def test_get_funding_fees(default_conf, mocker, exchange_name): pair="XRP/USDT", since=unix_time ) + + +def test_get_mark_price(): + return + + +def test_get_funding_fee_dates(): + return + + +def test_calculate_funding_fees(): + return diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 3794bb79c..a4281c595 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta from random import randint from unittest.mock import MagicMock @@ -191,3 +192,18 @@ def test_get_order_id(mocker, default_conf): } } assert exchange.get_order_id_conditional(order) == '1111' + + +@pytest.mark.parametrize("pair,when", [ + ('XRP/USDT', datetime.utcnow()), + ('ADA/BTC', datetime.utcnow()), + ('XRP/USDT', datetime.utcnow() - timedelta(hours=30)), +]) +def test__get_funding_rate(default_conf, mocker, pair, when): + api_mock = MagicMock() + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="ftx") + assert exchange._get_funding_rate(pair, when) is None + + +def test__get_funding_fee(): + return From 232d10f300b9a7296bd0bb1b0896b6a37d037446 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 9 Sep 2021 01:44:35 -0600 Subject: [PATCH 17/59] removed leverage/funding_fees --- freqtrade/exchange/ftx.py | 3 +- freqtrade/leverage/funding_fees.py | 75 ------------------------------ 2 files changed, 2 insertions(+), 76 deletions(-) delete mode 100644 freqtrade/leverage/funding_fees.py diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 11af26b32..a70a69d7d 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,9 +1,10 @@ """ FTX exchange subclass """ import logging +from datetime import datetime from typing import Any, Dict, List, Optional import ccxt -from datetime import datetime + from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange diff --git a/freqtrade/leverage/funding_fees.py b/freqtrade/leverage/funding_fees.py deleted file mode 100644 index e6e9e9f0d..000000000 --- a/freqtrade/leverage/funding_fees.py +++ /dev/null @@ -1,75 +0,0 @@ -from datetime import datetime, time -from typing import Optional - -from freqtrade.exceptions import OperationalException - - -def funding_fees( - exchange_name: str, - pair: str, - contract_size: float, - open_date: datetime, - close_date: datetime, - funding_times: [time] - # index_price: float, - # interest_rate: float -): - """ - Equation to calculate funding_fees on futures trades - - :param exchange_name: The exchanged being trading on - :param borrowed: The amount of currency being borrowed - :param rate: The rate of interest - :param hours: The time in hours that the currency has been borrowed for - - Raises: - OperationalException: Raised if freqtrade does - not support margin trading for this exchange - - Returns: The amount of interest owed (currency matches borrowed) - """ - exchange_name = exchange_name.lower() - # fees = 0 - if exchange_name == "binance": - for timeslot in funding_times: - # for each day in close_date - open_date - # mark_price = mark_price at this time - # rate = rate at this time - # fees = fees + funding_fee(exchange_name, contract_size, mark_price, rate) - # return fees - return - elif exchange_name == "kraken": - raise OperationalException("Funding_fees has not been implemented for Kraken") - elif exchange_name == "ftx": - # for timeslot in every hour since open_date: - # mark_price = mark_price at this time - # fees = fees + funding_fee(exchange_name, contract_size, mark_price, rate) - return - else: - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") - - -def funding_fee( - exchange_name: str, - contract_size: float, - mark_price: float, - rate: Optional[float], - # index_price: float, - # interest_rate: float -): - """ - Calculates a single funding fee - """ - if exchange_name == "binance": - assert isinstance(rate, float) - nominal_value = mark_price * contract_size - adjustment = nominal_value * rate - return adjustment - elif exchange_name == "kraken": - raise OperationalException("Funding fee has not been implemented for kraken") - elif exchange_name == "ftx": - """ - Always paid in USD on FTX # TODO: How do we account for this - """ - (contract_size * mark_price) / 24 - return From 8e83cb4d642bb54e74a81a420f7e06e2e944b6c4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 10 Sep 2021 16:28:34 -0600 Subject: [PATCH 18/59] temp commit message --- freqtrade/exchange/binance.py | 9 +++++---- freqtrade/exchange/exchange.py | 8 -------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4161b627d..fa96eae1a 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -92,7 +92,7 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - def _get_funding_rate(self, pair: str, when: datetime) -> Optional[float]: + def _calculate_funding_rate(self, pair: str, premium_index: float) -> Optional[float]: """ Get's the funding_rate for a pair at a specific date and time in the past """ @@ -101,9 +101,10 @@ class Binance(Exchange): def _get_funding_fee( self, + pair: str, contract_size: float, mark_price: float, - funding_rate: Optional[float], + premium_index: Optional[float], ) -> float: """ Calculates a single funding fee @@ -113,8 +114,8 @@ class Binance(Exchange): - interest rate: 0.03% daily, BNBUSDT, LINKUSDT, and LTCUSDT are 0% - premium: varies by price difference between the perpetual contract and mark price """ - if funding_rate is None: + if premium_index is None: raise OperationalException("Funding rate cannot be None for Binance._get_funding_fee") nominal_value = mark_price * contract_size - adjustment = nominal_value * funding_rate + adjustment = nominal_value * _calculate_funding_rate(pair, premium_index) return adjustment diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3236ee8f8..2f49cdcaa 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1555,14 +1555,6 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def _get_mark_price(self, pair: str, when: datetime): - """ - Get's the value of the underlying asset for a futures contract - at a specific date and time in the past - """ - # TODO-lev: implement - raise OperationalException(f"get_mark_price has not been implemented for {self.name}") - def _get_funding_rate(self, pair: str, when: datetime): """ Get's the funding_rate for a pair at a specific date and time in the past From 98b00e8dafdcb1a6cee1f692e293844a1f86a5c4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 15 Sep 2021 22:28:10 -0600 Subject: [PATCH 19/59] merged with feat/short --- .github/PULL_REQUEST_TEMPLATE.md | 6 +- .github/workflows/ci.yml | 6 +- .travis.yml | 2 +- Dockerfile | 2 +- README.md | 9 +- build_helpers/install_ta-lib.sh | 9 +- docs/advanced-hyperopt.md | 300 ++---------------- docs/bot-usage.md | 8 +- docs/configuration.md | 4 +- docs/deprecated.md | 5 + docs/edge.md | 2 +- docs/exchanges.md | 14 + docs/faq.md | 2 +- docs/hyperopt.md | 21 +- docs/includes/pairlists.md | 20 ++ docs/index.md | 1 + docs/requirements-docs.txt | 2 +- docs/utils.md | 83 +---- freqtrade/__init__.py | 2 +- freqtrade/commands/__init__.py | 8 +- freqtrade/commands/arguments.py | 32 +- freqtrade/commands/build_config_commands.py | 12 +- freqtrade/commands/cli_options.py | 6 +- freqtrade/commands/deploy_commands.py | 52 +-- freqtrade/commands/hyperopt_commands.py | 1 + freqtrade/commands/list_commands.py | 22 +- freqtrade/configuration/__init__.py | 2 +- freqtrade/configuration/check_exchange.py | 13 - freqtrade/configuration/config_setup.py | 5 +- freqtrade/constants.py | 2 - freqtrade/data/history/history_utils.py | 3 +- freqtrade/enums/signaltype.py | 2 +- freqtrade/exchange/__init__.py | 2 +- freqtrade/exchange/binance.py | 24 +- freqtrade/exchange/common.py | 13 + freqtrade/exchange/exchange.py | 59 ++-- freqtrade/exchange/ftx.py | 5 +- freqtrade/exchange/gateio.py | 2 + freqtrade/exchange/kucoin.py | 2 + freqtrade/freqtradebot.py | 170 +++++----- freqtrade/loggers.py | 2 +- freqtrade/main.py | 6 +- freqtrade/optimize/backtesting.py | 5 +- freqtrade/optimize/edge_cli.py | 6 +- freqtrade/optimize/hyperopt.py | 65 ++-- freqtrade/optimize/hyperopt_auto.py | 43 +-- freqtrade/optimize/hyperopt_interface.py | 41 +-- freqtrade/persistence/models.py | 2 +- freqtrade/plugins/pairlist/PrecisionFilter.py | 1 + freqtrade/plugins/pairlist/VolumePairList.py | 2 +- .../plugins/pairlist/pairlist_helpers.py | 4 +- freqtrade/plugins/pairlistmanager.py | 2 +- .../protections/max_drawdown_protection.py | 1 + .../plugins/protections/stoploss_guard.py | 2 + freqtrade/resolvers/hyperopt_resolver.py | 38 --- freqtrade/rpc/api_server/uvicorn_threaded.py | 16 +- freqtrade/rpc/rpc.py | 16 +- freqtrade/strategy/interface.py | 20 +- freqtrade/templates/base_config.json.j2 | 9 +- freqtrade/templates/base_hyperopt.py.j2 | 137 -------- freqtrade/templates/sample_hyperopt.py | 180 ----------- .../templates/sample_hyperopt_advanced.py | 272 ---------------- .../subtemplates/exchange_binance.j2 | 28 +- .../subtemplates/exchange_bittrex.j2 | 10 - .../templates/subtemplates/exchange_kraken.j2 | 22 +- .../templates/subtemplates/exchange_kucoin.j2 | 18 ++ .../subtemplates/hyperopt_buy_guards_full.j2 | 8 - .../hyperopt_buy_guards_minimal.j2 | 2 - .../subtemplates/hyperopt_buy_space_full.j2 | 9 - .../hyperopt_buy_space_minimal.j2 | 3 - .../subtemplates/hyperopt_sell_guards_full.j2 | 8 - .../hyperopt_sell_guards_minimal.j2 | 2 - .../subtemplates/hyperopt_sell_space_full.j2 | 11 - .../hyperopt_sell_space_minimal.j2 | 5 - mkdocs.yml | 72 ++--- requirements-dev.txt | 2 +- requirements-hyperopt.txt | 2 +- requirements-plot.txt | 2 +- requirements.txt | 4 +- setup.sh | 14 +- tests/commands/test_commands.py | 76 +---- tests/exchange/test_binance.py | 35 +- tests/exchange/test_ccxt_compat.py | 2 + tests/exchange/test_exchange.py | 56 +++- tests/optimize/conftest.py | 2 +- .../hyperopts/hyperopt_test_sep_file.py | 207 ------------ tests/optimize/test_hyperopt.py | 217 +++---------- tests/plugins/test_pairlocks.py | 2 +- tests/strategy/test_interface.py | 5 + tests/test_configuration.py | 15 +- tests/test_directory_operations.py | 8 +- tests/test_freqtradebot.py | 84 ++--- tests/test_integration.py | 4 +- 93 files changed, 673 insertions(+), 2067 deletions(-) delete mode 100644 freqtrade/templates/base_hyperopt.py.j2 delete mode 100644 freqtrade/templates/sample_hyperopt.py delete mode 100644 freqtrade/templates/sample_hyperopt_advanced.py create mode 100644 freqtrade/templates/subtemplates/exchange_kucoin.j2 delete mode 100644 freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 delete mode 100644 freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 delete mode 100644 freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 delete mode 100644 freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 delete mode 100644 freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 delete mode 100644 freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 delete mode 100644 freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 delete mode 100644 freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 delete mode 100644 tests/optimize/hyperopts/hyperopt_test_sep_file.py diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 20ef27f0f..7c0655b20 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,14 +2,16 @@ Thank you for sending your pull request. But first, have you included unit tests, and is your code PEP8 conformant? [More details](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) ## Summary + Explain in one sentence the goal of this PR Solve the issue: #___ ## Quick changelog -- -- +- +- ## What's new? + *Explain in details what this PR solve or improve. You can include visuals.* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb767efb1..228a60389 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: run: | cp config_examples/config_bittrex.example.json config.json freqtrade create-userdir --userdir user_data - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all - name: Flake8 run: | @@ -180,7 +180,7 @@ jobs: run: | cp config_examples/config_bittrex.example.json config.json freqtrade create-userdir --userdir user_data - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all - name: Flake8 run: | @@ -247,7 +247,7 @@ jobs: run: | cp config_examples/config_bittrex.example.json config.json freqtrade create-userdir --userdir user_data - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily --print-all - name: Flake8 run: | diff --git a/.travis.yml b/.travis.yml index f2a6d508d..15c174bfe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,7 @@ jobs: - script: - cp config_examples/config_bittrex.example.json config.json - freqtrade create-userdir --userdir user_data - - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily + - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt-loss SharpeHyperOptLossDaily name: hyperopt - script: flake8 name: flake8 diff --git a/Dockerfile b/Dockerfile index 4c4722452..f7e26efe3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN mkdir /freqtrade \ && apt-get update \ && apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \ && apt-get clean \ - && useradd -u 1000 -G sudo -U -m ftuser \ + && useradd -u 1000 -G sudo -U -m -s /bin/bash ftuser \ && chown ftuser:ftuser /freqtrade \ # Allow sudoers && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers diff --git a/README.md b/README.md index 309fab94b..01effd7bc 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even - [X] [Bittrex](https://bittrex.com/) - [X] [Kraken](https://kraken.com/) - [X] [FTX](https://ftx.com) +- [X] [Gate.io](https://www.gate.io/ref/6266643) - [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested @@ -78,22 +79,22 @@ For any other type of installation please refer to [Installation doc](https://ww ``` usage: freqtrade [-h] [-V] - {trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit} + {trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} ... Free, open source crypto trading bot positional arguments: - {trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit} + {trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} trade Trade module. create-userdir Create user-data directory. new-config Create new config - new-hyperopt Create new hyperopt new-strategy Create new strategy download-data Download backtesting data. convert-data Convert candle (OHLCV) data from one format to another. convert-trade-data Convert trade data from one format to another. + list-data List downloaded data. backtesting Backtesting module. edge Edge module. hyperopt Hyperopt module. @@ -107,8 +108,10 @@ positional arguments: list-timeframes Print available timeframes for the exchange. show-trades Show trades. test-pairlist Test your pairlist configuration. + install-ui Install FreqUI plot-dataframe Plot candles with indicators. plot-profit Generate plot showing profits. + webserver Webserver module. optional arguments: -h, --help show this help message and exit diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh index dd87cf105..d12b16364 100755 --- a/build_helpers/install_ta-lib.sh +++ b/build_helpers/install_ta-lib.sh @@ -12,9 +12,12 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \ && ./configure --prefix=${INSTALL_LOC}/ \ && make -j$(nproc) \ - && which sudo && sudo make install || make install \ - && cd .. + && which sudo && sudo make install || make install + if [ -x "$(command -v apt-get)" ]; then + echo "Updating library path using ldconfig" + sudo ldconfig + fi + cd .. && rm -rf ./ta-lib/ else echo "TA-lib already installed, skipping installation" fi -# && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h \ diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 8f233438b..f2f52b7dd 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -67,10 +67,10 @@ Currently, the arguments are: This function needs to return a floating point number (`float`). Smaller numbers will be interpreted as better results. The parameters and balancing for this is up to you. !!! Note - This function is called once per iteration - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily. + This function is called once per epoch - so please make sure to have this as optimized as possible to not slow hyperopt down unnecessarily. -!!! Note - Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface later. +!!! Note "`*args` and `**kwargs`" + Please keep the arguments `*args` and `**kwargs` in the interface to allow us to extend this interface in the future. ## Overriding pre-defined spaces @@ -80,10 +80,24 @@ To override a pre-defined space (`roi_space`, `generate_roi_table`, `stoploss_sp class MyAwesomeStrategy(IStrategy): class HyperOpt: # Define a custom stoploss space. - def stoploss_space(self): + def stoploss_space(): return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')] + + # Define custom ROI space + def roi_space() -> List[Dimension]: + return [ + Integer(10, 120, name='roi_t1'), + Integer(10, 60, name='roi_t2'), + Integer(10, 40, name='roi_t3'), + SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'), + SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'), + SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'), + ] ``` +!!! Note + All overrides are optional and can be mixed/matched as necessary. + ## Space options For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types: @@ -105,281 +119,3 @@ from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Assuming the definition of a rather small space (`SKDecimal(0.10, 0.15, decimals=2, name='xxx')`) - SKDecimal will have 5 possibilities (`[0.10, 0.11, 0.12, 0.13, 0.14, 0.15]`). A corresponding real space `Real(0.10, 0.15 name='xxx')` on the other hand has an almost unlimited number of possibilities (`[0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]`). - ---- - -## Legacy Hyperopt - -This Section explains the configuration of an explicit Hyperopt file (separate to the strategy). - -!!! Warning "Deprecated / legacy mode" - Since the 2021.4 release you no longer have to write a separate hyperopt class, but all strategies can be hyperopted. - Please read the [main hyperopt page](hyperopt.md) for more details. - -### Prepare hyperopt file - -Configuring an explicit hyperopt file is similar to writing your own strategy, and many tasks will be similar. - -!!! Tip "About this page" - For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. - -#### Create a Custom Hyperopt File - -The simplest way to get started is to use the following command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. - -Let assume you want a hyperopt file `AwesomeHyperopt.py`: - -``` bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - -#### Legacy Hyperopt checklist - -Checklist on all tasks / possibilities in hyperopt - -Depending on the space you want to optimize, only some of the below are required: - -* fill `buy_strategy_generator` - for buy signal optimization -* fill `indicator_space` - for buy signal optimization -* fill `sell_strategy_generator` - for sell signal optimization -* fill `sell_indicator_space` - for sell signal optimization - -!!! Note - `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. - -Optional in hyperopt - can also be loaded from a strategy (recommended): - -* `populate_indicators` - fallback to create indicators -* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy -* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy - -!!! Note - You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. - Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. - -Rarely you may also need to override: - -* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default) -* `generate_roi_table` - for custom ROI optimization (if you need the ranges for the values in the ROI table that differ from default or the number of entries (steps) in the ROI table which differs from the default 4 steps) -* `stoploss_space` - for custom stoploss optimization (if you need the range for the stoploss parameter in the optimization hyperspace that differs from default) -* `trailing_space` - for custom trailing stop optimization (if you need the ranges for the trailing stop parameters in the optimization hyperspace that differ from default) - -#### Defining a buy signal optimization - -Let's say you are curious: should you use MACD crossings or lower Bollinger -Bands to trigger your buys. And you also wonder should you use RSI or ADX to -help with those buy decisions. If you decide to use RSI or ADX, which values -should I use for them? So let's use hyperparameter optimization to solve this -mystery. - -We will start by defining a search space: - -```python - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching strategy parameters - """ - return [ - Integer(20, 40, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['bb_lower', 'macd_cross_signal'], name='trigger') - ] -``` - -Above definition says: I have five parameters I want you to randomly combine -to find the best combination. Two of them are integer values (`adx-value` and `rsi-value`) and I want you test in the range of values 20 to 40. -Then we have three category variables. First two are either `True` or `False`. -We use these to either enable or disable the ADX and RSI guards. -The last one we call `trigger` and use it to decide which buy trigger we want to use. - -So let's write the buy strategy generator using these values: - -```python - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - conditions = [] - # GUARDS AND TRENDS - if 'adx-enabled' in params and params['adx-enabled']: - conditions.append(dataframe['adx'] > params['adx-value']) - if 'rsi-enabled' in params and params['rsi-enabled']: - conditions.append(dataframe['rsi'] < params['rsi-value']) - - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) - - # Check that volume is not 0 - conditions.append(dataframe['volume'] > 0) - - if conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 - - return dataframe - - return populate_buy_trend -``` - -Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. -It will use the given historical data and make buys based on the buy signals generated with the above function. -Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)). - -!!! Note - The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. - When you want to test an indicator that isn't used by the bot currently, remember to - add it to the `populate_indicators()` method in your strategy or hyperopt file. - -#### Sell optimization - -Similar to the buy-signal above, sell-signals can also be optimized. -Place the corresponding settings into the following methods - -* Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. -* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. - -The configuration and rules are the same than for buy signals. -To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. - -### Execute Hyperopt - -Once you have updated your hyperopt configuration you can run it. -Because hyperopt tries a lot of combinations to find the best parameters it will take time to get a good result. More time usually results in better results. - -We strongly recommend to use `screen` or `tmux` to prevent any connection loss. - -```bash -freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all -``` - -Use `` as the name of the custom hyperopt used. - -The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. -Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. - -The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. - -!!! Note - Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. - Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. - You can find a list of filenames with `ls -l user_data/hyperopt_results/`. - -#### Running Hyperopt using methods from a strategy - -Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. - -```bash -freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy -``` - -### Understand the Hyperopt Result - -Once Hyperopt is completed you can use the result to create a new strategy. -Given the following result from hyperopt: - -``` -Best result: - - 44/100: 135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722%). Avg duration 180.4 mins. Objective: 1.94367 - -Buy hyperspace params: -{ 'adx-value': 44, - 'rsi-value': 29, - 'adx-enabled': False, - 'rsi-enabled': True, - 'trigger': 'bb_lower'} -``` - -You should understand this result like: - -* The buy trigger that worked best was `bb_lower`. -* You should not use ADX because `adx-enabled: False`) -* You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`) - -You have to look inside your strategy file into `buy_strategy_generator()` -method, what those values match to. - -So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block: - -```python -(dataframe['rsi'] < 29.0) -``` - -Translating your whole hyperopt result as the new buy-signal would then look like: - -```python -def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: - dataframe.loc[ - ( - (dataframe['rsi'] < 29.0) & # rsi-value - dataframe['close'] < dataframe['bb_lowerband'] # trigger - ), - 'buy'] = 1 - return dataframe -``` - -### Validate backtesting results - -Once the optimized parameters and conditions have been implemented into your strategy, you should backtest the strategy to make sure everything is working as expected. - -To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. - -Should results not match, please double-check to make sure you transferred all conditions correctly. -Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. -You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). - -### Sharing methods with your strategy - -Hyperopt classes provide access to the Strategy via the `strategy` class attribute. -This can be a great way to reduce code duplication if used correctly, but will also complicate usage for inexperienced users. - -``` python -from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy -import freqtrade.vendor.qtpylib.indicators as qtpylib - -class MyAwesomeStrategy(IStrategy): - - buy_params = { - 'rsi-value': 30, - 'adx-value': 35, - } - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - return self.buy_strategy_generator(self.buy_params, dataframe, metadata) - - @staticmethod - def buy_strategy_generator(params, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe.loc[ - ( - qtpylib.crossed_above(dataframe['rsi'], params['rsi-value']) & - dataframe['adx'] > params['adx-value']) & - dataframe['volume'] > 0 - ) - , 'buy'] = 1 - return dataframe - -class MyAwesomeHyperOpt(IHyperOpt): - ... - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - # Call strategy's buy strategy generator - return self.StrategyClass.buy_strategy_generator(params, dataframe, metadata) - - return populate_buy_trend -``` diff --git a/docs/bot-usage.md b/docs/bot-usage.md index b65220722..c6a7f6103 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -12,22 +12,22 @@ This page explains the different parameters of the bot and how to run it. ``` usage: freqtrade [-h] [-V] - {trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit} + {trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} ... Free, open source crypto trading bot positional arguments: - {trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit} + {trade,create-userdir,new-config,new-strategy,download-data,convert-data,convert-trade-data,list-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,install-ui,plot-dataframe,plot-profit,webserver} trade Trade module. create-userdir Create user-data directory. new-config Create new config - new-hyperopt Create new hyperopt new-strategy Create new strategy download-data Download backtesting data. convert-data Convert candle (OHLCV) data from one format to another. convert-trade-data Convert trade data from one format to another. + list-data List downloaded data. backtesting Backtesting module. edge Edge module. hyperopt Hyperopt module. @@ -41,8 +41,10 @@ positional arguments: list-timeframes Print available timeframes for the exchange. show-trades Show trades. test-pairlist Test your pairlist configuration. + install-ui Install FreqUI plot-dataframe Plot candles with indicators. plot-profit Generate plot showing profits. + webserver Webserver module. optional arguments: -h, --help show this help message and exit diff --git a/docs/configuration.md b/docs/configuration.md index 09198e019..6ccea4c73 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -444,8 +444,8 @@ The possible values are: `gtc` (default), `fok` or `ioc`. ``` !!! Warning - This is ongoing work. For now, it is supported only for binance. - Please don't change the default value unless you know what you are doing and have researched the impact of using different values. + This is ongoing work. For now, it is supported only for binance and kucoin. + Please don't change the default value unless you know what you are doing and have researched the impact of using different values for your particular exchange. ### Exchange configuration diff --git a/docs/deprecated.md b/docs/deprecated.md index b7ad847e6..d86a7ac7a 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -38,3 +38,8 @@ Since only quoteVolume can be compared between assets, the other options (bidVol Using `order_book_min` and `order_book_max` used to allow stepping the orderbook and trying to find the next ROI slot - trying to place sell-orders early. As this does however increase risk and provides no benefit, it's been removed for maintainability purposes in 2021.7. + +### Legacy Hyperopt mode + +Using separate hyperopt files was deprecated in 2021.4 and was removed in 2021.9. +Please switch to the new [Parametrized Strategies](hyperopt.md) to benefit from the new hyperopt interface. diff --git a/docs/edge.md b/docs/edge.md index 237ff36f6..4402d767f 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -3,7 +3,7 @@ The `Edge Positioning` module uses probability to calculate your win rate and risk reward ratio. It will use these statistics to control your strategy trade entry points, position size and, stoploss. !!! Warning - WHen using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data. + When using `Edge positioning` with a dynamic whitelist (VolumePairList), make sure to also use `AgeFilter` and set it to at least `calculate_since_number_of_days` to avoid problems with missing data. !!! Note `Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. diff --git a/docs/exchanges.md b/docs/exchanges.md index 5f54a524e..c0fbdc694 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -4,6 +4,8 @@ This page combines common gotchas and informations which are exchange-specific a ## Binance +Binance supports [time_in_force](configuration.md#understand-order_time_in_force). + !!! Tip "Stoploss on Exchange" Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. @@ -56,6 +58,12 @@ Bittrex does not support market orders. If you have a message at the bot startup Bittrex also does not support `VolumePairlist` due to limited / split API constellation at the moment. Please use `StaticPairlist`. Other pairlists (other than `VolumePairlist`) should not be affected. +### Volume pairlist + +Bittrex does not support the direct usage of VolumePairList. This can however be worked around by using the advanced mode with `lookback_days: 1` (or more), which will emulate 24h volume. + +Read more in the [pairlist documentation](plugins.md#volumepairlist-advanced-mode). + ### Restricted markets Bittrex split its exchange into US and International versions. @@ -113,8 +121,12 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th "key": "your_exchange_key", "secret": "your_exchange_secret", "password": "your_exchange_api_key_password", + // ... +} ``` +Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force). + ### Kucoin Blacklists For Kucoin, please add `"KCS/"` to your blacklist to avoid issues. @@ -158,6 +170,8 @@ For example, to test the order type `FOK` with Kraken, and modify candle limit t "order_time_in_force": ["gtc", "fok"], "ohlcv_candle_limit": 200 } + //... +} ``` !!! Warning diff --git a/docs/faq.md b/docs/faq.md index b8a3a44d8..285625491 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -167,7 +167,7 @@ Since hyperopt uses Bayesian search, running for too many epochs may not produce It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. ```bash -freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000 +freqtrade hyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000 ``` ### Why does it take a long time to run hyperopt? diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 1eb90f1bc..e69b761c4 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -44,9 +44,8 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [-p PAIRS [PAIRS ...]] [--hyperopt NAME] - [--hyperopt-path PATH] [--eps] [--dmmp] - [--enable-protections] + [-p PAIRS [PAIRS ...]] [--hyperopt-path PATH] + [--eps] [--dmmp] [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,protection,default} [{all,buy,sell,roi,stoploss,trailing,protection,default} ...]] [--print-all] [--no-color] [--print-json] [-j JOBS] @@ -73,10 +72,8 @@ optional arguments: -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] Limit command to these pairs. Pairs are space- separated. - --hyperopt NAME Specify hyperopt class name which will be used by the - bot. - --hyperopt-path PATH Specify additional lookup path for Hyperopt and - Hyperopt Loss functions. + --hyperopt-path PATH Specify additional lookup path for Hyperopt Loss + functions. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). @@ -558,7 +555,7 @@ For example, to use one month of data, pass `--timerange 20210101-20210201` (fro Full command: ```bash -freqtrade hyperopt --hyperopt --strategy --timerange 20210101-20210201 +freqtrade hyperopt --strategy --timerange 20210101-20210201 ``` ### Running Hyperopt with Smaller Search Space @@ -684,7 +681,7 @@ If you have the `generate_roi_table()` and `roi_space()` methods in your custom Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). -A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +A sample for these methods can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). !!! Note "Reduced search space" To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. @@ -726,7 +723,7 @@ If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimiza If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default. -Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). !!! Note "Reduced search space" To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. @@ -764,10 +761,10 @@ As stated in the comment, you can also use it as the values of the corresponding If you are optimizing trailing stop values, Freqtrade creates the 'trailing' optimization hyperspace for you. By default, the `trailing_stop` parameter is always set to True in that hyperspace, the value of the `trailing_only_offset_is_reached` vary between True and False, the values of the `trailing_stop_positive` and `trailing_stop_positive_offset` parameters vary in the ranges 0.02...0.35 and 0.01...0.1 correspondingly, which is sufficient in most cases. -Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in the [overriding pre-defined spaces section](advanced-hyperopt.md#overriding-pre-defined-spaces). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#overriding-pre-defined-spaces) to change this to your needs. ### Reproducible results diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 6e23c9003..69e12d5dc 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -82,6 +82,8 @@ Filtering instances (not the first position in the list) will not apply any cach You can define a minimum volume with `min_value` - which will filter out pairs with a volume lower than the specified value in the specified timerange. +### VolumePairList Advanced mode + `VolumePairList` can also operate in an advanced mode to build volume over a given timerange of specified candle size. It utilizes exchange historical candle data, builds a typical price (calculated by (open+high+low)/3) and multiplies the typical price with every candle's volume. The sum is the `quoteVolume` over the given range. This allows different scenarios, for a more smoothened volume, when using longer ranges with larger candle sizes, or the opposite when using a short range with small candles. For convenience `lookback_days` can be specified, which will imply that 1d candles will be used for the lookback. In the example below the pairlist would be created based on the last 7 days: @@ -105,6 +107,24 @@ For convenience `lookback_days` can be specified, which will imply that 1d candl !!! Warning "Performance implications when using lookback range" If used in first position in combination with lookback, the computation of the range based volume can be time and resource consuming, as it downloads candles for all tradable pairs. Hence it's highly advised to use the standard approach with `VolumeFilter` to narrow the pairlist down for further range volume calculation. +??? Tip "Unsupported exchanges (Bittrex, Gemini)" + On some exchanges (like Bittrex and Gemini), regular VolumePairList does not work as the api does not natively provide 24h volume. This can be worked around by using candle data to build the volume. + To roughly simulate 24h volume, you can use the following configuration. + Please note that These pairlists will only refresh once per day. + + ```json + "pairlists": [ + { + "method": "VolumePairList", + "number_assets": 20, + "sort_key": "quoteVolume", + "min_value": 0, + "refresh_period": 86400, + "lookback_days": 1 + } + ], + ``` + More sophisticated approach can be used, by using `lookback_timeframe` for candle size and `lookback_period` which specifies the amount of candles. This example will build the volume pairs based on a rolling period of 3 days of 1h candles: ```json diff --git a/docs/index.md b/docs/index.md index fd3b8f224..7735117e2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,6 +40,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [Bittrex](https://bittrex.com/) - [X] [FTX](https://ftx.com) - [X] [Kraken](https://kraken.com/) +- [X] [Gate.io](https://www.gate.io/ref/6266643) - [ ] [potentially many others through ccxt](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index d820c9412..9927740c2 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.2 -mkdocs-material==7.2.5 +mkdocs-material==7.2.6 mdx_truly_sane_lists==1.2 pymdown-extensions==8.2 diff --git a/docs/utils.md b/docs/utils.md index 6395fb6f9..d8fbcacb7 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -26,9 +26,7 @@ optional arguments: ├── data ├── hyperopt_results ├── hyperopts -│   ├── sample_hyperopt_advanced.py │   ├── sample_hyperopt_loss.py -│   └── sample_hyperopt.py ├── notebooks │   └── strategy_analysis_example.ipynb ├── plot @@ -111,46 +109,11 @@ Using the advanced template (populates all optional functions and methods) freqtrade new-strategy --strategy AwesomeStrategy --template advanced ``` -## Create new hyperopt +## List Strategies -Creates a new hyperopt from a template similar to SampleHyperopt. -The file will be named inline with your class name, and will not overwrite existing files. +Use the `list-strategies` subcommand to see all strategies in one particular directory. -Results will be located in `user_data/hyperopts/.py`. - -``` output -usage: freqtrade new-hyperopt [-h] [--userdir PATH] [--hyperopt NAME] - [--template {full,minimal,advanced}] - -optional arguments: - -h, --help show this help message and exit - --userdir PATH, --user-data-dir PATH - Path to userdata directory. - --hyperopt NAME Specify hyperopt class name which will be used by the - bot. - --template {full,minimal,advanced} - Use a template which is either `minimal`, `full` - (containing multiple sample indicators) or `advanced`. - Default: `full`. -``` - -### Sample usage of new-hyperopt - -```bash -freqtrade new-hyperopt --hyperopt AwesomeHyperopt -``` - -With custom user directory - -```bash -freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt -``` - -## List Strategies and List Hyperopts - -Use the `list-strategies` subcommand to see all strategies in one particular directory and the `list-hyperopts` subcommand to list custom Hyperopts. - -These subcommands are useful for finding problems in your environment with loading strategies or hyperopt classes: modules with strategies or hyperopt classes that contain errors and failed to load are printed in red (LOAD FAILED), while strategies or hyperopt classes with duplicate names are printed in yellow (DUPLICATE NAME). +This subcommand is useful for finding problems in your environment with loading strategies: modules with strategies that contain errors and failed to load are printed in red (LOAD FAILED), while strategies with duplicate names are printed in yellow (DUPLICATE NAME). ``` usage: freqtrade list-strategies [-h] [-v] [--logfile FILE] [-V] [-c PATH] @@ -164,34 +127,6 @@ optional arguments: --no-color Disable colorization of hyperopt results. May be useful if you are redirecting output to a file. -Common arguments: - -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: - 'syslog', 'journald'. See the documentation for more - details. - -V, --version show program's version number and exit - -c PATH, --config PATH - Specify configuration file (default: `config.json`). - Multiple --config options may be used. Can be set to - `-` to read config from stdin. - -d PATH, --datadir PATH - Path to directory with historical backtesting data. - --userdir PATH, --user-data-dir PATH - Path to userdata directory. -``` -``` -usage: freqtrade list-hyperopts [-h] [-v] [--logfile FILE] [-V] [-c PATH] - [-d PATH] [--userdir PATH] - [--hyperopt-path PATH] [-1] [--no-color] - -optional arguments: - -h, --help show this help message and exit - --hyperopt-path PATH Specify additional lookup path for Hyperopt and - Hyperopt Loss functions. - -1, --one-column Print output in one column. - --no-color Disable colorization of hyperopt results. May be - useful if you are redirecting output to a file. - Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). --logfile FILE Log to the file specified. Special values are: @@ -211,18 +146,16 @@ Common arguments: !!! Warning Using these commands will try to load all python files from a directory. This can be a security risk if untrusted files reside in this directory, since all module-level code is executed. -Example: Search default strategies and hyperopts directories (within the default userdir). +Example: Search default strategies directories (within the default userdir). ``` bash freqtrade list-strategies -freqtrade list-hyperopts ``` -Example: Search strategies and hyperopts directory within the userdir. +Example: Search strategies directory within the userdir. ``` bash freqtrade list-strategies --userdir ~/.freqtrade/ -freqtrade list-hyperopts --userdir ~/.freqtrade/ ``` Example: Search dedicated strategy path. @@ -231,12 +164,6 @@ Example: Search dedicated strategy path. freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/ ``` -Example: Search dedicated hyperopt path. - -``` bash -freqtrade list-hyperopt --hyperopt-path ~/.freqtrade/hyperopts/ -``` - ## List Exchanges Use the `list-exchanges` subcommand to see the exchanges available for the bot. diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index e96e7f530..2747efc96 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -22,7 +22,7 @@ if __version__ == 'develop': # subprocess.check_output( # ['git', 'log', '--format="%h"', '-n 1'], # stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"') - except Exception: + except Exception: # pragma: no cover # git not available, ignore try: # Try Fallback to freqtrade_commit file (created by CI while building docker image) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 04e46ee23..a6f14cff7 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -11,11 +11,11 @@ from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.data_commands import (start_convert_data, start_download_data, start_list_data) from freqtrade.commands.deploy_commands import (start_create_userdir, start_install_ui, - start_new_hyperopt, start_new_strategy) + start_new_strategy) from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show -from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts, - start_list_markets, start_list_strategies, - start_list_timeframes, start_show_trades) +from freqtrade.commands.list_commands import (start_list_exchanges, start_list_markets, + start_list_strategies, start_list_timeframes, + start_show_trades) from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 899998310..d424f3ce7 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -55,8 +55,6 @@ ARGS_BUILD_CONFIG = ["config"] ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"] -ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"] - ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] @@ -92,10 +90,10 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", "list-markets", "list-pairs", "list-strategies", "list-data", - "list-hyperopts", "hyperopt-list", "hyperopt-show", + "hyperopt-list", "hyperopt-show", "plot-dataframe", "plot-profit", "show-trades"] -NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"] +NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-strategy"] class Arguments: @@ -174,12 +172,11 @@ class Arguments: from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir, start_download_data, start_edge, start_hyperopt, start_hyperopt_list, start_hyperopt_show, start_install_ui, - start_list_data, start_list_exchanges, start_list_hyperopts, - start_list_markets, start_list_strategies, - start_list_timeframes, start_new_config, start_new_hyperopt, - start_new_strategy, start_plot_dataframe, start_plot_profit, - start_show_trades, start_test_pairlist, start_trading, - start_webserver) + start_list_data, start_list_exchanges, start_list_markets, + start_list_strategies, start_list_timeframes, + start_new_config, start_new_strategy, start_plot_dataframe, + start_plot_profit, start_show_trades, start_test_pairlist, + start_trading, start_webserver) subparsers = self.parser.add_subparsers(dest='command', # Use custom message when no subhandler is added @@ -206,12 +203,6 @@ class Arguments: build_config_cmd.set_defaults(func=start_new_config) self._build_args(optionlist=ARGS_BUILD_CONFIG, parser=build_config_cmd) - # add new-hyperopt subcommand - build_hyperopt_cmd = subparsers.add_parser('new-hyperopt', - help="Create new hyperopt") - build_hyperopt_cmd.set_defaults(func=start_new_hyperopt) - self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd) - # add new-strategy subcommand build_strategy_cmd = subparsers.add_parser('new-strategy', help="Create new strategy") @@ -300,15 +291,6 @@ class Arguments: list_exchanges_cmd.set_defaults(func=start_list_exchanges) self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) - # Add list-hyperopts subcommand - list_hyperopts_cmd = subparsers.add_parser( - 'list-hyperopts', - help='Print available hyperopt classes.', - parents=[_common_parser], - ) - list_hyperopts_cmd.set_defaults(func=start_list_hyperopts) - self._build_args(optionlist=ARGS_LIST_HYPEROPTS, parser=list_hyperopts_cmd) - # Add list-markets subcommand list_markets_cmd = subparsers.add_parser( 'list-markets', diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 1fe90e83a..faa8a98f4 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -61,13 +61,13 @@ def ask_user_config() -> Dict[str, Any]: "type": "text", "name": "stake_currency", "message": "Please insert your stake currency:", - "default": 'BTC', + "default": 'USDT', }, { "type": "text", "name": "stake_amount", "message": f"Please insert your stake amount (Number or '{UNLIMITED_STAKE_AMOUNT}'):", - "default": "0.01", + "default": "100", "validate": lambda val: val == UNLIMITED_STAKE_AMOUNT or validate_is_float(val), "filter": lambda val: '"' + UNLIMITED_STAKE_AMOUNT + '"' if val == UNLIMITED_STAKE_AMOUNT @@ -105,6 +105,8 @@ def ask_user_config() -> Dict[str, Any]: "bittrex", "kraken", "ftx", + "kucoin", + "gateio", Separator(), "other", ], @@ -128,6 +130,12 @@ def ask_user_config() -> Dict[str, Any]: "message": "Insert Exchange Secret", "when": lambda x: not x['dry_run'] }, + { + "type": "password", + "name": "exchange_key_password", + "message": "Insert Exchange API Key password", + "when": lambda x: not x['dry_run'] and x['exchange_name'] == 'kucoin' + }, { "type": "confirm", "name": "telegram", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index cf7cb804c..e3c7fe464 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -1,7 +1,7 @@ """ Definition of cli arguments used in arguments.py """ -from argparse import ArgumentTypeError +from argparse import SUPPRESS, ArgumentTypeError from freqtrade import __version__, constants from freqtrade.constants import HYPEROPT_LOSS_BUILTIN @@ -203,13 +203,13 @@ AVAILABLE_CLI_OPTIONS = { # Hyperopt "hyperopt": Arg( '--hyperopt', - help='Specify hyperopt class name which will be used by the bot.', + help=SUPPRESS, metavar='NAME', required=False, ), "hyperopt_path": Arg( '--hyperopt-path', - help='Specify additional lookup path for Hyperopt and Hyperopt Loss functions.', + help='Specify additional lookup path for Hyperopt Loss functions.', metavar='PATH', ), "epochs": Arg( diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index c98335e0b..4f9e5bbad 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -7,7 +7,7 @@ import requests from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir -from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES +from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.misc import render_template, render_template_with_fallback @@ -87,56 +87,6 @@ def start_new_strategy(args: Dict[str, Any]) -> None: raise OperationalException("`new-strategy` requires --strategy to be set.") -def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: str) -> None: - """ - Deploys a new hyperopt template to hyperopt_path - """ - fallback = 'full' - buy_guards = render_template_with_fallback( - templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2", - templatefallbackfile=f"subtemplates/hyperopt_buy_guards_{fallback}.j2", - ) - sell_guards = render_template_with_fallback( - templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2", - templatefallbackfile=f"subtemplates/hyperopt_sell_guards_{fallback}.j2", - ) - buy_space = render_template_with_fallback( - templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2", - templatefallbackfile=f"subtemplates/hyperopt_buy_space_{fallback}.j2", - ) - sell_space = render_template_with_fallback( - templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2", - templatefallbackfile=f"subtemplates/hyperopt_sell_space_{fallback}.j2", - ) - - strategy_text = render_template(templatefile='base_hyperopt.py.j2', - arguments={"hyperopt": hyperopt_name, - "buy_guards": buy_guards, - "sell_guards": sell_guards, - "buy_space": buy_space, - "sell_space": sell_space, - }) - - logger.info(f"Writing hyperopt to `{hyperopt_path}`.") - hyperopt_path.write_text(strategy_text) - - -def start_new_hyperopt(args: Dict[str, Any]) -> None: - - config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - - if 'hyperopt' in args and args['hyperopt']: - - new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args['hyperopt'] + '.py') - - if new_path.exists(): - raise OperationalException(f"`{new_path}` already exists. " - "Please choose another Hyperopt Name.") - deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) - else: - raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") - - def clean_ui_subdir(directory: Path): if directory.is_dir(): logger.info("Removing UI directory content.") diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 089529d15..d2d30f399 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -102,3 +102,4 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: HyperoptTools.show_epoch_details(val, total_epochs, print_json, no_header, header_str="Epoch details") +# TODO-lev: Hyperopt optimal leverage diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 410b9b72b..464b38967 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -10,7 +10,7 @@ from colorama import init as colorama_init from tabulate import tabulate from freqtrade.configuration import setup_utils_configuration -from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES +from freqtrade.constants import USERPATH_STRATEGIES from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.exchange import market_is_active, validate_exchanges @@ -92,25 +92,6 @@ def start_list_strategies(args: Dict[str, Any]) -> None: _print_objs_tabular(strategy_objs, config.get('print_colorized', False)) -def start_list_hyperopts(args: Dict[str, Any]) -> None: - """ - Print files with HyperOpt custom classes available in the directory - """ - from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver - - config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - - directory = Path(config.get('hyperopt_path', config['user_data_dir'] / USERPATH_HYPEROPTS)) - hyperopt_objs = HyperOptResolver.search_all_objects(directory, not args['print_one_column']) - # Sort alphabetically - hyperopt_objs = sorted(hyperopt_objs, key=lambda x: x['name']) - - if args['print_one_column']: - print('\n'.join([s['name'] for s in hyperopt_objs])) - else: - _print_objs_tabular(hyperopt_objs, config.get('print_colorized', False)) - - def start_list_timeframes(args: Dict[str, Any]) -> None: """ Print timeframes available on Exchange @@ -148,6 +129,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: quote_currencies = args.get('quote_currencies', []) try: + # TODO-lev: Add leverage amount to get markets that support a certain leverage pairs = exchange.get_markets(base_currencies=base_currencies, quote_currencies=quote_currencies, pairs_only=pairs_only, diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 607f9cdef..730a4e47f 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa: F401 -from freqtrade.configuration.check_exchange import check_exchange, remove_credentials +from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.configuration import Configuration diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index c4f038103..fa1f47f9b 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -10,19 +10,6 @@ from freqtrade.exchange import (available_exchanges, is_exchange_known_ccxt, logger = logging.getLogger(__name__) -def remove_credentials(config: Dict[str, Any]) -> None: - """ - Removes exchange keys from the configuration and specifies dry-run - Used for backtesting / hyperopt / edge and utils. - Modifies the input dict! - """ - config['exchange']['key'] = '' - config['exchange']['secret'] = '' - config['exchange']['password'] = '' - config['exchange']['uid'] = '' - config['dry_run'] = True - - def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool: """ Check if the exchange name in the config file is supported by Freqtrade diff --git a/freqtrade/configuration/config_setup.py b/freqtrade/configuration/config_setup.py index 22836ab19..02f2d4089 100644 --- a/freqtrade/configuration/config_setup.py +++ b/freqtrade/configuration/config_setup.py @@ -3,7 +3,6 @@ from typing import Any, Dict from freqtrade.enums import RunMode -from .check_exchange import remove_credentials from .config_validation import validate_config_consistency from .configuration import Configuration @@ -21,8 +20,8 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str configuration = Configuration(args, method) config = configuration.get_config() - # Ensure we do not use Exchange credentials - remove_credentials(config) + # Ensure these modes are using Dry-run + config['dry_run'] = True validate_config_consistency(config) return config diff --git a/freqtrade/constants.py b/freqtrade/constants.py index efcd1aaca..9ca43d459 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -69,9 +69,7 @@ DUST_PER_COIN = { # Source files with destination directories within user-directory USER_DATA_FILES = { 'sample_strategy.py': USERPATH_STRATEGIES, - 'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS, 'sample_hyperopt_loss.py': USERPATH_HYPEROPTS, - 'sample_hyperopt.py': USERPATH_HYPEROPTS, 'strategy_analysis_example.ipynb': USERPATH_NOTEBOOKS, } diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 6f125aaa9..e6b8db322 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -197,7 +197,8 @@ def _download_pair_history(pair: str, *, timeframe=timeframe, since_ms=since_ms if since_ms else arrow.utcnow().shift( - days=-new_pairs_days).int_timestamp * 1000 + days=-new_pairs_days).int_timestamp * 1000, + is_new_pair=data.empty ) # TODO: Maybe move parsing to exchange class (?) new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair, diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index d2995d57a..fc57e1ce7 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -3,7 +3,7 @@ from enum import Enum class SignalType(Enum): """ - Enum to distinguish between buy and sell signals + Enum to distinguish between enter and exit signals """ BUY = "buy" SELL = "sell" diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index b0c88a51a..b08213d28 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,6 +1,6 @@ # flake8: noqa: F401 # isort: off -from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS +from freqtrade.exchange.common import remove_credentials, MAP_EXCHANGE_CHILDCLASS from freqtrade.exchange.exchange import Exchange # isort: on from freqtrade.exchange.bibox import Bibox diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index fa96eae1a..0f30c7aa4 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -3,6 +3,7 @@ import logging from datetime import datetime from typing import Dict, List, Optional +import arrow import ccxt from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, @@ -19,6 +20,7 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, "order_time_in_force": ['gtc', 'fok', 'ioc'], + "time_in_force_parameter": "timeInForce", "ohlcv_candle_limit": 1000, "trades_pagination": "id", "trades_pagination_arg": "fromId", @@ -117,5 +119,25 @@ class Binance(Exchange): if premium_index is None: raise OperationalException("Funding rate cannot be None for Binance._get_funding_fee") nominal_value = mark_price * contract_size - adjustment = nominal_value * _calculate_funding_rate(pair, premium_index) + funding_rate = self._calculate_funding_rate(pair, premium_index) + if funding_rate is None: + raise OperationalException("Funding rate should never be none on Binance") + adjustment = nominal_value * funding_rate return adjustment + + async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, + since_ms: int, is_new_pair: bool + ) -> List: + """ + Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date + Does not work for other exchanges, which don't return the earliest data when called with "0" + """ + if is_new_pair: + x = await self._async_get_candle_history(pair, timeframe, 0) + if x and x[2] and x[2][0] and x[2][0][0] > since_ms: + # Set starting date to first available candle. + since_ms = x[2][0][0] + logger.info(f"Candle-data for {pair} available starting with " + f"{arrow.get(since_ms // 1000).isoformat()}.") + return await super()._async_get_historic_ohlcv( + pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 694aa3aa2..7b89adf06 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -51,6 +51,19 @@ EXCHANGE_HAS_OPTIONAL = [ ] +def remove_credentials(config) -> None: + """ + Removes exchange keys from the configuration and specifies dry-run + Used for backtesting / hyperopt / edge and utils. + Modifies the input dict! + """ + if config.get('dry_run', False): + config['exchange']['key'] = '' + config['exchange']['secret'] = '' + config['exchange']['password'] = '' + config['exchange']['uid'] = '' + + def calculate_backoff(retrycount, max_retries): """ Calculate backoff diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2f49cdcaa..e58493f60 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -26,9 +26,9 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFun InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, - EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier, - retrier_async) -from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 + EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, + remove_credentials, retrier, retrier_async) +from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist @@ -54,12 +54,16 @@ class Exchange: # Parameters to add directly to buy/sell calls (like agreeing to trading agreement) _params: Dict = {} + # Additional headers - added to the ccxt object + _headers: Dict = {} + # Dict to specify which options each exchange implements # This defines defaults, which can be selectively overridden by subclasses using _ft_has # or by specifying them in the configuration. _ft_has_default: Dict = { "stoploss_on_exchange": False, "order_time_in_force": ["gtc"], + "time_in_force_parameter": "timeInForce", "ohlcv_params": {}, "ohlcv_candle_limit": 500, "ohlcv_partial_candle": True, @@ -101,6 +105,7 @@ class Exchange: # Holds all open sell orders for dry_run self._dry_run_open_orders: Dict[str, Any] = {} + remove_credentials(config) if config['dry_run']: logger.info('Instance is running with dry_run enabled') @@ -170,7 +175,7 @@ class Exchange: asyncio.get_event_loop().run_until_complete(self._api_async.close()) def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt, - ccxt_kwargs: dict = None) -> ccxt.Exchange: + ccxt_kwargs: Dict = {}) -> ccxt.Exchange: """ Initialize ccxt with given config and return valid ccxt instance. @@ -189,6 +194,10 @@ class Exchange: } if ccxt_kwargs: logger.info('Applying additional ccxt config: %s', ccxt_kwargs) + if self._headers: + # Inject static headers after the above output to not confuse users. + ccxt_kwargs = deep_merge_dicts({'headers': self._headers}, ccxt_kwargs) + if ccxt_kwargs: ex_config.update(ccxt_kwargs) try: @@ -717,7 +726,8 @@ class Exchange: params = self._params.copy() if time_in_force != 'gtc' and ordertype != 'market': - params.update({'timeInForce': time_in_force}) + param = self._ft_has.get('time_in_force_parameter', '') + params.update({param: time_in_force}) try: # Set the precision for amount and price(rate) as accepted by the exchange @@ -1186,7 +1196,7 @@ class Exchange: # Historic data def get_historic_ohlcv(self, pair: str, timeframe: str, - since_ms: int) -> List: + since_ms: int, is_new_pair: bool = False) -> List: """ Get candle history using asyncio and returns the list of candles. Handles all async work for this. @@ -1198,7 +1208,7 @@ class Exchange: """ return asyncio.get_event_loop().run_until_complete( self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, - since_ms=since_ms)) + since_ms=since_ms, is_new_pair=is_new_pair)) def get_historic_ohlcv_as_df(self, pair: str, timeframe: str, since_ms: int) -> DataFrame: @@ -1213,11 +1223,12 @@ class Exchange: return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True, drop_incomplete=self._ohlcv_partial_candle) - async def _async_get_historic_ohlcv(self, pair: str, - timeframe: str, - since_ms: int) -> List: + async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, + since_ms: int, is_new_pair: bool + ) -> List: """ Download historic ohlcv + :param is_new_pair: used by binance subclass to allow "fast" new pair downloading """ one_call = timeframe_to_msecs(timeframe) * self.ohlcv_candle_limit(timeframe) @@ -1230,21 +1241,22 @@ class Exchange: pair, timeframe, since) for since in range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)] - results = await asyncio.gather(*input_coroutines, return_exceptions=True) - - # Combine gathered results data: List = [] - for res in results: - if isinstance(res, Exception): - logger.warning("Async code raised an exception: %s", res.__class__.__name__) - continue - # Deconstruct tuple if it's not an exception - p, _, new_data = res - if p == pair: - data.extend(new_data) + # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling + for input_coro in chunks(input_coroutines, 100): + + results = await asyncio.gather(*input_coro, return_exceptions=True) + for res in results: + if isinstance(res, Exception): + logger.warning("Async code raised an exception: %s", res.__class__.__name__) + continue + # Deconstruct tuple if it's not an exception + p, _, new_data = res + if p == pair: + data.extend(new_data) # Sort data again after extending the result - above calls return in "async order" data = sorted(data, key=lambda x: x[0]) - logger.info("Downloaded data for %s with length %s.", pair, len(data)) + logger.info(f"Downloaded data for {pair} with length {len(data)}.") return data def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, @@ -1564,9 +1576,10 @@ class Exchange: def _get_funding_fee( self, + pair: str, contract_size: float, mark_price: float, - funding_rate: Optional[float], + premium_index: Optional[float], # index_price: float, # interest_rate: float) ) -> float: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index a70a69d7d..ae3659711 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -161,9 +161,12 @@ class Ftx(Exchange): def _get_funding_fee( self, + pair: str, contract_size: float, mark_price: float, - funding_rate: Optional[float], + premium_index: Optional[float], + # index_price: float, + # interest_rate: float) ) -> float: """ Calculates a single funding fee diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index 9c910a10d..e6ee01c8a 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -21,3 +21,5 @@ class Gateio(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 1000, } + + _headers = {'X-Gate-Channel-Id': 'freqtrade'} diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 22886a1d8..5d818f6a2 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -21,4 +21,6 @@ class Kucoin(Exchange): _ft_has: Dict = { "l2_limit_range": [20, 100], "l2_limit_range_required": False, + "order_time_in_force": ['gtc', 'fok', 'ioc'], + "time_in_force_parameter": "timeInForce", } diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 574ade803..601c18001 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -67,6 +67,7 @@ class FreqtradeBot(LoggingMixin): init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) + # TODO-lev: Do anything with this? self.wallets = Wallets(self.config, self.exchange) PairLocks.timeframe = self.config['timeframe'] @@ -78,6 +79,7 @@ class FreqtradeBot(LoggingMixin): # so anything in the Freqtradebot instance should be ready (initialized), including # the initial state of the bot. # Keep this at the end of this initialization method. + # TODO-lev: Do I need to consider the rpc, pairlists or dataprovider? self.rpc: RPCManager = RPCManager(self) self.pairlists = PairListManager(self.exchange, self.config) @@ -100,7 +102,7 @@ class FreqtradeBot(LoggingMixin): self.state = State[initial_state.upper()] if initial_state else State.STOPPED # Protect sell-logic from forcesell and vice versa - self._sell_lock = Lock() + self._exit_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) if 'trading_mode' in self.config: @@ -177,14 +179,14 @@ class FreqtradeBot(LoggingMixin): self.strategy.analyze(self.active_pair_whitelist) - with self._sell_lock: + with self._exit_lock: # Check and handle any timed out open orders self.check_handle_timedout() - # Protect from collisions with forcesell. + # Protect from collisions with forceexit. # Without this, freqtrade my try to recreate stoploss_on_exchange orders # while selling is in process, since telegram messages arrive in an different thread. - with self._sell_lock: + with self._exit_lock: trades = Trade.get_open_trades() # First process current opened trades (positions) self.exit_positions(trades) @@ -312,16 +314,16 @@ class FreqtradeBot(LoggingMixin): def handle_insufficient_funds(self, trade: Trade): """ - Determine if we ever opened a sell order for this trade. - If not, try update buy fees - otherwise "refind" the open order we obviously lost. + Determine if we ever opened a exiting order for this trade. + If not, try update entering fees - otherwise "refind" the open order we obviously lost. """ sell_order = trade.select_order('sell', None) if sell_order: self.refind_lost_order(trade) else: - self.reupdate_buy_order_fees(trade) + self.reupdate_enter_order_fees(trade) - def reupdate_buy_order_fees(self, trade: Trade): + def reupdate_enter_order_fees(self, trade: Trade): """ Get buy order from database, and try to reupdate. Handles trades where the initial fee-update did not work. @@ -335,7 +337,7 @@ class FreqtradeBot(LoggingMixin): def refind_lost_order(self, trade): """ Try refinding a lost trade. - Only used when InsufficientFunds appears on sell orders (stoploss or sell). + Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy). Tries to walk the stored orders and sell them off eventually. """ logger.info(f"Trying to refind lost order for {trade}") @@ -346,7 +348,7 @@ class FreqtradeBot(LoggingMixin): logger.debug(f"Order {order} is no longer open.") continue if order.ft_order_side == 'buy': - # Skip buy side - this is handled by reupdate_buy_order_fees + # Skip buy side - this is handled by reupdate_enter_order_fees continue try: fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, @@ -373,7 +375,7 @@ class FreqtradeBot(LoggingMixin): def enter_positions(self) -> int: """ - Tries to execute buy orders for new trades (positions) + Tries to execute entry orders for new trades (positions) """ trades_created = 0 @@ -389,7 +391,7 @@ class FreqtradeBot(LoggingMixin): if not whitelist: logger.info("No currency pair in active pair whitelist, " - "but checking to sell open trades.") + "but checking to exit open trades.") return trades_created if PairLocks.is_global_lock(): lock = PairLocks.get_pair_longest_lock('*') @@ -408,7 +410,7 @@ class FreqtradeBot(LoggingMixin): logger.warning('Unable to create trade for %s: %s', pair, exception) if not trades_created: - logger.debug("Found no buy signals for whitelisted currencies. Trying again...") + logger.debug("Found no enter signals for whitelisted currencies. Trying again...") return trades_created @@ -499,21 +501,21 @@ class FreqtradeBot(LoggingMixin): time_in_force = self.strategy.order_time_in_force['buy'] if price: - buy_limit_requested = price + enter_limit_requested = price else: # Calculate price - proposed_buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy") + proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=proposed_buy_rate)( + default_retval=proposed_enter_rate)( pair=pair, current_time=datetime.now(timezone.utc), - proposed_rate=proposed_buy_rate) + proposed_rate=proposed_enter_rate) - buy_limit_requested = self.get_valid_price(custom_entry_price, proposed_buy_rate) + enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) - if not buy_limit_requested: + if not enter_limit_requested: raise PricingError('Could not determine buy price.') - min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested, + min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested, self.strategy.stoploss) if not self.edge: @@ -521,7 +523,7 @@ class FreqtradeBot(LoggingMixin): stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( pair=pair, current_time=datetime.now(timezone.utc), - current_rate=buy_limit_requested, proposed_stake=stake_amount, + current_rate=enter_limit_requested, proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) @@ -531,27 +533,29 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " f"{stake_amount} ...") - amount = stake_amount / buy_limit_requested + amount = stake_amount / enter_limit_requested order_type = self.strategy.order_types['buy'] if forcebuy: # Forcebuy can define a different ordertype + # TODO-lev: get a forceshort? What is this order_type = self.strategy.order_types.get('forcebuy', order_type) + # TODO-lev: Will this work for shorting? if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( - pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, + pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", - amount=amount, rate=buy_limit_requested, + amount=amount, rate=enter_limit_requested, time_in_force=time_in_force) order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') order_id = order['id'] order_status = order.get('status', None) # we assume the order is executed at the price requested - buy_limit_filled_price = buy_limit_requested + enter_limit_filled_price = enter_limit_requested amount_requested = amount if order_status == 'expired' or order_status == 'rejected': @@ -574,13 +578,13 @@ class FreqtradeBot(LoggingMixin): ) stake_amount = order['cost'] amount = safe_value_fallback(order, 'filled', 'amount') - buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') + enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') # in case of FOK the order may be filled immediately and fully elif order_status == 'closed': stake_amount = order['cost'] amount = safe_value_fallback(order, 'filled', 'amount') - buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') + enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') @@ -598,9 +602,9 @@ class FreqtradeBot(LoggingMixin): amount_requested=amount_requested, fee_open=fee, fee_close=fee, - open_rate=buy_limit_filled_price, - open_rate_requested=buy_limit_requested, - open_date=open_date, + open_rate=enter_limit_filled_price, + open_rate_requested=enter_limit_requested, + open_date=datetime.utcnow(), exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), @@ -621,13 +625,13 @@ class FreqtradeBot(LoggingMixin): # Updating wallets self.wallets.update() - self._notify_buy(trade, order_type) + self._notify_enter(trade, order_type) return True - def _notify_buy(self, trade: Trade, order_type: str) -> None: + def _notify_enter(self, trade: Trade, order_type: str) -> None: """ - Sends rpc notification when a buy occurred. + Sends rpc notification when a entry order occurred. """ msg = { 'trade_id': trade.id, @@ -648,9 +652,9 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) - def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ - Sends rpc notification when a buy cancel occurred. + Sends rpc notification when a entry order cancel occurred. """ current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") @@ -674,7 +678,7 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) - def _notify_buy_fill(self, trade: Trade) -> None: + def _notify_enter_fill(self, trade: Trade) -> None: msg = { 'trade_id': trade.id, 'type': RPCMessageType.BUY_FILL, @@ -696,7 +700,7 @@ class FreqtradeBot(LoggingMixin): def exit_positions(self, trades: List[Any]) -> int: """ - Tries to execute sell orders for open trades (positions) + Tries to execute exit orders for open trades (positions) """ trades_closed = 0 for trade in trades: @@ -712,7 +716,7 @@ class FreqtradeBot(LoggingMixin): trades_closed += 1 except DependencyException as exception: - logger.warning('Unable to sell trade %s: %s', trade.pair, exception) + logger.warning('Unable to exit trade %s: %s', trade.pair, exception) # Updating wallets if any trade occurred if trades_closed: @@ -722,8 +726,8 @@ class FreqtradeBot(LoggingMixin): def handle_trade(self, trade: Trade) -> bool: """ - Sells the current pair if the threshold is reached and updates the trade record. - :return: True if trade has been sold, False otherwise + Sells/exits_short the current pair if the threshold is reached and updates the trade record. + :return: True if trade has been sold/exited_short, False otherwise """ if not trade.is_open: raise DependencyException(f'Attempt to handle closed trade: {trade}') @@ -731,7 +735,7 @@ class FreqtradeBot(LoggingMixin): logger.debug('Handling %s ...', trade) (buy, sell) = (False, False) - + # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal if (self.config.get('use_sell_signal', True) or self.config.get('ignore_roi_if_buy_signal', False)): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, @@ -744,8 +748,8 @@ class FreqtradeBot(LoggingMixin): ) logger.debug('checking sell') - sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") - if self._check_and_execute_sell(trade, sell_rate, buy, sell): + exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") + if self._check_and_execute_exit(trade, exit_rate, buy, sell): return True logger.debug('Found no sell signal for %s.', trade) @@ -775,7 +779,7 @@ class FreqtradeBot(LoggingMixin): except InvalidOrderException as e: trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') - logger.warning('Selling the trade forcefully') + logger.warning('Exiting the trade forcefully') self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple( sell_type=SellType.EMERGENCY_SELL)) @@ -789,6 +793,8 @@ class FreqtradeBot(LoggingMixin): Check if trade is fulfilled in which case the stoploss on exchange should be added immediately if stoploss on exchange is enabled. + # TODO-lev: liquidation price will always be on exchange, even though + # TODO-lev: stoploss_on_exchange might not be enabled """ logger.debug('Handling stoploss on exchange %s ...', trade) @@ -807,13 +813,14 @@ class FreqtradeBot(LoggingMixin): # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): + # TODO-lev: Update to exit reason trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_order=True) # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') - self._notify_sell(trade, "stoploss") + self._notify_exit(trade, "stoploss") return True if trade.open_order_id or not trade.is_open: @@ -822,7 +829,7 @@ class FreqtradeBot(LoggingMixin): # The trade can be closed already (sell-order fill confirmation came in this iteration) return False - # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange + # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange if not stoploss_order: stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss stop_price = trade.open_rate * (1 + stoploss) @@ -882,19 +889,19 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") - def _check_and_execute_sell(self, trade: Trade, sell_rate: float, + def _check_and_execute_exit(self, trade: Trade, exit_rate: float, buy: bool, sell: bool) -> bool: """ - Check and execute sell + Check and execute exit """ should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.now(timezone.utc), buy, sell, + trade, exit_rate, datetime.now(timezone.utc), buy, sell, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) if should_sell.sell_flag: logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}') - self.execute_trade_exit(trade, sell_rate, should_sell) + self.execute_trade_exit(trade, exit_rate, should_sell) return True return False @@ -937,7 +944,7 @@ class FreqtradeBot(LoggingMixin): default_retval=False)(pair=trade.pair, trade=trade, order=order))): - self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT']) + self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( fully_cancelled @@ -946,7 +953,7 @@ class FreqtradeBot(LoggingMixin): default_retval=False)(pair=trade.pair, trade=trade, order=order))): - self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT']) + self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) def cancel_all_open_orders(self) -> None: """ @@ -962,17 +969,18 @@ class FreqtradeBot(LoggingMixin): continue if order['side'] == 'buy': - self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) + self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) elif order['side'] == 'sell': - self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) + self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) Trade.commit() - def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool: + def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool: """ Buy cancel - cancel order :return: True if order was fully cancelled """ + # TODO-lev: Pay back borrowed/interest and transfer back on leveraged trades was_trade_fully_canceled = False # Cancelled orders may have the status of 'canceled' or 'closed' @@ -1017,6 +1025,8 @@ class FreqtradeBot(LoggingMixin): # to the order dict acquired before cancelling. # we need to fall back to the values from order if corder does not contain these keys. trade.amount = filled_amount + # TODO-lev: Check edge cases, we don't want to make leverage > 1.0 if we don't have to + trade.stake_amount = trade.amount * trade.open_rate self.update_trade_state(trade, trade.open_order_id, corder) @@ -1025,13 +1035,13 @@ class FreqtradeBot(LoggingMixin): reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" self.wallets.update() - self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'], - reason=reason) + self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'], + reason=reason) return was_trade_fully_canceled - def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str: + def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: """ - Sell cancel - cancel order and update trade + exit order cancel - cancel order and update trade :return: Reason for cancel """ # if trade is not partially completed, just cancel the order @@ -1063,14 +1073,14 @@ class FreqtradeBot(LoggingMixin): reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] self.wallets.update() - self._notify_sell_cancel( + self._notify_exit_cancel( trade, order_type=self.strategy.order_types['sell'], reason=reason ) return reason - def _safe_sell_amount(self, pair: str, amount: float) -> float: + def _safe_exit_amount(self, pair: str, amount: float) -> float: """ Get sellable amount. Should be trade.amount - but will fall back to the available amount if necessary. @@ -1081,6 +1091,7 @@ class FreqtradeBot(LoggingMixin): :return: amount to sell :raise: DependencyException: if available balance is not within 2% of the available amount. """ + # TODO-lev Maybe update? # Update wallets to ensure amounts tied up in a stoploss is now free! self.wallets.update() trade_base_currency = self.exchange.get_pair_base_currency(pair) @@ -1093,7 +1104,7 @@ class FreqtradeBot(LoggingMixin): return wallet_amount else: raise DependencyException( - f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") + f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}") def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: """ @@ -1103,7 +1114,7 @@ class FreqtradeBot(LoggingMixin): :param sell_reason: Reason the sell was triggered :return: True if it succeeds (supported) False (not supported) """ - sell_type = 'sell' + sell_type = 'sell' # TODO-lev: Update to exit if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' @@ -1142,23 +1153,26 @@ class FreqtradeBot(LoggingMixin): # but we allow this value to be changed) order_type = self.strategy.order_types.get("forcesell", order_type) - amount = self._safe_sell_amount(trade.pair, trade.amount) + amount = self._safe_exit_amount(trade.pair, trade.amount) time_in_force = self.strategy.order_time_in_force['sell'] if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, - current_time=datetime.now(timezone.utc)): - logger.info(f"User requested abortion of selling {trade.pair}") + current_time=datetime.now(timezone.utc)): # TODO-lev: Update to exit + logger.info(f"User requested abortion of exiting {trade.pair}") return False try: # Execute sell and update trade record - order = self.exchange.create_order(pair=trade.pair, - ordertype=order_type, side="sell", - amount=amount, rate=limit, - time_in_force=time_in_force - ) + order = self.exchange.create_order( + pair=trade.pair, + ordertype=order_type, + side="sell", + amount=amount, + rate=limit, + time_in_force=time_in_force + ) except InsufficientFundsError as e: logger.warning(f"Unable to place order {e}.") # Try to figure out what went wrong @@ -1177,15 +1191,15 @@ class FreqtradeBot(LoggingMixin): self.update_trade_state(trade, trade.open_order_id, order) Trade.commit() - # Lock pair for one candle to prevent immediate re-buys + # Lock pair for one candle to prevent immediate re-trading self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') - self._notify_sell(trade, order_type) + self._notify_exit(trade, order_type) return True - def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None: + def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None: """ Sends rpc notification when a sell occurred. """ @@ -1227,7 +1241,7 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) - def _notify_sell_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ Sends rpc notification when a sell cancel occurred. """ @@ -1322,13 +1336,13 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: if not stoploss_order and not trade.open_order_id: - self._notify_sell(trade, '', True) + self._notify_exit(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() elif not trade.open_order_id: # Buy fill - self._notify_buy_fill(trade) + self._notify_enter_fill(trade) return False @@ -1341,6 +1355,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: # Eat into dust if we own more than base currency + # TODO-lev: won't be in "base"(quote) currency for shorts logger.info(f"Fee amount for {trade} was in base currency - " f"Eating Fee {fee_abs} into dust.") elif fee_abs != 0: @@ -1417,6 +1432,7 @@ class FreqtradeBot(LoggingMixin): trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): + # TODO-lev: leverage? logger.warning(f"Amount {amount} does not match amount {trade.amount}") raise DependencyException("Half bought? Amounts don't match") diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index fbb05d879..5c5831695 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -87,7 +87,7 @@ def setup_logging(config: Dict[str, Any]) -> None: # syslog config. The messages should be equal for this. handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) logging.root.addHandler(handler_sl) - elif s[0] == 'journald': + elif s[0] == 'journald': # pragma: no cover try: from systemd.journal import JournaldLogHandler except ImportError: diff --git a/freqtrade/main.py b/freqtrade/main.py index 2fd3d32bb..6593fbcb6 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -9,7 +9,7 @@ from typing import Any, List # check min. python version -if sys.version_info < (3, 7): +if sys.version_info < (3, 7): # pragma: no cover sys.exit("Freqtrade requires Python version >= 3.7") from freqtrade.commands import Arguments @@ -46,7 +46,7 @@ def main(sysargv: List[str] = None) -> None: "`freqtrade --help` or `freqtrade --help`." ) - except SystemExit as e: + except SystemExit as e: # pragma: no cover return_code = e except KeyboardInterrupt: logger.info('SIGINT received, aborting ...') @@ -60,5 +60,5 @@ def main(sysargv: List[str] = None) -> None: sys.exit(return_code) -if __name__ == '__main__': +if __name__ == '__main__': # pragma: no cover main() diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 084142646..9bbb15fb2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple from pandas import DataFrame -from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency +from freqtrade.configuration import TimeRange, validate_config_consistency from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data import history from freqtrade.data.btanalysis import trade_list_to_dataframe @@ -61,8 +61,7 @@ class Backtesting: self.config = config self.results: Optional[Dict[str, Any]] = None - # Reset keys for backtesting - remove_credentials(self.config) + config['dry_run'] = True self.strategylist: List[IStrategy] = [] self.all_results: Dict[str, Dict] = {} diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index aab7def05..417faa685 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -7,7 +7,7 @@ import logging from typing import Any, Dict from freqtrade import constants -from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency +from freqtrade.configuration import TimeRange, validate_config_consistency from freqtrade.edge import Edge from freqtrade.optimize.optimize_reports import generate_edge_table from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -28,8 +28,8 @@ class EdgeCli: def __init__(self, config: Dict[str, Any]) -> None: self.config = config - # Reset keys for edge - remove_credentials(self.config) + # Ensure using dry-run + self.config['dry_run'] = True self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.strategy = StrategyResolver.load_strategy(self.config) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e0b35df32..14b155546 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -22,6 +22,7 @@ from pandas import DataFrame from freqtrade.constants import DATETIME_PRINT_FORMAT, FTHYPT_FILEVERSION, LAST_BT_RESULT_FN from freqtrade.data.converter import trim_dataframes from freqtrade.data.history import get_timerange +from freqtrade.exceptions import OperationalException from freqtrade.misc import deep_merge_dicts, file_dump_json, plural from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules @@ -30,7 +31,7 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer from freqtrade.optimize.optimize_reports import generate_strategy_stats -from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver +from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver # Suppress scikit-learn FutureWarnings from skopt @@ -78,10 +79,10 @@ class Hyperopt: if not self.config.get('hyperopt'): self.custom_hyperopt = HyperOptAuto(self.config) - self.auto_hyperopt = True else: - self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) - self.auto_hyperopt = False + raise OperationalException( + "Using separate Hyperopt files has been removed in 2021.9. Please convert " + "your existing Hyperopt file to the new Hyperoptable strategy interface") self.backtesting._set_strategy(self.backtesting.strategylist[0]) self.custom_hyperopt.strategy = self.backtesting.strategy @@ -103,31 +104,6 @@ class Hyperopt: self.num_epochs_saved = 0 self.current_best_epoch: Optional[Dict[str, Any]] = None - if not self.auto_hyperopt: - # Populate "fallback" functions here - # (hasattr is slow so should not be run during "regular" operations) - if hasattr(self.custom_hyperopt, 'populate_indicators'): - logger.warning( - "DEPRECATED: Using `populate_indicators()` in the hyperopt file is deprecated. " - "Please move these methods to your strategy." - ) - self.backtesting.strategy.populate_indicators = ( # type: ignore - self.custom_hyperopt.populate_indicators) # type: ignore - if hasattr(self.custom_hyperopt, 'populate_buy_trend'): - logger.warning( - "DEPRECATED: Using `populate_buy_trend()` in the hyperopt file is deprecated. " - "Please move these methods to your strategy." - ) - self.backtesting.strategy.populate_buy_trend = ( # type: ignore - self.custom_hyperopt.populate_buy_trend) # type: ignore - if hasattr(self.custom_hyperopt, 'populate_sell_trend'): - logger.warning( - "DEPRECATED: Using `populate_sell_trend()` in the hyperopt file is deprecated. " - "Please move these methods to your strategy." - ) - self.backtesting.strategy.populate_sell_trend = ( # type: ignore - self.custom_hyperopt.populate_sell_trend) # type: ignore - # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): self.max_open_trades = self.config['max_open_trades'] @@ -256,7 +232,7 @@ class Hyperopt: """ Assign the dimensions in the hyperoptimization space. """ - if self.auto_hyperopt and HyperoptTools.has_space(self.config, 'protection'): + if HyperoptTools.has_space(self.config, 'protection'): # Protections can only be optimized when using the Parameter interface logger.debug("Hyperopt has 'protection' space") # Enable Protections if protection space is selected. @@ -285,6 +261,15 @@ class Hyperopt: self.dimensions = (self.buy_space + self.sell_space + self.protection_space + self.roi_space + self.stoploss_space + self.trailing_space) + def assign_params(self, params_dict: Dict, category: str) -> None: + """ + Assign hyperoptable parameters + """ + for attr_name, attr in self.backtesting.strategy.enumerate_parameters(category): + if attr.optimize: + # noinspection PyProtectedMember + attr.value = params_dict[attr_name] + def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict: """ Used Optimize function. @@ -296,18 +281,13 @@ class Hyperopt: # Apply parameters if HyperoptTools.has_space(self.config, 'buy'): - self.backtesting.strategy.advise_buy = ( # type: ignore - self.custom_hyperopt.buy_strategy_generator(params_dict)) + self.assign_params(params_dict, 'buy') if HyperoptTools.has_space(self.config, 'sell'): - self.backtesting.strategy.advise_sell = ( # type: ignore - self.custom_hyperopt.sell_strategy_generator(params_dict)) + self.assign_params(params_dict, 'sell') if HyperoptTools.has_space(self.config, 'protection'): - for attr_name, attr in self.backtesting.strategy.enumerate_parameters('protection'): - if attr.optimize: - # noinspection PyProtectedMember - attr.value = params_dict[attr_name] + self.assign_params(params_dict, 'protection') if HyperoptTools.has_space(self.config, 'roi'): self.backtesting.strategy.minimal_roi = ( # type: ignore @@ -517,11 +497,10 @@ class Hyperopt: f"saved to '{self.results_file}'.") if self.current_best_epoch: - if self.auto_hyperopt: - HyperoptTools.try_export_params( - self.config, - self.backtesting.strategy.get_strategy_name(), - self.current_best_epoch) + HyperoptTools.try_export_params( + self.config, + self.backtesting.strategy.get_strategy_name(), + self.current_best_epoch) HyperoptTools.show_epoch_details(self.current_best_epoch, self.total_epochs, self.print_json) diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 43e92d9c6..1f11cec80 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -4,9 +4,9 @@ This module implements a convenience auto-hyperopt class, which can be used toge that implement IHyperStrategy interface. """ from contextlib import suppress -from typing import Any, Callable, Dict, List +from typing import Callable, Dict, List -from pandas import DataFrame +from freqtrade.exceptions import OperationalException with suppress(ImportError): @@ -15,6 +15,14 @@ with suppress(ImportError): from freqtrade.optimize.hyperopt_interface import IHyperOpt +def _format_exception_message(space: str) -> str: + raise OperationalException( + f"The '{space}' space is included into the hyperoptimization " + f"but no parameter for this space was not found in your Strategy. " + f"Please make sure to have parameters for this space enabled for optimization " + f"or remove the '{space}' space from hyperoptimization.") + + class HyperOptAuto(IHyperOpt): """ This class delegates functionality to Strategy(IHyperStrategy) and Strategy.HyperOpt classes. @@ -22,26 +30,6 @@ class HyperOptAuto(IHyperOpt): sell_indicator_space methods, but other hyperopt methods can be overridden as well. """ - def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: - def populate_buy_trend(dataframe: DataFrame, metadata: dict): - for attr_name, attr in self.strategy.enumerate_parameters('buy'): - if attr.optimize: - # noinspection PyProtectedMember - attr.value = params[attr_name] - return self.strategy.populate_buy_trend(dataframe, metadata) - - return populate_buy_trend - - def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: - def populate_sell_trend(dataframe: DataFrame, metadata: dict): - for attr_name, attr in self.strategy.enumerate_parameters('sell'): - if attr.optimize: - # noinspection PyProtectedMember - attr.value = params[attr_name] - return self.strategy.populate_sell_trend(dataframe, metadata) - - return populate_sell_trend - def _get_func(self, name) -> Callable: """ Return a function defined in Strategy.HyperOpt class, or one defined in super() class. @@ -60,21 +48,22 @@ class HyperOptAuto(IHyperOpt): if attr.optimize: yield attr.get_space(attr_name) - def _get_indicator_space(self, category, fallback_method_name): + def _get_indicator_space(self, category): + # TODO: is this necessary, or can we call "generate_space" directly? indicator_space = list(self._generate_indicator_space(category)) if len(indicator_space) > 0: return indicator_space else: - return self._get_func(fallback_method_name)() + _format_exception_message(category) def indicator_space(self) -> List['Dimension']: - return self._get_indicator_space('buy', 'indicator_space') + return self._get_indicator_space('buy') def sell_indicator_space(self) -> List['Dimension']: - return self._get_indicator_space('sell', 'sell_indicator_space') + return self._get_indicator_space('sell') def protection_space(self) -> List['Dimension']: - return self._get_indicator_space('protection', 'protection_space') + return self._get_indicator_space('protection') def generate_roi_table(self, params: Dict) -> Dict[int, float]: return self._get_func('generate_roi_table')(params) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 500798627..8fb40f557 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -5,11 +5,10 @@ This module defines the interface to apply for hyperopt import logging import math from abc import ABC -from typing import Any, Callable, Dict, List +from typing import Dict, List from skopt.space import Categorical, Dimension, Integer -from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict from freqtrade.optimize.space import SKDecimal @@ -19,13 +18,6 @@ from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) -def _format_exception_message(method: str, space: str) -> str: - return (f"The '{space}' space is included into the hyperoptimization " - f"but {method}() method is not found in your " - f"custom Hyperopt class. You should either implement this " - f"method or remove the '{space}' space from hyperoptimization.") - - class IHyperOpt(ABC): """ Interface for freqtrade hyperopt @@ -45,37 +37,6 @@ class IHyperOpt(ABC): IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED IHyperOpt.timeframe = str(config['timeframe']) - def buy_strategy_generator(self, params: Dict[str, Any]) -> Callable: - """ - Create a buy strategy generator. - """ - raise OperationalException(_format_exception_message('buy_strategy_generator', 'buy')) - - def sell_strategy_generator(self, params: Dict[str, Any]) -> Callable: - """ - Create a sell strategy generator. - """ - raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell')) - - def protection_space(self) -> List[Dimension]: - """ - Create a protection space. - Only supported by the Parameter interface. - """ - raise OperationalException(_format_exception_message('indicator_space', 'protection')) - - def indicator_space(self) -> List[Dimension]: - """ - Create an indicator space. - """ - raise OperationalException(_format_exception_message('indicator_space', 'buy')) - - def sell_indicator_space(self) -> List[Dimension]: - """ - Create a sell indicator space. - """ - raise OperationalException(_format_exception_message('sell_indicator_space', 'sell')) - def generate_roi_table(self, params: Dict) -> Dict[int, float]: """ Create a ROI table. diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e15d31d6c..5f7c2c080 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -555,7 +555,7 @@ class LocalTrade(): if self.is_open: payment = "BUY" if self.is_short else "SELL" # TODO-lev: On shorts, you buy a little bit more than the amount (amount + interest) - # This wll only print the original amount + # TODO-lev: This wll only print the original amount logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.') # TODO-lev: Double check this self.close(safe_value_fallback(order, 'average', 'price')) diff --git a/freqtrade/plugins/pairlist/PrecisionFilter.py b/freqtrade/plugins/pairlist/PrecisionFilter.py index a3c262e8c..2c02ccdb3 100644 --- a/freqtrade/plugins/pairlist/PrecisionFilter.py +++ b/freqtrade/plugins/pairlist/PrecisionFilter.py @@ -18,6 +18,7 @@ class PrecisionFilter(IPairList): pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + # TODO-lev: Liquidation price? if 'stoploss' not in self._config: raise OperationalException( 'PrecisionFilter can only work with stoploss defined. Please add the ' diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index c70e4a904..0ffc8a8c8 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -123,7 +123,7 @@ class VolumePairList(IPairList): filtered_tickers = [ v for k, v in tickers.items() if (self._exchange.get_pair_quote_currency(k) == self._stake_currency - and v[self._sort_key] is not None)] + and (self._use_range or v[self._sort_key] is not None))] pairlist = [s['symbol'] for s in filtered_tickers] pairlist = self.filter_pairlist(pairlist, tickers) diff --git a/freqtrade/plugins/pairlist/pairlist_helpers.py b/freqtrade/plugins/pairlist/pairlist_helpers.py index 924bfb293..1de27fcbd 100644 --- a/freqtrade/plugins/pairlist/pairlist_helpers.py +++ b/freqtrade/plugins/pairlist/pairlist_helpers.py @@ -17,7 +17,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str], if keep_invalid: for pair_wc in wildcardpl: try: - comp = re.compile(pair_wc) + comp = re.compile(pair_wc, re.IGNORECASE) result_partial = [ pair for pair in available_pairs if re.fullmatch(comp, pair) ] @@ -33,7 +33,7 @@ def expand_pairlist(wildcardpl: List[str], available_pairs: List[str], else: for pair_wc in wildcardpl: try: - comp = re.compile(pair_wc) + comp = re.compile(pair_wc, re.IGNORECASE) result += [ pair for pair in available_pairs if re.fullmatch(comp, pair) ] diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index face79729..93b5e90e2 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -127,7 +127,7 @@ class PairListManager(): :return: pairlist - whitelisted pairs """ try: - + # TODO-lev: filter for pairlists that are able to trade at the desired leverage whitelist = expand_pairlist(pairlist, self._exchange.get_markets().keys(), keep_invalid) except ValueError as err: logger.error(f"Pair whitelist contains an invalid Wildcard: {err}") diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index 67e204039..89b723c60 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -36,6 +36,7 @@ class MaxDrawdown(IProtection): """ LockReason to use """ + # TODO-lev: < for shorts? return (f'{drawdown} > {self._max_allowed_drawdown} in {self.lookback_period_str}, ' f'locking for {self.stop_duration_str}.') diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 40edf1204..888dc0316 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -32,6 +32,7 @@ class StoplossGuard(IProtection): def _reason(self) -> str: """ LockReason to use + #TODO-lev: check if min is the right word for shorts """ return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, ' f'locking for {self._stop_duration} min.') @@ -51,6 +52,7 @@ class StoplossGuard(IProtection): # if pair: # filters.append(Trade.pair == pair) # trades = Trade.get_trades(filters).all() + # TODO-lev: Liquidation price? trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) trades = [trade for trade in trades1 if (str(trade.sell_reason) in ( diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 8327a4d13..6f0263e93 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -9,7 +9,6 @@ from typing import Dict from freqtrade.constants import HYPEROPT_LOSS_BUILTIN, USERPATH_HYPEROPTS from freqtrade.exceptions import OperationalException -from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver @@ -17,43 +16,6 @@ from freqtrade.resolvers import IResolver logger = logging.getLogger(__name__) -class HyperOptResolver(IResolver): - """ - This class contains all the logic to load custom hyperopt class - """ - object_type = IHyperOpt - object_type_str = "Hyperopt" - user_subdir = USERPATH_HYPEROPTS - initial_search_path = None - - @staticmethod - def load_hyperopt(config: Dict) -> IHyperOpt: - """ - Load the custom hyperopt class from config parameter - :param config: configuration dictionary - """ - if not config.get('hyperopt'): - raise OperationalException("No Hyperopt set. Please use `--hyperopt` to specify " - "the Hyperopt class to use.") - - hyperopt_name = config['hyperopt'] - - hyperopt = HyperOptResolver.load_object(hyperopt_name, config, - kwargs={'config': config}, - extra_dir=config.get('hyperopt_path')) - - if not hasattr(hyperopt, 'populate_indicators'): - logger.info("Hyperopt class does not provide populate_indicators() method. " - "Using populate_indicators from the strategy.") - if not hasattr(hyperopt, 'populate_buy_trend'): - logger.info("Hyperopt class does not provide populate_buy_trend() method. " - "Using populate_buy_trend from the strategy.") - if not hasattr(hyperopt, 'populate_sell_trend'): - logger.info("Hyperopt class does not provide populate_sell_trend() method. " - "Using populate_sell_trend from the strategy.") - return hyperopt - - class HyperOptLossResolver(IResolver): """ This class contains all the logic to load custom hyperopt loss class diff --git a/freqtrade/rpc/api_server/uvicorn_threaded.py b/freqtrade/rpc/api_server/uvicorn_threaded.py index b63999f51..79af659c7 100644 --- a/freqtrade/rpc/api_server/uvicorn_threaded.py +++ b/freqtrade/rpc/api_server/uvicorn_threaded.py @@ -5,6 +5,20 @@ import time import uvicorn +def asyncio_setup() -> None: # pragma: no cover + # Set eventloop for win32 setups + # Reverts a change done in uvicorn 0.15.0 - which now sets the eventloop + # via policy. + import sys + + if sys.version_info >= (3, 8) and sys.platform == "win32": + import asyncio + import selectors + selector = selectors.SelectSelector() + loop = asyncio.SelectorEventLoop(selector) + asyncio.set_event_loop(loop) + + class UvicornServer(uvicorn.Server): """ Multithreaded server - as found in https://github.com/encode/uvicorn/issues/742 @@ -28,7 +42,7 @@ class UvicornServer(uvicorn.Server): try: import uvloop # noqa except ImportError: # pragma: no cover - from uvicorn.loops.asyncio import asyncio_setup + asyncio_setup() else: asyncio.set_event_loop(uvloop.new_event_loop()) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 95a37452b..7facacf97 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -36,6 +36,7 @@ class RPCException(Exception): raise RPCException('*Status:* `no active trade`') """ + # TODO-lev: Add new configuration options introduced with leveraged/short trading def __init__(self, message: str) -> None: super().__init__(self) @@ -403,8 +404,11 @@ class RPC: # Doing the sum is not right - overall profit needs to be based on initial capital profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0 starting_balance = self._freqtrade.wallets.get_starting_balance() - profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance - profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance + profit_closed_ratio_fromstart = 0 + profit_all_ratio_fromstart = 0 + if starting_balance: + profit_closed_ratio_fromstart = profit_closed_coin_sum / starting_balance + profit_all_ratio_fromstart = profit_all_coin_sum / starting_balance profit_all_fiat = self._fiat_converter.convert_amount( profit_all_coin_sum, @@ -545,12 +549,12 @@ class RPC: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) if order['side'] == 'buy': - fully_canceled = self._freqtrade.handle_cancel_buy( + fully_canceled = self._freqtrade.handle_cancel_enter( trade, order, CANCEL_REASON['FORCE_SELL']) if order['side'] == 'sell': # Cancel order - so it is placed anew with a fresh price. - self._freqtrade.handle_cancel_sell(trade, order, CANCEL_REASON['FORCE_SELL']) + self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL']) if not fully_canceled: # Get current rate and execute sell @@ -563,7 +567,7 @@ class RPC: if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') - with self._freqtrade._sell_lock: + with self._freqtrade._exit_lock: if trade_id == 'all': # Execute sell for all open orders for trade in Trade.get_open_trades(): @@ -625,7 +629,7 @@ class RPC: Handler for delete . Delete the given trade and close eventually existing open orders. """ - with self._freqtrade._sell_lock: + with self._freqtrade._exit_lock: c_count = 0 trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() if not trade: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 194ea557a..4730e9fe1 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -168,7 +168,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ Check buy enter timeout function callback. This method can be used to override the enter-timeout. - It is called whenever a limit buy/short order has been created, + It is called whenever a limit entry order has been created, and is not yet fully filled. Configuration options in `unfilledtimeout` will be verified before this, so ensure to set these timeouts high enough. @@ -178,7 +178,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param trade: trade object. :param order: Order dictionary as returned from CCXT. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: When True is returned, then the buy/short-order is cancelled. + :return bool: When True is returned, then the entry order is cancelled. """ return False @@ -212,7 +212,7 @@ class IStrategy(ABC, HyperStrategyMixin): def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, **kwargs) -> bool: """ - Called right before placing a buy/short order. + Called right before placing a entry order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -236,7 +236,7 @@ class IStrategy(ABC, HyperStrategyMixin): rate: float, time_in_force: str, sell_reason: str, current_time: datetime, **kwargs) -> bool: """ - Called right before placing a regular sell/exit_short order. + Called right before placing a regular exit order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -410,7 +410,7 @@ class IStrategy(ABC, HyperStrategyMixin): Checks if a pair is currently locked The 2nd, optional parameter ensures that locks are applied until the new candle arrives, and not stop at 14:00:00 - while the next candle arrives at 14:00:02 leaving a gap - of 2 seconds for a buy/short to happen on an old signal. + of 2 seconds for an entry order to happen on an old signal. :param pair: "Pair to check" :param candle_date: Date of the last candle. Optional, defaults to current date :returns: locking state of the pair in question. @@ -426,7 +426,7 @@ class IStrategy(ABC, HyperStrategyMixin): def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Parses the given candle (OHLCV) data and returns a populated DataFrame - add several TA indicators and buy/short signal to it + add several TA indicators and entry order signal to it :param dataframe: Dataframe containing data from exchange :param metadata: Metadata dictionary with additional data (e.g. 'pair') :return: DataFrame of candle (OHLCV) data with indicator data and signals added @@ -541,7 +541,7 @@ class IStrategy(ABC, HyperStrategyMixin): dataframe: DataFrame ) -> Tuple[bool, bool, Optional[str]]: """ - Calculates current signal based based on the buy/short or sell/exit_short + Calculates current signal based based on the entry order or exit order columns of the dataframe. Used by Bot to get the signal to buy, sell, short, or exit_short :param pair: pair in format ANT/BTC @@ -606,7 +606,7 @@ class IStrategy(ABC, HyperStrategyMixin): sell: bool, low: float = None, high: float = None, force_stoploss: float = 0) -> SellCheckTuple: """ - This function evaluates if one of the conditions required to trigger a sell/exit_short + This function evaluates if one of the conditions required to trigger an exit order has been reached, which can either be a stop-loss, ROI or exit-signal. :param low: Only used during backtesting to simulate (long)stoploss/(short)ROI :param high: Only used during backtesting, to simulate (short)stoploss/(long)ROI @@ -810,7 +810,7 @@ class IStrategy(ABC, HyperStrategyMixin): def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Based on TA indicators, populates the buy/short signal for the given dataframe + Based on TA indicators, populates the entry order signal for the given dataframe This method should not be overridden. :param dataframe: DataFrame :param metadata: Additional information dictionary, with details like the @@ -829,7 +829,7 @@ class IStrategy(ABC, HyperStrategyMixin): def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ - Based on TA indicators, populates the sell/exit_short signal for the given dataframe + Based on TA indicators, populates the exit order signal for the given dataframe This method should not be overridden. :param dataframe: DataFrame :param metadata: Additional information dictionary, with details like the diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index a5782f7cd..68eebdbd4 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -1,3 +1,10 @@ +{%set volume_pairlist = '{ + "method": "VolumePairList", + "number_assets": 20, + "sort_key": "quoteVolume", + "min_value": 0, + "refresh_period": 1800 + }' %} { "max_open_trades": {{ max_open_trades }}, "stake_currency": "{{ stake_currency }}", @@ -29,7 +36,7 @@ }, {{ exchange | indent(4) }}, "pairlists": [ - {"method": "StaticPairList"} + {{ '{"method": "StaticPairList"}' if exchange_name == 'bittrex' else volume_pairlist }} ], "edge": { "enabled": false, diff --git a/freqtrade/templates/base_hyperopt.py.j2 b/freqtrade/templates/base_hyperopt.py.j2 deleted file mode 100644 index f6ca1477a..000000000 --- a/freqtrade/templates/base_hyperopt.py.j2 +++ /dev/null @@ -1,137 +0,0 @@ -# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement - -# --- Do not remove these libs --- -from functools import reduce -from typing import Any, Callable, Dict, List - -import numpy as np # noqa -import pandas as pd # noqa -from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real # noqa - -from freqtrade.optimize.hyperopt_interface import IHyperOpt - -# -------------------------------- -# Add your lib to import here -import talib.abstract as ta # noqa -import freqtrade.vendor.qtpylib.indicators as qtpylib - - -class {{ hyperopt }}(IHyperOpt): - """ - This is a Hyperopt template to get you started. - - More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/ - - You should: - - Add any lib you need to build your hyperopt. - - You must keep: - - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator. - - The methods roi_space, generate_roi_table and stoploss_space are not required - and are provided by default. - However, you may override them if you need 'roi' and 'stoploss' spaces that - differ from the defaults offered by Freqtrade. - Sample implementation of these methods will be copied to `user_data/hyperopts` when - creating the user-data directory using `freqtrade create-userdir --userdir user_data`, - or is available online under the following URL: - https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py. - """ - - @staticmethod - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching buy strategy parameters. - """ - return [ - {{ buy_space | indent(12) }} - ] - - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Buy strategy Hyperopt will build and use. - """ - conditions = [] - - # GUARDS AND TRENDS - {{ buy_guards | indent(12) }} - - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'bb_lower': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) - if params['trigger'] == 'sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['close'], dataframe['sar'] - )) - - # Check that the candle had volume - conditions.append(dataframe['volume'] > 0) - - if conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 - - return dataframe - - return populate_buy_trend - - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters. - """ - return [ - {{ sell_space | indent(12) }} - ] - - @staticmethod - def sell_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the sell strategy parameters to be used by Hyperopt. - """ - def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Sell strategy Hyperopt will build and use. - """ - conditions = [] - - # GUARDS AND TRENDS - {{ sell_guards | indent(12) }} - - # TRIGGERS - if 'sell-trigger' in params: - if params['sell-trigger'] == 'sell-bb_upper': - conditions.append(dataframe['close'] > dataframe['bb_upperband']) - if params['sell-trigger'] == 'sell-macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macdsignal'], dataframe['macd'] - )) - if params['sell-trigger'] == 'sell-sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['sar'], dataframe['close'] - )) - - # Check that the candle had volume - conditions.append(dataframe['volume'] > 0) - - if conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'sell'] = 1 - - return dataframe - - return populate_sell_trend - diff --git a/freqtrade/templates/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py deleted file mode 100644 index 7ed726d7a..000000000 --- a/freqtrade/templates/sample_hyperopt.py +++ /dev/null @@ -1,180 +0,0 @@ -# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement -# isort: skip_file - -# --- Do not remove these libs --- -from functools import reduce -from typing import Any, Callable, Dict, List - -import numpy as np # noqa -import pandas as pd # noqa -from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real # noqa - -from freqtrade.optimize.hyperopt_interface import IHyperOpt - -# -------------------------------- -# Add your lib to import here -import talib.abstract as ta # noqa -import freqtrade.vendor.qtpylib.indicators as qtpylib - - -class SampleHyperOpt(IHyperOpt): - """ - This is a sample Hyperopt to inspire you. - - More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/ - - You should: - - Rename the class name to some unique name. - - Add any methods you want to build your hyperopt. - - Add any lib you need to build your hyperopt. - - An easier way to get a new hyperopt file is by using - `freqtrade new-hyperopt --hyperopt MyCoolHyperopt`. - - You must keep: - - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator. - - The methods roi_space, generate_roi_table and stoploss_space are not required - and are provided by default. - However, you may override them if you need 'roi' and 'stoploss' spaces that - differ from the defaults offered by Freqtrade. - Sample implementation of these methods will be copied to `user_data/hyperopts` when - creating the user-data directory using `freqtrade create-userdir --userdir user_data`, - or is available online under the following URL: - https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py. - """ - - @staticmethod - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching buy strategy parameters. - """ - return [ - Integer(10, 25, name='mfi-value'), - Integer(15, 45, name='fastd-value'), - Integer(20, 50, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='mfi-enabled'), - Categorical([True, False], name='fastd-enabled'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger') - ] - - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Buy strategy Hyperopt will build and use. - """ - long_conditions = [] - - # GUARDS AND TRENDS - if 'mfi-enabled' in params and params['mfi-enabled']: - long_conditions.append(dataframe['mfi'] < params['mfi-value']) - if 'fastd-enabled' in params and params['fastd-enabled']: - long_conditions.append(dataframe['fastd'] < params['fastd-value']) - if 'adx-enabled' in params and params['adx-enabled']: - long_conditions.append(dataframe['adx'] > params['adx-value']) - if 'rsi-enabled' in params and params['rsi-enabled']: - long_conditions.append(dataframe['rsi'] < params['rsi-value']) - - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'boll': - long_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - long_conditions.append(qtpylib.crossed_above( - dataframe['macd'], - dataframe['macdsignal'] - )) - if params['trigger'] == 'sar_reversal': - long_conditions.append(qtpylib.crossed_above( - dataframe['close'], - dataframe['sar'] - )) - - # Check that volume is not 0 - long_conditions.append(dataframe['volume'] > 0) - - if long_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, long_conditions), - 'buy'] = 1 - - return dataframe - - return populate_buy_trend - - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters. - """ - return [ - Integer(75, 100, name='sell-mfi-value'), - Integer(50, 100, name='sell-fastd-value'), - Integer(50, 100, name='sell-adx-value'), - Integer(60, 100, name='sell-rsi-value'), - Categorical([True, False], name='sell-mfi-enabled'), - Categorical([True, False], name='sell-fastd-enabled'), - Categorical([True, False], name='sell-adx-enabled'), - Categorical([True, False], name='sell-rsi-enabled'), - Categorical(['sell-boll', - 'sell-macd_cross_signal', - 'sell-sar_reversal'], - name='sell-trigger' - ) - ] - - @staticmethod - def sell_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the sell strategy parameters to be used by Hyperopt. - """ - def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Sell strategy Hyperopt will build and use. - """ - exit_long_conditions = [] - - # GUARDS AND TRENDS - if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: - exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value']) - if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: - exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value']) - if 'sell-adx-enabled' in params and params['sell-adx-enabled']: - exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value']) - if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: - exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value']) - - # TRIGGERS - if 'sell-trigger' in params: - if params['sell-trigger'] == 'sell-boll': - exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband']) - if params['sell-trigger'] == 'sell-macd_cross_signal': - exit_long_conditions.append(qtpylib.crossed_above( - dataframe['macdsignal'], - dataframe['macd'] - )) - if params['sell-trigger'] == 'sell-sar_reversal': - exit_long_conditions.append(qtpylib.crossed_above( - dataframe['sar'], - dataframe['close'] - )) - - # Check that volume is not 0 - exit_long_conditions.append(dataframe['volume'] > 0) - - if exit_long_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, exit_long_conditions), - 'sell'] = 1 - - return dataframe - - return populate_sell_trend diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py deleted file mode 100644 index 733f1ef3e..000000000 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ /dev/null @@ -1,272 +0,0 @@ -# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement -# isort: skip_file -# --- Do not remove these libs --- -from functools import reduce -from typing import Any, Callable, Dict, List - -import numpy as np # noqa -import pandas as pd # noqa -from pandas import DataFrame -from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa - -from freqtrade.optimize.hyperopt_interface import IHyperOpt - -# -------------------------------- -# Add your lib to import here -import talib.abstract as ta # noqa -import freqtrade.vendor.qtpylib.indicators as qtpylib - - -class AdvancedSampleHyperOpt(IHyperOpt): - """ - This is a sample hyperopt to inspire you. - Feel free to customize it. - - More information in the documentation: https://www.freqtrade.io/en/latest/hyperopt/ - - You should: - - Rename the class name to some unique name. - - Add any methods you want to build your hyperopt. - - Add any lib you need to build your hyperopt. - - You must keep: - - The prototypes for the methods: populate_indicators, indicator_space, buy_strategy_generator. - - The methods roi_space, generate_roi_table and stoploss_space are not required - and are provided by default. - However, you may override them if you need the - 'roi' and the 'stoploss' spaces that differ from the defaults offered by Freqtrade. - - This sample illustrates how to override these methods. - """ - @staticmethod - def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - This method can also be loaded from the strategy, if it doesn't exist in the hyperopt class. - """ - dataframe['adx'] = ta.ADX(dataframe) - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - dataframe['mfi'] = ta.MFI(dataframe) - dataframe['rsi'] = ta.RSI(dataframe) - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # Bollinger bands - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_upperband'] = bollinger['upper'] - dataframe['sar'] = ta.SAR(dataframe) - return dataframe - - @staticmethod - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching buy strategy parameters. - """ - return [ - Integer(10, 25, name='mfi-value'), - Integer(15, 45, name='fastd-value'), - Integer(20, 50, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='mfi-enabled'), - Categorical([True, False], name='fastd-enabled'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger') - ] - - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by hyperopt - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Buy strategy Hyperopt will build and use - """ - long_conditions = [] - # GUARDS AND TRENDS - if 'mfi-enabled' in params and params['mfi-enabled']: - long_conditions.append(dataframe['mfi'] < params['mfi-value']) - if 'fastd-enabled' in params and params['fastd-enabled']: - long_conditions.append(dataframe['fastd'] < params['fastd-value']) - if 'adx-enabled' in params and params['adx-enabled']: - long_conditions.append(dataframe['adx'] > params['adx-value']) - if 'rsi-enabled' in params and params['rsi-enabled']: - long_conditions.append(dataframe['rsi'] < params['rsi-value']) - - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'boll': - long_conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - long_conditions.append(qtpylib.crossed_above( - dataframe['macd'], dataframe['macdsignal'] - )) - if params['trigger'] == 'sar_reversal': - long_conditions.append(qtpylib.crossed_above( - dataframe['close'], dataframe['sar'] - )) - - # Check that volume is not 0 - long_conditions.append(dataframe['volume'] > 0) - - if long_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, long_conditions), - 'buy'] = 1 - - return dataframe - - return populate_buy_trend - - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters. - """ - return [ - Integer(75, 100, name='sell-mfi-value'), - Integer(50, 100, name='sell-fastd-value'), - Integer(50, 100, name='sell-adx-value'), - Integer(60, 100, name='sell-rsi-value'), - Categorical([True, False], name='sell-mfi-enabled'), - Categorical([True, False], name='sell-fastd-enabled'), - Categorical([True, False], name='sell-adx-enabled'), - Categorical([True, False], name='sell-rsi-enabled'), - Categorical(['sell-boll', - 'sell-macd_cross_signal', - 'sell-sar_reversal'], - name='sell-trigger') - ] - - @staticmethod - def sell_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the sell strategy parameters to be used by hyperopt - """ - def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Sell strategy Hyperopt will build and use - """ - # print(params) - exit_long_conditions = [] - # GUARDS AND TRENDS - if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: - exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value']) - if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: - exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value']) - if 'sell-adx-enabled' in params and params['sell-adx-enabled']: - exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value']) - if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: - exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value']) - - # TRIGGERS - if 'sell-trigger' in params: - if params['sell-trigger'] == 'sell-boll': - exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband']) - if params['sell-trigger'] == 'sell-macd_cross_signal': - exit_long_conditions.append(qtpylib.crossed_above( - dataframe['macdsignal'], - dataframe['macd'] - )) - if params['sell-trigger'] == 'sell-sar_reversal': - exit_long_conditions.append(qtpylib.crossed_above( - dataframe['sar'], - dataframe['close'] - )) - - # Check that volume is not 0 - exit_long_conditions.append(dataframe['volume'] > 0) - - if exit_long_conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, exit_long_conditions), - 'sell'] = 1 - - return dataframe - - return populate_sell_trend - - @staticmethod - def generate_roi_table(params: Dict) -> Dict[int, float]: - """ - Generate the ROI table that will be used by Hyperopt - - This implementation generates the default legacy Freqtrade ROI tables. - - Change it if you need different number of steps in the generated - ROI tables or other structure of the ROI tables. - - Please keep it aligned with parameters in the 'roi' optimization - hyperspace defined by the roi_space method. - """ - roi_table = {} - roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3'] - roi_table[params['roi_t3']] = params['roi_p1'] + params['roi_p2'] - roi_table[params['roi_t3'] + params['roi_t2']] = params['roi_p1'] - roi_table[params['roi_t3'] + params['roi_t2'] + params['roi_t1']] = 0 - - return roi_table - - @staticmethod - def roi_space() -> List[Dimension]: - """ - Values to search for each ROI steps - - Override it if you need some different ranges for the parameters in the - 'roi' optimization hyperspace. - - Please keep it aligned with the implementation of the - generate_roi_table method. - """ - return [ - Integer(10, 120, name='roi_t1'), - Integer(10, 60, name='roi_t2'), - Integer(10, 40, name='roi_t3'), - SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'), - SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'), - SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'), - ] - - @staticmethod - def stoploss_space() -> List[Dimension]: - """ - Stoploss Value to search - - Override it if you need some different range for the parameter in the - 'stoploss' optimization hyperspace. - """ - return [ - SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'), - ] - - @staticmethod - def trailing_space() -> List[Dimension]: - """ - Create a trailing stoploss space. - - You may override it in your custom Hyperopt class. - """ - return [ - # It was decided to always set trailing_stop is to True if the 'trailing' hyperspace - # is used. Otherwise hyperopt will vary other parameters that won't have effect if - # trailing_stop is set False. - # This parameter is included into the hyperspace dimensions rather than assigning - # it explicitly in the code in order to have it printed in the results along with - # other 'trailing' hyperspace parameters. - Categorical([True], name='trailing_stop'), - - SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'), - - # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', - # so this intermediate parameter is used as the value of the difference between - # them. The value of the 'trailing_stop_positive_offset' is constructed in the - # generate_trailing_params() method. - # This is similar to the hyperspace dimensions used for constructing the ROI tables. - SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'), - - Categorical([True, False], name='trailing_only_offset_is_reached'), - ] diff --git a/freqtrade/templates/subtemplates/exchange_binance.j2 b/freqtrade/templates/subtemplates/exchange_binance.j2 index 38ba4fa5c..de58b6f72 100644 --- a/freqtrade/templates/subtemplates/exchange_binance.j2 +++ b/freqtrade/templates/subtemplates/exchange_binance.j2 @@ -8,34 +8,8 @@ "rateLimit": 200 }, "pair_whitelist": [ - "ALGO/BTC", - "ATOM/BTC", - "BAT/BTC", - "BCH/BTC", - "BRD/BTC", - "EOS/BTC", - "ETH/BTC", - "IOTA/BTC", - "LINK/BTC", - "LTC/BTC", - "NEO/BTC", - "NXS/BTC", - "XMR/BTC", - "XRP/BTC", - "XTZ/BTC" ], "pair_blacklist": [ - "BNB/BTC", - "BNB/BUSD", - "BNB/ETH", - "BNB/EUR", - "BNB/NGN", - "BNB/PAX", - "BNB/RUB", - "BNB/TRY", - "BNB/TUSD", - "BNB/USDC", - "BNB/USDS", - "BNB/USDT" + "BNB/.*" ] } diff --git a/freqtrade/templates/subtemplates/exchange_bittrex.j2 b/freqtrade/templates/subtemplates/exchange_bittrex.j2 index 7b27318ca..0394790ce 100644 --- a/freqtrade/templates/subtemplates/exchange_bittrex.j2 +++ b/freqtrade/templates/subtemplates/exchange_bittrex.j2 @@ -15,16 +15,6 @@ "rateLimit": 500 }, "pair_whitelist": [ - "ETH/BTC", - "LTC/BTC", - "ETC/BTC", - "DASH/BTC", - "ZEC/BTC", - "XLM/BTC", - "XRP/BTC", - "TRX/BTC", - "ADA/BTC", - "XMR/BTC" ], "pair_blacklist": [ ] diff --git a/freqtrade/templates/subtemplates/exchange_kraken.j2 b/freqtrade/templates/subtemplates/exchange_kraken.j2 index 7139a0830..4d0e4c1ff 100644 --- a/freqtrade/templates/subtemplates/exchange_kraken.j2 +++ b/freqtrade/templates/subtemplates/exchange_kraken.j2 @@ -7,28 +7,10 @@ "ccxt_async_config": { "enableRateLimit": true, "rateLimit": 1000 + // Enable the below for downoading data. + //"rateLimit": 3100 }, "pair_whitelist": [ - "ADA/EUR", - "ATOM/EUR", - "BAT/EUR", - "BCH/EUR", - "BTC/EUR", - "DAI/EUR", - "DASH/EUR", - "EOS/EUR", - "ETC/EUR", - "ETH/EUR", - "LINK/EUR", - "LTC/EUR", - "QTUM/EUR", - "REP/EUR", - "WAVES/EUR", - "XLM/EUR", - "XMR/EUR", - "XRP/EUR", - "XTZ/EUR", - "ZEC/EUR" ], "pair_blacklist": [ diff --git a/freqtrade/templates/subtemplates/exchange_kucoin.j2 b/freqtrade/templates/subtemplates/exchange_kucoin.j2 new file mode 100644 index 000000000..f9dfff663 --- /dev/null +++ b/freqtrade/templates/subtemplates/exchange_kucoin.j2 @@ -0,0 +1,18 @@ +"exchange": { + "name": "{{ exchange_name | lower }}", + "key": "{{ exchange_key }}", + "secret": "{{ exchange_secret }}", + "password": "{{ exchange_key_password }}", + "ccxt_config": { + "enableRateLimit": true + "rateLimit": 200 + }, + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 200 + }, + "pair_whitelist": [ + ], + "pair_blacklist": [ + ] +} diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 deleted file mode 100644 index 5b967f4ed..000000000 --- a/freqtrade/templates/subtemplates/hyperopt_buy_guards_full.j2 +++ /dev/null @@ -1,8 +0,0 @@ -if params.get('mfi-enabled'): - conditions.append(dataframe['mfi'] < params['mfi-value']) -if params.get('fastd-enabled'): - conditions.append(dataframe['fastd'] < params['fastd-value']) -if params.get('adx-enabled'): - conditions.append(dataframe['adx'] > params['adx-value']) -if params.get('rsi-enabled'): - conditions.append(dataframe['rsi'] < params['rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 deleted file mode 100644 index 5e1022f59..000000000 --- a/freqtrade/templates/subtemplates/hyperopt_buy_guards_minimal.j2 +++ /dev/null @@ -1,2 +0,0 @@ -if params.get('rsi-enabled'): - conditions.append(dataframe['rsi'] < params['rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 deleted file mode 100644 index 29bafbd93..000000000 --- a/freqtrade/templates/subtemplates/hyperopt_buy_space_full.j2 +++ /dev/null @@ -1,9 +0,0 @@ -Integer(10, 25, name='mfi-value'), -Integer(15, 45, name='fastd-value'), -Integer(20, 50, name='adx-value'), -Integer(20, 40, name='rsi-value'), -Categorical([True, False], name='mfi-enabled'), -Categorical([True, False], name='fastd-enabled'), -Categorical([True, False], name='adx-enabled'), -Categorical([True, False], name='rsi-enabled'), -Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 deleted file mode 100644 index 5ddf537fb..000000000 --- a/freqtrade/templates/subtemplates/hyperopt_buy_space_minimal.j2 +++ /dev/null @@ -1,3 +0,0 @@ -Integer(20, 40, name='rsi-value'), -Categorical([True, False], name='rsi-enabled'), -Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 deleted file mode 100644 index bd7b499f4..000000000 --- a/freqtrade/templates/subtemplates/hyperopt_sell_guards_full.j2 +++ /dev/null @@ -1,8 +0,0 @@ -if params.get('sell-mfi-enabled'): - conditions.append(dataframe['mfi'] > params['sell-mfi-value']) -if params.get('sell-fastd-enabled'): - conditions.append(dataframe['fastd'] > params['sell-fastd-value']) -if params.get('sell-adx-enabled'): - conditions.append(dataframe['adx'] < params['sell-adx-value']) -if params.get('sell-rsi-enabled'): - conditions.append(dataframe['rsi'] > params['sell-rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 deleted file mode 100644 index 8b4adebf6..000000000 --- a/freqtrade/templates/subtemplates/hyperopt_sell_guards_minimal.j2 +++ /dev/null @@ -1,2 +0,0 @@ -if params.get('sell-rsi-enabled'): - conditions.append(dataframe['rsi'] > params['sell-rsi-value']) diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 deleted file mode 100644 index 46469d532..000000000 --- a/freqtrade/templates/subtemplates/hyperopt_sell_space_full.j2 +++ /dev/null @@ -1,11 +0,0 @@ -Integer(75, 100, name='sell-mfi-value'), -Integer(50, 100, name='sell-fastd-value'), -Integer(50, 100, name='sell-adx-value'), -Integer(60, 100, name='sell-rsi-value'), -Categorical([True, False], name='sell-mfi-enabled'), -Categorical([True, False], name='sell-fastd-enabled'), -Categorical([True, False], name='sell-adx-enabled'), -Categorical([True, False], name='sell-rsi-enabled'), -Categorical(['sell-bb_upper', - 'sell-macd_cross_signal', - 'sell-sar_reversal'], name='sell-trigger') diff --git a/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 b/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 deleted file mode 100644 index dfb110543..000000000 --- a/freqtrade/templates/subtemplates/hyperopt_sell_space_minimal.j2 +++ /dev/null @@ -1,5 +0,0 @@ -Integer(60, 100, name='sell-rsi-value'), -Categorical([True, False], name='sell-rsi-enabled'), -Categorical(['sell-bb_upper', - 'sell-macd_cross_signal', - 'sell-sar_reversal'], name='sell-trigger') diff --git a/mkdocs.yml b/mkdocs.yml index 59f2bae73..45b8d2557 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,42 +3,42 @@ site_url: https://www.freqtrade.io/ repo_url: https://github.com/freqtrade/freqtrade use_directory_urls: True nav: - - Home: index.md - - Quickstart with Docker: docker_quickstart.md - - Installation: - - Linux/MacOS/Raspberry: installation.md - - Windows: windows_installation.md - - Freqtrade Basics: bot-basics.md - - Configuration: configuration.md - - Strategy Customization: strategy-customization.md - - Plugins: plugins.md - - Stoploss: stoploss.md - - Start the bot: bot-usage.md - - Control the bot: - - Telegram: telegram-usage.md - - REST API & FreqUI: rest-api.md - - Web Hook: webhook-config.md - - Data Downloading: data-download.md - - Backtesting: backtesting.md - - Leverage: leverage.md - - Hyperopt: hyperopt.md - - Utility Sub-commands: utils.md - - Plotting: plotting.md - - Data Analysis: - - Jupyter Notebooks: data-analysis.md - - Strategy analysis: strategy_analysis_example.md - - Exchange-specific Notes: exchanges.md - - Advanced Topics: - - Advanced Post-installation Tasks: advanced-setup.md - - Edge Positioning: edge.md - - Advanced Strategy: strategy-advanced.md - - Advanced Hyperopt: advanced-hyperopt.md - - Sandbox Testing: sandbox-testing.md - - FAQ: faq.md - - SQL Cheat-sheet: sql_cheatsheet.md - - Updating Freqtrade: updating.md - - Deprecated Features: deprecated.md - - Contributors Guide: developer.md + - Home: index.md + - Quickstart with Docker: docker_quickstart.md + - Installation: + - Linux/MacOS/Raspberry: installation.md + - Windows: windows_installation.md + - Freqtrade Basics: bot-basics.md + - Configuration: configuration.md + - Strategy Customization: strategy-customization.md + - Plugins: plugins.md + - Stoploss: stoploss.md + - Start the bot: bot-usage.md + - Control the bot: + - Telegram: telegram-usage.md + - REST API & FreqUI: rest-api.md + - Web Hook: webhook-config.md + - Data Downloading: data-download.md + - Backtesting: backtesting.md + - Hyperopt: hyperopt.md + - Leverage: leverage.md + - Utility Sub-commands: utils.md + - Plotting: plotting.md + - Exchange-specific Notes: exchanges.md + - Data Analysis: + - Jupyter Notebooks: data-analysis.md + - Strategy analysis: strategy_analysis_example.md + - Advanced Topics: + - Advanced Post-installation Tasks: advanced-setup.md + - Edge Positioning: edge.md + - Advanced Strategy: strategy-advanced.md + - Advanced Hyperopt: advanced-hyperopt.md + - Sandbox Testing: sandbox-testing.md + - FAQ: faq.md + - SQL Cheat-sheet: sql_cheatsheet.md + - Updating Freqtrade: updating.md + - Deprecated Features: deprecated.md + - Contributors Guide: developer.md theme: name: material logo: "images/logo.png" diff --git a/requirements-dev.txt b/requirements-dev.txt index 67ee0035b..34d5607f3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.9.2 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.4.1 mypy==0.910 -pytest==6.2.4 +pytest==6.2.5 pytest-asyncio==0.15.1 pytest-cov==2.12.1 pytest-mock==3.6.1 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index d7f22634b..7dc55a9fc 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -8,4 +8,4 @@ scikit-optimize==0.8.1 filelock==3.0.12 joblib==1.0.1 psutil==5.8.0 -progressbar2==3.53.1 +progressbar2==3.53.2 diff --git a/requirements-plot.txt b/requirements-plot.txt index 62836a729..8e17232b0 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.3.0 +plotly==5.3.1 diff --git a/requirements.txt b/requirements.txt index 73a4a9cb3..ad7d520e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.21.2 -pandas==1.3.2 +pandas==1.3.3 -ccxt==1.55.56 +ccxt==1.56.30 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.8 aiohttp==3.7.4.post0 diff --git a/setup.sh b/setup.sh index e5f81578d..217500569 100755 --- a/setup.sh +++ b/setup.sh @@ -95,19 +95,7 @@ function install_talib() { return fi - cd build_helpers - tar zxvf ta-lib-0.4.0-src.tar.gz - cd ta-lib - sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h - ./configure --prefix=/usr/local - make - sudo make install - if [ -x "$(command -v apt-get)" ]; then - echo "Updating library path using ldconfig" - sudo ldconfig - fi - cd .. && rm -rf ./ta-lib/ - cd .. + cd build_helpers && ./install_ta-lib.sh && cd .. } function install_mac_newer_python_dependencies() { diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 1da9e5100..135510b38 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -10,10 +10,10 @@ import pytest from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, start_hyperopt_list, start_hyperopt_show, start_install_ui, - start_list_data, start_list_exchanges, start_list_hyperopts, - start_list_markets, start_list_strategies, start_list_timeframes, - start_new_hyperopt, start_new_strategy, start_show_trades, - start_test_pairlist, start_trading, start_webserver) + start_list_data, start_list_exchanges, start_list_markets, + start_list_strategies, start_list_timeframes, start_new_strategy, + start_show_trades, start_test_pairlist, start_trading, + start_webserver) from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_install_ui, get_ui_download_url, read_ui_version) from freqtrade.configuration import setup_utils_configuration @@ -32,8 +32,6 @@ def test_setup_utils_configuration(): config = setup_utils_configuration(get_args(args), RunMode.OTHER) assert "exchange" in config assert config['dry_run'] is True - assert config['exchange']['key'] == '' - assert config['exchange']['secret'] == '' def test_start_trading_fail(mocker, caplog): @@ -519,37 +517,6 @@ def test_start_new_strategy_no_arg(mocker, caplog): start_new_strategy(get_args(args)) -def test_start_new_hyperopt(mocker, caplog): - wt_mock = mocker.patch.object(Path, "write_text", MagicMock()) - mocker.patch.object(Path, "exists", MagicMock(return_value=False)) - - args = [ - "new-hyperopt", - "--hyperopt", - "CoolNewhyperopt" - ] - start_new_hyperopt(get_args(args)) - - assert wt_mock.call_count == 1 - assert "CoolNewhyperopt" in wt_mock.call_args_list[0][0][0] - assert log_has_re("Writing hyperopt to .*", caplog) - - mocker.patch('freqtrade.commands.deploy_commands.setup_utils_configuration') - mocker.patch.object(Path, "exists", MagicMock(return_value=True)) - with pytest.raises(OperationalException, - match=r".* already exists. Please choose another Hyperopt Name\."): - start_new_hyperopt(get_args(args)) - - -def test_start_new_hyperopt_no_arg(mocker): - args = [ - "new-hyperopt", - ] - with pytest.raises(OperationalException, - match="`new-hyperopt` requires --hyperopt to be set."): - start_new_hyperopt(get_args(args)) - - def test_start_install_ui(mocker): clean_mock = mocker.patch('freqtrade.commands.deploy_commands.clean_ui_subdir') get_url_mock = mocker.patch('freqtrade.commands.deploy_commands.get_ui_download_url', @@ -824,37 +791,20 @@ def test_start_list_strategies(mocker, caplog, capsys): assert "legacy_strategy_v1.py" in captured.out assert "StrategyTestV2" in captured.out - -def test_start_list_hyperopts(mocker, caplog, capsys): - + # Test color output args = [ - "list-hyperopts", - "--hyperopt-path", - str(Path(__file__).parent.parent / "optimize" / "hyperopts"), - "-1" + "list-strategies", + "--strategy-path", + str(Path(__file__).parent.parent / "strategy" / "strats"), ] pargs = get_args(args) # pargs['config'] = None - start_list_hyperopts(pargs) + start_list_strategies(pargs) captured = capsys.readouterr() - assert "TestHyperoptLegacy" not in captured.out - assert "legacy_hyperopt.py" not in captured.out - assert "HyperoptTestSepFile" in captured.out - assert "test_hyperopt.py" not in captured.out - - # Test regular output - args = [ - "list-hyperopts", - "--hyperopt-path", - str(Path(__file__).parent.parent / "optimize" / "hyperopts"), - ] - pargs = get_args(args) - # pargs['config'] = None - start_list_hyperopts(pargs) - captured = capsys.readouterr() - assert "TestHyperoptLegacy" not in captured.out - assert "legacy_hyperopt.py" not in captured.out - assert "HyperoptTestSepFile" in captured.out + assert "TestStrategyLegacyV1" in captured.out + assert "legacy_strategy_v1.py" in captured.out + assert "StrategyTestV2" in captured.out + assert "LOAD FAILED" in captured.out def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 6e51dd22d..6c8798015 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -1,3 +1,4 @@ +from datetime import datetime, timezone from random import randint from unittest.mock import MagicMock @@ -5,7 +6,7 @@ import ccxt import pytest from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException -from tests.conftest import get_patched_exchange +from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -113,3 +114,35 @@ def test_get_funding_rate(): def test__get_funding_fee(): return + + +@pytest.mark.asyncio +async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): + ohlcv = [ + [ + int((datetime.now(timezone.utc).timestamp() - 1000) * 1000), + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ] + ] + + exchange = get_patched_exchange(mocker, default_conf, id='binance') + # Monkey-patch async function + exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) + + pair = 'ETH/BTC' + res = await exchange._async_get_historic_ohlcv(pair, "5m", + 1500000000000, is_new_pair=False) + # Call with very old timestamp - causes tons of requests + assert exchange._api_async.fetch_ohlcv.call_count > 400 + # assert res == ohlcv + exchange._api_async.fetch_ohlcv.reset_mock() + res = await exchange._async_get_historic_ohlcv(pair, "5m", 1500000000000, is_new_pair=True) + + # Called twice - one "init" call - and one to get the actual data. + assert exchange._api_async.fetch_ohlcv.call_count == 2 + assert res == ohlcv + assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 3a32d108b..d71dbe015 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -54,6 +54,8 @@ EXCHANGES = { def exchange_conf(): config = get_default_conf((Path(__file__).parent / "testdata").resolve()) config['exchange']['pair_whitelist'] = [] + config['exchange']['key'] = '' + config['exchange']['secret'] = '' config['dry_run'] = False return config diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index dc8e9ca2f..abbbbe4a7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1,5 +1,6 @@ import copy import logging +from copy import deepcopy from datetime import datetime, timedelta, timezone from math import isclose from random import randint @@ -14,7 +15,7 @@ from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOr OperationalException, PricingError, TemporaryError) from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, - calculate_backoff) + calculate_backoff, remove_credentials) from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) @@ -78,6 +79,22 @@ def test_init(default_conf, mocker, caplog): assert log_has('Instance is running with dry_run enabled', caplog) +def test_remove_credentials(default_conf, caplog) -> None: + conf = deepcopy(default_conf) + conf['dry_run'] = False + remove_credentials(conf) + + assert conf['exchange']['key'] != '' + assert conf['exchange']['secret'] != '' + + conf['dry_run'] = True + remove_credentials(conf) + assert conf['exchange']['key'] == '' + assert conf['exchange']['secret'] == '' + assert conf['exchange']['password'] == '' + assert conf['exchange']['uid'] == '' + + def test_init_ccxt_kwargs(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') @@ -108,6 +125,13 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): assert hasattr(ex._api_async, 'TestKWARG') assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog) assert log_has(asynclogmsg, caplog) + # Test additional headers case + Exchange._headers = {'hello': 'world'} + ex = Exchange(conf) + + assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog) + assert ex._api.headers == {'hello': 'world'} + Exchange._headers = {} def test_destroy(default_conf, mocker, caplog): @@ -178,7 +202,7 @@ def test_exchange_resolver(default_conf, mocker, caplog): def test_validate_order_time_in_force(default_conf, mocker, caplog): caplog.set_level(logging.INFO) - # explicitly test bittrex, exchanges implementing other policies need seperate tests + # explicitly test bittrex, exchanges implementing other policies need separate tests ex = get_patched_exchange(mocker, default_conf, id="bittrex") tif = { "buy": "gtc", @@ -1544,6 +1568,32 @@ def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name): assert 'high' in ret.columns +@pytest.mark.asyncio +@pytest.mark.parametrize("exchange_name", EXCHANGES) +async def test__async_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): + ohlcv = [ + [ + int((datetime.now(timezone.utc).timestamp() - 1000) * 1000), + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ] + ] + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + # Monkey-patch async function + exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) + + pair = 'ETH/USDT' + res = await exchange._async_get_historic_ohlcv(pair, "5m", + 1500000000000, is_new_pair=False) + # Call with very old timestamp - causes tons of requests + assert exchange._api_async.fetch_ohlcv.call_count > 200 + assert res[0] == ohlcv[0] + assert log_has_re(r'Downloaded data for .* with length .*\.', caplog) + + def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: ohlcv = [ [ @@ -2431,7 +2481,7 @@ def test_fetch_order(default_conf, mocker, exchange_name, caplog): @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_stoploss_order(default_conf, mocker, exchange_name): - # Don't test FTX here - that needs a seperate test + # Don't test FTX here - that needs a separate test if exchange_name == 'ftx': return default_conf['dry_run'] = True diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py index 95c9fef97..5c5171c3a 100644 --- a/tests/optimize/conftest.py +++ b/tests/optimize/conftest.py @@ -16,7 +16,7 @@ def hyperopt_conf(default_conf): hyperconf.update({ 'datadir': Path(default_conf['datadir']), 'runmode': RunMode.HYPEROPT, - 'hyperopt': 'HyperoptTestSepFile', + 'strategy': 'HyperoptableStrategy', 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), 'epochs': 1, diff --git a/tests/optimize/hyperopts/hyperopt_test_sep_file.py b/tests/optimize/hyperopts/hyperopt_test_sep_file.py deleted file mode 100644 index a19e55794..000000000 --- a/tests/optimize/hyperopts/hyperopt_test_sep_file.py +++ /dev/null @@ -1,207 +0,0 @@ -# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement - -from functools import reduce -from typing import Any, Callable, Dict, List - -import talib.abstract as ta -from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer - -import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.optimize.hyperopt_interface import IHyperOpt - - -class HyperoptTestSepFile(IHyperOpt): - """ - Default hyperopt provided by the Freqtrade bot. - You can override it with your own Hyperopt - """ - @staticmethod - def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Add several indicators needed for buy and sell strategies defined below. - """ - # ADX - dataframe['adx'] = ta.ADX(dataframe) - # MACD - macd = ta.MACD(dataframe) - dataframe['macd'] = macd['macd'] - dataframe['macdsignal'] = macd['macdsignal'] - # MFI - dataframe['mfi'] = ta.MFI(dataframe) - # RSI - dataframe['rsi'] = ta.RSI(dataframe) - # Stochastic Fast - stoch_fast = ta.STOCHF(dataframe) - dataframe['fastd'] = stoch_fast['fastd'] - # Minus-DI - dataframe['minus_di'] = ta.MINUS_DI(dataframe) - # Bollinger bands - bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) - dataframe['bb_lowerband'] = bollinger['lower'] - dataframe['bb_upperband'] = bollinger['upper'] - # SAR - dataframe['sar'] = ta.SAR(dataframe) - - return dataframe - - @staticmethod - def buy_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the buy strategy parameters to be used by Hyperopt. - """ - def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Buy strategy Hyperopt will build and use. - """ - conditions = [] - - # GUARDS AND TRENDS - if 'mfi-enabled' in params and params['mfi-enabled']: - conditions.append(dataframe['mfi'] < params['mfi-value']) - if 'fastd-enabled' in params and params['fastd-enabled']: - conditions.append(dataframe['fastd'] < params['fastd-value']) - if 'adx-enabled' in params and params['adx-enabled']: - conditions.append(dataframe['adx'] > params['adx-value']) - if 'rsi-enabled' in params and params['rsi-enabled']: - conditions.append(dataframe['rsi'] < params['rsi-value']) - - # TRIGGERS - if 'trigger' in params: - if params['trigger'] == 'boll': - conditions.append(dataframe['close'] < dataframe['bb_lowerband']) - if params['trigger'] == 'macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macd'], - dataframe['macdsignal'] - )) - if params['trigger'] == 'sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['close'], - dataframe['sar'] - )) - - if conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'buy'] = 1 - - return dataframe - - return populate_buy_trend - - @staticmethod - def indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching buy strategy parameters. - """ - return [ - Integer(10, 25, name='mfi-value'), - Integer(15, 45, name='fastd-value'), - Integer(20, 50, name='adx-value'), - Integer(20, 40, name='rsi-value'), - Categorical([True, False], name='mfi-enabled'), - Categorical([True, False], name='fastd-enabled'), - Categorical([True, False], name='adx-enabled'), - Categorical([True, False], name='rsi-enabled'), - Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger') - ] - - @staticmethod - def sell_strategy_generator(params: Dict[str, Any]) -> Callable: - """ - Define the sell strategy parameters to be used by Hyperopt. - """ - def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Sell strategy Hyperopt will build and use. - """ - conditions = [] - - # GUARDS AND TRENDS - if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']: - conditions.append(dataframe['mfi'] > params['sell-mfi-value']) - if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']: - conditions.append(dataframe['fastd'] > params['sell-fastd-value']) - if 'sell-adx-enabled' in params and params['sell-adx-enabled']: - conditions.append(dataframe['adx'] < params['sell-adx-value']) - if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']: - conditions.append(dataframe['rsi'] > params['sell-rsi-value']) - - # TRIGGERS - if 'sell-trigger' in params: - if params['sell-trigger'] == 'sell-boll': - conditions.append(dataframe['close'] > dataframe['bb_upperband']) - if params['sell-trigger'] == 'sell-macd_cross_signal': - conditions.append(qtpylib.crossed_above( - dataframe['macdsignal'], - dataframe['macd'] - )) - if params['sell-trigger'] == 'sell-sar_reversal': - conditions.append(qtpylib.crossed_above( - dataframe['sar'], - dataframe['close'] - )) - - if conditions: - dataframe.loc[ - reduce(lambda x, y: x & y, conditions), - 'sell'] = 1 - - return dataframe - - return populate_sell_trend - - @staticmethod - def sell_indicator_space() -> List[Dimension]: - """ - Define your Hyperopt space for searching sell strategy parameters. - """ - return [ - Integer(75, 100, name='sell-mfi-value'), - Integer(50, 100, name='sell-fastd-value'), - Integer(50, 100, name='sell-adx-value'), - Integer(60, 100, name='sell-rsi-value'), - Categorical([True, False], name='sell-mfi-enabled'), - Categorical([True, False], name='sell-fastd-enabled'), - Categorical([True, False], name='sell-adx-enabled'), - Categorical([True, False], name='sell-rsi-enabled'), - Categorical(['sell-boll', - 'sell-macd_cross_signal', - 'sell-sar_reversal'], - name='sell-trigger') - ] - - def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. Should be a copy of same method from strategy. - Must align to populate_indicators in this file. - Only used when --spaces does not include buy space. - """ - dataframe.loc[ - ( - (dataframe['close'] < dataframe['bb_lowerband']) & - (dataframe['mfi'] < 16) & - (dataframe['adx'] > 25) & - (dataframe['rsi'] < 21) - ), - 'buy'] = 1 - - return dataframe - - def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - """ - Based on TA indicators. Should be a copy of same method from strategy. - Must align to populate_indicators in this file. - Only used when --spaces does not include sell space. - """ - dataframe.loc[ - ( - (qtpylib.crossed_above( - dataframe['macdsignal'], dataframe['macd'] - )) & - (dataframe['fastd'] > 54) - ), - 'sell'] = 1 - - return dataframe diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 565d6077a..b34c3a916 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -17,13 +17,10 @@ from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal -from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.strategy.hyper import IntParameter from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) -from .hyperopts.hyperopt_test_sep_file import HyperoptTestSepFile - def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) @@ -31,7 +28,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'HyperoptTestSepFile', + '--strategy', 'HyperoptableStrategy', ] config = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) @@ -63,7 +60,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'HyperoptTestSepFile', + '--strategy', 'HyperoptableStrategy', '--datadir', '/foo/bar', '--timeframe', '1m', '--timerange', ':100', @@ -115,7 +112,7 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'HyperoptTestSepFile', + '--strategy', 'HyperoptableStrategy', '--stake-amount', '1', '--starting-balance', '2' ] @@ -133,47 +130,6 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) -def test_hyperoptresolver(mocker, default_conf, caplog) -> None: - patched_configuration_load_config_file(mocker, default_conf) - - hyperopt = HyperoptTestSepFile - delattr(hyperopt, 'populate_indicators') - delattr(hyperopt, 'populate_buy_trend') - delattr(hyperopt, 'populate_sell_trend') - mocker.patch( - 'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver.load_object', - MagicMock(return_value=hyperopt(default_conf)) - ) - default_conf.update({'hyperopt': 'HyperoptTestSepFile'}) - x = HyperOptResolver.load_hyperopt(default_conf) - assert not hasattr(x, 'populate_indicators') - assert not hasattr(x, 'populate_buy_trend') - assert not hasattr(x, 'populate_sell_trend') - assert log_has("Hyperopt class does not provide populate_indicators() method. " - "Using populate_indicators from the strategy.", caplog) - assert log_has("Hyperopt class does not provide populate_sell_trend() method. " - "Using populate_sell_trend from the strategy.", caplog) - assert log_has("Hyperopt class does not provide populate_buy_trend() method. " - "Using populate_buy_trend from the strategy.", caplog) - assert hasattr(x, "ticker_interval") # DEPRECATED - assert hasattr(x, "timeframe") - - -def test_hyperoptresolver_wrongname(default_conf) -> None: - default_conf.update({'hyperopt': "NonExistingHyperoptClass"}) - - with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'): - HyperOptResolver.load_hyperopt(default_conf) - - -def test_hyperoptresolver_noname(default_conf): - default_conf['hyperopt'] = '' - with pytest.raises(OperationalException, - match="No Hyperopt set. Please use `--hyperopt` to specify " - "the Hyperopt class to use."): - HyperOptResolver.load_hyperopt(default_conf) - - def test_start_not_installed(mocker, default_conf, import_fails) -> None: start_mock = MagicMock() patched_configuration_load_config_file(mocker, default_conf) @@ -184,9 +140,7 @@ def test_start_not_installed(mocker, default_conf, import_fails) -> None: args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'HyperoptTestSepFile', - '--hyperopt-path', - str(Path(__file__).parent / "hyperopts"), + '--strategy', 'HyperoptableStrategy', '--epochs', '5', '--hyperopt-loss', 'SharpeHyperOptLossDaily', ] @@ -196,7 +150,7 @@ def test_start_not_installed(mocker, default_conf, import_fails) -> None: start_hyperopt(pargs) -def test_start(mocker, hyperopt_conf, caplog) -> None: +def test_start_no_hyperopt_allowed(mocker, hyperopt_conf, caplog) -> None: start_mock = MagicMock() patched_configuration_load_config_file(mocker, hyperopt_conf) mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock) @@ -210,10 +164,8 @@ def test_start(mocker, hyperopt_conf, caplog) -> None: '--epochs', '5' ] pargs = get_args(args) - start_hyperopt(pargs) - - assert log_has('Starting freqtrade in Hyperopt mode', caplog) - assert start_mock.call_count == 1 + with pytest.raises(OperationalException, match=r"Using separate Hyperopt files has been.*"): + start_hyperopt(pargs) def test_start_no_data(mocker, hyperopt_conf) -> None: @@ -225,11 +177,11 @@ def test_start_no_data(mocker, hyperopt_conf) -> None: ) patch_exchange(mocker) - + # TODO: migrate to strategy-based hyperopt args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'HyperoptTestSepFile', + '--strategy', 'HyperoptableStrategy', '--hyperopt-loss', 'SharpeHyperOptLossDaily', '--epochs', '5' ] @@ -247,7 +199,7 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: args = [ 'hyperopt', '--config', 'config.json', - '--hyperopt', 'HyperoptTestSepFile', + '--strategy', 'HyperoptableStrategy', '--hyperopt-loss', 'SharpeHyperOptLossDaily', '--epochs', '5' ] @@ -427,66 +379,14 @@ def test_hyperopt_format_results(hyperopt): def test_populate_indicators(hyperopt, testdatadir) -> None: data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data) - dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], - {'pair': 'UNITTEST/BTC'}) + dataframe = dataframes['UNITTEST/BTC'] # Check if some indicators are generated. We will not test all of them assert 'adx' in dataframe - assert 'mfi' in dataframe + assert 'macd' in dataframe assert 'rsi' in dataframe -def test_buy_strategy_generator(hyperopt, testdatadir) -> None: - data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) - dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data) - dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], - {'pair': 'UNITTEST/BTC'}) - - populate_buy_trend = hyperopt.custom_hyperopt.buy_strategy_generator( - { - 'adx-value': 20, - 'fastd-value': 20, - 'mfi-value': 20, - 'rsi-value': 20, - 'adx-enabled': True, - 'fastd-enabled': True, - 'mfi-enabled': True, - 'rsi-enabled': True, - 'trigger': 'bb_lower' - } - ) - result = populate_buy_trend(dataframe, {'pair': 'UNITTEST/BTC'}) - # Check if some indicators are generated. We will not test all of them - assert 'buy' in result - assert 1 in result['buy'] - - -def test_sell_strategy_generator(hyperopt, testdatadir) -> None: - data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True) - dataframes = hyperopt.backtesting.strategy.advise_all_indicators(data) - dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], - {'pair': 'UNITTEST/BTC'}) - - populate_sell_trend = hyperopt.custom_hyperopt.sell_strategy_generator( - { - 'sell-adx-value': 20, - 'sell-fastd-value': 75, - 'sell-mfi-value': 80, - 'sell-rsi-value': 20, - 'sell-adx-enabled': True, - 'sell-fastd-enabled': True, - 'sell-mfi-enabled': True, - 'sell-rsi-enabled': True, - 'sell-trigger': 'sell-bb_upper' - } - ) - result = populate_sell_trend(dataframe, {'pair': 'UNITTEST/BTC'}) - # Check if some indicators are generated. We will not test all of them - print(result) - assert 'sell' in result - assert 1 in result['sell'] - - def test_generate_optimizer(mocker, hyperopt_conf) -> None: hyperopt_conf.update({'spaces': 'all', 'hyperopt_min_trades': 1, @@ -527,24 +427,12 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: mocker.patch('freqtrade.optimize.hyperopt.load', return_value={'XRP/BTC': None}) optimizer_param = { - 'adx-value': 0, - 'fastd-value': 35, - 'mfi-value': 0, - 'rsi-value': 0, - 'adx-enabled': False, - 'fastd-enabled': True, - 'mfi-enabled': False, - 'rsi-enabled': False, - 'trigger': 'macd_cross_signal', - 'sell-adx-value': 0, - 'sell-fastd-value': 75, - 'sell-mfi-value': 0, - 'sell-rsi-value': 0, - 'sell-adx-enabled': False, - 'sell-fastd-enabled': True, - 'sell-mfi-enabled': False, - 'sell-rsi-enabled': False, - 'sell-trigger': 'macd_cross_signal', + 'buy_plusdi': 0.02, + 'buy_rsi': 35, + 'sell_minusdi': 0.02, + 'sell_rsi': 75, + 'protection_cooldown_lookback': 20, + 'protection_enabled': True, 'roi_t1': 60.0, 'roi_t2': 30.0, 'roi_t3': 20.0, @@ -564,29 +452,19 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: '0.00003100 BTC ( 0.00%). ' 'Avg duration 0:50:00 min.' ), - 'params_details': {'buy': {'adx-enabled': False, - 'adx-value': 0, - 'fastd-enabled': True, - 'fastd-value': 35, - 'mfi-enabled': False, - 'mfi-value': 0, - 'rsi-enabled': False, - 'rsi-value': 0, - 'trigger': 'macd_cross_signal'}, + 'params_details': {'buy': {'buy_plusdi': 0.02, + 'buy_rsi': 35, + }, 'roi': {"0": 0.12000000000000001, "20.0": 0.02, "50.0": 0.01, "110.0": 0}, - 'protection': {}, - 'sell': {'sell-adx-enabled': False, - 'sell-adx-value': 0, - 'sell-fastd-enabled': True, - 'sell-fastd-value': 75, - 'sell-mfi-enabled': False, - 'sell-mfi-value': 0, - 'sell-rsi-enabled': False, - 'sell-rsi-value': 0, - 'sell-trigger': 'macd_cross_signal'}, + 'protection': {'protection_cooldown_lookback': 20, + 'protection_enabled': True, + }, + 'sell': {'sell_minusdi': 0.02, + 'sell_rsi': 75, + }, 'stoploss': {'stoploss': -0.4}, 'trailing': {'trailing_only_offset_is_reached': False, 'trailing_stop': True, @@ -808,11 +686,6 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) - del hyperopt.custom_hyperopt.__class__.buy_strategy_generator - del hyperopt.custom_hyperopt.__class__.sell_strategy_generator - del hyperopt.custom_hyperopt.__class__.indicator_space - del hyperopt.custom_hyperopt.__class__.sell_indicator_space - hyperopt.start() parallel.assert_called_once() @@ -843,16 +716,14 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None: hyperopt_conf.update({'spaces': 'all', }) + mocker.patch('freqtrade.optimize.hyperopt_auto.HyperOptAuto._generate_indicator_space', + return_value=[]) + hyperopt = Hyperopt(hyperopt_conf) hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) - del hyperopt.custom_hyperopt.__class__.buy_strategy_generator - del hyperopt.custom_hyperopt.__class__.sell_strategy_generator - del hyperopt.custom_hyperopt.__class__.indicator_space - del hyperopt.custom_hyperopt.__class__.sell_indicator_space - - with pytest.raises(OperationalException, match=r"The 'buy' space is included into *"): + with pytest.raises(OperationalException, match=r"The 'protection' space is included into *"): hyperopt.start() @@ -889,11 +760,6 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) - # TODO: sell_strategy_generator() is actually not called because - # run_optimizer_parallel() is mocked - del hyperopt.custom_hyperopt.__class__.sell_strategy_generator - del hyperopt.custom_hyperopt.__class__.sell_indicator_space - hyperopt.start() parallel.assert_called_once() @@ -943,11 +809,6 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) - # TODO: buy_strategy_generator() is actually not called because - # run_optimizer_parallel() is mocked - del hyperopt.custom_hyperopt.__class__.buy_strategy_generator - del hyperopt.custom_hyperopt.__class__.indicator_space - hyperopt.start() parallel.assert_called_once() @@ -964,13 +825,12 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: assert hasattr(hyperopt, "position_stacking") -@pytest.mark.parametrize("method,space", [ - ('buy_strategy_generator', 'buy'), - ('indicator_space', 'buy'), - ('sell_strategy_generator', 'sell'), - ('sell_indicator_space', 'sell'), +@pytest.mark.parametrize("space", [ + ('buy'), + ('sell'), + ('protection'), ]) -def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> None: +def test_simplified_interface_failed(mocker, hyperopt_conf, space) -> None: mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', @@ -979,6 +839,8 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) + mocker.patch('freqtrade.optimize.hyperopt_auto.HyperOptAuto._generate_indicator_space', + return_value=[]) patch_exchange(mocker) @@ -988,8 +850,6 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No hyperopt.backtesting.strategy.advise_all_indicators = MagicMock() hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={}) - delattr(hyperopt.custom_hyperopt.__class__, method) - with pytest.raises(OperationalException, match=f"The '{space}' space is included into *"): hyperopt.start() @@ -999,7 +859,6 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) (Path(tmpdir) / 'hyperopt_results').mkdir(parents=True) # No hyperopt needed - del hyperopt_conf['hyperopt'] hyperopt_conf.update({ 'strategy': 'HyperoptableStrategy', 'user_data_dir': Path(tmpdir), diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index fce3a8cd1..c694fd7c1 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -68,7 +68,7 @@ def test_PairLocks(use_db): # Global lock PairLocks.lock_pair('*', lock_time) assert PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) - # Global lock also locks every pair seperately + # Global lock also locks every pair separately assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50)) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 9c22badc8..5e9b86d4a 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -739,11 +739,16 @@ def test_auto_hyperopt_interface(default_conf): PairLocks.timeframe = default_conf['timeframe'] strategy = StrategyResolver.load_strategy(default_conf) + with pytest.raises(OperationalException): + next(strategy.enumerate_parameters('deadBeef')) + assert strategy.buy_rsi.value == strategy.buy_params['buy_rsi'] # PlusDI is NOT in the buy-params, so default should be used assert strategy.buy_plusdi.value == 0.5 assert strategy.sell_rsi.value == strategy.sell_params['sell_rsi'] + assert repr(strategy.sell_rsi) == 'IntParameter(74)' + # Parameter is disabled - so value from sell_param dict will NOT be used. assert strategy.sell_minusdi.value == 0.5 all_params = strategy.detect_all_parameters() diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 9aea4fa11..1ce45e4d5 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -11,8 +11,7 @@ import pytest from jsonschema import ValidationError from freqtrade.commands import Arguments -from freqtrade.configuration import (Configuration, check_exchange, remove_credentials, - validate_config_consistency) +from freqtrade.configuration import Configuration, check_exchange, validate_config_consistency from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.deprecated_settings import (check_conflicting_settings, process_deprecated_setting, @@ -617,18 +616,6 @@ def test_check_exchange(default_conf, caplog) -> None: check_exchange(default_conf) -def test_remove_credentials(default_conf, caplog) -> None: - conf = deepcopy(default_conf) - conf['dry_run'] = False - remove_credentials(conf) - - assert conf['dry_run'] is True - assert conf['exchange']['key'] == '' - assert conf['exchange']['secret'] == '' - assert conf['exchange']['password'] == '' - assert conf['exchange']['uid'] == '' - - def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index a11200526..905b078f9 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -74,16 +74,12 @@ def test_copy_sample_files(mocker, default_conf, caplog) -> None: copymock = mocker.patch('shutil.copy', MagicMock()) copy_sample_files(Path('/tmp/bar')) - assert copymock.call_count == 5 + assert copymock.call_count == 3 assert copymock.call_args_list[0][0][1] == str( Path('/tmp/bar') / 'strategies/sample_strategy.py') assert copymock.call_args_list[1][0][1] == str( - Path('/tmp/bar') / 'hyperopts/sample_hyperopt_advanced.py') - assert copymock.call_args_list[2][0][1] == str( Path('/tmp/bar') / 'hyperopts/sample_hyperopt_loss.py') - assert copymock.call_args_list[3][0][1] == str( - Path('/tmp/bar') / 'hyperopts/sample_hyperopt.py') - assert copymock.call_args_list[4][0][1] == str( + assert copymock.call_args_list[2][0][1] == str( Path('/tmp/bar') / 'notebooks/strategy_analysis_example.ipynb') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3432c34f6..f278604be 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -518,6 +518,7 @@ def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order, # 0 trades, but it's not because of pairlock. assert n == 0 assert not log_has_re(message, caplog) + caplog.clear() PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=20).datetime, 'Just because') n = freqtrade.enter_positions() @@ -1086,6 +1087,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) assert trade.stoploss_order_id is None assert trade.is_open is False + caplog.clear() mocker.patch( 'freqtrade.exchange.Binance.stoploss', @@ -1190,7 +1192,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert trade.stoploss_order_id is None assert trade.sell_reason == SellType.EMERGENCY_SELL.value assert log_has("Unable to place a stoploss order on exchange. ", caplog) - assert log_has("Selling the trade forcefully", caplog) + assert log_has("Exiting the trade forcefully", caplog) # Should call a market sell assert create_order_mock.call_count == 2 @@ -1659,7 +1661,7 @@ def test_enter_positions(mocker, default_conf, caplog) -> None: MagicMock(return_value=False)) n = freqtrade.enter_positions() assert n == 0 - assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) + assert log_has('Found no enter signals for whitelisted currencies. Trying again...', caplog) # create_trade should be called once for every pair in the whitelist. assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) @@ -1720,7 +1722,7 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) ) n = freqtrade.exit_positions(trades) assert n == 0 - assert log_has('Unable to sell trade ETH/BTC: ', caplog) + assert log_has('Unable to exit trade ETH/BTC: ', caplog) def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None: @@ -1743,10 +1745,12 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No ) assert not freqtrade.update_trade_state(trade, None) assert log_has_re(r'Orderid for trade .* is empty.', caplog) + caplog.clear() # Add datetime explicitly since sqlalchemy defaults apply only once written to database freqtrade.update_trade_state(trade, '123') # Test amount not modified by fee-logic assert not log_has_re(r'Applying fee to .*', caplog) + caplog.clear() assert trade.open_order_id is None assert trade.amount == limit_buy_order['amount'] @@ -2453,8 +2457,8 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', - handle_cancel_buy=MagicMock(), - handle_cancel_sell=MagicMock(), + handle_cancel_enter=MagicMock(), + handle_cancel_exit=MagicMock(), ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -2475,7 +2479,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke caplog) -def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> None: +def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> None: patch_RPCManager(mocker) patch_exchange(mocker) cancel_buy_order = deepcopy(limit_buy_order) @@ -2486,7 +2490,7 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) freqtrade = FreqtradeBot(default_conf) - freqtrade._notify_buy_cancel = MagicMock() + freqtrade._notify_enter_cancel = MagicMock() trade = MagicMock() trade.pair = 'LTC/USDT' @@ -2494,46 +2498,46 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non limit_buy_order['filled'] = 0.0 limit_buy_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] - assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 cancel_order_mock.reset_mock() caplog.clear() limit_buy_order['filled'] = 0.01 - assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 0 assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog) caplog.clear() cancel_order_mock.reset_mock() limit_buy_order['filled'] = 2 - assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 # Order remained open for some reason (cancel failed) cancel_buy_order['status'] = 'open' cancel_order_mock = MagicMock(return_value=cancel_buy_order) mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) - assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert log_has_re(r"Order .* for .* not cancelled.", caplog) @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], indirect=['limit_buy_order_canceled_empty']) -def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf, - limit_buy_order_canceled_empty) -> None: +def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf, + limit_buy_order_canceled_empty) -> None: patch_RPCManager(mocker) patch_exchange(mocker) cancel_order_mock = mocker.patch( 'freqtrade.exchange.Exchange.cancel_order_with_result', return_value=limit_buy_order_canceled_empty) - nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_buy_cancel') + nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_enter_cancel') freqtrade = FreqtradeBot(default_conf) reason = CANCEL_REASON['TIMEOUT'] trade = MagicMock() trade.pair = 'LTC/ETH' - assert freqtrade.handle_cancel_buy(trade, limit_buy_order_canceled_empty, reason) + assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason) assert cancel_order_mock.call_count == 0 assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog) assert nofiy_mock.call_count == 1 @@ -2545,8 +2549,8 @@ def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf, 'String Return value', 123 ]) -def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, - cancelorder) -> None: +def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order, + cancelorder) -> None: patch_RPCManager(mocker) patch_exchange(mocker) cancel_order_mock = MagicMock(return_value=cancelorder) @@ -2556,7 +2560,7 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, ) freqtrade = FreqtradeBot(default_conf) - freqtrade._notify_buy_cancel = MagicMock() + freqtrade._notify_enter_cancel = MagicMock() trade = MagicMock() trade.pair = 'LTC/USDT' @@ -2564,16 +2568,16 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, limit_buy_order['filled'] = 0.0 limit_buy_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] - assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 cancel_order_mock.reset_mock() limit_buy_order['filled'] = 1.0 - assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 -def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None: +def test_handle_cancel_exit_limit(mocker, default_conf, fee) -> None: send_msg_mock = patch_RPCManager(mocker) patch_exchange(mocker) cancel_order_mock = MagicMock() @@ -2599,26 +2603,26 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None: 'amount': 1, 'status': "open"} reason = CANCEL_REASON['TIMEOUT'] - assert freqtrade.handle_cancel_sell(trade, order, reason) + assert freqtrade.handle_cancel_exit(trade, order, reason) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 send_msg_mock.reset_mock() order['amount'] = 2 - assert freqtrade.handle_cancel_sell(trade, order, reason + assert freqtrade.handle_cancel_exit(trade, order, reason ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 - assert freqtrade.handle_cancel_sell(trade, order, reason + assert freqtrade.handle_cancel_exit(trade, order, reason ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Message should not be iterated again assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert send_msg_mock.call_count == 1 -def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None: +def test_handle_cancel_exit_cancel_exception(mocker, default_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch( @@ -2631,7 +2635,7 @@ def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None: order = {'remaining': 1, 'amount': 1, 'status': "open"} - assert freqtrade.handle_cancel_sell(trade, order, reason) == 'error cancelling order' + assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order' def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None: @@ -3303,7 +3307,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_ assert trade.amount != amnt -def test__safe_sell_amount(default_conf, fee, caplog, mocker): +def test__safe_exit_amount(default_conf, fee, caplog, mocker): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 @@ -3323,17 +3327,17 @@ def test__safe_sell_amount(default_conf, fee, caplog, mocker): patch_get_signal(freqtrade) wallet_update.reset_mock() - assert freqtrade._safe_sell_amount(trade.pair, trade.amount) == amount_wallet + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet assert log_has_re(r'.*Falling back to wallet-amount.', caplog) assert wallet_update.call_count == 1 caplog.clear() wallet_update.reset_mock() - assert freqtrade._safe_sell_amount(trade.pair, amount_wallet) == amount_wallet + assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) assert wallet_update.call_count == 1 -def test__safe_sell_amount_error(default_conf, fee, caplog, mocker): +def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 @@ -3350,8 +3354,8 @@ def test__safe_sell_amount_error(default_conf, fee, caplog, mocker): ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - with pytest.raises(DependencyException, match=r"Not enough amount to sell."): - assert freqtrade._safe_sell_amount(trade.pair, trade.amount) + with pytest.raises(DependencyException, match=r"Not enough amount to exit."): + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: @@ -3525,6 +3529,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_or assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0 profit: 0.2666%", caplog) assert log_has("ETH/BTC - Adjusting stoploss...", caplog) assert trade.stop_loss == 0.0000138501 + caplog.clear() mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ @@ -3585,6 +3590,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_orde assert log_has("ETH/BTC - Using positive stoploss: 0.01 offset: 0.011 profit: 0.2666%", caplog) assert log_has("ETH/BTC - Adjusting stoploss...", caplog) assert trade.stop_loss == 0.0000138501 + caplog.clear() mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ @@ -3649,6 +3655,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_ assert not log_has("ETH/BTC - Adjusting stoploss...", caplog) assert trade.stop_loss == 0.0000098910 + caplog.clear() # price rises above the offset (rises 12% when the offset is 5.5%) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', @@ -4316,8 +4323,8 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=[ ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order]) - buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') - sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') + buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_enter') + sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit') freqtrade = get_patched_freqtradebot(mocker, default_conf) create_mock_trades(fee) @@ -4351,6 +4358,7 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): freqtrade.update_open_orders() assert not log_has_re(r"Error updating Order .*", caplog) + caplog.clear() freqtrade.config['dry_run'] = False freqtrade.update_open_orders() @@ -4432,14 +4440,14 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): @pytest.mark.usefixtures("init_persistence") -def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): +def test_reupdate_enter_order_fees(mocker, default_conf, fee, caplog): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') create_mock_trades(fee) trades = Trade.get_trades().all() - freqtrade.reupdate_buy_order_fees(trades[0]) + freqtrade.reupdate_enter_order_fees(trades[0]) assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 1 assert mock_uts.call_args_list[0][0][0] == trades[0] @@ -4462,7 +4470,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): ) Trade.query.session.add(trade) - freqtrade.reupdate_buy_order_fees(trade) + freqtrade.reupdate_enter_order_fees(trade) assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 0 assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) @@ -4472,7 +4480,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): def test_handle_insufficient_funds(mocker, default_conf, fee): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') - mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees') + mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees') create_mock_trades(fee) trades = Trade.get_trades().all() diff --git a/tests/test_integration.py b/tests/test_integration.py index 215927098..a3484d438 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -70,7 +70,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', create_stoploss_order=MagicMock(return_value=True), - _notify_sell=MagicMock(), + _notify_exit=MagicMock(), ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) @@ -154,7 +154,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', create_stoploss_order=MagicMock(return_value=True), - _notify_sell=MagicMock(), + _notify_exit=MagicMock(), ) should_sell_mock = MagicMock(side_effect=[ SellCheckTuple(sell_type=SellType.NONE), From a8657bb1ce5181ab304b3d7ea0b00a08afae67de Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 16 Sep 2021 03:36:48 -0600 Subject: [PATCH 20/59] Removed backtesting funding-fee code --- freqtrade/exchange/binance.py | 55 +-------------------------- freqtrade/exchange/exchange.py | 67 --------------------------------- freqtrade/exchange/ftx.py | 25 +----------- freqtrade/persistence/models.py | 14 ------- tests/exchange/test_binance.py | 8 ---- tests/exchange/test_exchange.py | 12 ------ tests/exchange/test_ftx.py | 16 -------- 7 files changed, 2 insertions(+), 195 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index a87a5dc55..f7eb03b57 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,7 +1,6 @@ """ Binance exchange subclass """ import logging -from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Dict, List import arrow import ccxt @@ -27,13 +26,6 @@ class Binance(Exchange): "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } funding_fee_times: List[int] = [0, 8, 16] # hours of the day - _funding_interest_rates: Dict = {} # TODO-lev: delete - - def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: - super().__init__(config, validate) - # TODO-lev: Uncomment once lev-exchange merged in - # if self.trading_mode == TradingMode.FUTURES: - # self._funding_interest_rates = self._get_funding_interest_rates() def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: """ @@ -101,51 +93,6 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - def _get_premium_index(self, pair: str, date: datetime) -> float: - raise OperationalException(f'_get_premium_index has not been implemented on {self.name}') - - def _get_mark_price(self, pair: str, date: datetime) -> float: - raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') - - def _get_funding_interest_rates(self): - rates = self._api.fetch_funding_rates() - interest_rates = {} - for pair, data in rates.items(): - interest_rates[pair] = data['interestRate'] - return interest_rates - - def _calculate_funding_rate(self, pair: str, premium_index: float) -> Optional[float]: - """ - Get's the funding_rate for a pair at a specific date and time in the past - """ - return ( - premium_index + - max(min(self._funding_interest_rates[pair] - premium_index, 0.0005), -0.0005) - ) - - def _get_funding_fee( - self, - pair: str, - contract_size: float, - mark_price: float, - premium_index: Optional[float], - ) -> float: - """ - Calculates a single funding fee - :param contract_size: The amount/quanity - :param mark_price: The price of the asset that the contract is based off of - :param funding_rate: the interest rate and the premium - - interest rate: 0.03% daily, BNBUSDT, LINKUSDT, and LTCUSDT are 0% - - premium: varies by price difference between the perpetual contract and mark price - """ - if premium_index is None: - raise OperationalException("Premium index cannot be None for Binance._get_funding_fee") - nominal_value = mark_price * contract_size - funding_rate = self._calculate_funding_rate(pair, premium_index) - if funding_rate is None: - raise OperationalException("Funding rate should never be none on Binance") - return nominal_value * funding_rate - async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int, is_new_pair: bool ) -> List: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7e1fb9e57..786b8d168 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1529,14 +1529,6 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - # https://www.binance.com/en/support/faq/360033525031 - def fetch_funding_rate(self, pair): - if not self.exchange_has("fetchFundingHistory"): - raise OperationalException( - f"fetch_funding_history() has not been implemented on ccxt.{self.name}") - - return self._api.fetch_funding_rates() - @retrier def get_funding_fees_from_exchange(self, pair: str, since: Union[datetime, int]) -> float: """ @@ -1567,37 +1559,6 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def _get_premium_index(self, pair: str, date: datetime) -> float: - raise OperationalException(f'_get_premium_index has not been implemented on {self.name}') - - def _get_mark_price(self, pair: str, date: datetime) -> float: - raise OperationalException(f'_get_mark_price has not been implemented on {self.name}') - - def _get_funding_rate(self, pair: str, when: datetime): - """ - Get's the funding_rate for a pair at a specific date and time in the past - """ - # TODO-lev: implement - raise OperationalException(f"get_funding_rate has not been implemented for {self.name}") - - def _get_funding_fee( - self, - pair: str, - contract_size: float, - mark_price: float, - premium_index: Optional[float], - # index_price: float, - # interest_rate: float) - ) -> float: - """ - Calculates a single funding fee - :param contract_size: The amount/quanity - :param mark_price: The price of the asset that the contract is based off of - :param funding_rate: the interest rate and the premium - - premium: varies by price difference between the perpetual contract and mark price - """ - raise OperationalException(f"Funding fee has not been implemented for {self.name}") - def _get_funding_fee_dates(self, open_date: datetime, close_date: datetime): """ Get's the date and time of every funding fee that happened between two datetimes @@ -1614,34 +1575,6 @@ class Exchange: return results - def calculate_funding_fees( - self, - pair: str, - amount: float, - open_date: datetime, - close_date: datetime - ) -> float: - """ - calculates the sum of all funding fees that occurred for a pair during a futures trade - :param pair: The quote/base pair of the trade - :param amount: The quantity of the trade - :param open_date: The date and time that the trade started - :param close_date: The date and time that the trade ended - """ - - fees: float = 0 - for date in self._get_funding_fee_dates(open_date, close_date): - premium_index = self._get_premium_index(pair, date) - mark_price = self._get_mark_price(pair, date) - fees += self._get_funding_fee( - pair=pair, - contract_size=amount, - mark_price=mark_price, - premium_index=premium_index - ) - - return fees - def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index ae3659711..8abf84104 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,7 +1,6 @@ """ FTX exchange subclass """ import logging -from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List import ccxt @@ -154,25 +153,3 @@ class Ftx(Exchange): if order['type'] == 'stop': return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] - - def _get_funding_rate(self, pair: str, when: datetime) -> Optional[float]: - """FTX doesn't use this""" - return None - - def _get_funding_fee( - self, - pair: str, - contract_size: float, - mark_price: float, - premium_index: Optional[float], - # index_price: float, - # interest_rate: float) - ) -> float: - """ - Calculates a single funding fee - Always paid in USD on FTX # TODO: How do we account for this - : param contract_size: The amount/quanity - : param mark_price: The price of the asset that the contract is based off of - : param funding_rate: Must be None on ftx - """ - return (contract_size * mark_price) / 24 diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5f7c2c080..9de1947db 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -707,7 +707,6 @@ class LocalTrade(): return float(self._calc_base_close(amount, rate, fee) - total_interest) elif (trading_mode == TradingMode.FUTURES): - self.add_funding_fees() funding_fees = self.funding_fees or 0.0 return float(self._calc_base_close(amount, rate, fee)) + funding_fees else: @@ -786,19 +785,6 @@ class LocalTrade(): else: return None - def add_funding_fees(self): - if self.trading_mode == TradingMode.FUTURES: - # TODO-lev: Calculate this correctly and add it - # if self.config['runmode'].value in ('backtest', 'hyperopt'): - # self.funding_fees = getattr(Exchange, self.exchange).calculate_funding_fees( - # self.exchange, - # self.pair, - # self.amount, - # self.open_date_utc, - # self.close_date_utc - # ) - return - @staticmethod def get_trades_proxy(*, pair: str = None, is_open: bool = None, open_date: datetime = None, close_date: datetime = None, diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 6c8798015..dd85c3abe 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -108,14 +108,6 @@ def test_stoploss_adjust_binance(mocker, default_conf): assert not exchange.stoploss_adjust(1501, order) -def test_get_funding_rate(): - return - - -def test__get_funding_fee(): - return - - @pytest.mark.asyncio async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): ohlcv = [ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index abbbbe4a7..561a9cec5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3044,15 +3044,3 @@ def test_get_funding_fees(default_conf, mocker, exchange_name): pair="XRP/USDT", since=unix_time ) - - -def test_get_mark_price(): - return - - -def test_get_funding_fee_dates(): - return - - -def test_calculate_funding_fees(): - return diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index a4281c595..3794bb79c 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -1,4 +1,3 @@ -from datetime import datetime, timedelta from random import randint from unittest.mock import MagicMock @@ -192,18 +191,3 @@ def test_get_order_id(mocker, default_conf): } } assert exchange.get_order_id_conditional(order) == '1111' - - -@pytest.mark.parametrize("pair,when", [ - ('XRP/USDT', datetime.utcnow()), - ('ADA/BTC', datetime.utcnow()), - ('XRP/USDT', datetime.utcnow() - timedelta(hours=30)), -]) -def test__get_funding_rate(default_conf, mocker, pair, when): - api_mock = MagicMock() - exchange = get_patched_exchange(mocker, default_conf, api_mock, id="ftx") - assert exchange._get_funding_rate(pair, when) is None - - -def test__get_funding_fee(): - return From e7b6f3bfd1e87e9b1b622b90547f339d3f310ef3 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 16 Sep 2021 23:32:23 -0600 Subject: [PATCH 21/59] removed changes to test_persistence --- tests/test_persistence.py | 718 ++++++++++++-------------------------- 1 file changed, 217 insertions(+), 501 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 062aa65fe..5bd283196 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -11,10 +11,10 @@ import pytest from sqlalchemy import create_engine, inspect, text from freqtrade import constants -from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db -from tests.conftest import create_mock_trades, create_mock_trades_with_leverage, log_has, log_has_re +from tests.conftest import (create_mock_trades, create_mock_trades_with_leverage, get_sides, + log_has, log_has_re) def test_init_create_session(default_conf): @@ -65,8 +65,10 @@ def test_init_dryrun_db(default_conf, tmpdir): assert Path(filename).is_file() +@pytest.mark.parametrize('is_short', [False, True]) @pytest.mark.usefixtures("init_persistence") -def test_enter_exit_side(fee): +def test_enter_exit_side(fee, is_short): + enter_side, exit_side = get_sides(is_short) trade = Trade( id=2, pair='ADA/USDT', @@ -78,16 +80,11 @@ def test_enter_exit_side(fee): fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', - is_short=False, + is_short=is_short, leverage=2.0 ) - assert trade.enter_side == 'buy' - assert trade.exit_side == 'sell' - - trade.is_short = True - - assert trade.enter_side == 'sell' - assert trade.exit_side == 'buy' + assert trade.enter_side == enter_side + assert trade.exit_side == exit_side @pytest.mark.usefixtures("init_persistence") @@ -171,8 +168,32 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.initial_stop_loss == 0.09 +@pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest', [ + ("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8)), + ("binance", True, 3, 10, 0.0005, 0.000625), + ("binance", False, 3, 295, 0.0005, round(0.004166666666666667, 8)), + ("binance", True, 3, 295, 0.0005, round(0.0031249999999999997, 8)), + ("binance", False, 3, 295, 0.00025, round(0.0020833333333333333, 8)), + ("binance", True, 3, 295, 0.00025, round(0.0015624999999999999, 8)), + ("binance", False, 5, 295, 0.0005, 0.005), + ("binance", True, 5, 295, 0.0005, round(0.0031249999999999997, 8)), + ("binance", False, 1, 295, 0.0005, 0.0), + ("binance", True, 1, 295, 0.0005, 0.003125), + + ("kraken", False, 3, 10, 0.0005, 0.040), + ("kraken", True, 3, 10, 0.0005, 0.030), + ("kraken", False, 3, 295, 0.0005, 0.06), + ("kraken", True, 3, 295, 0.0005, 0.045), + ("kraken", False, 3, 295, 0.00025, 0.03), + ("kraken", True, 3, 295, 0.00025, 0.0225), + ("kraken", False, 5, 295, 0.0005, round(0.07200000000000001, 8)), + ("kraken", True, 5, 295, 0.0005, 0.045), + ("kraken", False, 1, 295, 0.0005, 0.0), + ("kraken", True, 1, 295, 0.0005, 0.045), + +]) @pytest.mark.usefixtures("init_persistence") -def test_interest(market_buy_order_usdt, fee): +def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, rate, interest): """ 10min, 5hr limit trade on Binance/Kraken at 3x,5x leverage fee: 0.25 % quote @@ -231,115 +252,27 @@ def test_interest(market_buy_order_usdt, fee): stake_amount=20.0, amount=30.0, open_rate=2.0, - open_date=datetime.utcnow() - timedelta(hours=0, minutes=10), + open_date=datetime.utcnow() - timedelta(minutes=minutes), fee_open=fee.return_value, fee_close=fee.return_value, - exchange='binance', - leverage=3.0, - interest_rate=0.0005, - trading_mode=TradingMode.MARGIN + exchange=exchange, + leverage=lev, + interest_rate=rate, + is_short=is_short ) - # 10min, 3x leverage - # binance - assert round(float(trade.calculate_interest()), 8) == round(0.0008333333333333334, 8) - # kraken - trade.exchange = "kraken" - assert float(trade.calculate_interest()) == 0.040 - # Short - trade.is_short = True - trade.recalc_open_trade_value() - # binace - trade.exchange = "binance" - assert float(trade.calculate_interest()) == 0.000625 - # kraken - trade.exchange = "kraken" - assert isclose(float(trade.calculate_interest()), 0.030) - - # 5hr, long - trade.open_date = datetime.utcnow() - timedelta(hours=4, minutes=55) - trade.is_short = False - trade.recalc_open_trade_value() - # binance - trade.exchange = "binance" - assert round(float(trade.calculate_interest()), 8) == round(0.004166666666666667, 8) - # kraken - trade.exchange = "kraken" - assert float(trade.calculate_interest()) == 0.06 - # short - trade.is_short = True - trade.recalc_open_trade_value() - # binace - trade.exchange = "binance" - assert round(float(trade.calculate_interest()), 8) == round(0.0031249999999999997, 8) - # kraken - trade.exchange = "kraken" - assert float(trade.calculate_interest()) == 0.045 - - # 0.00025 interest, 5hr, long - trade.is_short = False - trade.recalc_open_trade_value() - # binance - trade.exchange = "binance" - assert round(float(trade.calculate_interest(interest_rate=0.00025)), - 8) == round(0.0020833333333333333, 8) - # kraken - trade.exchange = "kraken" - assert isclose(float(trade.calculate_interest(interest_rate=0.00025)), 0.03) - # short - trade.is_short = True - trade.recalc_open_trade_value() - # binace - trade.exchange = "binance" - assert round(float(trade.calculate_interest(interest_rate=0.00025)), - 8) == round(0.0015624999999999999, 8) - # kraken - trade.exchange = "kraken" - assert float(trade.calculate_interest(interest_rate=0.00025)) == 0.0225 - - # 5x leverage, 0.0005 interest, 5hr, long - trade.is_short = False - trade.recalc_open_trade_value() - trade.leverage = 5.0 - # binance - trade.exchange = "binance" - assert round(float(trade.calculate_interest()), 8) == 0.005 - # kraken - trade.exchange = "kraken" - assert float(trade.calculate_interest()) == round(0.07200000000000001, 8) - # short - trade.is_short = True - trade.recalc_open_trade_value() - # binace - trade.exchange = "binance" - assert round(float(trade.calculate_interest()), 8) == round(0.0031249999999999997, 8) - # kraken - trade.exchange = "kraken" - assert float(trade.calculate_interest()) == 0.045 - - # 1x leverage, 0.0005 interest, 5hr - trade.is_short = False - trade.recalc_open_trade_value() - trade.leverage = 1.0 - # binance - trade.exchange = "binance" - assert float(trade.calculate_interest()) == 0.0 - # kraken - trade.exchange = "kraken" - assert float(trade.calculate_interest()) == 0.0 - # short - trade.is_short = True - trade.recalc_open_trade_value() - # binace - trade.exchange = "binance" - assert float(trade.calculate_interest()) == 0.003125 - # kraken - trade.exchange = "kraken" - assert float(trade.calculate_interest()) == 0.045 + assert round(float(trade.calculate_interest()), 8) == interest +@pytest.mark.parametrize('is_short,lev,borrowed', [ + (False, 1.0, 0.0), + (True, 1.0, 30.0), + (False, 3.0, 40.0), + (True, 3.0, 30.0), +]) @pytest.mark.usefixtures("init_persistence") -def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, caplog): +def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, + caplog, is_short, lev, borrowed): """ 10 minute limit trade on Binance/Kraken at 1x, 3x leverage fee: 0.25% quote @@ -413,20 +346,19 @@ def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, caplog): fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', + is_short=is_short, + leverage=lev ) - assert trade.borrowed == 0 - trade.is_short = True - trade.recalc_open_trade_value() - assert trade.borrowed == 30.0 - trade.leverage = 3.0 - assert trade.borrowed == 30.0 - trade.is_short = False - trade.recalc_open_trade_value() - assert trade.borrowed == 40.0 + assert trade.borrowed == borrowed +@pytest.mark.parametrize('is_short,open_rate,close_rate,lev,profit', [ + (False, 2.0, 2.2, 1.0, round(0.0945137157107232, 8)), + (True, 2.2, 2.0, 3.0, round(0.2589996297562085, 8)) +]) @pytest.mark.usefixtures("init_persistence") -def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, caplog): +def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_usdt, + is_short, open_rate, close_rate, lev, profit): """ 10 minute limit trade on Binance/Kraken at 1x, 3x leverage fee: 0.25% quote @@ -496,85 +428,52 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca """ + enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt + exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt + enter_side, exit_side = get_sides(is_short) + trade = Trade( id=2, pair='ADA/USDT', stake_amount=60.0, - open_rate=2.0, - amount=30.0, - is_open=True, - open_date=arrow.utcnow().datetime, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance' - ) - assert trade.open_order_id is None - assert trade.close_profit is None - assert trade.close_date is None - - trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) - assert trade.open_order_id is None - assert trade.open_rate == 2.00 - assert trade.close_profit is None - assert trade.close_date is None - assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " - r'pair=ADA/USDT, amount=30.00000000, ' - r"is_short=False, leverage=1.0, open_rate=2.00000000, open_since=.*\).", - caplog) - - caplog.clear() - trade.open_order_id = 'something' - trade.update(limit_sell_order_usdt) - assert trade.open_order_id is None - assert trade.close_rate == 2.20 - assert trade.close_profit == round(0.0945137157107232, 8) - assert trade.close_date is not None - assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " - r"pair=ADA/USDT, amount=30.00000000, " - r"is_short=False, leverage=1.0, open_rate=2.00000000, open_since=.*\).", - caplog) - caplog.clear() - - trade = Trade( - id=226531, - pair='ADA/USDT', - stake_amount=20.0, - open_rate=2.0, + open_rate=open_rate, amount=30.0, is_open=True, open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', - is_short=True, - leverage=3.0, + is_short=is_short, interest_rate=0.0005, - trading_mode=TradingMode.MARGIN + leverage=lev ) - trade.open_order_id = 'something' - trade.update(limit_sell_order_usdt) - assert trade.open_order_id is None - assert trade.open_rate == 2.20 assert trade.close_profit is None assert trade.close_date is None - assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=226531, " - r"pair=ADA/USDT, amount=30.00000000, " - r"is_short=True, leverage=3.0, open_rate=2.20000000, open_since=.*\).", - caplog) - caplog.clear() - trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) + trade.update(enter_order) assert trade.open_order_id is None - assert trade.close_rate == 2.00 - assert trade.close_profit == round(0.2589996297562085, 8) + assert trade.open_rate == open_rate + assert trade.close_profit is None + assert trade.close_date is None + assert log_has_re(f"LIMIT_{enter_side.upper()} has been fulfilled for " + r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, " + f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, " + r"open_since=.*\).", + caplog) + + caplog.clear() + trade.open_order_id = 'something' + trade.update(exit_order) + assert trade.open_order_id is None + assert trade.close_rate == close_rate + assert trade.close_profit == profit assert trade.close_date is not None - assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=226531, " - r"pair=ADA/USDT, amount=30.00000000, " - r"is_short=True, leverage=3.0, open_rate=2.20000000, open_since=.*\).", + assert log_has_re(f"LIMIT_{exit_side.upper()} has been fulfilled for " + r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, " + f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, " + r"open_since=.*\).", caplog) caplog.clear() @@ -619,9 +518,21 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, caplog) +@pytest.mark.parametrize('exchange,is_short,lev,open_value,close_value,profit,profit_ratio', [ + ("binance", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232), + ("binance", True, 1, 59.850, 66.1663784375, -6.316378437500013, -0.1055368159983292), + ("binance", False, 3, 60.15, 65.83416667, 5.684166670000003, 0.2834995845386534), + ("binance", True, 3, 59.85, 66.1663784375, -6.316378437500013, -0.3166104479949876), + + ("kraken", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232), + ("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.106619298245614), + ("kraken", False, 3, 60.15, 65.795, 5.645, 0.2815461346633419), + ("kraken", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842), +]) @pytest.mark.usefixtures("init_persistence") -def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee): - trade = Trade( +def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange, + is_short, lev, open_value, close_value, profit, profit_ratio): + trade: Trade = Trade( pair='ADA/USDT', stake_amount=60.0, open_rate=2.0, @@ -630,56 +541,22 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt interest_rate=0.0005, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='binance', + exchange=exchange, + is_short=is_short, + leverage=lev ) - trade.open_order_id = 'something' + trade.open_order_id = f'something-{is_short}-{lev}-{exchange}' + trade.update(limit_buy_order_usdt) trade.update(limit_sell_order_usdt) - # 1x leverage, binance - assert trade._calc_open_trade_value() == 60.15 - assert isclose(trade.calc_close_trade_value(), 65.835) - assert trade.calc_profit() == 5.685 - assert trade.calc_profit_ratio() == round(0.0945137157107232, 8) - # 3x leverage, binance - trade.trading_mode = TradingMode.MARGIN - trade.leverage = 3 - trade.exchange = "binance" - assert trade._calc_open_trade_value() == 60.15 - assert round(trade.calc_close_trade_value(), 8) == 65.83416667 - assert trade.calc_profit() == round(5.684166670000003, 8) - assert trade.calc_profit_ratio() == round(0.2834995845386534, 8) - trade.exchange = "kraken" - # 3x leverage, kraken - assert trade._calc_open_trade_value() == 60.15 - assert trade.calc_close_trade_value() == 65.795 - assert trade.calc_profit() == 5.645 - assert trade.calc_profit_ratio() == round(0.2815461346633419, 8) - trade.is_short = True + trade.open_rate = 2.0 + trade.close_rate = 2.2 trade.recalc_open_trade_value() - # 3x leverage, short, kraken - assert trade._calc_open_trade_value() == 59.850 - assert trade.calc_close_trade_value() == 66.231165 - assert trade.calc_profit() == round(-6.381165000000003, 8) - assert trade.calc_profit_ratio() == round(-0.319857894736842, 8) - trade.exchange = "binance" - # 3x leverage, short, binance - assert trade._calc_open_trade_value() == 59.85 - assert trade.calc_close_trade_value() == 66.1663784375 - assert trade.calc_profit() == round(-6.316378437500013, 8) - assert trade.calc_profit_ratio() == round(-0.3166104479949876, 8) - # 1x leverage, short, binance - trade.leverage = 1.0 - assert trade._calc_open_trade_value() == 59.850 - assert trade.calc_close_trade_value() == 66.1663784375 - assert trade.calc_profit() == round(-6.316378437500013, 8) - assert trade.calc_profit_ratio() == round(-0.1055368159983292, 8) - # 1x leverage, short, kraken - trade.exchange = "kraken" - assert trade._calc_open_trade_value() == 59.850 - assert trade.calc_close_trade_value() == 66.231165 - assert trade.calc_profit() == -6.381165 - assert trade.calc_profit_ratio() == round(-0.106619298245614, 8) + assert isclose(trade._calc_open_trade_value(), open_value) + assert isclose(trade.calc_close_trade_value(), close_value) + assert isclose(trade.calc_profit(), round(profit, 8)) + assert isclose(trade.calc_profit_ratio(), round(profit_ratio, 8)) @pytest.mark.usefixtures("init_persistence") @@ -770,8 +647,27 @@ def test_update_invalid_order(limit_buy_order_usdt): trade.update(limit_buy_order_usdt) +@pytest.mark.parametrize('exchange', ['binance', 'kraken']) +@pytest.mark.parametrize('lev', [1, 3]) +@pytest.mark.parametrize('is_short,fee_rate,result', [ + (False, 0.003, 60.18), + (False, 0.0025, 60.15), + (False, 0.003, 60.18), + (False, 0.0025, 60.15), + (True, 0.003, 59.82), + (True, 0.0025, 59.85), + (True, 0.003, 59.82), + (True, 0.0025, 59.85) +]) @pytest.mark.usefixtures("init_persistence") -def test_calc_open_trade_value(limit_buy_order_usdt, fee): +def test_calc_open_trade_value( + limit_buy_order_usdt, + exchange, + lev, + is_short, + fee_rate, + result +): # 10 minute limit trade on Binance/Kraken at 1x, 3x leverage # fee: 0.25 %, 0.3% quote # open_rate: 2.00 quote @@ -791,98 +687,104 @@ def test_calc_open_trade_value(limit_buy_order_usdt, fee): stake_amount=60.0, amount=30.0, open_rate=2.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), + fee_open=fee_rate, + fee_close=fee_rate, + exchange=exchange, + leverage=lev, + is_short=is_short ) trade.open_order_id = 'open_trade' - trade.update(limit_buy_order_usdt) # Get the open rate price with the standard fee rate - assert trade._calc_open_trade_value() == 60.15 - - # Margin - trade.trading_mode = TradingMode.MARGIN - trade.is_short = True - trade.recalc_open_trade_value() - assert trade._calc_open_trade_value() == 59.85 - - # 3x short margin leverage - trade.leverage = 3 - trade.exchange = "binance" - assert trade._calc_open_trade_value() == 59.85 - - # 3x long margin leverage - trade.is_short = False - trade.recalc_open_trade_value() - assert trade._calc_open_trade_value() == 60.15 - - # Get the open rate price with a custom fee rate - trade.fee_open = 0.003 - - assert trade._calc_open_trade_value() == 60.18 - trade.is_short = True - trade.recalc_open_trade_value() - assert trade._calc_open_trade_value() == 59.82 + assert trade._calc_open_trade_value() == result +@pytest.mark.parametrize('exchange,is_short,lev,open_rate,close_rate,fee_rate,result', [ + ('binance', False, 1, 2.0, 2.5, 0.0025, 74.8125), + ('binance', False, 1, 2.0, 2.5, 0.003, 74.775), + ('binance', False, 1, 2.0, 2.2, 0.005, 65.67), + ('binance', False, 3, 2.0, 2.5, 0.0025, 74.81166667), + ('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667), + ('kraken', False, 3, 2.0, 2.5, 0.0025, 74.7725), + ('kraken', False, 3, 2.0, 2.5, 0.003, 74.735), + ('kraken', True, 3, 2.2, 2.5, 0.0025, 75.2626875), + ('kraken', True, 3, 2.2, 2.5, 0.003, 75.300225), + ('binance', True, 3, 2.2, 2.5, 0.0025, 75.18906641), + ('binance', True, 3, 2.2, 2.5, 0.003, 75.22656719), + ('binance', True, 1, 2.2, 2.5, 0.0025, 75.18906641), + ('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719), + ('kraken', True, 1, 2.2, 2.5, 0.0025, 75.2626875), + ('kraken', True, 1, 2.2, 2.5, 0.003, 75.300225), +]) @pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee): +def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, open_rate, + exchange, is_short, lev, close_rate, fee_rate, result): trade = Trade( pair='ADA/USDT', stake_amount=60.0, amount=30.0, - open_rate=2.0, + open_rate=open_rate, open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance', + fee_open=fee_rate, + fee_close=fee_rate, + exchange=exchange, interest_rate=0.0005, + is_short=is_short, + leverage=lev ) trade.open_order_id = 'close_trade' - trade.update(limit_buy_order_usdt) - - # 1x leverage binance - assert trade.calc_close_trade_value(rate=2.5) == 74.8125 - assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 74.775 - trade.update(limit_sell_order_usdt) - assert trade.calc_close_trade_value(fee=0.005) == 65.67 - - # 3x leverage binance - trade.trading_mode = TradingMode.MARGIN - trade.leverage = 3.0 - assert round(trade.calc_close_trade_value(rate=2.5), 8) == 74.81166667 - assert round(trade.calc_close_trade_value(rate=2.5, fee=0.003), 8) == 74.77416667 - - # 3x leverage kraken - trade.exchange = "kraken" - assert trade.calc_close_trade_value(rate=2.5) == 74.7725 - assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 74.735 - - # 3x leverage kraken, short - trade.is_short = True - trade.recalc_open_trade_value() - assert round(trade.calc_close_trade_value(rate=2.5), 8) == 75.2626875 - assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 75.300225 - - # 3x leverage binance, short - trade.exchange = "binance" - assert round(trade.calc_close_trade_value(rate=2.5), 8) == 75.18906641 - assert round(trade.calc_close_trade_value(rate=2.5, fee=0.003), 8) == 75.22656719 - - trade.leverage = 1.0 - # 1x leverage binance, short - assert round(trade.calc_close_trade_value(rate=2.5), 8) == 75.18906641 - assert round(trade.calc_close_trade_value(rate=2.5, fee=0.003), 8) == 75.22656719 - - # 1x leverage kraken, short - trade.exchange = "kraken" - assert round(trade.calc_close_trade_value(rate=2.5), 8) == 75.2626875 - assert trade.calc_close_trade_value(rate=2.5, fee=0.003) == 75.300225 + assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result +@pytest.mark.parametrize('exchange,is_short,lev,close_rate,fee_close,profit,profit_ratio', [ + ('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673), + ('binance', False, 3, 2.1, 0.0025, 2.69166667, 0.13424771421446402), + ('binance', True, 1, 2.1, 0.0025, -3.308815781249997, -0.05528514254385963), + ('binance', True, 3, 2.1, 0.0025, -3.308815781249997, -0.1658554276315789), + + ('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632), + ('binance', False, 3, 1.9, 0.0025, -3.29333333, -0.16425602643391513), + ('binance', True, 1, 1.9, 0.0025, 2.7063095312499996, 0.045218204365079395), + ('binance', True, 3, 1.9, 0.0025, 2.7063095312499996, 0.13565461309523819), + + ('binance', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232), + ('binance', False, 3, 2.2, 0.0025, 5.68416667, 0.2834995845386534), + ('binance', True, 1, 2.2, 0.0025, -6.316378437499999, -0.1055368159983292), + ('binance', True, 3, 2.2, 0.0025, -6.316378437499999, -0.3166104479949876), + + ('kraken', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673), + ('kraken', False, 3, 2.1, 0.0025, 2.6525, 0.13229426433915248), + ('kraken', True, 1, 2.1, 0.0025, -3.3706575, -0.05631842105263152), + ('kraken', True, 3, 2.1, 0.0025, -3.3706575, -0.16895526315789455), + + ('kraken', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632), + ('kraken', False, 3, 1.9, 0.0025, -3.3325, -0.16620947630922667), + ('kraken', True, 1, 1.9, 0.0025, 2.6503575, 0.04428333333333334), + ('kraken', True, 3, 1.9, 0.0025, 2.6503575, 0.13285000000000002), + + ('kraken', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232), + ('kraken', False, 3, 2.2, 0.0025, 5.645, 0.2815461346633419), + ('kraken', True, 1, 2.2, 0.0025, -6.381165, -0.106619298245614), + ('kraken', True, 3, 2.2, 0.0025, -6.381165, -0.319857894736842), + + ('binance', False, 1, 2.1, 0.003, 2.6610000000000014, 0.04423940149625927), + ('binance', False, 1, 1.9, 0.003, -3.320999999999998, -0.05521197007481293), + ('binance', False, 1, 2.2, 0.003, 5.652000000000008, 0.09396508728179565), +]) @pytest.mark.usefixtures("init_persistence") -def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): +def test_calc_profit( + limit_buy_order_usdt, + limit_sell_order_usdt, + fee, + exchange, + is_short, + lev, + close_rate, + fee_close, + profit, + profit_ratio +): """ 10 minute limit trade on Binance/Kraken at 1x, 3x leverage arguments: @@ -1019,202 +921,16 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): open_rate=2.0, open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), interest_rate=0.0005, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance' + exchange=exchange, + is_short=is_short, + leverage=lev, + fee_open=0.0025, + fee_close=fee_close ) trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) # Buy @ 2.0 - # 1x Leverage, long - # Custom closing rate and regular fee rate - # Higher than open rate - 2.1 quote - assert trade.calc_profit(rate=2.1) == 2.6925 - # Lower than open rate - 1.9 quote - assert trade.calc_profit(rate=1.9) == round(-3.292499999999997, 8) - - # fee 0.003 - # Higher than open rate - 2.1 quote - assert trade.calc_profit(rate=2.1, fee=0.003) == 2.661 - # Lower than open rate - 1.9 quote - assert trade.calc_profit(rate=1.9, fee=0.003) == round(-3.320999999999998, 8) - - # Test when we apply a Sell order. Sell higher than open rate @ 2.2 - trade.update(limit_sell_order_usdt) - assert trade.calc_profit() == round(5.684999999999995, 8) - - # Test with a custom fee rate on the close trade - assert trade.calc_profit(fee=0.003) == round(5.652000000000008, 8) - - trade.open_trade_value = 0.0 - trade.open_trade_value = trade._calc_open_trade_value() - - # Margin - trade.trading_mode = TradingMode.MARGIN - # 3x leverage, long ################################################### - trade.leverage = 3.0 - # Higher than open rate - 2.1 quote - trade.exchange = "binance" # binance - assert trade.calc_profit(rate=2.1, fee=0.0025) == 2.69166667 - trade.exchange = "kraken" - assert trade.calc_profit(rate=2.1, fee=0.0025) == 2.6525 - - # 1.9 quote - trade.exchange = "binance" # binance - assert trade.calc_profit(rate=1.9, fee=0.0025) == -3.29333333 - trade.exchange = "kraken" - assert trade.calc_profit(rate=1.9, fee=0.0025) == -3.3325 - - # 2.2 quote - trade.exchange = "binance" # binance - assert trade.calc_profit(fee=0.0025) == 5.68416667 - trade.exchange = "kraken" - assert trade.calc_profit(fee=0.0025) == 5.645 - - # 3x leverage, short ################################################### - trade.is_short = True - trade.recalc_open_trade_value() - # 2.1 quote - Higher than open rate - trade.exchange = "binance" # binance - assert trade.calc_profit(rate=2.1, fee=0.0025) == round(-3.308815781249997, 8) - trade.exchange = "kraken" - assert trade.calc_profit(rate=2.1, fee=0.0025) == -3.3706575 - - # 1.9 quote - Lower than open rate - trade.exchange = "binance" # binance - assert trade.calc_profit(rate=1.9, fee=0.0025) == round(2.7063095312499996, 8) - trade.exchange = "kraken" - assert trade.calc_profit(rate=1.9, fee=0.0025) == 2.6503575 - - # Test when we apply a Sell order. Uses sell order used above - trade.exchange = "binance" # binance - assert trade.calc_profit(fee=0.0025) == round(-6.316378437499999, 8) - trade.exchange = "kraken" - assert trade.calc_profit(fee=0.0025) == -6.381165 - - # 1x leverage, short ################################################### - trade.leverage = 1.0 - # 2.1 quote - Higher than open rate - trade.exchange = "binance" # binance - assert trade.calc_profit(rate=2.1, fee=0.0025) == round(-3.308815781249997, 8) - trade.exchange = "kraken" - assert trade.calc_profit(rate=2.1, fee=0.0025) == -3.3706575 - - # 1.9 quote - Lower than open rate - trade.exchange = "binance" # binance - assert trade.calc_profit(rate=1.9, fee=0.0025) == round(2.7063095312499996, 8) - trade.exchange = "kraken" - assert trade.calc_profit(rate=1.9, fee=0.0025) == 2.6503575 - - # Test when we apply a Sell order. Uses sell order used above - trade.exchange = "binance" # binance - assert trade.calc_profit(fee=0.0025) == round(-6.316378437499999, 8) - trade.exchange = "kraken" - assert trade.calc_profit(fee=0.0025) == -6.381165 - - -@pytest.mark.usefixtures("init_persistence") -def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee): - trade = Trade( - pair='ADA/USDT', - stake_amount=60.0, - amount=30.0, - open_rate=2.0, - open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), - interest_rate=0.0005, - fee_open=fee.return_value, - fee_close=fee.return_value, - exchange='binance' - ) - trade.open_order_id = 'something' - trade.update(limit_buy_order_usdt) # Buy @ 2.0 - - # 1x Leverage, long - # Custom closing rate and regular fee rate - # Higher than open rate - 2.1 quote - assert trade.calc_profit_ratio(rate=2.1) == round(0.04476309226932673, 8) - # Lower than open rate - 1.9 quote - assert trade.calc_profit_ratio(rate=1.9) == round(-0.05473815461346632, 8) - - # fee 0.003 - # Higher than open rate - 2.1 quote - assert trade.calc_profit_ratio(rate=2.1, fee=0.003) == round(0.04423940149625927, 8) - # Lower than open rate - 1.9 quote - assert trade.calc_profit_ratio(rate=1.9, fee=0.003) == round(-0.05521197007481293, 8) - - # Test when we apply a Sell order. Sell higher than open rate @ 2.2 - trade.update(limit_sell_order_usdt) - assert trade.calc_profit_ratio() == round(0.0945137157107232, 8) - - # Test with a custom fee rate on the close trade - assert trade.calc_profit_ratio(fee=0.003) == round(0.09396508728179565, 8) - - trade.open_trade_value = 0.0 - assert trade.calc_profit_ratio(fee=0.003) == 0.0 - trade.open_trade_value = trade._calc_open_trade_value() - - # Margin - trade.trading_mode = TradingMode.MARGIN - # 3x leverage, long ################################################### - trade.leverage = 3.0 - # 2.1 quote - Higher than open rate - trade.exchange = "binance" # binance - assert trade.calc_profit_ratio(rate=2.1) == round(0.13424771421446402, 8) - trade.exchange = "kraken" - assert trade.calc_profit_ratio(rate=2.1) == round(0.13229426433915248, 8) - - # 1.9 quote - Lower than open rate - trade.exchange = "binance" # binance - assert trade.calc_profit_ratio(rate=1.9) == round(-0.16425602643391513, 8) - trade.exchange = "kraken" - assert trade.calc_profit_ratio(rate=1.9) == round(-0.16620947630922667, 8) - - # Test when we apply a Sell order. Uses sell order used above - trade.exchange = "binance" # binance - assert trade.calc_profit_ratio() == round(0.2834995845386534, 8) - trade.exchange = "kraken" - assert trade.calc_profit_ratio() == round(0.2815461346633419, 8) - - # 3x leverage, short ################################################### - trade.is_short = True - trade.recalc_open_trade_value() - # 2.1 quote - Higher than open rate - trade.exchange = "binance" # binance - assert trade.calc_profit_ratio(rate=2.1) == round(-0.1658554276315789, 8) - trade.exchange = "kraken" - assert trade.calc_profit_ratio(rate=2.1) == round(-0.16895526315789455, 8) - - # 1.9 quote - Lower than open rate - trade.exchange = "binance" # binance - assert trade.calc_profit_ratio(rate=1.9) == round(0.13565461309523819, 8) - trade.exchange = "kraken" - assert trade.calc_profit_ratio(rate=1.9) == round(0.13285000000000002, 8) - - # Test when we apply a Sell order. Uses sell order used above - trade.exchange = "binance" # binance - assert trade.calc_profit_ratio() == round(-0.3166104479949876, 8) - trade.exchange = "kraken" - assert trade.calc_profit_ratio() == round(-0.319857894736842, 8) - - # 1x leverage, short ################################################### - trade.leverage = 1.0 - # 2.1 quote - Higher than open rate - trade.exchange = "binance" # binance - assert trade.calc_profit_ratio(rate=2.1) == round(-0.05528514254385963, 8) - trade.exchange = "kraken" - assert trade.calc_profit_ratio(rate=2.1) == round(-0.05631842105263152, 8) - - # 1.9 quote - Lower than open rate - trade.exchange = "binance" - assert trade.calc_profit_ratio(rate=1.9) == round(0.045218204365079395, 8) - trade.exchange = "kraken" - assert trade.calc_profit_ratio(rate=1.9) == round(0.04428333333333334, 8) - - # Test when we apply a Sell order. Uses sell order used above - trade.exchange = "binance" - assert trade.calc_profit_ratio() == round(-0.1055368159983292, 8) - trade.exchange = "kraken" - assert trade.calc_profit_ratio() == round(-0.106619298245614, 8) + assert trade.calc_profit(rate=close_rate) == round(profit, 8) + assert trade.calc_profit_ratio(rate=close_rate) == round(profit_ratio, 8) @pytest.mark.usefixtures("init_persistence") @@ -1724,7 +1440,7 @@ def test_to_json(default_conf, fee): 'isolated_liq': None, 'is_short': None, 'trading_mode': None, - 'funding_fees': None, + 'funding_fees': None } # Simulate dry_run entries @@ -1797,7 +1513,7 @@ def test_to_json(default_conf, fee): 'isolated_liq': None, 'is_short': None, 'trading_mode': None, - 'funding_fees': None, + 'funding_fees': None } From 81235794424788cbe31e0f93661c7663b2b410c0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 16 Sep 2021 23:47:44 -0600 Subject: [PATCH 22/59] added trading mode to persistence tests --- tests/test_persistence.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 5bd283196..58ce47ea7 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -11,6 +11,7 @@ import pytest from sqlalchemy import create_engine, inspect, text from freqtrade import constants +from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db from tests.conftest import (create_mock_trades, create_mock_trades_with_leverage, get_sides, @@ -81,7 +82,8 @@ def test_enter_exit_side(fee, is_short): fee_close=fee.return_value, exchange='binance', is_short=is_short, - leverage=2.0 + leverage=2.0, + trading_mode=TradingMode.MARGIN ) assert trade.enter_side == enter_side assert trade.exit_side == exit_side @@ -101,7 +103,8 @@ def test_set_stop_loss_isolated_liq(fee): fee_close=fee.return_value, exchange='binance', is_short=False, - leverage=2.0 + leverage=2.0, + trading_mode=TradingMode.MARGIN ) trade.set_isolated_liq(0.09) assert trade.isolated_liq == 0.09 @@ -258,7 +261,8 @@ def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, exchange=exchange, leverage=lev, interest_rate=rate, - is_short=is_short + is_short=is_short, + trading_mode=TradingMode.MARGIN ) assert round(float(trade.calculate_interest()), 8) == interest @@ -347,7 +351,8 @@ def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, fee_close=fee.return_value, exchange='binance', is_short=is_short, - leverage=lev + leverage=lev, + trading_mode=TradingMode.MARGIN ) assert trade.borrowed == borrowed @@ -445,7 +450,8 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_ exchange='binance', is_short=is_short, interest_rate=0.0005, - leverage=lev + leverage=lev, + trading_mode=TradingMode.MARGIN ) assert trade.open_order_id is None assert trade.close_profit is None @@ -491,6 +497,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, fee_close=fee.return_value, open_date=arrow.utcnow().datetime, exchange='binance', + trading_mode=TradingMode.MARGIN ) trade.open_order_id = 'something' @@ -543,7 +550,8 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt fee_close=fee.return_value, exchange=exchange, is_short=is_short, - leverage=lev + leverage=lev, + trading_mode=TradingMode.MARGIN ) trade.open_order_id = f'something-{is_short}-{lev}-{exchange}' @@ -572,6 +580,7 @@ def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), interest_rate=0.0005, exchange='binance', + trading_mode=TradingMode.MARGIN ) assert trade.close_profit is None assert trade.close_date is None @@ -600,6 +609,7 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee): fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', + trading_mode=TradingMode.MARGIN ) trade.open_order_id = 'something' @@ -617,6 +627,7 @@ def test_update_open_order(limit_buy_order_usdt): fee_open=0.1, fee_close=0.1, exchange='binance', + trading_mode=TradingMode.MARGIN ) assert trade.open_order_id is None @@ -641,6 +652,7 @@ def test_update_invalid_order(limit_buy_order_usdt): fee_open=0.1, fee_close=0.1, exchange='binance', + trading_mode=TradingMode.MARGIN ) limit_buy_order_usdt['type'] = 'invalid' with pytest.raises(ValueError, match=r'Unknown order type'): @@ -692,7 +704,8 @@ def test_calc_open_trade_value( fee_close=fee_rate, exchange=exchange, leverage=lev, - is_short=is_short + is_short=is_short, + trading_mode=TradingMode.MARGIN ) trade.open_order_id = 'open_trade' @@ -731,7 +744,8 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, ope exchange=exchange, interest_rate=0.0005, is_short=is_short, - leverage=lev + leverage=lev, + trading_mode=TradingMode.MARGIN ) trade.open_order_id = 'close_trade' assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result @@ -925,7 +939,8 @@ def test_calc_profit( is_short=is_short, leverage=lev, fee_open=0.0025, - fee_close=fee_close + fee_close=fee_close, + trading_mode=TradingMode.MARGIN ) trade.open_order_id = 'something' From ac4f5adfe26a2d9dd7fd7d2a372a7713df3e84be Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 01:16:22 -0600 Subject: [PATCH 23/59] switched since = int(since.timestamp()) from %s --- 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 786b8d168..a248a780e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1543,7 +1543,7 @@ class Exchange: f"fetch_funding_history() has not been implemented on ccxt.{self.name}") if type(since) is datetime: - since = int(since.strftime('%s')) + since = int(since.timestamp()) try: funding_history = self._api.fetch_funding_history( From ddc203ca690d71568645d9b8231bd48f59b41d3d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 02:26:59 -0600 Subject: [PATCH 24/59] remove %s in test_exchange unix time --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 561a9cec5..bd0994c18 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3020,7 +3020,7 @@ def test_get_funding_fees(default_conf, mocker, exchange_name): # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') - unix_time = int(date_time.strftime('%s')) + unix_time = int(date_time.timestamp()) expected_fees = -0.001 # 0.14542341 + -0.14642341 fees_from_datetime = exchange.get_funding_fees_from_exchange( pair='XRP/USDT', From 60a678fea736ecff30a9b0b509875292f6774930 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 17:02:09 -0600 Subject: [PATCH 25/59] merged with feat/short --- docs/advanced-hyperopt.md | 32 + docs/hyperopt.md | 2 +- docs/includes/pairlists.md | 14 + docs/leverage.md | 4 + docs/strategy-advanced.md | 6 + docs/strategy-customization.md | 161 +++ freqtrade/commands/hyperopt_commands.py | 2 +- freqtrade/configuration/PeriodicCache.py | 19 + freqtrade/configuration/__init__.py | 1 + freqtrade/edge/edge_positioning.py | 2 +- freqtrade/exchange/bibox.py | 5 +- freqtrade/exchange/binance.py | 145 +- .../exchange/binance_leverage_brackets.json | 1214 +++++++++++++++++ freqtrade/exchange/exchange.py | 175 ++- freqtrade/exchange/ftx.py | 50 +- freqtrade/exchange/kraken.py | 87 +- freqtrade/freqtradebot.py | 27 +- freqtrade/leverage/interest.py | 7 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/optimize/edge_cli.py | 2 + freqtrade/optimize/hyperopt.py | 18 +- freqtrade/optimize/hyperopt_auto.py | 7 +- freqtrade/optimize/hyperopt_interface.py | 13 +- freqtrade/optimize/hyperopt_tools.py | 8 +- freqtrade/persistence/models.py | 10 +- freqtrade/plugins/pairlist/AgeFilter.py | 16 +- .../plugins/pairlist/PerformanceFilter.py | 11 +- freqtrade/rpc/api_server/api_schemas.py | 6 + freqtrade/rpc/rpc.py | 21 +- freqtrade/rpc/telegram.py | 22 +- freqtrade/strategy/__init__.py | 4 +- freqtrade/strategy/informative_decorator.py | 128 ++ freqtrade/strategy/interface.py | 49 +- freqtrade/strategy/strategy_helper.py | 45 +- requirements-dev.txt | 2 + setup.sh | 2 +- tests/conftest.py | 53 +- tests/exchange/test_binance.py | 286 +++- tests/exchange/test_exchange.py | 229 +++- tests/exchange/test_ftx.py | 116 +- tests/exchange/test_kraken.py | 108 +- .../{test_leverage.py => test_interest.py} | 7 +- tests/optimize/test_hyperopt.py | 4 + tests/plugins/test_pairlist.py | 108 +- tests/rpc/test_rpc_apiserver.py | 15 +- tests/rpc/test_rpc_telegram.py | 2 + .../strats/informative_decorator_strategy.py | 75 + tests/strategy/test_interface.py | 2 +- tests/strategy/test_strategy_helpers.py | 66 +- tests/strategy/test_strategy_loading.py | 6 +- tests/test_freqtradebot.py | 591 +++----- tests/test_periodiccache.py | 32 + 52 files changed, 3356 insertions(+), 663 deletions(-) create mode 100644 freqtrade/configuration/PeriodicCache.py create mode 100644 freqtrade/exchange/binance_leverage_brackets.json create mode 100644 freqtrade/strategy/informative_decorator.py rename tests/leverage/{test_leverage.py => test_interest.py} (83%) create mode 100644 tests/strategy/strats/informative_decorator_strategy.py create mode 100644 tests/test_periodiccache.py diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index f2f52b7dd..f5a52ff49 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -98,6 +98,38 @@ class MyAwesomeStrategy(IStrategy): !!! Note All overrides are optional and can be mixed/matched as necessary. +### Overriding Base estimator + +You can define your own estimator for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass. + +```python +class MyAwesomeStrategy(IStrategy): + class HyperOpt: + def generate_estimator(): + return "RF" + +``` + +Possible values are either one of "GP", "RF", "ET", "GBRT" (Details can be found in the [scikit-optimize documentation](https://scikit-optimize.github.io/)), or "an instance of a class that inherits from `RegressorMixin` (from sklearn) and where the `predict` method has an optional `return_std` argument, which returns `std(Y | x)` along with `E[Y | x]`". + +Some research will be necessary to find additional Regressors. + +Example for `ExtraTreesRegressor` ("ET") with additional parameters: + +```python +class MyAwesomeStrategy(IStrategy): + class HyperOpt: + def generate_estimator(): + from skopt.learning import ExtraTreesRegressor + # Corresponds to "ET" - but allows additional parameters. + return ExtraTreesRegressor(n_estimators=100) + +``` + +!!! Note + While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used. + If you're unsure about this, best use one of the Defaults (`"ET"` has proven to be the most versatile) without further parameters. + ## Space options For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types: diff --git a/docs/hyperopt.md b/docs/hyperopt.md index e69b761c4..09d43939a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -677,7 +677,7 @@ If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace f These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. -If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. +If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 69e12d5dc..b612a4ddf 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -165,6 +165,7 @@ Example to remove the first 10 pairs from the pairlist: ```json "pairlists": [ + // ... { "method": "OffsetFilter", "offset": 10 @@ -190,6 +191,19 @@ Sorts pairs by past trade performance, as follows: Trade count is used as a tie breaker. +You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window). +Not defining this parameter (or setting it to 0) will use all-time performance. + +```json +"pairlists": [ + // ... + { + "method": "PerformanceFilter", + "minutes": 1440 // rolling 24h + } +], +``` + !!! Note `PerformanceFilter` does not support backtesting mode. diff --git a/docs/leverage.md b/docs/leverage.md index c4b975a0b..9448c64c3 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -15,3 +15,7 @@ For longs, the currency which pays the interest fee for the `borrowed` will alre Rollover fee = P (borrowed money) * R (quat_hourly_interest) * ceiling(T/4) (in hours) I (interest) = Opening fee + Rollover fee [source](https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-) + +# TODO-lev: Mention that says you can't run 2 bots on the same account with leverage, + +#TODO-lev: Create a huge risk disclaimer \ No newline at end of file diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4409af6ea..2b9517f3b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -288,6 +288,12 @@ Stoploss values returned from `custom_stoploss()` always specify a percentage re The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. +### Calculating stoploss percentage from absolute price + +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. + +The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`. + #### Stepped stoploss Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index cfea60d22..725252b30 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -639,6 +639,167 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. +!!! Note + Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings. + This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade + is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `sell_reason` in + `confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when + `current_profit < open_relative_stop`. + +### *stoploss_from_absolute()* + +In some situations it may be confusing to deal with stops relative to current rate. Instead, you may define a stoploss level using an absolute price. + +??? Example "Returning a stoploss using absolute price from the custom stoploss function" + + If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)`. + + ``` python + + from datetime import datetime + from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, stoploss_from_open + + class AwesomeStrategy(IStrategy): + + use_custom_stoploss = True + + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) + return dataframe + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + candle = dataframe.iloc[-1].squeeze() + return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate) + + ``` + +### *@informative()* + +``` python +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to: + * {base}_{quote}_{column}_{timeframe} if asset is specified. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. + """ +``` + +In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation, +not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method. +When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter) +for more information. + +??? Example "Fast and easy way to define informative pairs" + + Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs. + + ``` python + + from datetime import datetime + from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, informative + + class AwesomeStrategy(IStrategy): + + # This method is not required. + # def informative_pairs(self): ... + + # Define informative upper timeframe for each pair. Decorators can be stacked on same + # method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'. + @informative('30m') + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. Available in populate_indicators and other methods as + # 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable + # instead of hardcoding actual stake currency. Available in populate_indicators and other + # methods as 'btc_usdt_rsi_1h' (when stake currency is USDT). + @informative('1h', 'BTC/{stake}') + def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/ETH informative pair. You must specify quote currency if it is different from + # stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'. + @informative('1h', 'ETH/BTC') + def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. A custom formatter may be specified for formatting + # column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom + # formatting. Available in populate_indicators and other methods as 'rsi_upper'. + @informative('1h', 'BTC/{stake}', '{column}') + def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Strategy timeframe indicators for current pair. + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + # Informative pairs are available in this method. + dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] + return dataframe + + ``` + +!!! Note + Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs + manually as described [in the DataProvider section](#complete-data-provider-sample). + +!!! Note + Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code. + + ``` python + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + stake = self.config['stake_currency'] + dataframe.loc[ + ( + (dataframe[f'btc_{stake}_rsi_1h'] < 35) + & + (dataframe['volume'] > 0) + ), + ['buy', 'buy_tag']] = (1, 'buy_signal_rsi') + + return dataframe + ``` + + Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`. + +!!! Warning "Duplicate method names" + Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method) + will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators + created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique! ## Additional data (Wallets) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index d2d30f399..ec1ff92cf 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -53,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if epochs and export_csv: HyperoptTools.export_csv_file( - config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv + config, epochs, export_csv ) diff --git a/freqtrade/configuration/PeriodicCache.py b/freqtrade/configuration/PeriodicCache.py new file mode 100644 index 000000000..25c0c47f3 --- /dev/null +++ b/freqtrade/configuration/PeriodicCache.py @@ -0,0 +1,19 @@ +from datetime import datetime, timezone + +from cachetools.ttl import TTLCache + + +class PeriodicCache(TTLCache): + """ + Special cache that expires at "straight" times + A timer with ttl of 3600 (1h) will expire at every full hour (:00). + """ + + def __init__(self, maxsize, ttl, getsizeof=None): + def local_timer(): + ts = datetime.now(timezone.utc).timestamp() + offset = (ts % ttl) + return ts - offset + + # Init with smlight offset + super().__init__(maxsize=maxsize, ttl=ttl-1e-5, timer=local_timer, getsizeof=getsizeof) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 730a4e47f..cf41c0ca9 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -4,4 +4,5 @@ from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.configuration import Configuration +from freqtrade.configuration.PeriodicCache import PeriodicCache from freqtrade.configuration.timerange import TimeRange diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index f12b1b37d..1950f0d08 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -119,7 +119,7 @@ class Edge: ) # Download informative pairs too res = defaultdict(list) - for p, t in self.strategy.informative_pairs(): + for p, t in self.strategy.gather_informative_pairs(): res[t].append(p) for timeframe, inf_pairs in res.items(): timerange_startup = deepcopy(self._timerange) diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index f0c2dd00b..074dd2b10 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -20,4 +20,7 @@ class Bibox(Exchange): # fetchCurrencies API point requires authentication for Bibox, # so switch it off for Freqtrade load_markets() - _ccxt_config: Dict = {"has": {"fetchCurrencies": False}} + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {"has": {"fetchCurrencies": False}} diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index f7eb03b57..8779fdc8b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,10 +1,13 @@ """ Binance exchange subclass """ +import json import logging -from typing import Dict, List +from pathlib import Path +from typing import Dict, List, Optional, Tuple import arrow import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -27,36 +30,74 @@ class Binance(Exchange): } funding_fee_times: List[int] = [0, 8, 16] # hours of the day - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # TradingMode.SPOT always supported and not required in this list + # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported + # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported + # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported + ] + + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + if self.trading_mode == TradingMode.MARGIN: + return { + "options": { + "defaultType": "margin" + } + } + elif self.trading_mode == TradingMode.FUTURES: + return { + "options": { + "defaultType": "future" + } + } + else: + return {} + + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. + :param side: "buy" or "sell" """ - return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) + + return order['type'] == 'stop_loss_limit' and ( + (side == "sell" and stop_loss > float(order['info']['stopPrice'])) or + (side == "buy" and stop_loss < float(order['info']['stopPrice'])) + ) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ creates a stoploss limit order. this stoploss-limit is binance-specific. It may work with a limited number of other exchanges, but this has not been tested yet. + :param side: "buy" or "sell" """ # Limit price threshold: As limit price should always be below stop-price limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) - rate = stop_price * limit_price_pct + if side == "sell": + # TODO: Name limit_rate in other exchange subclasses + rate = stop_price * limit_price_pct + else: + rate = stop_price * (2 - limit_price_pct) ordertype = "stop_loss_limit" stop_price = self.price_to_precision(pair, stop_price) + bad_stop_price = (stop_price <= rate) if side == "sell" else (stop_price >= rate) + # Ensure rate is less than stop price - if stop_price <= rate: + if bad_stop_price: raise OperationalException( - 'In stoploss limit order, stop price should be more than limit price') + 'In stoploss limit order, stop price should be better than limit price') if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: @@ -67,7 +108,8 @@ class Binance(Exchange): rate = self.price_to_precision(pair, rate) - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + self._lev_prep(pair, leverage) + order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, price=rate, params=params) logger.info('stoploss limit order added for %s. ' 'stop price: %s. limit: %s', pair, stop_price, rate) @@ -75,21 +117,96 @@ class Binance(Exchange): return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( - f'Insufficient funds to create {ordertype} sell order on market {pair}. ' - f'Tried to sell amount {amount} at rate {rate}. ' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' + f'Tried to {side} amount {amount} at rate {rate}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: # Errors: # `binance Order would trigger immediately.` raise InvalidOrderException( - f'Could not create {ordertype} sell order on market {pair}. ' - f'Tried to sell amount {amount} at rate {rate}. ' + f'Could not create {ordertype} {side} order on market {pair}. ' + f'Tried to {side} amount {amount} at rate {rate}. ' f'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 place sell order due to {e.__class__.__name__}. Message: {e}') from e + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + @retrier + def fill_leverage_brackets(self): + """ + Assigns property _leverage_brackets to a dictionary of information about the leverage + allowed on each pair + """ + if self.trading_mode == TradingMode.FUTURES: + try: + if self._config['dry_run']: + leverage_brackets_path = ( + Path(__file__).parent / 'binance_leverage_brackets.json' + ) + with open(leverage_brackets_path) as json_file: + leverage_brackets = json.load(json_file) + else: + leverage_brackets = self._api.load_leverage_brackets() + + for pair, brackets in leverage_brackets.items(): + self._leverage_brackets[pair] = [ + [ + min_amount, + float(margin_req) + ] for [ + min_amount, + margin_req + ] in brackets + ] + + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError(f'Could not fetch leverage amounts due to' + f'{e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at + :param pair: The base/quote currency pair being traded + :nominal_value: The total value of the trade in quote currency (collateral + debt) + """ + pair_brackets = self._leverage_brackets[pair] + max_lev = 1.0 + for [min_amount, margin_req] in pair_brackets: + if nominal_value >= min_amount: + max_lev = 1/margin_req + return max_lev + + @ retrier + def _set_leverage( + self, + leverage: float, + pair: Optional[str] = None, + trading_mode: Optional[TradingMode] = None + ): + """ + Set's the leverage before making a trade, in order to not + have the same leverage on every trade + """ + trading_mode = trading_mode or self.trading_mode + + if self._config['dry_run'] or trading_mode != TradingMode.FUTURES: + return + + try: + self._api.set_leverage(symbol=pair, leverage=leverage) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e diff --git a/freqtrade/exchange/binance_leverage_brackets.json b/freqtrade/exchange/binance_leverage_brackets.json new file mode 100644 index 000000000..4450b015e --- /dev/null +++ b/freqtrade/exchange/binance_leverage_brackets.json @@ -0,0 +1,1214 @@ +{ + "1000SHIB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "1INCH/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "AAVE/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "ADA/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "ADA/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "AKRO/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ALGO/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "ALICE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ALPHA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ANKR/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ATA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ATOM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "AUDIO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "AVAX/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "AXS/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"], + [15000000.0, "0.5"] + ], + "BAKE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAND/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BCH/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BEL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BLZ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BNB/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "BNB/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTC/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.0, "0.5"] + ], + "BTC/USDT": [ + [0.0, "0.004"], + [50000.0, "0.005"], + [250000.0, "0.01"], + [1000000.0, "0.025"], + [5000000.0, "0.05"], + [20000000.0, "0.1"], + [50000000.0, "0.125"], + [100000000.0, "0.15"], + [200000000.0, "0.25"], + [300000000.0, "0.5"] + ], + "BTCBUSD_210129": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCBUSD_210226": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCDOM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTCSTUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTCUSDT_210326": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTCUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTCUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "BTS/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BZRX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "C98/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CELR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CHR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CHZ/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "COMP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "COTI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CRV/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CTK/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CVC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DASH/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DEFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DENT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DGB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DODO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DOGE/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "DOGE/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "DOT/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "DOTECOUSDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DYDX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "EGLD/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ENJ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "EOS/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETC/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETH/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.0, "0.5"] + ], + "ETH/USDT": [ + [0.0, "0.005"], + [10000.0, "0.0065"], + [100000.0, "0.01"], + [500000.0, "0.02"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "ETHUSDT_210326": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETHUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETHUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "FIL/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "FLM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "FTM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "FTT/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "GRT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "GTC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HBAR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HNT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HOT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ICP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ICX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOST/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOTA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOTX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KAVA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KEEP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KNC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KSM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LENDUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LINA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LINK/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "LIT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LRC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LTC/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "LUNA/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"], + [15000000.0, "0.5"] + ], + "MANA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MASK/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MATIC/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "MKR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MTL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NEAR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NEO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NKN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OCEAN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OGN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OMG/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ONE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ONT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "QTUM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RAY/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "REEF/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "REN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RLC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RSR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RUNE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RVN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SAND/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SFP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SKL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SNX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SOL/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "SOL/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.25"], + [10000000.0, "0.5"] + ], + "SRM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "STMX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "STORJ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SUSHI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SXP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "THETA/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "TLM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TOMO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TRB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TRX/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "UNFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "UNI/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "VET/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "WAVES/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "XEM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "XLM/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XMR/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XRP/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "XRP/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XTZ/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "YFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "YFII/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZEC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZEN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZIL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZRX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ] +} diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a248a780e..b1ba1b5b8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -7,7 +7,7 @@ import http import inspect import logging from copy import deepcopy -from datetime import datetime, timedelta, timezone +from datetime import datetime, timezone from math import ceil from typing import Any, Dict, List, Optional, Tuple, Union @@ -22,6 +22,7 @@ from pandas import DataFrame from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, ListPairsWithTimeframes) from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) @@ -48,9 +49,6 @@ class Exchange: _config: Dict = {} - # Parameters to add directly to ccxt sync/async initialization. - _ccxt_config: Dict = {} - # Parameters to add directly to buy/sell calls (like agreeing to trading agreement) _params: Dict = {} @@ -75,6 +73,10 @@ class Exchange: _ft_has: Dict = {} funding_fee_times: List[int] = [] # hours of the day + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # TradingMode.SPOT always supported and not required in this list + ] + def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ Initializes this module with the given config, @@ -84,6 +86,7 @@ class Exchange: self._api: ccxt.Exchange = None self._api_async: ccxt_async.Exchange = None self._markets: Dict = {} + self._leverage_brackets: Dict = {} self._config.update(config) @@ -126,14 +129,25 @@ class Exchange: self._trades_pagination = self._ft_has['trades_pagination'] self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] + self.trading_mode: TradingMode = ( + TradingMode(config.get('trading_mode')) + if config.get('trading_mode') + else TradingMode.SPOT + ) + self.collateral: Optional[Collateral] = ( + Collateral(config.get('collateral')) + if config.get('collateral') + else None + ) + # Initialize ccxt objects - ccxt_config = self._ccxt_config.copy() + ccxt_config = self._ccxt_config ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) - ccxt_async_config = self._ccxt_config.copy() + ccxt_async_config = self._ccxt_config ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_async_config) ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), @@ -141,6 +155,9 @@ class Exchange: self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) + if self.trading_mode != TradingMode.SPOT: + self.fill_leverage_brackets() + logger.info('Using Exchange "%s"', self.name) if validate: @@ -158,7 +175,7 @@ class Exchange: self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_required_startup_candles(config.get('startup_candle_count', 0), config.get('timeframe', '')) - + self.validate_trading_mode_and_collateral(self.trading_mode, self.collateral) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( "markets_refresh_interval", 60) * 60 @@ -211,6 +228,11 @@ class Exchange: return api + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {} + @property def name(self) -> str: """exchange Name (from ccxt)""" @@ -356,6 +378,7 @@ class Exchange: # Also reload async markets to avoid issues with newly listed pairs self._load_async_markets(reload=True) self._last_markets_refresh = arrow.utcnow().int_timestamp + self.fill_leverage_brackets() except ccxt.BaseError: logger.exception("Could not reload markets.") @@ -483,6 +506,25 @@ class Exchange: f"This strategy requires {startup_candles} candles to start. " f"{self.name} only provides {candle_limit} for {timeframe}.") + def validate_trading_mode_and_collateral( + self, + trading_mode: TradingMode, + collateral: Optional[Collateral] # Only None when trading_mode = TradingMode.SPOT + ): + """ + Checks if freqtrade can perform trades using the configured + trading mode(Margin, Futures) and Collateral(Cross, Isolated) + Throws OperationalException: + If the trading_mode/collateral type are not supported by freqtrade on this exchange + """ + if trading_mode != TradingMode.SPOT and ( + (trading_mode, collateral) not in self._supported_trading_mode_collateral_pairs + ): + collateral_value = collateral and collateral.value + raise OperationalException( + f"Freqtrade does not support {collateral_value} {trading_mode.value} on {self.name}" + ) + def exchange_has(self, endpoint: str) -> bool: """ Checks if exchange implements a specific API endpoint. @@ -542,8 +584,8 @@ class Exchange: else: return 1 / pow(10, precision) - def get_min_pair_stake_amount(self, pair: str, price: float, - stoploss: float) -> Optional[float]: + def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float, + leverage: Optional[float] = 1.0) -> Optional[float]: try: market = self.markets[pair] except KeyError: @@ -577,12 +619,24 @@ class Exchange: # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return max(min_stake_amounts) * amount_reserve_percent + return self._get_stake_amount_considering_leverage( + max(min_stake_amounts) * amount_reserve_percent, + leverage or 1.0 + ) + + def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float): + """ + Takes the minimum stake amount for a pair with no leverage and returns the minimum + stake amount when leverage is considered + :param stake_amount: The stake amount for a pair before leverage is considered + :param leverage: The amount of leverage being used on the current trade + """ + return stake_amount / leverage # Dry-run methods def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, params: Dict = {}) -> Dict[str, Any]: + rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) dry_order: Dict[str, Any] = { @@ -599,7 +653,8 @@ class Exchange: 'timestamp': arrow.utcnow().int_timestamp * 1000, 'status': "closed" if ordertype == "market" else "open", 'fee': None, - 'info': {} + 'info': {}, + 'leverage': leverage } if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: dry_order["info"] = {"stopPrice": dry_order["price"]} @@ -609,7 +664,7 @@ class Exchange: average = self.get_dry_market_fill_price(pair, side, amount, rate) dry_order.update({ 'average': average, - 'cost': dry_order['amount'] * average, + 'cost': (dry_order['amount'] * average) / leverage }) dry_order = self.add_dry_order_fee(pair, dry_order) @@ -717,17 +772,26 @@ class Exchange: # Order handling - def create_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, time_in_force: str = 'gtc') -> Dict: - - if self._config['dry_run']: - dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate) - return dry_order + def _lev_prep(self, pair: str, leverage: float): + if self.trading_mode != TradingMode.SPOT: + self.set_margin_mode(pair, self.collateral) + self._set_leverage(leverage, pair) + def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: params = self._params.copy() if time_in_force != 'gtc' and ordertype != 'market': param = self._ft_has.get('time_in_force_parameter', '') params.update({param: time_in_force}) + return params + + def create_order(self, pair: str, ordertype: str, side: str, amount: float, + rate: float, leverage: float = 1.0, time_in_force: str = 'gtc') -> Dict: + # TODO-lev: remove default for leverage + if self._config['dry_run']: + dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage) + return dry_order + + params = self._get_params(ordertype, leverage, time_in_force) try: # Set the precision for amount and price(rate) as accepted by the exchange @@ -736,6 +800,7 @@ class Exchange: or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) rate_for_order = self.price_to_precision(pair, rate) if needs_price else None + self._lev_prep(pair, leverage) order = self._api.create_order(pair, ordertype, side, amount, rate_for_order, params) self._log_exchange_response('create_order', order) @@ -759,14 +824,15 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ raise OperationalException(f"stoploss is not implemented for {self.name}.") - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ creates a stoploss order. The precise ordertype is determined by the order_types dict or exchange default. @@ -1559,21 +1625,66 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def _get_funding_fee_dates(self, open_date: datetime, close_date: datetime): + def fill_leverage_brackets(self): """ - Get's the date and time of every funding fee that happened between two datetimes + # TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken + Assigns property _leverage_brackets to a dictionary of information about the leverage + allowed on each pair """ - open_date = datetime(open_date.year, open_date.month, open_date.day, open_date.hour) - close_date = datetime(close_date.year, close_date.month, close_date.day, close_date.hour) + return - results = [] - date_iterator = open_date - while date_iterator < close_date: - date_iterator += timedelta(hours=1) - if date_iterator.hour in self.funding_fee_times: - results.append(date_iterator) + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at + :param pair: The base/quote currency pair being traded + :nominal_value: The total value of the trade in quote currency (collateral + debt) + """ + return 1.0 - return results + @retrier + def _set_leverage( + self, + leverage: float, + pair: Optional[str] = None, + trading_mode: Optional[TradingMode] = None + ): + """ + Set's the leverage before making a trade, in order to not + have the same leverage on every trade + """ + if self._config['dry_run'] or not self.exchange_has("setLeverage"): + # Some exchanges only support one collateral type + return + + try: + self._api.set_leverage(symbol=pair, leverage=leverage) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + @retrier + def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}): + ''' + Set's the margin mode on the exchange to cross or isolated for a specific pair + :param symbol: base/quote currency pair (e.g. "ADA/USDT") + ''' + if self._config['dry_run'] or not self.exchange_has("setMarginMode"): + # Some exchanges only support one collateral type + return + + try: + self._api.set_margin_mode(pair, collateral.value, params) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 8abf84104..ef583de4f 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,9 +1,10 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional, Tuple import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -22,6 +23,12 @@ class Ftx(Exchange): } funding_fee_times: List[int] = list(range(0, 23)) + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # TradingMode.SPOT always supported and not required in this list + # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported + # (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: Uncomment once supported + ] + def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ Check if the market symbol is tradable by Freqtrade. @@ -32,15 +39,19 @@ class Ftx(Exchange): return (parent_check and market.get('spot', False) is True) - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - return order['type'] == 'stop' and stop_loss > float(order['price']) + return order['type'] == 'stop' and ( + side == "sell" and stop_loss > float(order['price']) or + side == "buy" and stop_loss < float(order['price']) + ) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ Creates a stoploss order. depending on order_types.stoploss configuration, uses 'market' or limit order. @@ -48,7 +59,10 @@ class Ftx(Exchange): Limit orders are defined by having orderPrice set, otherwise a market order is used. """ limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) - limit_rate = stop_price * limit_price_pct + if side == "sell": + limit_rate = stop_price * limit_price_pct + else: + limit_rate = stop_price * (2 - limit_price_pct) ordertype = "stop" @@ -56,7 +70,7 @@ class Ftx(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: @@ -68,7 +82,8 @@ class Ftx(Exchange): params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + self._lev_prep(pair, leverage) + order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, params=params) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' @@ -76,19 +91,19 @@ class Ftx(Exchange): return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( - f'Insufficient funds to create {ordertype} sell order on market {pair}. ' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Could not create {ordertype} sell order on market {pair}. ' + f'Could not create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'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 place sell order due to {e.__class__.__name__}. Message: {e}') from e + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e @@ -153,3 +168,18 @@ class Ftx(Exchange): if order['type'] == 'stop': return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] + + def fill_leverage_brackets(self): + """ + FTX leverage is static across the account, and doesn't change from pair to pair, + so _leverage_brackets doesn't need to be set + """ + return + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at, which is always 20 on ftx + :param pair: Here for super method, not used on FTX + :nominal_value: Here for super method, not used on FTX + """ + return 20.0 diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index a83b9f9cb..710260c76 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -1,9 +1,10 @@ """ Kraken exchange subclass """ import logging -from typing import Any, Dict, List +from typing import Any, Dict, List, Optional, Tuple import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -24,6 +25,12 @@ class Kraken(Exchange): } funding_fee_times: List[int] = [0, 4, 8, 12, 16, 20] # hours of the day + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # TradingMode.SPOT always supported and not required in this list + # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported + # (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: No CCXT support + ] + def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ Check if the market symbol is tradable by Freqtrade. @@ -68,16 +75,19 @@ class Kraken(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - return (order['type'] in ('stop-loss', 'stop-loss-limit') - and stop_loss > float(order['price'])) + return (order['type'] in ('stop-loss', 'stop-loss-limit') and ( + (side == "sell" and stop_loss > float(order['price'])) or + (side == "buy" and stop_loss < float(order['price'])) + )) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. @@ -87,7 +97,10 @@ class Kraken(Exchange): if order_types.get('stoploss', 'market') == 'limit': ordertype = "stop-loss-limit" limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) - limit_rate = stop_price * limit_price_pct + if side == "sell": + limit_rate = stop_price * limit_price_pct + else: + limit_rate = stop_price * (2 - limit_price_pct) params['price2'] = self.price_to_precision(pair, limit_rate) else: ordertype = "stop-loss" @@ -96,13 +109,13 @@ class Kraken(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: amount = self.amount_to_precision(pair, amount) - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, price=stop_price, params=params) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' @@ -110,18 +123,70 @@ class Kraken(Exchange): return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( - f'Insufficient funds to create {ordertype} sell order on market {pair}. ' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Could not create {ordertype} sell order on market {pair}. ' + f'Could not create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'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 place sell order due to {e.__class__.__name__}. Message: {e}') from e + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + def fill_leverage_brackets(self): + """ + Assigns property _leverage_brackets to a dictionary of information about the leverage + allowed on each pair + """ + leverages = {} + + for pair, market in self.markets.items(): + leverages[pair] = [1] + info = market['info'] + leverage_buy = info.get('leverage_buy', []) + leverage_sell = info.get('leverage_sell', []) + if len(leverage_buy) > 0 or len(leverage_sell) > 0: + if leverage_buy != leverage_sell: + logger.warning( + f"The buy({leverage_buy}) and sell({leverage_sell}) leverage are not equal" + "for {pair}. Please notify freqtrade because this has never happened before" + ) + if max(leverage_buy) <= max(leverage_sell): + leverages[pair] += [int(lev) for lev in leverage_buy] + else: + leverages[pair] += [int(lev) for lev in leverage_sell] + else: + leverages[pair] += [int(lev) for lev in leverage_buy] + self._leverage_brackets = leverages + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at + :param pair: The base/quote currency pair being traded + :nominal_value: Here for super class, not needed on Kraken + """ + return float(max(self._leverage_brackets[pair])) + + def _set_leverage( + self, + leverage: float, + pair: Optional[str] = None, + trading_mode: Optional[TradingMode] = None + ): + """ + Kraken set's the leverage as an option in the order object, so we need to + add it to params + """ + return + + def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: + params = super()._get_params(ordertype, leverage, time_in_force) + if leverage > 1.0: + params['leverage'] = leverage + return params diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 601c18001..02e0d2fbc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -86,10 +86,10 @@ class FreqtradeBot(LoggingMixin): self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) - # Attach Dataprovider to Strategy baseclass - IStrategy.dp = self.dataprovider - # Attach Wallets to Strategy baseclass - IStrategy.wallets = self.wallets + # Attach Dataprovider to strategy instance + self.strategy.dp = self.dataprovider + # Attach Wallets to strategy instance + self.strategy.wallets = self.wallets # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ @@ -173,7 +173,7 @@ class FreqtradeBot(LoggingMixin): # Refreshing candles self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), - self.strategy.informative_pairs()) + self.strategy.gather_informative_pairs()) strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() @@ -763,9 +763,14 @@ class FreqtradeBot(LoggingMixin): :return: True if the order succeeded, and False in case of problems. """ try: - stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount, - stop_price=stop_price, - order_types=self.strategy.order_types) + stoploss_order = self.exchange.stoploss( + pair=trade.pair, + amount=trade.amount, + stop_price=stop_price, + order_types=self.strategy.order_types, + side=trade.exit_side, + leverage=trade.leverage + ) order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') trade.orders.append(order_obj) @@ -857,11 +862,11 @@ class FreqtradeBot(LoggingMixin): # 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.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side) return False - def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None: + def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None: """ Check to see if stoploss on exchange should be updated in case of trailing stoploss on exchange @@ -869,7 +874,7 @@ class FreqtradeBot(LoggingMixin): :param order: Current on exchange stoploss order :return: None """ - if self.exchange.stoploss_adjust(trade.stop_loss, order): + if self.exchange.stoploss_adjust(trade.stop_loss, order, side): # we check if the update is necessary update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: diff --git a/freqtrade/leverage/interest.py b/freqtrade/leverage/interest.py index aacbb3532..2878ad784 100644 --- a/freqtrade/leverage/interest.py +++ b/freqtrade/leverage/interest.py @@ -20,7 +20,7 @@ def interest( :param exchange_name: The exchanged being trading on :param borrowed: The amount of currency being borrowed - :param rate: The rate of interest + :param rate: The rate of interest (i.e daily interest rate) :param hours: The time in hours that the currency has been borrowed for Raises: @@ -36,7 +36,8 @@ def interest( # Rounded based on https://kraken-fees-calculator.github.io/ return borrowed * rate * (one+ceil(hours/four)) elif exchange_name == "ftx": - # TODO-lev: Add FTX interest formula - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + # As Explained under #Interest rates section in + # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer + return borrowed * rate * ceil(hours)/twenty_four else: raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9bbb15fb2..d4964746a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -154,7 +154,7 @@ class Backtesting: self.strategy: IStrategy = strategy strategy.dp = self.dataprovider # Attach Wallets to Strategy baseclass - IStrategy.wallets = self.wallets + strategy.wallets = self.wallets # Set stoploss_on_exchange to false for backtesting, # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 417faa685..f211da750 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -8,6 +8,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import TimeRange, validate_config_consistency +from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.optimize.optimize_reports import generate_edge_table from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -33,6 +34,7 @@ class EdgeCli: self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.strategy = StrategyResolver.load_strategy(self.config) + self.strategy.dp = DataProvider(config, None) validate_config_consistency(self.config) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 14b155546..9549b4054 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -45,7 +45,7 @@ progressbar.streams.wrap_stdout() logger = logging.getLogger(__name__) -INITIAL_POINTS = 30 +INITIAL_POINTS = 5 # Keep no more than SKOPT_MODEL_QUEUE_SIZE models # in the skopt model queue, to optimize memory consumption @@ -241,7 +241,7 @@ class Hyperopt: if HyperoptTools.has_space(self.config, 'buy'): logger.debug("Hyperopt has 'buy' space") - self.buy_space = self.custom_hyperopt.indicator_space() + self.buy_space = self.custom_hyperopt.buy_indicator_space() if HyperoptTools.has_space(self.config, 'sell'): logger.debug("Hyperopt has 'sell' space") @@ -365,10 +365,20 @@ class Hyperopt: } def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: + estimator = self.custom_hyperopt.generate_estimator() + + acq_optimizer = "sampling" + if isinstance(estimator, str): + if estimator not in ("GP", "RF", "ET", "GBRT"): + raise OperationalException(f"Estimator {estimator} not supported.") + else: + acq_optimizer = "auto" + + logger.info(f"Using estimator {estimator}.") return Optimizer( dimensions, - base_estimator="ET", - acq_optimizer="auto", + base_estimator=estimator, + acq_optimizer=acq_optimizer, n_initial_points=INITIAL_POINTS, acq_optimizer_kwargs={'n_jobs': cpu_count}, random_state=self.random_state, diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 1f11cec80..c1c769c72 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -12,7 +12,7 @@ from freqtrade.exceptions import OperationalException with suppress(ImportError): from skopt.space import Dimension -from freqtrade.optimize.hyperopt_interface import IHyperOpt +from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt def _format_exception_message(space: str) -> str: @@ -56,7 +56,7 @@ class HyperOptAuto(IHyperOpt): else: _format_exception_message(category) - def indicator_space(self) -> List['Dimension']: + def buy_indicator_space(self) -> List['Dimension']: return self._get_indicator_space('buy') def sell_indicator_space(self) -> List['Dimension']: @@ -79,3 +79,6 @@ class HyperOptAuto(IHyperOpt): def trailing_space(self) -> List['Dimension']: return self._get_func('trailing_space')() + + def generate_estimator(self) -> EstimatorType: + return self._get_func('generate_estimator')() diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 8fb40f557..53b4f087c 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -5,8 +5,9 @@ This module defines the interface to apply for hyperopt import logging import math from abc import ABC -from typing import Dict, List +from typing import Dict, List, Union +from sklearn.base import RegressorMixin from skopt.space import Categorical, Dimension, Integer from freqtrade.exchange import timeframe_to_minutes @@ -17,6 +18,8 @@ from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) +EstimatorType = Union[RegressorMixin, str] + class IHyperOpt(ABC): """ @@ -37,6 +40,14 @@ class IHyperOpt(ABC): IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED IHyperOpt.timeframe = str(config['timeframe']) + def generate_estimator(self) -> EstimatorType: + """ + Return base_estimator. + Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class + inheriting from RegressorMixin (from sklearn). + """ + return 'ET' + def generate_roi_table(self, params: Dict) -> Dict[int, float]: """ Create a ROI table. diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index b2e024f65..cfbc2757e 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Tuple import numpy as np +import pandas as pd import rapidjson import tabulate from colorama import Fore, Style @@ -298,8 +299,8 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str: - + def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, + has_drawdown: bool) -> pd.DataFrame: trials['Best'] = '' if 'results_metrics.winsdrawslosses' not in trials.columns: @@ -435,8 +436,7 @@ class HyperoptTools(): return table @staticmethod - def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, - csv_file: str) -> None: + def export_csv_file(config: dict, results: list, csv_file: str) -> None: """ Log result to csv-file """ diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e785ba49b..50f4931d6 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from decimal import Decimal from typing import Any, Dict, List, Optional @@ -1057,17 +1057,21 @@ class Trade(_DECL_BASE, LocalTrade): return total_open_stake_amount or 0 @staticmethod - def get_overall_performance() -> List[Dict[str, Any]]: + def get_overall_performance(minutes=None) -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, including profit and trade count NOTE: Not supported in Backtesting. """ + filters = [Trade.is_open.is_(False)] + if minutes: + start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes) + filters.append(Trade.close_date >= start_date) pair_rates = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ + ).filter(*filters)\ .group_by(Trade.pair) \ .order_by(desc('profit_sum_abs')) \ .all() diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index dc5cab31e..5627d82ce 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional import arrow from pandas import DataFrame +from freqtrade.configuration import PeriodicCache from freqtrade.exceptions import OperationalException from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList @@ -18,14 +19,15 @@ logger = logging.getLogger(__name__) class AgeFilter(IPairList): - # Checked symbols cache (dictionary of ticker symbol => timestamp) - _symbolsChecked: Dict[str, int] = {} - def __init__(self, exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: Dict[str, Any], pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + # Checked symbols cache (dictionary of ticker symbol => timestamp) + self._symbolsChecked: Dict[str, int] = {} + self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400) + self._min_days_listed = pairlistconfig.get('min_days_listed', 10) self._max_days_listed = pairlistconfig.get('max_days_listed', None) @@ -69,9 +71,12 @@ class AgeFilter(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new allowlist """ - needed_pairs = [(p, '1d') for p in pairlist if p not in self._symbolsChecked] + needed_pairs = [ + (p, '1d') for p in pairlist + if p not in self._symbolsChecked and p not in self._symbolsCheckFailed] if not needed_pairs: - return pairlist + # Remove pairs that have been removed before + return [p for p in pairlist if p not in self._symbolsCheckFailed] since_days = -( self._max_days_listed if self._max_days_listed else self._min_days_listed @@ -118,5 +123,6 @@ class AgeFilter(IPairList): " or more than " f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}" ) if self._max_days_listed else ''), logger.info) + self._symbolsCheckFailed[pair] = arrow.utcnow().int_timestamp * 1000 return False return False diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 46a289ae6..301ee57ab 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -2,7 +2,7 @@ Performance pair list filter """ import logging -from typing import Dict, List +from typing import Any, Dict, List import pandas as pd @@ -15,6 +15,13 @@ logger = logging.getLogger(__name__) class PerformanceFilter(IPairList): + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._minutes = pairlistconfig.get('minutes', 0) + @property def needstickers(self) -> bool: """ @@ -40,7 +47,7 @@ class PerformanceFilter(IPairList): """ # Get the trading performance for pairs from database try: - performance = pd.DataFrame(Trade.get_overall_performance()) + performance = pd.DataFrame(Trade.get_overall_performance(self._minutes)) except AttributeError: # Performancefilter does not work in backtesting. self.log_once("PerformanceFilter is not available in this mode.", logger.warning) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 3adbebc16..46187f571 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -46,6 +46,12 @@ class Balances(BaseModel): value: float stake: str note: str + starting_capital: float + starting_capital_ratio: float + starting_capital_pct: float + starting_capital_fiat: float + starting_capital_fiat_ratio: float + starting_capital_fiat_pct: float class Count(BaseModel): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 7facacf97..b50f90de8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -459,6 +459,9 @@ class RPC: raise RPCException('Error getting current tickers.') self._freqtrade.wallets.update(require_update=False) + starting_capital = self._freqtrade.wallets.get_starting_balance() + starting_cap_fiat = self._fiat_converter.convert_amount( + starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0 for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: @@ -494,15 +497,25 @@ class RPC: else: raise RPCException('All balances are zero.') - symbol = fiat_display_currency - value = self._fiat_converter.convert_amount(total, stake_currency, - symbol) if self._fiat_converter else 0 + value = self._fiat_converter.convert_amount( + total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 + + starting_capital_ratio = 0.0 + starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0 + starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 + return { 'currencies': output, 'total': total, - 'symbol': symbol, + 'symbol': fiat_display_currency, 'value': value, 'stake': stake_currency, + 'starting_capital': starting_capital, + 'starting_capital_ratio': starting_capital_ratio, + 'starting_capital_pct': round(starting_capital_ratio * 100, 2), + 'starting_capital_fiat': starting_cap_fiat, + 'starting_capital_fiat_ratio': starting_cap_fiat_ratio, + 'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2), 'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else '' } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a988d2b60..19c58b63d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -603,12 +603,15 @@ class Telegram(RPCHandler): output = '' if self._config['dry_run']: - output += ( - f"*Warning:* Simulated balances in Dry Mode.\n" - "This mode is still experimental!\n" - "Starting capital: " - f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" - ) + output += "*Warning:* Simulated balances in Dry Mode.\n" + + output += ("Starting capital: " + f"`{result['starting_capital']}` {self._config['stake_currency']}" + ) + output += (f" `{result['starting_capital_fiat']}` " + f"{self._config['fiat_display_currency']}.\n" + ) if result['starting_capital_fiat'] > 0 else '.\n' + total_dust_balance = 0 total_dust_currencies = 0 for curr in result['currencies']: @@ -641,9 +644,12 @@ class Telegram(RPCHandler): f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n") output += ("\n*Estimated Value*:\n" - f"\t`{result['stake']}: {result['total']: .8f}`\n" + f"\t`{result['stake']}: " + f"{round_coin_value(result['total'], result['stake'], False)}`" + f" `({result['starting_capital_pct']}%)`\n" f"\t`{result['symbol']}: " - f"{round_coin_value(result['value'], result['symbol'], False)}`\n") + f"{round_coin_value(result['value'], result['symbol'], False)}`" + f" `({result['starting_capital_fiat_pct']}%)`\n") self._send_msg(output, reload_able=True, callback_path="update_balance", query=update.callback_query) except RPCException as e: diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index be655fc33..2ea0ad2b4 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,5 +3,7 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) +from freqtrade.strategy.informative_decorator import informative from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open +from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute, + stoploss_from_open) diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py new file mode 100644 index 000000000..4c5f21108 --- /dev/null +++ b/freqtrade/strategy/informative_decorator.py @@ -0,0 +1,128 @@ +from typing import Any, Callable, NamedTuple, Optional, Union + +from pandas import DataFrame + +from freqtrade.exceptions import OperationalException +from freqtrade.strategy.strategy_helper import merge_informative_pair + + +PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] + + +class InformativeData(NamedTuple): + asset: Optional[str] + timeframe: str + fmt: Union[str, Callable[[Any], str], None] + ffill: bool + + +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[Any], str]]] = None, + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to: + * {base}_{quote}_{column}_{timeframe} if asset is specified. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. + """ + _asset = asset + _timeframe = timeframe + _fmt = fmt + _ffill = ffill + + def decorator(fn: PopulateIndicators): + informative_pairs = getattr(fn, '_ft_informative', []) + informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill)) + setattr(fn, '_ft_informative', informative_pairs) + return fn + return decorator + + +def _format_pair_name(config, pair: str) -> str: + return pair.format(stake_currency=config['stake_currency'], + stake=config['stake_currency']).upper() + + +def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict, + inf_data: InformativeData, + populate_indicators: PopulateIndicators): + asset = inf_data.asset or '' + timeframe = inf_data.timeframe + fmt = inf_data.fmt + config = strategy.config + + if asset: + # Insert stake currency if needed. + asset = _format_pair_name(config, asset) + else: + # Not specifying an asset will define informative dataframe for current pair. + asset = metadata['pair'] + + if '/' in asset: + base, quote = asset.split('/') + else: + # When futures are supported this may need reevaluation. + # base, quote = asset, '' + raise OperationalException('Not implemented.') + + # Default format. This optimizes for the common case: informative pairs using same stake + # currency. When quote currency matches stake currency, column name will omit base currency. + # This allows easily reconfiguring strategy to use different base currency. In a rare case + # where it is desired to keep quote currency in column name at all times user should specify + # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. + if not fmt: + fmt = '{column}_{timeframe}' # Informatives of current pair + if inf_data.asset: + fmt = '{base}_{quote}_' + fmt # Informatives of other pairs + + inf_metadata = {'pair': asset, 'timeframe': timeframe} + inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) + inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) + + formatter: Any = None + if callable(fmt): + formatter = fmt # A custom user-specified formatter function. + else: + formatter = fmt.format # A default string formatter. + + fmt_args = { + 'BASE': base.upper(), + 'QUOTE': quote.upper(), + 'base': base.lower(), + 'quote': quote.lower(), + 'asset': asset, + 'timeframe': timeframe, + } + inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args), + inplace=True) + + date_column = formatter(column='date', **fmt_args) + if date_column in dataframe.columns: + raise OperationalException(f'Duplicate column name {date_column} exists in ' + f'dataframe! Ensure column names are unique!') + dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe, + ffill=inf_data.ffill, append_timeframe=False, + date_column=date_column) + return dataframe diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4730e9fe1..bdfe16d28 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -19,6 +19,9 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.hyper import HyperStrategyMixin +from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators, + _create_and_merge_informative_pair, + _format_pair_name) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -118,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) # and wallets - access to the current balance. - dp: Optional[DataProvider] = None + dp: Optional[DataProvider] wallets: Optional[Wallets] = None # Filled from configuration stake_currency: str @@ -134,6 +137,24 @@ class IStrategy(ABC, HyperStrategyMixin): self._last_candle_seen_per_pair: Dict[str, datetime] = {} super().__init__(config) + # Gather informative pairs from @informative-decorated methods. + self._ft_informative: List[Tuple[InformativeData, PopulateIndicators]] = [] + for attr_name in dir(self.__class__): + cls_method = getattr(self.__class__, attr_name) + if not callable(cls_method): + continue + informative_data_list = getattr(cls_method, '_ft_informative', None) + if not isinstance(informative_data_list, list): + # Type check is required because mocker would return a mock object that evaluates to + # True, confusing this code. + continue + strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe) + for informative_data in informative_data_list: + if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes: + raise OperationalException('Informative timeframe must be equal or higher than ' + 'strategy timeframe!') + self._ft_informative.append((informative_data, cls_method)) + @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -377,6 +398,23 @@ class IStrategy(ABC, HyperStrategyMixin): # END - Intended to be overridden by strategy ### + def gather_informative_pairs(self) -> ListPairsWithTimeframes: + """ + Internal method which gathers all informative pairs (user or automatically defined). + """ + informative_pairs = self.informative_pairs() + for inf_data, _ in self._ft_informative: + if inf_data.asset: + pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe) + informative_pairs.append(pair_tf) + else: + if not self.dp: + raise OperationalException('@informative decorator with unspecified asset ' + 'requires DataProvider instance.') + for pair in self.dp.current_whitelist(): + informative_pairs.append((pair, inf_data.timeframe)) + return list(set(informative_pairs)) + def get_strategy_name(self) -> str: """ Returns strategy class name @@ -786,10 +824,11 @@ class IStrategy(ABC, HyperStrategyMixin): Does not run advise_buy or advise_sell! Used by optimize operations only, not during dry / live runs. Using .copy() to get a fresh copy of the dataframe for every strategy run. + Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show. Has positive effects on memory usage for whatever reason - also when using only one strategy. """ - return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair}) + return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair}).copy() for pair, pair_data in data.items()} def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -801,6 +840,12 @@ class IStrategy(ABC, HyperStrategyMixin): :return: a Dataframe with all mandatory indicators for the strategies """ logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") + + # call populate_indicators_Nm() which were tagged with @informative decorator. + for inf_data, populate_fn in self._ft_informative: + dataframe = _create_and_merge_informative_pair( + self, dataframe, metadata, inf_data, populate_fn) + if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 121614fbc..de88de33b 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -4,7 +4,9 @@ from freqtrade.exchange import timeframe_to_minutes def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, - timeframe: str, timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: + timeframe: str, timeframe_inf: str, ffill: bool = True, + append_timeframe: bool = True, + date_column: str = 'date') -> pd.DataFrame: """ Correctly merge informative samples to the original dataframe, avoiding lookahead bias. @@ -24,6 +26,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :param timeframe: Timeframe of the original pair sample. :param timeframe_inf: Timeframe of the informative pair sample. :param ffill: Forwardfill missing values - optional but usually required + :param append_timeframe: Rename columns by appending timeframe. + :param date_column: A custom date column name. :return: Merged dataframe :raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe """ @@ -32,25 +36,29 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, minutes = timeframe_to_minutes(timeframe) if minutes == minutes_inf: # No need to forwardshift if the timeframes are identical - informative['date_merge'] = informative["date"] + informative['date_merge'] = informative[date_column] elif minutes < minutes_inf: # Subtract "small" timeframe so merging is not delayed by 1 small candle # Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073 informative['date_merge'] = ( - informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm') + informative[date_column] + pd.to_timedelta(minutes_inf, 'm') - + pd.to_timedelta(minutes, 'm') ) else: raise ValueError("Tried to merge a faster timeframe to a slower timeframe." "This would create new rows, and can throw off your regular indicators.") # Rename columns to be unique - informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] + date_merge = 'date_merge' + if append_timeframe: + date_merge = f'date_merge_{timeframe_inf}' + informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point dataframe = pd.merge(dataframe, informative, left_on='date', - right_on=f'date_merge_{timeframe_inf}', how='left') - dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1) + right_on=date_merge, how='left') + dataframe = dataframe.drop(date_merge, axis=1) if ffill: dataframe = dataframe.ffill() @@ -83,3 +91,28 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa # negative stoploss values indicate the requested stop price is higher than the current price return max(stoploss, 0.0) + + +def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: + """ + Given current price and desired stop price, return a stop loss value that is relative to current + price. + + The requested stop can be positive for a stop above the open price, or negative for + a stop below the open price. The return value is always >= 0. + + Returns 0 if the resulting stop price would be above the current price. + + :param stop_rate: Stop loss price. + :param current_rate: Current asset price. + :return: Positive stop loss value relative to current price + """ + + # formula is undefined for current_rate 0, return maximum value + if current_rate == 0: + return 1 + + stoploss = 1 - (stop_rate / current_rate) + + # negative stoploss values indicate the requested stop price is higher than the current price + return max(stoploss, 0.0) diff --git a/requirements-dev.txt b/requirements-dev.txt index 34d5607f3..4859e1cc6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,6 +14,8 @@ pytest-cov==2.12.1 pytest-mock==3.6.1 pytest-random-order==1.0.4 isort==5.9.3 +# For datetime mocking +time-machine==2.4.0 # Convert jupyter notebooks to markdown documents nbconvert==6.1.0 diff --git a/setup.sh b/setup.sh index 217500569..aee7c80b5 100755 --- a/setup.sh +++ b/setup.sh @@ -62,7 +62,7 @@ function updateenv() { then REQUIREMENTS_PLOT="-r requirements-plot.txt" fi - if [ "${SYS_ARCH}" == "armv7l" ]; then + if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then echo "Detected Raspberry, installing cython, skipping hyperopt installation." ${PYTHON} -m pip install --upgrade cython else diff --git a/tests/conftest.py b/tests/conftest.py index 609823409..d2f24fa69 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,7 @@ from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo -from freqtrade.enums import RunMode +from freqtrade.enums import Collateral, RunMode, TradingMode from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db @@ -81,7 +81,13 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None: +def patch_exchange( + mocker, + api_mock=None, + id='binance', + mock_markets=True, + mock_supported_modes=True +) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -90,10 +96,22 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) + if mock_markets: mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=get_markets())) + if mock_supported_modes: + mocker.patch( + f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_collateral_pairs', + PropertyMock(return_value=[ + (TradingMode.MARGIN, Collateral.CROSS), + (TradingMode.MARGIN, Collateral.ISOLATED), + (TradingMode.FUTURES, Collateral.CROSS), + (TradingMode.FUTURES, Collateral.ISOLATED) + ]) + ) + if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: @@ -101,8 +119,8 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No def get_patched_exchange(mocker, config, api_mock=None, id='binance', - mock_markets=True) -> Exchange: - patch_exchange(mocker, api_mock, id, mock_markets) + mock_markets=True, mock_supported_modes=True) -> Exchange: + patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes) config['exchange']['name'] = id try: exchange = ExchangeResolver.load_exchange(id, config) @@ -442,7 +460,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': ['2'], + 'leverage_sell': ['2'], + }, }, 'TKN/BTC': { 'id': 'tknbtc', @@ -468,7 +489,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': ['2', '3', '4', '5'], + 'leverage_sell': ['2', '3', '4', '5'], + }, }, 'BLK/BTC': { 'id': 'blkbtc', @@ -493,7 +517,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': ['2', '3'], + 'leverage_sell': ['2', '3'], + }, }, 'LTC/BTC': { 'id': 'ltcbtc', @@ -518,7 +545,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': [], + 'leverage_sell': [], + }, }, 'XRP/BTC': { 'id': 'xrpbtc', @@ -596,7 +626,10 @@ def get_markets(): 'max': None } }, - 'info': {}, + 'info': { + 'leverage_buy': [], + 'leverage_sell': [], + }, }, 'ETH/USDT': { 'id': 'USDT-ETH', @@ -712,6 +745,8 @@ def get_markets(): 'max': None } }, + 'info': { + } }, } diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index dd85c3abe..0c3e86fdd 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -1,21 +1,31 @@ from datetime import datetime, timezone from random import randint -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import ccxt import pytest +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers -@pytest.mark.parametrize('limitratio,expected', [ - (None, 220 * 0.99), - (0.99, 220 * 0.99), - (0.98, 220 * 0.98), +@pytest.mark.parametrize('limitratio,expected,side', [ + (None, 220 * 0.99, "sell"), + (0.99, 220 * 0.99, "sell"), + (0.98, 220 * 0.98, "sell"), + (None, 220 * 1.01, "buy"), + (0.99, 220 * 1.01, "buy"), + (0.98, 220 * 1.02, "buy"), ]) -def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): +def test_stoploss_order_binance( + default_conf, + mocker, + limitratio, + expected, + side +): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'stop_loss_limit' @@ -33,19 +43,32 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') with pytest.raises(OperationalException): - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side=side, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + leverage=1.0 + ) api_mock.create_order.reset_mock() order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio} - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types=order_types, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == order_type - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 # Price should be 1% below stopprice assert api_mock.create_order.call_args_list[0][1]['price'] == expected @@ -55,17 +78,31 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) def test_stoploss_order_dry_run_binance(default_conf, mocker): @@ -78,12 +115,25 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') with pytest.raises(OperationalException): - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side="sell", + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + leverage=1.0 + ) api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side="sell", + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -94,18 +144,202 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): assert order['amount'] == 1 -def test_stoploss_adjust_binance(mocker, default_conf): +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): exchange = get_patched_exchange(mocker, default_conf, id='binance') order = { 'type': 'stop_loss_limit', 'price': 1500, 'info': {'stopPrice': 1500}, } - assert exchange.stoploss_adjust(1501, order) - assert not exchange.stoploss_adjust(1499, order) + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) # Test with invalid order case order['type'] = 'stop_loss' - assert not exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(sl3, order, side=side) + + +@pytest.mark.parametrize('pair,nominal_value,max_lev', [ + ("BNB/BUSD", 0.0, 40.0), + ("BNB/USDT", 100.0, 153.84615384615384), + ("BTC/USDT", 170.30, 250.0), + ("BNB/BUSD", 999999.9, 10.0), + ("BNB/USDT", 5000000.0, 6.666666666666667), + ("BTC/USDT", 300000000.1, 2.0), +]) +def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max_lev): + exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange._leverage_brackets = { + 'BNB/BUSD': [[0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5]], + 'BNB/USDT': [[0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25]], + 'BTC/USDT': [[0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5]], + } + assert exchange.get_max_leverage(pair, nominal_value) == max_lev + + +def test_fill_leverage_brackets_binance(default_conf, mocker): + api_mock = MagicMock() + api_mock.load_leverage_brackets = MagicMock(return_value={ + 'ADA/BUSD': [[0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5]], + 'BTC/USDT': [[0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5]], + "ZEC/USDT": [[0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5]], + + }) + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['collateral'] = Collateral.ISOLATED + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange.fill_leverage_brackets() + + assert exchange._leverage_brackets == { + 'ADA/BUSD': [[0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5]], + 'BTC/USDT': [[0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5]], + "ZEC/USDT": [[0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5]], + } + + api_mock = MagicMock() + api_mock.load_leverage_brackets = MagicMock() + type(api_mock).has = PropertyMock(return_value={'loadLeverageBrackets': True}) + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "fill_leverage_brackets", + "load_leverage_brackets" + ) + + +def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): + api_mock = MagicMock() + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['collateral'] = Collateral.ISOLATED + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange.fill_leverage_brackets() + + leverage_brackets = { + "1000SHIB/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "1INCH/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "AAVE/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "ADA/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ] + } + + for key, value in leverage_brackets.items(): + assert exchange._leverage_brackets[key] == value + + +def test__set_leverage_binance(mocker, default_conf): + + api_mock = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + default_conf['dry_run'] = False + exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN) + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "_set_leverage", + "set_leverage", + pair="XRP/USDT", + leverage=5.0, + trading_mode=TradingMode.FUTURES + ) @pytest.mark.asyncio @@ -138,3 +372,15 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): assert exchange._api_async.fetch_ohlcv.call_count == 2 assert res == ohlcv assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) + + +@pytest.mark.parametrize("trading_mode,collateral,config", [ + ("", "", {}), + ("margin", "cross", {"options": {"defaultType": "margin"}}), + ("futures", "isolated", {"options": {"defaultType": "future"}}), +]) +def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config): + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = collateral + exchange = get_patched_exchange(mocker, default_conf, id="binance") + assert exchange._ccxt_config == config diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index bd0994c18..e0221fa6c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -11,6 +11,7 @@ import ccxt import pytest from pandas import DataFrame +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken @@ -131,6 +132,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog) assert ex._api.headers == {'hello': 'world'} + assert ex._ccxt_config == {} Exchange._headers = {} @@ -395,7 +397,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) - assert isclose(result, 2 * (1+0.05) / (1-abs(stoploss))) + expected_result = 2 * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0) + assert isclose(result, expected_result/3) # min amount is set markets["ETH/BTC"]["limits"] = { @@ -407,7 +413,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, 2 * 2 * (1+0.05) / (1-abs(stoploss))) + expected_result = 2 * 2 * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) + assert isclose(result, expected_result/5) # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -419,7 +429,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss))) + expected_result = max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) + assert isclose(result, expected_result/10) # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -431,14 +445,26 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) + expected_result = max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) + assert isclose(result, expected_result/7.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - assert isclose(result, max(8, 2 * 2) * 1.5) + expected_result = max(8, 2 * 2) * 1.5 + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) + assert isclose(result, expected_result/8.0) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) - assert isclose(result, max(8, 2 * 2) * 1.5) + expected_result = max(8, 2 * 2) * 1.5 + assert isclose(result, expected_result) + # With Leverage + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) + assert isclose(result, expected_result/12) def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: @@ -456,10 +482,10 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round( - max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), - 8 - ) + expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) + assert round(result, 8) == round(expected_result, 8) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) + assert round(result, 8) == round(expected_result/3, 8) def test_set_sandbox(default_conf, mocker): @@ -970,7 +996,13 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) order = exchange.create_dry_run_order( - pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) + pair='ETH/BTC', + ordertype='limit', + side=side, + amount=1, + rate=200, + leverage=1.0 + ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side @@ -993,7 +1025,13 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, ) order = exchange.create_dry_run_order( - pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice) + pair='LTC/USDT', + ordertype='limit', + side=side, + amount=1, + rate=startprice, + leverage=1.0 + ) assert order_book_l2_usd.call_count == 1 assert 'id' in order assert f'dry_run_{side}_' in order["id"] @@ -1039,7 +1077,13 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou ) order = exchange.create_dry_run_order( - pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate) + pair='LTC/USDT', + ordertype='market', + side=side, + amount=amount, + rate=rate, + leverage=1.0 + ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side @@ -1049,10 +1093,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou assert round(order["average"], 4) == round(endprice, 4) -@pytest.mark.parametrize("side", [ - ("buy"), - ("sell") -]) +@pytest.mark.parametrize("side", ["buy", "sell"]) @pytest.mark.parametrize("ordertype,rate,marketprice", [ ("market", None, None), ("market", 200, True), @@ -1074,9 +1115,17 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange._set_leverage = MagicMock() + exchange.set_margin_mode = MagicMock() order = exchange.create_order( - pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200) + pair='ETH/BTC', + ordertype=ordertype, + side=side, + amount=1, + rate=200, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -1086,6 +1135,21 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, assert api_mock.create_order.call_args[0][2] == side assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][4] is rate + assert exchange._set_leverage.call_count == 0 + assert exchange.set_margin_mode.call_count == 0 + + exchange.trading_mode = TradingMode.FUTURES + order = exchange.create_order( + pair='ETH/BTC', + ordertype=ordertype, + side=side, + amount=1, + rate=200, + leverage=3.0 + ) + + assert exchange._set_leverage.call_count == 1 + assert exchange.set_margin_mode.call_count == 1 def test_buy_dry_run(default_conf, mocker): @@ -2624,10 +2688,17 @@ def test_get_fee(default_conf, mocker, exchange_name): def test_stoploss_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='bittrex') with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side="sell", + leverage=1.0 + ) with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): - exchange.stoploss_adjust(1, {}) + exchange.stoploss_adjust(1, {}, side="sell") def test_merge_ft_has_dict(default_conf, mocker): @@ -2972,7 +3043,6 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: (3, 5, 5), (4, 5, 2), (5, 5, 1), - ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected @@ -3044,3 +3114,120 @@ def test_get_funding_fees(default_conf, mocker, exchange_name): pair="XRP/USDT", since=unix_time ) + + +@pytest.mark.parametrize('exchange', ['binance', 'kraken', 'ftx']) +@pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [ + (9.0, 3.0, 3.0), + (20.0, 5.0, 4.0), + (100.0, 100.0, 1.0) +]) +def test_get_stake_amount_considering_leverage( + exchange, + stake_amount, + leverage, + min_stake_with_lev, + mocker, + default_conf +): + exchange = get_patched_exchange(mocker, default_conf, id=exchange) + assert exchange._get_stake_amount_considering_leverage( + stake_amount, leverage) == min_stake_with_lev + + +@pytest.mark.parametrize("exchange_name,trading_mode", [ + ("binance", TradingMode.FUTURES), + ("ftx", TradingMode.MARGIN), + ("ftx", TradingMode.FUTURES) +]) +def test__set_leverage(mocker, default_conf, exchange_name, trading_mode): + + api_mock = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + default_conf['dry_run'] = False + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + exchange_name, + "_set_leverage", + "set_leverage", + pair="XRP/USDT", + leverage=5.0, + trading_mode=trading_mode + ) + + +@pytest.mark.parametrize("collateral", [ + (Collateral.CROSS), + (Collateral.ISOLATED) +]) +def test_set_margin_mode(mocker, default_conf, collateral): + + api_mock = MagicMock() + api_mock.set_margin_mode = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setMarginMode': True}) + default_conf['dry_run'] = False + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "set_margin_mode", + "set_margin_mode", + pair="XRP/USDT", + collateral=collateral + ) + + +@pytest.mark.parametrize("exchange_name, trading_mode, collateral, exception_thrown", [ + ("binance", TradingMode.SPOT, None, False), + ("binance", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("kraken", TradingMode.SPOT, None, False), + ("kraken", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("kraken", TradingMode.FUTURES, Collateral.ISOLATED, True), + ("ftx", TradingMode.SPOT, None, False), + ("ftx", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("ftx", TradingMode.FUTURES, Collateral.ISOLATED, True), + ("bittrex", TradingMode.SPOT, None, False), + ("bittrex", TradingMode.MARGIN, Collateral.CROSS, True), + ("bittrex", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("bittrex", TradingMode.FUTURES, Collateral.CROSS, True), + ("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True), + + # TODO-lev: Remove once implemented + ("binance", TradingMode.MARGIN, Collateral.CROSS, True), + ("binance", TradingMode.FUTURES, Collateral.CROSS, True), + ("binance", TradingMode.FUTURES, Collateral.ISOLATED, True), + ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), + ("kraken", TradingMode.FUTURES, Collateral.CROSS, True), + ("ftx", TradingMode.MARGIN, Collateral.CROSS, True), + ("ftx", TradingMode.FUTURES, Collateral.CROSS, True), + + # TODO-lev: Uncomment once implemented + # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), + # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), + # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), + # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), + # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), + # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), + # ("ftx", TradingMode.FUTURES, Collateral.CROSS, False) +]) +def test_validate_trading_mode_and_collateral( + default_conf, + mocker, + exchange_name, + trading_mode, + collateral, + exception_thrown +): + exchange = get_patched_exchange( + mocker, default_conf, id=exchange_name, mock_supported_modes=False) + if (exception_thrown): + with pytest.raises(OperationalException): + exchange.validate_trading_mode_and_collateral(trading_mode, collateral) + else: + exchange.validate_trading_mode_and_collateral(trading_mode, collateral) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 3794bb79c..ca6b24d64 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -14,7 +14,11 @@ from .test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop' -def test_stoploss_order_ftx(default_conf, mocker): +@pytest.mark.parametrize('order_price,exchangelimitratio,side', [ + (217.8, 1.05, "sell"), + (222.2, 0.95, "buy"), +]) +def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitratio, side): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -32,12 +36,18 @@ def test_stoploss_order_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side=side, + order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio}, + leverage=1.0 + ) assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] assert 'stopPrice' in api_mock.create_order.call_args_list[0][1]['params'] @@ -47,51 +57,79 @@ def test_stoploss_order_ftx(default_conf, mocker): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={'stoploss': 'limit'}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={'stoploss': 'limit'}, side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params'] - assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8 + assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == order_price assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 # test exception handling with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) -def test_stoploss_order_dry_run_ftx(default_conf, mocker): +@pytest.mark.parametrize('side', [("sell"), ("buy")]) +def test_stoploss_order_dry_run_ftx(default_conf, mocker, side): api_mock = MagicMock() default_conf['dry_run'] = True mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) @@ -101,7 +139,14 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -112,20 +157,24 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker): assert order['amount'] == 1 -def test_stoploss_adjust_ftx(mocker, default_conf): +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_ftx(mocker, default_conf, sl1, sl2, sl3, side): exchange = get_patched_exchange(mocker, default_conf, id='ftx') order = { 'type': STOPLOSS_ORDERTYPE, 'price': 1500, } - assert exchange.stoploss_adjust(1501, order) - assert not exchange.stoploss_adjust(1499, order) + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) # Test with invalid order case ... order['type'] = 'stop_loss_limit' - assert not exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(sl3, order, side=side) -def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): +def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order, limit_buy_order): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 @@ -158,6 +207,16 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): assert resp['type'] == 'stop' assert resp['status_stop'] == 'triggered' + api_mock.fetch_order = MagicMock(return_value=limit_buy_order) + + resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') + assert resp + assert api_mock.fetch_order.call_count == 1 + assert resp['id_stop'] == 'mocked_limit_buy' + assert resp['id'] == 'X' + assert resp['type'] == 'stop' + assert resp['status_stop'] == 'triggered' + with pytest.raises(InvalidOrderException): api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') @@ -191,3 +250,20 @@ def test_get_order_id(mocker, default_conf): } } assert exchange.get_order_id_conditional(order) == '1111' + + +@pytest.mark.parametrize('pair,nominal_value,max_lev', [ + ("ADA/BTC", 0.0, 20.0), + ("BTC/EUR", 100.0, 20.0), + ("ZEC/USD", 173.31, 20.0), +]) +def test_get_max_leverage_ftx(default_conf, mocker, pair, nominal_value, max_lev): + exchange = get_patched_exchange(mocker, default_conf, id="ftx") + assert exchange.get_max_leverage(pair, nominal_value) == max_lev + + +def test_fill_leverage_brackets_ftx(default_conf, mocker): + # FTX only has one account wide leverage, so there's no leverage brackets + exchange = get_patched_exchange(mocker, default_conf, id="ftx") + exchange.fill_leverage_brackets() + assert exchange._leverage_brackets == {} diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index eb79dfc10..a8cd8d8ef 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -166,7 +166,11 @@ def test_get_balances_prod(default_conf, mocker): @pytest.mark.parametrize('ordertype', ['market', 'limit']) -def test_stoploss_order_kraken(default_conf, mocker, ordertype): +@pytest.mark.parametrize('side,adjustedprice', [ + ("sell", 217.8), + ("buy", 222.2), +]) +def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -183,10 +187,17 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={'stoploss': ordertype, - 'stoploss_on_exchange_limit_ratio': 0.99 - }) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + side=side, + order_types={ + 'stoploss': ordertype, + 'stoploss_on_exchange_limit_ratio': 0.99 + }, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -195,12 +206,14 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): if ordertype == 'limit': assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_LIMIT_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['params'] == { - 'trading_agreement': 'agree', 'price2': 217.8} + 'trading_agreement': 'agree', + 'price2': adjustedprice + } else: assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['params'] == { 'trading_agreement': 'agree'} - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['price'] == 220 @@ -208,20 +221,36 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) -def test_stoploss_order_dry_run_kraken(default_conf, mocker): +@pytest.mark.parametrize('side', ['buy', 'sell']) +def test_stoploss_order_dry_run_kraken(default_conf, mocker, side): api_mock = MagicMock() default_conf['dry_run'] = True mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) @@ -231,7 +260,14 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -242,14 +278,54 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): assert order['amount'] == 1 -def test_stoploss_adjust_kraken(mocker, default_conf): +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_kraken(mocker, default_conf, sl1, sl2, sl3, side): exchange = get_patched_exchange(mocker, default_conf, id='kraken') order = { 'type': STOPLOSS_ORDERTYPE, 'price': 1500, } - assert exchange.stoploss_adjust(1501, order) - assert not exchange.stoploss_adjust(1499, order) + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) # Test with invalid order case ... order['type'] = 'stop_loss_limit' - assert not exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(sl3, order, side=side) + + +@pytest.mark.parametrize('pair,nominal_value,max_lev', [ + ("ADA/BTC", 0.0, 3.0), + ("BTC/EUR", 100.0, 5.0), + ("ZEC/USD", 173.31, 2.0), +]) +def test_get_max_leverage_kraken(default_conf, mocker, pair, nominal_value, max_lev): + exchange = get_patched_exchange(mocker, default_conf, id="kraken") + exchange._leverage_brackets = { + 'ADA/BTC': ['2', '3'], + 'BTC/EUR': ['2', '3', '4', '5'], + 'ZEC/USD': ['2'] + } + assert exchange.get_max_leverage(pair, nominal_value) == max_lev + + +def test_fill_leverage_brackets_kraken(default_conf, mocker): + api_mock = MagicMock() + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") + exchange.fill_leverage_brackets() + + assert exchange._leverage_brackets == { + 'BLK/BTC': [1, 2, 3], + 'TKN/BTC': [1, 2, 3, 4, 5], + 'ETH/BTC': [1, 2], + 'LTC/BTC': [1], + 'XRP/BTC': [1], + 'NEO/BTC': [1], + 'BTT/BTC': [1], + 'ETH/USDT': [1], + 'LTC/USDT': [1], + 'LTC/USD': [1], + 'XLTCUSDT': [1], + 'LTC/ETH': [1] + } diff --git a/tests/leverage/test_leverage.py b/tests/leverage/test_interest.py similarity index 83% rename from tests/leverage/test_leverage.py rename to tests/leverage/test_interest.py index 7b7ca0f9b..c7e787bdb 100644 --- a/tests/leverage/test_leverage.py +++ b/tests/leverage/test_interest.py @@ -22,9 +22,10 @@ twentyfive_hours = Decimal(25.0) ('kraken', 0.00025, five_hours, 0.045), ('kraken', 0.00025, twentyfive_hours, 0.12), # FTX - # TODO-lev: - implement FTX tests - # ('ftx', Decimal(0.0005), ten_mins, 0.06), - # ('ftx', Decimal(0.0005), five_hours, 0.045), + ('ftx', 0.0005, ten_mins, 0.00125), + ('ftx', 0.00025, ten_mins, 0.000625), + ('ftx', 0.00025, five_hours, 0.003125), + ('ftx', 0.00025, twentyfive_hours, 0.015625), ]) def test_interest(exchange, interest_rate, hours, expected): borrowed = Decimal(60.0) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index b34c3a916..e4ce29d44 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -884,6 +884,10 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: assert hyperopt.backtesting.strategy.buy_rsi.value != 35 assert hyperopt.backtesting.strategy.sell_rsi.value != 74 + hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1' + with pytest.raises(OperationalException, match="Estimator ET1 not supported."): + hyperopt.get_optimizer([], 2) + def test_SKDecimal(): space = SKDecimal(1, 2, decimals=2) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 5f0701a22..1ce8d172c 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -4,6 +4,7 @@ import time from unittest.mock import MagicMock, PropertyMock import pytest +import time_machine from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.exceptions import OperationalException @@ -11,7 +12,8 @@ from freqtrade.persistence import Trade from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver -from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re +from tests.conftest import (create_mock_trades, get_patched_exchange, get_patched_freqtradebot, + log_has, log_has_re) @pytest.fixture(scope="function") @@ -662,6 +664,31 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: assert log_has("PerformanceFilter is not available in this mode.", caplog) +@pytest.mark.usefixtures("init_persistence") +def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None: + whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC') + whitelist_conf['pairlists'] = [ + {"method": "StaticPairList"}, + {"method": "PerformanceFilter", "minutes": 60} + ] + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + exchange = get_patched_exchange(mocker, whitelist_conf) + pm = PairListManager(exchange, whitelist_conf) + pm.refresh_pairlist() + + assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + create_mock_trades(fee) + pm.refresh_pairlist() + assert pm.whitelist == ['XRP/BTC', 'ETH/BTC', 'TKN/BTC'] + + # Move to "outside" of lookback window, so original sorting is restored. + t.move_to("2021-09-01 07:00:00 +00:00") + pm.refresh_pairlist() + assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + + def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] @@ -815,32 +842,63 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history): - ohlcv_data = { - ('ETH/BTC', '1d'): ohlcv_history, - ('TKN/BTC', '1d'): ohlcv_history, - ('LTC/BTC', '1d'): ohlcv_history, - } - mocker.patch.multiple('freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), - ) + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + } + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), + ) - freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) - assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 - freqtrade.pairlists.refresh_pairlist() - assert len(freqtrade.pairlists.whitelist) == 3 - assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 - previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count - freqtrade.pairlists.refresh_pairlist() - assert len(freqtrade.pairlists.whitelist) == 3 - # Called once for XRP/BTC - assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + 1 + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + # Call to XRP/BTC cached + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 2 + + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history.iloc[[0]], + } + mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 + + # Move to next day + t.move_to("2021-09-02 01:00:00 +00:00") + mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 + + # Move another day with fresh mocks (now the pair is old enough) + t.move_to("2021-09-03 01:00:00 +00:00") + # Called once for XRP/BTC + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history, + } + mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 4 + # Called once (only for XRP/BTC) + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 def test_OffsetFilter_error(mocker, whitelist_conf) -> None: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2852486ed..7c98b2df7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -422,20 +422,22 @@ def test_api_stopbuy(botclient): assert ftbot.config['max_open_trades'] == 0 -def test_api_balance(botclient, mocker, rpc_balance): +def test_api_balance(botclient, mocker, rpc_balance, tickers): ftbot, client = botclient ftbot.config['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) + mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") ftbot.wallets.update() rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) - assert "currencies" in rc.json() - assert len(rc.json()["currencies"]) == 5 - assert rc.json()['currencies'][0] == { + response = rc.json() + assert "currencies" in response + assert len(response["currencies"]) == 5 + assert response['currencies'][0] == { 'currency': 'BTC', 'free': 12.0, 'balance': 12.0, @@ -443,6 +445,10 @@ def test_api_balance(botclient, mocker, rpc_balance): 'est_stake': 12.0, 'stake': 'BTC', } + assert 'starting_capital' in response + assert 'starting_capital_fiat' in response + assert 'starting_capital_pct' in response + assert 'starting_capital_ratio' in response def test_api_count(botclient, mocker, ticker, fee, markets): @@ -1218,6 +1224,7 @@ def test_api_strategies(botclient): assert_response(rc) assert rc.json() == {'strategies': [ 'HyperoptableStrategy', + 'InformativeDecoratorTest', 'StrategyTestV2', 'TestStrategyLegacyV1' ]} diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2013dad7d..21f1cd000 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -576,6 +576,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'total': 100.0, 'symbol': 100.0, 'value': 1000.0, + 'starting_capital': 1000, + 'starting_capital_fiat': 1000, }) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py new file mode 100644 index 000000000..a32ad79e8 --- /dev/null +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -0,0 +1,75 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +from pandas import DataFrame + +from freqtrade.strategy import informative, merge_informative_pair +from freqtrade.strategy.interface import IStrategy + + +class InformativeDecoratorTest(IStrategy): + """ + Strategy used by tests freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 2 + stoploss = -0.10 + timeframe = '5m' + startup_candle_count: int = 20 + + def informative_pairs(self): + return [('BTC/USDT', '5m')] + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['buy'] = 0 + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['sell'] = 0 + return dataframe + + # Decorator stacking test. + @informative('30m') + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Simple informative test. + @informative('1h', 'BTC/{stake}') + def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Quote currency different from stake currency test. + @informative('1h', 'ETH/BTC') + def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Formatting test. + @informative('30m', 'BTC/{stake}', '{column}_{BASE}_{QUOTE}_{base}_{quote}_{asset}_{timeframe}') + def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Custom formatter test + @informative('30m', 'ETH/{stake}', fmt=lambda column, **kwargs: column + '_from_callable') + def populate_indicators_eth_30m(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Strategy timeframe indicators for current pair. + dataframe['rsi'] = 14 + # Informative pairs are available in this method. + dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] + + # Mixing manual informative pairs with decorators. + informative = self.dp.get_pair_dataframe('BTC/USDT', '5m') + informative['rsi'] = 14 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, '5m', ffill=True) + + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 5e9b86d4a..d3c876782 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -611,7 +611,7 @@ def test_is_informative_pairs_callback(default_conf): strategy = StrategyResolver.load_strategy(default_conf) # Should return empty # Uses fallback to base implementation - assert [] == strategy.informative_pairs() + assert [] == strategy.gather_informative_pairs() @pytest.mark.parametrize('error', [ diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 3b84fc254..a01b55050 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -4,7 +4,9 @@ import numpy as np import pandas as pd import pytest -from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes +from freqtrade.data.dataprovider import DataProvider +from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open, + timeframe_to_minutes) def generate_test_data(timeframe: str, size: int): @@ -132,3 +134,65 @@ def test_stoploss_from_open(): assert stoploss == 0 else: assert isclose(stop_price, expected_stop_price, rel_tol=0.00001) + + +def test_stoploss_from_absolute(): + assert stoploss_from_absolute(90, 100) == 1 - (90 / 100) + assert stoploss_from_absolute(100, 100) == 0 + assert stoploss_from_absolute(110, 100) == 0 + assert stoploss_from_absolute(100, 0) == 1 + assert stoploss_from_absolute(0, 100) == 1 + + +def test_informative_decorator(mocker, default_conf): + test_data_5m = generate_test_data('5m', 40) + test_data_30m = generate_test_data('30m', 40) + test_data_1h = generate_test_data('1h', 40) + data = { + ('XRP/USDT', '5m'): test_data_5m, + ('XRP/USDT', '30m'): test_data_30m, + ('XRP/USDT', '1h'): test_data_1h, + ('LTC/USDT', '5m'): test_data_5m, + ('LTC/USDT', '30m'): test_data_30m, + ('LTC/USDT', '1h'): test_data_1h, + ('BTC/USDT', '30m'): test_data_30m, + ('BTC/USDT', '5m'): test_data_5m, + ('BTC/USDT', '1h'): test_data_1h, + ('ETH/USDT', '1h'): test_data_1h, + ('ETH/USDT', '30m'): test_data_30m, + ('ETH/BTC', '1h'): test_data_1h, + } + from .strats.informative_decorator_strategy import InformativeDecoratorTest + default_conf['stake_currency'] = 'USDT' + strategy = InformativeDecoratorTest(config=default_conf) + strategy.dp = DataProvider({}, None, None) + mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[ + 'XRP/USDT', 'LTC/USDT', 'BTC/USDT' + ]) + + assert len(strategy._ft_informative) == 6 # Equal to number of decorators used + informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'), + ('LTC/USDT', '30m'), ('BTC/USDT', '1h'), ('BTC/USDT', '30m'), + ('BTC/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')] + for inf_pair in informative_pairs: + assert inf_pair in strategy.gather_informative_pairs() + + def test_historic_ohlcv(pair, timeframe): + return data[(pair, timeframe or strategy.timeframe)].copy() + mocker.patch('freqtrade.data.dataprovider.DataProvider.historic_ohlcv', + side_effect=test_historic_ohlcv) + + analyzed = strategy.advise_all_indicators( + {p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')}) + expected_columns = [ + 'rsi_1h', 'rsi_30m', # Stacked informative decorators + 'btc_usdt_rsi_1h', # BTC 1h informative + 'rsi_BTC_USDT_btc_usdt_BTC/USDT_30m', # Column formatting + 'rsi_from_callable', # Custom column formatter + 'eth_btc_rsi_1h', # Quote currency not matching stake currency + 'rsi', 'rsi_less', # Non-informative columns + 'rsi_5m', # Manual informative dataframe + ] + for _, dataframe in analyzed.items(): + for col in expected_columns: + assert col in dataframe.columns diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 63c3496a2..8b7505883 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 3 + assert len(strategies) == 4 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 4 + assert len(strategies) == 5 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 3 + assert len([x for x in strategies if x['class'] is not None]) == 4 assert len([x for x in strategies if x['class'] is None]) == 1 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f278604be..bb9527011 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -78,11 +78,15 @@ def test_bot_cleanup(mocker, default_conf, caplog) -> None: assert coo_mock.call_count == 1 -def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: +@pytest.mark.parametrize('runmode', [ + RunMode.DRY_RUN, + RunMode.LIVE +]) +def test_order_dict(default_conf, mocker, runmode, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) conf = default_conf.copy() - conf['runmode'] = RunMode.DRY_RUN + conf['runmode'] = runmode conf['order_types'] = { 'buy': 'market', 'sell': 'limit', @@ -92,45 +96,14 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: conf['bid_strategy']['price_side'] = 'ask' freqtrade = FreqtradeBot(conf) + if runmode == RunMode.LIVE: + assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) assert freqtrade.strategy.order_types['stoploss_on_exchange'] caplog.clear() # is left untouched conf = default_conf.copy() - conf['runmode'] = RunMode.DRY_RUN - conf['order_types'] = { - 'buy': 'market', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False, - } - freqtrade = FreqtradeBot(conf) - assert not freqtrade.strategy.order_types['stoploss_on_exchange'] - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) - - -def test_order_dict_live(default_conf, mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - - conf = default_conf.copy() - conf['runmode'] = RunMode.LIVE - conf['order_types'] = { - 'buy': 'market', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': True, - } - conf['bid_strategy']['price_side'] = 'ask' - - freqtrade = FreqtradeBot(conf) - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) - assert freqtrade.strategy.order_types['stoploss_on_exchange'] - - caplog.clear() - # is left untouched - conf = default_conf.copy() - conf['runmode'] = RunMode.LIVE + conf['runmode'] = runmode conf['order_types'] = { 'buy': 'market', 'sell': 'limit', @@ -219,8 +192,14 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: 'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 -def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: - +@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [ + # Override stoploss + (0.79, False), + # Override strategy stoploss + (0.85, True) +]) +def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, + buy_price_mult, ignore_strat_sl, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) @@ -234,9 +213,9 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': buy_price * 0.79, - 'ask': buy_price * 0.79, - 'last': buy_price * 0.79 + 'bid': buy_price * buy_price_mult, + 'ask': buy_price * buy_price_mult, + 'last': buy_price * buy_price_mult, }), get_fee=fee, ) @@ -253,46 +232,10 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf ############################################# # stoploss shoud be hit - assert freqtrade.handle_trade(trade) is True - assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) - assert trade.sell_reason == SellType.STOP_LOSS.value - - -def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, - mocker, edge_conf) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - patch_edge(mocker) - edge_conf['max_open_trades'] = float('inf') - - # Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2 - # Thus, if price falls 15%, stoploss should not be triggered - # - # mocking the ticker: price is falling ... - buy_price = limit_buy_order['price'] - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': buy_price * 0.85, - 'ask': buy_price * 0.85, - 'last': buy_price * 0.85 - }), - get_fee=fee, - ) - ############################################# - - # Create a trade with "limit_buy_order" price - freqtrade = FreqtradeBot(edge_conf) - freqtrade.active_pair_whitelist = ['NEO/BTC'] - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.enter_positions() - trade = Trade.query.first() - trade.update(limit_buy_order) - ############################################# - - # stoploss shoud not be hit - assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_trade(trade) is not ignore_strat_sl + if not ignore_strat_sl: + assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) + assert trade.sell_reason == SellType.STOP_LOSS.value def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None: @@ -376,8 +319,16 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, freqtrade.create_trade('ETH/BTC') -def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: +@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [ + (0.0005, True, True, 99), + (0.000000005, True, False, 99), + (0, False, True, 99), + (UNLIMITED_STAKE_AMOUNT, False, True, 0), +]) +def test_create_trade_minimal_amount( + default_conf, ticker, limit_buy_order_open, fee, mocker, + stake_amount, create, amount_enough, max_open_trades, caplog +) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value=limit_buy_order_open) @@ -387,78 +338,33 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, create_order=buy_mock, get_fee=fee, ) - default_conf['stake_amount'] = 0.0005 + default_conf['max_open_trades'] = max_open_trades freqtrade = FreqtradeBot(default_conf) + freqtrade.config['stake_amount'] = stake_amount patch_get_signal(freqtrade) - freqtrade.create_trade('ETH/BTC') - rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] - assert rate * amount <= default_conf['stake_amount'] - - -def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - buy_mock = MagicMock(return_value=limit_buy_order_open) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=buy_mock, - get_fee=fee, - ) - - freqtrade = FreqtradeBot(default_conf) - freqtrade.config['stake_amount'] = 0.000000005 - - patch_get_signal(freqtrade) - - assert freqtrade.create_trade('ETH/BTC') - assert log_has_re(r"Stake amount for pair .* is too small.*", caplog) - - -def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - buy_mock = MagicMock(return_value=limit_buy_order_open) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=buy_mock, - get_fee=fee, - ) - - freqtrade = FreqtradeBot(default_conf) - freqtrade.config['stake_amount'] = 0 - - patch_get_signal(freqtrade) - - assert not freqtrade.create_trade('ETH/BTC') - - -def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=MagicMock(return_value=limit_buy_order_open), - get_fee=fee, - ) - default_conf['max_open_trades'] = 0 - default_conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 + if create: + assert freqtrade.create_trade('ETH/BTC') + if amount_enough: + rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] + assert rate * amount <= default_conf['stake_amount'] + else: + assert log_has_re( + r"Stake amount for pair .* is too small.*", + caplog + ) + else: + assert not freqtrade.create_trade('ETH/BTC') + if not max_open_trades: + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 +@pytest.mark.parametrize('whitelist,positions', [ + (["ETH/BTC"], 1), # No pairs left + ([], 0), # No pairs in whitelist +]) def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, - mocker, caplog) -> None: + whitelist, positions, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -467,36 +373,20 @@ def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_ope create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) - - default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] + default_conf['exchange']['pair_whitelist'] = whitelist freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) n = freqtrade.enter_positions() - assert n == 1 - assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) - n = freqtrade.enter_positions() - assert n == 0 - assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) - - -def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, - mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=MagicMock(return_value={'id': limit_buy_order['id']}), - get_fee=fee, - ) - default_conf['exchange']['pair_whitelist'] = [] - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - n = freqtrade.enter_positions() - assert n == 0 - assert log_has("Active pair whitelist is empty.", caplog) + assert n == positions + if positions: + assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) + n = freqtrade.enter_positions() + assert n == 0 + assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) + else: + assert n == 0 + assert log_has("Active pair whitelist is empty.", caplog) @pytest.mark.usefixtures("init_persistence") @@ -1252,6 +1142,7 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, @pytest.mark.usefixtures("init_persistence") def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, limit_buy_order, limit_sell_order) -> None: + # TODO-lev: test for short # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) @@ -1343,10 +1234,14 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, assert freqtrade.handle_stoploss_on_exchange(trade) is False cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') - stoploss_order_mock.assert_called_once_with(amount=85.32423208, - pair='ETH/BTC', - order_types=freqtrade.strategy.order_types, - stop_price=0.00002346 * 0.95) + stoploss_order_mock.assert_called_once_with( + amount=85.32423208, + pair='ETH/BTC', + order_types=freqtrade.strategy.order_types, + stop_price=0.00002346 * 0.95, + side="sell", + leverage=1.0 + ) # price fell below stoploss, so dry-run sells trade. mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ @@ -1359,6 +1254,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: + # TODO-lev: test for short # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_exchange(mocker) @@ -1417,7 +1313,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', return_value=stoploss_order_hanging) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) # Still try to create order @@ -1427,7 +1323,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c caplog.clear() cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -1436,6 +1332,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set + # TODO-lev: test for short stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) mocker.patch.multiple( @@ -1526,10 +1423,14 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, assert freqtrade.handle_stoploss_on_exchange(trade) is False cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') - stoploss_order_mock.assert_called_once_with(amount=85.32423208, - pair='ETH/BTC', - order_types=freqtrade.strategy.order_types, - stop_price=0.00002346 * 0.96) + stoploss_order_mock.assert_called_once_with( + amount=85.32423208, + pair='ETH/BTC', + order_types=freqtrade.strategy.order_types, + stop_price=0.00002346 * 0.96, + side="sell", + leverage=1.0 + ) # price fell below stoploss, so dry-run sells trade. mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ @@ -1542,7 +1443,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: - + # TODO-lev: test for short # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) @@ -1647,36 +1548,37 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, # stoploss should be set to 1% as trailing is on assert trade.stop_loss == 0.00002346 * 0.99 cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') - stoploss_order_mock.assert_called_once_with(amount=2132892.49146757, - pair='NEO/BTC', - order_types=freqtrade.strategy.order_types, - stop_price=0.00002346 * 0.99) + stoploss_order_mock.assert_called_once_with( + amount=2132892.49146757, + pair='NEO/BTC', + order_types=freqtrade.strategy.order_types, + stop_price=0.00002346 * 0.99, + side="sell", + leverage=1.0 + ) -def test_enter_positions(mocker, default_conf, caplog) -> None: +@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/BTC: ') +]) +def test_enter_positions(mocker, default_conf, return_value, side_effect, + log_message, caplog) -> None: caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_ct = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', - MagicMock(return_value=False)) - n = freqtrade.enter_positions() - assert n == 0 - assert log_has('Found no enter signals for whitelisted currencies. Trying again...', caplog) - # create_trade should be called once for every pair in the whitelist. - assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) - - -def test_enter_positions_exception(mocker, default_conf, caplog) -> None: - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_ct = mocker.patch( 'freqtrade.freqtradebot.FreqtradeBot.create_trade', - MagicMock(side_effect=DependencyException) + MagicMock( + return_value=return_value, + side_effect=side_effect + ) ) n = freqtrade.enter_positions() assert n == 0 + assert log_has(log_message, caplog) + # create_trade should be called once for every pair in the whitelist. assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) - assert log_has('Unable to create trade for ETH/BTC: ', caplog) def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: @@ -1770,8 +1672,13 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No assert log_has_re('Found open order for.*', caplog) +@pytest.mark.parametrize('initial_amount,has_rounding_fee', [ + (90.99181073 + 1e-14, True), + (8.0, False) +]) def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, - mocker): + mocker, initial_amount, has_rounding_fee, caplog): + trades_for_order[0]['amount'] = initial_amount mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) @@ -1792,32 +1699,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ freqtrade.update_trade_state(trade, '123456', limit_buy_order) assert trade.amount != amount assert trade.amount == limit_buy_order['amount'] - - -def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, fee, - limit_buy_order, mocker, caplog): - trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14 - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # fetch_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) - patch_exchange(mocker) - amount = sum(x['amount'] for x in trades_for_order) - freqtrade = get_patched_freqtradebot(mocker, default_conf) - trade = Trade( - pair='LTC/ETH', - amount=amount, - exchange='binance', - open_rate=0.245441, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_order_id='123456', - is_open=True, - open_date=arrow.utcnow().datetime, - ) - freqtrade.update_trade_state(trade, '123456', limit_buy_order) - assert trade.amount != amount - assert trade.amount == limit_buy_order['amount'] - assert log_has_re(r'Applying fee on amount for .*', caplog) + if has_rounding_fee: + assert log_has_re(r'Applying fee on amount for .*', caplog) def test_update_trade_state_exception(mocker, default_conf, @@ -3129,16 +3012,28 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee, assert mock_insuf.call_count == 1 -def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: +@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [ + # Enable profit + (True, 0.00001172, 0.00001173, False, True, SellType.SELL_SIGNAL.value), + # Disable profit + (False, 0.00002172, 0.00002173, True, False, SellType.SELL_SIGNAL.value), + # Enable loss + # * Shouldn't this be SellType.STOP_LOSS.value + (True, 0.00000172, 0.00000173, False, False, None), + # Disable loss + (False, 0.00000172, 0.00000173, True, False, SellType.SELL_SIGNAL.value), +]) +def test_sell_profit_only( + default_conf, limit_buy_order, limit_buy_order_open, + fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': 0.00001172, - 'ask': 0.00001173, - 'last': 0.00001172 + 'bid': bid, + 'ask': ask, + 'last': bid }), create_order=MagicMock(side_effect=[ limit_buy_order_open, @@ -3148,128 +3043,29 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy ) default_conf.update({ 'use_sell_signal': True, - 'sell_profit_only': True, + 'sell_profit_only': profit_only, 'sell_profit_offset': 0.1, }) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - + if sell_type == SellType.SELL_SIGNAL.value: + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) + else: + freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( + sell_type=SellType.NONE)) freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_trade(trade) is handle_first - freqtrade.strategy.sell_profit_offset = 0.0 - assert freqtrade.handle_trade(trade) is True + if handle_second: + freqtrade.strategy.sell_profit_offset = 0.0 + assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value - - -def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00002172, - 'ask': 0.00002173, - 'last': 0.00002172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': False, - }) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value - - -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00000172, - 'ask': 0.00000173, - 'last': 0.00000172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': True, - }) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( - sell_type=SellType.NONE)) - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is False - - -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.0000172, - 'ask': 0.0000173, - 'last': 0.0000172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': False, - }) - - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value + assert trade.sell_reason == sell_type def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open, @@ -3307,11 +3103,15 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_ assert trade.amount != amnt -def test__safe_exit_amount(default_conf, fee, caplog, mocker): +@pytest.mark.parametrize('amount_wallet,has_err', [ + (95.29, False), + (91.29, True) +]) +def test__safe_exit_amount(default_conf, fee, caplog, mocker, amount_wallet, has_err): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 - amount_wallet = 95.29 + amount_wallet = amount_wallet mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) wallet_update = mocker.patch('freqtrade.wallets.Wallets.update') trade = Trade( @@ -3325,37 +3125,19 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker): ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - - wallet_update.reset_mock() - assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet - assert log_has_re(r'.*Falling back to wallet-amount.', caplog) - assert wallet_update.call_count == 1 - caplog.clear() - wallet_update.reset_mock() - assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet - assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) - assert wallet_update.call_count == 1 - - -def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): - patch_RPCManager(mocker) - patch_exchange(mocker) - amount = 95.33 - amount_wallet = 91.29 - mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) - trade = Trade( - pair='LTC/ETH', - amount=amount, - exchange='binance', - open_rate=0.245441, - open_order_id="123456", - fee_open=fee.return_value, - fee_close=fee.return_value, - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - with pytest.raises(DependencyException, match=r"Not enough amount to exit."): - assert freqtrade._safe_exit_amount(trade.pair, trade.amount) + if has_err: + with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."): + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) + else: + wallet_update.reset_mock() + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet + assert log_has_re(r'.*Falling back to wallet-amount.', caplog) + assert wallet_update.call_count == 1 + caplog.clear() + wallet_update.reset_mock() + assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet + assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) + assert wallet_update.call_count == 1 def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: @@ -4143,50 +3925,37 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o assert trade is None -def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: +@pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [ + (False, 0.045, 0.046, 2, None), + (True, 0.042, 0.046, 1, {'bids': [[]], 'asks': [[]]}) +]) +def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, exception_thrown, + ask, last, order_book_top, order_book, caplog) -> None: """ - test if function get_rate will return the order book price - instead of the ask rate + test if function get_rate will return the order book price instead of the ask rate """ patch_exchange(mocker) - ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046}) + ticker_mock = MagicMock(return_value={'ask': ask, 'last': last}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_l2_order_book=order_book_l2, + fetch_l2_order_book=MagicMock(return_value=order_book) if order_book else order_book_l2, fetch_ticker=ticker_mock, - ) default_conf['exchange']['name'] = 'binance' default_conf['bid_strategy']['use_order_book'] = True - default_conf['bid_strategy']['order_book_top'] = 2 + default_conf['bid_strategy']['order_book_top'] = order_book_top default_conf['bid_strategy']['ask_last_balance'] = 0 default_conf['telegram']['enabled'] = False freqtrade = FreqtradeBot(default_conf) - assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 - assert ticker_mock.call_count == 0 - - -def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None: - patch_exchange(mocker) - ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046}) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_l2_order_book=MagicMock(return_value={'bids': [[]], 'asks': [[]]}), - fetch_ticker=ticker_mock, - - ) - default_conf['exchange']['name'] = 'binance' - default_conf['bid_strategy']['use_order_book'] = True - default_conf['bid_strategy']['order_book_top'] = 1 - default_conf['bid_strategy']['ask_last_balance'] = 0 - default_conf['telegram']['enabled'] = False - - freqtrade = FreqtradeBot(default_conf) - # orderbook shall be used even if tickers would be lower. - with pytest.raises(PricingError): - freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") - assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog) + if exception_thrown: + with pytest.raises(PricingError): + freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") + assert log_has_re( + r'Buy Price at location 1 from orderbook could not be determined.', caplog) + else: + assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 + assert ticker_mock.call_count == 0 def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: diff --git a/tests/test_periodiccache.py b/tests/test_periodiccache.py new file mode 100644 index 000000000..f874f9041 --- /dev/null +++ b/tests/test_periodiccache.py @@ -0,0 +1,32 @@ +import time_machine + +from freqtrade.configuration import PeriodicCache + + +def test_ttl_cache(): + + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + + cache = PeriodicCache(5, ttl=60) + cache1h = PeriodicCache(5, ttl=3600) + + assert cache.timer() == 1630472400.0 + cache['a'] = 1235 + cache1h['a'] = 555123 + assert 'a' in cache + assert 'a' in cache1h + + t.move_to("2021-09-01 05:00:59 +00:00") + assert 'a' in cache + assert 'a' in cache1h + + # Cache expired + t.move_to("2021-09-01 05:01:00 +00:00") + assert 'a' not in cache + assert 'a' in cache1h + + t.move_to("2021-09-01 05:59:59 +00:00") + assert 'a' in cache1h + + t.move_to("2021-09-01 06:00:00 +00:00") + assert 'a' not in cache1h From d6b36231e7b4986701c9a63bd36ac5b08205b470 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 20 Sep 2021 23:12:17 -0600 Subject: [PATCH 26/59] added schedule to environment.yml --- environment.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index f58434c15..780fda7fb 100644 --- a/environment.yml +++ b/environment.yml @@ -29,7 +29,7 @@ dependencies: - colorama - questionary - prompt-toolkit - + - schedule # ============================ # 2/4 req dev @@ -59,6 +59,7 @@ dependencies: - plotly - jupyter + - pip: - pycoingecko - py_find_1st From 5113ceb6c85a577149e14dca48c09b77fc70684b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 21 Sep 2021 15:52:12 -0600 Subject: [PATCH 27/59] added schedule to setup.py --- setup.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 727c40c7c..bbf797ac5 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ hyperopt = [ 'joblib', 'progressbar2', 'psutil', - ] +] develop = [ 'coveralls', @@ -31,7 +31,7 @@ jupyter = [ 'nbstripout', 'ipykernel', 'nbconvert', - ] +] all_extra = plot + develop + jupyter + hyperopt @@ -41,7 +41,7 @@ setup( 'pytest-asyncio', 'pytest-cov', 'pytest-mock', - ], + ], install_requires=[ # from requirements.txt 'ccxt>=1.50.48', @@ -71,7 +71,8 @@ setup( 'fastapi', 'uvicorn', 'pyjwt', - 'aiofiles' + 'aiofiles', + 'schedule' ], extras_require={ 'dev': all_extra, From c4ac8761836032c5e2d4042ffbd3ed79d5e46b31 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 29 Sep 2021 22:16:44 -0600 Subject: [PATCH 28/59] Replace datetime.utcnow with datetime.now(timezone.utc) --- freqtrade/freqtradebot.py | 12 ++++++------ tests/conftest.py | 2 +- tests/plugins/test_protections.py | 9 +++++---- tests/rpc/test_rpc.py | 12 ++++++------ tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 21 +++++++++++---------- tests/strategy/test_default_strategy.py | 4 ++-- tests/test_persistence.py | 2 +- 8 files changed, 34 insertions(+), 32 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ebc91f97f..59ddafb16 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -594,7 +594,7 @@ class FreqtradeBot(LoggingMixin): # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') - open_date = datetime.utcnow() + open_date = datetime.now(timezone.utc) if self.trading_mode == TradingMode.FUTURES: funding_fees = self.exchange.get_funding_fees_from_exchange(pair, open_date) else: @@ -610,7 +610,7 @@ class FreqtradeBot(LoggingMixin): fee_close=fee, open_rate=enter_limit_filled_price, open_rate_requested=enter_limit_requested, - open_date=datetime.utcnow(), + open_date=datetime.now(timezone.utc), exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), @@ -652,7 +652,7 @@ class FreqtradeBot(LoggingMixin): 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), 'amount': trade.amount, - 'open_date': trade.open_date or datetime.utcnow(), + 'open_date': trade.open_date or datetime.now(timezone.utc), 'current_rate': trade.open_rate_requested, } @@ -848,7 +848,7 @@ class FreqtradeBot(LoggingMixin): stop_price = trade.open_rate * (1 + stoploss) if self.create_stoploss_order(trade=trade, stop_price=stop_price): - trade.stoploss_last_update = datetime.utcnow() + trade.stoploss_last_update = datetime.now(timezone.utc) return False # If stoploss order is canceled for some reason we add it @@ -885,7 +885,7 @@ class FreqtradeBot(LoggingMixin): if self.exchange.stoploss_adjust(trade.stop_loss, order, side): # we check if the update is necessary update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) - if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: + if (datetime.now(timezone.utc) - trade.stoploss_last_update).total_seconds() >= update_beat: # cancelling the current stoploss on exchange first logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} " f"(orderid:{order['id']}) in order to add another one ...") @@ -1241,7 +1241,7 @@ class FreqtradeBot(LoggingMixin): 'profit_ratio': profit_ratio, 'sell_reason': trade.sell_reason, 'open_date': trade.open_date, - 'close_date': trade.close_date or datetime.utcnow(), + 'close_date': trade.close_date or datetime.now(timezone.utc), 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), } diff --git a/tests/conftest.py b/tests/conftest.py index b35ff17d6..40f1e6e56 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -420,7 +420,7 @@ def get_default_conf(testdatadir): @pytest.fixture def update(): _update = Update(0) - _update.message = Message(0, datetime.utcnow(), Chat(0, 0)) + _update.message = Message(0, datetime.now(timezone.utc)(), Chat(0, 0)) return _update diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index c0a9ae72a..19ed2915e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -22,8 +22,8 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, stake_amount=0.01, fee_open=fee, fee_close=fee, - open_date=datetime.utcnow() - timedelta(minutes=min_ago_open or 200), - close_date=datetime.utcnow() - timedelta(minutes=min_ago_close or 30), + open_date=datetime.now(timezone.utc)() - timedelta(minutes=min_ago_open or 200), + close_date=datetime.now(timezone.utc)() - timedelta(minutes=min_ago_close or 30), open_rate=open_rate, is_open=is_open, amount=0.01 / open_rate, @@ -45,9 +45,10 @@ def test_protectionmanager(mocker, default_conf): for handler in freqtrade.protections._protection_handlers: assert handler.name in constants.AVAILABLE_PROTECTIONS if not handler.has_global_stop: - assert handler.global_stop(datetime.utcnow()) == (False, None, None) + assert handler.global_stop(datetime.now(timezone.utc)()) == (False, None, None) if not handler.has_local_stop: - assert handler.stop_per_pair('XRP/BTC', datetime.utcnow()) == (False, None, None) + assert handler.stop_per_pair( + 'XRP/BTC', datetime.now(timezone.utc)()) == (False, None, None) @pytest.mark.parametrize('timeframe,expected,protconf', [ diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 586fadff8..f195ce0b8 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -265,7 +265,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, # Simulate buy & sell trade.update(limit_buy_order) trade.update(limit_sell_order) - trade.close_date = datetime.utcnow() + trade.close_date = datetime.now(timezone.utc)() trade.is_open = False # Try valid data @@ -282,7 +282,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, assert (day['fiat_value'] == 0.0 or day['fiat_value'] == 0.76748865) # ensure first day is current date - assert str(days['data'][0]['date']) == str(datetime.utcnow().date()) + assert str(days['data'][0]['date']) == str(datetime.now(timezone.utc)().date()) # Try invalid data with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'): @@ -409,7 +409,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, fetch_ticker=ticker_sell_up ) trade.update(limit_sell_order) - trade.close_date = datetime.utcnow() + trade.close_date = datetime.now(timezone.utc)() trade.is_open = False freqtradebot.enter_positions() @@ -423,7 +423,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, fetch_ticker=ticker_sell_up ) trade.update(limit_sell_order) - trade.close_date = datetime.utcnow() + trade.close_date = datetime.now(timezone.utc)() trade.is_open = False stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) @@ -489,7 +489,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, get_fee=fee ) trade.update(limit_sell_order) - trade.close_date = datetime.utcnow() + trade.close_date = datetime.now(timezone.utc)() trade.is_open = False for trade in Trade.query.order_by(Trade.id).all(): @@ -831,7 +831,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, # Simulate fulfilled LIMIT_SELL order for trade trade.update(limit_sell_order) - trade.close_date = datetime.utcnow() + trade.close_date = datetime.now(timezone.utc)() trade.is_open = False res = rpc._rpc_performance() assert len(res) == 1 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index afce87b88..4ed679762 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -546,7 +546,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): assert len(rc.json()['data']) == 7 assert rc.json()['stake_currency'] == 'BTC' assert rc.json()['fiat_display_currency'] == 'USD' - assert rc.json()['data'][0]['date'] == str(datetime.utcnow().date()) + assert rc.json()['data'][0]['date'] == str(datetime.now(timezone.utc)().date()) def test_api_trades(botclient, mocker, fee, markets): @@ -983,7 +983,7 @@ def test_api_forcebuy(botclient, mocker, fee): stake_amount=1, open_rate=0.245441, open_order_id="123456", - open_date=datetime.utcnow(), + open_date=datetime.now(timezone.utc)(), is_open=False, fee_close=fee.return_value, fee_open=fee.return_value, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 23ccadca0..9f5fe71ca 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -33,6 +33,7 @@ class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator """ + def __init__(self, rpc: RPC, config) -> None: super().__init__(rpc, config) self.state = {'called': False} @@ -132,7 +133,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) chat = Chat(0xdeadbeef, 0) update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), datetime.utcnow(), chat) + update.message = Message(randint(1, 100), datetime.now(timezone.utc)(), chat) default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -343,7 +344,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, # Simulate fulfilled LIMIT_SELL order for trade trade.update(limit_sell_order) - trade.close_date = datetime.utcnow() + trade.close_date = datetime.now(timezone.utc)() trade.is_open = False # Try valid data @@ -353,7 +354,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert 'Daily' in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(datetime.now(timezone.utc)().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -365,7 +366,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert 'Daily' in msg_mock.call_args_list[0][0][0] - assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(datetime.now(timezone.utc)().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -382,7 +383,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, for trade in trades: trade.update(limit_buy_order) trade.update(limit_sell_order) - trade.close_date = datetime.utcnow() + trade.close_date = datetime.now(timezone.utc)() trade.is_open = False # /daily 1 @@ -462,7 +463,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) trade.update(limit_sell_order) - trade.close_date = datetime.utcnow() + trade.close_date = datetime.now(timezone.utc)() trade.is_open = False telegram._profit(update=update, context=MagicMock()) @@ -966,7 +967,7 @@ def test_performance_handle(default_conf, update, ticker, fee, # Simulate fulfilled LIMIT_SELL order for trade trade.update(limit_sell_order) - trade.close_date = datetime.utcnow() + trade.close_date = datetime.now(timezone.utc)() trade.is_open = False telegram._performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -997,9 +998,9 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: msg = ('
  current    max    total stake\n---------  -----  -------------\n'
            '        1      {}          {}
').format( - default_conf['max_open_trades'], - default_conf['stake_amount'] - ) + default_conf['max_open_trades'], + default_conf['stake_amount'] + ) assert msg in msg_mock.call_args_list[0][0][0] diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index a995491f2..2d09590da 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -37,10 +37,10 @@ def test_strategy_test_v2(result, fee): assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', - current_time=datetime.utcnow(), side='long') is True + current_time=datetime.now(timezone.utc)(), side='long') is True assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', sell_reason='roi', - current_time=datetime.utcnow()) is True + current_time=datetime.now(timezone.utc)()) is True # TODO-lev: Test for shorts? assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 58ce47ea7..0c077899d 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -255,7 +255,7 @@ def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, stake_amount=20.0, amount=30.0, open_rate=2.0, - open_date=datetime.utcnow() - timedelta(minutes=minutes), + open_date=datetime.now(timezone.utc)() - timedelta(minutes=minutes), fee_open=fee.return_value, fee_close=fee.return_value, exchange=exchange, From 993dc672b46ff39c93dd12a7dea16240c4c05888 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 29 Sep 2021 22:18:15 -0600 Subject: [PATCH 29/59] timestamp * 1000 in get_funding_fees_from_exchange --- 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 b1ba1b5b8..315ab62c5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1609,7 +1609,7 @@ class Exchange: f"fetch_funding_history() has not been implemented on ccxt.{self.name}") if type(since) is datetime: - since = int(since.timestamp()) + since = int(since.timestamp()) * 1000 # * 1000 for ms try: funding_history = self._api.fetch_funding_history( From af6afd0ac2dcfafa35c6bfcfe20a819e8196e497 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 29 Sep 2021 22:27:21 -0600 Subject: [PATCH 30/59] Revert "Replace datetime.utcnow with datetime.now(timezone.utc)" This reverts commit c4ac8761836032c5e2d4042ffbd3ed79d5e46b31. --- freqtrade/freqtradebot.py | 12 ++++++------ tests/conftest.py | 2 +- tests/plugins/test_protections.py | 9 ++++----- tests/rpc/test_rpc.py | 12 ++++++------ tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 21 ++++++++++----------- tests/strategy/test_default_strategy.py | 4 ++-- tests/test_persistence.py | 2 +- 8 files changed, 32 insertions(+), 34 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 59ddafb16..ebc91f97f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -594,7 +594,7 @@ class FreqtradeBot(LoggingMixin): # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') - open_date = datetime.now(timezone.utc) + open_date = datetime.utcnow() if self.trading_mode == TradingMode.FUTURES: funding_fees = self.exchange.get_funding_fees_from_exchange(pair, open_date) else: @@ -610,7 +610,7 @@ class FreqtradeBot(LoggingMixin): fee_close=fee, open_rate=enter_limit_filled_price, open_rate_requested=enter_limit_requested, - open_date=datetime.now(timezone.utc), + open_date=datetime.utcnow(), exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), @@ -652,7 +652,7 @@ class FreqtradeBot(LoggingMixin): 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), 'amount': trade.amount, - 'open_date': trade.open_date or datetime.now(timezone.utc), + 'open_date': trade.open_date or datetime.utcnow(), 'current_rate': trade.open_rate_requested, } @@ -848,7 +848,7 @@ class FreqtradeBot(LoggingMixin): stop_price = trade.open_rate * (1 + stoploss) if self.create_stoploss_order(trade=trade, stop_price=stop_price): - trade.stoploss_last_update = datetime.now(timezone.utc) + trade.stoploss_last_update = datetime.utcnow() return False # If stoploss order is canceled for some reason we add it @@ -885,7 +885,7 @@ class FreqtradeBot(LoggingMixin): if self.exchange.stoploss_adjust(trade.stop_loss, order, side): # we check if the update is necessary update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) - if (datetime.now(timezone.utc) - trade.stoploss_last_update).total_seconds() >= update_beat: + if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: # cancelling the current stoploss on exchange first logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} " f"(orderid:{order['id']}) in order to add another one ...") @@ -1241,7 +1241,7 @@ class FreqtradeBot(LoggingMixin): 'profit_ratio': profit_ratio, 'sell_reason': trade.sell_reason, 'open_date': trade.open_date, - 'close_date': trade.close_date or datetime.now(timezone.utc), + 'close_date': trade.close_date or datetime.utcnow(), 'stake_currency': self.config['stake_currency'], 'fiat_currency': self.config.get('fiat_display_currency', None), } diff --git a/tests/conftest.py b/tests/conftest.py index 40f1e6e56..b35ff17d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -420,7 +420,7 @@ def get_default_conf(testdatadir): @pytest.fixture def update(): _update = Update(0) - _update.message = Message(0, datetime.now(timezone.utc)(), Chat(0, 0)) + _update.message = Message(0, datetime.utcnow(), Chat(0, 0)) return _update diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 19ed2915e..c0a9ae72a 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -22,8 +22,8 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, stake_amount=0.01, fee_open=fee, fee_close=fee, - open_date=datetime.now(timezone.utc)() - timedelta(minutes=min_ago_open or 200), - close_date=datetime.now(timezone.utc)() - timedelta(minutes=min_ago_close or 30), + open_date=datetime.utcnow() - timedelta(minutes=min_ago_open or 200), + close_date=datetime.utcnow() - timedelta(minutes=min_ago_close or 30), open_rate=open_rate, is_open=is_open, amount=0.01 / open_rate, @@ -45,10 +45,9 @@ def test_protectionmanager(mocker, default_conf): for handler in freqtrade.protections._protection_handlers: assert handler.name in constants.AVAILABLE_PROTECTIONS if not handler.has_global_stop: - assert handler.global_stop(datetime.now(timezone.utc)()) == (False, None, None) + assert handler.global_stop(datetime.utcnow()) == (False, None, None) if not handler.has_local_stop: - assert handler.stop_per_pair( - 'XRP/BTC', datetime.now(timezone.utc)()) == (False, None, None) + assert handler.stop_per_pair('XRP/BTC', datetime.utcnow()) == (False, None, None) @pytest.mark.parametrize('timeframe,expected,protconf', [ diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index f195ce0b8..586fadff8 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -265,7 +265,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, # Simulate buy & sell trade.update(limit_buy_order) trade.update(limit_sell_order) - trade.close_date = datetime.now(timezone.utc)() + trade.close_date = datetime.utcnow() trade.is_open = False # Try valid data @@ -282,7 +282,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, assert (day['fiat_value'] == 0.0 or day['fiat_value'] == 0.76748865) # ensure first day is current date - assert str(days['data'][0]['date']) == str(datetime.now(timezone.utc)().date()) + assert str(days['data'][0]['date']) == str(datetime.utcnow().date()) # Try invalid data with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'): @@ -409,7 +409,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, fetch_ticker=ticker_sell_up ) trade.update(limit_sell_order) - trade.close_date = datetime.now(timezone.utc)() + trade.close_date = datetime.utcnow() trade.is_open = False freqtradebot.enter_positions() @@ -423,7 +423,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, fetch_ticker=ticker_sell_up ) trade.update(limit_sell_order) - trade.close_date = datetime.now(timezone.utc)() + trade.close_date = datetime.utcnow() trade.is_open = False stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) @@ -489,7 +489,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, get_fee=fee ) trade.update(limit_sell_order) - trade.close_date = datetime.now(timezone.utc)() + trade.close_date = datetime.utcnow() trade.is_open = False for trade in Trade.query.order_by(Trade.id).all(): @@ -831,7 +831,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, # Simulate fulfilled LIMIT_SELL order for trade trade.update(limit_sell_order) - trade.close_date = datetime.now(timezone.utc)() + trade.close_date = datetime.utcnow() trade.is_open = False res = rpc._rpc_performance() assert len(res) == 1 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 4ed679762..afce87b88 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -546,7 +546,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): assert len(rc.json()['data']) == 7 assert rc.json()['stake_currency'] == 'BTC' assert rc.json()['fiat_display_currency'] == 'USD' - assert rc.json()['data'][0]['date'] == str(datetime.now(timezone.utc)().date()) + assert rc.json()['data'][0]['date'] == str(datetime.utcnow().date()) def test_api_trades(botclient, mocker, fee, markets): @@ -983,7 +983,7 @@ def test_api_forcebuy(botclient, mocker, fee): stake_amount=1, open_rate=0.245441, open_order_id="123456", - open_date=datetime.now(timezone.utc)(), + open_date=datetime.utcnow(), is_open=False, fee_close=fee.return_value, fee_open=fee.return_value, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 9f5fe71ca..23ccadca0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -33,7 +33,6 @@ class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator """ - def __init__(self, rpc: RPC, config) -> None: super().__init__(rpc, config) self.state = {'called': False} @@ -133,7 +132,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) chat = Chat(0xdeadbeef, 0) update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), datetime.now(timezone.utc)(), chat) + update.message = Message(randint(1, 100), datetime.utcnow(), chat) default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -344,7 +343,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, # Simulate fulfilled LIMIT_SELL order for trade trade.update(limit_sell_order) - trade.close_date = datetime.now(timezone.utc)() + trade.close_date = datetime.utcnow() trade.is_open = False # Try valid data @@ -354,7 +353,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert 'Daily' in msg_mock.call_args_list[0][0][0] - assert str(datetime.now(timezone.utc)().date()) in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -366,7 +365,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, telegram._daily(update=update, context=context) assert msg_mock.call_count == 1 assert 'Daily' in msg_mock.call_args_list[0][0][0] - assert str(datetime.now(timezone.utc)().date()) in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] @@ -383,7 +382,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, for trade in trades: trade.update(limit_buy_order) trade.update(limit_sell_order) - trade.close_date = datetime.now(timezone.utc)() + trade.close_date = datetime.utcnow() trade.is_open = False # /daily 1 @@ -463,7 +462,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) trade.update(limit_sell_order) - trade.close_date = datetime.now(timezone.utc)() + trade.close_date = datetime.utcnow() trade.is_open = False telegram._profit(update=update, context=MagicMock()) @@ -967,7 +966,7 @@ def test_performance_handle(default_conf, update, ticker, fee, # Simulate fulfilled LIMIT_SELL order for trade trade.update(limit_sell_order) - trade.close_date = datetime.now(timezone.utc)() + trade.close_date = datetime.utcnow() trade.is_open = False telegram._performance(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -998,9 +997,9 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: msg = ('
  current    max    total stake\n---------  -----  -------------\n'
            '        1      {}          {}
').format( - default_conf['max_open_trades'], - default_conf['stake_amount'] - ) + default_conf['max_open_trades'], + default_conf['stake_amount'] + ) assert msg in msg_mock.call_args_list[0][0][0] diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 2d09590da..a995491f2 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -37,10 +37,10 @@ def test_strategy_test_v2(result, fee): assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', - current_time=datetime.now(timezone.utc)(), side='long') is True + current_time=datetime.utcnow(), side='long') is True assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', sell_reason='roi', - current_time=datetime.now(timezone.utc)()) is True + current_time=datetime.utcnow()) is True # TODO-lev: Test for shorts? assert strategy.custom_stoploss(pair='ETH/BTC', trade=trade, current_time=datetime.now(), diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0c077899d..58ce47ea7 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -255,7 +255,7 @@ def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, stake_amount=20.0, amount=30.0, open_rate=2.0, - open_date=datetime.now(timezone.utc)() - timedelta(minutes=minutes), + open_date=datetime.utcnow() - timedelta(minutes=minutes), fee_open=fee.return_value, fee_close=fee.return_value, exchange=exchange, From 157223f6ab057a822542f9e474c764f638dfbbe0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 29 Sep 2021 22:32:02 -0600 Subject: [PATCH 31/59] datetime.utc -> datetime.now(timezone.utc) --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ebc91f97f..12338a501 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -594,7 +594,7 @@ class FreqtradeBot(LoggingMixin): # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') - open_date = datetime.utcnow() + open_date = datetime.now(timezone.utc) if self.trading_mode == TradingMode.FUTURES: funding_fees = self.exchange.get_funding_fees_from_exchange(pair, open_date) else: @@ -610,7 +610,7 @@ class FreqtradeBot(LoggingMixin): fee_close=fee, open_rate=enter_limit_filled_price, open_rate_requested=enter_limit_requested, - open_date=datetime.utcnow(), + open_date=open_date, exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), From ba60aad89de7471f1c354236ae4319ee58839a53 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 29 Sep 2021 22:56:10 -0600 Subject: [PATCH 32/59] parameterized TradingMode in persistence --- tests/test_freqtradebot.py | 4 + tests/test_persistence.py | 231 ++++++++++++++++++++----------------- 2 files changed, 126 insertions(+), 109 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 71926f9b7..5e7288967 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4421,3 +4421,7 @@ def test_get_valid_price(mocker, default_conf) -> None: assert valid_price_at_min_alwd > custom_price_under_min_alwd assert valid_price_at_min_alwd < proposed_price + + +def test_update_funding_fees(): + return diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 58ce47ea7..7724df957 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -18,6 +18,9 @@ from tests.conftest import (create_mock_trades, create_mock_trades_with_leverage log_has, log_has_re) +spot, margin = TradingMode.SPOT, TradingMode.MARGIN + + def test_init_create_session(default_conf): # Check if init create a session init_db(default_conf['db_url'], default_conf['dry_run']) @@ -83,7 +86,7 @@ def test_enter_exit_side(fee, is_short): exchange='binance', is_short=is_short, leverage=2.0, - trading_mode=TradingMode.MARGIN + trading_mode=margin ) assert trade.enter_side == enter_side assert trade.exit_side == exit_side @@ -104,7 +107,7 @@ def test_set_stop_loss_isolated_liq(fee): exchange='binance', is_short=False, leverage=2.0, - trading_mode=TradingMode.MARGIN + trading_mode=margin ) trade.set_isolated_liq(0.09) assert trade.isolated_liq == 0.09 @@ -171,32 +174,33 @@ def test_set_stop_loss_isolated_liq(fee): assert trade.initial_stop_loss == 0.09 -@pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest', [ - ("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8)), - ("binance", True, 3, 10, 0.0005, 0.000625), - ("binance", False, 3, 295, 0.0005, round(0.004166666666666667, 8)), - ("binance", True, 3, 295, 0.0005, round(0.0031249999999999997, 8)), - ("binance", False, 3, 295, 0.00025, round(0.0020833333333333333, 8)), - ("binance", True, 3, 295, 0.00025, round(0.0015624999999999999, 8)), - ("binance", False, 5, 295, 0.0005, 0.005), - ("binance", True, 5, 295, 0.0005, round(0.0031249999999999997, 8)), - ("binance", False, 1, 295, 0.0005, 0.0), - ("binance", True, 1, 295, 0.0005, 0.003125), +@pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [ + ("binance", False, 3, 10, 0.0005, round(0.0008333333333333334, 8), margin), + ("binance", True, 3, 10, 0.0005, 0.000625, margin), + ("binance", False, 3, 295, 0.0005, round(0.004166666666666667, 8), margin), + ("binance", True, 3, 295, 0.0005, round(0.0031249999999999997, 8), margin), + ("binance", False, 3, 295, 0.00025, round(0.0020833333333333333, 8), margin), + ("binance", True, 3, 295, 0.00025, round(0.0015624999999999999, 8), margin), + ("binance", False, 5, 295, 0.0005, 0.005, margin), + ("binance", True, 5, 295, 0.0005, round(0.0031249999999999997, 8), margin), + ("binance", False, 1, 295, 0.0005, 0.0, spot), + ("binance", True, 1, 295, 0.0005, 0.003125, margin), - ("kraken", False, 3, 10, 0.0005, 0.040), - ("kraken", True, 3, 10, 0.0005, 0.030), - ("kraken", False, 3, 295, 0.0005, 0.06), - ("kraken", True, 3, 295, 0.0005, 0.045), - ("kraken", False, 3, 295, 0.00025, 0.03), - ("kraken", True, 3, 295, 0.00025, 0.0225), - ("kraken", False, 5, 295, 0.0005, round(0.07200000000000001, 8)), - ("kraken", True, 5, 295, 0.0005, 0.045), - ("kraken", False, 1, 295, 0.0005, 0.0), - ("kraken", True, 1, 295, 0.0005, 0.045), + ("kraken", False, 3, 10, 0.0005, 0.040, margin), + ("kraken", True, 3, 10, 0.0005, 0.030, margin), + ("kraken", False, 3, 295, 0.0005, 0.06, margin), + ("kraken", True, 3, 295, 0.0005, 0.045, margin), + ("kraken", False, 3, 295, 0.00025, 0.03, margin), + ("kraken", True, 3, 295, 0.00025, 0.0225, margin), + ("kraken", False, 5, 295, 0.0005, round(0.07200000000000001, 8), margin), + ("kraken", True, 5, 295, 0.0005, 0.045, margin), + ("kraken", False, 1, 295, 0.0005, 0.0, spot), + ("kraken", True, 1, 295, 0.0005, 0.045, margin), ]) @pytest.mark.usefixtures("init_persistence") -def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, rate, interest): +def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, rate, interest, + trading_mode): """ 10min, 5hr limit trade on Binance/Kraken at 3x,5x leverage fee: 0.25 % quote @@ -262,21 +266,21 @@ def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, leverage=lev, interest_rate=rate, is_short=is_short, - trading_mode=TradingMode.MARGIN + trading_mode=trading_mode ) assert round(float(trade.calculate_interest()), 8) == interest -@pytest.mark.parametrize('is_short,lev,borrowed', [ - (False, 1.0, 0.0), - (True, 1.0, 30.0), - (False, 3.0, 40.0), - (True, 3.0, 30.0), +@pytest.mark.parametrize('is_short,lev,borrowed,trading_mode', [ + (False, 1.0, 0.0, spot), + (True, 1.0, 30.0, margin), + (False, 3.0, 40.0, margin), + (True, 3.0, 30.0, margin), ]) @pytest.mark.usefixtures("init_persistence") def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, - caplog, is_short, lev, borrowed): + caplog, is_short, lev, borrowed, trading_mode): """ 10 minute limit trade on Binance/Kraken at 1x, 3x leverage fee: 0.25% quote @@ -352,18 +356,18 @@ def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange='binance', is_short=is_short, leverage=lev, - trading_mode=TradingMode.MARGIN + trading_mode=trading_mode ) assert trade.borrowed == borrowed -@pytest.mark.parametrize('is_short,open_rate,close_rate,lev,profit', [ - (False, 2.0, 2.2, 1.0, round(0.0945137157107232, 8)), - (True, 2.2, 2.0, 3.0, round(0.2589996297562085, 8)) +@pytest.mark.parametrize('is_short,open_rate,close_rate,lev,profit,trading_mode', [ + (False, 2.0, 2.2, 1.0, round(0.0945137157107232, 8), spot), + (True, 2.2, 2.0, 3.0, round(0.2589996297562085, 8), margin), ]) @pytest.mark.usefixtures("init_persistence") def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_usdt, - is_short, open_rate, close_rate, lev, profit): + is_short, open_rate, close_rate, lev, profit, trading_mode): """ 10 minute limit trade on Binance/Kraken at 1x, 3x leverage fee: 0.25% quote @@ -451,7 +455,7 @@ def test_update_limit_order(fee, caplog, limit_buy_order_usdt, limit_sell_order_ is_short=is_short, interest_rate=0.0005, leverage=lev, - trading_mode=TradingMode.MARGIN + trading_mode=trading_mode ) assert trade.open_order_id is None assert trade.close_profit is None @@ -497,7 +501,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, fee_close=fee.return_value, open_date=arrow.utcnow().datetime, exchange='binance', - trading_mode=TradingMode.MARGIN + trading_mode=margin ) trade.open_order_id = 'something' @@ -525,20 +529,22 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, caplog) -@pytest.mark.parametrize('exchange,is_short,lev,open_value,close_value,profit,profit_ratio', [ - ("binance", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232), - ("binance", True, 1, 59.850, 66.1663784375, -6.316378437500013, -0.1055368159983292), - ("binance", False, 3, 60.15, 65.83416667, 5.684166670000003, 0.2834995845386534), - ("binance", True, 3, 59.85, 66.1663784375, -6.316378437500013, -0.3166104479949876), - - ("kraken", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232), - ("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.106619298245614), - ("kraken", False, 3, 60.15, 65.795, 5.645, 0.2815461346633419), - ("kraken", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842), -]) +@pytest.mark.parametrize( + 'exchange,is_short,lev,open_value,close_value,profit,profit_ratio,trading_mode', [ + ("binance", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot), + ("binance", True, 1, 59.850, 66.1663784375, -6.316378437500013, -0.105536815998329, margin), + ("binance", False, 3, 60.15, 65.83416667, 5.684166670000003, 0.2834995845386534, margin), + ("binance", True, 3, 59.85, 66.1663784375, -6.316378437500013, -0.3166104479949876, margin), + ("kraken", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot), + ("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.106619298245614, margin), + ("kraken", False, 3, 60.15, 65.795, 5.645, 0.2815461346633419, margin), + ("kraken", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842, margin), + ]) @pytest.mark.usefixtures("init_persistence") -def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange, - is_short, lev, open_value, close_value, profit, profit_ratio): +def test_calc_open_close_trade_price( + limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange, is_short, lev, + open_value, close_value, profit, profit_ratio, trading_mode +): trade: Trade = Trade( pair='ADA/USDT', stake_amount=60.0, @@ -551,7 +557,7 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt exchange=exchange, is_short=is_short, leverage=lev, - trading_mode=TradingMode.MARGIN + trading_mode=trading_mode ) trade.open_order_id = f'something-{is_short}-{lev}-{exchange}' @@ -580,7 +586,7 @@ def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), interest_rate=0.0005, exchange='binance', - trading_mode=TradingMode.MARGIN + trading_mode=margin ) assert trade.close_profit is None assert trade.close_date is None @@ -609,7 +615,7 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee): fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', - trading_mode=TradingMode.MARGIN + trading_mode=margin ) trade.open_order_id = 'something' @@ -627,7 +633,7 @@ def test_update_open_order(limit_buy_order_usdt): fee_open=0.1, fee_close=0.1, exchange='binance', - trading_mode=TradingMode.MARGIN + trading_mode=margin ) assert trade.open_order_id is None @@ -652,7 +658,7 @@ def test_update_invalid_order(limit_buy_order_usdt): fee_open=0.1, fee_close=0.1, exchange='binance', - trading_mode=TradingMode.MARGIN + trading_mode=margin ) limit_buy_order_usdt['type'] = 'invalid' with pytest.raises(ValueError, match=r'Unknown order type'): @@ -660,6 +666,7 @@ def test_update_invalid_order(limit_buy_order_usdt): @pytest.mark.parametrize('exchange', ['binance', 'kraken']) +@pytest.mark.parametrize('trading_mode', [spot, margin]) @pytest.mark.parametrize('lev', [1, 3]) @pytest.mark.parametrize('is_short,fee_rate,result', [ (False, 0.003, 60.18), @@ -678,7 +685,8 @@ def test_calc_open_trade_value( lev, is_short, fee_rate, - result + result, + trading_mode ): # 10 minute limit trade on Binance/Kraken at 1x, 3x leverage # fee: 0.25 %, 0.3% quote @@ -705,7 +713,7 @@ def test_calc_open_trade_value( exchange=exchange, leverage=lev, is_short=is_short, - trading_mode=TradingMode.MARGIN + trading_mode=trading_mode ) trade.open_order_id = 'open_trade' @@ -713,26 +721,29 @@ def test_calc_open_trade_value( assert trade._calc_open_trade_value() == result -@pytest.mark.parametrize('exchange,is_short,lev,open_rate,close_rate,fee_rate,result', [ - ('binance', False, 1, 2.0, 2.5, 0.0025, 74.8125), - ('binance', False, 1, 2.0, 2.5, 0.003, 74.775), - ('binance', False, 1, 2.0, 2.2, 0.005, 65.67), - ('binance', False, 3, 2.0, 2.5, 0.0025, 74.81166667), - ('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667), - ('kraken', False, 3, 2.0, 2.5, 0.0025, 74.7725), - ('kraken', False, 3, 2.0, 2.5, 0.003, 74.735), - ('kraken', True, 3, 2.2, 2.5, 0.0025, 75.2626875), - ('kraken', True, 3, 2.2, 2.5, 0.003, 75.300225), - ('binance', True, 3, 2.2, 2.5, 0.0025, 75.18906641), - ('binance', True, 3, 2.2, 2.5, 0.003, 75.22656719), - ('binance', True, 1, 2.2, 2.5, 0.0025, 75.18906641), - ('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719), - ('kraken', True, 1, 2.2, 2.5, 0.0025, 75.2626875), - ('kraken', True, 1, 2.2, 2.5, 0.003, 75.300225), -]) +@pytest.mark.parametrize( + 'exchange,is_short,lev,open_rate,close_rate,fee_rate,result,trading_mode', [ + ('binance', False, 1, 2.0, 2.5, 0.0025, 74.8125, spot), + ('binance', False, 1, 2.0, 2.5, 0.003, 74.775, spot), + ('binance', False, 1, 2.0, 2.2, 0.005, 65.67, margin), + ('binance', False, 3, 2.0, 2.5, 0.0025, 74.81166667, margin), + ('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667, margin), + ('kraken', False, 3, 2.0, 2.5, 0.0025, 74.7725, margin), + ('kraken', False, 3, 2.0, 2.5, 0.003, 74.735, margin), + ('kraken', True, 3, 2.2, 2.5, 0.0025, 75.2626875, margin), + ('kraken', True, 3, 2.2, 2.5, 0.003, 75.300225, margin), + ('binance', True, 3, 2.2, 2.5, 0.0025, 75.18906641, margin), + ('binance', True, 3, 2.2, 2.5, 0.003, 75.22656719, margin), + ('binance', True, 1, 2.2, 2.5, 0.0025, 75.18906641, margin), + ('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719, margin), + ('kraken', True, 1, 2.2, 2.5, 0.0025, 75.2626875, margin), + ('kraken', True, 1, 2.2, 2.5, 0.003, 75.300225, margin), + ]) @pytest.mark.usefixtures("init_persistence") -def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, open_rate, - exchange, is_short, lev, close_rate, fee_rate, result): +def test_calc_close_trade_price( + limit_buy_order_usdt, limit_sell_order_usdt, open_rate, exchange, is_short, + lev, close_rate, fee_rate, result, trading_mode +): trade = Trade( pair='ADA/USDT', stake_amount=60.0, @@ -745,47 +756,48 @@ def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, ope interest_rate=0.0005, is_short=is_short, leverage=lev, - trading_mode=TradingMode.MARGIN + trading_mode=trading_mode ) trade.open_order_id = 'close_trade' assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result -@pytest.mark.parametrize('exchange,is_short,lev,close_rate,fee_close,profit,profit_ratio', [ - ('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673), - ('binance', False, 3, 2.1, 0.0025, 2.69166667, 0.13424771421446402), - ('binance', True, 1, 2.1, 0.0025, -3.308815781249997, -0.05528514254385963), - ('binance', True, 3, 2.1, 0.0025, -3.308815781249997, -0.1658554276315789), +@pytest.mark.parametrize( + 'exchange,is_short,lev,close_rate,fee_close,profit,profit_ratio,trading_mode', [ + ('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot), + ('binance', False, 3, 2.1, 0.0025, 2.69166667, 0.13424771421446402, margin), + ('binance', True, 1, 2.1, 0.0025, -3.308815781249997, -0.05528514254385963, margin), + ('binance', True, 3, 2.1, 0.0025, -3.308815781249997, -0.1658554276315789, margin), - ('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632), - ('binance', False, 3, 1.9, 0.0025, -3.29333333, -0.16425602643391513), - ('binance', True, 1, 1.9, 0.0025, 2.7063095312499996, 0.045218204365079395), - ('binance', True, 3, 1.9, 0.0025, 2.7063095312499996, 0.13565461309523819), + ('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin), + ('binance', False, 3, 1.9, 0.0025, -3.29333333, -0.16425602643391513, margin), + ('binance', True, 1, 1.9, 0.0025, 2.7063095312499996, 0.045218204365079395, margin), + ('binance', True, 3, 1.9, 0.0025, 2.7063095312499996, 0.13565461309523819, margin), - ('binance', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232), - ('binance', False, 3, 2.2, 0.0025, 5.68416667, 0.2834995845386534), - ('binance', True, 1, 2.2, 0.0025, -6.316378437499999, -0.1055368159983292), - ('binance', True, 3, 2.2, 0.0025, -6.316378437499999, -0.3166104479949876), + ('binance', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin), + ('binance', False, 3, 2.2, 0.0025, 5.68416667, 0.2834995845386534, margin), + ('binance', True, 1, 2.2, 0.0025, -6.316378437499999, -0.1055368159983292, margin), + ('binance', True, 3, 2.2, 0.0025, -6.316378437499999, -0.3166104479949876, margin), - ('kraken', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673), - ('kraken', False, 3, 2.1, 0.0025, 2.6525, 0.13229426433915248), - ('kraken', True, 1, 2.1, 0.0025, -3.3706575, -0.05631842105263152), - ('kraken', True, 3, 2.1, 0.0025, -3.3706575, -0.16895526315789455), + ('kraken', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot), + ('kraken', False, 3, 2.1, 0.0025, 2.6525, 0.13229426433915248, margin), + ('kraken', True, 1, 2.1, 0.0025, -3.3706575, -0.05631842105263152, margin), + ('kraken', True, 3, 2.1, 0.0025, -3.3706575, -0.16895526315789455, margin), - ('kraken', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632), - ('kraken', False, 3, 1.9, 0.0025, -3.3325, -0.16620947630922667), - ('kraken', True, 1, 1.9, 0.0025, 2.6503575, 0.04428333333333334), - ('kraken', True, 3, 1.9, 0.0025, 2.6503575, 0.13285000000000002), + ('kraken', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin), + ('kraken', False, 3, 1.9, 0.0025, -3.3325, -0.16620947630922667, margin), + ('kraken', True, 1, 1.9, 0.0025, 2.6503575, 0.04428333333333334, margin), + ('kraken', True, 3, 1.9, 0.0025, 2.6503575, 0.13285000000000002, margin), - ('kraken', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232), - ('kraken', False, 3, 2.2, 0.0025, 5.645, 0.2815461346633419), - ('kraken', True, 1, 2.2, 0.0025, -6.381165, -0.106619298245614), - ('kraken', True, 3, 2.2, 0.0025, -6.381165, -0.319857894736842), + ('kraken', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin), + ('kraken', False, 3, 2.2, 0.0025, 5.645, 0.2815461346633419, margin), + ('kraken', True, 1, 2.2, 0.0025, -6.381165, -0.106619298245614, margin), + ('kraken', True, 3, 2.2, 0.0025, -6.381165, -0.319857894736842, margin), - ('binance', False, 1, 2.1, 0.003, 2.6610000000000014, 0.04423940149625927), - ('binance', False, 1, 1.9, 0.003, -3.320999999999998, -0.05521197007481293), - ('binance', False, 1, 2.2, 0.003, 5.652000000000008, 0.09396508728179565), -]) + ('binance', False, 1, 2.1, 0.003, 2.6610000000000014, 0.04423940149625927, spot), + ('binance', False, 1, 1.9, 0.003, -3.320999999999998, -0.05521197007481293, spot), + ('binance', False, 1, 2.2, 0.003, 5.652000000000008, 0.09396508728179565, spot), + ]) @pytest.mark.usefixtures("init_persistence") def test_calc_profit( limit_buy_order_usdt, @@ -797,7 +809,8 @@ def test_calc_profit( close_rate, fee_close, profit, - profit_ratio + profit_ratio, + trading_mode ): """ 10 minute limit trade on Binance/Kraken at 1x, 3x leverage @@ -940,7 +953,7 @@ def test_calc_profit( leverage=lev, fee_open=0.0025, fee_close=fee_close, - trading_mode=TradingMode.MARGIN + trading_mode=trading_mode ) trade.open_order_id = 'something' From 6e86bdb82088b1a7797c48a9e5a37da7285c964e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 29 Sep 2021 23:11:01 -0600 Subject: [PATCH 33/59] Added test_update_funding_fees --- tests/test_freqtradebot.py | 39 +++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5e7288967..88134642a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -6,12 +6,13 @@ import time from copy import deepcopy from math import isclose from unittest.mock import ANY, MagicMock, PropertyMock +import time_machine import arrow import pytest from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT -from freqtrade.enums import RPCMessageType, RunMode, SellType, State +from freqtrade.enums import RPCMessageType, RunMode, SellType, State, TradingMode from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) @@ -4423,5 +4424,37 @@ def test_get_valid_price(mocker, default_conf) -> None: assert valid_price_at_min_alwd < proposed_price -def test_update_funding_fees(): - return +@pytest.mark.parametrize('exchange,trading_mode,calls', [ + ("ftx", TradingMode.SPOT, 0), + ("ftx", TradingMode.MARGIN, 0), + ("binance", TradingMode.FUTURES, 1), + ("kraken", TradingMode.FUTURES, 2), + ("ftx", TradingMode.FUTURES, 8), +]) +def test_update_funding_fees(mocker, default_conf, exchange, trading_mode, calls): + + patch_RPCManager(mocker) + patch_exchange(mocker) + freqtrade = FreqtradeBot(default_conf) + + with time_machine.travel("2021-09-01 00:00:00 +00:00") as t: + + # trade = Trade( + # id=2, + # pair='ADA/USDT', + # stake_amount=60.0, + # open_rate=2.0, + # amount=30.0, + # is_open=True, + # open_date=arrow.utcnow().datetime, + # fee_open=fee.return_value, + # fee_close=fee.return_value, + # exchange='binance', + # is_short=False, + # leverage=3.0, + # trading_mode=trading_mode + # ) + + t.move_to("2021-09-01 08:00:00 +00:00") + + assert freqtrade.update_funding_fees.call_count == calls From 77d3a8b4576f80a289980a77f777ee1b7b5dd350 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 30 Sep 2021 20:18:56 -0600 Subject: [PATCH 34/59] Added bybit funding-fee times --- freqtrade/exchange/bybit.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 163f8c44e..c4ffcdd0b 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -1,6 +1,6 @@ """ Bybit exchange subclass """ import logging -from typing import Dict +from typing import Dict, List from freqtrade.exchange import Exchange @@ -21,3 +21,5 @@ class Bybit(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 200, } + + funding_fee_times: List[int] = [0, 8, 16] # hours of the day From 9ea2dd05d8f7bd2ff7df5f2e256a1129c3b62023 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 1 Oct 2021 21:21:59 -0600 Subject: [PATCH 35/59] Removed space in retrier --- freqtrade/exchange/binance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 8779fdc8b..dc3d4bb5e 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -184,7 +184,7 @@ class Binance(Exchange): max_lev = 1/margin_req return max_lev - @ retrier + @retrier def _set_leverage( self, leverage: float, From 72388d33765bac61ea8eb604c0261111c7d4c077 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 2 Oct 2021 03:52:00 -0600 Subject: [PATCH 36/59] tried to solve test_update_funding_fees: --- tests/test_freqtradebot.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 88134642a..850572a62 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4435,7 +4435,11 @@ def test_update_funding_fees(mocker, default_conf, exchange, trading_mode, calls patch_RPCManager(mocker) patch_exchange(mocker) - freqtrade = FreqtradeBot(default_conf) + mocker.patch( + 'freqtrade.freqtradebot', + update_funding_fees=MagicMock(return_value=True) + ) + freqtrade = get_patched_freqtradebot(mocker, default_conf) with time_machine.travel("2021-09-01 00:00:00 +00:00") as t: From 6e1e1e00c259d13b81d50c84120a542378650b59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 4 Oct 2021 06:59:08 +0200 Subject: [PATCH 37/59] Fix mock going into nirvana --- tests/test_freqtradebot.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c2dfaeb24..5eb59981e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4292,10 +4292,7 @@ def test_update_funding_fees(mocker, default_conf, exchange, trading_mode, calls patch_RPCManager(mocker) patch_exchange(mocker) - mocker.patch( - 'freqtrade.freqtradebot', - update_funding_fees=MagicMock(return_value=True) - ) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_funding_fees', return_value=True) freqtrade = get_patched_freqtradebot(mocker, default_conf) with time_machine.travel("2021-09-01 00:00:00 +00:00") as t: From 29e582c6d961397b8eaf184e6b446064c0ce8bfe Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 5 Oct 2021 01:42:46 -0600 Subject: [PATCH 38/59] Fixed time format for schedule and update_funding_fees conf is mocked better --- freqtrade/freqtradebot.py | 7 +++++-- tests/test_freqtradebot.py | 12 ++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c7cb16a14..8307dd185 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime, timezone +from datetime import datetime, time, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -112,7 +112,7 @@ class FreqtradeBot(LoggingMixin): if self.trading_mode == TradingMode.FUTURES: for time_slot in self.exchange.funding_fee_times: - schedule.every().day.at(time_slot).do(self.update_funding_fees()) + schedule.every().day.at(str(time(time_slot))).do(self.update_funding_fees) self.wallets.update() def notify_status(self, msg: str) -> None: @@ -195,6 +195,9 @@ class FreqtradeBot(LoggingMixin): if self.get_free_open_trades(): self.enter_positions() + if self.trading_mode == TradingMode.FUTURES: + schedule.run_pending() + Trade.commit() def process_stopped(self) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5eb59981e..0e849f5ad 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -7,6 +7,7 @@ from copy import deepcopy from math import isclose from unittest.mock import ANY, MagicMock, PropertyMock import time_machine +import schedule import arrow import pytest @@ -4284,15 +4285,17 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: @pytest.mark.parametrize('exchange,trading_mode,calls', [ ("ftx", TradingMode.SPOT, 0), ("ftx", TradingMode.MARGIN, 0), - ("binance", TradingMode.FUTURES, 1), - ("kraken", TradingMode.FUTURES, 2), - ("ftx", TradingMode.FUTURES, 8), + ("binance", TradingMode.FUTURES, 2), + ("kraken", TradingMode.FUTURES, 3), + ("ftx", TradingMode.FUTURES, 9), ]) def test_update_funding_fees(mocker, default_conf, exchange, trading_mode, calls): patch_RPCManager(mocker) - patch_exchange(mocker) + patch_exchange(mocker, id=exchange) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_funding_fees', return_value=True) + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = 'isolated' freqtrade = get_patched_freqtradebot(mocker, default_conf) with time_machine.travel("2021-09-01 00:00:00 +00:00") as t: @@ -4314,5 +4317,6 @@ def test_update_funding_fees(mocker, default_conf, exchange, trading_mode, calls # ) t.move_to("2021-09-01 08:00:00 +00:00") + schedule.run_pending() assert freqtrade.update_funding_fees.call_count == calls From a4a5c1aad0b4a281c0821305e49a8941c3400580 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Oct 2021 07:05:34 +0200 Subject: [PATCH 39/59] Fix scheduling test (a little bit) --- tests/test_freqtradebot.py | 42 ++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0e849f5ad..11463f0ee 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -6,7 +6,6 @@ import time from copy import deepcopy from math import isclose from unittest.mock import ANY, MagicMock, PropertyMock -import time_machine import schedule import arrow @@ -4289,7 +4288,8 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: ("kraken", TradingMode.FUTURES, 3), ("ftx", TradingMode.FUTURES, 9), ]) -def test_update_funding_fees(mocker, default_conf, exchange, trading_mode, calls): +def test_update_funding_fees(mocker, default_conf, exchange, trading_mode, calls, time_machine): + time_machine.move_to("2021-09-01 00:00:00 +00:00") patch_RPCManager(mocker) patch_exchange(mocker, id=exchange) @@ -4298,25 +4298,23 @@ def test_update_funding_fees(mocker, default_conf, exchange, trading_mode, calls default_conf['collateral'] = 'isolated' freqtrade = get_patched_freqtradebot(mocker, default_conf) - with time_machine.travel("2021-09-01 00:00:00 +00:00") as t: + # trade = Trade( + # id=2, + # pair='ADA/USDT', + # stake_amount=60.0, + # open_rate=2.0, + # amount=30.0, + # is_open=True, + # open_date=arrow.utcnow().datetime, + # fee_open=fee.return_value, + # fee_close=fee.return_value, + # exchange='binance', + # is_short=False, + # leverage=3.0, + # trading_mode=trading_mode + # ) - # trade = Trade( - # id=2, - # pair='ADA/USDT', - # stake_amount=60.0, - # open_rate=2.0, - # amount=30.0, - # is_open=True, - # open_date=arrow.utcnow().datetime, - # fee_open=fee.return_value, - # fee_close=fee.return_value, - # exchange='binance', - # is_short=False, - # leverage=3.0, - # trading_mode=trading_mode - # ) + time_machine.move_to("2021-09-01 08:00:00 +00:00") + schedule.run_pending() - t.move_to("2021-09-01 08:00:00 +00:00") - schedule.run_pending() - - assert freqtrade.update_funding_fees.call_count == calls + assert freqtrade.update_funding_fees.call_count == calls From e367f84b06896304405a8370f686538ee3c635ac Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 6 Oct 2021 01:39:02 -0600 Subject: [PATCH 40/59] Added more update_funding_fee tests, set exchange of default conf --- freqtrade/exchange/binance.py | 1 + freqtrade/exchange/bybit.py | 9 +++++++- freqtrade/exchange/ftx.py | 2 +- freqtrade/freqtradebot.py | 9 ++++++-- tests/test_freqtradebot.py | 41 +++++++++++++---------------------- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index dc3d4bb5e..d23f84e7b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -29,6 +29,7 @@ class Binance(Exchange): "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } funding_fee_times: List[int] = [0, 8, 16] # hours of the day + # but the schedule won't check within this timeframe _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index c4ffcdd0b..df19a671b 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -1,7 +1,8 @@ """ Bybit exchange subclass """ import logging -from typing import Dict, List +from typing import Dict, List, Tuple +from freqtrade.enums import Collateral, TradingMode from freqtrade.exchange import Exchange @@ -23,3 +24,9 @@ class Bybit(Exchange): } funding_fee_times: List[int] = [0, 8, 16] # hours of the day + + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # TradingMode.SPOT always supported and not required in this list + # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported + # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported + ] diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index ef583de4f..5072d653e 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -21,7 +21,7 @@ class Ftx(Exchange): "stoploss_on_exchange": True, "ohlcv_candle_limit": 1500, } - funding_fee_times: List[int] = list(range(0, 23)) + funding_fee_times: List[int] = list(range(0, 24)) _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8307dd185..d6734fa43 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -111,10 +111,15 @@ class FreqtradeBot(LoggingMixin): self.trading_mode = TradingMode.SPOT if self.trading_mode == TradingMode.FUTURES: - for time_slot in self.exchange.funding_fee_times: - schedule.every().day.at(str(time(time_slot))).do(self.update_funding_fees) + + def update(): + self.update_funding_fees() self.wallets.update() + for time_slot in self.exchange.funding_fee_times: + t = str(time(time_slot)) + schedule.every().day.at(t).do(update) + def notify_status(self, msg: str) -> None: """ Public method for users of this class (worker, etc.) to send notifications diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 11463f0ee..2353c9f14 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -6,10 +6,10 @@ import time from copy import deepcopy from math import isclose from unittest.mock import ANY, MagicMock, PropertyMock -import schedule import arrow import pytest +import schedule from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT from freqtrade.enums import RPCMessageType, RunMode, SellType, State, TradingMode @@ -4281,40 +4281,29 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: assert valid_price_at_min_alwd < proposed_price -@pytest.mark.parametrize('exchange,trading_mode,calls', [ - ("ftx", TradingMode.SPOT, 0), - ("ftx", TradingMode.MARGIN, 0), - ("binance", TradingMode.FUTURES, 2), - ("kraken", TradingMode.FUTURES, 3), - ("ftx", TradingMode.FUTURES, 9), +@pytest.mark.parametrize('exchange,trading_mode,calls,t1,t2', [ + ("ftx", TradingMode.SPOT, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), + ("ftx", TradingMode.MARGIN, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), + ("binance", TradingMode.FUTURES, 1, "2021-09-01 00:00:01", "2021-09-01 08:00:00"), + ("kraken", TradingMode.FUTURES, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00"), + ("ftx", TradingMode.FUTURES, 8, "2021-09-01 00:00:01", "2021-09-01 08:00:00"), + ("binance", TradingMode.FUTURES, 2, "2021-08-31 23:59:59", "2021-09-01 08:00:01"), + ("kraken", TradingMode.FUTURES, 3, "2021-08-31 23:59:59", "2021-09-01 08:00:01"), + ("ftx", TradingMode.FUTURES, 9, "2021-08-31 23:59:59", "2021-09-01 08:00:01"), ]) -def test_update_funding_fees(mocker, default_conf, exchange, trading_mode, calls, time_machine): - time_machine.move_to("2021-09-01 00:00:00 +00:00") +def test_update_funding_fees(mocker, default_conf, exchange, trading_mode, calls, time_machine, + t1, t2): + time_machine.move_to(f"{t1} +00:00") patch_RPCManager(mocker) patch_exchange(mocker, id=exchange) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_funding_fees', return_value=True) default_conf['trading_mode'] = trading_mode default_conf['collateral'] = 'isolated' + default_conf['exchange']['name'] = exchange freqtrade = get_patched_freqtradebot(mocker, default_conf) - # trade = Trade( - # id=2, - # pair='ADA/USDT', - # stake_amount=60.0, - # open_rate=2.0, - # amount=30.0, - # is_open=True, - # open_date=arrow.utcnow().datetime, - # fee_open=fee.return_value, - # fee_close=fee.return_value, - # exchange='binance', - # is_short=False, - # leverage=3.0, - # trading_mode=trading_mode - # ) - - time_machine.move_to("2021-09-01 08:00:00 +00:00") + time_machine.move_to(f"{t2} +00:00") schedule.run_pending() assert freqtrade.update_funding_fees.call_count == calls From 39be675f1f1e4da03619b6e3dc99c2953cecd63e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 9 Oct 2021 10:39:14 -0600 Subject: [PATCH 41/59] Adjusted time to utc in schedule --- freqtrade/freqtradebot.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d6734fa43..9b8018515 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime, time, timezone +from datetime import datetime, time, timedelta, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -117,9 +117,20 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() for time_slot in self.exchange.funding_fee_times: - t = str(time(time_slot)) + t = str(time(self.utc_hour_to_local(time_slot))) schedule.every().day.at(t).do(update) + def utc_hour_to_local(self, hour): + local_timezone = datetime.now( + timezone.utc).astimezone().tzinfo + local_time = datetime.now(local_timezone) + offset = local_time.utcoffset().total_seconds() + td = timedelta(seconds=offset) + t = datetime.strptime(f'26 Sep 2021 {hour}:00:00', '%d %b %Y %H:%M:%S') + utc = t + td + print(hour, utc) + return int(utc.strftime("%H").lstrip("0") or 0) + def notify_status(self, msg: str) -> None: """ Public method for users of this class (worker, etc.) to send notifications From 795d51b68ca7c3b90b8d44b01f93043e81fd560c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 9 Oct 2021 11:27:26 -0600 Subject: [PATCH 42/59] Switched scheduler to get funding fees every hour for any exchange --- freqtrade/freqtradebot.py | 17 +++-------------- tests/test_freqtradebot.py | 19 +++++++------------ 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9b8018515..d389750dd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime, time, timedelta, timezone +from datetime import datetime, time, timezone, timedelta from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -116,21 +116,10 @@ class FreqtradeBot(LoggingMixin): self.update_funding_fees() self.wallets.update() - for time_slot in self.exchange.funding_fee_times: - t = str(time(self.utc_hour_to_local(time_slot))) + for time_slot in range(0, 24): + t = str(time(time_slot)) schedule.every().day.at(t).do(update) - def utc_hour_to_local(self, hour): - local_timezone = datetime.now( - timezone.utc).astimezone().tzinfo - local_time = datetime.now(local_timezone) - offset = local_time.utcoffset().total_seconds() - td = timedelta(seconds=offset) - t = datetime.strptime(f'26 Sep 2021 {hour}:00:00', '%d %b %Y %H:%M:%S') - utc = t + td - print(hour, utc) - return int(utc.strftime("%H").lstrip("0") or 0) - def notify_status(self, msg: str) -> None: """ Public method for users of this class (worker, etc.) to send notifications diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2353c9f14..57ab363dd 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4281,26 +4281,21 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: assert valid_price_at_min_alwd < proposed_price -@pytest.mark.parametrize('exchange,trading_mode,calls,t1,t2', [ - ("ftx", TradingMode.SPOT, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), - ("ftx", TradingMode.MARGIN, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), - ("binance", TradingMode.FUTURES, 1, "2021-09-01 00:00:01", "2021-09-01 08:00:00"), - ("kraken", TradingMode.FUTURES, 2, "2021-09-01 00:00:01", "2021-09-01 08:00:00"), - ("ftx", TradingMode.FUTURES, 8, "2021-09-01 00:00:01", "2021-09-01 08:00:00"), - ("binance", TradingMode.FUTURES, 2, "2021-08-31 23:59:59", "2021-09-01 08:00:01"), - ("kraken", TradingMode.FUTURES, 3, "2021-08-31 23:59:59", "2021-09-01 08:00:01"), - ("ftx", TradingMode.FUTURES, 9, "2021-08-31 23:59:59", "2021-09-01 08:00:01"), +@pytest.mark.parametrize('trading_mode,calls,t1,t2', [ + (TradingMode.SPOT, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), + (TradingMode.MARGIN, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), + (TradingMode.FUTURES, 8, "2021-09-01 00:00:01", "2021-09-01 08:00:00"), + (TradingMode.FUTURES, 9, "2021-08-31 23:59:59", "2021-09-01 08:00:01"), ]) -def test_update_funding_fees(mocker, default_conf, exchange, trading_mode, calls, time_machine, +def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_machine, t1, t2): time_machine.move_to(f"{t1} +00:00") patch_RPCManager(mocker) - patch_exchange(mocker, id=exchange) + patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_funding_fees', return_value=True) default_conf['trading_mode'] = trading_mode default_conf['collateral'] = 'isolated' - default_conf['exchange']['name'] = exchange freqtrade = get_patched_freqtradebot(mocker, default_conf) time_machine.move_to(f"{t2} +00:00") From 057b048f31a10cf96b1d0f6bd87c4e60feb6af37 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 9 Oct 2021 12:24:25 -0600 Subject: [PATCH 43/59] Started added timezone offset stuff --- freqtrade/freqtradebot.py | 23 +++++++++++++++++++++-- tests/test_freqtradebot.py | 26 ++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d389750dd..2673feed1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime, time, timezone, timedelta +from datetime import datetime, time, timezone, timedelta, tzinfo from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -116,10 +116,29 @@ class FreqtradeBot(LoggingMixin): self.update_funding_fees() self.wallets.update() + local_timezone = datetime.now( + timezone.utc).astimezone().tzinfo + minutes = self.time_zone_minutes(local_timezone) for time_slot in range(0, 24): - t = str(time(time_slot)) + t = str(time(time_slot, minutes)) schedule.every().day.at(t).do(update) + def time_zone_minutes(self, local_timezone): + """ + Returns the minute offset of a timezone + :param local_timezone: The operating systems timezone + """ + local_time = datetime.now(local_timezone) + offset = local_time.utcoffset().total_seconds() + half_hour_tz = (offset * 2) % 2 != 0.0 + quart_hour_tz = (offset * 4) % 4 != 0.0 + if quart_hour_tz: + return 45 + elif half_hour_tz: + return 30 + else: + return 0 + def notify_status(self, msg: str) -> None: """ Public method for users of this class (worker, etc.) to send notifications diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 57ab363dd..9b83c8595 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4,6 +4,7 @@ import logging import time from copy import deepcopy +# from datetime import tzinfo from math import isclose from unittest.mock import ANY, MagicMock, PropertyMock @@ -4302,3 +4303,28 @@ def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_mac schedule.run_pending() assert freqtrade.update_funding_fees.call_count == calls + + +@pytest.mark.parametrize('tz,minute_offset', [ + ('IST', 30), + ('ACST', 30), + ('ACWST', 45), + ('ACST', 30), + ('ACDT', 30), + ('CCT', 30), + ('CHAST', 45), + ('NST', 30), + ('IST', 30), + ('AFT', 30), + ('IRST', 30), + ('IRDT', 30), + ('MMT', 30), + ('NPT', 45), + ('MART', 30), +]) +def test_time_zone_minutes(mocker, default_conf, tz, minute_offset): + patch_RPCManager(mocker) + patch_exchange(mocker) + freqtrade = get_patched_freqtradebot(mocker, default_conf) + return freqtrade + # freqtrade.time_zone_minutes(tzinfo('IST')) From b83933a10a82fb5570dc5081461042f63fc19aba Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 9 Oct 2021 12:36:00 -0600 Subject: [PATCH 44/59] Added gateio and kucoin funding fee times --- environment.yml | 1 - freqtrade/exchange/gateio.py | 4 +++- freqtrade/exchange/kucoin.py | 4 +++- freqtrade/freqtradebot.py | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/environment.yml b/environment.yml index 780fda7fb..fa71b5fe9 100644 --- a/environment.yml +++ b/environment.yml @@ -59,7 +59,6 @@ dependencies: - plotly - jupyter - - pip: - pycoingecko - py_find_1st diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index e6ee01c8a..cb6b7a2ac 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -1,6 +1,6 @@ """ Gate.io exchange subclass """ import logging -from typing import Dict +from typing import Dict, List from freqtrade.exchange import Exchange @@ -23,3 +23,5 @@ class Gateio(Exchange): } _headers = {'X-Gate-Channel-Id': 'freqtrade'} + + funding_fee_times: List[int] = [0, 8, 16] # hours of the day diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 5d818f6a2..51de75ea4 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -1,6 +1,6 @@ """ Kucoin exchange subclass """ import logging -from typing import Dict +from typing import Dict, List from freqtrade.exchange import Exchange @@ -24,3 +24,5 @@ class Kucoin(Exchange): "order_time_in_force": ['gtc', 'fok', 'ioc'], "time_in_force_parameter": "timeInForce", } + + funding_fee_times: List[int] = [4, 12, 20] # hours of the day diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2673feed1..f104de56f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime, time, timezone, timedelta, tzinfo +from datetime import datetime, time, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional From 95be5121ec439f2508e67424c7cc0f4b45c28593 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 9 Oct 2021 13:14:41 -0600 Subject: [PATCH 45/59] Added bibox and hitbtc funding fee times --- freqtrade/exchange/bibox.py | 4 +++- freqtrade/exchange/hitbtc.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index 074dd2b10..e0741e34a 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -1,6 +1,6 @@ """ Bibox exchange subclass """ import logging -from typing import Dict +from typing import Dict, List from freqtrade.exchange import Exchange @@ -24,3 +24,5 @@ class Bibox(Exchange): def _ccxt_config(self) -> Dict: # Parameters to add directly to ccxt sync/async initialization. return {"has": {"fetchCurrencies": False}} + + funding_fee_times: List[int] = [0, 8, 16] # hours of the day diff --git a/freqtrade/exchange/hitbtc.py b/freqtrade/exchange/hitbtc.py index a48c9a198..8e0a009f0 100644 --- a/freqtrade/exchange/hitbtc.py +++ b/freqtrade/exchange/hitbtc.py @@ -1,5 +1,5 @@ import logging -from typing import Dict +from typing import Dict, List from freqtrade.exchange import Exchange @@ -21,3 +21,5 @@ class Hitbtc(Exchange): "ohlcv_candle_limit": 1000, "ohlcv_params": {"sort": "DESC"} } + + funding_fee_times: List[int] = [0, 8, 16] # hours of the day From 3b962433fbae2ce0b47ec3638614b2fc8066a16b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 9 Oct 2021 17:48:53 -0600 Subject: [PATCH 46/59] Switched shcedule to perform every 15 minutes --- freqtrade/freqtradebot.py | 26 +++++--------------------- tests/test_freqtradebot.py | 29 ++--------------------------- 2 files changed, 7 insertions(+), 48 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f104de56f..50e5c1415 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -116,28 +116,12 @@ class FreqtradeBot(LoggingMixin): self.update_funding_fees() self.wallets.update() - local_timezone = datetime.now( - timezone.utc).astimezone().tzinfo - minutes = self.time_zone_minutes(local_timezone) + # 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 for time_slot in range(0, 24): - t = str(time(time_slot, minutes)) - schedule.every().day.at(t).do(update) - - def time_zone_minutes(self, local_timezone): - """ - Returns the minute offset of a timezone - :param local_timezone: The operating systems timezone - """ - local_time = datetime.now(local_timezone) - offset = local_time.utcoffset().total_seconds() - half_hour_tz = (offset * 2) % 2 != 0.0 - quart_hour_tz = (offset * 4) % 4 != 0.0 - if quart_hour_tz: - return 45 - elif half_hour_tz: - return 30 - else: - return 0 + for minutes in [0, 15, 30, 45]: + t = str(time(time_slot, minutes)) + schedule.every().day.at(t).do(update) def notify_status(self, msg: str) -> None: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9b83c8595..a69414dfc 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4285,8 +4285,8 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: @pytest.mark.parametrize('trading_mode,calls,t1,t2', [ (TradingMode.SPOT, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), (TradingMode.MARGIN, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), - (TradingMode.FUTURES, 8, "2021-09-01 00:00:01", "2021-09-01 08:00:00"), - (TradingMode.FUTURES, 9, "2021-08-31 23:59:59", "2021-09-01 08:00:01"), + (TradingMode.FUTURES, 32, "2021-09-01 00:00:01", "2021-09-01 08:00:00"), + (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:01"), ]) def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_machine, t1, t2): @@ -4303,28 +4303,3 @@ def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_mac schedule.run_pending() assert freqtrade.update_funding_fees.call_count == calls - - -@pytest.mark.parametrize('tz,minute_offset', [ - ('IST', 30), - ('ACST', 30), - ('ACWST', 45), - ('ACST', 30), - ('ACDT', 30), - ('CCT', 30), - ('CHAST', 45), - ('NST', 30), - ('IST', 30), - ('AFT', 30), - ('IRST', 30), - ('IRDT', 30), - ('MMT', 30), - ('NPT', 45), - ('MART', 30), -]) -def test_time_zone_minutes(mocker, default_conf, tz, minute_offset): - patch_RPCManager(mocker) - patch_exchange(mocker) - freqtrade = get_patched_freqtradebot(mocker, default_conf) - return freqtrade - # freqtrade.time_zone_minutes(tzinfo('IST')) From 855b26f846fdda1eeb3314db2218a87835519eb1 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 11 Oct 2021 01:31:21 -0600 Subject: [PATCH 47/59] Parametrized more time machine tests in test_update_funding_fees --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 50e5c1415..f2297833e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -120,7 +120,7 @@ class FreqtradeBot(LoggingMixin): # TODO: funding interval, specified by funding_fee_times on the exchange classes for time_slot in range(0, 24): for minutes in [0, 15, 30, 45]: - t = str(time(time_slot, minutes)) + t = str(time(time_slot, minutes, 2)) schedule.every().day.at(t).do(update) def notify_status(self, msg: str) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a69414dfc..f7b0808b1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4,7 +4,6 @@ import logging import time from copy import deepcopy -# from datetime import tzinfo from math import isclose from unittest.mock import ANY, MagicMock, PropertyMock @@ -4285,8 +4284,12 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: @pytest.mark.parametrize('trading_mode,calls,t1,t2', [ (TradingMode.SPOT, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), (TradingMode.MARGIN, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), - (TradingMode.FUTURES, 32, "2021-09-01 00:00:01", "2021-09-01 08:00:00"), - (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:01"), + (TradingMode.FUTURES, 31, "2021-09-01 00:00:02", "2021-09-01 08:00:01"), + # (TradingMode.FUTURES, 32, "2021-09-01 00:00:01", "2021-09-01 08:00:01"), + (TradingMode.FUTURES, 33, "2021-09-01 00:00:01", "2021-09-01 08:00:02"), + (TradingMode.FUTURES, 33, "2021-09-01 00:00:00", "2021-09-01 08:00:02"), + (TradingMode.FUTURES, 34, "2021-08-31 23:59:59", "2021-09-01 08:00:02"), + (TradingMode.FUTURES, 34, "2021-08-31 23:59:59", "2021-09-01 08:00:03"), ]) def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_machine, t1, t2): From d5a1385fdc1d1aa35c7bb6cf6f230c3fcd6fa24f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 11 Oct 2021 04:14:59 -0600 Subject: [PATCH 48/59] Changes described on github --- freqtrade/freqtradebot.py | 2 +- tests/exchange/test_exchange.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f2297833e..bd4e8b9b8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -106,7 +106,7 @@ class FreqtradeBot(LoggingMixin): LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) if 'trading_mode' in self.config: - self.trading_mode = self.config['trading_mode'] + self.trading_mode = TradingMode(self.config['trading_mode']) else: self.trading_mode = TradingMode.SPOT diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 95a91f7cc..0f8c35e1b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3053,36 +3053,36 @@ def test_get_funding_fees_from_exchange(default_conf, mocker, exchange_name): api_mock = MagicMock() api_mock.fetch_funding_history = MagicMock(return_value=[ { - 'amount': 0.14542341, + 'amount': 0.14542, 'code': 'USDT', 'datetime': '2021-09-01T08:00:01.000Z', 'id': '485478', 'info': {'asset': 'USDT', - 'income': '0.14542341', + 'income': '0.14542', 'incomeType': 'FUNDING_FEE', 'info': 'FUNDING_FEE', 'symbol': 'XRPUSDT', - 'time': '1630512001000', + 'time': '1630382001000', 'tradeId': '', - 'tranId': '4854789484855218760'}, + 'tranId': '993203'}, 'symbol': 'XRP/USDT', - 'timestamp': 1630512001000 + 'timestamp': 1630382001000 }, { - 'amount': -0.14642341, + 'amount': -0.14642, 'code': 'USDT', 'datetime': '2021-09-01T16:00:01.000Z', 'id': '485479', 'info': {'asset': 'USDT', - 'income': '-0.14642341', + 'income': '-0.14642', 'incomeType': 'FUNDING_FEE', 'info': 'FUNDING_FEE', 'symbol': 'XRPUSDT', - 'time': '1630512001000', + 'time': '1630314001000', 'tradeId': '', - 'tranId': '4854789484855218760'}, + 'tranId': '993204'}, 'symbol': 'XRP/USDT', - 'timestamp': 1630512001000 + 'timestamp': 1630314001000 } ]) type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) From ae3688a18a114e32cbd9a7ca7fbdf674d10c9c5c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 11 Oct 2021 05:56:27 -0600 Subject: [PATCH 49/59] Updated LocalTrade.calc_close_trade_value formula for shorting futures --- freqtrade/persistence/models.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 50f4931d6..6614de34e 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -709,7 +709,10 @@ class LocalTrade(): elif (trading_mode == TradingMode.FUTURES): funding_fees = self.funding_fees or 0.0 - return float(self._calc_base_close(amount, rate, fee)) + funding_fees + if self.is_short: + return float(self._calc_base_close(amount, rate, fee)) - funding_fees + else: + return float(self._calc_base_close(amount, rate, fee)) + funding_fees else: raise OperationalException( f"{self.trading_mode.value} trading is not yet available using freqtrade") From 01a9e90057836727b8d08b8ebc04a51dd2c79ccc Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 11 Oct 2021 07:03:14 -0600 Subject: [PATCH 50/59] Added futures tests to test_persistence.test_calc_profit --- tests/test_persistence.py | 201 ++++++++++++++++++++++++++++++++------ 1 file changed, 169 insertions(+), 32 deletions(-) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 7724df957..7fa04ed54 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -18,7 +18,7 @@ from tests.conftest import (create_mock_trades, create_mock_trades_with_leverage log_has, log_has_re) -spot, margin = TradingMode.SPOT, TradingMode.MARGIN +spot, margin, futures = TradingMode.SPOT, TradingMode.MARGIN, TradingMode.FUTURES def test_init_create_session(default_conf): @@ -186,6 +186,13 @@ def test_set_stop_loss_isolated_liq(fee): ("binance", False, 1, 295, 0.0005, 0.0, spot), ("binance", True, 1, 295, 0.0005, 0.003125, margin), + # ("binance", False, 3, 10, 0.0005, 0.0, futures), + # ("binance", True, 3, 295, 0.0005, 0.0, futures), + # ("binance", False, 5, 295, 0.0005, 0.0, futures), + # ("binance", True, 5, 295, 0.0005, 0.0, futures), + # ("binance", False, 1, 295, 0.0005, 0.0, futures), + # ("binance", True, 1, 295, 0.0005, 0.0, futures), + ("kraken", False, 3, 10, 0.0005, 0.040, margin), ("kraken", True, 3, 10, 0.0005, 0.030, margin), ("kraken", False, 3, 295, 0.0005, 0.06, margin), @@ -277,6 +284,8 @@ def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, (True, 1.0, 30.0, margin), (False, 3.0, 40.0, margin), (True, 3.0, 30.0, margin), + # (False, 3.0, 0.0, futures), + # (True, 3.0, 0.0, futures), ]) @pytest.mark.usefixtures("init_persistence") def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, @@ -535,10 +544,16 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, ("binance", True, 1, 59.850, 66.1663784375, -6.316378437500013, -0.105536815998329, margin), ("binance", False, 3, 60.15, 65.83416667, 5.684166670000003, 0.2834995845386534, margin), ("binance", True, 3, 59.85, 66.1663784375, -6.316378437500013, -0.3166104479949876, margin), + ("kraken", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot), ("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.106619298245614, margin), ("kraken", False, 3, 60.15, 65.795, 5.645, 0.2815461346633419, margin), ("kraken", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842, margin), + + # TODO-lev + # ("binance", True, 1, 59.850, 66.1663784375, -6.316378437500013, -0.105536815998329, futures), + # ("binance", False, 3, 60.15, 65.83416667, 5.684166670000003, 0.2834995845386534, futures), + # ("binance", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842, futures), ]) @pytest.mark.usefixtures("init_persistence") def test_calc_open_close_trade_price( @@ -666,7 +681,7 @@ def test_update_invalid_order(limit_buy_order_usdt): @pytest.mark.parametrize('exchange', ['binance', 'kraken']) -@pytest.mark.parametrize('trading_mode', [spot, margin]) +@pytest.mark.parametrize('trading_mode', [spot, margin, futures]) @pytest.mark.parametrize('lev', [1, 3]) @pytest.mark.parametrize('is_short,fee_rate,result', [ (False, 0.003, 60.18), @@ -738,6 +753,11 @@ def test_calc_open_trade_value( ('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719, margin), ('kraken', True, 1, 2.2, 2.5, 0.0025, 75.2626875, margin), ('kraken', True, 1, 2.2, 2.5, 0.003, 75.300225, margin), + + # TODO-lev + # ('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667, futures), + # ('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719, futures), + # ('binance', True, 1, 2.2, 2.5, 0.0025, 75.2626875, futures), ]) @pytest.mark.usefixtures("init_persistence") def test_calc_close_trade_price( @@ -763,40 +783,73 @@ def test_calc_close_trade_price( @pytest.mark.parametrize( - 'exchange,is_short,lev,close_rate,fee_close,profit,profit_ratio,trading_mode', [ - ('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot), - ('binance', False, 3, 2.1, 0.0025, 2.69166667, 0.13424771421446402, margin), - ('binance', True, 1, 2.1, 0.0025, -3.308815781249997, -0.05528514254385963, margin), - ('binance', True, 3, 2.1, 0.0025, -3.308815781249997, -0.1658554276315789, margin), + 'exchange,is_short,lev,close_rate,fee_close,profit,profit_ratio,trading_mode,funding_fees', [ + ('binance', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot, 0), + ('binance', False, 3, 2.1, 0.0025, 2.69166667, 0.13424771421446402, margin, 0), + ('binance', True, 1, 2.1, 0.0025, -3.308815781249997, -0.05528514254385963, margin, 0), + ('binance', True, 3, 2.1, 0.0025, -3.308815781249997, -0.1658554276315789, margin, 0), - ('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin), - ('binance', False, 3, 1.9, 0.0025, -3.29333333, -0.16425602643391513, margin), - ('binance', True, 1, 1.9, 0.0025, 2.7063095312499996, 0.045218204365079395, margin), - ('binance', True, 3, 1.9, 0.0025, 2.7063095312499996, 0.13565461309523819, margin), + ('binance', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin, 0), + ('binance', False, 3, 1.9, 0.0025, -3.29333333, -0.16425602643391513, margin, 0), + ('binance', True, 1, 1.9, 0.0025, 2.7063095312499996, 0.045218204365079395, margin, 0), + ('binance', True, 3, 1.9, 0.0025, 2.7063095312499996, 0.13565461309523819, margin, 0), - ('binance', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin), - ('binance', False, 3, 2.2, 0.0025, 5.68416667, 0.2834995845386534, margin), - ('binance', True, 1, 2.2, 0.0025, -6.316378437499999, -0.1055368159983292, margin), - ('binance', True, 3, 2.2, 0.0025, -6.316378437499999, -0.3166104479949876, margin), + ('binance', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin, 0), + ('binance', False, 3, 2.2, 0.0025, 5.68416667, 0.2834995845386534, margin, 0), + ('binance', True, 1, 2.2, 0.0025, -6.316378437499999, -0.1055368159983292, margin, 0), + ('binance', True, 3, 2.2, 0.0025, -6.316378437499999, -0.3166104479949876, margin, 0), - ('kraken', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot), - ('kraken', False, 3, 2.1, 0.0025, 2.6525, 0.13229426433915248, margin), - ('kraken', True, 1, 2.1, 0.0025, -3.3706575, -0.05631842105263152, margin), - ('kraken', True, 3, 2.1, 0.0025, -3.3706575, -0.16895526315789455, margin), + # # Kraken + ('kraken', False, 1, 2.1, 0.0025, 2.6925, 0.04476309226932673, spot, 0), + ('kraken', False, 3, 2.1, 0.0025, 2.6525, 0.13229426433915248, margin, 0), + ('kraken', True, 1, 2.1, 0.0025, -3.3706575, -0.05631842105263152, margin, 0), + ('kraken', True, 3, 2.1, 0.0025, -3.3706575, -0.16895526315789455, margin, 0), - ('kraken', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin), - ('kraken', False, 3, 1.9, 0.0025, -3.3325, -0.16620947630922667, margin), - ('kraken', True, 1, 1.9, 0.0025, 2.6503575, 0.04428333333333334, margin), - ('kraken', True, 3, 1.9, 0.0025, 2.6503575, 0.13285000000000002, margin), + ('kraken', False, 1, 1.9, 0.0025, -3.2925, -0.05473815461346632, margin, 0), + ('kraken', False, 3, 1.9, 0.0025, -3.3325, -0.16620947630922667, margin, 0), + ('kraken', True, 1, 1.9, 0.0025, 2.6503575, 0.04428333333333334, margin, 0), + ('kraken', True, 3, 1.9, 0.0025, 2.6503575, 0.13285000000000002, margin, 0), - ('kraken', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin), - ('kraken', False, 3, 2.2, 0.0025, 5.645, 0.2815461346633419, margin), - ('kraken', True, 1, 2.2, 0.0025, -6.381165, -0.106619298245614, margin), - ('kraken', True, 3, 2.2, 0.0025, -6.381165, -0.319857894736842, margin), + ('kraken', False, 1, 2.2, 0.0025, 5.685, 0.0945137157107232, margin, 0), + ('kraken', False, 3, 2.2, 0.0025, 5.645, 0.2815461346633419, margin, 0), + ('kraken', True, 1, 2.2, 0.0025, -6.381165, -0.106619298245614, margin, 0), + ('kraken', True, 3, 2.2, 0.0025, -6.381165, -0.319857894736842, margin, 0), - ('binance', False, 1, 2.1, 0.003, 2.6610000000000014, 0.04423940149625927, spot), - ('binance', False, 1, 1.9, 0.003, -3.320999999999998, -0.05521197007481293, spot), - ('binance', False, 1, 2.2, 0.003, 5.652000000000008, 0.09396508728179565, spot), + ('binance', False, 1, 2.1, 0.003, 2.6610000000000014, 0.04423940149625927, spot, 0), + ('binance', False, 1, 1.9, 0.003, -3.320999999999998, -0.05521197007481293, spot, 0), + ('binance', False, 1, 2.2, 0.003, 5.652000000000008, 0.09396508728179565, spot, 0), + + # # FUTURES, funding_fee=1 + ('binance', False, 1, 2.1, 0.0025, 3.6925, 0.06138819617622615, futures, 1), + ('binance', False, 3, 2.1, 0.0025, 3.6925, 0.18416458852867845, futures, 1), + ('binance', True, 1, 2.1, 0.0025, -2.3074999999999974, -0.038554720133667564, futures, 1), + ('binance', True, 3, 2.1, 0.0025, -2.3074999999999974, -0.11566416040100269, futures, 1), + + ('binance', False, 1, 1.9, 0.0025, -2.2925, -0.0381130507065669, futures, 1), + ('binance', False, 3, 1.9, 0.0025, -2.2925, -0.1143391521197007, futures, 1), + ('binance', True, 1, 1.9, 0.0025, 3.707500000000003, 0.06194653299916464, futures, 1), + ('binance', True, 3, 1.9, 0.0025, 3.707500000000003, 0.18583959899749392, futures, 1), + + ('binance', False, 1, 2.2, 0.0025, 6.685, 0.11113881961762262, futures, 1), + ('binance', False, 3, 2.2, 0.0025, 6.685, 0.33341645885286786, futures, 1), + ('binance', True, 1, 2.2, 0.0025, -5.315000000000005, -0.08880534670008355, futures, 1), + ('binance', True, 3, 2.2, 0.0025, -5.315000000000005, -0.26641604010025066, futures, 1), + + # FUTURES, funding_fee=-1 + ('binance', False, 1, 2.1, 0.0025, 1.6925000000000026, 0.028137988362427313, futures, -1), + ('binance', False, 3, 2.1, 0.0025, 1.6925000000000026, 0.08441396508728194, futures, -1), + ('binance', True, 1, 2.1, 0.0025, -4.307499999999997, -0.07197159565580624, futures, -1), + ('binance', True, 3, 2.1, 0.0025, -4.307499999999997, -0.21591478696741873, futures, -1), + + ('binance', False, 1, 1.9, 0.0025, -4.292499999999997, -0.07136325852036574, futures, -1), + ('binance', False, 3, 1.9, 0.0025, -4.292499999999997, -0.2140897755610972, futures, -1), + ('binance', True, 1, 1.9, 0.0025, 1.7075000000000031, 0.02852965747702596, futures, -1), + ('binance', True, 3, 1.9, 0.0025, 1.7075000000000031, 0.08558897243107788, futures, -1), + + ('binance', False, 1, 2.2, 0.0025, 4.684999999999995, 0.07788861180382378, futures, -1), + ('binance', False, 3, 2.2, 0.0025, 4.684999999999995, 0.23366583541147135, futures, -1), + ('binance', True, 1, 2.2, 0.0025, -7.315000000000005, -0.12222222222222223, futures, -1), + ('binance', True, 3, 2.2, 0.0025, -7.315000000000005, -0.3666666666666667, futures, -1), ]) @pytest.mark.usefixtures("init_persistence") def test_calc_profit( @@ -810,7 +863,8 @@ def test_calc_profit( fee_close, profit, profit_ratio, - trading_mode + trading_mode, + funding_fees ): """ 10 minute limit trade on Binance/Kraken at 1x, 3x leverage @@ -829,6 +883,7 @@ def test_calc_profit( 1x,-1x: 60.0 quote 3x,-3x: 20.0 quote hours: 1/6 (10 minutes) + funding_fees: 1 borrowed 1x: 0 quote 3x: 40 quote @@ -940,6 +995,87 @@ def test_calc_profit( 2.1 quote: (62.811 / 60.15) - 1 = 0.04423940149625927 1.9 quote: (56.829 / 60.15) - 1 = -0.05521197007481293 2.2 quote: (65.802 / 60.15) - 1 = 0.09396508728179565 + futures (live): + funding_fee: 1 + close_value: + equations: + 1x,3x: (amount * close_rate) - (amount * close_rate * fee) + funding_fees + -1x,-3x: (amount * close_rate) + (amount * close_rate * fee) - funding_fees + 2.1 quote + 1x,3x: (30.00 * 2.1) - (30.00 * 2.1 * 0.0025) + 1 = 63.8425 + -1x,-3x: (30.00 * 2.1) + (30.00 * 2.1 * 0.0025) - 1 = 62.1575 + 1.9 quote + 1x,3x: (30.00 * 1.9) - (30.00 * 1.9 * 0.0025) + 1 = 57.8575 + -1x,-3x: (30.00 * 1.9) + (30.00 * 1.9 * 0.0025) - 1 = 56.1425 + 2.2 quote: + 1x,3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) + 1 = 66.835 + -1x,-3x: (30.00 * 2.20) + (30.00 * 2.20 * 0.0025) - 1 = 65.165 + total_profit: + 2.1 quote + 1x,3x: 63.8425 - 60.15 = 3.6925 + -1x,-3x: 59.850 - 62.1575 = -2.3074999999999974 + 1.9 quote + 1x,3x: 57.8575 - 60.15 = -2.2925 + -1x,-3x: 59.850 - 56.1425 = 3.707500000000003 + 2.2 quote: + 1x,3x: 66.835 - 60.15 = 6.685 + -1x,-3x: 59.850 - 65.165 = -5.315000000000005 + total_profit_ratio: + 2.1 quote + 1x: (63.8425 / 60.15) - 1 = 0.06138819617622615 + 3x: ((63.8425 / 60.15) - 1)*3 = 0.18416458852867845 + -1x: 1 - (62.1575 / 59.850) = -0.038554720133667564 + -3x: (1 - (62.1575 / 59.850))*3 = -0.11566416040100269 + 1.9 quote + 1x: (57.8575 / 60.15) - 1 = -0.0381130507065669 + 3x: ((57.8575 / 60.15) - 1)*3 = -0.1143391521197007 + -1x: 1 - (56.1425 / 59.850) = 0.06194653299916464 + -3x: (1 - (56.1425 / 59.850))*3 = 0.18583959899749392 + 2.2 quote + 1x: (66.835 / 60.15) - 1 = 0.11113881961762262 + 3x: ((66.835 / 60.15) - 1)*3 = 0.33341645885286786 + -1x: 1 - (65.165 / 59.850) = -0.08880534670008355 + -3x: (1 - (65.165 / 59.850))*3 = -0.26641604010025066 + funding_fee: -1 + close_value: + equations: + (amount * close_rate) - (amount * close_rate * fee) + funding_fees + (amount * close_rate) - (amount * close_rate * fee) - funding_fees + 2.1 quote + 1x,3x: (30.00 * 2.1) - (30.00 * 2.1 * 0.0025) + (-1) = 61.8425 + -1x,-3x: (30.00 * 2.1) + (30.00 * 2.1 * 0.0025) - (-1) = 64.1575 + 1.9 quote + 1x,3x: (30.00 * 1.9) - (30.00 * 1.9 * 0.0025) + (-1) = 55.8575 + -1x,-3x: (30.00 * 1.9) + (30.00 * 1.9 * 0.0025) - (-1) = 58.1425 + 2.2 quote: + 1x,3x: (30.00 * 2.20) - (30.00 * 2.20 * 0.0025) + (-1) = 64.835 + -1x,-3x: (30.00 * 2.20) + (30.00 * 2.20 * 0.0025) - (-1) = 67.165 + total_profit: + 2.1 quote + 1x,3x: 61.8425 - 60.15 = 1.6925000000000026 + -1x,-3x: 59.850 - 64.1575 = -4.307499999999997 + 1.9 quote + 1x,3x: 55.8575 - 60.15 = -4.292499999999997 + -1x,-3x: 59.850 - 58.1425 = 1.7075000000000031 + 2.2 quote: + 1x,3x: 64.835 - 60.15 = 4.684999999999995 + -1x,-3x: 59.850 - 67.165 = -7.315000000000005 + total_profit_ratio: + 2.1 quote + 1x: (61.8425 / 60.15) - 1 = 0.028137988362427313 + 3x: ((61.8425 / 60.15) - 1)*3 = 0.08441396508728194 + -1x: 1 - (64.1575 / 59.850) = -0.07197159565580624 + -3x: (1 - (64.1575 / 59.850))*3 = -0.21591478696741873 + 1.9 quote + 1x: (55.8575 / 60.15) - 1 = -0.07136325852036574 + 3x: ((55.8575 / 60.15) - 1)*3 = -0.2140897755610972 + -1x: 1 - (58.1425 / 59.850) = 0.02852965747702596 + -3x: (1 - (58.1425 / 59.850))*3 = 0.08558897243107788 + 2.2 quote + 1x: (64.835 / 60.15) - 1 = 0.07788861180382378 + 3x: ((64.835 / 60.15) - 1)*3 = 0.23366583541147135 + -1x: 1 - (67.165 / 59.850) = -0.12222222222222223 + -3x: (1 - (67.165 / 59.850))*3 = -0.3666666666666667 """ trade = Trade( pair='ADA/USDT', @@ -953,7 +1089,8 @@ def test_calc_profit( leverage=lev, fee_open=0.0025, fee_close=fee_close, - trading_mode=trading_mode + trading_mode=trading_mode, + funding_fees=funding_fees ) trade.open_order_id = 'something' From bdad604fab3c04780a3c6dc0748ef71a28999dc6 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 11 Oct 2021 07:48:31 -0600 Subject: [PATCH 51/59] Added persistence futures tests --- freqtrade/persistence/models.py | 2 +- tests/test_persistence.py | 93 +++++++++++++++++---------------- 2 files changed, 49 insertions(+), 46 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 6614de34e..51ba72afa 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -649,7 +649,7 @@ class LocalTrade(): zero = Decimal(0.0) # If nothing was borrowed - if self.has_no_leverage: + if self.has_no_leverage or self.trading_mode != TradingMode.MARGIN: return zero open_date = self.open_date.replace(tzinfo=None) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 7fa04ed54..7128fcd89 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -186,12 +186,12 @@ def test_set_stop_loss_isolated_liq(fee): ("binance", False, 1, 295, 0.0005, 0.0, spot), ("binance", True, 1, 295, 0.0005, 0.003125, margin), - # ("binance", False, 3, 10, 0.0005, 0.0, futures), - # ("binance", True, 3, 295, 0.0005, 0.0, futures), - # ("binance", False, 5, 295, 0.0005, 0.0, futures), - # ("binance", True, 5, 295, 0.0005, 0.0, futures), - # ("binance", False, 1, 295, 0.0005, 0.0, futures), - # ("binance", True, 1, 295, 0.0005, 0.0, futures), + ("binance", False, 3, 10, 0.0005, 0.0, futures), + ("binance", True, 3, 295, 0.0005, 0.0, futures), + ("binance", False, 5, 295, 0.0005, 0.0, futures), + ("binance", True, 5, 295, 0.0005, 0.0, futures), + ("binance", False, 1, 295, 0.0005, 0.0, futures), + ("binance", True, 1, 295, 0.0005, 0.0, futures), ("kraken", False, 3, 10, 0.0005, 0.040, margin), ("kraken", True, 3, 10, 0.0005, 0.030, margin), @@ -284,8 +284,6 @@ def test_interest(market_buy_order_usdt, fee, exchange, is_short, lev, minutes, (True, 1.0, 30.0, margin), (False, 3.0, 40.0, margin), (True, 3.0, 30.0, margin), - # (False, 3.0, 0.0, futures), - # (True, 3.0, 0.0, futures), ]) @pytest.mark.usefixtures("init_persistence") def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, @@ -539,26 +537,26 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, @pytest.mark.parametrize( - 'exchange,is_short,lev,open_value,close_value,profit,profit_ratio,trading_mode', [ - ("binance", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot), - ("binance", True, 1, 59.850, 66.1663784375, -6.316378437500013, -0.105536815998329, margin), - ("binance", False, 3, 60.15, 65.83416667, 5.684166670000003, 0.2834995845386534, margin), - ("binance", True, 3, 59.85, 66.1663784375, -6.316378437500013, -0.3166104479949876, margin), + 'exchange,is_short,lev,open_value,close_value,profit,profit_ratio,trading_mode,funding_fees', [ + ("binance", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot, 0.0), + ("binance", True, 1, 59.850, 66.1663784375, -6.3163784375, -0.105536815998329, margin, 0.0), + ("binance", False, 3, 60.15, 65.83416667, 5.68416667, 0.2834995845386534, margin, 0.0), + ("binance", True, 3, 59.85, 66.1663784375, -6.3163784375, -0.3166104479949876, margin, 0.0), - ("kraken", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot), - ("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.106619298245614, margin), - ("kraken", False, 3, 60.15, 65.795, 5.645, 0.2815461346633419, margin), - ("kraken", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842, margin), + ("kraken", False, 1, 60.15, 65.835, 5.685, 0.0945137157107232, spot, 0.0), + ("kraken", True, 1, 59.850, 66.231165, -6.381165, -0.106619298245614, margin, 0.0), + ("kraken", False, 3, 60.15, 65.795, 5.645, 0.2815461346633419, margin, 0.0), + ("kraken", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842, margin, 0.0), - # TODO-lev - # ("binance", True, 1, 59.850, 66.1663784375, -6.316378437500013, -0.105536815998329, futures), - # ("binance", False, 3, 60.15, 65.83416667, 5.684166670000003, 0.2834995845386534, futures), - # ("binance", True, 3, 59.850, 66.231165, -6.381165000000003, -0.319857894736842, futures), + ("binance", False, 1, 60.15, 66.835, 6.685, 0.11113881961762262, futures, 1.0), + ("binance", True, 1, 59.85, 67.165, -7.315, -0.12222222222222223, futures, -1.0), + ("binance", False, 3, 60.15, 64.835, 4.685, 0.23366583541147135, futures, -1.0), + ("binance", True, 3, 59.85, 65.165, -5.315, -0.26641604010025066, futures, 1.0), ]) @pytest.mark.usefixtures("init_persistence") def test_calc_open_close_trade_price( limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange, is_short, lev, - open_value, close_value, profit, profit_ratio, trading_mode + open_value, close_value, profit, profit_ratio, trading_mode, funding_fees ): trade: Trade = Trade( pair='ADA/USDT', @@ -572,7 +570,8 @@ def test_calc_open_close_trade_price( exchange=exchange, is_short=is_short, leverage=lev, - trading_mode=trading_mode + trading_mode=trading_mode, + funding_fees=funding_fees ) trade.open_order_id = f'something-{is_short}-{lev}-{exchange}' @@ -737,32 +736,35 @@ def test_calc_open_trade_value( @pytest.mark.parametrize( - 'exchange,is_short,lev,open_rate,close_rate,fee_rate,result,trading_mode', [ - ('binance', False, 1, 2.0, 2.5, 0.0025, 74.8125, spot), - ('binance', False, 1, 2.0, 2.5, 0.003, 74.775, spot), - ('binance', False, 1, 2.0, 2.2, 0.005, 65.67, margin), - ('binance', False, 3, 2.0, 2.5, 0.0025, 74.81166667, margin), - ('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667, margin), - ('kraken', False, 3, 2.0, 2.5, 0.0025, 74.7725, margin), - ('kraken', False, 3, 2.0, 2.5, 0.003, 74.735, margin), - ('kraken', True, 3, 2.2, 2.5, 0.0025, 75.2626875, margin), - ('kraken', True, 3, 2.2, 2.5, 0.003, 75.300225, margin), - ('binance', True, 3, 2.2, 2.5, 0.0025, 75.18906641, margin), - ('binance', True, 3, 2.2, 2.5, 0.003, 75.22656719, margin), - ('binance', True, 1, 2.2, 2.5, 0.0025, 75.18906641, margin), - ('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719, margin), - ('kraken', True, 1, 2.2, 2.5, 0.0025, 75.2626875, margin), - ('kraken', True, 1, 2.2, 2.5, 0.003, 75.300225, margin), + 'exchange,is_short,lev,open_rate,close_rate,fee_rate,result,trading_mode,funding_fees', [ + ('binance', False, 1, 2.0, 2.5, 0.0025, 74.8125, spot, 0), + ('binance', False, 1, 2.0, 2.5, 0.003, 74.775, spot, 0), + ('binance', False, 1, 2.0, 2.2, 0.005, 65.67, margin, 0), + ('binance', False, 3, 2.0, 2.5, 0.0025, 74.81166667, margin, 0), + ('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667, margin, 0), + ('binance', True, 3, 2.2, 2.5, 0.0025, 75.18906641, margin, 0), + ('binance', True, 3, 2.2, 2.5, 0.003, 75.22656719, margin, 0), + ('binance', True, 1, 2.2, 2.5, 0.0025, 75.18906641, margin, 0), + ('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719, margin, 0), + + # Kraken + ('kraken', False, 3, 2.0, 2.5, 0.0025, 74.7725, margin, 0), + ('kraken', False, 3, 2.0, 2.5, 0.003, 74.735, margin, 0), + ('kraken', True, 3, 2.2, 2.5, 0.0025, 75.2626875, margin, 0), + ('kraken', True, 3, 2.2, 2.5, 0.003, 75.300225, margin, 0), + ('kraken', True, 1, 2.2, 2.5, 0.0025, 75.2626875, margin, 0), + ('kraken', True, 1, 2.2, 2.5, 0.003, 75.300225, margin, 0), + + ('binance', False, 1, 2.0, 2.5, 0.0025, 75.8125, futures, 1), + ('binance', False, 3, 2.0, 2.5, 0.0025, 73.8125, futures, -1), + ('binance', True, 3, 2.0, 2.5, 0.0025, 74.1875, futures, 1), + ('binance', True, 1, 2.0, 2.5, 0.0025, 76.1875, futures, -1), - # TODO-lev - # ('binance', False, 3, 2.0, 2.5, 0.003, 74.77416667, futures), - # ('binance', True, 1, 2.2, 2.5, 0.003, 75.22656719, futures), - # ('binance', True, 1, 2.2, 2.5, 0.0025, 75.2626875, futures), ]) @pytest.mark.usefixtures("init_persistence") def test_calc_close_trade_price( limit_buy_order_usdt, limit_sell_order_usdt, open_rate, exchange, is_short, - lev, close_rate, fee_rate, result, trading_mode + lev, close_rate, fee_rate, result, trading_mode, funding_fees ): trade = Trade( pair='ADA/USDT', @@ -776,7 +778,8 @@ def test_calc_close_trade_price( interest_rate=0.0005, is_short=is_short, leverage=lev, - trading_mode=trading_mode + trading_mode=trading_mode, + funding_fees=funding_fees ) trade.open_order_id = 'close_trade' assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result From 70000b58434cbde9952232f87ec2e6e8d241e21f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Oct 2021 20:28:23 +0200 Subject: [PATCH 52/59] Use scheduler as Object, not the automatic Singleton --- freqtrade/freqtradebot.py | 7 ++++--- tests/test_freqtradebot.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bd4e8b9b8..b937810f1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -10,7 +10,7 @@ from threading import Lock from typing import Any, Dict, List, Optional import arrow -import schedule +from schedule import Scheduler from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency @@ -109,6 +109,7 @@ class FreqtradeBot(LoggingMixin): self.trading_mode = TradingMode(self.config['trading_mode']) else: self.trading_mode = TradingMode.SPOT + self._schedule = Scheduler() if self.trading_mode == TradingMode.FUTURES: @@ -121,7 +122,7 @@ class FreqtradeBot(LoggingMixin): for time_slot in range(0, 24): for minutes in [0, 15, 30, 45]: t = str(time(time_slot, minutes, 2)) - schedule.every().day.at(t).do(update) + self._schedule.every().day.at(t).do(update) def notify_status(self, msg: str) -> None: """ @@ -293,7 +294,7 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Error updating Order {order.order_id} due to {e}") if self.trading_mode == TradingMode.FUTURES: - schedule.run_pending() + self._schedule.run_pending() def update_closed_trades_without_assigned_fees(self): """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f7b0808b1..5354ee618 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4303,6 +4303,6 @@ def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_mac freqtrade = get_patched_freqtradebot(mocker, default_conf) time_machine.move_to(f"{t2} +00:00") - schedule.run_pending() + freqtrade._schedule.run_pending() assert freqtrade.update_funding_fees.call_count == calls From 952d83ad241f42c9d1a4ed4133c8b60091fffb22 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Oct 2021 20:35:18 +0200 Subject: [PATCH 53/59] Reenable additional test --- tests/test_freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5354ee618..82150a704 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4285,7 +4285,7 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: (TradingMode.SPOT, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), (TradingMode.MARGIN, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), (TradingMode.FUTURES, 31, "2021-09-01 00:00:02", "2021-09-01 08:00:01"), - # (TradingMode.FUTURES, 32, "2021-09-01 00:00:01", "2021-09-01 08:00:01"), + (TradingMode.FUTURES, 32, "2021-09-01 00:00:01", "2021-09-01 08:00:01"), (TradingMode.FUTURES, 33, "2021-09-01 00:00:01", "2021-09-01 08:00:02"), (TradingMode.FUTURES, 33, "2021-09-01 00:00:00", "2021-09-01 08:00:02"), (TradingMode.FUTURES, 34, "2021-08-31 23:59:59", "2021-09-01 08:00:02"), @@ -4303,6 +4303,7 @@ def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_mac freqtrade = get_patched_freqtradebot(mocker, default_conf) time_machine.move_to(f"{t2} +00:00") + # Check schedule jobs in debugging with freqtrade._schedule.jobs freqtrade._schedule.run_pending() assert freqtrade.update_funding_fees.call_count == calls From 86cbd0039ff9ff270009e6517005b70c0c0812fb Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 12 Oct 2021 02:24:35 -0600 Subject: [PATCH 54/59] Fixed bugs --- freqtrade/freqtradebot.py | 3 --- tests/test_freqtradebot.py | 5 ++--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b937810f1..88b26115e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -204,9 +204,6 @@ class FreqtradeBot(LoggingMixin): if self.get_free_open_trades(): self.enter_positions() - if self.trading_mode == TradingMode.FUTURES: - schedule.run_pending() - Trade.commit() def process_stopped(self) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 82150a704..3cd489685 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -9,7 +9,6 @@ from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest -import schedule from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT from freqtrade.enums import RPCMessageType, RunMode, SellType, State, TradingMode @@ -4288,8 +4287,8 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: (TradingMode.FUTURES, 32, "2021-09-01 00:00:01", "2021-09-01 08:00:01"), (TradingMode.FUTURES, 33, "2021-09-01 00:00:01", "2021-09-01 08:00:02"), (TradingMode.FUTURES, 33, "2021-09-01 00:00:00", "2021-09-01 08:00:02"), - (TradingMode.FUTURES, 34, "2021-08-31 23:59:59", "2021-09-01 08:00:02"), - (TradingMode.FUTURES, 34, "2021-08-31 23:59:59", "2021-09-01 08:00:03"), + (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:02"), + (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:03"), ]) def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_machine, t1, t2): From f290ff5c9aa68c06fc162a463097fc34b1fd8594 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Oct 2021 19:10:38 +0200 Subject: [PATCH 55/59] Re-add schedule.run_pending --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 88b26115e..ddb4b148f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -203,7 +203,8 @@ class FreqtradeBot(LoggingMixin): # Then looking for buy opportunities if self.get_free_open_trades(): self.enter_positions() - + if self.trading_mode == TradingMode.FUTURES: + self._schedule.run_pending() Trade.commit() def process_stopped(self) -> None: From 532a9341d2506a00a6c7d617fec443670a8fdadb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Oct 2021 20:41:48 +0200 Subject: [PATCH 56/59] Fix migration issue --- freqtrade/exchange/exchange.py | 3 +++ freqtrade/persistence/migrations.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 315ab62c5..ca546eef4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -71,6 +71,9 @@ class Exchange: "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) } _ft_has: Dict = {} + + # funding_fee_times is currently unused, but should ideally be used to properly + # schedule refresh times funding_fee_times: List[int] = [] # hours of the day _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index ec6f10e3f..2b1d10bc1 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -180,7 +180,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: table_back_name = get_backup_name(tabs, 'trades_bak') # Check for latest column - if not has_column(cols, 'is_short'): + if not has_column(cols, 'funding_fees'): logger.info(f'Running database migration for trades - backup: {table_back_name}') migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) # Reread columns - the above recreated the table! From 0fcc7eca62099a0c4c9adec2430ac04a64f06112 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 12 Oct 2021 20:28:46 -0600 Subject: [PATCH 57/59] Added more tests to test_update_funding_fees --- tests/test_freqtradebot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3cd489685..c13dfca0a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4289,6 +4289,11 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: (TradingMode.FUTURES, 33, "2021-09-01 00:00:00", "2021-09-01 08:00:02"), (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:02"), (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:03"), + (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:04"), + (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:05"), + (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:06"), + (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:07"), + (TradingMode.FUTURES, 33, "2021-08-31 23:59:58", "2021-09-01 08:00:07"), ]) def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_machine, t1, t2): From 0dbad19b4002704df1ac0116447ed2e2bf5eeb6b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 12 Oct 2021 20:34:19 -0600 Subject: [PATCH 58/59] trading_mode default null in models.Trade --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 51ba72afa..bbb390e75 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -923,7 +923,7 @@ class Trade(_DECL_BASE, LocalTrade): buy_tag = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) - trading_mode = Column(Enum(TradingMode)) + trading_mode = Column(Enum(TradingMode), nullable=True) # Leverage trading properties leverage = Column(Float, nullable=True, default=1.0) From 2c6290a100a8f00a8ef5b68054850475364a430e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 13 Oct 2021 07:04:21 +0200 Subject: [PATCH 59/59] Small updates to prevent random test failures --- freqtrade/exchange/exchange.py | 1 + tests/test_freqtradebot.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ca546eef4..a61c7b39a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1606,6 +1606,7 @@ class Exchange: :param since: The earliest time of consideration for calculating funding fees, in unix time or as a datetime """ + # TODO-lev: Add dry-run handling for this. if not self.exchange_has("fetchFundingHistory"): raise OperationalException( diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c13dfca0a..d09fc18a2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4284,8 +4284,8 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None: (TradingMode.SPOT, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), (TradingMode.MARGIN, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"), (TradingMode.FUTURES, 31, "2021-09-01 00:00:02", "2021-09-01 08:00:01"), - (TradingMode.FUTURES, 32, "2021-09-01 00:00:01", "2021-09-01 08:00:01"), - (TradingMode.FUTURES, 33, "2021-09-01 00:00:01", "2021-09-01 08:00:02"), + (TradingMode.FUTURES, 32, "2021-09-01 00:00:00", "2021-09-01 08:00:01"), + (TradingMode.FUTURES, 32, "2021-09-01 00:00:02", "2021-09-01 08:00:02"), (TradingMode.FUTURES, 33, "2021-09-01 00:00:00", "2021-09-01 08:00:02"), (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:02"), (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:03"),