merged with feat/short

This commit is contained in:
Sam Germain 2021-09-19 17:02:09 -06:00
parent ddc203ca69
commit 60a678fea7
52 changed files with 3356 additions and 663 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

@ -165,6 +165,7 @@ Example to remove the first 10 pairs from the pairlist:
```json ```json
"pairlists": [ "pairlists": [
// ...
{ {
"method": "OffsetFilter", "method": "OffsetFilter",
"offset": 10 "offset": 10
@ -190,6 +191,19 @@ Sorts pairs by past trade performance, as follows:
Trade count is used as a tie breaker. 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 !!! Note
`PerformanceFilter` does not support backtesting mode. `PerformanceFilter` does not support backtesting mode.

View File

@ -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) Rollover fee = P (borrowed money) * R (quat_hourly_interest) * ceiling(T/4) (in hours)
I (interest) = Opening fee + Rollover fee I (interest) = Opening fee + Rollover fee
[source](https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-) [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

View File

@ -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()`. 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 #### Stepped stoploss
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.

View File

@ -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. 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) ## Additional data (Wallets)

View File

@ -53,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
if epochs and export_csv: if epochs and export_csv:
HyperoptTools.export_csv_file( HyperoptTools.export_csv_file(
config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv config, epochs, export_csv
) )

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

@ -119,7 +119,7 @@ class Edge:
) )
# Download informative pairs too # Download informative pairs too
res = defaultdict(list) res = defaultdict(list)
for p, t in self.strategy.informative_pairs(): for p, t in self.strategy.gather_informative_pairs():
res[t].append(p) res[t].append(p)
for timeframe, inf_pairs in res.items(): for timeframe, inf_pairs in res.items():
timerange_startup = deepcopy(self._timerange) timerange_startup = deepcopy(self._timerange)

View File

@ -20,4 +20,7 @@ class Bibox(Exchange):
# fetchCurrencies API point requires authentication for Bibox, # fetchCurrencies API point requires authentication for Bibox,
# so switch it off for Freqtrade load_markets() # 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}}

View File

@ -1,10 +1,13 @@
""" Binance exchange subclass """ """ Binance exchange subclass """
import json
import logging import logging
from typing import Dict, List from pathlib import Path
from typing import Dict, List, Optional, Tuple
import arrow import arrow
import ccxt import ccxt
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -27,36 +30,74 @@ class Binance(Exchange):
} }
funding_fee_times: List[int] = [0, 8, 16] # hours of the day 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) Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary. 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) @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. creates a stoploss limit order.
this stoploss-limit is binance-specific. this stoploss-limit is binance-specific.
It may work with a limited number of other exchanges, but this has not been tested yet. 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 threshold: As limit price should always be below stop-price
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) 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" ordertype = "stop_loss_limit"
stop_price = self.price_to_precision(pair, stop_price) 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 # Ensure rate is less than stop price
if stop_price <= rate: if bad_stop_price:
raise OperationalException( 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']: if self._config['dry_run']:
dry_order = self.create_dry_run_order( dry_order = self.create_dry_run_order(
pair, ordertype, "sell", amount, stop_price) pair, ordertype, side, amount, stop_price, leverage)
return dry_order return dry_order
try: try:
@ -67,7 +108,8 @@ class Binance(Exchange):
rate = self.price_to_precision(pair, rate) 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) amount=amount, price=rate, params=params)
logger.info('stoploss limit order added for %s. ' logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate) 'stop price: %s. limit: %s', pair, stop_price, rate)
@ -75,21 +117,96 @@ class Binance(Exchange):
return order return order
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise InsufficientFundsError( 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 sell amount {amount} at rate {rate}. ' f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
# Errors: # Errors:
# `binance Order would trigger immediately.` # `binance Order would trigger immediately.`
raise InvalidOrderException( 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 sell amount {amount} at rate {rate}. ' f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,7 @@ import http
import inspect import inspect
import logging import logging
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timedelta, timezone from datetime import datetime, timezone
from math import ceil from math import ceil
from typing import Any, Dict, List, Optional, Tuple, Union 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, from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES,
ListPairsWithTimeframes) ListPairsWithTimeframes)
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list 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, from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
InvalidOrderException, OperationalException, PricingError, InvalidOrderException, OperationalException, PricingError,
RetryableOrderError, TemporaryError) RetryableOrderError, TemporaryError)
@ -48,9 +49,6 @@ class Exchange:
_config: Dict = {} _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) # Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
_params: Dict = {} _params: Dict = {}
@ -75,6 +73,10 @@ class Exchange:
_ft_has: Dict = {} _ft_has: Dict = {}
funding_fee_times: List[int] = [] # hours of the day 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: def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
""" """
Initializes this module with the given config, Initializes this module with the given config,
@ -84,6 +86,7 @@ class Exchange:
self._api: ccxt.Exchange = None self._api: ccxt.Exchange = None
self._api_async: ccxt_async.Exchange = None self._api_async: ccxt_async.Exchange = None
self._markets: Dict = {} self._markets: Dict = {}
self._leverage_brackets: Dict = {}
self._config.update(config) self._config.update(config)
@ -126,14 +129,25 @@ class Exchange:
self._trades_pagination = self._ft_has['trades_pagination'] self._trades_pagination = self._ft_has['trades_pagination']
self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] 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 # 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_config', {}), ccxt_config)
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_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) 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 = deep_merge_dicts(exchange_config.get('ccxt_config', {}),
ccxt_async_config) ccxt_async_config)
ccxt_async_config = deep_merge_dicts(exchange_config.get('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( self._api_async = self._init_ccxt(
exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) 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) logger.info('Using Exchange "%s"', self.name)
if validate: if validate:
@ -158,7 +175,7 @@ class Exchange:
self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {}))
self.validate_required_startup_candles(config.get('startup_candle_count', 0), self.validate_required_startup_candles(config.get('startup_candle_count', 0),
config.get('timeframe', '')) config.get('timeframe', ''))
self.validate_trading_mode_and_collateral(self.trading_mode, self.collateral)
# Converts the interval provided in minutes in config to seconds # Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_config.get( self.markets_refresh_interval: int = exchange_config.get(
"markets_refresh_interval", 60) * 60 "markets_refresh_interval", 60) * 60
@ -211,6 +228,11 @@ class Exchange:
return api return api
@property
def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization.
return {}
@property @property
def name(self) -> str: def name(self) -> str:
"""exchange Name (from ccxt)""" """exchange Name (from ccxt)"""
@ -356,6 +378,7 @@ class Exchange:
# Also reload async markets to avoid issues with newly listed pairs # Also reload async markets to avoid issues with newly listed pairs
self._load_async_markets(reload=True) self._load_async_markets(reload=True)
self._last_markets_refresh = arrow.utcnow().int_timestamp self._last_markets_refresh = arrow.utcnow().int_timestamp
self.fill_leverage_brackets()
except ccxt.BaseError: except ccxt.BaseError:
logger.exception("Could not reload markets.") logger.exception("Could not reload markets.")
@ -483,6 +506,25 @@ class Exchange:
f"This strategy requires {startup_candles} candles to start. " 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} 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: def exchange_has(self, endpoint: str) -> bool:
""" """
Checks if exchange implements a specific API endpoint. Checks if exchange implements a specific API endpoint.
@ -542,8 +584,8 @@ class Exchange:
else: else:
return 1 / pow(10, precision) return 1 / pow(10, precision)
def get_min_pair_stake_amount(self, pair: str, price: float, def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float,
stoploss: float) -> Optional[float]: leverage: Optional[float] = 1.0) -> Optional[float]:
try: try:
market = self.markets[pair] market = self.markets[pair]
except KeyError: except KeyError:
@ -577,12 +619,24 @@ class Exchange:
# The value returned should satisfy both limits: for amount (base currency) and # The value returned should satisfy both limits: for amount (base currency) and
# for cost (quote, stake currency), so max() is used here. # for cost (quote, stake currency), so max() is used here.
# See also #2575 at github. # 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 # Dry-run methods
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, 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()}' order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
_amount = self.amount_to_precision(pair, amount) _amount = self.amount_to_precision(pair, amount)
dry_order: Dict[str, Any] = { dry_order: Dict[str, Any] = {
@ -599,7 +653,8 @@ class Exchange:
'timestamp': arrow.utcnow().int_timestamp * 1000, 'timestamp': arrow.utcnow().int_timestamp * 1000,
'status': "closed" if ordertype == "market" else "open", 'status': "closed" if ordertype == "market" else "open",
'fee': None, 'fee': None,
'info': {} 'info': {},
'leverage': leverage
} }
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
dry_order["info"] = {"stopPrice": dry_order["price"]} dry_order["info"] = {"stopPrice": dry_order["price"]}
@ -609,7 +664,7 @@ class Exchange:
average = self.get_dry_market_fill_price(pair, side, amount, rate) average = self.get_dry_market_fill_price(pair, side, amount, rate)
dry_order.update({ dry_order.update({
'average': average, 'average': average,
'cost': dry_order['amount'] * average, 'cost': (dry_order['amount'] * average) / leverage
}) })
dry_order = self.add_dry_order_fee(pair, dry_order) dry_order = self.add_dry_order_fee(pair, dry_order)
@ -717,17 +772,26 @@ class Exchange:
# Order handling # Order handling
def create_order(self, pair: str, ordertype: str, side: str, amount: float, def _lev_prep(self, pair: str, leverage: float):
rate: float, time_in_force: str = 'gtc') -> Dict: if self.trading_mode != TradingMode.SPOT:
self.set_margin_mode(pair, self.collateral)
if self._config['dry_run']: self._set_leverage(leverage, pair)
dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate)
return dry_order
def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict:
params = self._params.copy() params = self._params.copy()
if time_in_force != 'gtc' and ordertype != 'market': if time_in_force != 'gtc' and ordertype != 'market':
param = self._ft_has.get('time_in_force_parameter', '') param = self._ft_has.get('time_in_force_parameter', '')
params.update({param: time_in_force}) 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: try:
# Set the precision for amount and price(rate) as accepted by the exchange # 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)) or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None 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, order = self._api.create_order(pair, ordertype, side,
amount, rate_for_order, params) amount, rate_for_order, params)
self._log_exchange_response('create_order', order) self._log_exchange_response('create_order', order)
@ -759,14 +824,15 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from 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) Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary. Returns True if adjustment is necessary.
""" """
raise OperationalException(f"stoploss is not implemented for {self.name}.") 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. creates a stoploss order.
The precise ordertype is determined by the order_types dict or exchange default. The precise ordertype is determined by the order_types dict or exchange default.
@ -1559,21 +1625,66 @@ class Exchange:
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from 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) return
close_date = datetime(close_date.year, close_date.month, close_date.day, close_date.hour)
results = [] def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
date_iterator = open_date """
while date_iterator < close_date: Returns the maximum leverage that a pair can be traded at
date_iterator += timedelta(hours=1) :param pair: The base/quote currency pair being traded
if date_iterator.hour in self.funding_fee_times: :nominal_value: The total value of the trade in quote currency (collateral + debt)
results.append(date_iterator) """
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: def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:

View File

@ -1,9 +1,10 @@
""" FTX exchange subclass """ """ FTX exchange subclass """
import logging import logging
from typing import Any, Dict, List from typing import Any, Dict, List, Optional, Tuple
import ccxt import ccxt
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
@ -22,6 +23,12 @@ class Ftx(Exchange):
} }
funding_fee_times: List[int] = list(range(0, 23)) 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: def market_is_tradable(self, market: Dict[str, Any]) -> bool:
""" """
Check if the market symbol is tradable by Freqtrade. Check if the market symbol is tradable by Freqtrade.
@ -32,15 +39,19 @@ class Ftx(Exchange):
return (parent_check and return (parent_check and
market.get('spot', False) is True) 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) Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary. 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) @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. Creates a stoploss order.
depending on order_types.stoploss configuration, uses 'market' or limit 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 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_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" ordertype = "stop"
@ -56,7 +70,7 @@ class Ftx(Exchange):
if self._config['dry_run']: if self._config['dry_run']:
dry_order = self.create_dry_run_order( dry_order = self.create_dry_run_order(
pair, ordertype, "sell", amount, stop_price) pair, ordertype, side, amount, stop_price, leverage)
return dry_order return dry_order
try: try:
@ -68,7 +82,8 @@ class Ftx(Exchange):
params['stopPrice'] = stop_price params['stopPrice'] = stop_price
amount = self.amount_to_precision(pair, amount) 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) amount=amount, params=params)
self._log_exchange_response('create_stoploss_order', order) self._log_exchange_response('create_stoploss_order', order)
logger.info('stoploss order added for %s. ' logger.info('stoploss order added for %s. '
@ -76,19 +91,19 @@ class Ftx(Exchange):
return order return order
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise InsufficientFundsError( 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'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise InvalidOrderException( 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'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
@ -153,3 +168,18 @@ class Ftx(Exchange):
if order['type'] == 'stop': if order['type'] == 'stop':
return safe_value_fallback2(order, order, 'id_stop', 'id') return safe_value_fallback2(order, order, 'id_stop', 'id')
return order['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

View File

@ -1,9 +1,10 @@
""" Kraken exchange subclass """ """ Kraken exchange subclass """
import logging import logging
from typing import Any, Dict, List from typing import Any, Dict, List, Optional, Tuple
import ccxt import ccxt
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange import Exchange 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 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: def market_is_tradable(self, market: Dict[str, Any]) -> bool:
""" """
Check if the market symbol is tradable by Freqtrade. Check if the market symbol is tradable by Freqtrade.
@ -68,16 +75,19 @@ class Kraken(Exchange):
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from 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) Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary. Returns True if adjustment is necessary.
""" """
return (order['type'] in ('stop-loss', 'stop-loss-limit') return (order['type'] in ('stop-loss', 'stop-loss-limit') and (
and stop_loss > float(order['price'])) (side == "sell" and stop_loss > float(order['price'])) or
(side == "buy" and stop_loss < float(order['price']))
))
@retrier(retries=0) @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. Creates a stoploss market order.
Stoploss market orders is the only stoploss type supported by kraken. 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': if order_types.get('stoploss', 'market') == 'limit':
ordertype = "stop-loss-limit" ordertype = "stop-loss-limit"
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) 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) params['price2'] = self.price_to_precision(pair, limit_rate)
else: else:
ordertype = "stop-loss" ordertype = "stop-loss"
@ -96,13 +109,13 @@ class Kraken(Exchange):
if self._config['dry_run']: if self._config['dry_run']:
dry_order = self.create_dry_run_order( dry_order = self.create_dry_run_order(
pair, ordertype, "sell", amount, stop_price) pair, ordertype, side, amount, stop_price, leverage)
return dry_order return dry_order
try: try:
amount = self.amount_to_precision(pair, amount) 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) amount=amount, price=stop_price, params=params)
self._log_exchange_response('create_stoploss_order', order) self._log_exchange_response('create_stoploss_order', order)
logger.info('stoploss order added for %s. ' logger.info('stoploss order added for %s. '
@ -110,18 +123,70 @@ class Kraken(Exchange):
return order return order
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise InsufficientFundsError( 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'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise InvalidOrderException( 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'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( 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: except ccxt.BaseError as e:
raise OperationalException(e) from 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

View File

@ -86,10 +86,10 @@ class FreqtradeBot(LoggingMixin):
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
# Attach Dataprovider to Strategy baseclass # Attach Dataprovider to strategy instance
IStrategy.dp = self.dataprovider self.strategy.dp = self.dataprovider
# Attach Wallets to Strategy baseclass # Attach Wallets to strategy instance
IStrategy.wallets = self.wallets self.strategy.wallets = self.wallets
# Initializing Edge only if enabled # Initializing Edge only if enabled
self.edge = Edge(self.config, self.exchange, self.strategy) if \ self.edge = Edge(self.config, self.exchange, self.strategy) if \
@ -173,7 +173,7 @@ class FreqtradeBot(LoggingMixin):
# Refreshing candles # Refreshing candles
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), 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)() 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. :return: True if the order succeeded, and False in case of problems.
""" """
try: try:
stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount, stoploss_order = self.exchange.stoploss(
stop_price=stop_price, pair=trade.pair,
order_types=self.strategy.order_types) 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') order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss')
trade.orders.append(order_obj) trade.orders.append(order_obj)
@ -857,11 +862,11 @@ class FreqtradeBot(LoggingMixin):
# if trailing stoploss is enabled we check if stoploss value has changed # 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 # in which case we cancel stoploss order and put another one with new
# value immediately # 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 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 Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange in case of trailing stoploss on exchange
@ -869,7 +874,7 @@ class FreqtradeBot(LoggingMixin):
:param order: Current on exchange stoploss order :param order: Current on exchange stoploss order
:return: None :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 # we check if the update is necessary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) 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.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:

View File

@ -20,7 +20,7 @@ def interest(
:param exchange_name: The exchanged being trading on :param exchange_name: The exchanged being trading on
:param borrowed: The amount of currency being borrowed :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 :param hours: The time in hours that the currency has been borrowed for
Raises: Raises:
@ -36,7 +36,8 @@ def interest(
# Rounded based on https://kraken-fees-calculator.github.io/ # Rounded based on https://kraken-fees-calculator.github.io/
return borrowed * rate * (one+ceil(hours/four)) return borrowed * rate * (one+ceil(hours/four))
elif exchange_name == "ftx": elif exchange_name == "ftx":
# TODO-lev: Add FTX interest formula # As Explained under #Interest rates section in
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer
return borrowed * rate * ceil(hours)/twenty_four
else: else:
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")

View File

@ -154,7 +154,7 @@ class Backtesting:
self.strategy: IStrategy = strategy self.strategy: IStrategy = strategy
strategy.dp = self.dataprovider strategy.dp = self.dataprovider
# Attach Wallets to Strategy baseclass # Attach Wallets to Strategy baseclass
IStrategy.wallets = self.wallets strategy.wallets = self.wallets
# Set stoploss_on_exchange to false for backtesting, # Set stoploss_on_exchange to false for backtesting,
# since a "perfect" stoploss-sell is assumed anyway # since a "perfect" stoploss-sell is assumed anyway
# And the regular "stoploss" function would not apply to that case # And the regular "stoploss" function would not apply to that case

View File

@ -8,6 +8,7 @@ from typing import Any, Dict
from freqtrade import constants from freqtrade import constants
from freqtrade.configuration import TimeRange, validate_config_consistency from freqtrade.configuration import TimeRange, validate_config_consistency
from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.optimize.optimize_reports import generate_edge_table from freqtrade.optimize.optimize_reports import generate_edge_table
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
@ -33,6 +34,7 @@ class EdgeCli:
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
self.strategy = StrategyResolver.load_strategy(self.config) self.strategy = StrategyResolver.load_strategy(self.config)
self.strategy.dp = DataProvider(config, None)
validate_config_consistency(self.config) validate_config_consistency(self.config)

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

@ -7,6 +7,7 @@ from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple from typing import Any, Dict, Iterator, List, Optional, Tuple
import numpy as np import numpy as np
import pandas as pd
import rapidjson import rapidjson
import tabulate import tabulate
from colorama import Fore, Style from colorama import Fore, Style
@ -298,8 +299,8 @@ class HyperoptTools():
f"Objective: {results['loss']:.5f}") f"Objective: {results['loss']:.5f}")
@staticmethod @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'] = '' trials['Best'] = ''
if 'results_metrics.winsdrawslosses' not in trials.columns: if 'results_metrics.winsdrawslosses' not in trials.columns:
@ -435,8 +436,7 @@ class HyperoptTools():
return table return table
@staticmethod @staticmethod
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, def export_csv_file(config: dict, results: list, csv_file: str) -> None:
csv_file: str) -> None:
""" """
Log result to csv-file Log result to csv-file
""" """

View File

@ -2,7 +2,7 @@
This module contains the class to persist trades into SQLite This module contains the class to persist trades into SQLite
""" """
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timedelta, timezone
from decimal import Decimal from decimal import Decimal
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
@ -1057,17 +1057,21 @@ class Trade(_DECL_BASE, LocalTrade):
return total_open_stake_amount or 0 return total_open_stake_amount or 0
@staticmethod @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 Returns List of dicts containing all Trades, including profit and trade count
NOTE: Not supported in Backtesting. 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( pair_rates = Trade.query.with_entities(
Trade.pair, Trade.pair,
func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit).label('profit_sum'),
func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
func.count(Trade.pair).label('count') func.count(Trade.pair).label('count')
).filter(Trade.is_open.is_(False))\ ).filter(*filters)\
.group_by(Trade.pair) \ .group_by(Trade.pair) \
.order_by(desc('profit_sum_abs')) \ .order_by(desc('profit_sum_abs')) \
.all() .all()

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

@ -2,7 +2,7 @@
Performance pair list filter Performance pair list filter
""" """
import logging import logging
from typing import Dict, List from typing import Any, Dict, List
import pandas as pd import pandas as pd
@ -15,6 +15,13 @@ logger = logging.getLogger(__name__)
class PerformanceFilter(IPairList): 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 @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
""" """
@ -40,7 +47,7 @@ class PerformanceFilter(IPairList):
""" """
# Get the trading performance for pairs from database # Get the trading performance for pairs from database
try: try:
performance = pd.DataFrame(Trade.get_overall_performance()) performance = pd.DataFrame(Trade.get_overall_performance(self._minutes))
except AttributeError: except AttributeError:
# Performancefilter does not work in backtesting. # Performancefilter does not work in backtesting.
self.log_once("PerformanceFilter is not available in this mode.", logger.warning) self.log_once("PerformanceFilter is not available in this mode.", logger.warning)

View File

@ -46,6 +46,12 @@ class Balances(BaseModel):
value: float value: float
stake: str stake: str
note: 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): class Count(BaseModel):

View File

@ -459,6 +459,9 @@ class RPC:
raise RPCException('Error getting current tickers.') raise RPCException('Error getting current tickers.')
self._freqtrade.wallets.update(require_update=False) 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(): for coin, balance in self._freqtrade.wallets.get_all_balances().items():
if not balance.total: if not balance.total:
@ -494,15 +497,25 @@ class RPC:
else: else:
raise RPCException('All balances are zero.') raise RPCException('All balances are zero.')
symbol = fiat_display_currency value = self._fiat_converter.convert_amount(
value = self._fiat_converter.convert_amount(total, stake_currency, total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
symbol) 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 { return {
'currencies': output, 'currencies': output,
'total': total, 'total': total,
'symbol': symbol, 'symbol': fiat_display_currency,
'value': value, 'value': value,
'stake': stake_currency, '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 '' 'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
} }

View File

@ -603,12 +603,15 @@ class Telegram(RPCHandler):
output = '' output = ''
if self._config['dry_run']: if self._config['dry_run']:
output += ( output += "*Warning:* Simulated balances in Dry Mode.\n"
f"*Warning:* Simulated balances in Dry Mode.\n"
"This mode is still experimental!\n" output += ("Starting capital: "
"Starting capital: " f"`{result['starting_capital']}` {self._config['stake_currency']}"
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" )
) 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_balance = 0
total_dust_currencies = 0 total_dust_currencies = 0
for curr in result['currencies']: for curr in result['currencies']:
@ -641,9 +644,12 @@ class Telegram(RPCHandler):
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n") f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
output += ("\n*Estimated Value*:\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"\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", self._send_msg(output, reload_able=True, callback_path="update_balance",
query=update.callback_query) query=update.callback_query)
except RPCException as e: except RPCException as e:

View File

@ -3,5 +3,7 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr
timeframe_to_prev_date, timeframe_to_seconds) timeframe_to_prev_date, timeframe_to_seconds)
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter, from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
IntParameter, RealParameter) IntParameter, RealParameter)
from freqtrade.strategy.informative_decorator import informative
from freqtrade.strategy.interface import IStrategy 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)

View File

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

View File

@ -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.exchange.exchange import timeframe_to_next_date
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.strategy.hyper import HyperStrategyMixin 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.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
@ -118,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin):
# Class level variables (intentional) containing # Class level variables (intentional) containing
# the dataprovider (dp) (access to other candles, historic data, ...) # the dataprovider (dp) (access to other candles, historic data, ...)
# and wallets - access to the current balance. # and wallets - access to the current balance.
dp: Optional[DataProvider] = None dp: Optional[DataProvider]
wallets: Optional[Wallets] = None wallets: Optional[Wallets] = None
# Filled from configuration # Filled from configuration
stake_currency: str stake_currency: str
@ -134,6 +137,24 @@ class IStrategy(ABC, HyperStrategyMixin):
self._last_candle_seen_per_pair: Dict[str, datetime] = {} self._last_candle_seen_per_pair: Dict[str, datetime] = {}
super().__init__(config) 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 @abstractmethod
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
@ -377,6 +398,23 @@ class IStrategy(ABC, HyperStrategyMixin):
# END - Intended to be overridden by strategy # 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: def get_strategy_name(self) -> str:
""" """
Returns strategy class name Returns strategy class name
@ -786,10 +824,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:
@ -801,6 +840,12 @@ class IStrategy(ABC, HyperStrategyMixin):
:return: a Dataframe with all mandatory indicators for the strategies :return: a Dataframe with all mandatory indicators for the strategies
""" """
logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") 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: if self._populate_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see " warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning) "the current function headers!", DeprecationWarning)

View File

@ -4,7 +4,9 @@ from freqtrade.exchange import timeframe_to_minutes
def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, 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. 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: Timeframe of the original pair sample.
:param timeframe_inf: Timeframe of the informative pair sample. :param timeframe_inf: Timeframe of the informative pair sample.
:param ffill: Forwardfill missing values - optional but usually required :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 :return: Merged dataframe
:raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe :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) minutes = timeframe_to_minutes(timeframe)
if minutes == minutes_inf: if minutes == minutes_inf:
# No need to forwardshift if the timeframes are identical # No need to forwardshift if the timeframes are identical
informative['date_merge'] = informative["date"] informative['date_merge'] = informative[date_column]
elif minutes < minutes_inf: elif minutes < minutes_inf:
# Subtract "small" timeframe so merging is not delayed by 1 small candle # Subtract "small" timeframe so merging is not delayed by 1 small candle
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073 # Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
informative['date_merge'] = ( 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: else:
raise ValueError("Tried to merge a faster timeframe to a slower timeframe." raise ValueError("Tried to merge a faster timeframe to a slower timeframe."
"This would create new rows, and can throw off your regular indicators.") "This would create new rows, and can throw off your regular indicators.")
# Rename columns to be unique # 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 # Combine the 2 dataframes
# all indicators on the informative sample MUST be calculated before this point # all indicators on the informative sample MUST be calculated before this point
dataframe = pd.merge(dataframe, informative, left_on='date', dataframe = pd.merge(dataframe, informative, left_on='date',
right_on=f'date_merge_{timeframe_inf}', how='left') right_on=date_merge, how='left')
dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1) dataframe = dataframe.drop(date_merge, axis=1)
if ffill: if ffill:
dataframe = dataframe.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 # negative stoploss values indicate the requested stop price is higher than the current price
return max(stoploss, 0.0) 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)

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

@ -62,7 +62,7 @@ function updateenv() {
then then
REQUIREMENTS_PLOT="-r requirements-plot.txt" REQUIREMENTS_PLOT="-r requirements-plot.txt"
fi fi
if [ "${SYS_ARCH}" == "armv7l" ]; then if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then
echo "Detected Raspberry, installing cython, skipping hyperopt installation." echo "Detected Raspberry, installing cython, skipping hyperopt installation."
${PYTHON} -m pip install --upgrade cython ${PYTHON} -m pip install --upgrade cython
else else

View File

@ -18,7 +18,7 @@ from freqtrade import constants
from freqtrade.commands import Arguments from freqtrade.commands import Arguments
from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.converter import ohlcv_to_dataframe
from freqtrade.edge import Edge, PairInfo 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.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import LocalTrade, Trade, init_db 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._load_async_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', 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.id', PropertyMock(return_value=id))
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
if mock_markets: if mock_markets:
mocker.patch('freqtrade.exchange.Exchange.markets', mocker.patch('freqtrade.exchange.Exchange.markets',
PropertyMock(return_value=get_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: if api_mock:
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
else: 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', def get_patched_exchange(mocker, config, api_mock=None, id='binance',
mock_markets=True) -> Exchange: mock_markets=True, mock_supported_modes=True) -> Exchange:
patch_exchange(mocker, api_mock, id, mock_markets) patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes)
config['exchange']['name'] = id config['exchange']['name'] = id
try: try:
exchange = ExchangeResolver.load_exchange(id, config) exchange = ExchangeResolver.load_exchange(id, config)
@ -442,7 +460,10 @@ def get_markets():
'max': 500000, 'max': 500000,
}, },
}, },
'info': {}, 'info': {
'leverage_buy': ['2'],
'leverage_sell': ['2'],
},
}, },
'TKN/BTC': { 'TKN/BTC': {
'id': 'tknbtc', 'id': 'tknbtc',
@ -468,7 +489,10 @@ def get_markets():
'max': 500000, 'max': 500000,
}, },
}, },
'info': {}, 'info': {
'leverage_buy': ['2', '3', '4', '5'],
'leverage_sell': ['2', '3', '4', '5'],
},
}, },
'BLK/BTC': { 'BLK/BTC': {
'id': 'blkbtc', 'id': 'blkbtc',
@ -493,7 +517,10 @@ def get_markets():
'max': 500000, 'max': 500000,
}, },
}, },
'info': {}, 'info': {
'leverage_buy': ['2', '3'],
'leverage_sell': ['2', '3'],
},
}, },
'LTC/BTC': { 'LTC/BTC': {
'id': 'ltcbtc', 'id': 'ltcbtc',
@ -518,7 +545,10 @@ def get_markets():
'max': 500000, 'max': 500000,
}, },
}, },
'info': {}, 'info': {
'leverage_buy': [],
'leverage_sell': [],
},
}, },
'XRP/BTC': { 'XRP/BTC': {
'id': 'xrpbtc', 'id': 'xrpbtc',
@ -596,7 +626,10 @@ def get_markets():
'max': None 'max': None
} }
}, },
'info': {}, 'info': {
'leverage_buy': [],
'leverage_sell': [],
},
}, },
'ETH/USDT': { 'ETH/USDT': {
'id': 'USDT-ETH', 'id': 'USDT-ETH',
@ -712,6 +745,8 @@ def get_markets():
'max': None 'max': None
} }
}, },
'info': {
}
}, },
} }

View File

@ -1,21 +1,31 @@
from datetime import datetime, timezone from datetime import datetime, timezone
from random import randint from random import randint
from unittest.mock import MagicMock from unittest.mock import MagicMock, PropertyMock
import ccxt import ccxt
import pytest import pytest
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re
from tests.exchange.test_exchange import ccxt_exceptionhandlers from tests.exchange.test_exchange import ccxt_exceptionhandlers
@pytest.mark.parametrize('limitratio,expected', [ @pytest.mark.parametrize('limitratio,expected,side', [
(None, 220 * 0.99), (None, 220 * 0.99, "sell"),
(0.99, 220 * 0.99), (0.99, 220 * 0.99, "sell"),
(0.98, 220 * 0.98), (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() api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop_loss_limit' 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') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, order = exchange.stoploss(
order_types={'stoploss_on_exchange_limit_ratio': 1.05}) 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() api_mock.create_order.reset_mock()
order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio} 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 'id' in order
assert 'info' in order assert 'info' in order
assert order['id'] == order_id 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]['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]['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 assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
# Price should be 1% below stopprice # Price should be 1% below stopprice
assert api_mock.create_order.call_args_list[0][1]['price'] == expected 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): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') 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): with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock( api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') 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", ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance",
"stoploss", "create_order", retries=1, "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): 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') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, order = exchange.stoploss(
order_types={'stoploss_on_exchange_limit_ratio': 1.05}) 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() 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 'id' in order
assert 'info' in order assert 'info' in order
@ -94,18 +144,202 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker):
assert order['amount'] == 1 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') exchange = get_patched_exchange(mocker, default_conf, id='binance')
order = { order = {
'type': 'stop_loss_limit', 'type': 'stop_loss_limit',
'price': 1500, 'price': 1500,
'info': {'stopPrice': 1500}, 'info': {'stopPrice': 1500},
} }
assert exchange.stoploss_adjust(1501, order) assert exchange.stoploss_adjust(sl1, order, side=side)
assert not exchange.stoploss_adjust(1499, order) assert not exchange.stoploss_adjust(sl2, order, side=side)
# Test with invalid order case # Test with invalid order case
order['type'] = 'stop_loss' 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 @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 exchange._api_async.fetch_ohlcv.call_count == 2
assert res == ohlcv assert res == ohlcv
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) 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

View File

@ -11,6 +11,7 @@ import ccxt
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade.enums import Collateral, TradingMode
from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException,
OperationalException, PricingError, TemporaryError) OperationalException, PricingError, TemporaryError)
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken 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 log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog)
assert ex._api.headers == {'hello': 'world'} assert ex._api.headers == {'hello': 'world'}
assert ex._ccxt_config == {}
Exchange._headers = {} Exchange._headers = {}
@ -395,7 +397,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) 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 # min amount is set
markets["ETH/BTC"]["limits"] = { markets["ETH/BTC"]["limits"] = {
@ -407,7 +413,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) 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) # min amount and cost are set (cost is minimal)
markets["ETH/BTC"]["limits"] = { markets["ETH/BTC"]["limits"] = {
@ -419,7 +429,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) 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) # min amount and cost are set (amount is minial)
markets["ETH/BTC"]["limits"] = { markets["ETH/BTC"]["limits"] = {
@ -431,14 +445,26 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) 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) 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 # Really big stoploss
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) 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: 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) PropertyMock(return_value=markets)
) )
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss)
assert round(result, 8) == round( expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss))
max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), assert round(result, 8) == round(expected_result, 8)
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): 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) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
order = exchange.create_dry_run_order( 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 'id' in order
assert f'dry_run_{side}_' in order["id"] assert f'dry_run_{side}_' in order["id"]
assert order["side"] == side 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( 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 order_book_l2_usd.call_count == 1
assert 'id' in order assert 'id' in order
assert f'dry_run_{side}_' in order["id"] 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( 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 'id' in order
assert f'dry_run_{side}_' in order["id"] assert f'dry_run_{side}_' in order["id"]
assert order["side"] == side 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) assert round(order["average"], 4) == round(endprice, 4)
@pytest.mark.parametrize("side", [ @pytest.mark.parametrize("side", ["buy", "sell"])
("buy"),
("sell")
])
@pytest.mark.parametrize("ordertype,rate,marketprice", [ @pytest.mark.parametrize("ordertype,rate,marketprice", [
("market", None, None), ("market", None, None),
("market", 200, True), ("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.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_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 = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange._set_leverage = MagicMock()
exchange.set_margin_mode = MagicMock()
order = exchange.create_order( 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 'id' in order
assert 'info' 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][2] == side
assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] is rate 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): 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): def test_stoploss_order_unsupported_exchange(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id='bittrex') exchange = get_patched_exchange(mocker, default_conf, id='bittrex')
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): 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 .*"): 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): 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), (3, 5, 5),
(4, 5, 2), (4, 5, 2),
(5, 5, 1), (5, 5, 1),
]) ])
def test_calculate_backoff(retrycount, max_retries, expected): def test_calculate_backoff(retrycount, max_retries, expected):
assert 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", pair="XRP/USDT",
since=unix_time 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)

View File

@ -14,7 +14,11 @@ from .test_exchange import ccxt_exceptionhandlers
STOPLOSS_ORDERTYPE = 'stop' 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() api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) 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') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
# stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, order = exchange.stoploss(
order_types={'stoploss_on_exchange_limit_ratio': 1.05}) 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]['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]['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 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 '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'] 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() 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 'id' in order
assert 'info' in order assert 'info' in order
assert order['id'] == order_id 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]['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]['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 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 '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 assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220
api_mock.create_order.reset_mock() api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order = exchange.stoploss(
order_types={'stoploss': 'limit'}) pair='ETH/BTC',
amount=1,
stop_price=220,
order_types={'stoploss': 'limit'}, side=side,
leverage=1.0
)
assert 'id' in order assert 'id' in order
assert 'info' in order assert 'info' in order
assert order['id'] == order_id 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]['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]['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 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 '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 assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220
# test exception handling # test exception handling
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') 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): with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock( api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately.")) side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') 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", ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx",
"stoploss", "create_order", retries=1, "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() api_mock = MagicMock()
default_conf['dry_run'] = True default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) 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() 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 'id' in order
assert 'info' in order assert 'info' in order
@ -112,20 +157,24 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker):
assert order['amount'] == 1 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') exchange = get_patched_exchange(mocker, default_conf, id='ftx')
order = { order = {
'type': STOPLOSS_ORDERTYPE, 'type': STOPLOSS_ORDERTYPE,
'price': 1500, 'price': 1500,
} }
assert exchange.stoploss_adjust(1501, order) assert exchange.stoploss_adjust(sl1, order, side=side)
assert not exchange.stoploss_adjust(1499, order) assert not exchange.stoploss_adjust(sl2, order, side=side)
# Test with invalid order case ... # Test with invalid order case ...
order['type'] = 'stop_loss_limit' 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 default_conf['dry_run'] = True
order = MagicMock() order = MagicMock()
order.myid = 123 order.myid = 123
@ -158,6 +207,16 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order):
assert resp['type'] == 'stop' assert resp['type'] == 'stop'
assert resp['status_stop'] == 'triggered' 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): with pytest.raises(InvalidOrderException):
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') 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' 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 == {}

View File

@ -166,7 +166,11 @@ def test_get_balances_prod(default_conf, mocker):
@pytest.mark.parametrize('ordertype', ['market', 'limit']) @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() api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) 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') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order = exchange.stoploss(
order_types={'stoploss': ordertype, pair='ETH/BTC',
'stoploss_on_exchange_limit_ratio': 0.99 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 'id' in order
assert 'info' in order assert 'info' in order
@ -195,12 +206,14 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype):
if ordertype == 'limit': 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]['type'] == STOPLOSS_LIMIT_ORDERTYPE
assert api_mock.create_order.call_args_list[0][1]['params'] == { assert api_mock.create_order.call_args_list[0][1]['params'] == {
'trading_agreement': 'agree', 'price2': 217.8} 'trading_agreement': 'agree',
'price2': adjustedprice
}
else: else:
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
assert api_mock.create_order.call_args_list[0][1]['params'] == { assert api_mock.create_order.call_args_list[0][1]['params'] == {
'trading_agreement': 'agree'} '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]['amount'] == 1
assert api_mock.create_order.call_args_list[0][1]['price'] == 220 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): with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') 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): with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock( api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately.")) side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') 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", ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
"stoploss", "create_order", retries=1, "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() api_mock = MagicMock()
default_conf['dry_run'] = True default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) 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() 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 'id' in order
assert 'info' in order assert 'info' in order
@ -242,14 +278,54 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
assert order['amount'] == 1 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') exchange = get_patched_exchange(mocker, default_conf, id='kraken')
order = { order = {
'type': STOPLOSS_ORDERTYPE, 'type': STOPLOSS_ORDERTYPE,
'price': 1500, 'price': 1500,
} }
assert exchange.stoploss_adjust(1501, order) assert exchange.stoploss_adjust(sl1, order, side=side)
assert not exchange.stoploss_adjust(1499, order) assert not exchange.stoploss_adjust(sl2, order, side=side)
# Test with invalid order case ... # Test with invalid order case ...
order['type'] = 'stop_loss_limit' 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]
}

View File

@ -22,9 +22,10 @@ twentyfive_hours = Decimal(25.0)
('kraken', 0.00025, five_hours, 0.045), ('kraken', 0.00025, five_hours, 0.045),
('kraken', 0.00025, twentyfive_hours, 0.12), ('kraken', 0.00025, twentyfive_hours, 0.12),
# FTX # FTX
# TODO-lev: - implement FTX tests ('ftx', 0.0005, ten_mins, 0.00125),
# ('ftx', Decimal(0.0005), ten_mins, 0.06), ('ftx', 0.00025, ten_mins, 0.000625),
# ('ftx', Decimal(0.0005), five_hours, 0.045), ('ftx', 0.00025, five_hours, 0.003125),
('ftx', 0.00025, twentyfive_hours, 0.015625),
]) ])
def test_interest(exchange, interest_rate, hours, expected): def test_interest(exchange, interest_rate, hours, expected):
borrowed = Decimal(60.0) borrowed = Decimal(60.0)

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
@ -11,7 +12,8 @@ from freqtrade.persistence import Trade
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.resolvers import PairListResolver 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") @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) 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: def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] 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): def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history):
ohlcv_data = { with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
('ETH/BTC', '1d'): ohlcv_history, ohlcv_data = {
('TKN/BTC', '1d'): ohlcv_history, ('ETH/BTC', '1d'): ohlcv_history,
('LTC/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), mocker.patch.multiple(
exchange_has=MagicMock(return_value=True), 'freqtrade.exchange.Exchange',
get_tickers=tickers markets=PropertyMock(return_value=markets),
) exchange_has=MagicMock(return_value=True),
mocker.patch.multiple( get_tickers=tickers,
'freqtrade.exchange.Exchange', refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data),
refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), )
)
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter)
assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0
freqtrade.pairlists.refresh_pairlist() freqtrade.pairlists.refresh_pairlist()
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
# Called once for XRP/BTC assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 2
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.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: def test_OffsetFilter_error(mocker, whitelist_conf) -> None:

View File

@ -422,20 +422,22 @@ def test_api_stopbuy(botclient):
assert ftbot.config['max_open_trades'] == 0 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, client = botclient
ftbot.config['dry_run'] = False ftbot.config['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) 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', mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
side_effect=lambda a, b: f"{a}/{b}") side_effect=lambda a, b: f"{a}/{b}")
ftbot.wallets.update() ftbot.wallets.update()
rc = client_get(client, f"{BASE_URI}/balance") rc = client_get(client, f"{BASE_URI}/balance")
assert_response(rc) assert_response(rc)
assert "currencies" in rc.json() response = rc.json()
assert len(rc.json()["currencies"]) == 5 assert "currencies" in response
assert rc.json()['currencies'][0] == { assert len(response["currencies"]) == 5
assert response['currencies'][0] == {
'currency': 'BTC', 'currency': 'BTC',
'free': 12.0, 'free': 12.0,
'balance': 12.0, 'balance': 12.0,
@ -443,6 +445,10 @@ def test_api_balance(botclient, mocker, rpc_balance):
'est_stake': 12.0, 'est_stake': 12.0,
'stake': 'BTC', '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): def test_api_count(botclient, mocker, ticker, fee, markets):
@ -1218,6 +1224,7 @@ def test_api_strategies(botclient):
assert_response(rc) assert_response(rc)
assert rc.json() == {'strategies': [ assert rc.json() == {'strategies': [
'HyperoptableStrategy', 'HyperoptableStrategy',
'InformativeDecoratorTest',
'StrategyTestV2', 'StrategyTestV2',
'TestStrategyLegacyV1' 'TestStrategyLegacyV1'
]} ]}

View File

@ -576,6 +576,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None
'total': 100.0, 'total': 100.0,
'symbol': 100.0, 'symbol': 100.0,
'value': 1000.0, 'value': 1000.0,
'starting_capital': 1000,
'starting_capital_fiat': 1000,
}) })
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)

View File

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

View File

@ -611,7 +611,7 @@ def test_is_informative_pairs_callback(default_conf):
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
# Should return empty # Should return empty
# Uses fallback to base implementation # Uses fallback to base implementation
assert [] == strategy.informative_pairs() assert [] == strategy.gather_informative_pairs()
@pytest.mark.parametrize('error', [ @pytest.mark.parametrize('error', [

View File

@ -4,7 +4,9 @@ import numpy as np
import pandas as pd import pandas as pd
import pytest 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): def generate_test_data(timeframe: str, size: int):
@ -132,3 +134,65 @@ def test_stoploss_from_open():
assert stoploss == 0 assert stoploss == 0
else: else:
assert isclose(stop_price, expected_stop_price, rel_tol=0.00001) 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

View File

@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed():
directory = Path(__file__).parent / "strats" directory = Path(__file__).parent / "strats"
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
assert isinstance(strategies, list) assert isinstance(strategies, list)
assert len(strategies) == 3 assert len(strategies) == 4
assert isinstance(strategies[0], dict) assert isinstance(strategies[0], dict)
@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed():
directory = Path(__file__).parent / "strats" directory = Path(__file__).parent / "strats"
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
assert isinstance(strategies, list) assert isinstance(strategies, list)
assert len(strategies) == 4 assert len(strategies) == 5
# with enum_failed=True search_all_objects() shall find 2 good strategies # with enum_failed=True search_all_objects() shall find 2 good strategies
# and 1 which fails to load # 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 assert len([x for x in strategies if x['class'] is None]) == 1

View File

@ -78,11 +78,15 @@ def test_bot_cleanup(mocker, default_conf, caplog) -> None:
assert coo_mock.call_count == 1 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_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
conf = default_conf.copy() conf = default_conf.copy()
conf['runmode'] = RunMode.DRY_RUN conf['runmode'] = runmode
conf['order_types'] = { conf['order_types'] = {
'buy': 'market', 'buy': 'market',
'sell': 'limit', 'sell': 'limit',
@ -92,45 +96,14 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
conf['bid_strategy']['price_side'] = 'ask' conf['bid_strategy']['price_side'] = 'ask'
freqtrade = FreqtradeBot(conf) 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'] assert freqtrade.strategy.order_types['stoploss_on_exchange']
caplog.clear() caplog.clear()
# is left untouched # is left untouched
conf = default_conf.copy() conf = default_conf.copy()
conf['runmode'] = RunMode.DRY_RUN conf['runmode'] = runmode
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['order_types'] = { conf['order_types'] = {
'buy': 'market', 'buy': 'market',
'sell': 'limit', '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 '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_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
patch_edge(mocker) patch_edge(mocker)
@ -234,9 +213,9 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={ fetch_ticker=MagicMock(return_value={
'bid': buy_price * 0.79, 'bid': buy_price * buy_price_mult,
'ask': buy_price * 0.79, 'ask': buy_price * buy_price_mult,
'last': buy_price * 0.79 'last': buy_price * buy_price_mult,
}), }),
get_fee=fee, get_fee=fee,
) )
@ -253,46 +232,10 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
############################################# #############################################
# stoploss shoud be hit # stoploss shoud be hit
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is not ignore_strat_sl
assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) if not ignore_strat_sl:
assert trade.sell_reason == SellType.STOP_LOSS.value 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
def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None: 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') freqtrade.create_trade('ETH/BTC')
def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, @pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [
fee, mocker) -> None: (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_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
buy_mock = MagicMock(return_value=limit_buy_order_open) 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, create_order=buy_mock,
get_fee=fee, get_fee=fee,
) )
default_conf['stake_amount'] = 0.0005 default_conf['max_open_trades'] = max_open_trades
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
freqtrade.config['stake_amount'] = stake_amount
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
freqtrade.create_trade('ETH/BTC') if create:
rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] assert freqtrade.create_trade('ETH/BTC')
assert rate * amount <= default_conf['stake_amount'] if amount_enough:
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, else:
fee, mocker, caplog) -> None: assert log_has_re(
patch_RPCManager(mocker) r"Stake amount for pair .* is too small.*",
patch_exchange(mocker) caplog
buy_mock = MagicMock(return_value=limit_buy_order_open) )
mocker.patch.multiple( else:
'freqtrade.exchange.Exchange', assert not freqtrade.create_trade('ETH/BTC')
fetch_ticker=ticker, if not max_open_trades:
create_order=buy_mock, assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0
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
@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, 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_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( 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), create_order=MagicMock(return_value=limit_buy_order_open),
get_fee=fee, get_fee=fee,
) )
default_conf['exchange']['pair_whitelist'] = whitelist
default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
n = freqtrade.enter_positions() n = freqtrade.enter_positions()
assert n == 1 assert n == positions
assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) if positions:
n = freqtrade.enter_positions() assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog)
assert n == 0 n = freqtrade.enter_positions()
assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) assert n == 0
assert log_has_re(r"No currency pair in active pair whitelist.*", caplog)
else:
def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, assert n == 0
mocker, caplog) -> None: assert log_has("Active pair whitelist is empty.", caplog)
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)
@pytest.mark.usefixtures("init_persistence") @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") @pytest.mark.usefixtures("init_persistence")
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
limit_buy_order, limit_sell_order) -> None: limit_buy_order, limit_sell_order) -> None:
# TODO-lev: test for short
# When trailing stoploss is set # When trailing stoploss is set
stoploss = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker) 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 assert freqtrade.handle_stoploss_on_exchange(trade) is False
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
stoploss_order_mock.assert_called_once_with(amount=85.32423208, stoploss_order_mock.assert_called_once_with(
pair='ETH/BTC', amount=85.32423208,
order_types=freqtrade.strategy.order_types, pair='ETH/BTC',
stop_price=0.00002346 * 0.95) 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. # price fell below stoploss, so dry-run sells trade.
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 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, def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
limit_buy_order, limit_sell_order) -> None: limit_buy_order, limit_sell_order) -> None:
# TODO-lev: test for short
# When trailing stoploss is set # When trailing stoploss is set
stoploss = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
patch_exchange(mocker) patch_exchange(mocker)
@ -1417,7 +1313,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
side_effect=InvalidOrderException()) side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
return_value=stoploss_order_hanging) 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) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
# Still try to create order # Still try to create order
@ -1427,7 +1323,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
caplog.clear() caplog.clear()
cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock())
mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) 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 cancel_mock.call_count == 1
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) 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, def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee,
limit_buy_order, limit_sell_order) -> None: limit_buy_order, limit_sell_order) -> None:
# When trailing stoploss is set # When trailing stoploss is set
# TODO-lev: test for short
stoploss = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker) patch_RPCManager(mocker)
mocker.patch.multiple( 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 assert freqtrade.handle_stoploss_on_exchange(trade) is False
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
stoploss_order_mock.assert_called_once_with(amount=85.32423208, stoploss_order_mock.assert_called_once_with(
pair='ETH/BTC', amount=85.32423208,
order_types=freqtrade.strategy.order_types, pair='ETH/BTC',
stop_price=0.00002346 * 0.96) 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. # price fell below stoploss, so dry-run sells trade.
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 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, def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
limit_buy_order, limit_sell_order) -> None: limit_buy_order, limit_sell_order) -> None:
# TODO-lev: test for short
# When trailing stoploss is set # When trailing stoploss is set
stoploss = MagicMock(return_value={'id': 13434334}) stoploss = MagicMock(return_value={'id': 13434334})
patch_RPCManager(mocker) 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 # stoploss should be set to 1% as trailing is on
assert trade.stop_loss == 0.00002346 * 0.99 assert trade.stop_loss == 0.00002346 * 0.99
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
stoploss_order_mock.assert_called_once_with(amount=2132892.49146757, stoploss_order_mock.assert_called_once_with(
pair='NEO/BTC', amount=2132892.49146757,
order_types=freqtrade.strategy.order_types, pair='NEO/BTC',
stop_price=0.00002346 * 0.99) 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) caplog.set_level(logging.DEBUG)
freqtrade = get_patched_freqtradebot(mocker, default_conf) 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( mock_ct = mocker.patch(
'freqtrade.freqtradebot.FreqtradeBot.create_trade', 'freqtrade.freqtradebot.FreqtradeBot.create_trade',
MagicMock(side_effect=DependencyException) MagicMock(
return_value=return_value,
side_effect=side_effect
)
) )
n = freqtrade.enter_positions() n = freqtrade.enter_positions()
assert n == 0 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 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: 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) 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, 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) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
# fetch_order should not be called!! # fetch_order should not be called!!
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) 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) freqtrade.update_trade_state(trade, '123456', limit_buy_order)
assert trade.amount != amount assert trade.amount != amount
assert trade.amount == limit_buy_order['amount'] assert trade.amount == limit_buy_order['amount']
if has_rounding_fee:
assert log_has_re(r'Applying fee on amount for .*', caplog)
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)
def test_update_trade_state_exception(mocker, default_conf, 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 assert mock_insuf.call_count == 1
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, @pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [
fee, mocker) -> None: # 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_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={ fetch_ticker=MagicMock(return_value={
'bid': 0.00001172, 'bid': bid,
'ask': 0.00001173, 'ask': ask,
'last': 0.00001172 'last': bid
}), }),
create_order=MagicMock(side_effect=[ create_order=MagicMock(side_effect=[
limit_buy_order_open, 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({ default_conf.update({
'use_sell_signal': True, 'use_sell_signal': True,
'sell_profit_only': True, 'sell_profit_only': profit_only,
'sell_profit_offset': 0.1, 'sell_profit_offset': 0.1,
}) })
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade) 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() freqtrade.enter_positions()
trade = Trade.query.first() trade = Trade.query.first()
trade.update(limit_buy_order) trade.update(limit_buy_order)
freqtrade.wallets.update() freqtrade.wallets.update()
patch_get_signal(freqtrade, value=(False, True, None)) 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 if handle_second:
assert freqtrade.handle_trade(trade) is True freqtrade.strategy.sell_profit_offset = 0.0
assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.SELL_SIGNAL.value assert trade.sell_reason == sell_type
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
def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open, 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 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_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
amount = 95.33 amount = 95.33
amount_wallet = 95.29 amount_wallet = amount_wallet
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet))
wallet_update = mocker.patch('freqtrade.wallets.Wallets.update') wallet_update = mocker.patch('freqtrade.wallets.Wallets.update')
trade = Trade( trade = Trade(
@ -3325,37 +3125,19 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker):
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
if has_err:
wallet_update.reset_mock() with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."):
assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
assert log_has_re(r'.*Falling back to wallet-amount.', caplog) else:
assert wallet_update.call_count == 1 wallet_update.reset_mock()
caplog.clear() assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet
wallet_update.reset_mock() assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet assert wallet_update.call_count == 1
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) caplog.clear()
assert wallet_update.call_count == 1 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)
def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): assert wallet_update.call_count == 1
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)
def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: 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 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 test if function get_rate will return the order book price instead of the ask rate
instead of the ask rate
""" """
patch_exchange(mocker) 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( mocker.patch.multiple(
'freqtrade.exchange.Exchange', '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, fetch_ticker=ticker_mock,
) )
default_conf['exchange']['name'] = 'binance' default_conf['exchange']['name'] = 'binance'
default_conf['bid_strategy']['use_order_book'] = True 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['bid_strategy']['ask_last_balance'] = 0
default_conf['telegram']['enabled'] = False default_conf['telegram']['enabled'] = False
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 if exception_thrown:
assert ticker_mock.call_count == 0 with pytest.raises(PricingError):
freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy")
assert log_has_re(
def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None: r'Buy Price at location 1 from orderbook could not be determined.', caplog)
patch_exchange(mocker) else:
ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046}) assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935
mocker.patch.multiple( assert ticker_mock.call_count == 0
'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)
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> 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