From b7891485b35d52223793cab10d9fd3859f0f2726 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 3 Aug 2021 12:55:22 -0600 Subject: [PATCH 001/109] 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 002/109] 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 003/109] 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 004/109] 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 005/109] 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 006/109] 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 007/109] 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 008/109] 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 009/109] 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 010/109] 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 011/109] 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 012/109] 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 013/109] 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 014/109] 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 015/109] 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 016/109] 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 017/109] 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 018/109] 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 019/109] 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 020/109] 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 021/109] 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 022/109] 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 023/109] 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 024/109] 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 025/109] 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 026/109] 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 027/109] 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 097da448e2d297ace7f2158efb4fac6164168028 Mon Sep 17 00:00:00 2001 From: froggleston Date: Sat, 25 Sep 2021 15:48:42 +0100 Subject: [PATCH 028/109] Add CPU,RAM sysinfo support to the REST API to help with bot system monitoring --- freqtrade/rpc/api_server/api_v1.py | 4 ++++ freqtrade/rpc/rpc.py | 5 ++++- requirements.txt | 1 + scripts/rest_client.py | 6 ++++++ 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 7e613f184..733fa7383 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -259,3 +259,7 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option 'pair_interval': pair_interval, } return result + +@router.get('/sysinfo', tags=['info']) +def sysinfo(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_sysinfo() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f6599b429..9b0d4b0f7 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1,7 +1,7 @@ """ This module contains class to define a RPC communications """ -import logging +import logging, psutil from abc import abstractmethod from datetime import date, datetime, timedelta, timezone from math import isnan @@ -870,3 +870,6 @@ class RPC: 'subplots' not in self._freqtrade.strategy.plot_config): self._freqtrade.strategy.plot_config['subplots'] = {} return self._freqtrade.strategy.plot_config + + def _rpc_sysinfo(self) -> Dict[str, Any]: + return {"cpu_pct": psutil.cpu_percent(interval=1, percpu=True), "ram_pct": psutil.virtual_memory().percent} diff --git a/requirements.txt b/requirements.txt index d1d10dd1d..6a2cfec01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,6 +36,7 @@ fastapi==0.68.1 uvicorn==0.15.0 pyjwt==2.1.0 aiofiles==0.7.0 +psutil==5.8.0 # Support for colorized terminal output colorama==0.4.4 diff --git a/scripts/rest_client.py b/scripts/rest_client.py index ece0a253e..52de3c534 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -334,6 +334,12 @@ class FtRestClient(): "timerange": timerange if timerange else '', }) + def sysinfo(self): + """Provides system information (CPU, RAM usage) + + :return: json object + """ + return self._get("sysinfo") def add_arguments(): parser = argparse.ArgumentParser() From c4ac8761836032c5e2d4042ffbd3ed79d5e46b31 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 29 Sep 2021 22:16:44 -0600 Subject: [PATCH 029/109] 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 030/109] 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 031/109] 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 032/109] 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 033/109] 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 034/109] 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 035/109] 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 036/109] 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 037/109] 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 e73f5ab4802823b6cfa2dba745d6218be0a7f97b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Oct 2021 09:48:19 +0200 Subject: [PATCH 038/109] Add test confirming #5652 --- tests/exchange/test_exchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 79b4a3ff5..691cf3c03 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -275,6 +275,7 @@ def test_amount_to_precision(default_conf, mocker, amount, precision_mode, preci (234.43, 4, 0.5, 234.5), (234.53, 4, 0.5, 235.0), (0.891534, 4, 0.0001, 0.8916), + (64968.89, 4, 0.01, 64968.89), ]) def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected): @@ -293,7 +294,7 @@ def test_price_to_precision(default_conf, mocker, price, precision_mode, precisi PropertyMock(return_value=precision_mode)) pair = 'ETH/BTC' - assert pytest.approx(exchange.price_to_precision(pair, price)) == expected + assert exchange.price_to_precision(pair, price) == expected @pytest.mark.parametrize("price,precision_mode,precision,expected", [ From f5e5203388b6666664c5b9edd09683956d196478 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Oct 2021 09:48:50 +0200 Subject: [PATCH 039/109] Use "round" to 12 digits for TickSize mode Avoids float rounding problems, fix #5652 --- 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 2b9b08d70..e9d0316d2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -523,7 +523,7 @@ class Exchange: precision = self.markets[pair]['precision']['price'] missing = price % precision if missing != 0: - price = price - missing + precision + price = round(price - missing + precision, 10) else: symbol_prec = self.markets[pair]['precision']['price'] big_price = price * pow(10, symbol_prec) From 1c63d01cec878e363075e2f43d79296be2ab3d60 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 3 Oct 2021 14:14:16 +0200 Subject: [PATCH 040/109] Prevent using market-orders on gateio GateIo does not support market orders on spot markets --- freqtrade/exchange/gateio.py | 8 ++++++++ tests/exchange/test_gateio.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 tests/exchange/test_gateio.py diff --git a/freqtrade/exchange/gateio.py b/freqtrade/exchange/gateio.py index e6ee01c8a..018248a99 100644 --- a/freqtrade/exchange/gateio.py +++ b/freqtrade/exchange/gateio.py @@ -2,6 +2,7 @@ import logging from typing import Dict +from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange @@ -23,3 +24,10 @@ class Gateio(Exchange): } _headers = {'X-Gate-Channel-Id': 'freqtrade'} + + def validate_ordertypes(self, order_types: Dict) -> None: + super().validate_ordertypes(order_types) + + if any(v == 'market' for k, v in order_types.items()): + raise OperationalException( + f'Exchange {self.name} does not support market orders.') diff --git a/tests/exchange/test_gateio.py b/tests/exchange/test_gateio.py new file mode 100644 index 000000000..6f7862909 --- /dev/null +++ b/tests/exchange/test_gateio.py @@ -0,0 +1,28 @@ +import pytest + +from freqtrade.exceptions import OperationalException +from freqtrade.exchange import Gateio +from freqtrade.resolvers.exchange_resolver import ExchangeResolver + + +def test_validate_order_types_gateio(default_conf, mocker): + default_conf['exchange']['name'] = 'gateio' + mocker.patch('freqtrade.exchange.Exchange._init_ccxt') + mocker.patch('freqtrade.exchange.Exchange._load_markets', return_value={}) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs') + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') + exch = ExchangeResolver.load_exchange('gateio', default_conf, True) + assert isinstance(exch, Gateio) + + default_conf['order_types'] = { + 'buy': 'market', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': False + } + + with pytest.raises(OperationalException, + match=r'Exchange .* does not support market orders.'): + ExchangeResolver.load_exchange('gateio', default_conf, True) From 0d9beaa3f36a23641d17ee3a7dd2e77933b39846 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 03:01:04 +0000 Subject: [PATCH 041/109] Bump filelock from 3.0.12 to 3.3.0 Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.0.12 to 3.3.0. - [Release notes](https://github.com/tox-dev/py-filelock/releases) - [Commits](https://github.com/tox-dev/py-filelock/compare/v3.0.12...3.3.0) --- updated-dependencies: - dependency-name: filelock dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 9feec80f1..b4067d1db 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,7 +5,7 @@ scipy==1.7.1 scikit-learn==0.24.2 scikit-optimize==0.8.1 -filelock==3.0.12 +filelock==3.3.0 joblib==1.0.1 psutil==5.8.0 progressbar2==3.53.3 From d220c55d405460de1eed9f5ae9e0dc214e6d8996 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 03:01:11 +0000 Subject: [PATCH 042/109] Bump pymdown-extensions from 8.2 to 9.0 Bumps [pymdown-extensions](https://github.com/facelessuser/pymdown-extensions) from 8.2 to 9.0. - [Release notes](https://github.com/facelessuser/pymdown-extensions/releases) - [Commits](https://github.com/facelessuser/pymdown-extensions/compare/8.2...9.0) --- updated-dependencies: - dependency-name: pymdown-extensions dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 9b7c12a43..67d2c9da8 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.2 mkdocs-material==7.3.0 mdx_truly_sane_lists==1.2 -pymdown-extensions==8.2 +pymdown-extensions==9.0 From ff45d52d497629340d9424728da87060fc369c64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 03:01:14 +0000 Subject: [PATCH 043/109] Bump types-filelock from 0.1.5 to 3.2.0 Bumps [types-filelock](https://github.com/python/typeshed) from 0.1.5 to 3.2.0. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-filelock dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2f03255a0..3d45247c1 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,6 +22,6 @@ nbconvert==6.2.0 # mypy types types-cachetools==4.2.0 -types-filelock==0.1.5 +types-filelock==3.2.0 types-requests==2.25.9 types-tabulate==0.8.2 From 35c4a0a188c0a928600be63a177739d4fb1e42cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 03:01:17 +0000 Subject: [PATCH 044/109] Bump jsonschema from 3.2.0 to 4.0.1 Bumps [jsonschema](https://github.com/Julian/jsonschema) from 3.2.0 to 4.0.1. - [Release notes](https://github.com/Julian/jsonschema/releases) - [Changelog](https://github.com/Julian/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/Julian/jsonschema/compare/v3.2.0...v4.0.1) --- updated-dependencies: - dependency-name: jsonschema dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index feeb4d942..370766f8d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ cachetools==4.2.2 requests==2.26.0 urllib3==1.26.7 wrapt==1.12.1 -jsonschema==3.2.0 +jsonschema==4.0.1 TA-Lib==0.4.21 technical==1.3.0 tabulate==0.8.9 From 0071d002b68c244716ba3010deb110bfbeaad2a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 03:01:22 +0000 Subject: [PATCH 045/109] Bump ccxt from 1.57.3 to 1.57.38 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.57.3 to 1.57.38. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.57.3...1.57.38) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index feeb4d942..5006d356f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.2 pandas==1.3.3 pandas-ta==0.3.14b -ccxt==1.57.3 +ccxt==1.57.38 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.8 aiohttp==3.7.4.post0 From 2b41066ab7ccbe032586f231449f82704d37301f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 03:01:29 +0000 Subject: [PATCH 046/109] Bump pytest-cov from 2.12.1 to 3.0.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.12.1 to 3.0.0. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.12.1...v3.0.0) --- updated-dependencies: - dependency-name: pytest-cov dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 2f03255a0..109413e6e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ flake8-tidy-imports==4.4.1 mypy==0.910 pytest==6.2.5 pytest-asyncio==0.15.1 -pytest-cov==2.12.1 +pytest-cov==3.0.0 pytest-mock==3.6.1 pytest-random-order==1.0.4 isort==5.9.3 From 949f4fbbbfdbae9987d212edae7b9a6a3c1db00c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 04:36:11 +0000 Subject: [PATCH 047/109] Bump types-cachetools from 4.2.0 to 4.2.2 Bumps [types-cachetools](https://github.com/python/typeshed) from 4.2.0 to 4.2.2. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-cachetools dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3d45247c1..858a46389 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -21,7 +21,7 @@ time-machine==2.4.0 nbconvert==6.2.0 # mypy types -types-cachetools==4.2.0 +types-cachetools==4.2.2 types-filelock==3.2.0 types-requests==2.25.9 types-tabulate==0.8.2 From f41fd4e88d7ca79294c40942a5fd181f574469d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Oct 2021 04:40:24 +0000 Subject: [PATCH 048/109] Bump mkdocs-material from 7.3.0 to 7.3.1 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.3.0 to 7.3.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.3.0...7.3.1) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 67d2c9da8..bbbb240ba 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.2 -mkdocs-material==7.3.0 +mkdocs-material==7.3.1 mdx_truly_sane_lists==1.2 pymdown-extensions==9.0 From 6e1e1e00c259d13b81d50c84120a542378650b59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 4 Oct 2021 06:59:08 +0200 Subject: [PATCH 049/109] 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 f15922a16858247c6225d24dd69ae89ceb3e6034 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 4 Oct 2021 19:11:35 +0200 Subject: [PATCH 050/109] Fix custom_stoploss in strategy template closes #5658 --- .../templates/subtemplates/strategy_methods_advanced.j2 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 2df23f365..fb467ecaa 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -32,8 +32,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: use_custom_stoploss = True def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', - current_rate: float, current_profit: float, dataframe: DataFrame, - **kwargs) -> float: + current_rate: float, current_profit: float, **kwargs) -> float: """ Custom stoploss logic, returning the new distance relative to current_rate (as ratio). e.g. returning -0.05 would create a stoploss 5% below current_rate. @@ -44,14 +43,13 @@ def custom_stoploss(self, pair: str, trade: 'Trade', current_time: 'datetime', When not implemented by a strategy, returns the initial stoploss value Only called when use_custom_stoploss is set to True. - :param pair: Pair that's about to be sold. + :param pair: Pair that's currently analyzed :param trade: trade object. :param current_time: datetime object, containing the current datetime :param current_rate: Rate, calculated based on pricing settings in ask_strategy. :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param dataframe: Analyzed dataframe for this pair. Can contain future data in backtesting. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New stoploss value, relative to the currentrate + :return float: New stoploss value, relative to the current_rate """ return self.stoploss From 7f4baab420ce98c97deeedb69170c3129828c9b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 4 Oct 2021 20:14:14 +0200 Subject: [PATCH 051/109] Remove explicit rateLimits, improve docs --- config_examples/config_binance.example.json | 4 +- config_examples/config_ftx.example.json | 7 +-- config_examples/config_full.example.json | 8 +-- config_examples/config_kraken.example.json | 4 +- docs/configuration.md | 39 --------------- docs/exchanges.md | 50 +++++++++++++++++++ docs/faq.md | 10 ++-- .../subtemplates/exchange_binance.j2 | 7 +-- .../subtemplates/exchange_generic.j2 | 6 +-- 9 files changed, 65 insertions(+), 70 deletions(-) diff --git a/config_examples/config_binance.example.json b/config_examples/config_binance.example.json index 938bc9342..d59ff96cb 100644 --- a/config_examples/config_binance.example.json +++ b/config_examples/config_binance.example.json @@ -28,10 +28,8 @@ "name": "binance", "key": "your_exchange_key", "secret": "your_exchange_secret", - "ccxt_config": {"enableRateLimit": true}, + "ccxt_config": {}, "ccxt_async_config": { - "enableRateLimit": true, - "rateLimit": 200 }, "pair_whitelist": [ "ALGO/BTC", diff --git a/config_examples/config_ftx.example.json b/config_examples/config_ftx.example.json index 48651f04c..4d9633cc0 100644 --- a/config_examples/config_ftx.example.json +++ b/config_examples/config_ftx.example.json @@ -28,11 +28,8 @@ "name": "ftx", "key": "your_exchange_key", "secret": "your_exchange_secret", - "ccxt_config": {"enableRateLimit": true}, - "ccxt_async_config": { - "enableRateLimit": true, - "rateLimit": 50 - }, + "ccxt_config": {}, + "ccxt_async_config": {}, "pair_whitelist": [ "BTC/USD", "ETH/USD", diff --git a/config_examples/config_full.example.json b/config_examples/config_full.example.json index c415d70b0..83b8a27d0 100644 --- a/config_examples/config_full.example.json +++ b/config_examples/config_full.example.json @@ -84,12 +84,8 @@ "key": "your_exchange_key", "secret": "your_exchange_secret", "password": "", - "ccxt_config": {"enableRateLimit": true}, - "ccxt_async_config": { - "enableRateLimit": true, - "rateLimit": 500, - "aiohttp_trust_env": false - }, + "ccxt_config": {}, + "ccxt_async_config": {}, "pair_whitelist": [ "ALGO/BTC", "ATOM/BTC", diff --git a/config_examples/config_kraken.example.json b/config_examples/config_kraken.example.json index bf3548568..32def895c 100644 --- a/config_examples/config_kraken.example.json +++ b/config_examples/config_kraken.example.json @@ -28,10 +28,8 @@ "name": "kraken", "key": "your_exchange_key", "secret": "your_exchange_key", - "ccxt_config": {"enableRateLimit": true}, + "ccxt_config": {}, "ccxt_async_config": { - "enableRateLimit": true, - "rateLimit": 1000 }, "pair_whitelist": [ "ADA/EUR", diff --git a/docs/configuration.md b/docs/configuration.md index 6ccea4c73..bc8a40dcb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -447,45 +447,6 @@ The possible values are: `gtc` (default), `fok` or `ioc`. 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 - -Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency -exchange markets and trading APIs. The complete up-to-date list can be found in the -[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). - However, the bot was tested by the development team with only Bittrex, Binance and Kraken, - so these are the only officially supported exchanges: - -- [Bittrex](https://bittrex.com/): "bittrex" -- [Binance](https://www.binance.com/): "binance" -- [Kraken](https://kraken.com/): "kraken" - -Feel free to test other exchanges and submit your PR to improve the bot. - -Some exchanges require special configuration, which can be found on the [Exchange-specific Notes](exchanges.md) documentation page. - -#### Sample exchange configuration - -A exchange configuration for "binance" would look as follows: - -```json -"exchange": { - "name": "binance", - "key": "your_exchange_key", - "secret": "your_exchange_secret", - "ccxt_config": {"enableRateLimit": true}, - "ccxt_async_config": { - "enableRateLimit": true, - "rateLimit": 200 - }, -``` - -This configuration enables binance, as well as rate-limiting to avoid bans from the exchange. -`"rateLimit": 200` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false. - -!!! Note - Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. - We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step. - ### What values can be used for fiat_display_currency? The `fiat_display_currency` configuration parameter sets the base currency to use for the diff --git a/docs/exchanges.md b/docs/exchanges.md index c0fbdc694..badaa484a 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -2,6 +2,56 @@ This page combines common gotchas and informations which are exchange-specific and most likely don't apply to other exchanges. +## Exchange configuration + +Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency +exchange markets and trading APIs. The complete up-to-date list can be found in the +[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). +However, the bot was tested by the development team with only a few exchanges. +A current list of these can be found in the "Home" section of this documentation. + +Feel free to test other exchanges and submit your feedback or PR to improve the bot or confirm exchanges that work flawlessly.. + +Some exchanges require special configuration, which can be found below. + +### Sample exchange configuration + +A exchange configuration for "binance" would look as follows: + +```json +"exchange": { + "name": "binance", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "ccxt_config": {}, + "ccxt_async_config": {}, + // ... +``` + +### Setting rate limits + +Usually, rate limits set by CCXT are reliable and work well. +In case of problems related to rate-limits (usually DDOS Exceptions in your logs), it's easy to change rateLimit settings to other values. + +```json +"exchange": { + "name": "kraken", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "ccxt_config": {"enableRateLimit": true}, + "ccxt_async_config": { + "enableRateLimit": true, + "rateLimit": 3100 + }, +``` + +This configuration enables kraken, as well as rate-limiting to avoid bans from the exchange. +`"rateLimit": 3100` defines a wait-event of 0.2s between each call. This can also be completely disabled by setting `"enableRateLimit"` to false. + +!!! Note + Optimal settings for rate-limiting depend on the exchange and the size of the whitelist, so an ideal parameter will vary on many other settings. + We try to provide sensible defaults per exchange where possible, if you encounter bans please make sure that `"enableRateLimit"` is enabled and increase the `"rateLimit"` parameter step by step. + ## Binance Binance supports [time_in_force](configuration.md#understand-order_time_in_force). diff --git a/docs/faq.md b/docs/faq.md index 285625491..75c40a681 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -82,11 +82,11 @@ Currently known to happen for US Bittrex users. Read [the Bittrex section about restricted markets](exchanges.md#restricted-markets) for more information. -### I'm getting the "Exchange Bittrex does not support market orders." message and cannot run my strategy +### I'm getting the "Exchange XXX does not support market orders." message and cannot run my strategy -As the message says, Bittrex does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex). +As the message says, your exchange does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex and Gate.io). -To fix it for Bittrex, redefine order types in the strategy to use "limit" instead of "market": +To fix this, redefine order types in the strategy to use "limit" instead of "market": ``` order_types = { @@ -136,6 +136,8 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us > type \path\to\mylogfile.log | findstr "something" ``` +## Hyperopt module + ### Why does freqtrade not have GPU support? First of all, most indicator libraries don't have GPU support - as such, there would be little benefit for indicator calculations. @@ -152,8 +154,6 @@ The benefit of using GPU would therefore be pretty slim - and will not justify t There is however nothing preventing you from using GPU-enabled indicators within your strategy if you think you must have this - you will however probably be disappointed by the slim gain that will give you (compared to the complexity). -## Hyperopt module - ### How many epochs do I need to get a good Hyperopt result? Per default Hyperopt called without the `-e`/`--epochs` command line option will only diff --git a/freqtrade/templates/subtemplates/exchange_binance.j2 b/freqtrade/templates/subtemplates/exchange_binance.j2 index de58b6f72..dc2272119 100644 --- a/freqtrade/templates/subtemplates/exchange_binance.j2 +++ b/freqtrade/templates/subtemplates/exchange_binance.j2 @@ -2,11 +2,8 @@ "name": "{{ exchange_name | lower }}", "key": "{{ exchange_key }}", "secret": "{{ exchange_secret }}", - "ccxt_config": {"enableRateLimit": true}, - "ccxt_async_config": { - "enableRateLimit": true, - "rateLimit": 200 - }, + "ccxt_config": {}, + "ccxt_async_config": {}, "pair_whitelist": [ ], "pair_blacklist": [ diff --git a/freqtrade/templates/subtemplates/exchange_generic.j2 b/freqtrade/templates/subtemplates/exchange_generic.j2 index ade9c2f28..08b11f365 100644 --- a/freqtrade/templates/subtemplates/exchange_generic.j2 +++ b/freqtrade/templates/subtemplates/exchange_generic.j2 @@ -2,10 +2,8 @@ "name": "{{ exchange_name | lower }}", "key": "{{ exchange_key }}", "secret": "{{ exchange_secret }}", - "ccxt_config": {"enableRateLimit": true}, - "ccxt_async_config": { - "enableRateLimit": true - }, + "ccxt_config": {}, + "ccxt_async_config": {}, "pair_whitelist": [ ], From 92f8f231afe79eafa5079d494b56db71e7f30baf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 4 Oct 2021 20:22:41 +0200 Subject: [PATCH 052/109] Remove ratelimit from kucoin template --- freqtrade/templates/subtemplates/exchange_kucoin.j2 | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/freqtrade/templates/subtemplates/exchange_kucoin.j2 b/freqtrade/templates/subtemplates/exchange_kucoin.j2 index 9882c51c7..b797dda41 100644 --- a/freqtrade/templates/subtemplates/exchange_kucoin.j2 +++ b/freqtrade/templates/subtemplates/exchange_kucoin.j2 @@ -3,14 +3,8 @@ "key": "{{ exchange_key }}", "secret": "{{ exchange_secret }}", "password": "{{ exchange_key_password }}", - "ccxt_config": { - "enableRateLimit": true, - "rateLimit": 200 - }, - "ccxt_async_config": { - "enableRateLimit": true, - "rateLimit": 200 - }, + "ccxt_config": {}, + "ccxt_async_config": {}, "pair_whitelist": [ ], "pair_blacklist": [ From 0db5c07314a02beb7dde0baf76a06fff458fd5c6 Mon Sep 17 00:00:00 2001 From: froggleston Date: Tue, 5 Oct 2021 00:10:39 +0100 Subject: [PATCH 053/109] Fix issues with sysinfo rpc/API code, add SysInfo api_schema --- freqtrade/rpc/api_server/api_schemas.py | 4 ++++ freqtrade/rpc/api_server/api_v1.py | 8 ++++---- freqtrade/rpc/rpc.py | 3 ++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 46187f571..b03400900 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -347,3 +347,7 @@ class BacktestResponse(BaseModel): trade_count: Optional[float] # TODO: Properly type backtestresult... backtest_result: Optional[Dict[str, Any]] + +class SysInfo(BaseModel): + cpu_pct: float + ram_pct: float diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 733fa7383..d52e8c10d 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -18,7 +18,7 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac OpenTradeSchema, PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, ShowConfig, Stats, StatusMsg, StrategyListResponse, - StrategyResponse, Version, WhitelistResponse) + StrategyResponse, SysInfo, Version, WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -260,6 +260,6 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option } return result -@router.get('/sysinfo', tags=['info']) -def sysinfo(rpc: RPC = Depends(get_rpc)): - return rpc._rpc_sysinfo() +@router.get('/sysinfo', response_model=SysInfo, tags=['info']) +def sysinfo(): + return RPC._rpc_sysinfo() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9b0d4b0f7..699e3b384 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -871,5 +871,6 @@ class RPC: self._freqtrade.strategy.plot_config['subplots'] = {} return self._freqtrade.strategy.plot_config - def _rpc_sysinfo(self) -> Dict[str, Any]: + @staticmethod + def _rpc_sysinfo() -> Dict[str, Any]: return {"cpu_pct": psutil.cpu_percent(interval=1, percpu=True), "ram_pct": psutil.virtual_memory().percent} From 29e582c6d961397b8eaf184e6b446064c0ce8bfe Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 5 Oct 2021 01:42:46 -0600 Subject: [PATCH 054/109] 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 949d616082c49be11211a396de676077ad741c8d Mon Sep 17 00:00:00 2001 From: jonny07 Date: Tue, 5 Oct 2021 21:33:15 +0200 Subject: [PATCH 055/109] Update docker_quickstart.md Got help in the discord chat to get the UI running, I think most people will need this... --- docs/docker_quickstart.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 27a9091b1..0fe69933a 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -70,6 +70,40 @@ docker-compose up -d !!! Warning "Default configuration" While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. +#### Acessing the UI + +Uncommend the 2 lines below and add your IP adress in the following format (like 192.168.2.67:8080:8080) to the ft_userdata/docker-compose.yml: +'''bash + ports: + - "yourIPadress:8080:8080" +''' +Your ft_userdata/user_data/config.json should look like: +'''bash +api_server": { + "enabled": true, + "listen_ip_address": "0.0.0.0", + "listen_port": 8080, + "verbosity": "error", + "enable_openapi": false, + "jwt_secret_key": "****", + "CORS_origins": [], + "username": "****", + "password": "****" + }, +''' +instead of "****" you will have your data in. +Then rebuild your docker file: +Linux: +'''bash +sudo docker-compose down && sudo docker-compose pull && sudo docker-compose build && sudo docker-compose up -d +''' +Windows: +'''bash +docker-compose down && docker-compose pull && docker-compose build && docker-compose up -d +''' + +You can now access the UI by typing yourIPadress:8080 in your browser. + #### Monitoring the bot You can check for running instances with `docker-compose ps`. From a4a5c1aad0b4a281c0821305e49a8941c3400580 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Oct 2021 07:05:34 +0200 Subject: [PATCH 056/109] 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 c0d01dbc26a1552562ca339a92111f6193e7a02c Mon Sep 17 00:00:00 2001 From: sid Date: Wed, 6 Oct 2021 13:24:27 +0530 Subject: [PATCH 057/109] add max_drawdown loss --- .../optimize/hyperopt_loss_max_drawdown.py | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 freqtrade/optimize/hyperopt_loss_max_drawdown.py diff --git a/freqtrade/optimize/hyperopt_loss_max_drawdown.py b/freqtrade/optimize/hyperopt_loss_max_drawdown.py new file mode 100644 index 000000000..e6f73e04a --- /dev/null +++ b/freqtrade/optimize/hyperopt_loss_max_drawdown.py @@ -0,0 +1,42 @@ +""" +MaxDrawDownHyperOptLoss + +This module defines the alternative HyperOptLoss class which can be used for +Hyperoptimization. +""" +from datetime import datetime +from freqtrade.data.btanalysis import calculate_max_drawdown +from freqtrade.optimize.hyperopt import IHyperOptLoss + +from pandas import DataFrame + + +class MaxDrawDownHyperOptLoss(IHyperOptLoss): + + """ + Defines the loss function for hyperopt. + + This implementation optimizes for max draw down and profit + Less max drawdown more profit -> Lower return value + """ + + @staticmethod + def hyperopt_loss_function(results: DataFrame, trade_count: int, + min_date: datetime, max_date: datetime, + *args, **kwargs) -> float: + + """ + Objective function. + + Uses profit ratio weighted max_drawdown when drawdown is available. + Otherwise directly optimizes profit ratio. + """ + total_profit = results['profit_ratio'].sum() + try: + max_drawdown = calculate_max_drawdown(results) + except ValueError: + # No losing trade, therefore no drawdown. + return -total_profit + max_drawdown_rev = 1 / max_drawdown[0] + ret = max_drawdown_rev * total_profit + return -ret \ No newline at end of file From 6ba46b38bdd434ddd65b065c6393beb0b29aa492 Mon Sep 17 00:00:00 2001 From: sid Date: Wed, 6 Oct 2021 13:46:05 +0530 Subject: [PATCH 058/109] fix formatting --- freqtrade/optimize/hyperopt_loss_max_drawdown.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_max_drawdown.py b/freqtrade/optimize/hyperopt_loss_max_drawdown.py index e6f73e04a..4fa32c00e 100644 --- a/freqtrade/optimize/hyperopt_loss_max_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss_max_drawdown.py @@ -5,11 +5,12 @@ This module defines the alternative HyperOptLoss class which can be used for Hyperoptimization. """ from datetime import datetime -from freqtrade.data.btanalysis import calculate_max_drawdown -from freqtrade.optimize.hyperopt import IHyperOptLoss from pandas import DataFrame +from freqtrade.data.btanalysis import calculate_max_drawdown +from freqtrade.optimize.hyperopt import IHyperOptLoss + class MaxDrawDownHyperOptLoss(IHyperOptLoss): @@ -31,7 +32,7 @@ class MaxDrawDownHyperOptLoss(IHyperOptLoss): Uses profit ratio weighted max_drawdown when drawdown is available. Otherwise directly optimizes profit ratio. """ - total_profit = results['profit_ratio'].sum() + total_profit = results['profit_ratio'].sum() try: max_drawdown = calculate_max_drawdown(results) except ValueError: @@ -39,4 +40,4 @@ class MaxDrawDownHyperOptLoss(IHyperOptLoss): return -total_profit max_drawdown_rev = 1 / max_drawdown[0] ret = max_drawdown_rev * total_profit - return -ret \ No newline at end of file + return -ret From 57ef25789e3db1dd59de1e76096bf730fefdf0d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Oct 2021 19:36:28 +0200 Subject: [PATCH 059/109] Fix style errors --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 4 +++- freqtrade/rpc/rpc.py | 8 ++++++-- scripts/rest_client.py | 1 + 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index b03400900..bde6af35b 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -348,6 +348,7 @@ class BacktestResponse(BaseModel): # TODO: Properly type backtestresult... backtest_result: Optional[Dict[str, Any]] + class SysInfo(BaseModel): cpu_pct: float ram_pct: float diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index d52e8c10d..06230a7db 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -18,7 +18,8 @@ from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, Blac OpenTradeSchema, PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, ShowConfig, Stats, StatusMsg, StrategyListResponse, - StrategyResponse, SysInfo, Version, WhitelistResponse) + StrategyResponse, SysInfo, Version, + WhitelistResponse) from freqtrade.rpc.api_server.deps import get_config, get_rpc, get_rpc_optional from freqtrade.rpc.rpc import RPCException @@ -260,6 +261,7 @@ def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Option } return result + @router.get('/sysinfo', response_model=SysInfo, tags=['info']) def sysinfo(): return RPC._rpc_sysinfo() diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 699e3b384..d0858350c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -1,13 +1,14 @@ """ This module contains class to define a RPC communications """ -import logging, psutil +import logging from abc import abstractmethod from datetime import date, datetime, timedelta, timezone from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union import arrow +import psutil from numpy import NAN, inf, int64, mean from pandas import DataFrame @@ -873,4 +874,7 @@ class RPC: @staticmethod def _rpc_sysinfo() -> Dict[str, Any]: - return {"cpu_pct": psutil.cpu_percent(interval=1, percpu=True), "ram_pct": psutil.virtual_memory().percent} + return { + "cpu_pct": psutil.cpu_percent(interval=1, percpu=True), + "ram_pct": psutil.virtual_memory().percent + } diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 52de3c534..ac3b6defe 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -341,6 +341,7 @@ class FtRestClient(): """ return self._get("sysinfo") + def add_arguments(): parser = argparse.ArgumentParser() parser.add_argument("command", From 992cef56e653844c4b8613c683db81057959f569 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Oct 2021 19:36:51 +0200 Subject: [PATCH 060/109] Add test for sysinfo endpoint --- tests/rpc/test_rpc_apiserver.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7c98b2df7..117b1fa49 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1271,6 +1271,16 @@ def test_list_available_pairs(botclient): assert len(rc.json()['pair_interval']) == 1 +def test_sysinfo(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/sysinfo") + assert_response(rc) + result = rc.json() + assert 'cpu_pct' in result + assert 'ram_pct' in result + + def test_api_backtesting(botclient, mocker, fee, caplog): ftbot, client = botclient mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) From 65d4df938df14881729a63bf2c58ace1aca57d22 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Oct 2021 20:09:08 +0200 Subject: [PATCH 061/109] Improve docker port api --- docker-compose.yml | 6 +++--- docs/docker_quickstart.md | 43 ++++++++++++++++++++------------------- docs/rest-api.md | 2 +- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 80e194ab2..445fbaea0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,10 +15,10 @@ services: volumes: - "./user_data:/freqtrade/user_data" # Expose api on port 8080 (localhost only) - # Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation + # Please read the https://www.freqtrade.io/en/stable/rest-api/ documentation # before enabling this. - # ports: - # - "127.0.0.1:8080:8080" + ports: + - "127.0.0.1:8080:8080" # Default command used when running `docker compose up` command: > trade diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 27a9091b1..d5bec54c9 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -148,27 +148,9 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the dockerfile: "./Dockerfile." ``` -You can then run `docker-compose build` to build the docker image, and run it using the commands described above. +You can then run `docker-compose build --pull` to build the docker image, and run it using the commands described above. -### Troubleshooting - -#### Docker on Windows - -* Error: `"Timestamp for this request is outside of the recvWindow."` - * The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past. - To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so). - A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler. - ``` - taskkill /IM "Docker Desktop.exe" /F - wsl --shutdown - start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe" - ``` - -!!! Warning - Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting. - Best use a linux-VPS for running freqtrade reliably. - -## Plotting with docker-compose +### Plotting with docker-compose Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file. You can then use these commands as follows: @@ -179,7 +161,7 @@ docker-compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p B The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser. -## Data analysis using docker compose +### Data analysis using docker compose Freqtrade provides a docker-compose file which starts up a jupyter lab server. You can run this server using the following command: @@ -196,3 +178,22 @@ Since part of this image is built on your machine, it is recommended to rebuild ``` bash docker-compose -f docker/docker-compose-jupyter.yml build --no-cache ``` + +## Troubleshooting + +### Docker on Windows + +* Error: `"Timestamp for this request is outside of the recvWindow."` + * The market api requests require a synchronized clock but the time in the docker container shifts a bit over time into the past. + To fix this issue temporarily you need to run `wsl --shutdown` and restart docker again (a popup on windows 10 will ask you to do so). + A permanent solution is either to host the docker container on a linux host or restart the wsl from time to time with the scheduler. + + ``` bash + taskkill /IM "Docker Desktop.exe" /F + wsl --shutdown + start "" "C:\Program Files\Docker\Docker\Docker Desktop.exe" + ``` + +!!! Warning + Due to the above, we do not recommend the usage of docker on windows for production setups, but only for experimentation, datadownload and backtesting. + Best use a linux-VPS for running freqtrade reliably. diff --git a/docs/rest-api.md b/docs/rest-api.md index b9b2b29be..b4992e047 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -78,7 +78,7 @@ If you run your bot using docker, you'll need to have the bot listen to incoming }, ``` -Uncomment the following from your docker-compose file: +Make sure that the following 2 lines are available in your docker-compose file: ```yml ports: From 526bdaa2dc04cd84dbea35c72482697cc865080e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 6 Oct 2021 20:14:59 +0200 Subject: [PATCH 062/109] Recommend using 0.0.0.0 as listen address for docker --- freqtrade/commands/build_config_commands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index faa8a98f4..34ae35aff 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -163,7 +163,8 @@ def ask_user_config() -> Dict[str, Any]: { "type": "text", "name": "api_server_listen_addr", - "message": "Insert Api server Listen Address (best left untouched default!)", + "message": ("Insert Api server Listen Address (0.0.0.0 for docker, " + "otherwise best left untouched)"), "default": "127.0.0.1", "when": lambda x: x['api_server'] }, From 46c320513aa0b825b370034feba9d6e9f29af312 Mon Sep 17 00:00:00 2001 From: sid Date: Thu, 7 Oct 2021 08:07:07 +0530 Subject: [PATCH 063/109] use profit_abs --- freqtrade/optimize/hyperopt_loss_max_drawdown.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_max_drawdown.py b/freqtrade/optimize/hyperopt_loss_max_drawdown.py index 4fa32c00e..6777fb2e8 100644 --- a/freqtrade/optimize/hyperopt_loss_max_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss_max_drawdown.py @@ -32,9 +32,9 @@ class MaxDrawDownHyperOptLoss(IHyperOptLoss): Uses profit ratio weighted max_drawdown when drawdown is available. Otherwise directly optimizes profit ratio. """ - total_profit = results['profit_ratio'].sum() + total_profit = results['profit_abs'].sum() try: - max_drawdown = calculate_max_drawdown(results) + max_drawdown = calculate_max_drawdown(results, value_col='profit_abs') except ValueError: # No losing trade, therefore no drawdown. return -total_profit From 29863ad2bf7d364e0bc17dc5c3506e24f20b31b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 7 Oct 2021 06:51:29 +0200 Subject: [PATCH 064/109] Fix error when ask_last_balance is not set closes #5181 --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e9d0316d2..7cc436430 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1058,7 +1058,7 @@ class Exchange: ticker_rate = ticker[conf_strategy['price_side']] if ticker['last'] and ticker_rate: if side == 'buy' and ticker_rate > ticker['last']: - balance = conf_strategy['ask_last_balance'] + balance = conf_strategy.get('ask_last_balance', 0.0) ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) elif side == 'sell' and ticker_rate < ticker['last']: balance = conf_strategy.get('bid_last_balance', 0.0) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 691cf3c03..8cb494edb 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1832,6 +1832,7 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name): ('ask', 20, 19, 10, 0.3, 17), # Between ask and last ('ask', 5, 6, 10, 1.0, 5), # last bigger than ask ('ask', 5, 6, 10, 0.5, 5), # last bigger than ask + ('ask', 20, 19, 10, None, 20), # ask_last_balance missing ('ask', 10, 20, None, 0.5, 10), # last not available - uses ask ('ask', 4, 5, None, 0.5, 4), # last not available - uses ask ('ask', 4, 5, None, 1, 4), # last not available - uses ask @@ -1842,6 +1843,7 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name): ('bid', 21, 20, 10, 0.7, 13), # Between bid and last ('bid', 21, 20, 10, 0.3, 17), # Between bid and last ('bid', 6, 5, 10, 1.0, 5), # last bigger than bid + ('bid', 21, 20, 10, None, 20), # ask_last_balance missing ('bid', 6, 5, 10, 0.5, 5), # last bigger than bid ('bid', 21, 20, None, 0.5, 20), # last not available - uses bid ('bid', 6, 5, None, 0.5, 5), # last not available - uses bid @@ -1851,7 +1853,10 @@ def test_fetch_l2_order_book_exception(default_conf, mocker, exchange_name): def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, last, last_ab, expected) -> None: caplog.set_level(logging.DEBUG) - default_conf['bid_strategy']['ask_last_balance'] = last_ab + if last_ab is None: + del default_conf['bid_strategy']['ask_last_balance'] + else: + default_conf['bid_strategy']['ask_last_balance'] = last_ab default_conf['bid_strategy']['price_side'] = side exchange = get_patched_exchange(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', @@ -1876,6 +1881,7 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, ('bid', 12.0, 11.2, 10.5, 1.0, 11.2), # Last smaller than bid - uses bid ('bid', 12.0, 11.2, 10.5, 0.5, 11.2), # Last smaller than bid - uses bid ('bid', 0.003, 0.002, 0.005, 0.0, 0.002), + ('bid', 0.003, 0.002, 0.005, None, 0.002), ('ask', 12.0, 11.0, 12.5, 0.0, 12.0), # full ask side ('ask', 12.0, 11.0, 12.5, 1.0, 12.5), # full last side ('ask', 12.0, 11.0, 12.5, 0.5, 12.25), # between bid and lat @@ -1886,6 +1892,7 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, ('ask', 10.11, 11.2, 11.0, 0.0, 10.11), ('ask', 0.001, 0.002, 11.0, 0.0, 0.001), ('ask', 0.006, 1.0, 11.0, 0.0, 0.006), + ('ask', 0.006, 1.0, 11.0, None, 0.006), ]) def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, last, last_ab, expected) -> None: From 45b7a0c8377a5493496abafa70e48b0872d0b4b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 7 Oct 2021 07:12:45 +0200 Subject: [PATCH 065/109] Add Test and docs for MaxDrawDownHyperOptLoss --- docs/hyperopt.md | 18 ++++++++++-------- freqtrade/constants.py | 3 ++- tests/optimize/test_hyperoptloss.py | 5 +++-- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 09d43939a..45e0d444d 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -60,7 +60,7 @@ optional arguments: Specify what timerange of data to use. --data-format-ohlcv {json,jsongz,hdf5} Storage format for downloaded candle (OHLCV) data. - (default: `None`). + (default: `json`). --max-open-trades INT Override the value of the `max_open_trades` configuration setting. @@ -114,7 +114,8 @@ optional arguments: Hyperopt-loss-functions are: ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, - SortinoHyperOptLoss, SortinoHyperOptLossDaily + SortinoHyperOptLoss, SortinoHyperOptLossDaily, + MaxDrawDownHyperOptLoss --disable-param-export Disable automatic hyperopt parameter export. @@ -512,12 +513,13 @@ This class should be in its own file within the `user_data/hyperopts/` directory Currently, the following loss functions are builtin: -* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. -* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) -* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) -* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) -* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation) -* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation) +* `ShortTradeDurHyperOptLoss` - (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. +* `OnlyProfitHyperOptLoss` - takes only amount of profit into consideration. +* `SharpeHyperOptLoss` - optimizes Sharpe Ratio calculated on trade returns relative to standard deviation. +* `SharpeHyperOptLossDaily` - optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation. +* `SortinoHyperOptLoss` - optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation. +* `SortinoHyperOptLossDaily` - optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation. +* `MaxDrawDownHyperOptLoss` - Optimizes Maximum drawdown. Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index fca319a0f..c6b8f0e62 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -24,7 +24,8 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', - 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] + 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily', + 'MaxDrawDownHyperOptLoss'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'OffsetFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py index 923e3fc32..a39190934 100644 --- a/tests/optimize/test_hyperoptloss.py +++ b/tests/optimize/test_hyperoptloss.py @@ -84,13 +84,14 @@ def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> "SortinoHyperOptLossDaily", "SharpeHyperOptLoss", "SharpeHyperOptLossDaily", + "MaxDrawDownHyperOptLoss", ]) def test_loss_functions_better_profits(default_conf, hyperopt_results, lossfunction) -> None: results_over = hyperopt_results.copy() - results_over['profit_abs'] = hyperopt_results['profit_abs'] * 2 + results_over['profit_abs'] = hyperopt_results['profit_abs'] * 2 + 0.2 results_over['profit_ratio'] = hyperopt_results['profit_ratio'] * 2 results_under = hyperopt_results.copy() - results_under['profit_abs'] = hyperopt_results['profit_abs'] / 2 + results_under['profit_abs'] = hyperopt_results['profit_abs'] / 2 - 0.2 results_under['profit_ratio'] = hyperopt_results['profit_ratio'] / 2 default_conf.update({'hyperopt_loss': lossfunction}) From a1be6124f221d6e3bd294c376a76848ff3e6d197 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 7 Oct 2021 07:15:09 +0200 Subject: [PATCH 066/109] Don't set bid_last_balance if None in tests part of #5681 --- tests/exchange/test_exchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8cb494edb..e3369182d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1899,7 +1899,8 @@ def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, caplog.set_level(logging.DEBUG) default_conf['ask_strategy']['price_side'] = side - default_conf['ask_strategy']['bid_last_balance'] = last_ab + if last_ab is not None: + default_conf['ask_strategy']['bid_last_balance'] = last_ab mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'ask': ask, 'bid': bid, 'last': last}) pair = "ETH/BTC" From e367f84b06896304405a8370f686538ee3c635ac Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 6 Oct 2021 01:39:02 -0600 Subject: [PATCH 067/109] 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 f07eeddda0e912e44467bff42b6c08f125902ae1 Mon Sep 17 00:00:00 2001 From: Robert Davey Date: Thu, 7 Oct 2021 12:04:42 +0100 Subject: [PATCH 068/109] Update api_schemas.py Fix api schema for cpu_pct float List. --- freqtrade/rpc/api_server/api_schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index bde6af35b..e9985c3c6 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -350,5 +350,5 @@ class BacktestResponse(BaseModel): class SysInfo(BaseModel): - cpu_pct: float + cpu_pct: List[float] ram_pct: float From 1327c21d0103eee5e040e03e598662c993bcfac8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=AE=AE=E0=AE=A9=E0=AF=8B=E0=AE=9C=E0=AF=8D=E0=AE=95?= =?UTF-8?q?=E0=AF=81=E0=AE=AE=E0=AE=BE=E0=AE=B0=E0=AF=8D=20=E0=AE=AA?= =?UTF-8?q?=E0=AE=B4=E0=AE=A9=E0=AE=BF=E0=AE=9A=E0=AF=8D=E0=AE=9A=E0=AE=BE?= =?UTF-8?q?=E0=AE=AE=E0=AE=BF?= Date: Thu, 7 Oct 2021 19:12:09 +0530 Subject: [PATCH 069/109] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 01effd7bc..79763983d 100644 --- a/README.md +++ b/README.md @@ -66,9 +66,7 @@ Please find the complete documentation on our [website](https://www.freqtrade.io Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. ```bash -git clone -b develop https://github.com/freqtrade/freqtrade.git -cd freqtrade -./setup.sh --install +pip install freqtrade ``` For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/). From 482f4418c68c2b99635a7af3cc65a14c1022e031 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 8 Oct 2021 14:36:52 +0200 Subject: [PATCH 070/109] Clarify "required candle" message --- freqtrade/exchange/exchange.py | 62 ++++++++++++++-------------------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7cc436430..b6cfb8d8b 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, - remove_credentials, retrier, retrier_async) -from freqtrade.misc import chunks, deep_merge_dicts, safe_value_fallback2 + EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED, retrier, + retrier_async) +from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist @@ -54,16 +54,12 @@ 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, @@ -104,7 +100,6 @@ 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') @@ -174,7 +169,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 = {}) -> ccxt.Exchange: + ccxt_kwargs: dict = None) -> ccxt.Exchange: """ Initialize ccxt with given config and return valid ccxt instance. @@ -193,10 +188,6 @@ 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: @@ -480,7 +471,7 @@ class Exchange: if startup_candles + 5 > candle_limit: raise OperationalException( f"This strategy requires {startup_candles} candles to start. " - f"{self.name} only provides {candle_limit} for {timeframe}.") + f"{self.name} only provides {candle_limit - 5} for {timeframe}.") def exchange_has(self, endpoint: str) -> bool: """ @@ -523,7 +514,7 @@ class Exchange: precision = self.markets[pair]['precision']['price'] missing = price % precision if missing != 0: - price = round(price - missing + precision, 10) + price = price - missing + precision else: symbol_prec = self.markets[pair]['precision']['price'] big_price = price * pow(10, symbol_prec) @@ -725,8 +716,7 @@ class Exchange: 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}) + params.update({'timeInForce': time_in_force}) try: # Set the precision for amount and price(rate) as accepted by the exchange @@ -1058,7 +1048,7 @@ class Exchange: ticker_rate = ticker[conf_strategy['price_side']] if ticker['last'] and ticker_rate: if side == 'buy' and ticker_rate > ticker['last']: - balance = conf_strategy.get('ask_last_balance', 0.0) + balance = conf_strategy['ask_last_balance'] ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) elif side == 'sell' and ticker_rate < ticker['last']: balance = conf_strategy.get('bid_last_balance', 0.0) @@ -1195,7 +1185,7 @@ class Exchange: # Historic data def get_historic_ohlcv(self, pair: str, timeframe: str, - since_ms: int, is_new_pair: bool = False) -> List: + since_ms: int) -> List: """ Get candle history using asyncio and returns the list of candles. Handles all async work for this. @@ -1207,7 +1197,7 @@ class Exchange: """ return asyncio.get_event_loop().run_until_complete( self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, - since_ms=since_ms, is_new_pair=is_new_pair)) + since_ms=since_ms)) def get_historic_ohlcv_as_df(self, pair: str, timeframe: str, since_ms: int) -> DataFrame: @@ -1222,12 +1212,11 @@ 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, is_new_pair: bool - ) -> List: + async def _async_get_historic_ohlcv(self, pair: str, + timeframe: str, + since_ms: int) -> 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) @@ -1240,22 +1229,21 @@ class Exchange: pair, timeframe, since) for since in range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)] - data: List = [] - # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling - for input_coro in chunks(input_coroutines, 100): + results = await asyncio.gather(*input_coroutines, return_exceptions=True) - 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) + # 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) # Sort data again after extending the result - above calls return in "async order" data = sorted(data, key=lambda x: x[0]) - logger.info(f"Downloaded data for {pair} with length {len(data)}.") + logger.info("Downloaded data for %s with length %s.", pair, len(data)) return data def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, From 11ec1d9b062ae7a063d8b6549cdfd5e468047d63 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 8 Oct 2021 20:22:07 +0200 Subject: [PATCH 071/109] Revert previous commit --- freqtrade/exchange/exchange.py | 60 ++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b6cfb8d8b..4143b79a5 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, @@ -100,6 +104,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') @@ -169,7 +174,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. @@ -188,6 +193,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: @@ -514,7 +523,7 @@ class Exchange: precision = self.markets[pair]['precision']['price'] missing = price % precision if missing != 0: - price = price - missing + precision + price = round(price - missing + precision, 10) else: symbol_prec = self.markets[pair]['precision']['price'] big_price = price * pow(10, symbol_prec) @@ -716,7 +725,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 @@ -1048,7 +1058,7 @@ class Exchange: ticker_rate = ticker[conf_strategy['price_side']] if ticker['last'] and ticker_rate: if side == 'buy' and ticker_rate > ticker['last']: - balance = conf_strategy['ask_last_balance'] + balance = conf_strategy.get('ask_last_balance', 0.0) ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate) elif side == 'sell' and ticker_rate < ticker['last']: balance = conf_strategy.get('bid_last_balance', 0.0) @@ -1185,7 +1195,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. @@ -1197,7 +1207,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: @@ -1212,11 +1222,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) @@ -1229,21 +1240,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, *, From 30bc96cf3f802a882e4e5e35e0c0df8db876acfc Mon Sep 17 00:00:00 2001 From: sid Date: Sat, 9 Oct 2021 06:36:23 +0530 Subject: [PATCH 072/109] simplify expression --- freqtrade/optimize/hyperopt_loss_max_drawdown.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_max_drawdown.py b/freqtrade/optimize/hyperopt_loss_max_drawdown.py index 6777fb2e8..ce955d928 100644 --- a/freqtrade/optimize/hyperopt_loss_max_drawdown.py +++ b/freqtrade/optimize/hyperopt_loss_max_drawdown.py @@ -38,6 +38,4 @@ class MaxDrawDownHyperOptLoss(IHyperOptLoss): except ValueError: # No losing trade, therefore no drawdown. return -total_profit - max_drawdown_rev = 1 / max_drawdown[0] - ret = max_drawdown_rev * total_profit - return -ret + return -total_profit / max_drawdown[0] From 7b1c888665b214aee599958bf0a5b9656d13531b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Oct 2021 08:39:32 +0200 Subject: [PATCH 073/109] Add FAQ entry for incomplete candles closes #5687 --- docs/faq.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 75c40a681..d9777ddf1 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -54,9 +54,11 @@ you can't say much from few trades. Yes. You can edit your config and use the `/reload_config` command to reload the configuration. The bot will stop, reload the configuration and strategy and will restart with the new configuration and strategy. -### I want to improve the bot with a new strategy +### I want to use incomplete candles -That's great. We have a nice backtesting and hyperoptimization setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). +Freqtrade will not provide incomplete candles to strategies. Using incomplete candles will lead to repainting and consequently to strategies with "ghost" buys, which are impossible to both backtest, and verify after they happened. + +You can use "current" market data by using the [dataprovider](strategy-customization.md#orderbookpair-maximum)'s orderbook or ticker methods - which however cannot be used during backtesting. ### Is there a setting to only SELL the coins being held and not perform anymore BUYS? From 2c68342140979f7ade3e271d6841b9be81590b51 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Oct 2021 10:37:33 +0200 Subject: [PATCH 074/109] Move pypi installation to documentation --- README.md | 8 +++++--- docs/installation.md | 7 +++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 79763983d..0a4d6424e 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Please find the complete documentation on our [website](https://www.freqtrade.io - [x] **Dry-run**: Run the bot without paying money. - [x] **Backtesting**: Run a simulation of your buy/sell strategy. - [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. -- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/latest/edge/). +- [x] **Edge position sizing** Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. [Learn more](https://www.freqtrade.io/en/stable/edge/). - [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade or use dynamic whitelists. - [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. - [x] **Manageable via Telegram**: Manage the bot with Telegram. @@ -66,10 +66,12 @@ Please find the complete documentation on our [website](https://www.freqtrade.io Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. ```bash -pip install freqtrade +git clone -b develop https://github.com/freqtrade/freqtrade.git +cd freqtrade +./setup.sh --install ``` -For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/latest/installation/). +For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/stable/installation/). ## Basic Usage diff --git a/docs/installation.md b/docs/installation.md index 5e4a19d88..d468786d3 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -113,6 +113,13 @@ git checkout develop You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands. +??? Note "Install from pypi" + An alternative way to install Freqtrade is from [pypi](https://pypi.org/project/freqtrade/). The downside is that this method requires ta-lib to be correctly installed beforehand, and is therefore currently not the recommended way to install Freqtrade. + + ``` bash + pip install freqtrade + ``` + ------ ## Script Installation From 1a3b41ed9718338c6458de3f87527591337a03cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 9 Oct 2021 15:35:39 +0200 Subject: [PATCH 075/109] Rephrase and simplify UI access section in docker quickstart --- docs/docker_quickstart.md | 40 +++++++++------------------------------ 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index cf525b926..95df37811 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -70,39 +70,17 @@ docker-compose up -d !!! Warning "Default configuration" While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. -#### Acessing the UI +#### Accessing the UI -Uncommend the 2 lines below and add your IP adress in the following format (like 192.168.2.67:8080:8080) to the ft_userdata/docker-compose.yml: -'''bash - ports: - - "yourIPadress:8080:8080" -''' -Your ft_userdata/user_data/config.json should look like: -'''bash -api_server": { - "enabled": true, - "listen_ip_address": "0.0.0.0", - "listen_port": 8080, - "verbosity": "error", - "enable_openapi": false, - "jwt_secret_key": "****", - "CORS_origins": [], - "username": "****", - "password": "****" - }, -''' -instead of "****" you will have your data in. -Then rebuild your docker file: -Linux: -'''bash -sudo docker-compose down && sudo docker-compose pull && sudo docker-compose build && sudo docker-compose up -d -''' -Windows: -'''bash -docker-compose down && docker-compose pull && docker-compose build && docker-compose up -d -''' +If you've selected to enable FreqUI in the `new-config` step, you will have freqUI available at port `localhost:8080`. -You can now access the UI by typing yourIPadress:8080 in your browser. +You can now access the UI by typing localhost:8080 in your browser. + +??? Note "UI Access on a remote servers" + If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot. + This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box). + Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet. + Please also read the [API configuration with docker](rest-api.md#configuration-with-docker) section to learn more about this configuration. #### Monitoring the bot From 39be675f1f1e4da03619b6e3dc99c2953cecd63e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 9 Oct 2021 10:39:14 -0600 Subject: [PATCH 076/109] 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 077/109] 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 078/109] 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 079/109] 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 080/109] 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 081/109] 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 57095d7167aa481bf184f19f39acfeded8149a7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 03:01:22 +0000 Subject: [PATCH 082/109] Bump wrapt from 1.12.1 to 1.13.1 Bumps [wrapt](https://github.com/GrahamDumpleton/wrapt) from 1.12.1 to 1.13.1. - [Release notes](https://github.com/GrahamDumpleton/wrapt/releases) - [Changelog](https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst) - [Commits](https://github.com/GrahamDumpleton/wrapt/compare/1.12.1...1.13.1) --- updated-dependencies: - dependency-name: wrapt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7fe06b9d2..4eb2bf66b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ arrow==1.1.1 cachetools==4.2.2 requests==2.26.0 urllib3==1.26.7 -wrapt==1.12.1 +wrapt==1.13.1 jsonschema==4.0.1 TA-Lib==0.4.21 technical==1.3.0 From 7323ffa25a408a64a94ce577caeece6530bdfa1b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 03:01:31 +0000 Subject: [PATCH 083/109] Bump blosc from 1.10.4 to 1.10.6 Bumps [blosc](https://github.com/blosc/python-blosc) from 1.10.4 to 1.10.6. - [Release notes](https://github.com/blosc/python-blosc/releases) - [Changelog](https://github.com/Blosc/python-blosc/blob/master/RELEASE_NOTES.rst) - [Commits](https://github.com/blosc/python-blosc/compare/v1.10.4...v1.10.6) --- updated-dependencies: - dependency-name: blosc dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7fe06b9d2..3a49977c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ tabulate==0.8.9 pycoingecko==2.2.0 jinja2==3.0.1 tables==3.6.1 -blosc==1.10.4 +blosc==1.10.6 # find first, C search in arrays py_find_1st==1.1.5 From 5fb0401dca6b9ae19c678b0a7577129969901ff9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 03:01:39 +0000 Subject: [PATCH 084/109] Bump cryptography from 3.4.8 to 35.0.0 Bumps [cryptography](https://github.com/pyca/cryptography) from 3.4.8 to 35.0.0. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/3.4.8...35.0.0) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7fe06b9d2..89728f782 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas-ta==0.3.14b ccxt==1.57.38 # Pin cryptography for now due to rust build errors with piwheels -cryptography==3.4.8 +cryptography==35.0.0 aiohttp==3.7.4.post0 SQLAlchemy==1.4.25 python-telegram-bot==13.7 From 3fdc62d29c3f81f4ebdd826e0cd5ad21cde6d1a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 05:05:14 +0000 Subject: [PATCH 085/109] Bump flake8 from 3.9.2 to 4.0.0 Bumps [flake8](https://github.com/pycqa/flake8) from 3.9.2 to 4.0.0. - [Release notes](https://github.com/pycqa/flake8/releases) - [Commits](https://github.com/pycqa/flake8/compare/3.9.2...4.0.0) --- updated-dependencies: - dependency-name: flake8 dependency-type: direct:development update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index a3ed37bea..3f2d6035d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ -r requirements-hyperopt.txt coveralls==3.2.0 -flake8==3.9.2 +flake8==4.0.0 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.4.1 mypy==0.910 From e467491dbecc7f49444c60471485daacf546dfa7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 05:05:23 +0000 Subject: [PATCH 086/109] Bump jsonschema from 4.0.1 to 4.1.0 Bumps [jsonschema](https://github.com/Julian/jsonschema) from 4.0.1 to 4.1.0. - [Release notes](https://github.com/Julian/jsonschema/releases) - [Changelog](https://github.com/Julian/jsonschema/blob/main/CHANGELOG.rst) - [Commits](https://github.com/Julian/jsonschema/compare/v4.0.1...v4.1.0) --- updated-dependencies: - dependency-name: jsonschema dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4eb2bf66b..641264d53 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ cachetools==4.2.2 requests==2.26.0 urllib3==1.26.7 wrapt==1.13.1 -jsonschema==4.0.1 +jsonschema==4.1.0 TA-Lib==0.4.21 technical==1.3.0 tabulate==0.8.9 From afc086f33c85162107a27dda013544d37ced3a51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 05:05:30 +0000 Subject: [PATCH 087/109] Bump arrow from 1.1.1 to 1.2.0 Bumps [arrow](https://github.com/arrow-py/arrow) from 1.1.1 to 1.2.0. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/commits) --- updated-dependencies: - dependency-name: arrow dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4eb2bf66b..90ef02ae5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ cryptography==3.4.8 aiohttp==3.7.4.post0 SQLAlchemy==1.4.25 python-telegram-bot==13.7 -arrow==1.1.1 +arrow==1.2.0 cachetools==4.2.2 requests==2.26.0 urllib3==1.26.7 From 32174f8f906304f61e7f21afa3669eab4458dc5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 05:14:08 +0000 Subject: [PATCH 088/109] Bump pyjwt from 2.1.0 to 2.2.0 Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.1.0 to 2.2.0. - [Release notes](https://github.com/jpadilla/pyjwt/releases) - [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst) - [Commits](https://github.com/jpadilla/pyjwt/compare/2.1.0...2.2.0) --- updated-dependencies: - dependency-name: pyjwt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4eb2bf66b..133aee144 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ tabulate==0.8.9 pycoingecko==2.2.0 jinja2==3.0.1 tables==3.6.1 -blosc==1.10.4 +blosc==1.10.6 # find first, C search in arrays py_find_1st==1.1.5 @@ -34,7 +34,7 @@ sdnotify==0.3.2 # API Server fastapi==0.68.1 uvicorn==0.15.0 -pyjwt==2.1.0 +pyjwt==2.2.0 aiofiles==0.7.0 psutil==5.8.0 From 4921a4caecd0cfbffa7df3894f4649876d4dd28a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 05:14:19 +0000 Subject: [PATCH 089/109] Bump jinja2 from 3.0.1 to 3.0.2 Bumps [jinja2](https://github.com/pallets/jinja) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.0.1...3.0.2) --- updated-dependencies: - dependency-name: jinja2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1d9aaf4b5..e2de4f629 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,7 +18,7 @@ TA-Lib==0.4.21 technical==1.3.0 tabulate==0.8.9 pycoingecko==2.2.0 -jinja2==3.0.1 +jinja2==3.0.2 tables==3.6.1 blosc==1.10.6 From 90ea3d444060359320ec534044785699617e0cb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 05:18:24 +0000 Subject: [PATCH 090/109] Bump mkdocs-material from 7.3.1 to 7.3.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.3.1 to 7.3.2. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.3.1...7.3.2) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index bbbb240ba..9a733d8f7 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.2 -mkdocs-material==7.3.1 +mkdocs-material==7.3.2 mdx_truly_sane_lists==1.2 pymdown-extensions==9.0 From 29371b2f28591a611e039bad4da718ae726dfb23 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 06:15:03 +0000 Subject: [PATCH 091/109] Bump joblib from 1.0.1 to 1.1.0 Bumps [joblib](https://github.com/joblib/joblib) from 1.0.1 to 1.1.0. - [Release notes](https://github.com/joblib/joblib/releases) - [Changelog](https://github.com/joblib/joblib/blob/master/CHANGES.rst) - [Commits](https://github.com/joblib/joblib/compare/1.0.1...1.1.0) --- updated-dependencies: - dependency-name: joblib dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index b4067d1db..96690bcbb 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,6 +6,6 @@ scipy==1.7.1 scikit-learn==0.24.2 scikit-optimize==0.8.1 filelock==3.3.0 -joblib==1.0.1 +joblib==1.1.0 psutil==5.8.0 progressbar2==3.53.3 From 802599bdc99cdd82e390d51cdb000c87d03e96a5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 06:15:32 +0000 Subject: [PATCH 092/109] Bump ccxt from 1.57.38 to 1.57.94 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.57.38 to 1.57.94. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.57.38...1.57.94) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 788801987..ff9ea9423 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.2 pandas==1.3.3 pandas-ta==0.3.14b -ccxt==1.57.38 +ccxt==1.57.94 # Pin cryptography for now due to rust build errors with piwheels cryptography==35.0.0 aiohttp==3.7.4.post0 @@ -34,7 +34,7 @@ sdnotify==0.3.2 # API Server fastapi==0.68.1 uvicorn==0.15.0 -pyjwt==2.1.0 +pyjwt==2.2.0 aiofiles==0.7.0 psutil==5.8.0 From 855b26f846fdda1eeb3314db2218a87835519eb1 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 11 Oct 2021 01:31:21 -0600 Subject: [PATCH 093/109] 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 fa00b52c4742cbe7b2ec4c583ee5a5828ca00ff7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Oct 2021 07:41:16 +0000 Subject: [PATCH 094/109] Bump scikit-learn from 0.24.2 to 1.0 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 0.24.2 to 1.0. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/0.24.2...1.0) --- updated-dependencies: - dependency-name: scikit-learn dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 96690bcbb..edd078e9e 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.7.1 -scikit-learn==0.24.2 +scikit-learn==1.0 scikit-optimize==0.8.1 filelock==3.3.0 joblib==1.1.0 From d5a1385fdc1d1aa35c7bb6cf6f230c3fcd6fa24f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 11 Oct 2021 04:14:59 -0600 Subject: [PATCH 095/109] 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 096/109] 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 097/109] 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 098/109] 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 396bc9b2e3d33993052a3b2038cad9ee19d3736d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Oct 2021 20:00:53 +0200 Subject: [PATCH 099/109] Version bump flake8-tidy-imports to 4.5.0 --- requirements-dev.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3f2d6035d..74ebee479 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,8 +5,7 @@ coveralls==3.2.0 flake8==4.0.0 -flake8-type-annotations==0.1.0 -flake8-tidy-imports==4.4.1 +flake8-tidy-imports==4.5.0 mypy==0.910 pytest==6.2.5 pytest-asyncio==0.15.1 From 70000b58434cbde9952232f87ec2e6e8d241e21f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 11 Oct 2021 20:28:23 +0200 Subject: [PATCH 100/109] 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 101/109] 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 ce9debe9fd89bab9171f35d3632f88f27c0d080a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Oct 2021 06:44:07 +0200 Subject: [PATCH 102/109] Add version argument to freqUI installer --- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 6 ++++++ freqtrade/commands/deploy_commands.py | 16 ++++++++++++---- tests/commands/test_commands.py | 27 ++++++++++++++++++++++----- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 9643705a5..a02faa736 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -73,7 +73,7 @@ ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source", "timeframe", "plot_auto_open"] -ARGS_INSTALL_UI = ["erase_ui_only"] +ARGS_INSTALL_UI = ["erase_ui_only", 'ui_version'] ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index d350a9426..30a9b0137 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -414,6 +414,12 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', default=False, ), + "ui_version": Arg( + '--ui-version', + help=('Specify a specific version of FreqUI to install. ' + 'Not specifying this installs the latest version.'), + type=str, + ), # Templating options "template": Arg( '--template', diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 4f9e5bbad..92c9adf66 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -128,7 +128,7 @@ def download_and_install_ui(dest_folder: Path, dl_url: str, version: str): f.write(version) -def get_ui_download_url() -> Tuple[str, str]: +def get_ui_download_url(version: Optional[str] = None) -> Tuple[str, str]: base_url = 'https://api.github.com/repos/freqtrade/frequi/' # Get base UI Repo path @@ -136,8 +136,16 @@ def get_ui_download_url() -> Tuple[str, str]: resp.raise_for_status() r = resp.json() - latest_version = r[0]['name'] - assets = r[0].get('assets', []) + if version: + tmp = [x for x in r if x['name'] == version] + if tmp: + latest_version = tmp[0]['name'] + assets = tmp[0].get('assets', []) + else: + raise ValueError("UI-Version not found.") + else: + latest_version = r[0]['name'] + assets = r[0].get('assets', []) dl_url = '' if assets and len(assets) > 0: dl_url = assets[0]['browser_download_url'] @@ -156,7 +164,7 @@ def start_install_ui(args: Dict[str, Any]) -> None: dest_folder = Path(__file__).parents[1] / 'rpc/api_server/ui/installed/' # First make sure the assets are removed. - dl_url, latest_version = get_ui_download_url() + dl_url, latest_version = get_ui_download_url(args.get('ui_version')) curr_version = read_ui_version(dest_folder) if curr_version == latest_version and not args.get('erase_ui_only'): diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 8889617ba..6a0e741d9 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -605,16 +605,33 @@ def test_get_ui_download_url(mocker): def test_get_ui_download_url_direct(mocker): response = MagicMock() response.json = MagicMock( - side_effect=[[{ - 'assets_url': 'http://whatever.json', - 'name': '0.0.1', - 'assets': [{'browser_download_url': 'http://download11.zip'}]}]]) + return_value=[ + { + 'assets_url': 'http://whatever.json', + 'name': '0.0.2', + 'assets': [{'browser_download_url': 'http://download22.zip'}] + }, + { + 'assets_url': 'http://whatever.json', + 'name': '0.0.1', + 'assets': [{'browser_download_url': 'http://download1.zip'}] + }, + ]) get_mock = mocker.patch("freqtrade.commands.deploy_commands.requests.get", return_value=response) x, last_version = get_ui_download_url() assert get_mock.call_count == 1 + assert last_version == '0.0.2' + assert x == 'http://download22.zip' + get_mock.reset_mock() + response.json.reset_mock() + + x, last_version = get_ui_download_url('0.0.1') assert last_version == '0.0.1' - assert x == 'http://download11.zip' + assert x == 'http://download1.zip' + + with pytest.raises(ValueError, match="UI-Version not found."): + x, last_version = get_ui_download_url('0.0.3') def test_download_data_keyboardInterrupt(mocker, caplog, markets): From 86cbd0039ff9ff270009e6517005b70c0c0812fb Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 12 Oct 2021 02:24:35 -0600 Subject: [PATCH 103/109] 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 8798ae567744ceefed92577acbd66fb974df6b15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Oct 2021 19:06:23 +0200 Subject: [PATCH 104/109] Version bump also scikit-optimize --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index edd078e9e..e97e78638 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -4,7 +4,7 @@ # Required for hyperopt scipy==1.7.1 scikit-learn==1.0 -scikit-optimize==0.8.1 +scikit-optimize==0.9.0 filelock==3.3.0 joblib==1.1.0 psutil==5.8.0 From f290ff5c9aa68c06fc162a463097fc34b1fd8594 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 12 Oct 2021 19:10:38 +0200 Subject: [PATCH 105/109] 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 106/109] 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 107/109] 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 108/109] 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 109/109] 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"),