merged lev-freqtradebot with lev-strat

This commit is contained in:
Sam Germain 2021-09-19 19:06:43 -06:00
commit d7c7448632
55 changed files with 2704 additions and 1036 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,5 +1,7 @@
""" Binance exchange subclass """ """ Binance exchange subclass """
import json
import logging import logging
from pathlib import Path
from typing import Dict, List, Optional, Tuple from typing import Dict, List, Optional, Tuple
import arrow import arrow
@ -31,9 +33,27 @@ class Binance(Exchange):
# TradingMode.SPOT always supported and not required in this list # TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
# (TradingMode.FUTURES, 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 # (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: 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)
@ -47,8 +67,8 @@ class Binance(Exchange):
) )
@retrier(retries=0) @retrier(retries=0)
def stoploss(self, pair: str, amount: float, def stoploss(self, pair: str, amount: float, stop_price: float,
stop_price: float, order_types: Dict, side: str) -> Dict: 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.
@ -76,7 +96,7 @@ class Binance(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, side, amount, stop_price) pair, ordertype, side, amount, stop_price, leverage)
return dry_order return dry_order
try: try:
@ -87,6 +107,7 @@ class Binance(Exchange):
rate = self.price_to_precision(pair, rate) rate = self.price_to_precision(pair, rate)
self._lev_prep(pair, leverage)
order = self._api.create_order(symbol=pair, type=ordertype, side=side, 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. '
@ -119,26 +140,35 @@ class Binance(Exchange):
Assigns property _leverage_brackets to a dictionary of information about the leverage Assigns property _leverage_brackets to a dictionary of information about the leverage
allowed on each pair allowed on each pair
""" """
try: if self.trading_mode == TradingMode.FUTURES:
leverage_brackets = self._api.load_leverage_brackets() try:
for pair, brackets in leverage_brackets.items(): if self._config['dry_run']:
self._leverage_brackets[pair] = [ leverage_brackets_path = (
[ Path(__file__).parent / 'binance_leverage_brackets.json'
min_amount, )
float(margin_req) with open(leverage_brackets_path) as json_file:
] for [ leverage_brackets = json.load(json_file)
min_amount, else:
margin_req leverage_brackets = self._api.load_leverage_brackets()
] in brackets
]
except ccxt.DDoSProtection as e: for pair, brackets in leverage_brackets.items():
raise DDosProtection(e) from e self._leverage_brackets[pair] = [
except (ccxt.NetworkError, ccxt.ExchangeError) as e: [
raise TemporaryError(f'Could not fetch leverage amounts due to' min_amount,
f'{e.__class__.__name__}. Message: {e}') from e float(margin_req)
except ccxt.BaseError as e: ] for [
raise OperationalException(e) from e 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: def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
""" """
@ -166,9 +196,11 @@ class Binance(Exchange):
""" """
trading_mode = trading_mode or self.trading_mode trading_mode = trading_mode or self.trading_mode
if self._config['dry_run'] or trading_mode != TradingMode.FUTURES:
return
try: try:
if trading_mode == TradingMode.FUTURES: self._api.set_leverage(symbol=pair, leverage=leverage)
self._api.set_leverage(symbol=pair, leverage=leverage)
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:

File diff suppressed because it is too large Load Diff

View File

@ -49,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 = {}
@ -131,14 +128,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', {}),
@ -146,17 +154,6 @@ 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)
self.trading_mode: TradingMode = (
TradingMode(config.get('trading_mode'))
if config.get('trading_mode')
else TradingMode.SPOT
)
collateral: Optional[Collateral] = (
Collateral(config.get('collateral'))
if config.get('collateral')
else None
)
if self.trading_mode != TradingMode.SPOT: if self.trading_mode != TradingMode.SPOT:
self.fill_leverage_brackets() self.fill_leverage_brackets()
@ -177,7 +174,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, collateral) 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
@ -210,7 +207,6 @@ class Exchange:
'secret': exchange_config.get('secret'), 'secret': exchange_config.get('secret'),
'password': exchange_config.get('password'), 'password': exchange_config.get('password'),
'uid': exchange_config.get('uid', ''), 'uid': exchange_config.get('uid', ''),
'options': exchange_config.get('options', {})
} }
if ccxt_kwargs: if ccxt_kwargs:
logger.info('Applying additional ccxt config: %s', ccxt_kwargs) logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
@ -231,6 +227,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)"""
@ -617,15 +618,13 @@ 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 self._apply_leverage_to_stake_amount( return self._get_stake_amount_considering_leverage(
max(min_stake_amounts) * amount_reserve_percent, max(min_stake_amounts) * amount_reserve_percent,
leverage or 1.0 leverage or 1.0
) )
def _apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float):
""" """
#TODO-lev: Find out how this works on Kraken and FTX
# * Should be implemented by child classes if leverage affects the stake_amount
Takes the minimum stake amount for a pair with no leverage and returns the minimum Takes the minimum stake amount for a pair with no leverage and returns the minimum
stake amount when leverage is considered stake amount when leverage is considered
:param stake_amount: The stake amount for a pair before leverage is considered :param stake_amount: The stake amount for a pair before leverage is considered
@ -636,7 +635,7 @@ class Exchange:
# 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] = {
@ -653,7 +652,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"]}
@ -663,7 +663,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)
@ -771,19 +771,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', leverage=1.0) -> Dict:
if self._config['dry_run']:
dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate)
return dry_order
if self.trading_mode != TradingMode.SPOT: if self.trading_mode != TradingMode.SPOT:
self.set_margin_mode(pair, self.collateral)
self._set_leverage(leverage, pair) self._set_leverage(leverage, pair)
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
@ -792,6 +799,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)
@ -822,8 +830,8 @@ class Exchange:
""" """
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, def stoploss(self, pair: str, amount: float, stop_price: float,
stop_price: float, order_types: Dict, side: str) -> Dict: 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.
@ -1586,15 +1594,13 @@ class Exchange:
self._async_get_trade_history(pair=pair, since=since, self._async_get_trade_history(pair=pair, since=since,
until=until, from_id=from_id)) until=until, from_id=from_id))
@retrier
def fill_leverage_brackets(self): def fill_leverage_brackets(self):
""" """
#TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken # 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 Assigns property _leverage_brackets to a dictionary of information about the leverage
allowed on each pair allowed on each pair
""" """
raise OperationalException( return
f"{self.name.capitalize()}.fill_leverage_brackets has not been implemented.")
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
""" """
@ -1615,7 +1621,7 @@ class Exchange:
Set's the leverage before making a trade, in order to not Set's the leverage before making a trade, in order to not
have the same leverage on every trade have the same leverage on every trade
""" """
if not self.exchange_has("setLeverage"): if self._config['dry_run'] or not self.exchange_has("setLeverage"):
# Some exchanges only support one collateral type # Some exchanges only support one collateral type
return return
@ -1635,7 +1641,7 @@ class Exchange:
Set's the margin mode on the exchange to cross or isolated for a specific pair 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") :param symbol: base/quote currency pair (e.g. "ADA/USDT")
''' '''
if not self.exchange_has("setMarginMode"): if self._config['dry_run'] or not self.exchange_has("setMarginMode"):
# Some exchanges only support one collateral type # Some exchanges only support one collateral type
return return

View File

@ -49,8 +49,8 @@ class Ftx(Exchange):
) )
@retrier(retries=0) @retrier(retries=0)
def stoploss(self, pair: str, amount: float, def stoploss(self, pair: str, amount: float, stop_price: float,
stop_price: float, order_types: Dict, side: str) -> Dict: 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.
@ -69,7 +69,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, side, amount, stop_price) pair, ordertype, side, amount, stop_price, leverage)
return dry_order return dry_order
try: try:
@ -81,6 +81,7 @@ 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)
self._lev_prep(pair, leverage)
order = self._api.create_order(symbol=pair, type=ordertype, side=side, 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)

View File

@ -85,8 +85,8 @@ class Kraken(Exchange):
)) ))
@retrier(retries=0) @retrier(retries=0)
def stoploss(self, pair: str, amount: float, def stoploss(self, pair: str, amount: float, stop_price: float,
stop_price: float, order_types: Dict, side: str) -> Dict: 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.
@ -108,7 +108,7 @@ 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, side, amount, stop_price) pair, ordertype, side, amount, stop_price, leverage)
return dry_order return dry_order
try: try:
@ -182,8 +182,16 @@ class Kraken(Exchange):
Kraken set's the leverage as an option in the order object, so we need to Kraken set's the leverage as an option in the order object, so we need to
add it to params add it to params
""" """
if leverage > 1.0: if leverage > 1.0:
self._params['leverage'] = leverage self._params['leverage'] = leverage
else: else:
if 'leverage' in self._params: if 'leverage' in self._params:
del self._params['leverage'] del self._params['leverage']
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 \
@ -175,7 +175,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)()
@ -812,10 +812,8 @@ class FreqtradeBot(LoggingMixin):
exit_signal_type = "exit_short" if trade.is_short else "exit_long" exit_signal_type = "exit_short" if trade.is_short else "exit_long"
# TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal
if ( if (self.config.get('use_sell_signal', True) or
self.config.get('use_sell_signal', True) or self.config.get('ignore_roi_if_buy_signal', False)):
self.config.get('ignore_roi_if_buy_signal', False)
):
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
self.strategy.timeframe) self.strategy.timeframe)
@ -847,7 +845,8 @@ class FreqtradeBot(LoggingMixin):
amount=trade.amount, amount=trade.amount,
stop_price=stop_price, stop_price=stop_price,
order_types=self.strategy.order_types, order_types=self.strategy.order_types,
side=trade.exit_side 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')
@ -947,7 +946,7 @@ class FreqtradeBot(LoggingMixin):
return False return False
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None: def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> 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

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

@ -157,7 +157,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
@ -1025,17 +1025,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:
""" """
@ -379,6 +400,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
@ -461,12 +499,12 @@ class IStrategy(ABC, HyperStrategyMixin):
self.dp._set_cached_df(pair, self.timeframe, dataframe) self.dp._set_cached_df(pair, self.timeframe, dataframe)
else: else:
logger.debug("Skipping TA Analysis for already analyzed candle") logger.debug("Skipping TA Analysis for already analyzed candle")
dataframe['buy'] = 0 dataframe[SignalType.ENTER_LONG.value] = 0
dataframe['sell'] = 0 dataframe[SignalType.EXIT_LONG.value] = 0
dataframe['enter_short'] = 0 dataframe[SignalType.ENTER_SHORT.value] = 0
dataframe['exit_short'] = 0 dataframe[SignalType.EXIT_SHORT.value] = 0
dataframe['buy_tag'] = None dataframe[SignalTagType.BUY_TAG.value] = None
dataframe['short_tag'] = None dataframe[SignalTagType.SHORT_TAG.value] = None
# Other Defs in strategy that want to be called every loop here # Other Defs in strategy that want to be called every loop here
# twitter_sell = self.watch_twitter_feed(dataframe, metadata) # twitter_sell = self.watch_twitter_feed(dataframe, metadata)
@ -862,10 +900,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:
@ -877,6 +916,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

@ -5,7 +5,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.
@ -25,6 +27,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
""" """
@ -33,25 +37,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()
@ -97,3 +105,28 @@ def stoploss_from_open(
return min(stoploss, 0.0) return min(stoploss, 0.0)
else: else:
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

@ -122,7 +122,7 @@ class {{ strategy }}(IStrategy):
{{ buy_trend | indent(16) }} {{ buy_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0 (dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'buy'] = 1 'enter_long'] = 1
return dataframe return dataframe
@ -138,6 +138,6 @@ class {{ strategy }}(IStrategy):
{{ sell_trend | indent(16) }} {{ sell_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0 (dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'sell'] = 1 'exit_long'] = 1
return dataframe return dataframe
{{ additional_methods | indent(4) }} {{ additional_methods | indent(4) }}

View File

@ -354,7 +354,7 @@ class SampleStrategy(IStrategy):
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
(dataframe['volume'] > 0) # Make sure Volume is not 0 (dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'buy'] = 1 'enter_long'] = 1
dataframe.loc[ dataframe.loc[
( (
@ -383,7 +383,8 @@ class SampleStrategy(IStrategy):
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
(dataframe['volume'] > 0) # Make sure Volume is not 0 (dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'sell'] = 1
'exit_long'] = 1
dataframe.loc[ dataframe.loc[
( (

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.enums.signaltype import SignalDirection from freqtrade.enums.signaltype import SignalDirection
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
@ -90,7 +90,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())
@ -99,10 +105,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:
@ -110,8 +128,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)

View File

@ -5,7 +5,7 @@ from unittest.mock import MagicMock, PropertyMock
import ccxt import ccxt
import pytest import pytest
from freqtrade.enums import TradingMode 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
@ -48,13 +48,20 @@ def test_stoploss_order_binance(
amount=1, amount=1,
stop_price=190, stop_price=190,
side=side, side=side,
order_types={'stoploss_on_exchange_limit_ratio': 1.05} 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 = exchange.stoploss(
order_types=order_types, side=side) 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
@ -71,17 +78,31 @@ def test_stoploss_order_binance(
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={}, side=side) 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={}, side=side) 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={}, side=side) 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):
@ -94,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, side="sell", 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={}, side="sell") 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
@ -194,6 +228,9 @@ def test_fill_leverage_brackets_binance(default_conf, mocker):
[1000000.0, 0.5]], [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 = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
exchange.fill_leverage_brackets() exchange.fill_leverage_brackets()
@ -236,12 +273,59 @@ def test_fill_leverage_brackets_binance(default_conf, mocker):
) )
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): def test__set_leverage_binance(mocker, default_conf):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.set_leverage = MagicMock() api_mock.set_leverage = MagicMock()
type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) type(api_mock).has = PropertyMock(return_value={'setLeverage': True})
default_conf['dry_run'] = False
exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange = get_patched_exchange(mocker, default_conf, id="binance")
exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN) exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN)
@ -288,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

@ -132,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 = {}
# TODO-lev: Test with options # TODO-lev: Test with options
@ -403,7 +404,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0)
assert isclose(result, expected_result/3) assert isclose(result, expected_result/3)
# TODO-lev: Min stake for base, kraken and ftx
# min amount is set # min amount is set
markets["ETH/BTC"]["limits"] = { markets["ETH/BTC"]["limits"] = {
@ -420,27 +420,21 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0)
assert isclose(result, expected_result/5) assert isclose(result, expected_result/5)
# TODO-lev: Min stake for base, kraken and ftx
# min amount and cost are set (cost is minimal)
markets["ETH/BTC"]["limits"] = {
'cost': {'min': 2},
'amount': {'min': 2} 'amount': {'min': 2}
} }
mocker.patch( mocker.patch(
'freqtrade.exchange.Exchange.markets', 'freqtrade.exchange.Exchange.markets',
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)
expected_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) assert isclose(result, expected_result)
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10)
assert isclose(result, expected_result/10) assert isclose(result, expected_result/10)
# TODO-lev: Min stake for base, kraken and ftx
# 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"]={
'cost': {'min': 8}, 'cost': {'min': 8},
'amount': {'min': 2} 'amount': {'min': 2}
} }
@ -448,39 +442,28 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
'freqtrade.exchange.Exchange.markets', 'freqtrade.exchange.Exchange.markets',
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)
expected_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) assert isclose(result, expected_result)
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0)
assert isclose(result, expected_result/7.0) assert isclose(result, expected_result/7.0)
# TODO-lev: Min stake for base, kraken and ftx
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4)
expected_result = max(8, 2 * 2) * 1.5 expected_result=max(8, 2 * 2) * 1.5
assert isclose(result, expected_result) assert isclose(result, expected_result)
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0)
assert isclose(result, expected_result/8.0) assert isclose(result, expected_result/8.0)
# TODO-lev: Min stake for base, kraken and ftx
# Really big stoploss
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1)
expected_result = max(8, 2 * 2) * 1.5
assert isclose(result, expected_result) assert isclose(result, expected_result)
# With Leverage # With Leverage
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0)
assert isclose(result, expected_result/12) assert isclose(result, expected_result/12)
# TODO-lev: Min stake for base, kraken and ftx stoploss=-0.05
markets={'ETH/BTC': {'symbol': 'ETH/BTC'}}
def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
exchange = get_patched_exchange(mocker, default_conf, id="binance")
stoploss = -0.05
markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}}
# Real Binance data # Real Binance data
markets["ETH/BTC"]["limits"] = { markets["ETH/BTC"]["limits"]={
'cost': {'min': 0.0001}, 'cost': {'min': 0.0001},
'amount': {'min': 0.001} 'amount': {'min': 0.001}
} }
@ -488,28 +471,27 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
'freqtrade.exchange.Exchange.markets', 'freqtrade.exchange.Exchange.markets',
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)
expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) expected_result=max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss))
assert round(result, 8) == round(expected_result, 8) assert round(result, 8) == round(expected_result, 8)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) result=exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0)
assert round(result, 8) == round(expected_result/3, 8) assert round(result, 8) == round(expected_result/3, 8)
# TODO-lev: Min stake for base, kraken and ftx
def test_set_sandbox(default_conf, mocker): def test_set_sandbox(default_conf, mocker):
""" """
Test working scenario Test working scenario
""" """
api_mock = MagicMock() api_mock=MagicMock()
api_mock.load_markets = MagicMock(return_value={ api_mock.load_markets=MagicMock(return_value = {
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
}) })
url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com", url_mock=PropertyMock(return_value = {'test': "api-public.sandbox.gdax.com",
'api': 'https://api.gdax.com'}) 'api': 'https://api.gdax.com'})
type(api_mock).urls = url_mock type(api_mock).urls=url_mock
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange=get_patched_exchange(mocker, default_conf, api_mock)
liveurl = exchange._api.urls['api'] liveurl=exchange._api.urls['api']
default_conf['exchange']['sandbox'] = True default_conf['exchange']['sandbox']=True
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
assert exchange._api.urls['api'] != liveurl assert exchange._api.urls['api'] != liveurl
@ -518,16 +500,16 @@ def test_set_sandbox_exception(default_conf, mocker):
""" """
Test Fail scenario Test Fail scenario
""" """
api_mock = MagicMock() api_mock=MagicMock()
api_mock.load_markets = MagicMock(return_value={ api_mock.load_markets=MagicMock(return_value = {
'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': ''
}) })
url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'}) url_mock=PropertyMock(return_value = {'api': 'https://api.gdax.com'})
type(api_mock).urls = url_mock type(api_mock).urls=url_mock
with pytest.raises(OperationalException, match=r'does not provide a sandbox api'): with pytest.raises(OperationalException, match = r'does not provide a sandbox api'):
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange=get_patched_exchange(mocker, default_conf, api_mock)
default_conf['exchange']['sandbox'] = True default_conf['exchange']['sandbox']=True
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
@ -537,13 +519,13 @@ def test__load_async_markets(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_markets') mocker.patch('freqtrade.exchange.Exchange._load_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
exchange = Exchange(default_conf) exchange=Exchange(default_conf)
exchange._api_async.load_markets = get_mock_coro(None) exchange._api_async.load_markets=get_mock_coro(None)
exchange._load_async_markets() exchange._load_async_markets()
assert exchange._api_async.load_markets.call_count == 1 assert exchange._api_async.load_markets.call_count == 1
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef")) exchange._api_async.load_markets=Mock(side_effect = ccxt.BaseError("deadbeef"))
exchange._load_async_markets() exchange._load_async_markets()
assert log_has('Could not load async markets. Reason: deadbeef', caplog) assert log_has('Could not load async markets. Reason: deadbeef', caplog)
@ -551,8 +533,8 @@ def test__load_async_markets(default_conf, mocker, caplog):
def test__load_markets(default_conf, mocker, caplog): def test__load_markets(default_conf, mocker, caplog):
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
api_mock = MagicMock() api_mock=MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError")) api_mock.load_markets=MagicMock(side_effect = ccxt.BaseError("SomeError"))
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_pairs')
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
@ -561,28 +543,28 @@ def test__load_markets(default_conf, mocker, caplog):
Exchange(default_conf) Exchange(default_conf)
assert log_has('Unable to initialize markets.', caplog) assert log_has('Unable to initialize markets.', caplog)
expected_return = {'ETH/BTC': 'available'} expected_return={'ETH/BTC': 'available'}
api_mock = MagicMock() api_mock=MagicMock()
api_mock.load_markets = MagicMock(return_value=expected_return) api_mock.load_markets=MagicMock(return_value = expected_return)
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
default_conf['exchange']['pair_whitelist'] = ['ETH/BTC'] default_conf['exchange']['pair_whitelist']=['ETH/BTC']
ex = Exchange(default_conf) ex=Exchange(default_conf)
assert ex.markets == expected_return assert ex.markets == expected_return
def test_reload_markets(default_conf, mocker, caplog): def test_reload_markets(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
initial_markets = {'ETH/BTC': {}} initial_markets={'ETH/BTC': {}}
updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} updated_markets={'ETH/BTC': {}, "LTC/BTC": {}}
api_mock = MagicMock() api_mock=MagicMock()
api_mock.load_markets = MagicMock(return_value=initial_markets) api_mock.load_markets=MagicMock(return_value = initial_markets)
default_conf['exchange']['markets_refresh_interval'] = 10 default_conf['exchange']['markets_refresh_interval']=10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", exchange=get_patched_exchange(mocker, default_conf, api_mock, id = "binance",
mock_markets=False) mock_markets = False)
exchange._load_async_markets = MagicMock() exchange._load_async_markets=MagicMock()
exchange._last_markets_refresh = arrow.utcnow().int_timestamp exchange._last_markets_refresh=arrow.utcnow().int_timestamp
assert exchange.markets == initial_markets assert exchange.markets == initial_markets
@ -591,9 +573,9 @@ def test_reload_markets(default_conf, mocker, caplog):
assert exchange.markets == initial_markets assert exchange.markets == initial_markets
assert exchange._load_async_markets.call_count == 0 assert exchange._load_async_markets.call_count == 0
api_mock.load_markets = MagicMock(return_value=updated_markets) api_mock.load_markets=MagicMock(return_value = updated_markets)
# more than 10 minutes have passed, reload is executed # more than 10 minutes have passed, reload is executed
exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60 exchange._last_markets_refresh=arrow.utcnow().int_timestamp - 15 * 60
exchange.reload_markets() exchange.reload_markets()
assert exchange.markets == updated_markets assert exchange.markets == updated_markets
assert exchange._load_async_markets.call_count == 1 assert exchange._load_async_markets.call_count == 1
@ -603,10 +585,10 @@ def test_reload_markets(default_conf, mocker, caplog):
def test_reload_markets_exception(default_conf, mocker, caplog): def test_reload_markets_exception(default_conf, mocker, caplog):
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
api_mock = MagicMock() api_mock=MagicMock()
api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError")) api_mock.load_markets=MagicMock(side_effect = ccxt.NetworkError("LoadError"))
default_conf['exchange']['markets_refresh_interval'] = 10 default_conf['exchange']['markets_refresh_interval']=10
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange=get_patched_exchange(mocker, default_conf, api_mock, id = "binance")
# less than 10 minutes have passed, no reload # less than 10 minutes have passed, no reload
exchange.reload_markets() exchange.reload_markets()
@ -614,11 +596,11 @@ def test_reload_markets_exception(default_conf, mocker, caplog):
assert log_has_re(r"Could not reload markets.*", caplog) assert log_has_re(r"Could not reload markets.*", caplog)
@pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT']) @ pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT'])
def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog): def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
default_conf['stake_currency'] = stake_currency default_conf['stake_currency']=stake_currency
api_mock = MagicMock() api_mock=MagicMock()
type(api_mock).load_markets = MagicMock(return_value={ type(api_mock).load_markets=MagicMock(return_value = {
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
}) })
@ -630,9 +612,9 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog):
def test_validate_stakecurrency_error(default_conf, mocker, caplog): def test_validate_stakecurrency_error(default_conf, mocker, caplog):
default_conf['stake_currency'] = 'XRP' default_conf['stake_currency']='XRP'
api_mock = MagicMock() api_mock=MagicMock()
type(api_mock).load_markets = MagicMock(return_value={ type(api_mock).load_markets=MagicMock(return_value = {
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'},
}) })
@ -1004,7 +986,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
@ -1027,7 +1015,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"]
@ -1073,7 +1067,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
@ -1083,10 +1083,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),
@ -1108,9 +1105,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
@ -1120,6 +1125,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):
@ -2658,7 +2678,14 @@ 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={}, side="sell") 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, {}, side="sell") exchange.stoploss_adjust(1, {}, side="sell")
@ -3006,7 +3033,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
@ -3018,7 +3044,7 @@ def test_calculate_backoff(retrycount, max_retries, expected):
(20.0, 5.0, 4.0), (20.0, 5.0, 4.0),
(100.0, 100.0, 1.0) (100.0, 100.0, 1.0)
]) ])
def test_apply_leverage_to_stake_amount( def test_get_stake_amount_considering_leverage(
exchange, exchange,
stake_amount, stake_amount,
leverage, leverage,
@ -3027,7 +3053,8 @@ def test_apply_leverage_to_stake_amount(
default_conf default_conf
): ):
exchange = get_patched_exchange(mocker, default_conf, id=exchange) exchange = get_patched_exchange(mocker, default_conf, id=exchange)
assert exchange._apply_leverage_to_stake_amount(stake_amount, leverage) == min_stake_with_lev assert exchange._get_stake_amount_considering_leverage(
stake_amount, leverage) == min_stake_with_lev
@pytest.mark.parametrize("exchange_name,trading_mode", [ @pytest.mark.parametrize("exchange_name,trading_mode", [
@ -3040,6 +3067,7 @@ def test__set_leverage(mocker, default_conf, exchange_name, trading_mode):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.set_leverage = MagicMock() api_mock.set_leverage = MagicMock()
type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) type(api_mock).has = PropertyMock(return_value={'setLeverage': True})
default_conf['dry_run'] = False
ccxt_exceptionhandlers( ccxt_exceptionhandlers(
mocker, mocker,
@ -3063,6 +3091,7 @@ def test_set_margin_mode(mocker, default_conf, collateral):
api_mock = MagicMock() api_mock = MagicMock()
api_mock.set_margin_mode = MagicMock() api_mock.set_margin_mode = MagicMock()
type(api_mock).has = PropertyMock(return_value={'setMarginMode': True}) type(api_mock).has = PropertyMock(return_value={'setMarginMode': True})
default_conf['dry_run'] = False
ccxt_exceptionhandlers( ccxt_exceptionhandlers(
mocker, mocker,
@ -3117,7 +3146,8 @@ def test_validate_trading_mode_and_collateral(
collateral, collateral,
exception_thrown exception_thrown
): ):
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange = get_patched_exchange(
mocker, default_conf, id=exchange_name, mock_supported_modes=False)
if (exception_thrown): if (exception_thrown):
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
exchange.validate_trading_mode_and_collateral(trading_mode, collateral) exchange.validate_trading_mode_and_collateral(trading_mode, collateral)

View File

@ -37,8 +37,14 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati
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, side=side, order = exchange.stoploss(
order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio}) 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
@ -52,7 +58,14 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati
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={}, side=side) 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
@ -65,8 +78,13 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati
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'}, side=side) 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
@ -83,17 +101,32 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati
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={}, side=side) 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={}, side=side) 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={}, side=side) pair='ETH/BTC', amount=1, stop_price=220, order_types={},
side=side, leverage=1.0)
@pytest.mark.parametrize('side', [("sell"), ("buy")]) @pytest.mark.parametrize('side', [("sell"), ("buy")])
@ -107,7 +140,14 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker, side):
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={}, side=side) 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
@ -228,26 +268,3 @@ def test_fill_leverage_brackets_ftx(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id="ftx") exchange = get_patched_exchange(mocker, default_conf, id="ftx")
exchange.fill_leverage_brackets() exchange.fill_leverage_brackets()
assert exchange._leverage_brackets == {} assert exchange._leverage_brackets == {}
@pytest.mark.parametrize("trading_mode", [
(TradingMode.MARGIN),
(TradingMode.FUTURES)
])
def test__set_leverage(mocker, default_conf, trading_mode):
api_mock = MagicMock()
api_mock.set_leverage = MagicMock()
type(api_mock).has = PropertyMock(return_value={'setLeverage': True})
ccxt_exceptionhandlers(
mocker,
default_conf,
api_mock,
"ftx",
"_set_leverage",
"set_leverage",
pair="XRP/USDT",
leverage=5.0,
trading_mode=trading_mode
)

View File

@ -197,7 +197,9 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr
order_types={ order_types={
'stoploss': ordertype, 'stoploss': ordertype,
'stoploss_on_exchange_limit_ratio': 0.99 '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
@ -221,17 +223,32 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr
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={}, side=side) 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={}, side=side) 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={}, side=side) pair='ETH/BTC', amount=1, stop_price=220, order_types={},
side=side, leverage=1.0)
@pytest.mark.parametrize('side', ['buy', 'sell']) @pytest.mark.parametrize('side', ['buy', 'sell'])
@ -245,7 +262,14 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker, side):
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={}, side=side) 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
@ -307,15 +331,3 @@ def test_fill_leverage_brackets_kraken(default_conf, mocker):
'XLTCUSDT': [1], 'XLTCUSDT': [1],
'LTC/ETH': [1] 'LTC/ETH': [1]
} }
def test__set_leverage_kraken(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, id="kraken")
exchange._set_leverage(1)
assert 'leverage' not in exchange._params
exchange._set_leverage(3)
assert exchange._params['leverage'] == 3
exchange._set_leverage(1.0)
assert 'leverage' not in exchange._params
exchange._set_leverage(3.0)
assert exchange._params['leverage'] == 3

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

@ -1,271 +0,0 @@
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
from functools import reduce
from typing import Any, Callable, Dict, List
import talib.abstract as ta
from pandas import DataFrame
from skopt.space import Categorical, Dimension, Integer
import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt
class DefaultHyperOpt(IHyperOpt):
"""
Default hyperopt provided by the Freqtrade bot.
You can override it with your own Hyperopt
"""
@staticmethod
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Add several indicators needed for buy and sell strategies defined below.
"""
# ADX
dataframe['adx'] = ta.ADX(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Stochastic Fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
# Minus-DI
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_upperband'] = bollinger['upper']
# SAR
dataframe['sar'] = ta.SAR(dataframe)
return dataframe
@staticmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the buy strategy parameters to be used by Hyperopt.
"""
def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Buy strategy Hyperopt will build and use.
"""
long_conditions = []
short_conditions = []
# GUARDS AND TRENDS
if 'mfi-enabled' in params and params['mfi-enabled']:
long_conditions.append(dataframe['mfi'] < params['mfi-value'])
short_conditions.append(dataframe['mfi'] > params['short-mfi-value'])
if 'fastd-enabled' in params and params['fastd-enabled']:
long_conditions.append(dataframe['fastd'] < params['fastd-value'])
short_conditions.append(dataframe['fastd'] > params['short-fastd-value'])
if 'adx-enabled' in params and params['adx-enabled']:
long_conditions.append(dataframe['adx'] > params['adx-value'])
short_conditions.append(dataframe['adx'] < params['short-adx-value'])
if 'rsi-enabled' in params and params['rsi-enabled']:
long_conditions.append(dataframe['rsi'] < params['rsi-value'])
short_conditions.append(dataframe['rsi'] > params['short-rsi-value'])
# TRIGGERS
if 'trigger' in params:
if params['trigger'] == 'boll':
long_conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
short_conditions.append(dataframe['close'] > dataframe['bb_upperband'])
if params['trigger'] == 'macd_cross_signal':
long_conditions.append(qtpylib.crossed_above(
dataframe['macd'],
dataframe['macdsignal']
))
short_conditions.append(qtpylib.crossed_below(
dataframe['macd'],
dataframe['macdsignal']
))
if params['trigger'] == 'sar_reversal':
long_conditions.append(qtpylib.crossed_above(
dataframe['close'],
dataframe['sar']
))
short_conditions.append(qtpylib.crossed_below(
dataframe['close'],
dataframe['sar']
))
if long_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, long_conditions),
'buy'] = 1
if short_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, short_conditions),
'enter_short'] = 1
return dataframe
return populate_buy_trend
@staticmethod
def indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching buy strategy parameters.
"""
return [
Integer(10, 25, name='mfi-value'),
Integer(15, 45, name='fastd-value'),
Integer(20, 50, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Integer(75, 90, name='short-mfi-value'),
Integer(55, 85, name='short-fastd-value'),
Integer(50, 80, name='short-adx-value'),
Integer(60, 80, name='short-rsi-value'),
Categorical([True, False], name='mfi-enabled'),
Categorical([True, False], name='fastd-enabled'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['boll', 'macd_cross_signal', 'sar_reversal'], name='trigger')
]
@staticmethod
def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
"""
Define the sell strategy parameters to be used by Hyperopt.
"""
def populate_sell_trend(dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Sell strategy Hyperopt will build and use.
"""
exit_long_conditions = []
exit_short_conditions = []
# GUARDS AND TRENDS
if 'sell-mfi-enabled' in params and params['sell-mfi-enabled']:
exit_long_conditions.append(dataframe['mfi'] > params['sell-mfi-value'])
exit_short_conditions.append(dataframe['mfi'] < params['exit-short-mfi-value'])
if 'sell-fastd-enabled' in params and params['sell-fastd-enabled']:
exit_long_conditions.append(dataframe['fastd'] > params['sell-fastd-value'])
exit_short_conditions.append(dataframe['fastd'] < params['exit-short-fastd-value'])
if 'sell-adx-enabled' in params and params['sell-adx-enabled']:
exit_long_conditions.append(dataframe['adx'] < params['sell-adx-value'])
exit_short_conditions.append(dataframe['adx'] > params['exit-short-adx-value'])
if 'sell-rsi-enabled' in params and params['sell-rsi-enabled']:
exit_long_conditions.append(dataframe['rsi'] > params['sell-rsi-value'])
exit_short_conditions.append(dataframe['rsi'] < params['exit-short-rsi-value'])
# TRIGGERS
if 'sell-trigger' in params:
if params['sell-trigger'] == 'sell-boll':
exit_long_conditions.append(dataframe['close'] > dataframe['bb_upperband'])
exit_short_conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['sell-trigger'] == 'sell-macd_cross_signal':
exit_long_conditions.append(qtpylib.crossed_above(
dataframe['macdsignal'],
dataframe['macd']
))
exit_short_conditions.append(qtpylib.crossed_below(
dataframe['macdsignal'],
dataframe['macd']
))
if params['sell-trigger'] == 'sell-sar_reversal':
exit_long_conditions.append(qtpylib.crossed_above(
dataframe['sar'],
dataframe['close']
))
exit_short_conditions.append(qtpylib.crossed_below(
dataframe['sar'],
dataframe['close']
))
if exit_long_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, exit_long_conditions),
'sell'] = 1
if exit_short_conditions:
dataframe.loc[
reduce(lambda x, y: x & y, exit_short_conditions),
'exit-short'] = 1
return dataframe
return populate_sell_trend
@staticmethod
def sell_indicator_space() -> List[Dimension]:
"""
Define your Hyperopt space for searching sell strategy parameters.
"""
return [
Integer(75, 100, name='sell-mfi-value'),
Integer(50, 100, name='sell-fastd-value'),
Integer(50, 100, name='sell-adx-value'),
Integer(60, 100, name='sell-rsi-value'),
Integer(1, 25, name='exit-short-mfi-value'),
Integer(1, 50, name='exit-short-fastd-value'),
Integer(1, 50, name='exit-short-adx-value'),
Integer(1, 40, name='exit-short-rsi-value'),
Categorical([True, False], name='sell-mfi-enabled'),
Categorical([True, False], name='sell-fastd-enabled'),
Categorical([True, False], name='sell-adx-enabled'),
Categorical([True, False], name='sell-rsi-enabled'),
Categorical(['sell-boll',
'sell-macd_cross_signal',
'sell-sar_reversal'],
name='sell-trigger')
]
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of same method from strategy.
Must align to populate_indicators in this file.
Only used when --spaces does not include buy space.
"""
dataframe.loc[
(
(dataframe['close'] < dataframe['bb_lowerband']) &
(dataframe['mfi'] < 16) &
(dataframe['adx'] > 25) &
(dataframe['rsi'] < 21)
),
'buy'] = 1
dataframe.loc[
(
(dataframe['close'] > dataframe['bb_upperband']) &
(dataframe['mfi'] < 84) &
(dataframe['adx'] > 75) &
(dataframe['rsi'] < 79)
),
'enter_short'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators. Should be a copy of same method from strategy.
Must align to populate_indicators in this file.
Only used when --spaces does not include sell space.
"""
dataframe.loc[
(
(qtpylib.crossed_above(
dataframe['macdsignal'], dataframe['macd']
)) &
(dataframe['fastd'] > 54)
),
'sell'] = 1
dataframe.loc[
(
(qtpylib.crossed_below(
dataframe['macdsignal'], dataframe['macd']
)) &
(dataframe['fastd'] < 46)
),
'exit_short'] = 1
return dataframe

View File

@ -887,6 +887,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

@ -648,7 +648,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

@ -98,11 +98,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',
@ -112,45 +116,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',
@ -239,8 +212,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)
@ -254,9 +233,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,
) )
@ -273,46 +252,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('Exit for NEO/BTC detected. Reason: stop_loss', caplog) if not ignore_strat_sl:
assert trade.sell_reason == SellType.STOP_LOSS.value assert log_has('Exit for NEO/BTC detected. 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:
@ -406,37 +349,16 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order,
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, @pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [
limit_sell_order_open, fee, mocker, is_short) -> None: (0.0005, True, True, 99),
patch_RPCManager(mocker) (0.000000005, True, False, 99),
patch_exchange(mocker) (0, False, True, 99),
enter_mock = ( (UNLIMITED_STAKE_AMOUNT, False, True, 0),
MagicMock(return_value=limit_sell_order_open) ])
if is_short else def test_create_trade_minimal_amount(
MagicMock(return_value=limit_buy_order_open) default_conf, ticker, limit_buy_order_open, fee, mocker,
) stake_amount, create, amount_enough, max_open_trades, caplog, is_short
mocker.patch.multiple( ) -> None:
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
create_order=enter_mock,
get_fee=fee,
)
default_conf['stake_amount'] = 0.0005
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
freqtrade.create_trade('ETH/BTC')
rate, amount = enter_mock.call_args[1]['rate'], enter_mock.call_args[1]['amount']
assert rate * amount <= default_conf['stake_amount']
# TODO-lev: paramatrize and convert to USDT
# @pytest.mark.parametrize("stake_amount,leverage", [
# "buy, sell"
# ])
def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open,
fee, mocker, 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)
@ -446,58 +368,33 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord
create_order=buy_mock, create_order=buy_mock,
get_fee=fee, get_fee=fee,
) )
default_conf['max_open_trades'] = max_open_trades
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
freqtrade.config['stake_amount'] = 0.000000005 freqtrade.config['stake_amount'] = stake_amount
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
assert freqtrade.create_trade('ETH/BTC') if create:
assert log_has_re(r"Stake amount for pair .* is too small.*", caplog) assert freqtrade.create_trade('ETH/BTC')
if amount_enough:
rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount']
def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_open, assert rate * amount <= default_conf['stake_amount']
fee, mocker) -> None: else:
patch_RPCManager(mocker) assert log_has_re(
patch_exchange(mocker) r"Stake amount for pair .* is too small.*",
buy_mock = MagicMock(return_value=limit_buy_order_open) caplog
mocker.patch.multiple( )
'freqtrade.exchange.Exchange', else:
fetch_ticker=ticker, assert not freqtrade.create_trade('ETH/BTC')
create_order=buy_mock, if not max_open_trades:
get_fee=fee, assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0
)
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(
@ -506,36 +403,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")
@ -953,7 +834,7 @@ def test_execute_entry(mocker, default_conf, fee, limit_buy_order, limit_sell_or
# Fail to get price... # Fail to get price...
mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0)) mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0))
with pytest.raises(PricingError, match=f"Could not determine {'sell' if is_short else 'buy'} price."): with pytest.raises(PricingError, match=f"Could not determine {enter_side(is_short)} price."):
freqtrade.execute_entry(pair, stake_amount, is_short=is_short) freqtrade.execute_entry(pair, stake_amount, is_short=is_short)
# In case of custom entry price # In case of custom entry price
@ -1338,6 +1219,7 @@ def test_create_stoploss_order_insufficient_funds(
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, is_short, def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, is_short,
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
enter_order = limit_sell_order if is_short else limit_buy_order enter_order = limit_sell_order if is_short else limit_buy_order
exit_order = limit_buy_order if is_short else limit_sell_order exit_order = limit_buy_order if is_short else limit_sell_order
@ -1437,7 +1319,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, is_shor
pair='ETH/BTC', pair='ETH/BTC',
order_types=freqtrade.strategy.order_types, order_types=freqtrade.strategy.order_types,
stop_price=0.00002346 * 0.95, stop_price=0.00002346 * 0.95,
side="sell" side="sell",
leverage=1.0
) )
# price fell below stoploss, so dry-run sells trade. # price fell below stoploss, so dry-run sells trade.
@ -1540,6 +1423,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, is_s
exit_order = limit_buy_order if is_short else limit_sell_order exit_order = limit_buy_order if is_short else limit_sell_order
# 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(
@ -1635,7 +1519,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, is_s
pair='ETH/BTC', pair='ETH/BTC',
order_types=freqtrade.strategy.order_types, order_types=freqtrade.strategy.order_types,
stop_price=0.00002346 * 0.96, stop_price=0.00002346 * 0.96,
side="sell" side="sell",
leverage=1.0
) )
# price fell below stoploss, so dry-run sells trade. # price fell below stoploss, so dry-run sells trade.
@ -1764,34 +1649,32 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, is
pair='NEO/BTC', pair='NEO/BTC',
order_types=freqtrade.strategy.order_types, order_types=freqtrade.strategy.order_types,
stop_price=0.00002346 * 0.99, stop_price=0.00002346 * 0.99,
side="sell" 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)
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -1896,9 +1779,15 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order,
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@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,
limit_sell_order, is_short, mocker): mocker, initial_amount, has_rounding_fee,
limit_sell_order, is_short, caplog):
order = limit_sell_order if is_short else limit_buy_order order = limit_sell_order if is_short else limit_buy_order
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))
@ -1919,38 +1808,9 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_
) )
freqtrade.update_trade_state(trade, '123456', order) freqtrade.update_trade_state(trade, '123456', order)
assert trade.amount != amount assert trade.amount != amount
assert trade.amount == order['amount'] assert trade.amount == limit_buy_order['amount']
if has_rounding_fee:
assert log_has_re(r'Applying fee on amount for .*', caplog)
@pytest.mark.parametrize("is_short", [False, True])
def test_update_trade_state_withorderdict_rounding_fee(
default_conf, trades_for_order, fee, limit_buy_order, limit_sell_order,
mocker, caplog, is_short
):
order = limit_sell_order if is_short else limit_buy_order
trades_for_order[0]['amount'] = 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,
is_short=is_short
)
freqtrade.update_trade_state(trade, '123456', order)
assert trade.amount != amount
assert trade.amount == order['amount']
assert log_has_re(r'Applying fee on amount for .*', caplog)
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -3354,16 +3214,28 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee,
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
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', [
is_short, 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, is_short,
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,
@ -3373,131 +3245,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, enter_long=False, exit_long=True) patch_get_signal(freqtrade, enter_long=False, exit_long=True)
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
@pytest.mark.parametrize("is_short", [False, True])
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open,
is_short, 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, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.SELL_SIGNAL.value
@pytest.mark.parametrize("is_short", [False, True])
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open,
is_short, 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, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is False
@pytest.mark.parametrize("is_short", [False, True])
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open,
is_short, 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, enter_long=False, exit_long=True)
assert freqtrade.handle_trade(trade) is True
assert trade.sell_reason == SellType.SELL_SIGNAL.value
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -3536,11 +3306,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(
@ -3554,37 +3328,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)
@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("is_short", [False, True])
@ -3847,8 +3603,8 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_orde
def test_tsl_only_offset_reached(default_conf, limit_buy_order_usdt, limit_buy_order_usdt_open, def test_tsl_only_offset_reached(default_conf, limit_buy_order_usdt, limit_buy_order_usdt_open,
fee, is_short, limit_sell_order_usdt, fee, is_short, limit_sell_order_usdt,
limit_sell_order_usdt_open, caplog, mocker) -> None: limit_sell_order_usdt_open, caplog, mocker) -> None:
limit_order = limit_sell_usdt_order if is_short else limit_buy_order_usdt limit_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
limit_order_open = limit_sell_order_usdt_open if is_short else limit_buy_order_open_usdt limit_order_open = limit_sell_order_usdt_open if is_short else limit_buy_order_usdt_open
enter_price = limit_order['price'] enter_price = limit_order['price']
# enter_price: 2.0 # enter_price: 2.0
@ -3885,9 +3641,9 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order_usdt, limit_buy_o
# Raise ticker above buy price # Raise ticker above buy price
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
MagicMock(return_value={ MagicMock(return_value={
'bid': buy_price + 0.0000004, 'bid': enter_price + 0.0000004,
'ask': buy_price + 0.0000004, 'ask': enter_price + 0.0000004,
'last': buy_price + 0.0000004 'last': enter_price + 0.0000004
})) }))
# stop-loss should not be adjusted as offset is not reached yet # stop-loss should not be adjusted as offset is not reached yet
@ -3900,9 +3656,9 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order_usdt, limit_buy_o
# price rises above the offset (rises 12% when the offset is 5.5%) # price rises above the offset (rises 12% when the offset is 5.5%)
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
MagicMock(return_value={ MagicMock(return_value={
'bid': buy_price + 0.0000014, 'bid': enter_price + 0.0000014,
'ask': buy_price + 0.0000014, 'ask': enter_price + 0.0000014,
'last': buy_price + 0.0000014 'last': enter_price + 0.0000014
})) }))
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
@ -4384,50 +4140,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(default_conf, mocker, order_book_l2) -> None: def test_check_depth_of_market(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