Merge remote-tracking branch 'origin/feat/binance_liq' into feat/binance_liq

This commit is contained in:
Arunavo Ray 2021-09-19 11:49:27 +05:30
commit bc835a1a00
20 changed files with 607 additions and 534 deletions

View File

@ -98,6 +98,38 @@ class MyAwesomeStrategy(IStrategy):
!!! Note !!! Note
All overrides are optional and can be mixed/matched as necessary. 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 ## Space options
For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types: For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types:

View File

@ -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. 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). 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).

View File

@ -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)

View File

@ -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_setup import setup_utils_configuration
from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.config_validation import validate_config_consistency
from freqtrade.configuration.configuration import Configuration from freqtrade.configuration.configuration import Configuration
from freqtrade.configuration.PeriodicCache import PeriodicCache
from freqtrade.configuration.timerange import TimeRange from freqtrade.configuration.timerange import TimeRange

View File

@ -16,10 +16,11 @@ from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.enums import RPCMessageType, SellType, State from freqtrade.enums import Collateral, RPCMessageType, SellType, State, TradingMode
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
InvalidOrderException, PricingError) InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.maintenance_margin import MaintenanceMargin
from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.misc import safe_value_fallback, safe_value_fallback2
from freqtrade.mixins import LoggingMixin from freqtrade.mixins import LoggingMixin
from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db
@ -41,6 +42,9 @@ class FreqtradeBot(LoggingMixin):
This is from here the bot start its logic. This is from here the bot start its logic.
""" """
collateral: Optional[Collateral] = None
trading_mode: TradingMode = TradingMode.SPOT
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
""" """
Init all variables and objects the bot needs to work Init all variables and objects the bot needs to work
@ -104,6 +108,22 @@ class FreqtradeBot(LoggingMixin):
self._exit_lock = Lock() self._exit_lock = Lock()
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
if self.config.get("trading_mode"):
self.trading_mode = TradingMode(self.config.get("trading_mode"))
if self.config.get('collateral'):
self.collateral = Collateral(self.config.get('collateral'))
# Start calculating maintenance margin if on cross margin
# TODO: Add margin_mode to freqtrade.configuration?
if self.collateral == Collateral.CROSS:
self.maintenance_margin = MaintenanceMargin(
exchange_name=self.exchange.name,
trading_mode=self.trading_mode)
self.maintenance_margin.run()
def notify_status(self, msg: str) -> None: def notify_status(self, msg: str) -> None:
""" """
Public method for users of this class (worker, etc.) to send notifications Public method for users of this class (worker, etc.) to send notifications
@ -588,6 +608,9 @@ class FreqtradeBot(LoggingMixin):
if order_status == 'closed': if order_status == 'closed':
self.update_trade_state(trade, order_id, order) self.update_trade_state(trade, order_id, order)
if self.collateral == Collateral.CROSS:
self.maintenance_margin.add_new_trade(trade)
Trade.query.session.add(trade) Trade.query.session.add(trade)
Trade.commit() Trade.commit()
@ -1165,9 +1188,18 @@ class FreqtradeBot(LoggingMixin):
reason='Auto lock') reason='Auto lock')
self._notify_exit(trade, order_type) self._notify_exit(trade, order_type)
self._remove_maintenance_trade(trade)
return True return True
def _remove_maintenance_trade(self, trade: Trade):
"""
Removes a trade from the maintenance margin object
:param trade: The trade to remove from the maintenance margin
"""
if self.collateral == Collateral.CROSS:
self.maintenance_margin.remove_trade(trade)
def _notify_exit(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. Sends rpc notification when a sell occurred.

View File

@ -19,6 +19,7 @@ def liquidation_price(
entry_price_1: Optional[float], entry_price_1: Optional[float],
maintenance_margin_rate: Optional[float] maintenance_margin_rate: Optional[float]
) -> Optional[float]: ) -> Optional[float]:
if trading_mode == TradingMode.SPOT: if trading_mode == TradingMode.SPOT:
return None return None

View File

@ -0,0 +1,52 @@
from typing import List
from freqtrade.enums import TradingMode
from freqtrade.leverage import liquidation_price
from freqtrade.persistence import Trade
class MaintenanceMargin:
trades: List[Trade]
exchange_name: str
trading_mode: TradingMode
@property
def margin_level(self):
# This is the current value of all assets,
# and if you pass below liq_level, you are liquidated
# TODO-lev: Add args to formula
return liquidation_price(
trading_mode=self.trading_mode,
exchange_name=self.exchange_name
)
@property
def liq_level(self): # This may be a constant value and may not need a function
# TODO-lev: The is the value that you are liquidated at
return # If constant, would need to be recalculated after each new trade
def __init__(self, exchange_name: str, trading_mode: TradingMode):
self.exchange_name = exchange_name
self.trading_mode = trading_mode
return
def add_new_trade(self, trade):
self.trades.append(trade)
def remove_trade(self, trade):
self.trades.remove(trade)
# ? def update_trade_pric(self):
def sell_all(self):
# TODO-lev
return
def run(self):
# TODO-lev: implement a thread that constantly updates with every price change,
# TODO-lev: must update at least every few seconds or so
# while true:
# if self.margin_level <= self.liq_level:
# self.sell_all()
return

View File

@ -45,7 +45,7 @@ progressbar.streams.wrap_stdout()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
INITIAL_POINTS = 30 INITIAL_POINTS = 5
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models # Keep no more than SKOPT_MODEL_QUEUE_SIZE models
# in the skopt model queue, to optimize memory consumption # in the skopt model queue, to optimize memory consumption
@ -241,7 +241,7 @@ class Hyperopt:
if HyperoptTools.has_space(self.config, 'buy'): if HyperoptTools.has_space(self.config, 'buy'):
logger.debug("Hyperopt has 'buy' space") 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'): if HyperoptTools.has_space(self.config, 'sell'):
logger.debug("Hyperopt has 'sell' space") logger.debug("Hyperopt has 'sell' space")
@ -365,10 +365,20 @@ class Hyperopt:
} }
def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: 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( return Optimizer(
dimensions, dimensions,
base_estimator="ET", base_estimator=estimator,
acq_optimizer="auto", acq_optimizer=acq_optimizer,
n_initial_points=INITIAL_POINTS, n_initial_points=INITIAL_POINTS,
acq_optimizer_kwargs={'n_jobs': cpu_count}, acq_optimizer_kwargs={'n_jobs': cpu_count},
random_state=self.random_state, random_state=self.random_state,

View File

@ -12,7 +12,7 @@ from freqtrade.exceptions import OperationalException
with suppress(ImportError): with suppress(ImportError):
from skopt.space import Dimension 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: def _format_exception_message(space: str) -> str:
@ -56,7 +56,7 @@ class HyperOptAuto(IHyperOpt):
else: else:
_format_exception_message(category) _format_exception_message(category)
def indicator_space(self) -> List['Dimension']: def buy_indicator_space(self) -> List['Dimension']:
return self._get_indicator_space('buy') return self._get_indicator_space('buy')
def sell_indicator_space(self) -> List['Dimension']: def sell_indicator_space(self) -> List['Dimension']:
@ -79,3 +79,6 @@ class HyperOptAuto(IHyperOpt):
def trailing_space(self) -> List['Dimension']: def trailing_space(self) -> List['Dimension']:
return self._get_func('trailing_space')() return self._get_func('trailing_space')()
def generate_estimator(self) -> EstimatorType:
return self._get_func('generate_estimator')()

View File

@ -5,8 +5,9 @@ This module defines the interface to apply for hyperopt
import logging import logging
import math import math
from abc import ABC 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 skopt.space import Categorical, Dimension, Integer
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
@ -17,6 +18,8 @@ from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
EstimatorType = Union[RegressorMixin, str]
class IHyperOpt(ABC): class IHyperOpt(ABC):
""" """
@ -37,6 +40,14 @@ class IHyperOpt(ABC):
IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED
IHyperOpt.timeframe = str(config['timeframe']) 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]: def generate_roi_table(self, params: Dict) -> Dict[int, float]:
""" """
Create a ROI table. Create a ROI table.

View File

@ -14,9 +14,9 @@ from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.schema import UniqueConstraint from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
from freqtrade.enums import SellType from freqtrade.enums import Collateral, SellType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.leverage import interest from freqtrade.leverage import interest, liquidation_price
from freqtrade.misc import safe_value_fallback from freqtrade.misc import safe_value_fallback
from freqtrade.persistence.migrations import check_migrate from freqtrade.persistence.migrations import check_migrate
@ -265,6 +265,8 @@ class LocalTrade():
buy_tag: Optional[str] = None buy_tag: Optional[str] = None
timeframe: Optional[int] = None timeframe: Optional[int] = None
trading_mode: TradingMode
# Leverage trading properties # Leverage trading properties
is_short: bool = False is_short: bool = False
isolated_liq: Optional[float] = None isolated_liq: Optional[float] = None
@ -318,8 +320,9 @@ class LocalTrade():
for key in kwargs: for key in kwargs:
setattr(self, key, kwargs[key]) setattr(self, key, kwargs[key])
if self.isolated_liq: if self.isolated_liq:
self.set_isolated_liq(self.isolated_liq) self.set_isolated_liq(isolated_liq=self.isolated_liq)
self.recalc_open_trade_value() self.recalc_open_trade_value()
# TODO-lev: Throw exception if on margin and interest_rate is none
def _set_stop_loss(self, stop_loss: float, percent: float): def _set_stop_loss(self, stop_loss: float, percent: float):
""" """
@ -344,11 +347,25 @@ class LocalTrade():
self.stop_loss_pct = -1 * abs(percent) self.stop_loss_pct = -1 * abs(percent)
self.stoploss_last_update = datetime.utcnow() self.stoploss_last_update = datetime.utcnow()
def set_isolated_liq(self, isolated_liq: float): def set_isolated_liq(self, isolated_liq: Optional[float]):
""" """
Method you should use to set self.liquidation price. Method you should use to set self.liquidation price.
Assures stop_loss is not passed the liquidation price Assures stop_loss is not passed the liquidation price
""" """
if not isolated_liq:
isolated_liq = liquidation_price(
exchange_name=self.exchange,
open_rate=self.open_rate,
is_short=self.is_short,
leverage=self.leverage,
trading_mode=self.trading_mode,
collateral=Collateral.ISOLATED
)
if isolated_liq is None:
raise OperationalException(
"leverage/isolated_liq returned None. This exception should never happen"
)
if self.stop_loss is not None: if self.stop_loss is not None:
if self.is_short: if self.is_short:
self.stop_loss = min(self.stop_loss, isolated_liq) self.stop_loss = min(self.stop_loss, isolated_liq)

View File

@ -8,6 +8,7 @@ from typing import Any, Dict, List, Optional
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration import PeriodicCache
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.plugins.pairlist.IPairList import IPairList
@ -18,14 +19,15 @@ logger = logging.getLogger(__name__)
class AgeFilter(IPairList): class AgeFilter(IPairList):
# Checked symbols cache (dictionary of ticker symbol => timestamp)
_symbolsChecked: Dict[str, int] = {}
def __init__(self, exchange, pairlistmanager, def __init__(self, exchange, pairlistmanager,
config: Dict[str, Any], pairlistconfig: Dict[str, Any], config: Dict[str, Any], pairlistconfig: Dict[str, Any],
pairlist_pos: int) -> None: pairlist_pos: int) -> None:
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) 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._min_days_listed = pairlistconfig.get('min_days_listed', 10)
self._max_days_listed = pairlistconfig.get('max_days_listed', None) 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. :param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new allowlist :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: 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 = -( since_days = -(
self._max_days_listed if self._max_days_listed else self._min_days_listed self._max_days_listed if self._max_days_listed else self._min_days_listed
@ -118,5 +123,6 @@ class AgeFilter(IPairList):
" or more than " " or more than "
f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}" f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}"
) if self._max_days_listed else ''), logger.info) ) if self._max_days_listed else ''), logger.info)
self._symbolsCheckFailed[pair] = arrow.utcnow().int_timestamp * 1000
return False return False
return False return False

View File

@ -786,10 +786,11 @@ class IStrategy(ABC, HyperStrategyMixin):
Does not run advise_buy or advise_sell! Does not run advise_buy or advise_sell!
Used by optimize operations only, not during dry / live runs. Used by optimize operations only, not during dry / live runs.
Using .copy() to get a fresh copy of the dataframe for every strategy run. 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 Has positive effects on memory usage for whatever reason - also when
using only one strategy. 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()} for pair, pair_data in data.items()}
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@ -14,6 +14,8 @@ pytest-cov==2.12.1
pytest-mock==3.6.1 pytest-mock==3.6.1
pytest-random-order==1.0.4 pytest-random-order==1.0.4
isort==5.9.3 isort==5.9.3
# For datetime mocking
time-machine==2.4.0
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==6.1.0 nbconvert==6.1.0

View File

@ -6,6 +6,7 @@ from copy import deepcopy
from datetime import datetime, timedelta from datetime import datetime, timedelta
from functools import reduce from functools import reduce
from pathlib import Path from pathlib import Path
from typing import Tuple
from unittest.mock import MagicMock, Mock, PropertyMock from unittest.mock import MagicMock, Mock, PropertyMock
import arrow import arrow
@ -262,6 +263,10 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True):
Trade.query.session.flush() Trade.query.session.flush()
def get_sides(is_short: bool) -> Tuple[str, str]:
return ("sell", "buy") if is_short else ("buy", "sell")
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def patch_coingekko(mocker) -> None: def patch_coingekko(mocker) -> None:
""" """

View File

@ -1,11 +1,92 @@
import pytest
from math import isclose from math import isclose
import pytest from freqtrade.enums import Collateral, TradingMode
from freqtrade.enums import TradingMode, Collateral
from freqtrade.leverage import liquidation_price from freqtrade.leverage import liquidation_price
# from freqtrade.exceptions import OperationalException
spot = TradingMode.SPOT
margin = TradingMode.MARGIN
futures = TradingMode.FUTURES
cross = Collateral.CROSS
isolated = Collateral.ISOLATED
@pytest.mark.parametrize('exchange_name,open_rate,is_short,leverage,trading_mode,collateral', [
# Bittrex
('bittrex', "2.0", False, "3.0", spot, None),
('bittrex', "2.0", False, "1.0", spot, cross),
('bittrex', "2.0", True, "3.0", spot, isolated),
# Binance
('binance', "2.0", False, "3.0", spot, None),
('binance', "2.0", False, "1.0", spot, cross),
('binance', "2.0", True, "3.0", spot, isolated),
# Kraken
('kraken', "2.0", False, "3.0", spot, None),
('kraken', "2.0", True, "3.0", spot, cross),
('kraken', "2.0", False, "1.0", spot, isolated),
# FTX
('ftx', "2.0", True, "3.0", spot, None),
('ftx', "2.0", False, "3.0", spot, cross),
('ftx', "2.0", False, "3.0", spot, isolated),
])
def test_liquidation_price_is_none(
exchange_name,
open_rate,
is_short,
leverage,
trading_mode,
collateral
):
assert liquidation_price(
exchange_name,
open_rate,
is_short,
leverage,
trading_mode,
collateral,
1535443.01,
71200.81144,
-56354.57,
135365.00,
3683.979,
1456.84,
0.10,
) is None
@pytest.mark.parametrize('exchange_name,open_rate,is_short,leverage,trading_mode,collateral', [
# Bittrex
('bittrex', "2.0", False, "3.0", margin, cross),
('bittrex', "2.0", False, "3.0", margin, isolated),
('bittrex', "2.0", False, "3.0", futures, cross),
('bittrex', "2.0", False, "3.0", futures, isolated),
# Binance
# Binance supports isolated margin, but freqtrade likely won't for a while on Binance
('binance', "2.0", True, "3.0", margin, isolated),
# Kraken
('kraken', "2.0", False, "1.0", margin, isolated),
('kraken', "2.0", False, "1.0", futures, isolated),
# FTX
('ftx', "2.0", False, "3.0", margin, isolated),
('ftx', "2.0", False, "3.0", futures, isolated),
])
def test_liquidation_price_exception_thrown(
exchange_name,
open_rate,
is_short,
leverage,
trading_mode,
collateral,
result
):
# TODO-lev assert exception is thrown
return # Here to avoid indent error, remove when implemented
@pytest.mark.parametrize( @pytest.mark.parametrize(
'exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, ' 'exchange_name, open_rate, is_short, leverage, trading_mode, collateral, wallet_balance, '
'maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount, position_1, entry_price_1, ' 'maintenance_margin_ex_1, unrealized_pnl_ex_1, maintenance_amount, position_1, entry_price_1, '

View File

@ -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.buy_rsi.value != 35
assert hyperopt.backtesting.strategy.sell_rsi.value != 74 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(): def test_SKDecimal():
space = SKDecimal(1, 2, decimals=2) space = SKDecimal(1, 2, decimals=2)

View File

@ -4,6 +4,7 @@ import time
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import pytest import pytest
import time_machine
from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.constants import AVAILABLE_PAIRLISTS
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
@ -815,18 +816,17 @@ 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): def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history):
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
ohlcv_data = { ohlcv_data = {
('ETH/BTC', '1d'): ohlcv_history, ('ETH/BTC', '1d'): ohlcv_history,
('TKN/BTC', '1d'): ohlcv_history, ('TKN/BTC', '1d'): ohlcv_history,
('LTC/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( mocker.patch.multiple(
'freqtrade.exchange.Exchange', '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), refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
) )
@ -836,11 +836,43 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o
assert len(freqtrade.pairlists.whitelist) == 3 assert len(freqtrade.pairlists.whitelist) == 3
assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0
previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count
freqtrade.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
assert len(freqtrade.pairlists.whitelist) == 3 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 # Called once for XRP/BTC
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + 1 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: def test_OffsetFilter_error(mocker, whitelist_conf) -> None:

View File

@ -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

View File

@ -13,7 +13,8 @@ from sqlalchemy import create_engine, inspect, text
from freqtrade import constants from freqtrade import constants
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.persistence import LocalTrade, Order, Trade, clean_dry_run_db, init_db 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): def test_init_create_session(default_conf):
@ -64,8 +65,10 @@ def test_init_dryrun_db(default_conf, tmpdir):
assert Path(filename).is_file() assert Path(filename).is_file()
@pytest.mark.parametrize('is_short', [False, True])
@pytest.mark.usefixtures("init_persistence") @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( trade = Trade(
id=2, id=2,
pair='ADA/USDT', pair='ADA/USDT',
@ -77,16 +80,11 @@ def test_enter_exit_side(fee):
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
exchange='binance', exchange='binance',
is_short=False, is_short=is_short,
leverage=2.0 leverage=2.0
) )
assert trade.enter_side == 'buy' assert trade.enter_side == enter_side
assert trade.exit_side == 'sell' assert trade.exit_side == exit_side
trade.is_short = True
assert trade.enter_side == 'sell'
assert trade.exit_side == 'buy'
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@ -105,7 +103,7 @@ def test_set_stop_loss_isolated_liq(fee):
is_short=False, is_short=False,
leverage=2.0 leverage=2.0
) )
trade.set_isolated_liq(0.09) trade.set_isolated_liq(isolated_liq=0.09)
assert trade.isolated_liq == 0.09 assert trade.isolated_liq == 0.09
assert trade.stop_loss == 0.09 assert trade.stop_loss == 0.09
assert trade.initial_stop_loss == 0.09 assert trade.initial_stop_loss == 0.09
@ -115,12 +113,12 @@ def test_set_stop_loss_isolated_liq(fee):
assert trade.stop_loss == 0.1 assert trade.stop_loss == 0.1
assert trade.initial_stop_loss == 0.09 assert trade.initial_stop_loss == 0.09
trade.set_isolated_liq(0.08) trade.set_isolated_liq(isolated_liq=0.08)
assert trade.isolated_liq == 0.08 assert trade.isolated_liq == 0.08
assert trade.stop_loss == 0.1 assert trade.stop_loss == 0.1
assert trade.initial_stop_loss == 0.09 assert trade.initial_stop_loss == 0.09
trade.set_isolated_liq(0.11) trade.set_isolated_liq(isolated_liq=0.11)
assert trade.isolated_liq == 0.11 assert trade.isolated_liq == 0.11
assert trade.stop_loss == 0.11 assert trade.stop_loss == 0.11
assert trade.initial_stop_loss == 0.09 assert trade.initial_stop_loss == 0.09
@ -170,8 +168,32 @@ def test_set_stop_loss_isolated_liq(fee):
assert trade.initial_stop_loss == 0.09 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") @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 10min, 5hr limit trade on Binance/Kraken at 3x,5x leverage
fee: 0.25 % quote fee: 0.25 % quote
@ -230,114 +252,27 @@ def test_interest(market_buy_order_usdt, fee):
stake_amount=20.0, stake_amount=20.0,
amount=30.0, amount=30.0,
open_rate=2.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_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
exchange='binance', exchange=exchange,
leverage=3.0, leverage=lev,
interest_rate=0.0005, interest_rate=rate,
is_short=is_short
) )
# 10min, 3x leverage assert round(float(trade.calculate_interest()), 8) == interest
# 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
@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") @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 10 minute limit trade on Binance/Kraken at 1x, 3x leverage
fee: 0.25% quote fee: 0.25% quote
@ -411,20 +346,19 @@ def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, caplog):
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
exchange='binance', exchange='binance',
is_short=is_short,
leverage=lev
) )
assert trade.borrowed == 0 assert trade.borrowed == borrowed
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
@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") @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 10 minute limit trade on Binance/Kraken at 1x, 3x leverage
fee: 0.25% quote fee: 0.25% quote
@ -494,84 +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( trade = Trade(
id=2, id=2,
pair='ADA/USDT', pair='ADA/USDT',
stake_amount=60.0, stake_amount=60.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'
)
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,
amount=30.0, amount=30.0,
is_open=True, is_open=True,
open_date=arrow.utcnow().datetime, open_date=arrow.utcnow().datetime,
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
exchange='binance', exchange='binance',
is_short=True, is_short=is_short,
leverage=3.0,
interest_rate=0.0005, interest_rate=0.0005,
leverage=lev
) )
trade.open_order_id = 'something'
trade.update(limit_sell_order_usdt)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.open_rate == 2.20
assert trade.close_profit is None assert trade.close_profit is None
assert trade.close_date 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.open_order_id = 'something'
trade.update(limit_buy_order_usdt) trade.update(enter_order)
assert trade.open_order_id is None assert trade.open_order_id is None
assert trade.close_rate == 2.00 assert trade.open_rate == open_rate
assert trade.close_profit == round(0.2589996297562085, 8) 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 trade.close_date is not None
assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=226531, " assert log_has_re(f"LIMIT_{exit_side.upper()} has been fulfilled for "
r"pair=ADA/USDT, amount=30.00000000, " r"Trade\(id=2, pair=ADA/USDT, amount=30.00000000, "
r"is_short=True, leverage=3.0, open_rate=2.20000000, open_since=.*\).", f"is_short={is_short}, leverage={lev}, open_rate={open_rate}0000000, "
r"open_since=.*\).",
caplog) caplog)
caplog.clear() caplog.clear()
@ -616,9 +518,21 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee,
caplog) 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") @pytest.mark.usefixtures("init_persistence")
def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee): def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee, exchange,
trade = Trade( is_short, lev, open_value, close_value, profit, profit_ratio):
trade: Trade = Trade(
pair='ADA/USDT', pair='ADA/USDT',
stake_amount=60.0, stake_amount=60.0,
open_rate=2.0, open_rate=2.0,
@ -627,55 +541,22 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt
interest_rate=0.0005, interest_rate=0.0005,
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=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_buy_order_usdt)
trade.update(limit_sell_order_usdt) trade.update(limit_sell_order_usdt)
# 1x leverage, binance trade.open_rate = 2.0
assert trade._calc_open_trade_value() == 60.15 trade.close_rate = 2.2
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.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.recalc_open_trade_value() trade.recalc_open_trade_value()
# 3x leverage, short, kraken assert isclose(trade._calc_open_trade_value(), open_value)
assert trade._calc_open_trade_value() == 59.850 assert isclose(trade.calc_close_trade_value(), close_value)
assert trade.calc_close_trade_value() == 66.231165 assert isclose(trade.calc_profit(), round(profit, 8))
assert trade.calc_profit() == round(-6.381165000000003, 8) assert isclose(trade.calc_profit_ratio(), round(profit_ratio, 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)
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@ -766,8 +647,27 @@ def test_update_invalid_order(limit_buy_order_usdt):
trade.update(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") @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 # 10 minute limit trade on Binance/Kraken at 1x, 3x leverage
# fee: 0.25 %, 0.3% quote # fee: 0.25 %, 0.3% quote
# open_rate: 2.00 quote # open_rate: 2.00 quote
@ -787,90 +687,104 @@ def test_calc_open_trade_value(limit_buy_order_usdt, fee):
stake_amount=60.0, stake_amount=60.0,
amount=30.0, amount=30.0,
open_rate=2.0, open_rate=2.0,
fee_open=fee.return_value, open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10),
fee_close=fee.return_value, fee_open=fee_rate,
exchange='binance', fee_close=fee_rate,
exchange=exchange,
leverage=lev,
is_short=is_short
) )
trade.open_order_id = 'open_trade' trade.open_order_id = 'open_trade'
trade.update(limit_buy_order_usdt)
# Get the open rate price with the standard fee rate # Get the open rate price with the standard fee rate
assert trade._calc_open_trade_value() == 60.15 assert trade._calc_open_trade_value() == result
trade.is_short = True
trade.recalc_open_trade_value()
assert trade._calc_open_trade_value() == 59.85
trade.leverage = 3
trade.exchange = "binance"
assert trade._calc_open_trade_value() == 59.85
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
@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") @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( trade = Trade(
pair='ADA/USDT', pair='ADA/USDT',
stake_amount=60.0, stake_amount=60.0,
amount=30.0, amount=30.0,
open_rate=2.0, open_rate=open_rate,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10),
fee_open=fee.return_value, fee_open=fee_rate,
fee_close=fee.return_value, fee_close=fee_rate,
exchange='binance', exchange=exchange,
interest_rate=0.0005, interest_rate=0.0005,
is_short=is_short,
leverage=lev
) )
trade.open_order_id = 'close_trade' trade.open_order_id = 'close_trade'
trade.update(limit_buy_order_usdt) assert round(trade.calc_close_trade_value(rate=close_rate, fee=fee_rate), 8) == result
# 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.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
@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") @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 10 minute limit trade on Binance/Kraken at 1x, 3x leverage
arguments: arguments:
@ -1007,198 +921,16 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee):
open_rate=2.0, open_rate=2.0,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10), open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=10),
interest_rate=0.0005, interest_rate=0.0005,
fee_open=fee.return_value, exchange=exchange,
fee_close=fee.return_value, is_short=is_short,
exchange='binance' leverage=lev,
fee_open=0.0025,
fee_close=fee_close
) )
trade.open_order_id = 'something' trade.open_order_id = 'something'
trade.update(limit_buy_order_usdt) # Buy @ 2.0
# 1x Leverage, long assert trade.calc_profit(rate=close_rate) == round(profit, 8)
# Custom closing rate and regular fee rate assert trade.calc_profit_ratio(rate=close_rate) == round(profit_ratio, 8)
# 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()
# 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()
# 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)
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
@ -1570,7 +1302,7 @@ def test_adjust_stop_loss_short(fee):
assert trade.initial_stop_loss == 1.05 assert trade.initial_stop_loss == 1.05
assert trade.initial_stop_loss_pct == 0.05 assert trade.initial_stop_loss_pct == 0.05
assert trade.stop_loss_pct == 0.1 assert trade.stop_loss_pct == 0.1
trade.set_isolated_liq(0.63) trade.set_isolated_liq(isolated_liq=0.63)
trade.adjust_stop_loss(0.59, -0.1) trade.adjust_stop_loss(0.59, -0.1)
assert trade.stop_loss == 0.63 assert trade.stop_loss == 0.63
assert trade.isolated_liq == 0.63 assert trade.isolated_liq == 0.63
@ -1894,7 +1626,7 @@ def test_stoploss_reinitialization_short(default_conf, fee):
assert trade_adj.initial_stop_loss == 1.04 assert trade_adj.initial_stop_loss == 1.04
assert trade_adj.initial_stop_loss_pct == 0.04 assert trade_adj.initial_stop_loss_pct == 0.04
# Stoploss can't go above liquidation price # Stoploss can't go above liquidation price
trade_adj.set_isolated_liq(1.0) trade_adj.set_isolated_liq(isolated_liq=1.0)
trade.adjust_stop_loss(0.97, -0.04) trade.adjust_stop_loss(0.97, -0.04)
assert trade_adj.stop_loss == 1.0 assert trade_adj.stop_loss == 1.0
assert trade_adj.stop_loss == 1.0 assert trade_adj.stop_loss == 1.0