From a239e5f72593dd99a2cc218f8a28b424a4705e39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Nov 2021 12:57:51 +0100 Subject: [PATCH 01/70] Add segment on colliding signals --- docs/strategy-customization.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index d54bae710..aeb837e16 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -1016,6 +1016,10 @@ The following lists some common patterns which should be avoided to prevent frus - don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling().mean()` instead - don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead. +### Colliding signals + +When buy and sell signals collide (both `'buy'` and `'sell'` are 1), freqtrade will do nothing. This is to avoid trades that buy, and sell immediately. Obviously, this can lead to both missed entries, as well as missed exits. + ## Further strategy ideas To get additional Ideas for strategies, head over to the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk. From f8f7d81fc2a029eb04e228b0eea295e9fc0090a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Nov 2021 08:51:15 +0100 Subject: [PATCH 02/70] Update strategy template to use parameters --- freqtrade/templates/base_strategy.py.j2 | 11 ++++++++--- freqtrade/templates/subtemplates/buy_trend_full.j2 | 2 +- freqtrade/templates/subtemplates/buy_trend_minimal.j2 | 2 +- freqtrade/templates/subtemplates/sell_trend_full.j2 | 2 +- .../templates/subtemplates/sell_trend_minimal.j2 | 2 +- 5 files changed, 12 insertions(+), 7 deletions(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 06d7cbc5c..7f5399672 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -12,6 +12,7 @@ from freqtrade.strategy import (BooleanParameter, CategoricalParameter, DecimalP # -------------------------------- # Add your lib to import here import talib.abstract as ta +import pandas_ta as pta import freqtrade.vendor.qtpylib.indicators as qtpylib @@ -36,6 +37,9 @@ class {{ strategy }}(IStrategy): # Check the documentation or the Sample strategy to get the latest version. INTERFACE_VERSION = 2 + # Optimal timeframe for the strategy. + timeframe = '5m' + # Minimal ROI designed for the strategy. # This attribute will be overridden if the config file contains "minimal_roi". minimal_roi = { @@ -54,9 +58,6 @@ class {{ strategy }}(IStrategy): # trailing_stop_positive = 0.01 # trailing_stop_positive_offset = 0.0 # Disabled / not configured - # Optimal timeframe for the strategy. - timeframe = '5m' - # Run "populate_indicators()" only for new candle. process_only_new_candles = False @@ -68,6 +69,10 @@ class {{ strategy }}(IStrategy): # Number of candles the strategy requires before producing valid signals startup_candle_count: int = 30 + # Strategy parameters + buy_rsi = IntParameter(10, 40, default=30, space="buy") + sell_rsi = IntParameter(60, 90, default=70, space="sell") + # Optional order type mapping. order_types = { 'buy': 'limit', diff --git a/freqtrade/templates/subtemplates/buy_trend_full.j2 b/freqtrade/templates/subtemplates/buy_trend_full.j2 index 1a0d326b3..aac8325a7 100644 --- a/freqtrade/templates/subtemplates/buy_trend_full.j2 +++ b/freqtrade/templates/subtemplates/buy_trend_full.j2 @@ -1,3 +1,3 @@ -(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 +(qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)) & # Signal: RSI crosses above buy_rsi (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard: tema below BB middle (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising diff --git a/freqtrade/templates/subtemplates/buy_trend_minimal.j2 b/freqtrade/templates/subtemplates/buy_trend_minimal.j2 index 6a4079cf3..e89d3779e 100644 --- a/freqtrade/templates/subtemplates/buy_trend_minimal.j2 +++ b/freqtrade/templates/subtemplates/buy_trend_minimal.j2 @@ -1 +1 @@ -(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 +(qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)) & # Signal: RSI crosses above buy_rsi diff --git a/freqtrade/templates/subtemplates/sell_trend_full.j2 b/freqtrade/templates/subtemplates/sell_trend_full.j2 index 36c08c947..3068d8d57 100644 --- a/freqtrade/templates/subtemplates/sell_trend_full.j2 +++ b/freqtrade/templates/subtemplates/sell_trend_full.j2 @@ -1,3 +1,3 @@ -(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 +(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) & # Signal: RSI crosses above sell_rsi (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling diff --git a/freqtrade/templates/subtemplates/sell_trend_minimal.j2 b/freqtrade/templates/subtemplates/sell_trend_minimal.j2 index 42a7b81a2..5dabc5910 100644 --- a/freqtrade/templates/subtemplates/sell_trend_minimal.j2 +++ b/freqtrade/templates/subtemplates/sell_trend_minimal.j2 @@ -1 +1 @@ -(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70 +(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) & # Signal: RSI crosses above sell_rsi From d8ee72554fb7ab1a5a9d5322fb670df1ff5ea7d5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Nov 2021 20:17:18 +0100 Subject: [PATCH 03/70] Improve callback documentation --- docs/strategy-advanced.md | 217 +++++++++++++++++++++----------------- 1 file changed, 119 insertions(+), 98 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 47d7ee6ae..1b301ee06 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -77,43 +77,6 @@ class AwesomeStrategy(IStrategy): *** -## Custom sell signal - -It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. - -For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. - -Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. - -!!! Note - Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. - -An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: - -``` python -class AwesomeStrategy(IStrategy): - def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, **kwargs): - dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) - last_candle = dataframe.iloc[-1].squeeze() - - # Above 20% profit, sell when rsi < 80 - if current_profit > 0.2: - if last_candle['rsi'] < 80: - return 'rsi_below_80' - - # Between 2% and 10%, sell if EMA-long above EMA-short - if 0.02 < current_profit < 0.1: - if last_candle['emalong'] > last_candle['emashort']: - return 'ema_long_below_80' - - # Sell any positions at a loss if they are held for more than one day. - if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1: - return 'unclog' -``` - -See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. - ## Buy Tag When your strategy has multiple buy signals, you can name the signal that triggered. @@ -164,7 +127,26 @@ The provided exit-tag is then used as sell-reason - and shown as such in backtes !!! Note `sell_reason` is limited to 100 characters, remaining data will be truncated. -## Bot loop start callback +## Callbacks + +While the main strategy functions (`populate_indicators()`, `populate_buy_trend()`, `populate_sell_trend()`) should be used in a vectorized way, and are only called [once during backtesting](bot-basics.md#backtesting-hyperopt-execution-logic), callbacks are called "whenever needed". + +As such, you should avoid doing heavy calculations in callbacks to avoid delays during operations. +Depending on the callback used, they may be called when entering / exiting a trade, or throughout the duration of a trade. + +Currently available callbacks: + +* [`bot_loop_start()`](#bot-loop-start) +* [`custom_stake_amount()`](#custom-stake-size) +* [`custom_sell()`](#custom-sell-signal) +* [`custom_stoploss()`](#custom-stoploss) +* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules) +* [`check_buy_timeout()` and `check_sell_timeout()](#custom-order-timeout-rules) +* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation) +* [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation) + + +### Bot loop start A simple callback which is called once at the start of every bot throttling iteration. This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc. @@ -190,7 +172,77 @@ class AwesomeStrategy(IStrategy): ``` -## Custom stoploss +### Custom Stake size + +It is possible to manage your risk by reducing or increasing stake amount when placing a new trade. + +```python +class AwesomeStrategy(IStrategy): + def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, + proposed_stake: float, min_stake: float, max_stake: float, + **kwargs) -> float: + + dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) + current_candle = dataframe.iloc[-1].squeeze() + + if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']: + if self.config['stake_amount'] == 'unlimited': + # Use entire available wallet during favorable conditions when in compounding mode. + return max_stake + else: + # Compound profits during favorable conditions instead of using a static stake. + return self.wallets.get_total_stake_amount() / self.config['max_open_trades'] + + # Use default stake amount. + return proposed_stake +``` + +Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged. + +!!! Tip + You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged. + +!!! Tip + Returning `0` or `None` will prevent trades from being placed. + +### Custom sell signal + +It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. + +For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. + +Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. + +!!! Note + Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. + +An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: + +``` python +class AwesomeStrategy(IStrategy): + def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, **kwargs): + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + + # Above 20% profit, sell when rsi < 80 + if current_profit > 0.2: + if last_candle['rsi'] < 80: + return 'rsi_below_80' + + # Between 2% and 10%, sell if EMA-long above EMA-short + if 0.02 < current_profit < 0.1: + if last_candle['emalong'] > last_candle['emashort']: + return 'ema_long_below_80' + + # Sell any positions at a loss if they are held for more than one day. + if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1: + return 'unclog' +``` + +See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. + +### Custom stoploss The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. @@ -244,12 +296,12 @@ Stoploss on exchange works similar to `trailing_stop`, and the stoploss on excha !!! Tip "Trailing stoploss" It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior. -### Custom stoploss examples +#### Custom stoploss examples The next section will show some examples on what's possible with the custom stoploss function. Of course, many more things are possible, and all examples can be combined at will. -#### Time based trailing stop +##### Time based trailing stop Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. @@ -274,7 +326,7 @@ class AwesomeStrategy(IStrategy): return 1 ``` -#### Different stoploss per pair +##### Different stoploss per pair Use a different stoploss depending on the pair. In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs. @@ -299,7 +351,7 @@ class AwesomeStrategy(IStrategy): return -0.15 ``` -#### Trailing stoploss with positive offset +##### Trailing stoploss with positive offset Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. @@ -328,19 +380,7 @@ class AwesomeStrategy(IStrategy): return max(min(desired_stoploss, 0.05), 0.025) ``` -#### Calculating stoploss relative to open price - -Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` 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_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. @@ -375,7 +415,7 @@ class AwesomeStrategy(IStrategy): return 1 ``` -#### Custom stoploss using an indicator from dataframe example +##### Custom stoploss using an indicator from dataframe example Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss. @@ -407,9 +447,23 @@ class AwesomeStrategy(IStrategy): See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. +#### Common helpers for stoploss calculations + +##### Stoploss relative to open price + +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` 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_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()`. + +##### 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()`. + --- -## Custom order price rules +### Custom order price rules By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy. @@ -418,7 +472,7 @@ You can use this feature by creating a `custom_entry_price()` function in your s !!! Note If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration. -### Custom order entry and exit price example +#### Custom order entry and exit price example ``` python from datetime import datetime, timedelta, timezone @@ -458,7 +512,7 @@ class AwesomeStrategy(IStrategy): !!! Warning "No backtesting support" Custom entry-prices are currently not supported during backtesting. -## Custom order timeout rules +### Custom order timeout rules Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. @@ -467,7 +521,7 @@ However, freqtrade also offers a custom callback for both order types, which all !!! Note Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances. -### Custom order timeout example +#### Custom order timeout example A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below. It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins. @@ -511,7 +565,7 @@ class AwesomeStrategy(IStrategy): !!! Note For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first. -### Custom order timeout example (using additional data) +#### Custom order timeout example (using additional data) ``` python from datetime import datetime @@ -547,9 +601,9 @@ class AwesomeStrategy(IStrategy): --- -## Bot order confirmation +### Bot order confirmation -### Trade entry (buy order) confirmation +#### Trade entry (buy order) confirmation `confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect). @@ -583,7 +637,7 @@ class AwesomeStrategy(IStrategy): ``` -### Trade exit (sell order) confirmation +#### Trade exit (sell order) confirmation `confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect). @@ -629,39 +683,6 @@ class AwesomeStrategy(IStrategy): ``` -### Stake size management - -It is possible to manage your risk by reducing or increasing stake amount when placing a new trade. - -```python -class AwesomeStrategy(IStrategy): - def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, - proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: - - dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) - current_candle = dataframe.iloc[-1].squeeze() - - if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']: - if self.config['stake_amount'] == 'unlimited': - # Use entire available wallet during favorable conditions when in compounding mode. - return max_stake - else: - # Compound profits during favorable conditions instead of using a static stake. - return self.wallets.get_total_stake_amount() / self.config['max_open_trades'] - - # Use default stake amount. - return proposed_stake -``` - -Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged. - -!!! Tip - You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged. - -!!! Tip - Returning `0` or `None` will prevent trades from being placed. - --- ## Derived strategies From ef67a2adfc7e9d04e64e769c9ff07021fd5b53a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Nov 2021 20:24:07 +0100 Subject: [PATCH 04/70] Improve callback documentation --- docs/strategy-advanced.md | 23 +++++++++++++++++------ docs/strategy-customization.md | 2 +- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 1b301ee06..5fd36c5ee 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -145,10 +145,12 @@ Currently available callbacks: * [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation) * [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation) +!!! Tip "Callback calling sequence" + You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic) ### Bot loop start -A simple callback which is called once at the start of every bot throttling iteration. +A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently). This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc. ``` python @@ -174,7 +176,7 @@ class AwesomeStrategy(IStrategy): ### Custom Stake size -It is possible to manage your risk by reducing or increasing stake amount when placing a new trade. +Called before entering a trade, makes it possible to manage your position size when placing a new trade. ```python class AwesomeStrategy(IStrategy): @@ -207,14 +209,16 @@ Freqtrade will fall back to the `proposed_stake` value should your code raise an ### Custom sell signal -It is possible to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need the trade profit to take the sell decision. +Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed. + +Allows to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need trade data to make an exit decision. For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. !!! Note - Returning a `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. + Returning a (none-empty) `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: @@ -244,9 +248,11 @@ See [Dataframe access](#dataframe-access) for more information about dataframe u ### Custom stoploss -The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. - +Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed. The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object. + +The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss (before this method is called for the first time for a trade). + The method must return a stoploss value (float / number) as a percentage of the current price. E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD. @@ -469,6 +475,8 @@ By default, freqtrade use the orderbook to automatically set an order price([Rel You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits. +Each of these methods are called right before placing an order on the exchange. + !!! Note If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration. @@ -523,6 +531,9 @@ However, freqtrade also offers a custom callback for both order types, which all #### Custom order timeout example +Called for every open order until that order is either filled or cancelled. +`check_buy_timeout()` is called for trade entries, while `check_sell_timeout()` is called for trade exit orders. + A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below. It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index aeb837e16..d518ea648 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -1018,7 +1018,7 @@ The following lists some common patterns which should be avoided to prevent frus ### Colliding signals -When buy and sell signals collide (both `'buy'` and `'sell'` are 1), freqtrade will do nothing. This is to avoid trades that buy, and sell immediately. Obviously, this can lead to both missed entries, as well as missed exits. +When buy and sell signals collide (both `'buy'` and `'sell'` are 1), freqtrade will do nothing and ignore the entry (buy) signal. This will avoid trades that buy, and sell immediately. Obviously, this can potentially lead to missed entries. ## Further strategy ideas From 0bae1471bdf7fbdf7ae0b60c32dfc5e858117862 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Nov 2021 11:44:56 +0100 Subject: [PATCH 05/70] Extract callbacks into a separate site --- docs/strategy-advanced.md | 569 ------------------------------------- docs/strategy-callbacks.md | 566 ++++++++++++++++++++++++++++++++++++ mkdocs.yml | 3 +- 3 files changed, 568 insertions(+), 570 deletions(-) create mode 100644 docs/strategy-callbacks.md diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 5fd36c5ee..573d184ff 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -127,575 +127,6 @@ The provided exit-tag is then used as sell-reason - and shown as such in backtes !!! Note `sell_reason` is limited to 100 characters, remaining data will be truncated. -## Callbacks - -While the main strategy functions (`populate_indicators()`, `populate_buy_trend()`, `populate_sell_trend()`) should be used in a vectorized way, and are only called [once during backtesting](bot-basics.md#backtesting-hyperopt-execution-logic), callbacks are called "whenever needed". - -As such, you should avoid doing heavy calculations in callbacks to avoid delays during operations. -Depending on the callback used, they may be called when entering / exiting a trade, or throughout the duration of a trade. - -Currently available callbacks: - -* [`bot_loop_start()`](#bot-loop-start) -* [`custom_stake_amount()`](#custom-stake-size) -* [`custom_sell()`](#custom-sell-signal) -* [`custom_stoploss()`](#custom-stoploss) -* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules) -* [`check_buy_timeout()` and `check_sell_timeout()](#custom-order-timeout-rules) -* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation) -* [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation) - -!!! Tip "Callback calling sequence" - You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic) - -### Bot loop start - -A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently). -This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc. - -``` python -import requests - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - def bot_loop_start(self, **kwargs) -> None: - """ - Called at the start of the bot iteration (one loop). - Might be used to perform pair-independent tasks - (e.g. gather some remote resource for comparison) - :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - """ - if self.config['runmode'].value in ('live', 'dry_run'): - # Assign this to the class by using self.* - # can then be used by populate_* methods - self.remote_data = requests.get('https://some_remote_source.example.com') - -``` - -### Custom Stake size - -Called before entering a trade, makes it possible to manage your position size when placing a new trade. - -```python -class AwesomeStrategy(IStrategy): - def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, - proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: - - dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) - current_candle = dataframe.iloc[-1].squeeze() - - if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']: - if self.config['stake_amount'] == 'unlimited': - # Use entire available wallet during favorable conditions when in compounding mode. - return max_stake - else: - # Compound profits during favorable conditions instead of using a static stake. - return self.wallets.get_total_stake_amount() / self.config['max_open_trades'] - - # Use default stake amount. - return proposed_stake -``` - -Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged. - -!!! Tip - You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged. - -!!! Tip - Returning `0` or `None` will prevent trades from being placed. - -### Custom sell signal - -Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed. - -Allows to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need trade data to make an exit decision. - -For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. - -Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. - -!!! Note - Returning a (none-empty) `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. - -An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: - -``` python -class AwesomeStrategy(IStrategy): - def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, - current_profit: float, **kwargs): - dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) - last_candle = dataframe.iloc[-1].squeeze() - - # Above 20% profit, sell when rsi < 80 - if current_profit > 0.2: - if last_candle['rsi'] < 80: - return 'rsi_below_80' - - # Between 2% and 10%, sell if EMA-long above EMA-short - if 0.02 < current_profit < 0.1: - if last_candle['emalong'] > last_candle['emashort']: - return 'ema_long_below_80' - - # Sell any positions at a loss if they are held for more than one day. - if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1: - return 'unclog' -``` - -See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. - -### Custom stoploss - -Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed. -The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object. - -The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss (before this method is called for the first time for a trade). - -The method must return a stoploss value (float / number) as a percentage of the current price. -E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD. - -The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. - -To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: - -``` python -# additional imports required -from datetime import datetime -from freqtrade.persistence import Trade - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - use_custom_stoploss = True - - def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: - """ - Custom stoploss logic, returning the new distance relative to current_rate (as ratio). - e.g. returning -0.05 would create a stoploss 5% below current_rate. - The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. - - For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ - - When not implemented by a strategy, returns the initial stoploss value - Only called when use_custom_stoploss is set to True. - - :param pair: Pair that's currently analyzed - :param trade: trade object. - :param current_time: datetime object, containing the current datetime - :param current_rate: Rate, calculated based on pricing settings in ask_strategy. - :param current_profit: Current profit (as ratio), calculated based on current_rate. - :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return float: New stoploss value, relative to the current rate - """ - return -0.04 -``` - -Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). - -!!! Note "Use of dates" - All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. - -!!! Tip "Trailing stoploss" - It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior. - -#### Custom stoploss examples - -The next section will show some examples on what's possible with the custom stoploss function. -Of course, many more things are possible, and all examples can be combined at will. - -##### Time based trailing stop - -Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. - -``` python -from datetime import datetime, timedelta -from freqtrade.persistence import Trade - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - use_custom_stoploss = True - - def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: - - # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. - if current_time - timedelta(minutes=120) > trade.open_date_utc: - return -0.05 - elif current_time - timedelta(minutes=60) > trade.open_date_utc: - return -0.10 - return 1 -``` - -##### Different stoploss per pair - -Use a different stoploss depending on the pair. -In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs. - -``` python -from datetime import datetime -from freqtrade.persistence import Trade - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - use_custom_stoploss = True - - def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: - - if pair in ('ETH/BTC', 'XRP/BTC'): - return -0.10 - elif pair in ('LTC/BTC'): - return -0.05 - return -0.15 -``` - -##### Trailing stoploss with positive offset - -Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. - -Please note that the stoploss can only increase, values lower than the current stoploss are ignored. - -``` python -from datetime import datetime, timedelta -from freqtrade.persistence import Trade - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - use_custom_stoploss = True - - def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: - - if current_profit < 0.04: - return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss - - # After reaching the desired offset, allow the stoploss to trail by half the profit - desired_stoploss = current_profit / 2 - - # Use a minimum of 2.5% and a maximum of 5% - return max(min(desired_stoploss, 0.05), 0.025) -``` - -##### Stepped stoploss - -Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. - -* Use the regular stoploss until 20% profit is reached -* Once profit is > 20% - set stoploss to 7% above open price. -* Once profit is > 25% - set stoploss to 15% above open price. -* Once profit is > 40% - set stoploss to 25% above open price. - -``` python -from datetime import datetime -from freqtrade.persistence import Trade -from freqtrade.strategy import stoploss_from_open - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - use_custom_stoploss = True - - def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, - current_rate: float, current_profit: float, **kwargs) -> float: - - # evaluate highest to lowest, so that highest possible stop is used - if current_profit > 0.40: - return stoploss_from_open(0.25, current_profit) - elif current_profit > 0.25: - return stoploss_from_open(0.15, current_profit) - elif current_profit > 0.20: - return stoploss_from_open(0.07, current_profit) - - # return maximum stoploss value, keeping current stoploss price unchanged - return 1 -``` - -##### Custom stoploss using an indicator from dataframe example - -Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss. - -``` python -class AwesomeStrategy(IStrategy): - - def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - # <...> - dataframe['sar'] = ta.SAR(dataframe) - - use_custom_stoploss = True - - 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) - last_candle = dataframe.iloc[-1].squeeze() - - # Use parabolic sar as absolute stoploss price - stoploss_price = last_candle['sar'] - - # Convert absolute price to percentage relative to current_rate - if stoploss_price < current_rate: - return (stoploss_price / current_rate) - 1 - - # return maximum stoploss value, keeping current stoploss price unchanged - return 1 -``` - -See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. - -#### Common helpers for stoploss calculations - -##### Stoploss relative to open price - -Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` 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_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()`. - -##### 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()`. - ---- - -### Custom order price rules - -By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy. - -You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits. - -Each of these methods are called right before placing an order on the exchange. - -!!! Note - If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration. - -#### Custom order entry and exit price example - -``` python -from datetime import datetime, timedelta, timezone -from freqtrade.persistence import Trade - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - def custom_entry_price(self, pair: str, current_time: datetime, - proposed_rate, **kwargs) -> float: - - dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, - timeframe=self.timeframe) - new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1] - - return new_entryprice - - def custom_exit_price(self, pair: str, trade: Trade, - current_time: datetime, proposed_rate: float, - current_profit: float, **kwargs) -> float: - - dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, - timeframe=self.timeframe) - new_exitprice = dataframe['bollinger_10_upperband'].iat[-1] - - return new_exitprice - -``` - -!!! Warning - Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter. - -!!! Example - If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98. - -!!! Warning "No backtesting support" - Custom entry-prices are currently not supported during backtesting. - -### Custom order timeout rules - -Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. - -However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not. - -!!! Note - Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances. - -#### Custom order timeout example - -Called for every open order until that order is either filled or cancelled. -`check_buy_timeout()` is called for trade entries, while `check_sell_timeout()` is called for trade exit orders. - -A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below. -It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins. - -The function must return either `True` (cancel order) or `False` (keep order alive). - -``` python -from datetime import datetime, timedelta, timezone -from freqtrade.persistence import Trade - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. - unfilledtimeout = { - 'buy': 60 * 25, - 'sell': 60 * 25 - } - - def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: - if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): - return True - elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): - return True - elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): - return True - return False - - - def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: - if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): - return True - elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): - return True - elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): - return True - return False -``` - -!!! Note - For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first. - -#### Custom order timeout example (using additional data) - -``` python -from datetime import datetime -from freqtrade.persistence import Trade - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. - unfilledtimeout = { - 'buy': 60 * 25, - 'sell': 60 * 25 - } - - def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: - ob = self.dp.orderbook(pair, 1) - current_price = ob['bids'][0][0] - # Cancel buy order if price is more than 2% above the order. - if current_price > order['price'] * 1.02: - return True - return False - - - def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: - ob = self.dp.orderbook(pair, 1) - current_price = ob['asks'][0][0] - # Cancel sell order if price is more than 2% below the order. - if current_price < order['price'] * 0.98: - return True - return False -``` - ---- - -### Bot order confirmation - -#### Trade entry (buy order) confirmation - -`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect). - -``` python -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: datetime, **kwargs) -> bool: - """ - Called right before placing a buy order. - Timing for this function is critical, so avoid doing heavy computations or - network requests in this method. - - For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ - - When not implemented by a strategy, returns True (always confirming). - - :param pair: Pair that's about to be bought. - :param order_type: Order type (as configured in order_types). usually limit or market. - :param amount: Amount in target (quote) currency that's going to be traded. - :param rate: Rate that's going to be used when using limit orders - :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). - :param current_time: datetime object, containing the current datetime - :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: When True is returned, then the buy-order is placed on the exchange. - False aborts the process - """ - return True - -``` - -#### Trade exit (sell order) confirmation - -`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect). - -``` python -from freqtrade.persistence import Trade - - -class AwesomeStrategy(IStrategy): - - # ... populate_* methods - - def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, - rate: float, time_in_force: str, sell_reason: str, - current_time: datetime, **kwargs) -> bool: - """ - Called right before placing a regular sell order. - Timing for this function is critical, so avoid doing heavy computations or - network requests in this method. - - For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ - - When not implemented by a strategy, returns True (always confirming). - - :param pair: Pair that's about to be sold. - :param order_type: Order type (as configured in order_types). usually limit or market. - :param amount: Amount in quote currency. - :param rate: Rate that's going to be used when using limit orders - :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). - :param sell_reason: Sell reason. - Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', - 'sell_signal', 'force_sell', 'emergency_sell'] - :param current_time: datetime object, containing the current datetime - :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. - :return bool: When True is returned, then the sell-order is placed on the exchange. - False aborts the process - """ - if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: - # Reject force-sells with negative profit - # This is just a sample, please adjust to your needs - # (this does not necessarily make sense, assuming you know when you're force-selling) - return False - return True - -``` - ---- - ## Derived strategies The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched: diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md new file mode 100644 index 000000000..820023a19 --- /dev/null +++ b/docs/strategy-callbacks.md @@ -0,0 +1,566 @@ +# Strategy Callbacks + +While the main strategy functions (`populate_indicators()`, `populate_buy_trend()`, `populate_sell_trend()`) should be used in a vectorized way, and are only called [once during backtesting](bot-basics.md#backtesting-hyperopt-execution-logic), callbacks are called "whenever needed". + +As such, you should avoid doing heavy calculations in callbacks to avoid delays during operations. +Depending on the callback used, they may be called when entering / exiting a trade, or throughout the duration of a trade. + +Currently available callbacks: + +* [`bot_loop_start()`](#bot-loop-start) +* [`custom_stake_amount()`](#custom-stake-size) +* [`custom_sell()`](#custom-sell-signal) +* [`custom_stoploss()`](#custom-stoploss) +* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules) +* [`check_buy_timeout()` and `check_sell_timeout()](#custom-order-timeout-rules) +* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation) +* [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation) + +!!! Tip "Callback calling sequence" + You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic) + +## Bot loop start + +A simple callback which is called once at the start of every bot throttling iteration (roughly every 5 seconds, unless configured differently). +This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc. + +``` python +import requests + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + def bot_loop_start(self, **kwargs) -> None: + """ + Called at the start of the bot iteration (one loop). + Might be used to perform pair-independent tasks + (e.g. gather some remote resource for comparison) + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + """ + if self.config['runmode'].value in ('live', 'dry_run'): + # Assign this to the class by using self.* + # can then be used by populate_* methods + self.remote_data = requests.get('https://some_remote_source.example.com') + +``` + +## Custom Stake size + +Called before entering a trade, makes it possible to manage your position size when placing a new trade. + +```python +class AwesomeStrategy(IStrategy): + def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, + proposed_stake: float, min_stake: float, max_stake: float, + **kwargs) -> float: + + dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) + current_candle = dataframe.iloc[-1].squeeze() + + if current_candle['fastk_rsi_1h'] > current_candle['fastd_rsi_1h']: + if self.config['stake_amount'] == 'unlimited': + # Use entire available wallet during favorable conditions when in compounding mode. + return max_stake + else: + # Compound profits during favorable conditions instead of using a static stake. + return self.wallets.get_total_stake_amount() / self.config['max_open_trades'] + + # Use default stake amount. + return proposed_stake +``` + +Freqtrade will fall back to the `proposed_stake` value should your code raise an exception. The exception itself will be logged. + +!!! Tip + You do not _have_ to ensure that `min_stake <= returned_value <= max_stake`. Trades will succeed as the returned value will be clamped to supported range and this acton will be logged. + +!!! Tip + Returning `0` or `None` will prevent trades from being placed. + +## Custom sell signal + +Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed. + +Allows to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need trade data to make an exit decision. + +For example you could implement a 1:2 risk-reward ROI with `custom_sell()`. + +Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange. + +!!! Note + Returning a (none-empty) `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters. + +An example of how we can use different indicators depending on the current profit and also sell trades that were open longer than one day: + +``` python +class AwesomeStrategy(IStrategy): + def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float, + current_profit: float, **kwargs): + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + + # Above 20% profit, sell when rsi < 80 + if current_profit > 0.2: + if last_candle['rsi'] < 80: + return 'rsi_below_80' + + # Between 2% and 10%, sell if EMA-long above EMA-short + if 0.02 < current_profit < 0.1: + if last_candle['emalong'] > last_candle['emashort']: + return 'ema_long_below_80' + + # Sell any positions at a loss if they are held for more than one day. + if current_profit < 0.0 and (current_time - trade.open_date_utc).days >= 1: + return 'unclog' +``` + +See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. + +## Custom stoploss + +Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed. +The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object. + +The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss (before this method is called for the first time for a trade). + +The method must return a stoploss value (float / number) as a percentage of the current price. +E.g. If the `current_rate` is 200 USD, then returning `0.02` will set the stoploss price 2% lower, at 196 USD. + +The absolute value of the return value is used (the sign is ignored), so returning `0.05` or `-0.05` have the same result, a stoploss 5% below the current price. + +To simulate a regular trailing stoploss of 4% (trailing 4% behind the maximum reached price) you would use the following very simple method: + +``` python +# additional imports required +from datetime import datetime +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + """ + Custom stoploss logic, returning the new distance relative to current_rate (as ratio). + e.g. returning -0.05 would create a stoploss 5% below current_rate. + The custom stoploss can never be below self.stoploss, which serves as a hard maximum loss. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns the initial stoploss value + Only called when use_custom_stoploss is set to True. + + :param pair: Pair that's currently analyzed + :param trade: trade object. + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param current_profit: Current profit (as ratio), calculated based on current_rate. + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return float: New stoploss value, relative to the current rate + """ + return -0.04 +``` + +Stoploss on exchange works similar to `trailing_stop`, and the stoploss on exchange is updated as configured in `stoploss_on_exchange_interval` ([More details about stoploss on exchange](stoploss.md#stop-loss-on-exchange-freqtrade)). + +!!! Note "Use of dates" + All time-based calculations should be done based on `current_time` - using `datetime.now()` or `datetime.utcnow()` is discouraged, as this will break backtesting support. + +!!! Tip "Trailing stoploss" + It's recommended to disable `trailing_stop` when using custom stoploss values. Both can work in tandem, but you might encounter the trailing stop to move the price higher while your custom function would not want this, causing conflicting behavior. + +### Custom stoploss examples + +The next section will show some examples on what's possible with the custom stoploss function. +Of course, many more things are possible, and all examples can be combined at will. + +#### Time based trailing stop + +Use the initial stoploss for the first 60 minutes, after this change to 10% trailing stoploss, and after 2 hours (120 minutes) we use a 5% trailing stoploss. + +``` python +from datetime import datetime, timedelta +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + # Make sure you have the longest interval first - these conditions are evaluated from top to bottom. + if current_time - timedelta(minutes=120) > trade.open_date_utc: + return -0.05 + elif current_time - timedelta(minutes=60) > trade.open_date_utc: + return -0.10 + return 1 +``` + +#### Different stoploss per pair + +Use a different stoploss depending on the pair. +In this example, we'll trail the highest price with 10% trailing stoploss for `ETH/BTC` and `XRP/BTC`, with 5% trailing stoploss for `LTC/BTC` and with 15% for all other pairs. + +``` python +from datetime import datetime +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + if pair in ('ETH/BTC', 'XRP/BTC'): + return -0.10 + elif pair in ('LTC/BTC'): + return -0.05 + return -0.15 +``` + +#### Trailing stoploss with positive offset + +Use the initial stoploss until the profit is above 4%, then use a trailing stoploss of 50% of the current profit with a minimum of 2.5% and a maximum of 5%. + +Please note that the stoploss can only increase, values lower than the current stoploss are ignored. + +``` python +from datetime import datetime, timedelta +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + if current_profit < 0.04: + return -1 # return a value bigger than the initial stoploss to keep using the initial stoploss + + # After reaching the desired offset, allow the stoploss to trail by half the profit + desired_stoploss = current_profit / 2 + + # Use a minimum of 2.5% and a maximum of 5% + return max(min(desired_stoploss, 0.05), 0.025) +``` + +#### Stepped stoploss + +Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. + +* Use the regular stoploss until 20% profit is reached +* Once profit is > 20% - set stoploss to 7% above open price. +* Once profit is > 25% - set stoploss to 15% above open price. +* Once profit is > 40% - set stoploss to 25% above open price. + +``` python +from datetime import datetime +from freqtrade.persistence import Trade +from freqtrade.strategy import stoploss_from_open + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + # evaluate highest to lowest, so that highest possible stop is used + if current_profit > 0.40: + return stoploss_from_open(0.25, current_profit) + elif current_profit > 0.25: + return stoploss_from_open(0.15, current_profit) + elif current_profit > 0.20: + return stoploss_from_open(0.07, current_profit) + + # return maximum stoploss value, keeping current stoploss price unchanged + return 1 +``` + +#### Custom stoploss using an indicator from dataframe example + +Absolute stoploss value may be derived from indicators stored in dataframe. Example uses parabolic SAR below the price as stoploss. + +``` python +class AwesomeStrategy(IStrategy): + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # <...> + dataframe['sar'] = ta.SAR(dataframe) + + use_custom_stoploss = True + + 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) + last_candle = dataframe.iloc[-1].squeeze() + + # Use parabolic sar as absolute stoploss price + stoploss_price = last_candle['sar'] + + # Convert absolute price to percentage relative to current_rate + if stoploss_price < current_rate: + return (stoploss_price / current_rate) - 1 + + # return maximum stoploss value, keeping current stoploss price unchanged + return 1 +``` + +See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. + +### Common helpers for stoploss calculations + +#### Stoploss relative to open price + +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss relative to the *open* price, we need to use `current_profit` 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_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()`. + +#### 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()`. + +--- + +## Custom order price rules + +By default, freqtrade use the orderbook to automatically set an order price([Relevant documentation](configuration.md#prices-used-for-orders)), you also have the option to create custom order prices based on your strategy. + +You can use this feature by creating a `custom_entry_price()` function in your strategy file to customize entry prices and `custom_exit_price()` for exits. + +Each of these methods are called right before placing an order on the exchange. + +!!! Note + If your custom pricing function return None or an invalid value, price will fall back to `proposed_rate`, which is based on the regular pricing configuration. + +### Custom order entry and exit price example + +``` python +from datetime import datetime, timedelta, timezone +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + def custom_entry_price(self, pair: str, current_time: datetime, + proposed_rate, **kwargs) -> float: + + dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, + timeframe=self.timeframe) + new_entryprice = dataframe['bollinger_10_lowerband'].iat[-1] + + return new_entryprice + + def custom_exit_price(self, pair: str, trade: Trade, + current_time: datetime, proposed_rate: float, + current_profit: float, **kwargs) -> float: + + dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=pair, + timeframe=self.timeframe) + new_exitprice = dataframe['bollinger_10_upperband'].iat[-1] + + return new_exitprice + +``` + +!!! Warning + Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter. + +!!! Example + If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98. + +!!! Warning "No backtesting support" + Custom entry-prices are currently not supported during backtesting. + +## Custom order timeout rules + +Simple, time-based order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. + +However, freqtrade also offers a custom callback for both order types, which allows you to decide based on custom criteria if an order did time out or not. + +!!! Note + Unfilled order timeouts are not relevant during backtesting or hyperopt, and are only relevant during real (live) trading. Therefore these methods are only called in these circumstances. + +### Custom order timeout example + +Called for every open order until that order is either filled or cancelled. +`check_buy_timeout()` is called for trade entries, while `check_sell_timeout()` is called for trade exit orders. + +A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below. +It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins. + +The function must return either `True` (cancel order) or `False` (keep order alive). + +``` python +from datetime import datetime, timedelta, timezone +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. + unfilledtimeout = { + 'buy': 60 * 25, + 'sell': 60 * 25 + } + + def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: + if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): + return True + elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): + return True + elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): + return True + return False + + + def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: + if trade.open_rate > 100 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=5): + return True + elif trade.open_rate > 10 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(minutes=3): + return True + elif trade.open_rate < 1 and trade.open_date_utc < datetime.now(timezone.utc) - timedelta(hours=24): + return True + return False +``` + +!!! Note + For the above example, `unfilledtimeout` must be set to something bigger than 24h, otherwise that type of timeout will apply first. + +### Custom order timeout example (using additional data) + +``` python +from datetime import datetime +from freqtrade.persistence import Trade + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + # Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours. + unfilledtimeout = { + 'buy': 60 * 25, + 'sell': 60 * 25 + } + + def check_buy_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: + ob = self.dp.orderbook(pair, 1) + current_price = ob['bids'][0][0] + # Cancel buy order if price is more than 2% above the order. + if current_price > order['price'] * 1.02: + return True + return False + + + def check_sell_timeout(self, pair: str, trade: Trade, order: dict, **kwargs) -> bool: + ob = self.dp.orderbook(pair, 1) + current_price = ob['asks'][0][0] + # Cancel sell order if price is more than 2% below the order. + if current_price < order['price'] * 0.98: + return True + return False +``` + +--- + +## Bot order confirmation + +### Trade entry (buy order) confirmation + +`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect). + +``` python +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, current_time: datetime, **kwargs) -> bool: + """ + Called right before placing a buy order. + Timing for this function is critical, so avoid doing heavy computations or + network requests in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be bought. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in target (quote) currency that's going to be traded. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param current_time: datetime object, containing the current datetime + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the buy-order is placed on the exchange. + False aborts the process + """ + return True + +``` + +### Trade exit (sell order) confirmation + +`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect). + +``` python +from freqtrade.persistence import Trade + + +class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, + rate: float, time_in_force: str, sell_reason: str, + current_time: datetime, **kwargs) -> bool: + """ + Called right before placing a regular sell order. + Timing for this function is critical, so avoid doing heavy computations or + network requests in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be sold. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in quote currency. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param sell_reason: Sell reason. + Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', + 'sell_signal', 'force_sell', 'emergency_sell'] + :param current_time: datetime object, containing the current datetime + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the sell-order is placed on the exchange. + False aborts the process + """ + if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: + # Reject force-sells with negative profit + # This is just a sample, please adjust to your needs + # (this does not necessarily make sense, assuming you know when you're force-selling) + return False + return True + +``` diff --git a/mkdocs.yml b/mkdocs.yml index 0daf462c2..b9c053324 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -11,8 +11,9 @@ nav: - Freqtrade Basics: bot-basics.md - Configuration: configuration.md - Strategy Customization: strategy-customization.md - - Plugins: plugins.md + - Strategy Callbacks: strategy-callbacks.md - Stoploss: stoploss.md + - Plugins: plugins.md - Start the bot: bot-usage.md - Control the bot: - Telegram: telegram-usage.md From b36fe8fe0ff893afc3088fd18b33d8a10ace2ab8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Nov 2021 11:48:11 +0100 Subject: [PATCH 06/70] Simplify strategy documentation --- docs/strategy-customization.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index d518ea648..178ed108b 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -317,20 +317,14 @@ class AwesomeStrategy(IStrategy): Setting a stoploss is highly recommended to protect your capital from strong moves against you. -Sample: +Sample of setting a 10% stoploss: ``` python stoploss = -0.10 ``` -This would signify a stoploss of -10%. - For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md). -If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order_types dictionary, so your stoploss is on the exchange and cannot be missed due to network problems, high load or other reasons. - -For more information on order_types please look [here](configuration.md#understand-order_types). - ### Timeframe (formerly ticker interval) This is the set of candles the bot should download and use for the analysis. @@ -346,7 +340,7 @@ The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `p Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`. The Metadata-dict should not be modified and does not persist information across multiple calls. -Instead, have a look at the section [Storing information](strategy-advanced.md#Storing-information) +Instead, have a look at the [Storing information](strategy-advanced.md#Storing-information) section. ## Strategy file loading From c0cc3f5f973155a9ee2386e0e2659ead802188e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 20 Nov 2021 16:15:03 +0100 Subject: [PATCH 07/70] Small doc improvements to callback documentation --- docs/strategy-callbacks.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/strategy-callbacks.md b/docs/strategy-callbacks.md index 820023a19..7a7756652 100644 --- a/docs/strategy-callbacks.md +++ b/docs/strategy-callbacks.md @@ -115,7 +115,7 @@ class AwesomeStrategy(IStrategy): return 'unclog' ``` -See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. +See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks. ## Custom stoploss @@ -322,7 +322,7 @@ class AwesomeStrategy(IStrategy): return 1 ``` -See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. +See [Dataframe access](strategy-advanced.md#dataframe-access) for more information about dataframe use in strategy callbacks. ### Common helpers for stoploss calculations @@ -383,10 +383,9 @@ class AwesomeStrategy(IStrategy): ``` !!! Warning - Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter. - -!!! Example - If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98. + Modifying entry and exit prices will only work for limit orders. Depending on the price chosen, this can result in a lot of unfilled orders. By default the maximum allowed distance between the current price and the custom price is 2%, this value can be changed in config with the `custom_price_max_distance_ratio` parameter. + **Example**: + If the new_entryprice is 97, the proposed_rate is 100 and the `custom_price_max_distance_ratio` is set to 2%, The retained valid custom entry price will be 98, which is 2% below the current (proposed) rate. !!! Warning "No backtesting support" Custom entry-prices are currently not supported during backtesting. @@ -485,6 +484,9 @@ class AwesomeStrategy(IStrategy): ## Bot order confirmation +Confirm trade entry / exits. +This are the last methods that will be called before an order is placed. + ### Trade entry (buy order) confirmation `confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect). From ab93e136825cc0580df80df9563272d94762d839 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Nov 2021 03:01:36 +0000 Subject: [PATCH 08/70] Bump filelock from 3.3.2 to 3.4.0 Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.3.2 to 3.4.0. - [Release notes](https://github.com/tox-dev/py-filelock/releases) - [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst) - [Commits](https://github.com/tox-dev/py-filelock/compare/3.3.2...3.4.0) --- updated-dependencies: - dependency-name: filelock dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 7efbb47cd..a3da8f0be 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,7 +5,7 @@ scipy==1.7.2 scikit-learn==1.0.1 scikit-optimize==0.9.0 -filelock==3.3.2 +filelock==3.4.0 joblib==1.1.0 psutil==5.8.0 progressbar2==3.55.0 From fdc6ca1bd827d16ac3eeb3d3077f6c59def8670f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Nov 2021 03:01:48 +0000 Subject: [PATCH 09/70] Bump cryptography from 35.0.0 to 36.0.0 Bumps [cryptography](https://github.com/pyca/cryptography) from 35.0.0 to 36.0.0. - [Release notes](https://github.com/pyca/cryptography/releases) - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/35.0.0...36.0.0) --- updated-dependencies: - dependency-name: cryptography dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d715b8f52..0822e2555 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas-ta==0.3.14b ccxt==1.61.24 # Pin cryptography for now due to rust build errors with piwheels -cryptography==35.0.0 +cryptography==36.0.0 aiohttp==3.7.4.post0 SQLAlchemy==1.4.27 python-telegram-bot==13.8.1 From 247f855ba9ab4665dd893c2bf16f65206cdbcbfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Nov 2021 03:01:52 +0000 Subject: [PATCH 10/70] Bump types-cachetools from 4.2.4 to 4.2.5 Bumps [types-cachetools](https://github.com/python/typeshed) from 4.2.4 to 4.2.5. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-cachetools dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ab06468b9..4c06e657b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,7 +20,7 @@ time-machine==2.4.0 nbconvert==6.3.0 # mypy types -types-cachetools==4.2.4 +types-cachetools==4.2.5 types-filelock==3.2.1 types-requests==2.26.0 types-tabulate==0.8.3 From 0ef99206b0aac071bb36dd7420b240cd7cde7e4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Nov 2021 03:01:56 +0000 Subject: [PATCH 11/70] Bump plotly from 5.3.1 to 5.4.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 5.3.1 to 5.4.0. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v5.3.1...v5.4.0) --- updated-dependencies: - dependency-name: plotly dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 8e17232b0..488ef73d6 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==5.3.1 +plotly==5.4.0 From 80946cd9d6f1149dc8f10103c28b6733827464ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Nov 2021 05:47:48 +0000 Subject: [PATCH 12/70] Bump ccxt from 1.61.24 to 1.61.92 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.61.24 to 1.61.92. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.61.24...1.61.92) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 0822e2555..8f612451c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.4 pandas==1.3.4 pandas-ta==0.3.14b -ccxt==1.61.24 +ccxt==1.61.92 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.0 aiohttp==3.7.4.post0 From ecf2ac3c219f135b1d268a837948eca182e1a9dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Nov 2021 06:51:06 +0100 Subject: [PATCH 13/70] Bump aiohttp to 3.8.1 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8f612451c..a5af330af 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ pandas-ta==0.3.14b ccxt==1.61.92 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.0 -aiohttp==3.7.4.post0 +aiohttp==3.8.1 SQLAlchemy==1.4.27 python-telegram-bot==13.8.1 arrow==1.2.1 @@ -43,4 +43,4 @@ colorama==0.4.4 questionary==1.10.0 prompt-toolkit==3.0.22 # Extensions to datetime library -python-dateutil==2.8.2 \ No newline at end of file +python-dateutil==2.8.2 From 64e34f382ee31860c03a659f718f1e32f2724624 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 22 Nov 2021 07:13:22 +0100 Subject: [PATCH 14/70] Sell-fill should include open-rate --- freqtrade/rpc/telegram.py | 6 +++--- tests/rpc/test_rpc_telegram.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0e1a6fe27..6c6f745e7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -274,11 +274,11 @@ class Telegram(RPCHandler): f"*Buy Tag:* `{msg['buy_tag']}`\n" f"*Sell Reason:* `{msg['sell_reason']}`\n" f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n" - f"*Amount:* `{msg['amount']:.8f}`\n") + f"*Amount:* `{msg['amount']:.8f}`\n" + f"*Open Rate:* `{msg['open_rate']:.8f}`\n") if msg['type'] == RPCMessageType.SELL: - message += (f"*Open Rate:* `{msg['open_rate']:.8f}`\n" - f"*Current Rate:* `{msg['current_rate']:.8f}`\n" + message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n" f"*Close Rate:* `{msg['limit']:.8f}`") elif msg['type'] == RPCMessageType.SELL_FILL: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1247affae..ce3b044be 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1843,6 +1843,7 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: '*Sell Reason:* `stop_loss`\n' '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Amount:* `1333.33333333`\n' + '*Open Rate:* `0.00007500`\n' '*Close Rate:* `0.00003201`' ) From 78a00f2518e4c2c2022194c86d0601b7713cda5c Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Mon, 22 Nov 2021 09:27:33 +0200 Subject: [PATCH 15/70] Use market data to get base and quote currencies in @informative() decorator. --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/strategy/informative_decorator.py | 11 +++++----- .../strats/informative_decorator_strategy.py | 10 +++++----- tests/strategy/test_strategy_helpers.py | 20 ++++++++++--------- 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 49957c2bb..5c41041b4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -67,7 +67,7 @@ class Backtesting: self.all_results: Dict[str, Dict] = {} self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - self.dataprovider = DataProvider(self.config, None) + self.dataprovider = DataProvider(self.config, self.exchange) if self.config.get('strategy_list', None): for strat in list(self.config['strategy_list']): diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py index 4c5f21108..722e7a128 100644 --- a/freqtrade/strategy/informative_decorator.py +++ b/freqtrade/strategy/informative_decorator.py @@ -80,12 +80,11 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: # 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.') + market = strategy.dp.market(asset) + if market is None: + raise OperationalException(f'Market {asset} is not available.') + base = market['base'] + quote = market['quote'] # 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. diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py index a32ad79e8..17d4df018 100644 --- a/tests/strategy/strats/informative_decorator_strategy.py +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -20,7 +20,7 @@ class InformativeDecoratorTest(IStrategy): startup_candle_count: int = 20 def informative_pairs(self): - return [('BTC/USDT', '5m')] + return [('NEO/USDT', '5m')] def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['buy'] = 0 @@ -38,8 +38,8 @@ class InformativeDecoratorTest(IStrategy): return dataframe # Simple informative test. - @informative('1h', 'BTC/{stake}') - def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + @informative('1h', 'NEO/{stake}') + def populate_indicators_neo_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['rsi'] = 14 return dataframe @@ -50,7 +50,7 @@ class InformativeDecoratorTest(IStrategy): return dataframe # Formatting test. - @informative('30m', 'BTC/{stake}', '{column}_{BASE}_{QUOTE}_{base}_{quote}_{asset}_{timeframe}') + @informative('30m', 'NEO/{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 @@ -68,7 +68,7 @@ class InformativeDecoratorTest(IStrategy): dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] # Mixing manual informative pairs with decorators. - informative = self.dp.get_pair_dataframe('BTC/USDT', '5m') + informative = self.dp.get_pair_dataframe('NEO/USDT', '5m') informative['rsi'] = 14 dataframe = merge_informative_pair(dataframe, informative, self.timeframe, '5m', ffill=True) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index cb7cf97a1..9e546869a 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -7,6 +7,7 @@ import pytest from freqtrade.data.dataprovider import DataProvider from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open, timeframe_to_minutes) +from tests.conftest import get_patched_exchange def generate_test_data(timeframe: str, size: int, start: str = '2020-07-05'): @@ -155,9 +156,9 @@ def test_informative_decorator(mocker, default_conf): ('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, + ('NEO/USDT', '30m'): test_data_30m, + ('NEO/USDT', '5m'): test_data_5m, + ('NEO/USDT', '1h'): test_data_1h, ('ETH/USDT', '1h'): test_data_1h, ('ETH/USDT', '30m'): test_data_30m, ('ETH/BTC', '1h'): test_data_1h, @@ -165,15 +166,16 @@ def test_informative_decorator(mocker, default_conf): from .strats.informative_decorator_strategy import InformativeDecoratorTest default_conf['stake_currency'] = 'USDT' strategy = InformativeDecoratorTest(config=default_conf) - strategy.dp = DataProvider({}, None, None) + exchange = get_patched_exchange(mocker, default_conf) + strategy.dp = DataProvider({}, exchange, None) mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[ - 'XRP/USDT', 'LTC/USDT', 'BTC/USDT' + 'XRP/USDT', 'LTC/USDT', 'NEO/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')] + ('LTC/USDT', '30m'), ('NEO/USDT', '1h'), ('NEO/USDT', '30m'), + ('NEO/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')] for inf_pair in informative_pairs: assert inf_pair in strategy.gather_informative_pairs() @@ -186,8 +188,8 @@ def test_informative_decorator(mocker, default_conf): {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 + 'neo_usdt_rsi_1h', # NEO 1h informative + 'rsi_NEO_USDT_neo_usdt_NEO/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 From 43dab3ee607c82e9aae86bec884efcba8845a1f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ald=C3=A1n=20Creo?= <20495460+ACMCMC@users.noreply.github.com> Date: Mon, 22 Nov 2021 09:46:10 +0100 Subject: [PATCH 16/70] Changed the wording of the documentation to be clearer The sentence I've changed was continued on a different paragraph before, even though they were connected ideas. I have changed it so that they are part of the same paragraph now. --- docs/configuration.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 6c810fba2..c4689f0a6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -202,9 +202,8 @@ There are several methods to configure how much of the stake currency the bot wi #### Minimum trade stake The minimum stake amount will depend on exchange and pair and is usually listed in the exchange support pages. -Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.6$. -The minimum stake amount to buy this pair is, therefore, `20 * 0.6 ~= 12`. +Assuming the minimum tradable amount for XRP/USD is 20 XRP (given by the exchange), and the price is 0.6$, the minimum stake amount to buy this pair is `20 * 0.6 ~= 12`. This exchange has also a limit on USD - where all orders must be > 10$ - which however does not apply in this case. To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%). From c245a2a897afe67f7e40c7a668deb0a7e660a2d9 Mon Sep 17 00:00:00 2001 From: flozzone Date: Mon, 22 Nov 2021 20:10:26 +0100 Subject: [PATCH 17/70] fix typo in Volatility filter description. --- docs/includes/pairlists.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 4f56f8e98..bbfe74510 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -292,7 +292,7 @@ If the trading range over the last 10 days is <1% or >99%, remove the pair from #### VolatilityFilter -Volatility is the degree of historical variation of a pairs over time, is is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). +Volatility is the degree of historical variation of a pairs over time, it is measured by the standard deviation of logarithmic daily returns. Returns are assumed to be normally distributed, although actual distribution might be different. In a normal distribution, 68% of observations fall within one standard deviation and 95% of observations fall within two standard deviations. Assuming a volatility of 0.05 means that the expected returns for 20 out of 30 days is expected to be less than 5% (one standard deviation). Volatility is a positive ratio of the expected deviation of return and can be greater than 1.00. Please refer to the wikipedia definition of [`volatility`](https://en.wikipedia.org/wiki/Volatility_(finance)). This filter removes pairs if the average volatility over a `lookback_days` days is below `min_volatility` or above `max_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. From b8cefd687e0baf0abf27c3880bf39f8ca6b0ff0f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 23 Nov 2021 07:06:53 +0100 Subject: [PATCH 18/70] Add api_version to botresponse --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 9 ++++++++- tests/rpc/test_rpc_apiserver.py | 2 ++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index c9ff0ddaf..268d50fdb 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -144,6 +144,7 @@ class OrderTypes(BaseModel): class ShowConfig(BaseModel): version: str + api_version: float dry_run: bool stake_currency: str stake_amount: Union[float, str] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 06230a7db..0467e4705 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -26,6 +26,11 @@ from freqtrade.rpc.rpc import RPCException logger = logging.getLogger(__name__) +# API version +# Pre-1.1, no version was provided +# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen. +API_VERSION = 1.1 + # Public API, requires no auth. router_public = APIRouter() # Private API, protected by authentication @@ -117,7 +122,9 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g state = '' if rpc: state = rpc._freqtrade.state - return RPC._rpc_show_config(config, state) + resp = RPC._rpc_show_config(config, state) + resp['api_version'] = API_VERSION + return resp @router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading']) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 074e312d9..76372df55 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -538,6 +538,8 @@ def test_api_show_config(botclient): assert 'ask_strategy' in rc.json() assert 'unfilledtimeout' in rc.json() assert 'version' in rc.json() + assert 'api_version' in rc.json() + assert 1.1 <= rc.json()['api_version'] <= 1.2 def test_api_daily(botclient, mocker, ticker, fee, markets): From c23ca35d23721eb165e485fe17c841560573e03e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 23 Nov 2021 07:13:39 +0100 Subject: [PATCH 19/70] Update ARMHF image to 3.9 --- Dockerfile | 2 +- docker/Dockerfile.armhf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index f7e26efe3..8f5b85698 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.7-slim-buster as base +FROM python:3.9.9-slim-bullseye as base # Setup env ENV LANG C.UTF-8 diff --git a/docker/Dockerfile.armhf b/docker/Dockerfile.armhf index f9827774e..16f2aebcd 100644 --- a/docker/Dockerfile.armhf +++ b/docker/Dockerfile.armhf @@ -1,4 +1,4 @@ -FROM python:3.7.10-slim-buster as base +FROM python:3.9.9-slim-bullseye as base # Setup env ENV LANG C.UTF-8 From e8feac367401952455433ee351066cdecef689f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 23 Nov 2021 20:02:07 +0100 Subject: [PATCH 20/70] Improve tests for pair_to_filename --- tests/test_misc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_misc.py b/tests/test_misc.py index 221c7b712..75f4c8622 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -67,6 +67,9 @@ def test_file_load_json(mocker, testdatadir) -> None: @pytest.mark.parametrize("pair,expected_result", [ ("ETH/BTC", 'ETH_BTC'), + ("ETH/USDT", 'ETH_USDT'), + ("ETH/USDT:USDT", 'ETH_USDT_USDT'), # swap with USDT as settlement currency + ("ETH/USDT:USDT-210625", 'ETH_USDT_USDT-210625'), # expiring futures ("Fabric Token/ETH", 'Fabric_Token_ETH'), ("ETHH20", 'ETHH20'), (".XBTBON2H", '_XBTBON2H'), From 65906d330f3c6291a22c63d9592f2e11b42aeee4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 23 Nov 2021 20:07:54 +0100 Subject: [PATCH 21/70] Improve tests for pair_to_filename --- tests/test_misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_misc.py b/tests/test_misc.py index 75f4c8622..de3f368e9 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -69,7 +69,7 @@ def test_file_load_json(mocker, testdatadir) -> None: ("ETH/BTC", 'ETH_BTC'), ("ETH/USDT", 'ETH_USDT'), ("ETH/USDT:USDT", 'ETH_USDT_USDT'), # swap with USDT as settlement currency - ("ETH/USDT:USDT-210625", 'ETH_USDT_USDT-210625'), # expiring futures + ("ETH/USDT:USDT-210625", 'ETH_USDT_USDT_210625'), # expiring futures ("Fabric Token/ETH", 'Fabric_Token_ETH'), ("ETHH20", 'ETHH20'), (".XBTBON2H", '_XBTBON2H'), From 338fe333a9bac02cc4ffc80eb8e0505ae7da94de Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Nov 2021 20:11:04 +0100 Subject: [PATCH 22/70] Allow forcebuy to specify order_type --- freqtrade/freqtradebot.py | 10 ++++------ freqtrade/rpc/api_server/api_schemas.py | 22 ++++++++++++++++------ freqtrade/rpc/api_server/api_v1.py | 5 +++-- freqtrade/rpc/rpc.py | 8 ++++++-- tests/rpc/test_rpc.py | 2 +- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index db0453cd7..57d5e0528 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -466,8 +466,8 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") return False - def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, - forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: + def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, *, + order_type: Optional[str] = None, buy_tag: Optional[str] = None) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY @@ -510,10 +510,8 @@ class FreqtradeBot(LoggingMixin): f"{stake_amount} ...") amount = stake_amount / enter_limit_requested - order_type = self.strategy.order_types['buy'] - if forcebuy: - # Forcebuy can define a different ordertype - order_type = self.strategy.order_types.get('forcebuy', order_type) + if not order_type: + order_type = self.strategy.order_types['buy'] if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 268d50fdb..ed483b18d 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,4 +1,5 @@ from datetime import date, datetime +from enum import Enum from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel @@ -131,13 +132,21 @@ class UnfilledTimeout(BaseModel): exit_timeout_count: Optional[int] +class OrderTypeValues(Enum): + limit = 'limit' + market = 'market' + + class Config: + use_enum_values = True + + class OrderTypes(BaseModel): - buy: str - sell: str - emergencysell: Optional[str] - forcesell: Optional[str] - forcebuy: Optional[str] - stoploss: str + buy: OrderTypeValues + sell: OrderTypeValues + emergencysell: Optional[OrderTypeValues] + forcesell: Optional[OrderTypeValues] + forcebuy: Optional[OrderTypeValues] + stoploss: OrderTypeValues stoploss_on_exchange: bool stoploss_on_exchange_interval: Optional[int] @@ -274,6 +283,7 @@ class Logs(BaseModel): class ForceBuyPayload(BaseModel): pair: str price: Optional[float] + ordertype: Optional[OrderTypeValues] class ForceSellPayload(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 0467e4705..6fc135820 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -29,7 +29,8 @@ logger = logging.getLogger(__name__) # API version # Pre-1.1, no version was provided # Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen. -API_VERSION = 1.1 +# 1.11: forcebuy accepts new option with ordertype +API_VERSION = 1.11 # Public API, requires no auth. router_public = APIRouter() @@ -129,7 +130,7 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g @router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading']) def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): - trade = rpc._rpc_forcebuy(payload.pair, payload.price) + trade = rpc._rpc_forcebuy(payload.pair, payload.price, payload.ordertype) if trade: return ForceBuyResponse.parse_obj(trade.to_json()) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 28585e4e8..fc1c0c777 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -692,7 +692,8 @@ class RPC: self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} - def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]: + def _rpc_forcebuy(self, pair: str, price: Optional[float], + order_type: Optional[str] = None) -> Optional[Trade]: """ Handler for forcebuy Buys a pair trade at the given or current price @@ -720,7 +721,10 @@ class RPC: stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair) # execute buy - if self._freqtrade.execute_entry(pair, stakeamount, price, forcebuy=True): + if not order_type: + order_type = self._freqtrade.strategy.order_types.get( + 'forcebuy', self._freqtrade.strategy.order_types['buy']) + if self._freqtrade.execute_entry(pair, stakeamount, price, order_type=order_type): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 2852ada81..b6fe1c691 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1093,7 +1093,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'): rpc._rpc_forcebuy(pair, 0.0001) pair = 'XRP/BTC' - trade = rpc._rpc_forcebuy(pair, 0.0001) + trade = rpc._rpc_forcebuy(pair, 0.0001, order_type='limit') assert isinstance(trade, Trade) assert trade.pair == pair assert trade.open_rate == 0.0001 From 0d1e84cf553822001dfb126a9cb1af0289f30f44 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Thu, 25 Nov 2021 16:00:10 +0900 Subject: [PATCH 23/70] Add more words Because apparently, we get at least 1 question about this everyday in Discord --- freqtrade/optimize/backtesting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 49957c2bb..15613cb06 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -89,7 +89,8 @@ class Backtesting: self.init_backtest_detail() self.pairlists = PairListManager(self.exchange, self.config) if 'VolumePairList' in self.pairlists.name_list: - raise OperationalException("VolumePairList not allowed for backtesting.") + raise OperationalException("VolumePairList not allowed for backtesting. " + "Please use StaticPairlist.") if 'PerformanceFilter' in self.pairlists.name_list: raise OperationalException("PerformanceFilter not allowed for backtesting.") From 0c629fc951f484019c4d563e8792f3c314088755 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Thu, 25 Nov 2021 16:03:29 +0900 Subject: [PATCH 24/70] Update test_backtesting.py --- tests/optimize/test_backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index ab7aa74a1..fb9282c2d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -438,7 +438,7 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> Backtesting(default_conf) default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}] - with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'): + with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting. Please use StaticPairlist.'): Backtesting(default_conf) default_conf.update({ @@ -470,7 +470,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti default_conf['timerange'] = '20180101-20180102' default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}] - with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'): + with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting. Please use StaticPairlist.'): Backtesting(default_conf) default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] From c23d90e2b84434a796ed9822ed00630df268c348 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Thu, 25 Nov 2021 16:56:56 +0900 Subject: [PATCH 25/70] Update test_backtesting.py --- tests/optimize/test_backtesting.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index fb9282c2d..548d13b31 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -438,7 +438,8 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> Backtesting(default_conf) default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}] - with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting. Please use StaticPairlist.'): + with pytest.raises(OperationalException, + match='VolumePairList not allowed for backtesting. Please use StaticPairlist.'): Backtesting(default_conf) default_conf.update({ @@ -470,7 +471,8 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti default_conf['timerange'] = '20180101-20180102' default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}] - with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting. Please use StaticPairlist.'): + with pytest.raises(OperationalException, + match='VolumePairList not allowed for backtesting. Please use StaticPairlist.'): Backtesting(default_conf) default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] From 5307d2bf3bfe3c8ed37143b9a9eda1cdd90183b3 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Thu, 25 Nov 2021 17:04:04 +0900 Subject: [PATCH 26/70] Trimming the sentence --- freqtrade/optimize/backtesting.py | 2 +- tests/optimize/test_backtesting.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 15613cb06..b16aced28 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -90,7 +90,7 @@ class Backtesting: self.pairlists = PairListManager(self.exchange, self.config) if 'VolumePairList' in self.pairlists.name_list: raise OperationalException("VolumePairList not allowed for backtesting. " - "Please use StaticPairlist.") + "Use StaticPairlist.") if 'PerformanceFilter' in self.pairlists.name_list: raise OperationalException("PerformanceFilter not allowed for backtesting.") diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 548d13b31..2a017ed7d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -439,7 +439,7 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}] with pytest.raises(OperationalException, - match='VolumePairList not allowed for backtesting. Please use StaticPairlist.'): + match='VolumePairList not allowed for backtesting. Use StaticPairlist.'): Backtesting(default_conf) default_conf.update({ @@ -472,7 +472,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}] with pytest.raises(OperationalException, - match='VolumePairList not allowed for backtesting. Please use StaticPairlist.'): + match='VolumePairList not allowed for backtesting. Use StaticPairlist.'): Backtesting(default_conf) default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] From f4bc30c927ea544560c801dec398b427eef9f3f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Nov 2021 06:23:29 +0100 Subject: [PATCH 27/70] Update docs to include "vpn/ssh" section --- docs/rest-api.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/rest-api.md b/docs/rest-api.md index 7299e0282..8c2599cbc 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -38,6 +38,11 @@ Sample configuration: !!! Danger "Security warning" By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot. +??? Note "API/UI Access on a remote servers" + If you're running on a VPS, you should consider using either a ssh tunnel, or setup a VPN (openVPN, wireguard) to connect to your bot. + This will ensure that freqUI is not directly exposed to the internet, which is not recommended for security reasons (freqUI does not support https out of the box). + Setup of these tools is not part of this tutorial, however many good tutorials can be found on the internet. + You can then access the API by going to `http://127.0.0.1:8080/api/v1/ping` in a browser to check if the API is running correctly. This should return the response: From 897788de17dc70c78d44fba6dfeb1366ab167f11 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Nov 2021 06:27:06 +0100 Subject: [PATCH 28/70] Reformulate exception to be "nicer" --- freqtrade/optimize/backtesting.py | 2 +- tests/optimize/test_backtesting.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b16aced28..219a4f069 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -90,7 +90,7 @@ class Backtesting: self.pairlists = PairListManager(self.exchange, self.config) if 'VolumePairList' in self.pairlists.name_list: raise OperationalException("VolumePairList not allowed for backtesting. " - "Use StaticPairlist.") + "Please use StaticPairlist instead.") if 'PerformanceFilter' in self.pairlists.name_list: raise OperationalException("PerformanceFilter not allowed for backtesting.") diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 2a017ed7d..f5e182c1d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -439,7 +439,7 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}] with pytest.raises(OperationalException, - match='VolumePairList not allowed for backtesting. Use StaticPairlist.'): + match=r'VolumePairList not allowed for backtesting\..*StaticPairlist.*'): Backtesting(default_conf) default_conf.update({ @@ -472,7 +472,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}] with pytest.raises(OperationalException, - match='VolumePairList not allowed for backtesting. Use StaticPairlist.'): + match=r'VolumePairList not allowed for backtesting\..*StaticPairlist.*'): Backtesting(default_conf) default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] From 80ed5283b24096168a441f0890fabb2075c5d929 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 09:10:18 +0100 Subject: [PATCH 29/70] Add forcesell market/limit distinction --- freqtrade/freqtradebot.py | 18 ++++++++---------- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 6 +++--- freqtrade/rpc/rpc.py | 10 +++++++--- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 57d5e0528..a6d1b36b9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -467,7 +467,7 @@ class FreqtradeBot(LoggingMixin): return False def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, *, - order_type: Optional[str] = None, buy_tag: Optional[str] = None) -> bool: + ordertype: Optional[str] = None, buy_tag: Optional[str] = None) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY @@ -510,8 +510,7 @@ class FreqtradeBot(LoggingMixin): f"{stake_amount} ...") amount = stake_amount / enter_limit_requested - if not order_type: - order_type = self.strategy.order_types['buy'] + order_type = ordertype or self.strategy.order_types['buy'] if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, @@ -866,7 +865,7 @@ class FreqtradeBot(LoggingMixin): logger.info( f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. ' f'Tag: {exit_tag if exit_tag is not None else "None"}') - self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag) + self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag=exit_tag) return True return False @@ -1079,7 +1078,10 @@ class FreqtradeBot(LoggingMixin): trade: Trade, limit: float, sell_reason: SellCheckTuple, - exit_tag: Optional[str] = None) -> bool: + *, + exit_tag: Optional[str] = None, + ordertype: Optional[str] = None, + ) -> bool: """ Executes a trade exit for the given trade and limit :param trade: Trade instance @@ -1117,14 +1119,10 @@ class FreqtradeBot(LoggingMixin): except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") - order_type = self.strategy.order_types[sell_type] + order_type = ordertype or self.strategy.order_types[sell_type] if sell_reason.sell_type == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") - if sell_reason.sell_type == SellType.FORCE_SELL: - # Force sells (default to the sell_type defined in the strategy, - # but we allow this value to be changed) - order_type = self.strategy.order_types.get("forcesell", order_type) amount = self._safe_exit_amount(trade.pair, trade.amount) time_in_force = self.strategy.order_time_in_force['sell'] diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index ed483b18d..d0e772848 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -288,6 +288,7 @@ class ForceBuyPayload(BaseModel): class ForceSellPayload(BaseModel): tradeid: str + ordertype: Optional[OrderTypeValues] class BlacklistPayload(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 6fc135820..1fd4ca74b 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -29,7 +29,7 @@ logger = logging.getLogger(__name__) # API version # Pre-1.1, no version was provided # Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen. -# 1.11: forcebuy accepts new option with ordertype +# 1.11: forcebuy and forcesell accept ordertype API_VERSION = 1.11 # Public API, requires no auth. @@ -130,7 +130,7 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g @router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading']) def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): - trade = rpc._rpc_forcebuy(payload.pair, payload.price, payload.ordertype) + trade = rpc._rpc_forcebuy(payload.pair, payload.price, payload.ordertype.value) if trade: return ForceBuyResponse.parse_obj(trade.to_json()) @@ -140,7 +140,7 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): @router.post('/forcesell', response_model=ResultMsg, tags=['trading']) def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_forcesell(payload.tradeid) + return rpc._rpc_forcesell(payload.tradeid, payload.ordertype.value) @router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index fc1c0c777..c21890b7d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -640,7 +640,7 @@ class RPC: return {'status': 'No more buy will occur from now. Run /reload_config to reset.'} - def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]: + def _rpc_forcesell(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]: """ Handler for forcesell . Sells the given trade at current price @@ -664,7 +664,11 @@ class RPC: current_rate = self._freqtrade.exchange.get_rate( trade.pair, refresh=False, side="sell") sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) - self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason) + order_type = ordertype or self._freqtrade.strategy.order_types.get( + "forcesell", self._freqtrade.strategy.order_types["sell"]) + + self._freqtrade.execute_trade_exit( + trade, current_rate, sell_reason, ordertype=order_type) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: @@ -724,7 +728,7 @@ class RPC: if not order_type: order_type = self._freqtrade.strategy.order_types.get( 'forcebuy', self._freqtrade.strategy.order_types['buy']) - if self._freqtrade.execute_entry(pair, stakeamount, price, order_type=order_type): + if self._freqtrade.execute_entry(pair, stakeamount, price, ordertype=order_type): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade From bc52b3db56b02f448ccef3d6a8220b01849fc9ed Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 09:26:14 +0100 Subject: [PATCH 30/70] Properly handle None values via API --- freqtrade/rpc/api_server/api_v1.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 1fd4ca74b..65b6941e2 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -130,7 +130,8 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g @router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading']) def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): - trade = rpc._rpc_forcebuy(payload.pair, payload.price, payload.ordertype.value) + ordertype = payload.ordertype.value if payload.ordertype else None + trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype) if trade: return ForceBuyResponse.parse_obj(trade.to_json()) @@ -140,7 +141,8 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): @router.post('/forcesell', response_model=ResultMsg, tags=['trading']) def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_forcesell(payload.tradeid, payload.ordertype.value) + ordertype = payload.ordertype.value if payload.ordertype else None + return rpc._rpc_forcesell(payload.tradeid, ordertype) @router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) From 6ca6f62509122d06fcbf4a9d435a8dc96a27bbad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 09:39:10 +0100 Subject: [PATCH 31/70] Remove duplicate code in optimize_reports --- freqtrade/optimize/optimize_reports.py | 50 +++----------------------- 1 file changed, 4 insertions(+), 46 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index c4002fcbe..dcd6b4e1f 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -46,20 +46,11 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]: '.2f', 'd', 's', 's'] -def _get_line_header(first_column: str, stake_currency: str) -> List[str]: +def _get_line_header(first_column: str, stake_currency: str, direction: str = 'Buys') -> List[str]: """ Generate header lines (goes in line with _generate_result_line()) """ - return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %', - f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', - 'Win Draw Loss Win%'] - - -def _get_line_header_sell(first_column: str, stake_currency: str) -> List[str]: - """ - Generate header lines (goes in line with _generate_result_line()) - """ - return [first_column, 'Sells', 'Avg Profit %', 'Cum Profit %', + return [first_column, direction, 'Avg Profit %', 'Cum Profit %', f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration', 'Win Draw Loss Win%'] @@ -156,7 +147,7 @@ def generate_tag_metrics(tag_type: str, if skip_nan and result['profit_abs'].isnull().all(): continue - tabular_data.append(_generate_tag_result_line(result, starting_balance, tag)) + tabular_data.append(_generate_result_line(result, starting_balance, tag)) # Sort by total profit %: tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True) @@ -168,39 +159,6 @@ def generate_tag_metrics(tag_type: str, return [] -def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict: - """ - Generate one result dict, with "first_column" as key. - """ - profit_sum = result['profit_ratio'].sum() - # (end-capital - starting capital) / starting capital - profit_total = result['profit_abs'].sum() / starting_balance - - return { - 'key': first_column, - 'trades': len(result), - 'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0, - 'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0, - 'profit_sum': profit_sum, - 'profit_sum_pct': round(profit_sum * 100.0, 2), - 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total': profit_total, - 'profit_total_pct': round(profit_total * 100.0, 2), - 'duration_avg': str(timedelta( - minutes=round(result['trade_duration'].mean())) - ) if not result.empty else '0:00', - # 'duration_max': str(timedelta( - # minutes=round(result['trade_duration'].max())) - # ) if not result.empty else '0:00', - # 'duration_min': str(timedelta( - # minutes=round(result['trade_duration'].min())) - # ) if not result.empty else '0:00', - 'wins': len(result[result['profit_abs'] > 0]), - 'draws': len(result[result['profit_abs'] == 0]), - 'losses': len(result[result['profit_abs'] < 0]), - } - - def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]: """ Generate small table outlining Backtest results @@ -631,7 +589,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr if(tag_type == "buy_tag"): headers = _get_line_header("TAG", stake_currency) else: - headers = _get_line_header_sell("TAG", stake_currency) + headers = _get_line_header("TAG", stake_currency, 'Sells') floatfmt = _get_line_floatfmt(stake_currency) output = [ [ From a629777890a9fd2d10cf39eff32ad999964a42ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 09:53:05 +0100 Subject: [PATCH 32/70] Improve test coverage in telegram module --- tests/rpc/test_rpc_telegram.py | 64 +++++++++++++++++++++++++++------- 1 file changed, 51 insertions(+), 13 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ce3b044be..6c32e59fc 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -24,6 +24,7 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC +from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.telegram import Telegram, authorized_only from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist) @@ -1186,8 +1187,8 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: assert fbuy_mock.call_count == 1 -def test_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: +def test_telegram_performance_handle(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1216,8 +1217,8 @@ def test_performance_handle(default_conf, update, ticker, fee, assert 'ETH/BTC\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] -def test_buy_tag_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: +def test_telegram_buy_tag_performance_handle(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -1240,15 +1241,27 @@ def test_buy_tag_performance_handle(default_conf, update, ticker, fee, trade.close_date = datetime.utcnow() trade.is_open = False - - telegram._buy_tag_performance(update=update, context=MagicMock()) + context = MagicMock() + telegram._buy_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Buy Tag Performance' in msg_mock.call_args_list[0][0][0] assert 'TESTBUY\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + context.args = [trade.pair] + telegram._buy_tag_performance(update=update, context=context) + assert msg_mock.call_count == 2 -def test_sell_reason_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: + msg_mock.reset_mock() + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_buy_tag_performance', + side_effect=RPCException('Error')) + telegram._buy_tag_performance(update=update, context=MagicMock()) + + assert msg_mock.call_count == 1 + assert "Error" in msg_mock.call_args_list[0][0][0] + + +def test_telegram_sell_reason_performance_handle(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -1271,15 +1284,27 @@ def test_sell_reason_performance_handle(default_conf, update, ticker, fee, trade.close_date = datetime.utcnow() trade.is_open = False - - telegram._sell_reason_performance(update=update, context=MagicMock()) + context = MagicMock() + telegram._sell_reason_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Sell Reason Performance' in msg_mock.call_args_list[0][0][0] assert 'TESTSELL\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0] + context.args = [trade.pair] + + telegram._sell_reason_performance(update=update, context=context) + assert msg_mock.call_count == 2 + + msg_mock.reset_mock() + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_sell_reason_performance', + side_effect=RPCException('Error')) + telegram._sell_reason_performance(update=update, context=MagicMock()) + + assert msg_mock.call_count == 1 + assert "Error" in msg_mock.call_args_list[0][0][0] -def test_mix_tag_performance_handle(default_conf, update, ticker, fee, - limit_buy_order, limit_sell_order, mocker) -> None: +def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee, + limit_buy_order, limit_sell_order, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -1305,12 +1330,25 @@ def test_mix_tag_performance_handle(default_conf, update, ticker, fee, trade.close_date = datetime.utcnow() trade.is_open = False - telegram._mix_tag_performance(update=update, context=MagicMock()) + context = MagicMock() + telegram._mix_tag_performance(update=update, context=context) assert msg_mock.call_count == 1 assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0] assert ('TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)' in msg_mock.call_args_list[0][0][0]) + context.args = [trade.pair] + telegram._mix_tag_performance(update=update, context=context) + assert msg_mock.call_count == 2 + + msg_mock.reset_mock() + mocker.patch('freqtrade.rpc.rpc.RPC._rpc_mix_tag_performance', + side_effect=RPCException('Error')) + telegram._mix_tag_performance(update=update, context=MagicMock()) + + assert msg_mock.call_count == 1 + assert "Error" in msg_mock.call_args_list[0][0][0] + def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( From 8c52ba3360667a7eb9611f0ddb83709d48bc14ef Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Sat, 27 Nov 2021 16:21:23 +0100 Subject: [PATCH 33/70] ShuffleFilterDetectLiveMode # Apply seed in backtesting mode to get comparable results, # but not in live modes to get a non-repeating order of pairs during live modes. --- freqtrade/plugins/pairlist/ShuffleFilter.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/ShuffleFilter.py b/freqtrade/plugins/pairlist/ShuffleFilter.py index 4d3dd29e3..834fc9a43 100644 --- a/freqtrade/plugins/pairlist/ShuffleFilter.py +++ b/freqtrade/plugins/pairlist/ShuffleFilter.py @@ -18,7 +18,15 @@ class ShuffleFilter(IPairList): pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - self._seed = pairlistconfig.get('seed') + # Apply seed in backtesting mode to get comparable results, + # but not in live modes to get a non-repeating order of pairs during live modes. + if config['runmode'].value in ('live', 'dry_run'): + self._seed = None + logger.info("live mode detected, not applying seed.") + else: + self._seed = pairlistconfig.get('seed') + logger.info("Backtesting mode detected, applying seed value: " + str(self._seed)) + self._random = random.Random(self._seed) @property From 2f0f576fce8af2d41e02136b759382d5746dff78 Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Sat, 27 Nov 2021 16:28:41 +0100 Subject: [PATCH 34/70] Update pairlists.md ShuffleFilter will automatically detect runmodes and apply the `seed` only for backtesting modes - if ad `seed` value is set. --- docs/includes/pairlists.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index bbfe74510..c6b320e62 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -254,10 +254,10 @@ Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - #### ShuffleFilter -Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority. +Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority. !!! Tip - You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order. + You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order. ShuffleFilter will automatically detect runmodes and apply the `seed` only for backtesting modes - if a `seed` value is set. #### SpreadFilter From b90303c9a3d2bdbbb46f4557a6b68bdee463bcd4 Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Sat, 27 Nov 2021 18:26:30 +0100 Subject: [PATCH 35/70] Update ShuffleFilter.py random.Random() is deprecated since 3.9 --- freqtrade/plugins/pairlist/ShuffleFilter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/ShuffleFilter.py b/freqtrade/plugins/pairlist/ShuffleFilter.py index 834fc9a43..3cefee236 100644 --- a/freqtrade/plugins/pairlist/ShuffleFilter.py +++ b/freqtrade/plugins/pairlist/ShuffleFilter.py @@ -27,7 +27,8 @@ class ShuffleFilter(IPairList): self._seed = pairlistconfig.get('seed') logger.info("Backtesting mode detected, applying seed value: " + str(self._seed)) - self._random = random.Random(self._seed) + # deprecated since 3.9 + #self._random = random.Random(self._seed) @property def needstickers(self) -> bool: @@ -54,6 +55,7 @@ class ShuffleFilter(IPairList): :return: new whitelist """ # Shuffle is done inplace - self._random.shuffle(pairlist) + random.seed(self._seed) + random.shuffle(pairlist) return pairlist From 409a80176320a7d934eb5159faf35cb2a1ae9989 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 19:30:49 +0100 Subject: [PATCH 36/70] Fix caching problem in refresh_ohlcv closes #5978 --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 19ad4e4b6..5fa852eb0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1294,7 +1294,7 @@ class Exchange: cached_pairs = [] # Gather coroutines to run for pair, timeframe in set(pair_list): - if ((pair, timeframe) not in self._klines + if ((pair, timeframe) not in self._klines or not cache or self._now_is_time_to_refresh(pair, timeframe)): if not since_ms and self.required_candle_call_count > 1: # Multiple calls for one pair - to get more history diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 12b11ff3d..b642b3fa2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1667,12 +1667,21 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: assert len(res) == len(pairs) assert exchange._api_async.fetch_ohlcv.call_count == 0 + exchange.required_candle_call_count = 1 assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, " f"timeframe {pairs[0][1]} ...", caplog) res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')], cache=False) assert len(res) == 3 + assert exchange._api_async.fetch_ohlcv.call_count == 3 + + # Test the same again, should NOT return from cache! + exchange._api_async.fetch_ohlcv.reset_mock() + res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')], + cache=False) + assert len(res) == 3 + assert exchange._api_async.fetch_ohlcv.call_count == 3 @pytest.mark.asyncio From 2b3e7eeb2136e1f50ff9de7573ed1cd82bb23d96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 19:41:36 +0100 Subject: [PATCH 37/70] Use Enum values within bot code --- freqtrade/plugins/pairlist/ShuffleFilter.py | 7 ++++--- tests/plugins/test_pairlist.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/freqtrade/plugins/pairlist/ShuffleFilter.py b/freqtrade/plugins/pairlist/ShuffleFilter.py index 3cefee236..dfdfdda73 100644 --- a/freqtrade/plugins/pairlist/ShuffleFilter.py +++ b/freqtrade/plugins/pairlist/ShuffleFilter.py @@ -5,6 +5,7 @@ import logging import random from typing import Any, Dict, List +from freqtrade.enums.runmode import RunMode from freqtrade.plugins.pairlist.IPairList import IPairList @@ -20,12 +21,12 @@ class ShuffleFilter(IPairList): # Apply seed in backtesting mode to get comparable results, # but not in live modes to get a non-repeating order of pairs during live modes. - if config['runmode'].value in ('live', 'dry_run'): + if config.get('runmode') in (RunMode.LIVE, RunMode.DRY_RUN): self._seed = None - logger.info("live mode detected, not applying seed.") + logger.info("Live mode detected, not applying seed.") else: self._seed = pairlistconfig.get('seed') - logger.info("Backtesting mode detected, applying seed value: " + str(self._seed)) + logger.info(f"Backtesting mode detected, applying seed value: {self._seed}") # deprecated since 3.9 #self._random = random.Random(self._seed) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 6333266aa..ba8e6c3c3 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -7,6 +7,7 @@ import pytest import time_machine from freqtrade.constants import AVAILABLE_PAIRLISTS +from freqtrade.enums.runmode import RunMode from freqtrade.exceptions import OperationalException from freqtrade.persistence import Trade from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist @@ -657,6 +658,22 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: assert log_has("PerformanceFilter is not available in this mode.", caplog) +def test_ShuffleFilter_init(mocker, whitelist_conf, caplog) -> None: + whitelist_conf['pairlists'] = [ + {"method": "StaticPairList"}, + {"method": "ShuffleFilter", "seed": 42} + ] + + exchange = get_patched_exchange(mocker, whitelist_conf) + PairListManager(exchange, whitelist_conf) + assert log_has("Backtesting mode detected, applying seed value: 42", caplog) + caplog.clear() + whitelist_conf['runmode'] = RunMode.DRY_RUN + PairListManager(exchange, whitelist_conf) + assert not log_has("Backtesting mode detected, applying seed value: 42", caplog) + assert log_has("Live mode detected, not applying seed.", caplog) + + @pytest.mark.usefixtures("init_persistence") def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None: whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC') From 6429205d3920c7c3a7f9c4ce85903ce11b2b4a3f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 19:53:37 +0100 Subject: [PATCH 38/70] Improve Notebook documentation to include Dataprovider fix #5975 --- docs/strategy_analysis_example.md | 4 +++- freqtrade/templates/strategy_analysis_example.ipynb | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index dd7e07824..90d8d8800 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -50,7 +50,9 @@ candles.head() ```python # Load strategy using values set above from freqtrade.resolvers import StrategyResolver +from freqtrade.data.dataprovider import DataProvider strategy = StrategyResolver.load_strategy(config) +strategy.dp = DataProvider(config, None, None) # Generate buy/sell signals using strategy df = strategy.analyze_ticker(candles, {'pair': pair}) @@ -228,7 +230,7 @@ graph = generate_candlestick_graph(pair=pair, # Show graph inline # graph.show() -# Render graph in a separate window +# Render graph in a seperate window graph.show(renderer="browser") ``` diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 99720ae6e..3b937d1c5 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -79,7 +79,9 @@ "source": [ "# Load strategy using values set above\n", "from freqtrade.resolvers import StrategyResolver\n", + "from freqtrade.data.dataprovider import DataProvider\n", "strategy = StrategyResolver.load_strategy(config)\n", + "strategy.dp = DataProvider(config, None, None)\n", "\n", "# Generate buy/sell signals using strategy\n", "df = strategy.analyze_ticker(candles, {'pair': pair})\n", From fd9bf2adb05a79dac902f7223bcfb935583846a6 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Sun, 28 Nov 2021 08:23:02 +0900 Subject: [PATCH 39/70] add weekly and monthly to valid keys --- freqtrade/rpc/telegram.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6c6f745e7..e6624f94d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -112,6 +112,7 @@ class Telegram(RPCHandler): r'/stats$', r'/count$', r'/locks$', r'/balance$', r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/edge$', + r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', r'/forcebuy$', r'/help$', r'/version$'] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] From fb6ae174b97536a589fd857b68699e0518c4662f Mon Sep 17 00:00:00 2001 From: Spat Date: Sun, 28 Nov 2021 11:42:57 +1100 Subject: [PATCH 40/70] Added raw config and retry config to webhook --- docs/webhook-config.md | 33 ++++++++++++++++++++++++-- freqtrade/rpc/webhook.py | 44 +++++++++++++++++++++++++---------- tests/rpc/test_rpc_webhook.py | 11 +++++++++ 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index ec944cb50..bea555385 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -50,7 +50,7 @@ Sample configuration (tested using IFTTT). The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert your event and key to the url. -You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use `"format": "form"` or `"format": "json"` respectively. Example configuration for Mattermost Cloud integration: +You can set the POST body format to Form-Encoded (default), JSON-Encoded, or raw data. Use `"format": "form"`, `"format": "json"`, or `"format": "raw"` respectively. Example configuration for Mattermost Cloud integration: ```json "webhook": { @@ -63,7 +63,36 @@ You can set the POST body format to Form-Encoded (default) or JSON-Encoded. Use }, ``` -The result would be POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel. +The result would be a POST request with e.g. `{"text":"Status: running"}` body and `Content-Type: application/json` header which results `Status: running` message in the Mattermost channel. + +When using the Form-Encoded or JSON-Encoded configuration you can configure any number of payload values, and both the key and value will be ouput in the POST request. However, when using the raw data format you can only configure one value and it **must** be named `"data"`. In this instance the data key will not be output in the POST request, only the value. For example: + +```json + "webhook": { + "enabled": true, + "url": "https://", + "format": "raw", + "webhookstatus": { + "data": "Status: {status}" + } + }, +``` + +The result would be a POST request with e.g. `Status: running` body and `Content-Type: text/plain` header. + +Optional parameters are available to enable automatic retries for webhook messages. The `webhook.retries` parameter can be set for the maximum number of retries the webhook request should attempt if it is unsuccessful (i.e. HTTP response status is not 200). By default this is set to `0` which is disabled. An additional `webhook.retry_delay` parameter can be set to specify the time in seconds between retry attempts. By default this is set to `0.1` (i.e. 100ms). Note that increasing the number of retries or retry delay may slow down the trader if there are connectivity issues with the webhook. Example configuration for retries: + +```json + "webhook": { + "enabled": true, + "url": "https://", + "retries": 3, + "retry_delay": 0.2, + "webhookstatus": { + "status": "Status: {status}" + } + }, +``` Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called. diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index b4c55649e..99077948e 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -2,6 +2,7 @@ This module manages webhook communication """ import logging +import time from typing import Any, Dict from requests import RequestException, post @@ -28,12 +29,16 @@ class Webhook(RPCHandler): super().__init__(rpc, config) self._url = self._config['webhook']['url'] - self._format = self._config['webhook'].get('format', 'form') + self._retries = self._config['webhook'].get('retries', 0) + self._retry_delay = self._config['webhook'].get('retry_delay', 0.1) - if self._format != 'form' and self._format != 'json': + if self._retries < 0: self._retries = 0 + if self._retry_delay < 0: self._retry_delay = 0 + + if not (self._format in ['form', 'json', 'raw']): raise NotImplementedError('Unknown webhook format `{}`, possible values are ' - '`form` (default) and `json`'.format(self._format)) + '`form` (default), `json`, and `raw`'.format(self._format)) def cleanup(self) -> None: """ @@ -77,13 +82,28 @@ class Webhook(RPCHandler): def _send_msg(self, payload: dict) -> None: """do the actual call to the webhook""" - try: - if self._format == 'form': - post(self._url, data=payload) - elif self._format == 'json': - post(self._url, json=payload) - else: - raise NotImplementedError('Unknown format: {}'.format(self._format)) + success = False + attempts = 0 + while not success and attempts <= self._retries: + if attempts: + if self._retry_delay: time.sleep(self._retry_delay) + logger.info("Retrying webhook...") - except RequestException as exc: - logger.warning("Could not call webhook url. Exception: %s", exc) + attempts += 1 + + try: + if self._format == 'form': + response = post(self._url, data=payload) + elif self._format == 'json': + response = post(self._url, json=payload) + elif self._format == 'raw': + response = post(self._url, data=payload['data'], headers={'Content-Type': 'text/plain'}) + else: + raise NotImplementedError('Unknown format: {}'.format(self._format)) + + """throw a RequestException if the post was not successful""" + response.raise_for_status() + success = True + + except RequestException as exc: + logger.warning("Could not call webhook url. Exception: %s", exc) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 04e63a3be..735d2ada2 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -292,3 +292,14 @@ def test__send_msg_with_json_format(default_conf, mocker, caplog): webhook._send_msg(msg) assert post.call_args[1] == {'json': msg} + +def test__send_msg_with_raw_format(default_conf, mocker, caplog): + default_conf["webhook"] = get_webhook_dict() + default_conf["webhook"]["format"] = "raw" + webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) + msg = {'data': 'Hello'} + post = MagicMock() + mocker.patch("freqtrade.rpc.webhook.post", post) + webhook._send_msg(msg) + + assert post.call_args[1] == {'data': msg['data'], 'headers': {'Content-Type': 'text/plain'}} From c7d10e2c7e85cabb3b34e8ffd0e3fbc1da4b121c Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Sun, 28 Nov 2021 19:05:02 +0100 Subject: [PATCH 41/70] delete unneeded comment --- freqtrade/plugins/pairlist/ShuffleFilter.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/plugins/pairlist/ShuffleFilter.py b/freqtrade/plugins/pairlist/ShuffleFilter.py index dfdfdda73..d0eff1b17 100644 --- a/freqtrade/plugins/pairlist/ShuffleFilter.py +++ b/freqtrade/plugins/pairlist/ShuffleFilter.py @@ -28,9 +28,6 @@ class ShuffleFilter(IPairList): self._seed = pairlistconfig.get('seed') logger.info(f"Backtesting mode detected, applying seed value: {self._seed}") - # deprecated since 3.9 - #self._random = random.Random(self._seed) - @property def needstickers(self) -> bool: """ From cf5ff9257d432d170af14c4881fc3d098caa817d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Nov 2021 19:39:43 +0100 Subject: [PATCH 42/70] Add plotconfig as property documentation and sample --- docs/plotting.md | 110 +++++++++++++----- freqtrade/templates/base_strategy.py.j2 | 1 + .../subtemplates/plot_config_full.j2 | 30 ++--- 3 files changed, 97 insertions(+), 44 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 9fae38504..b2d7654f6 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -164,16 +164,17 @@ The resulting plot will have the following elements: An advanced plot configuration can be specified in the strategy in the `plot_config` parameter. -Additional features when using plot_config include: +Additional features when using `plot_config` include: * Specify colors per indicator * Specify additional subplots -* Specify indicator pairs to fill area in between +* Specify indicator pairs to fill area in between The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult. It also allows multiple subplots to display both MACD and RSI at the same time. Plot type can be configured using `type` key. Possible types are: + * `scatter` corresponding to `plotly.graph_objects.Scatter` class (default). * `bar` corresponding to `plotly.graph_objects.Bar` class. @@ -182,40 +183,89 @@ Extra parameters to `plotly.graph_objects.*` constructor can be specified in `pl Sample configuration with inline comments explaining the process: ``` python - plot_config = { - 'main_plot': { - # Configuration for main plot indicators. - # Specifies `ema10` to be red, and `ema50` to be a shade of gray - 'ema10': {'color': 'red'}, - 'ema50': {'color': '#CCCCCC'}, - # By omitting color, a random color is selected. - 'sar': {}, - # fill area between senkou_a and senkou_b - 'senkou_a': { - 'color': 'green', #optional - 'fill_to': 'senkou_b', - 'fill_label': 'Ichimoku Cloud', #optional - 'fill_color': 'rgba(255,76,46,0.2)', #optional - }, - # plot senkou_b, too. Not only the area to it. - 'senkou_b': {} +@property +def plot_config(self): + """ + There are a lot of solutions how to build the return dictionary. + The only important point is the return value. + Example: + plot_config = {'main_plot': {}, 'subplots': {}} + + """ + plot_config = {} + plot_config['main_plot'] = { + # Configuration for main plot indicators. + # Assumes 2 parameters, emashort and emalong to be specified. + f'ema_{self.emashort.value}': {'color': 'red'}, + f'ema_{self.emalong.value}': {'color': '#CCCCCC'}, + # By omitting color, a random color is selected. + 'sar': {}, + # fill area between senkou_a and senkou_b + 'senkou_a': { + 'color': 'green', #optional + 'fill_to': 'senkou_b', + 'fill_label': 'Ichimoku Cloud', #optional + 'fill_color': 'rgba(255,76,46,0.2)', #optional }, - 'subplots': { - # Create subplot MACD - "MACD": { - 'macd': {'color': 'blue', 'fill_to': 'macdhist'}, - 'macdsignal': {'color': 'orange'}, - 'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}} - }, - # Additional subplot RSI - "RSI": { - 'rsi': {'color': 'red'} - } + # plot senkou_b, too. Not only the area to it. + 'senkou_b': {} + } + plot_config['subplots'] = { + # Create subplot MACD + "MACD": { + 'macd': {'color': 'blue', 'fill_to': 'macdhist'}, + 'macdsignal': {'color': 'orange'}, + 'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}} + }, + # Additional subplot RSI + "RSI": { + 'rsi': {'color': 'red'} } } + return plot_config ``` +??? Note "As attribute (former method)" + Assigning plot_config is also possible as Attribute (this used to be the default way). + This has the disadvantage that strategy parameters are not available, preventing certain configurations from working. + + ``` python + plot_config = { + 'main_plot': { + # Configuration for main plot indicators. + # Specifies `ema10` to be red, and `ema50` to be a shade of gray + 'ema10': {'color': 'red'}, + 'ema50': {'color': '#CCCCCC'}, + # By omitting color, a random color is selected. + 'sar': {}, + # fill area between senkou_a and senkou_b + 'senkou_a': { + 'color': 'green', #optional + 'fill_to': 'senkou_b', + 'fill_label': 'Ichimoku Cloud', #optional + 'fill_color': 'rgba(255,76,46,0.2)', #optional + }, + # plot senkou_b, too. Not only the area to it. + 'senkou_b': {} + }, + 'subplots': { + # Create subplot MACD + "MACD": { + 'macd': {'color': 'blue', 'fill_to': 'macdhist'}, + 'macdsignal': {'color': 'orange'}, + 'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}} + }, + # Additional subplot RSI + "RSI": { + 'rsi': {'color': 'red'} + } + } + } + + ``` + + !!! Note The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, `macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy. diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 7f5399672..035468d58 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -87,6 +87,7 @@ class {{ strategy }}(IStrategy): 'sell': 'gtc' } {{ plot_config | indent(4) }} + def informative_pairs(self): """ Define additional, informative pair/interval combinations to be cached from the exchange. diff --git a/freqtrade/templates/subtemplates/plot_config_full.j2 b/freqtrade/templates/subtemplates/plot_config_full.j2 index ab02c7892..e3f9e7ca0 100644 --- a/freqtrade/templates/subtemplates/plot_config_full.j2 +++ b/freqtrade/templates/subtemplates/plot_config_full.j2 @@ -1,18 +1,20 @@ -plot_config = { - # Main plot indicators (Moving averages, ...) - 'main_plot': { - 'tema': {}, - 'sar': {'color': 'white'}, - }, - 'subplots': { - # Subplots - each dict defines one additional plot - "MACD": { - 'macd': {'color': 'blue'}, - 'macdsignal': {'color': 'orange'}, +@property +def plot_config(self): + return { + # Main plot indicators (Moving averages, ...) + 'main_plot': { + 'tema': {}, + 'sar': {'color': 'white'}, }, - "RSI": { - 'rsi': {'color': 'red'}, + 'subplots': { + # Subplots - each dict defines one additional plot + "MACD": { + 'macd': {'color': 'blue'}, + 'macdsignal': {'color': 'orange'}, + }, + "RSI": { + 'rsi': {'color': 'red'}, + } } } -} From 0fa5bf54cd92c08a14e7ad2a194f7faa3f30f0ec Mon Sep 17 00:00:00 2001 From: Spat Date: Mon, 29 Nov 2021 10:30:41 +1100 Subject: [PATCH 43/70] Changed comment --- freqtrade/rpc/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 99077948e..f76d50b0e 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -101,7 +101,7 @@ class Webhook(RPCHandler): else: raise NotImplementedError('Unknown format: {}'.format(self._format)) - """throw a RequestException if the post was not successful""" + # Throw a RequestException if the post was not successful response.raise_for_status() success = True From 29180a1d2b2d1b02d99d46bcf6904aaa5a62bee5 Mon Sep 17 00:00:00 2001 From: Spat Date: Mon, 29 Nov 2021 10:48:35 +1100 Subject: [PATCH 44/70] Moved retry config to constants --- freqtrade/constants.py | 2 ++ freqtrade/rpc/webhook.py | 3 --- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index e775e39fc..51ded6c49 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -312,6 +312,8 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'enabled': {'type': 'boolean'}, + 'retries': {'type': 'integer', 'minimum': 0}, + 'retry_delay': {'type': 'number', 'minimum': 0}, 'webhookbuy': {'type': 'object'}, 'webhookbuycancel': {'type': 'object'}, 'webhooksell': {'type': 'object'}, diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index f76d50b0e..1973f212e 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -33,9 +33,6 @@ class Webhook(RPCHandler): self._retries = self._config['webhook'].get('retries', 0) self._retry_delay = self._config['webhook'].get('retry_delay', 0.1) - if self._retries < 0: self._retries = 0 - if self._retry_delay < 0: self._retry_delay = 0 - if not (self._format in ['form', 'json', 'raw']): raise NotImplementedError('Unknown webhook format `{}`, possible values are ' '`form` (default), `json`, and `raw`'.format(self._format)) From df09fe5df6740b34e49813e5833898528e7d08d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 03:01:20 +0000 Subject: [PATCH 45/70] Bump prompt-toolkit from 3.0.22 to 3.0.23 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.22 to 3.0.23. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.22...3.0.23) --- updated-dependencies: - dependency-name: prompt-toolkit dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a5af330af..4491fecd0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -41,6 +41,6 @@ psutil==5.8.0 colorama==0.4.4 # Building config files interactively questionary==1.10.0 -prompt-toolkit==3.0.22 +prompt-toolkit==3.0.23 # Extensions to datetime library python-dateutil==2.8.2 From e9e8023d732b3308b1a7b0a3d6e9adc1eed4eaf1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 03:01:27 +0000 Subject: [PATCH 46/70] Bump ccxt from 1.61.92 to 1.62.42 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.61.92 to 1.62.42. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.61.92...1.62.42) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a5af330af..64061ffcf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.21.4 pandas==1.3.4 pandas-ta==0.3.14b -ccxt==1.61.92 +ccxt==1.62.42 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.0 aiohttp==3.8.1 From 589c9f55e000f70c71c40617bf0b46619b688d6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 03:01:33 +0000 Subject: [PATCH 47/70] Bump scipy from 1.7.2 to 1.7.3 Bumps [scipy](https://github.com/scipy/scipy) from 1.7.2 to 1.7.3. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.7.2...v1.7.3) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index a3da8f0be..05ea21703 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.7.2 +scipy==1.7.3 scikit-learn==1.0.1 scikit-optimize==0.9.0 filelock==3.4.0 From c2a7b1930bfae63e58713f95b6953303c526db4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 03:01:36 +0000 Subject: [PATCH 48/70] Bump types-cachetools from 4.2.5 to 4.2.6 Bumps [types-cachetools](https://github.com/python/typeshed) from 4.2.5 to 4.2.6. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-cachetools dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4c06e657b..ebad74278 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,7 +20,7 @@ time-machine==2.4.0 nbconvert==6.3.0 # mypy types -types-cachetools==4.2.5 +types-cachetools==4.2.6 types-filelock==3.2.1 types-requests==2.26.0 types-tabulate==0.8.3 From b0b2fdba708a7493265cd556dd567894bc62fc5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 03:01:38 +0000 Subject: [PATCH 49/70] Bump aiofiles from 0.7.0 to 0.8.0 Bumps [aiofiles](https://github.com/Tinche/aiofiles) from 0.7.0 to 0.8.0. - [Release notes](https://github.com/Tinche/aiofiles/releases) - [Commits](https://github.com/Tinche/aiofiles/compare/v0.7.0...v0.8.0) --- updated-dependencies: - dependency-name: aiofiles dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a5af330af..828b5754e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ sdnotify==0.3.2 fastapi==0.70.0 uvicorn==0.15.0 pyjwt==2.3.0 -aiofiles==0.7.0 +aiofiles==0.8.0 psutil==5.8.0 # Support for colorized terminal output From 39c3175b69ae65292d88a8cca1f3cb9371b084ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 03:01:43 +0000 Subject: [PATCH 50/70] Bump types-python-dateutil from 2.8.2 to 2.8.3 Bumps [types-python-dateutil](https://github.com/python/typeshed) from 2.8.2 to 2.8.3. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-python-dateutil dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4c06e657b..a6f066be9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,4 +26,4 @@ types-requests==2.26.0 types-tabulate==0.8.3 # Extensions to datetime library -types-python-dateutil==2.8.2 \ No newline at end of file +types-python-dateutil==2.8.3 \ No newline at end of file From b81d768eb3b355edbfe574cb837971f80fbeb97a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 03:01:45 +0000 Subject: [PATCH 51/70] Bump types-requests from 2.26.0 to 2.26.1 Bumps [types-requests](https://github.com/python/typeshed) from 2.26.0 to 2.26.1. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 4c06e657b..34cb222d4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,7 +22,7 @@ nbconvert==6.3.0 # mypy types types-cachetools==4.2.5 types-filelock==3.2.1 -types-requests==2.26.0 +types-requests==2.26.1 types-tabulate==0.8.3 # Extensions to datetime library From 24997fb36ff7921dcb5f6acc4f1b33f37707fb64 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 03:01:50 +0000 Subject: [PATCH 52/70] Bump mkdocs-material from 7.3.6 to 8.0.1 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.3.6 to 8.0.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Upgrade guide](https://github.com/squidfunk/mkdocs-material/blob/master/docs/upgrade.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.3.6...8.0.1) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 772919436..351f45af6 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.3 -mkdocs-material==7.3.6 +mkdocs-material==8.0.1 mdx_truly_sane_lists==1.2 pymdown-extensions==9.1 From c126d2530aa8dc10f814e984275b061495d84aef Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Mon, 29 Nov 2021 14:32:33 +0900 Subject: [PATCH 53/70] Add few sentences on docs - Add warning that PrecisionFilter can't be used on backtest that use multiple strategies - Add note that not all pairlist handlers can be used on backtest --- docs/backtesting.md | 2 +- docs/includes/pairlists.md | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 49a94b05e..a49e4700a 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -115,7 +115,7 @@ The result of backtesting will confirm if your bot has better odds of making a p All profit calculations include fees, and freqtrade will use the exchange's default fees for the calculation. !!! Warning "Using dynamic pairlists for backtesting" - Using dynamic pairlists is possible, however it relies on the current market conditions - which will not reflect the historic status of the pairlist. + Using dynamic pairlists is possible (not all of the handlers are allowed to be used in backtest mode), however it relies on the current market conditions - which will not reflect the historic status of the pairlist. Also, when using pairlists other than StaticPairlist, reproducibility of backtesting-results cannot be guaranteed. Please read the [pairlists documentation](plugins.md#pairlists) for more information. diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index bbfe74510..29e20a32f 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -220,6 +220,9 @@ As this Filter uses past performance of the bot, it'll have some startup-period Filters low-value coins which would not allow setting stoplosses. +!!! Warning "Backtesting" + `PrecisionFilter` does not support backtesting mode using multiple strategies. + #### PriceFilter The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported: From 57e55eb93836d46c2e95f66f97fb2ec7c6bcc7af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Nov 2021 06:00:32 +0000 Subject: [PATCH 54/70] Bump time-machine from 2.4.0 to 2.4.1 Bumps [time-machine](https://github.com/adamchainz/time-machine) from 2.4.0 to 2.4.1. - [Release notes](https://github.com/adamchainz/time-machine/releases) - [Changelog](https://github.com/adamchainz/time-machine/blob/main/HISTORY.rst) - [Commits](https://github.com/adamchainz/time-machine/compare/2.4.0...2.4.1) --- updated-dependencies: - dependency-name: time-machine dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index d127eabcc..055a2a35d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,7 +14,7 @@ pytest-mock==3.6.1 pytest-random-order==1.0.4 isort==5.10.1 # For datetime mocking -time-machine==2.4.0 +time-machine==2.4.1 # Convert jupyter notebooks to markdown documents nbconvert==6.3.0 From 018407852a1cca078e0cbdb6c5b31f1f5eb914e2 Mon Sep 17 00:00:00 2001 From: Spat Date: Mon, 29 Nov 2021 18:17:59 +1100 Subject: [PATCH 55/70] Added missing webhook config params to constants --- freqtrade/constants.py | 6 ++++++ freqtrade/rpc/webhook.py | 4 ---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 51ded6c49..e074718ca 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -50,6 +50,8 @@ USERPATH_STRATEGIES = 'strategies' USERPATH_NOTEBOOKS = 'notebooks' TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] +WEBHOOK_FORMAT_OPTIONS = ['form', 'json', 'raw'] + ENV_VAR_PREFIX = 'FREQTRADE__' NON_OPEN_EXCHANGE_STATES = ('cancelled', 'canceled', 'closed', 'expired') @@ -312,12 +314,16 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'enabled': {'type': 'boolean'}, + 'url': {'type': 'string'}, + 'format': {'type': 'string', 'enum': WEBHOOK_FORMAT_OPTIONS, 'default': 'form'}, 'retries': {'type': 'integer', 'minimum': 0}, 'retry_delay': {'type': 'number', 'minimum': 0}, 'webhookbuy': {'type': 'object'}, 'webhookbuycancel': {'type': 'object'}, + 'webhookbuyfill': {'type': 'object'}, 'webhooksell': {'type': 'object'}, 'webhooksellcancel': {'type': 'object'}, + 'webhooksellfill': {'type': 'object'}, 'webhookstatus': {'type': 'object'}, }, }, diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 1973f212e..2a848787d 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -33,10 +33,6 @@ class Webhook(RPCHandler): self._retries = self._config['webhook'].get('retries', 0) self._retry_delay = self._config['webhook'].get('retry_delay', 0.1) - if not (self._format in ['form', 'json', 'raw']): - raise NotImplementedError('Unknown webhook format `{}`, possible values are ' - '`form` (default), `json`, and `raw`'.format(self._format)) - def cleanup(self) -> None: """ Cleanup pending module resources. From 2e5147745540686d04821d33063b5b186b564b4b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Nov 2021 19:32:16 +0100 Subject: [PATCH 56/70] Update mkdocs file to 8.0 --- mkdocs.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index b9c053324..9eebd75e3 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -81,8 +81,9 @@ markdown_extensions: - pymdownx.snippets: base_path: docs check_paths: true - - pymdownx.tabbed - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true - pymdownx.tasklist: custom_checkbox: true - mdx_truly_sane_lists From f8cb3d290188269808d6f3e88bd172fb526b88d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Nov 2021 19:52:40 +0100 Subject: [PATCH 57/70] Restore openAPI functioning --- freqtrade/rpc/api_server/api_schemas.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index d0e772848..98cc8e15a 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -132,13 +132,10 @@ class UnfilledTimeout(BaseModel): exit_timeout_count: Optional[int] -class OrderTypeValues(Enum): +class OrderTypeValues(str, Enum): limit = 'limit' market = 'market' - class Config: - use_enum_values = True - class OrderTypes(BaseModel): buy: OrderTypeValues From dfb148f8d7cdbe6f3a5f8ca345f54f74943538dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Nov 2021 19:54:54 +0100 Subject: [PATCH 58/70] Fix formatting --- freqtrade/rpc/webhook.py | 8 +++++--- tests/rpc/test_rpc_webhook.py | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 2a848787d..58b75769e 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -79,7 +79,8 @@ class Webhook(RPCHandler): attempts = 0 while not success and attempts <= self._retries: if attempts: - if self._retry_delay: time.sleep(self._retry_delay) + if self._retry_delay: + time.sleep(self._retry_delay) logger.info("Retrying webhook...") attempts += 1 @@ -90,10 +91,11 @@ class Webhook(RPCHandler): elif self._format == 'json': response = post(self._url, json=payload) elif self._format == 'raw': - response = post(self._url, data=payload['data'], headers={'Content-Type': 'text/plain'}) + response = post(self._url, data=payload['data'], + headers={'Content-Type': 'text/plain'}) else: raise NotImplementedError('Unknown format: {}'.format(self._format)) - + # Throw a RequestException if the post was not successful response.raise_for_status() success = True diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 735d2ada2..17d1baca9 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -293,6 +293,7 @@ def test__send_msg_with_json_format(default_conf, mocker, caplog): assert post.call_args[1] == {'json': msg} + def test__send_msg_with_raw_format(default_conf, mocker, caplog): default_conf["webhook"] = get_webhook_dict() default_conf["webhook"]["format"] = "raw" From 60eca8b1f1de636d19b5e6d2c07f412c8c25e866 Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Mon, 29 Nov 2021 20:35:43 +0100 Subject: [PATCH 59/70] revert to random object --- freqtrade/plugins/pairlist/ShuffleFilter.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/ShuffleFilter.py b/freqtrade/plugins/pairlist/ShuffleFilter.py index d0eff1b17..55cf9938f 100644 --- a/freqtrade/plugins/pairlist/ShuffleFilter.py +++ b/freqtrade/plugins/pairlist/ShuffleFilter.py @@ -28,6 +28,8 @@ class ShuffleFilter(IPairList): self._seed = pairlistconfig.get('seed') logger.info(f"Backtesting mode detected, applying seed value: {self._seed}") + self._random = random.Random(self._seed) + @property def needstickers(self) -> bool: """ @@ -53,7 +55,6 @@ class ShuffleFilter(IPairList): :return: new whitelist """ # Shuffle is done inplace - random.seed(self._seed) - random.shuffle(pairlist) + self._random.shuffle(pairlist) return pairlist From 85b1f6f6b34491ae88026789b3e765eba76a0b75 Mon Sep 17 00:00:00 2001 From: incrementby1 <91958753+incrementby1@users.noreply.github.com> Date: Mon, 29 Nov 2021 20:44:51 +0100 Subject: [PATCH 60/70] Update pairlists.md --- docs/includes/pairlists.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index c6b320e62..9ef4204f4 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -254,7 +254,7 @@ Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - #### ShuffleFilter -Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority. +Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority. !!! Tip You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order. ShuffleFilter will automatically detect runmodes and apply the `seed` only for backtesting modes - if a `seed` value is set. From 231b1e2f572c2017eee89cacdea1e126063d1065 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 06:58:32 +0100 Subject: [PATCH 61/70] Improve Async error message content --- freqtrade/exchange/exchange.py | 4 ++-- tests/exchange/test_exchange.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5fa852eb0..e25be9ae1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1263,7 +1263,7 @@ class Exchange: results = await asyncio.gather(*input_coro, return_exceptions=True) for res in results: if isinstance(res, Exception): - logger.warning("Async code raised an exception: %s", res.__class__.__name__) + logger.warning(f"Async code raised an exception: {repr(res)}") if raise_: raise continue @@ -1324,7 +1324,7 @@ class Exchange: # handle caching for res in results: if isinstance(res, Exception): - logger.warning("Async code raised an exception: %s", res.__class__.__name__) + logger.warning(f"Async code raised an exception: {repr(res)}") continue # Deconstruct tuple (has 3 elements) pair, timeframe, ticks = res diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index b642b3fa2..5a35675a8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1777,7 +1777,7 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): assert len(res) == 1 # Test that each is in list at least once as order is not guaranteed assert log_has("Error loading ETH/BTC. Result was [[]].", caplog) - assert log_has("Async code raised an exception: TypeError", caplog) + assert log_has("Async code raised an exception: TypeError()", caplog) def test_get_next_limit_in_list(): From f0abe218a2cbdcd0a7296b07894d23a5233ed39e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 07:02:49 +0100 Subject: [PATCH 62/70] Batch ohlcv requests to not overwelm ccxt's async throttler closes #6003 --- freqtrade/exchange/exchange.py | 43 ++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e25be9ae1..0ae78cf1b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1317,27 +1317,30 @@ class Exchange: ) cached_pairs.append((pair, timeframe)) - results = asyncio.get_event_loop().run_until_complete( - asyncio.gather(*input_coroutines, return_exceptions=True)) - results_df = {} - # handle caching - for res in results: - if isinstance(res, Exception): - logger.warning(f"Async code raised an exception: {repr(res)}") - continue - # Deconstruct tuple (has 3 elements) - pair, timeframe, ticks = res - # keeping last candle time as last refreshed time of the pair - if ticks: - self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 - # keeping parsed dataframe in cache - ohlcv_df = ohlcv_to_dataframe( - ticks, timeframe, pair=pair, fill_missing=True, - drop_incomplete=self._ohlcv_partial_candle) - results_df[(pair, timeframe)] = ohlcv_df - if cache: - self._klines[(pair, timeframe)] = ohlcv_df + # Chunk requests into batches of 100 to avoid overwelming ccxt Throttling + for input_coro in chunks(input_coroutines, 100): + results = asyncio.get_event_loop().run_until_complete( + asyncio.gather(*input_coro, return_exceptions=True)) + + # handle caching + for res in results: + if isinstance(res, Exception): + logger.warning(f"Async code raised an exception: {repr(res)}") + continue + # Deconstruct tuple (has 3 elements) + pair, timeframe, ticks = res + # keeping last candle time as last refreshed time of the pair + if ticks: + self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 + # keeping parsed dataframe in cache + ohlcv_df = ohlcv_to_dataframe( + ticks, timeframe, pair=pair, fill_missing=True, + drop_incomplete=self._ohlcv_partial_candle) + results_df[(pair, timeframe)] = ohlcv_df + if cache: + self._klines[(pair, timeframe)] = ohlcv_df + # Return cached klines for pair, timeframe in cached_pairs: results_df[(pair, timeframe)] = self.klines((pair, timeframe), copy=False) From 542963c7a60e699c1ebd0602ecd4b5be7a86f6a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 19:45:20 +0100 Subject: [PATCH 63/70] Reduce code complexity by combining buy and buy_fill methods --- docs/webhook-config.md | 5 ++++- freqtrade/freqtradebot.py | 26 ++++++-------------------- mkdocs.yml | 1 + 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index bea555385..40915c988 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -104,7 +104,8 @@ Possible parameters are: * `trade_id` * `exchange` * `pair` -* `limit` +* ~~`limit` # Deprecated - should no longer be used.~~ +* `open_rate` * `amount` * `open_date` * `stake_amount` @@ -146,6 +147,8 @@ Possible parameters are: * `stake_amount` * `stake_currency` * `fiat_currency` +* `order_type` +* `current_rate` * `buy_tag` ### Webhooksell diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a6d1b36b9..32f08c178 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -592,17 +592,19 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_enter(self, trade: Trade, order_type: str) -> None: + def _notify_enter(self, trade: Trade, order_type: Optional[str] = None, + fill: bool = False) -> None: """ Sends rpc notification when a buy occurred. """ msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY, + 'type': RPCMessageType.BUY_FILL if fill else RPCMessageType.BUY, 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, - 'limit': trade.open_rate, + 'limit': trade.open_rate, # Deprecated (?) + 'open_rate': trade.open_rate, 'order_type': order_type, 'stake_amount': trade.stake_amount, 'stake_currency': self.config['stake_currency'], @@ -641,22 +643,6 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) - def _notify_enter_fill(self, trade: Trade) -> None: - msg = { - 'trade_id': trade.id, - 'type': RPCMessageType.BUY_FILL, - 'buy_tag': trade.buy_tag, - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'open_rate': trade.open_rate, - 'stake_amount': trade.stake_amount, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': trade.amount, - 'open_date': trade.open_date, - } - self.rpc.send_msg(msg) - # # SELL / exit positions / close trades logic and methods # @@ -1312,7 +1298,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() elif not trade.open_order_id: # Buy fill - self._notify_enter_fill(trade) + self._notify_enter(trade, fill=True) return False diff --git a/mkdocs.yml b/mkdocs.yml index 9eebd75e3..fb1b80ebf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -86,4 +86,5 @@ markdown_extensions: alternate_style: true - pymdownx.tasklist: custom_checkbox: true + - pymdownx.tilde - mdx_truly_sane_lists From c22f381dfe69a2a9e3b5fabc3318c80d4c06153d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 20:46:47 +0100 Subject: [PATCH 64/70] Fix Schema issue closes #6010 --- freqtrade/rpc/api_server/api_schemas.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 98cc8e15a..c1720a836 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -126,9 +126,9 @@ class Daily(BaseModel): class UnfilledTimeout(BaseModel): - buy: int - sell: int - unit: str + buy: Optional[int] + sell: Optional[int] + unit: Optional[str] exit_timeout_count: Optional[int] From 5ce1eeecf5b7cdd892191c599ac257f7705cf70e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Nov 2021 20:19:59 +0100 Subject: [PATCH 65/70] Reorder messages to be sent in correct order buy first, then buy fill, sell first, then sell fill. --- freqtrade/freqtradebot.py | 30 ++++++++++++++++-------------- tests/rpc/test_rpc_telegram.py | 6 +++--- tests/test_freqtradebot.py | 2 +- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 32f08c178..7d8e0ec2f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -278,7 +278,8 @@ class FreqtradeBot(LoggingMixin): if order: logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id, - stoploss_order=order.ft_order_side == 'stoploss') + stoploss_order=order.ft_order_side == 'stoploss', + send_msg=False) trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() for trade in trades: @@ -286,7 +287,7 @@ class FreqtradeBot(LoggingMixin): order = trade.select_order('buy', False) if order: logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id) + self.update_trade_state(trade, order.order_id, send_msg=False) def handle_insufficient_funds(self, trade: Trade): """ @@ -308,7 +309,7 @@ class FreqtradeBot(LoggingMixin): order = trade.select_order('buy', False) if order: logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id) + self.update_trade_state(trade, order.order_id, send_msg=False) def refind_lost_order(self, trade): """ @@ -578,10 +579,6 @@ class FreqtradeBot(LoggingMixin): ) trade.orders.append(order_obj) - # Update fees if order is closed - if order_status == 'closed': - self.update_trade_state(trade, order_id, order) - Trade.query.session.add(trade) Trade.commit() @@ -590,6 +587,10 @@ class FreqtradeBot(LoggingMixin): self._notify_enter(trade, order_type) + # Update fees if order is closed + if order_status == 'closed': + self.update_trade_state(trade, order_id, order) + return True def _notify_enter(self, trade: Trade, order_type: Optional[str] = None, @@ -1140,16 +1141,16 @@ class FreqtradeBot(LoggingMixin): trade.sell_order_status = '' trade.close_rate_requested = limit trade.sell_reason = exit_tag or sell_reason.sell_reason - # In case of market sell orders the order can be closed immediately - if order.get('status', 'unknown') in ('closed', 'expired'): - self.update_trade_state(trade, trade.open_order_id, order) - Trade.commit() # Lock pair for one candle to prevent immediate re-buys self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') self._notify_exit(trade, order_type) + # In case of market sell orders the order can be closed immediately + if order.get('status', 'unknown') in ('closed', 'expired'): + self.update_trade_state(trade, trade.open_order_id, order) + Trade.commit() return True @@ -1246,13 +1247,14 @@ class FreqtradeBot(LoggingMixin): # def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None, - stoploss_order: bool = False) -> bool: + stoploss_order: bool = False, send_msg: bool = True) -> bool: """ Checks trades with open orders and updates the amount if necessary Handles closing both buy and sell orders. :param trade: Trade object of the trade we're analyzing :param order_id: Order-id of the order we're analyzing :param action_order: Already acquired order object + :param send_msg: Send notification - should always be True except in "recovery" methods :return: True if order has been cancelled without being filled partially, False otherwise """ if not order_id: @@ -1292,11 +1294,11 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: - if not stoploss_order and not trade.open_order_id: + if send_msg and not stoploss_order and not trade.open_order_id: self._notify_exit(trade, '', True) self.handle_protections(trade.pair) self.wallets.update() - elif not trade.open_order_id: + elif send_msg and not trade.open_order_id: # Buy fill self._notify_enter(trade, fill=True) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 6c32e59fc..6adce7b4d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -937,7 +937,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, telegram._forcesell(update=update, context=context) assert msg_mock.call_count == 4 - last_msg = msg_mock.call_args_list[-1][0][0] + last_msg = msg_mock.call_args_list[-2][0][0] assert { 'type': RPCMessageType.SELL, 'trade_id': 1, @@ -1001,7 +1001,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, assert msg_mock.call_count == 4 - last_msg = msg_mock.call_args_list[-1][0][0] + last_msg = msg_mock.call_args_list[-2][0][0] assert { 'type': RPCMessageType.SELL, 'trade_id': 1, @@ -1055,7 +1055,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None # Called for each trade 2 times assert msg_mock.call_count == 8 - msg = msg_mock.call_args_list[1][0][0] + msg = msg_mock.call_args_list[0][0][0] assert { 'type': RPCMessageType.SELL, 'trade_id': 1, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e5dae5461..dd1fcd6e2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2979,7 +2979,7 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, assert trade.close_profit == 0.09451372 assert rpc_mock.call_count == 3 - last_msg = rpc_mock.call_args_list[-1][0][0] + last_msg = rpc_mock.call_args_list[-2][0][0] assert { 'type': RPCMessageType.SELL, 'trade_id': 1, From 0375a083029ff1ba2a88944b0dd8d79b929651e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 1 Dec 2021 20:32:23 +0100 Subject: [PATCH 66/70] use to_hdf instead of HDFStore --- freqtrade/data/history/hdf5datahandler.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index dd60530aa..1ede3de98 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -61,10 +61,10 @@ class HDF5DataHandler(IDataHandler): filename = self._pair_data_filename(self._datadir, pair, timeframe) - ds = pd.HDFStore(filename, mode='a', complevel=9, complib='blosc') - ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date']) - - ds.close() + _data.loc[:, self._columns].to_hdf( + filename, key, mode='a', complevel=9, complib='blosc', + format='table', data_columns=['date'] + ) def _ohlcv_load(self, pair: str, timeframe: str, timerange: Optional[TimeRange] = None) -> pd.DataFrame: @@ -142,11 +142,11 @@ class HDF5DataHandler(IDataHandler): """ key = self._pair_trades_key(pair) - ds = pd.HDFStore(self._pair_trades_filename(self._datadir, pair), - mode='a', complevel=9, complib='blosc') - ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS), - format='table', data_columns=['timestamp']) - ds.close() + pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS).to_hdf( + self._pair_trades_filename(self._datadir, pair), key, + mode='a', complevel=9, complib='blosc', + format='table', data_columns=['timestamp'] + ) def trades_append(self, pair: str, data: TradeList): """ From 294c98ed5eda6d708769033b95a4d106c90e64e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 06:55:08 +0100 Subject: [PATCH 67/70] Document exchange.uid part of #6016 --- docs/configuration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index c4689f0a6..00ab66ceb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -126,9 +126,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String +| `exchange.uid` | API uid to use for the exchange. Only required when you are in production mode and for exchanges that use uid for API requests.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String | `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Supports regex pairs as `.*/BTC`. Not used by VolumePairList. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting. [More information](plugins.md#pairlists-and-pairlist-handlers).
**Datatype:** List -| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict +| `exchange.ccxt_config` | Additional CCXT parameters passed to both ccxt instances (sync and async). This is usually the correct place for additional ccxt configurations. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation). Please avoid adding exchange secrets here (use the dedicated fields instead), as they may be contained in logs.
**Datatype:** Dict | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
**Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer From d3ad4fb52e032f00e58077cd210d59be31fa2f7f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 19:17:00 +0100 Subject: [PATCH 68/70] Don't crash dry-run if orderbook side is empty closes #6018 --- freqtrade/exchange/exchange.py | 24 ++++++++++++++---------- tests/exchange/test_exchange.py | 6 ++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0ae78cf1b..22041ddef 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -685,16 +685,20 @@ class Exchange: if not self.exchange_has('fetchL2OrderBook'): return True ob = self.fetch_l2_order_book(pair, 1) - if side == 'buy': - price = ob['asks'][0][0] - logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}") - if limit >= price: - return True - else: - price = ob['bids'][0][0] - logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}") - if limit <= price: - return True + try: + if side == 'buy': + price = ob['asks'][0][0] + logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}") + if limit >= price: + return True + else: + price = ob['bids'][0][0] + logger.debug(f"{pair} checking dry sell-order: price={price}, limit={limit}") + if limit <= price: + return True + except IndexError: + # Ignore empty orderbooks when filling - can be filled with the next iteration. + pass return False def check_dry_limit_order_filled(self, order: Dict[str, Any]) -> Dict[str, Any]: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5a35675a8..b33e0cbb7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1026,6 +1026,12 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, assert order_closed['status'] == 'closed' assert order['fee'] + # Empty orderbook test + mocker.patch('freqtrade.exchange.Exchange.fetch_l2_order_book', + return_value={'asks': [], 'bids': []}) + exchange._dry_run_open_orders[order['id']]['status'] = 'open' + order_closed = exchange.fetch_dry_run_order(order['id']) + @pytest.mark.parametrize("side,rate,amount,endprice", [ # spread is 25.263-25.266 From ad5c8f601cb8455bcb16d7ae53eefab1f1d88a94 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Dec 2021 20:19:22 +0100 Subject: [PATCH 69/70] Simplify datahandler classes by exploiting commonalities --- freqtrade/data/history/hdf5datahandler.py | 40 ++--------------------- freqtrade/data/history/idatahandler.py | 32 ++++++++++++++++-- freqtrade/data/history/jsondatahandler.py | 24 -------------- 3 files changed, 33 insertions(+), 63 deletions(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index 1ede3de98..49fac99ea 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -6,7 +6,6 @@ from typing import List, Optional import numpy as np import pandas as pd -from freqtrade import misc from freqtrade.configuration import TimeRange from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, ListPairsWithTimeframes, TradeList) @@ -99,19 +98,6 @@ class HDF5DataHandler(IDataHandler): 'low': 'float', 'close': 'float', 'volume': 'float'}) return pairdata - def ohlcv_purge(self, pair: str, timeframe: str) -> bool: - """ - Remove data for this pair - :param pair: Delete data for this pair. - :param timeframe: Timeframe (e.g. "5m") - :return: True when deleted, false if file did not exist. - """ - filename = self._pair_data_filename(self._datadir, pair, timeframe) - if filename.exists(): - filename.unlink() - return True - return False - def ohlcv_append(self, pair: str, timeframe: str, data: pd.DataFrame) -> None: """ Append data to existing data structures @@ -180,17 +166,9 @@ class HDF5DataHandler(IDataHandler): trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None}) return trades.values.tolist() - def trades_purge(self, pair: str) -> bool: - """ - Remove data for this pair - :param pair: Delete data for this pair. - :return: True when deleted, false if file did not exist. - """ - filename = self._pair_trades_filename(self._datadir, pair) - if filename.exists(): - filename.unlink() - return True - return False + @classmethod + def _get_file_extension(cls): + return "h5" @classmethod def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str: @@ -199,15 +177,3 @@ class HDF5DataHandler(IDataHandler): @classmethod def _pair_trades_key(cls, pair: str) -> str: return f"{pair}/trades" - - @classmethod - def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path: - pair_s = misc.pair_to_filename(pair) - filename = datadir.joinpath(f'{pair_s}-{timeframe}.h5') - return filename - - @classmethod - def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path: - pair_s = misc.pair_to_filename(pair) - filename = datadir.joinpath(f'{pair_s}-trades.h5') - return filename diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 05052b2d7..578d0b5bf 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -12,6 +12,7 @@ from typing import List, Optional, Type from pandas import DataFrame +from freqtrade import misc from freqtrade.configuration import TimeRange from freqtrade.constants import ListPairsWithTimeframes, TradeList from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe @@ -26,6 +27,13 @@ class IDataHandler(ABC): def __init__(self, datadir: Path) -> None: self._datadir = datadir + @classmethod + def _get_file_extension(cls) -> str: + """ + Get file extension for this particular datahandler + """ + raise NotImplementedError() + @abstractclassmethod def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes: """ @@ -70,7 +78,6 @@ class IDataHandler(ABC): :return: DataFrame with ohlcv data, or empty DataFrame """ - @abstractmethod def ohlcv_purge(self, pair: str, timeframe: str) -> bool: """ Remove data for this pair @@ -78,6 +85,11 @@ class IDataHandler(ABC): :param timeframe: Timeframe (e.g. "5m") :return: True when deleted, false if file did not exist. """ + filename = self._pair_data_filename(self._datadir, pair, timeframe) + if filename.exists(): + filename.unlink() + return True + return False @abstractmethod def ohlcv_append(self, pair: str, timeframe: str, data: DataFrame) -> None: @@ -123,13 +135,17 @@ class IDataHandler(ABC): :return: List of trades """ - @abstractmethod def trades_purge(self, pair: str) -> bool: """ Remove data for this pair :param pair: Delete data for this pair. :return: True when deleted, false if file did not exist. """ + filename = self._pair_trades_filename(self._datadir, pair) + if filename.exists(): + filename.unlink() + return True + return False def trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList: """ @@ -141,6 +157,18 @@ class IDataHandler(ABC): """ return trades_remove_duplicates(self._trades_load(pair, timerange=timerange)) + @classmethod + def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path: + pair_s = misc.pair_to_filename(pair) + filename = datadir.joinpath(f'{pair_s}-{timeframe}.{cls._get_file_extension()}') + return filename + + @classmethod + def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path: + pair_s = misc.pair_to_filename(pair) + filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}') + return filename + def ohlcv_load(self, pair, timeframe: str, timerange: Optional[TimeRange] = None, fill_missing: bool = True, diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 24d6e814b..ccefc8356 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -174,34 +174,10 @@ class JsonDataHandler(IDataHandler): pass return tradesdata - def trades_purge(self, pair: str) -> bool: - """ - Remove data for this pair - :param pair: Delete data for this pair. - :return: True when deleted, false if file did not exist. - """ - filename = self._pair_trades_filename(self._datadir, pair) - if filename.exists(): - filename.unlink() - return True - return False - - @classmethod - def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path: - pair_s = misc.pair_to_filename(pair) - filename = datadir.joinpath(f'{pair_s}-{timeframe}.{cls._get_file_extension()}') - return filename - @classmethod def _get_file_extension(cls): return "json.gz" if cls._use_zip else "json" - @classmethod - def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path: - pair_s = misc.pair_to_filename(pair) - filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}') - return filename - class JsonGzDataHandler(JsonDataHandler): From d09a30cc671dc9b2b0f3626e8194b0b727545305 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Dec 2021 15:33:58 +0100 Subject: [PATCH 70/70] OrderTypeValues should be in enums --- freqtrade/enums/__init__.py | 1 + freqtrade/enums/ordertypevalue.py | 6 ++++++ freqtrade/rpc/api_server/api_schemas.py | 7 +------ 3 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 freqtrade/enums/ordertypevalue.py diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index d803baf31..eab483db3 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 from freqtrade.enums.backteststate import BacktestState +from freqtrade.enums.ordertypevalue import OrderTypeValues from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.selltype import SellType diff --git a/freqtrade/enums/ordertypevalue.py b/freqtrade/enums/ordertypevalue.py new file mode 100644 index 000000000..9bb716171 --- /dev/null +++ b/freqtrade/enums/ordertypevalue.py @@ -0,0 +1,6 @@ +from enum import Enum + + +class OrderTypeValues(str, Enum): + limit = 'limit' + market = 'market' diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index c1720a836..f9389d810 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -1,10 +1,10 @@ from datetime import date, datetime -from enum import Enum from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.enums import OrderTypeValues class Ping(BaseModel): @@ -132,11 +132,6 @@ class UnfilledTimeout(BaseModel): exit_timeout_count: Optional[int] -class OrderTypeValues(str, Enum): - limit = 'limit' - market = 'market' - - class OrderTypes(BaseModel): buy: OrderTypeValues sell: OrderTypeValues