diff --git a/config_full.json.example b/config_full.json.example
index 45c5c695c..5ee2a1faf 100644
--- a/config_full.json.example
+++ b/config_full.json.example
@@ -67,7 +67,13 @@
{"method": "AgeFilter", "min_days_listed": 10},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010},
- {"method": "SpreadFilter", "max_spread_ratio": 0.005}
+ {"method": "SpreadFilter", "max_spread_ratio": 0.005},
+ {
+ "method": "RangeStabilityFilter",
+ "lookback_days": 10,
+ "min_rate_of_change": 0.01,
+ "refresh_period": 1440
+ }
],
"exchange": {
"name": "bittrex",
diff --git a/docs/backtesting.md b/docs/backtesting.md
index 84911568b..277b11083 100644
--- a/docs/backtesting.md
+++ b/docs/backtesting.md
@@ -162,6 +162,8 @@ A backtesting result will look like that:
|-----------------------+---------------------|
| Backtesting from | 2019-01-01 00:00:00 |
| Backtesting to | 2019-05-01 00:00:00 |
+| Max open trades | 3 |
+| | |
| Total trades | 429 |
| First trade | 2019-01-01 18:30:00 |
| First trade Pair | EOS/USDT |
@@ -233,6 +235,8 @@ It contains some useful key metrics about performance of your strategy on backte
|-----------------------+---------------------|
| Backtesting from | 2019-01-01 00:00:00 |
| Backtesting to | 2019-05-01 00:00:00 |
+| Max open trades | 3 |
+| | |
| Total trades | 429 |
| First trade | 2019-01-01 18:30:00 |
| First trade Pair | EOS/USDT |
@@ -251,16 +255,17 @@ It contains some useful key metrics about performance of your strategy on backte
```
+- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
+- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - to clearly see settings for this.
- `Total trades`: Identical to the total trades of the backtest output table.
- `First trade`: First trade entered.
- `First trade pair`: Which pair was part of the first trade.
-- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
- `Total Profit %`: Total profit per stake amount. Aligned to the TOTAL column of the first table.
- `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
- `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
- `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced).
-- `Drawdown Start` / `Drawdown End`: Start and end datetimes for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
+- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
### Assumptions made by backtesting
diff --git a/docs/configuration.md b/docs/configuration.md
index 56ba13414..2e8f6555f 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -177,7 +177,7 @@ In the example above this would mean:
This option only applies with [Static stake amount](#static-stake-amount) - since [Dynamic stake amount](#dynamic-stake-amount) divides the balances evenly.
!!! Note
- The minimum last stake amount can be configured using `amend_last_stake_amount` - which defaults to 0.5 (50%). This means that the minimum stake amount that's ever used is `stake_amount * 0.5`. This avoids very low stake amounts, that are close to the minimum tradable amount for the pair and can be refused by the exchange.
+ The minimum last stake amount can be configured using `last_stake_amount_min_ratio` - which defaults to 0.5 (50%). This means that the minimum stake amount that's ever used is `stake_amount * 0.5`. This avoids very low stake amounts, that are close to the minimum tradable amount for the pair and can be refused by the exchange.
#### Static stake amount
diff --git a/docs/exchanges.md b/docs/exchanges.md
index fcf7c1cad..d877e6da2 100644
--- a/docs/exchanges.md
+++ b/docs/exchanges.md
@@ -23,7 +23,8 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f
## Kraken
!!! Tip "Stoploss on Exchange"
- Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it, however since the resulting order is a stoploss-market order, sell-rates are not guaranteed, which makes this feature less secure than on other exchanges. This limitation is based on kraken's policy [source](https://blog.kraken.com/post/1234/announcement-delisting-pairs-and-temporary-suspension-of-advanced-order-types/) and [source2](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) - which has stoploss-limit orders disabled.
+ Kraken supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
+ You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use.
### Historic Kraken data
@@ -75,8 +76,7 @@ print(res)
!!! Tip "Stoploss on Exchange"
FTX supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
- You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide.
-
+ You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
### Using subaccounts
@@ -99,10 +99,10 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
-
## Random notes for other exchanges
* The Ocean (exchange id: `theocean`) exchange uses Web3 functionality and requires `web3` python package to be installed:
+
```shell
$ pip3 install web3
```
diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md
index f8b33b27d..5bb1daa4a 100644
--- a/docs/includes/pairlists.md
+++ b/docs/includes/pairlists.md
@@ -20,6 +20,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac
* [`PriceFilter`](#pricefilter)
* [`ShuffleFilter`](#shufflefilter)
* [`SpreadFilter`](#spreadfilter)
+* [`RangeStabilityFilter`](#rangestabilityfilter)
!!! Tip "Testing pairlists"
Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly.
@@ -60,7 +61,7 @@ The `refresh_period` setting allows to define the period (in seconds), at which
"method": "VolumePairList",
"number_assets": 20,
"sort_key": "quoteVolume",
- "refresh_period": 1800,
+ "refresh_period": 1800
}],
```
@@ -123,6 +124,27 @@ Example:
If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out.
+#### RangeStabilityFilter
+
+Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`.
+
+In the below example:
+If the trading range over the last 10 days is <1%, remove the pair from the whitelist.
+
+```json
+"pairlists": [
+ {
+ "method": "RangeStabilityFilter",
+ "lookback_days": 10,
+ "min_rate_of_change": 0.01,
+ "refresh_period": 1440
+ }
+]
+```
+
+!!! Tip
+ This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit.
+
### Full example of Pairlist Handlers
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value.
@@ -142,6 +164,12 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets,
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.01},
{"method": "SpreadFilter", "max_spread_ratio": 0.005},
+ {
+ "method": "RangeStabilityFilter",
+ "lookback_days": 10,
+ "min_rate_of_change": 0.01,
+ "refresh_period": 1440
+ },
{"method": "ShuffleFilter", "seed": 42}
],
```
diff --git a/docs/stoploss.md b/docs/stoploss.md
index fa888cd47..1e21fc50d 100644
--- a/docs/stoploss.md
+++ b/docs/stoploss.md
@@ -23,11 +23,12 @@ These modes can be configured with these values:
```
!!! Note
- Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now.
- Do not set too low stoploss value if using stop loss on exchange!
- If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work
+ Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now.
+ Do not set too low/tight stoploss value if using stop loss on exchange!
+ If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
### stoploss_on_exchange and stoploss_on_exchange_limit_ratio
+
Enable or Disable stop loss on exchange.
If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled.
@@ -35,18 +36,23 @@ If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the st
`stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this.
If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type.
-Calculation example: we bought the asset at 100$.
-Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the limit order fill can happen between 95$ and 94.05$.
+Calculation example: we bought the asset at 100\$.
+Stop-price is 95\$, then limit would be `95 * 0.99 = 94.05$` - so the limit order fill can happen between 95$ and 94.05$.
For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order.
+!!! Note
+ If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order.
+
### stoploss_on_exchange_interval
+
In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary.
The bot cannot do these every 5 seconds (at each iteration), otherwise it would get banned by the exchange.
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
### emergencysell
+
`emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails.
The below is the default which is used if not changed in strategy or configuration file.
@@ -84,6 +90,7 @@ Example of stop loss:
```
For example, simplified math:
+
* the bot buys an asset at a price of 100$
* the stop loss is defined at -10%
* the stop loss would get triggered once the asset drops below 90$
@@ -107,7 +114,7 @@ For example, simplified math:
* the stop loss would get triggered once the asset drops below 90$
* assuming the asset now increases to 102$
* the stop loss will now be -10% of 102$ = 91.8$
-* now the asset drops in value to 101$, the stop loss will still be 91.8$ and would trigger at 91.8$.
+* now the asset drops in value to 101\$, the stop loss will still be 91.8$ and would trigger at 91.8$.
In summary: The stoploss will be adjusted to be always be -10% of the highest observed price.
@@ -133,8 +140,8 @@ For example, simplified math:
* the stop loss is defined at -10%
* the stop loss would get triggered once the asset drops below 90$
* assuming the asset now increases to 102$
-* the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increasements with -2%)
-* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$
+* the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increments with -2%)
+* now the asset drops in value to 101\$, the stop loss will still be 99.96$ and would trigger at 99.96$
The 0.02 would translate to a -2% stop loss.
Before this, `stoploss` is used for the trailing stoploss.
@@ -151,7 +158,7 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai
trailing_only_offset_is_reached = True
```
-Configuration (offset is buyprice + 3%):
+Configuration (offset is buy-price + 3%):
``` python
stoploss = -0.10
@@ -169,7 +176,7 @@ For example, simplified math:
* stoploss will remain at 90$ unless asset increases to or above our configured offset
* assuming the asset now increases to 103$ (where we have the offset configured)
* the stop loss will now be -2% of 103$ = 100.94$
-* now the asset drops in value to 101$, the stop loss will still be 100.94$ and would trigger at 100.94$
+* now the asset drops in value to 101\$, the stop loss will still be 100.94$ and would trigger at 100.94$
!!! Tip
Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade.
diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py
index d4612d8e0..ab21bc686 100644
--- a/freqtrade/configuration/config_validation.py
+++ b/freqtrade/configuration/config_validation.py
@@ -137,6 +137,10 @@ def _validate_edge(conf: Dict[str, Any]) -> None:
"Edge and VolumePairList are incompatible, "
"Edge will override whatever pairs VolumePairlist selects."
)
+ if not conf.get('ask_strategy', {}).get('use_sell_signal', True):
+ raise OperationalException(
+ "Edge requires `use_sell_signal` to be True, otherwise no sells will happen."
+ )
def _validate_whitelist(conf: Dict[str, Any]) -> None:
diff --git a/freqtrade/constants.py b/freqtrade/constants.py
index f47301fa6..20cc70d2e 100644
--- a/freqtrade/constants.py
+++ b/freqtrade/constants.py
@@ -25,7 +25,8 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
'AgeFilter', 'PrecisionFilter', 'PriceFilter',
- 'ShuffleFilter', 'SpreadFilter', 'PerformanceFilter']
+ 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter',
+ 'PerformanceFilter']
AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
DRY_RUN_WALLET = 1000
DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S'
diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py
index 17b510b92..3b8b5a2f0 100644
--- a/freqtrade/data/history/history_utils.py
+++ b/freqtrade/data/history/history_utils.py
@@ -214,10 +214,9 @@ def _download_pair_history(datadir: Path,
data_handler.ohlcv_store(pair, timeframe, data=data)
return True
- except Exception as e:
- logger.error(
- f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}. '
- f'Error: {e}'
+ except Exception:
+ logger.exception(
+ f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}.'
)
return False
@@ -304,10 +303,9 @@ def _download_trades_history(exchange: Exchange,
logger.info(f"New Amount of trades: {len(trades)}")
return True
- except Exception as e:
- logger.error(
+ except Exception:
+ logger.exception(
f'Failed to download historic trades for pair: "{pair}". '
- f'Error: {e}'
)
return False
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 2bbdb0d59..611ce4abd 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -524,7 +524,7 @@ class Exchange:
'rate': self.get_fee(pair)
}
})
- if closed_order["type"] in ["stop_loss_limit"]:
+ if closed_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
closed_order["info"].update({"stopPrice": closed_order["price"]})
self._dry_run_open_orders[closed_order["id"]] = closed_order
@@ -679,12 +679,25 @@ class Exchange:
:param pair: Pair to download
:param timeframe: Timeframe to get data for
:param since_ms: Timestamp in milliseconds to get history from
- :returns List with candle (OHLCV) data
+ :return: List with candle (OHLCV) data
"""
return asyncio.get_event_loop().run_until_complete(
self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe,
since_ms=since_ms))
+ def get_historic_ohlcv_as_df(self, pair: str, timeframe: str,
+ since_ms: int) -> DataFrame:
+ """
+ Minimal wrapper around get_historic_ohlcv - converting the result into a dataframe
+ :param pair: Pair to download
+ :param timeframe: Timeframe to get data for
+ :param since_ms: Timestamp in milliseconds to get history from
+ :return: OHLCV DataFrame
+ """
+ ticks = self.get_historic_ohlcv(pair, timeframe, since_ms=since_ms)
+ return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True,
+ drop_incomplete=self._ohlcv_partial_candle)
+
async def _async_get_historic_ohlcv(self, pair: str,
timeframe: str,
since_ms: int) -> List:
diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py
index 5b7aa5c5b..4e4713052 100644
--- a/freqtrade/exchange/kraken.py
+++ b/freqtrade/exchange/kraken.py
@@ -69,7 +69,8 @@ class Kraken(Exchange):
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
- return order['type'] == 'stop-loss' and stop_loss > float(order['price'])
+ return (order['type'] in ('stop-loss', 'stop-loss-limit')
+ and stop_loss > float(order['price']))
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
@@ -77,8 +78,15 @@ class Kraken(Exchange):
Creates a stoploss market order.
Stoploss market orders is the only stoploss type supported by kraken.
"""
+ params = self._params.copy()
- ordertype = "stop-loss"
+ if order_types.get('stoploss', 'market') == 'limit':
+ ordertype = "stop-loss-limit"
+ limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
+ limit_rate = stop_price * limit_price_pct
+ params['price2'] = self.price_to_precision(pair, limit_rate)
+ else:
+ ordertype = "stop-loss"
stop_price = self.price_to_precision(pair, stop_price)
@@ -88,8 +96,6 @@ class Kraken(Exchange):
return dry_order
try:
- params = self._params.copy()
-
amount = self.amount_to_precision(pair, amount)
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py
index 169cd2610..fbb05d879 100644
--- a/freqtrade/loggers.py
+++ b/freqtrade/loggers.py
@@ -37,6 +37,13 @@ def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
)
+def get_existing_handlers(handlertype):
+ """
+ Returns Existing handler or None (if the handler has not yet been added to the root handlers).
+ """
+ return next((h for h in logging.root.handlers if isinstance(h, handlertype)), None)
+
+
def setup_logging_pre() -> None:
"""
Early setup for logging.
@@ -71,18 +78,24 @@ def setup_logging(config: Dict[str, Any]) -> None:
# config['logfilename']), which defaults to '/dev/log', applicable for most
# of the systems.
address = (s[1], int(s[2])) if len(s) > 2 else s[1] if len(s) > 1 else '/dev/log'
- handler = SysLogHandler(address=address)
+ handler_sl = get_existing_handlers(SysLogHandler)
+ if handler_sl:
+ logging.root.removeHandler(handler_sl)
+ handler_sl = SysLogHandler(address=address)
# No datetime field for logging into syslog, to allow syslog
# to perform reduction of repeating messages if this is set in the
# syslog config. The messages should be equal for this.
- handler.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
- logging.root.addHandler(handler)
+ handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
+ logging.root.addHandler(handler_sl)
elif s[0] == 'journald':
try:
from systemd.journal import JournaldLogHandler
except ImportError:
raise OperationalException("You need the systemd python package be installed in "
"order to use logging to journald.")
+ handler_jd = get_existing_handlers(JournaldLogHandler)
+ if handler_jd:
+ logging.root.removeHandler(handler_jd)
handler_jd = JournaldLogHandler()
# No datetime field for logging into journald, to allow syslog
# to perform reduction of repeating messages if this is set in the
@@ -90,6 +103,9 @@ def setup_logging(config: Dict[str, Any]) -> None:
handler_jd.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
logging.root.addHandler(handler_jd)
else:
+ handler_rf = get_existing_handlers(RotatingFileHandler)
+ if handler_rf:
+ logging.root.removeHandler(handler_rf)
handler_rf = RotatingFileHandler(logfile,
maxBytes=1024 * 1024 * 10, # 10Mb
backupCount=10)
diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py
index c977a991b..fc04cbd93 100644
--- a/freqtrade/optimize/optimize_reports.py
+++ b/freqtrade/optimize/optimize_reports.py
@@ -396,6 +396,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
metrics = [
('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)),
('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)),
+ ('Max open trades', strat_results['max_open_trades']),
+ ('', ''), # Empty line to improve readability
('Total trades', strat_results['total_trades']),
('First trade', min_trade['open_date'].strftime(DATETIME_PRINT_FORMAT)),
('First trade Pair', min_trade['pair']),
diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py
index 19cf1c090..e2a13c20a 100644
--- a/freqtrade/pairlist/AgeFilter.py
+++ b/freqtrade/pairlist/AgeFilter.py
@@ -37,7 +37,7 @@ class AgeFilter(IPairList):
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
- If no Pairlist requires tickers, an empty List is passed
+ If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return True
@@ -49,7 +49,7 @@ class AgeFilter(IPairList):
return (f"{self.name} - Filtering pairs with age less than "
f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.")
- def _validate_pair(self, ticker: dict) -> bool:
+ def _validate_pair(self, ticker: Dict) -> bool:
"""
Validate age for the ticker
:param ticker: ticker dict as returned from ccxt.load_markets()
diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py
index 6b5bd11e7..c869e499b 100644
--- a/freqtrade/pairlist/IPairList.py
+++ b/freqtrade/pairlist/IPairList.py
@@ -68,7 +68,7 @@ class IPairList(ABC):
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
- If no Pairlist requires tickers, an empty List is passed
+ If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py
index cf853397b..29e32fd44 100644
--- a/freqtrade/pairlist/PrecisionFilter.py
+++ b/freqtrade/pairlist/PrecisionFilter.py
@@ -32,7 +32,7 @@ class PrecisionFilter(IPairList):
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
- If no Pairlist requires tickers, an empty List is passed
+ If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return True
diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py
index 8cd57ee1d..bef1c0a15 100644
--- a/freqtrade/pairlist/PriceFilter.py
+++ b/freqtrade/pairlist/PriceFilter.py
@@ -35,7 +35,7 @@ class PriceFilter(IPairList):
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
- If no Pairlist requires tickers, an empty List is passed
+ If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return True
diff --git a/freqtrade/pairlist/ShuffleFilter.py b/freqtrade/pairlist/ShuffleFilter.py
index eb4f6dcc3..28778db7b 100644
--- a/freqtrade/pairlist/ShuffleFilter.py
+++ b/freqtrade/pairlist/ShuffleFilter.py
@@ -25,7 +25,7 @@ class ShuffleFilter(IPairList):
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
- If no Pairlist requires tickers, an empty List is passed
+ If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py
index 2527a3131..a636b90bd 100644
--- a/freqtrade/pairlist/SpreadFilter.py
+++ b/freqtrade/pairlist/SpreadFilter.py
@@ -24,7 +24,7 @@ class SpreadFilter(IPairList):
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
- If no Pairlist requires tickers, an empty List is passed
+ If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return True
diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py
index 3b6440763..2879cb364 100644
--- a/freqtrade/pairlist/StaticPairList.py
+++ b/freqtrade/pairlist/StaticPairList.py
@@ -30,7 +30,7 @@ class StaticPairList(IPairList):
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
- If no Pairlist requires tickers, an empty List is passed
+ If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return False
diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py
index 44e5c52d7..7d3c2c653 100644
--- a/freqtrade/pairlist/VolumePairList.py
+++ b/freqtrade/pairlist/VolumePairList.py
@@ -49,7 +49,7 @@ class VolumePairList(IPairList):
def needstickers(self) -> bool:
"""
Boolean property defining if tickers are necessary.
- If no Pairlist requires tickers, an empty List is passed
+ If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return True
diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py
new file mode 100644
index 000000000..b460ff477
--- /dev/null
+++ b/freqtrade/pairlist/rangestabilityfilter.py
@@ -0,0 +1,89 @@
+"""
+Rate of change pairlist filter
+"""
+import logging
+from typing import Any, Dict
+
+import arrow
+from cachetools.ttl import TTLCache
+
+from freqtrade.exceptions import OperationalException
+from freqtrade.misc import plural
+from freqtrade.pairlist.IPairList import IPairList
+
+
+logger = logging.getLogger(__name__)
+
+
+class RangeStabilityFilter(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._days = pairlistconfig.get('lookback_days', 10)
+ self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
+ self._refresh_period = pairlistconfig.get('refresh_period', 1440)
+
+ self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period)
+
+ if self._days < 1:
+ raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1")
+ if self._days > exchange.ohlcv_candle_limit:
+ raise OperationalException("RangeStabilityFilter requires lookback_days to not "
+ "exceed exchange max request size "
+ f"({exchange.ohlcv_candle_limit})")
+
+ @property
+ def needstickers(self) -> bool:
+ """
+ Boolean property defining if tickers are necessary.
+ If no Pairlist requires tickers, an empty List is passed
+ as tickers argument to filter_pairlist
+ """
+ return True
+
+ def short_desc(self) -> str:
+ """
+ Short whitelist method description - used for startup-messages
+ """
+ return (f"{self.name} - Filtering pairs with rate of change below "
+ f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.")
+
+ def _validate_pair(self, ticker: Dict) -> bool:
+ """
+ Validate trading range
+ :param ticker: ticker dict as returned from ccxt.load_markets()
+ :return: True if the pair can stay, False if it should be removed
+ """
+ pair = ticker['symbol']
+ # Check symbol in cache
+ if pair in self._pair_cache:
+ return self._pair_cache[pair]
+
+ since_ms = int(arrow.utcnow()
+ .floor('day')
+ .shift(days=-self._days)
+ .float_timestamp) * 1000
+
+ daily_candles = self._exchange.get_historic_ohlcv_as_df(pair=pair,
+ timeframe='1d',
+ since_ms=since_ms)
+ result = False
+ if daily_candles is not None and not daily_candles.empty:
+ highest_high = daily_candles['high'].max()
+ lowest_low = daily_candles['low'].min()
+ pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0
+ if pct_change >= self._min_rate_of_change:
+ result = True
+ else:
+ self.log_on_refresh(logger.info,
+ f"Removed {pair} from whitelist, "
+ f"because rate of change over {plural(self._days, 'day')} is "
+ f"{pct_change:.3f}, which is below the "
+ f"threshold of {self._min_rate_of_change}.")
+ result = False
+ self._pair_cache[pair] = result
+
+ return result
diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
index 8160ffbbf..6027908da 100644
--- a/freqtrade/persistence/models.py
+++ b/freqtrade/persistence/models.py
@@ -397,7 +397,7 @@ class Trade(_DECL_BASE):
if self.is_open:
logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.')
self.close(safe_value_fallback(order, 'average', 'price'))
- elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'):
+ elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'):
self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss
if self.is_open:
diff --git a/mkdocs.yml b/mkdocs.yml
index 8d1ce1cfe..2cc0c9fcb 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -20,13 +20,13 @@ nav:
- Hyperopt: hyperopt.md
- Edge Positioning: edge.md
- Utility Subcommands: utils.md
- - Exchange-specific Notes: exchanges.md
- FAQ: faq.md
- Data Analysis:
- Jupyter Notebooks: data-analysis.md
- Strategy analysis: strategy_analysis_example.md
- Plotting: plotting.md
- SQL Cheatsheet: sql_cheatsheet.md
+ - Exchange-specific Notes: exchanges.md
- Advanced Post-installation Tasks: advanced-setup.md
- Advanced Strategy: strategy-advanced.md
- Advanced Hyperopt: advanced-hyperopt.md
diff --git a/tests/data/test_history.py b/tests/data/test_history.py
index 905798041..99b22adda 100644
--- a/tests/data/test_history.py
+++ b/tests/data/test_history.py
@@ -312,10 +312,7 @@ def test_download_backtesting_data_exception(ohlcv_history, mocker, caplog,
# clean files freshly downloaded
_clean_test_file(file1_1)
_clean_test_file(file1_5)
- assert log_has(
- 'Failed to download history data for pair: "MEME/BTC", timeframe: 1m. '
- 'Error: File Error', caplog
- )
+ assert log_has('Failed to download history data for pair: "MEME/BTC", timeframe: 1m.', caplog)
def test_load_partial_missing(testdatadir, caplog) -> None:
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index e4452a83c..42681b367 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -1307,6 +1307,57 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name):
assert log_has_re(r"Async code raised an exception: .*", caplog)
+@pytest.mark.parametrize("exchange_name", EXCHANGES)
+def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name):
+ exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
+ ohlcv = [
+ [
+ arrow.utcnow().int_timestamp * 1000, # unix timestamp ms
+ 1, # open
+ 2, # high
+ 3, # low
+ 4, # close
+ 5, # volume (in quote currency)
+ ],
+ [
+ arrow.utcnow().shift(minutes=5).int_timestamp * 1000, # unix timestamp ms
+ 1, # open
+ 2, # high
+ 3, # low
+ 4, # close
+ 5, # volume (in quote currency)
+ ],
+ [
+ arrow.utcnow().shift(minutes=10).int_timestamp * 1000, # unix timestamp ms
+ 1, # open
+ 2, # high
+ 3, # low
+ 4, # close
+ 5, # volume (in quote currency)
+ ]
+ ]
+ pair = 'ETH/BTC'
+
+ async def mock_candle_hist(pair, timeframe, since_ms):
+ return pair, timeframe, ohlcv
+
+ exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
+ # one_call calculation * 1.8 should do 2 calls
+
+ since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8
+ ret = exchange.get_historic_ohlcv_as_df(pair, "5m", int((
+ arrow.utcnow().int_timestamp - since) * 1000))
+
+ assert exchange._async_get_candle_history.call_count == 2
+ # Returns twice the above OHLCV data
+ assert len(ret) == 2
+ assert isinstance(ret, DataFrame)
+ assert 'date' in ret.columns
+ assert 'open' in ret.columns
+ assert 'close' in ret.columns
+ assert 'high' in ret.columns
+
+
def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
ohlcv = [
[
diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py
index 31b79a202..3803658eb 100644
--- a/tests/exchange/test_kraken.py
+++ b/tests/exchange/test_kraken.py
@@ -10,6 +10,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
STOPLOSS_ORDERTYPE = 'stop-loss'
+STOPLOSS_LIMIT_ORDERTYPE = 'stop-loss-limit'
def test_buy_kraken_trading_agreement(default_conf, mocker):
@@ -156,7 +157,8 @@ def test_get_balances_prod(default_conf, mocker):
"get_balances", "fetch_balance")
-def test_stoploss_order_kraken(default_conf, mocker):
+@pytest.mark.parametrize('ordertype', ['market', 'limit'])
+def test_stoploss_order_kraken(default_conf, mocker, ordertype):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
@@ -173,24 +175,26 @@ def test_stoploss_order_kraken(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
- # stoploss_on_exchange_limit_ratio is irrelevant for kraken market orders
- order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
- order_types={'stoploss_on_exchange_limit_ratio': 1.05})
- assert api_mock.create_order.call_count == 1
-
- api_mock.create_order.reset_mock()
-
- order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
+ order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
+ order_types={'stoploss': ordertype,
+ 'stoploss_on_exchange_limit_ratio': 0.99
+ })
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
- assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
+ if ordertype == 'limit':
+ assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_LIMIT_ORDERTYPE
+ assert api_mock.create_order.call_args_list[0][1]['params'] == {
+ 'trading_agreement': 'agree', 'price2': 217.8}
+ else:
+ assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
+ assert api_mock.create_order.call_args_list[0][1]['params'] == {
+ 'trading_agreement': 'agree'}
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
- assert api_mock.create_order.call_args_list[0][1]['params'] == {'trading_agreement': 'agree'}
# test exception handling
with pytest.raises(DependencyException):
diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py
index a99651727..65a0fa835 100644
--- a/tests/pairlist/test_pairlist.py
+++ b/tests/pairlist/test_pairlist.py
@@ -58,7 +58,7 @@ def whitelist_conf_2(default_conf):
@pytest.fixture(scope="function")
-def whitelist_conf_3(default_conf):
+def whitelist_conf_agefilter(default_conf):
default_conf['stake_currency'] = 'BTC'
default_conf['exchange']['pair_whitelist'] = [
'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC',
@@ -351,6 +351,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "low_price_ratio": 0.02}],
"USDT", ['ETH/USDT', 'NANO/USDT']),
+ ([{"method": "StaticPairList"},
+ {"method": "RangeStabilityFilter", "lookback_days": 10,
+ "min_rate_of_change": 0.01, "refresh_period": 1440}],
+ "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']),
])
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
ohlcv_history_list, pairlists, base_currency,
@@ -575,7 +579,7 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick
get_patched_freqtradebot(mocker, default_conf)
-def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list):
+def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history_list):
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
@@ -587,7 +591,7 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his
get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list),
)
- freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_3)
+ freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter)
assert freqtrade.exchange.get_historic_ohlcv.call_count == 0
freqtrade.pairlists.refresh_pairlist()
assert freqtrade.exchange.get_historic_ohlcv.call_count > 0
@@ -598,6 +602,62 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his
assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count
+def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers):
+ default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
+ {'method': 'RangeStabilityFilter', 'lookback_days': 99999}]
+
+ mocker.patch.multiple('freqtrade.exchange.Exchange',
+ markets=PropertyMock(return_value=markets),
+ exchange_has=MagicMock(return_value=True),
+ get_tickers=tickers
+ )
+
+ with pytest.raises(OperationalException,
+ match=r'RangeStabilityFilter requires lookback_days to not exceed '
+ r'exchange max request size \([0-9]+\)'):
+ get_patched_freqtradebot(mocker, default_conf)
+
+ default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
+ {'method': 'RangeStabilityFilter', 'lookback_days': 0}]
+
+ with pytest.raises(OperationalException,
+ match='RangeStabilityFilter requires lookback_days to be >= 1'):
+ get_patched_freqtradebot(mocker, default_conf)
+
+
+@pytest.mark.parametrize('min_rate_of_change,expected_length', [
+ (0.01, 5),
+ (0.05, 0), # Setting rate_of_change to 5% removes all pairs from the whitelist.
+])
+def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history_list,
+ min_rate_of_change, expected_length):
+ default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
+ {'method': 'RangeStabilityFilter', 'lookback_days': 2,
+ 'min_rate_of_change': min_rate_of_change}]
+
+ mocker.patch.multiple('freqtrade.exchange.Exchange',
+ markets=PropertyMock(return_value=markets),
+ exchange_has=MagicMock(return_value=True),
+ get_tickers=tickers
+ )
+ mocker.patch.multiple(
+ 'freqtrade.exchange.Exchange',
+ get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list),
+ )
+
+ freqtrade = get_patched_freqtradebot(mocker, default_conf)
+ assert freqtrade.exchange.get_historic_ohlcv.call_count == 0
+ freqtrade.pairlists.refresh_pairlist()
+ assert len(freqtrade.pairlists.whitelist) == expected_length
+ assert freqtrade.exchange.get_historic_ohlcv.call_count > 0
+
+ previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count
+ freqtrade.pairlists.refresh_pairlist()
+ assert len(freqtrade.pairlists.whitelist) == expected_length
+ # Should not have increased since first call.
+ assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count
+
+
@pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [
({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010,
"max_price": 1.0},
@@ -633,6 +693,11 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his
None,
"PriceFilter requires max_price to be >= 0"
), # OperationalException expected
+ ({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01},
+ "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below "
+ "0.01 over the last days.'}]",
+ None
+ ),
])
def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig,
desc_expected, exception_expected):
diff --git a/tests/test_configuration.py b/tests/test_configuration.py
index 7d6c81f74..e6c91a96e 100644
--- a/tests/test_configuration.py
+++ b/tests/test_configuration.py
@@ -663,7 +663,7 @@ def test_set_loggers() -> None:
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
-def test_set_loggers_syslog(mocker):
+def test_set_loggers_syslog():
logger = logging.getLogger()
orig_handlers = logger.handlers
logger.handlers = []
@@ -678,10 +678,38 @@ def test_set_loggers_syslog(mocker):
assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler]
assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
assert [x for x in logger.handlers if type(x) == logging.handlers.BufferingHandler]
+ # setting up logging again should NOT cause the loggers to be added a second time.
+ setup_logging(config)
+ assert len(logger.handlers) == 3
# reset handlers to not break pytest
logger.handlers = orig_handlers
+@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
+def test_set_loggers_Filehandler(tmpdir):
+ logger = logging.getLogger()
+ orig_handlers = logger.handlers
+ logger.handlers = []
+ logfile = Path(tmpdir) / 'ft_logfile.log'
+ config = {'verbosity': 2,
+ 'logfile': str(logfile),
+ }
+
+ setup_logging_pre()
+ setup_logging(config)
+ assert len(logger.handlers) == 3
+ assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler]
+ assert [x for x in logger.handlers if type(x) == logging.StreamHandler]
+ assert [x for x in logger.handlers if type(x) == logging.handlers.BufferingHandler]
+ # setting up logging again should NOT cause the loggers to be added a second time.
+ setup_logging(config)
+ assert len(logger.handlers) == 3
+ # reset handlers to not break pytest
+ if logfile.exists:
+ logfile.unlink()
+ logger.handlers = orig_handlers
+
+
@pytest.mark.skip(reason="systemd is not installed on every system, so we're not testing this.")
def test_set_loggers_journald(mocker):
logger = logging.getLogger()
@@ -812,6 +840,21 @@ def test_validate_edge(edge_conf):
validate_config_consistency(edge_conf)
+def test_validate_edge2(edge_conf):
+ edge_conf.update({"ask_strategy": {
+ "use_sell_signal": True,
+ }})
+ # Passes test
+ validate_config_consistency(edge_conf)
+
+ edge_conf.update({"ask_strategy": {
+ "use_sell_signal": False,
+ }})
+ with pytest.raises(OperationalException, match="Edge requires `use_sell_signal` to be True, "
+ "otherwise no sells will happen."):
+ validate_config_consistency(edge_conf)
+
+
def test_validate_whitelist(default_conf):
default_conf['runmode'] = RunMode.DRY_RUN
# Test regular case - has whitelist and uses StaticPairlist