diff --git a/config_full.json.example b/config_full.json.example
index 6aeb756f3..bc9f33f96 100644
--- a/config_full.json.example
+++ b/config_full.json.example
@@ -178,7 +178,9 @@
"sell_fill": "on",
"buy_cancel": "on",
"sell_cancel": "on"
- }
+ },
+ "reload": true,
+ "balance_dust_level": 0.01
},
"api_server": {
"enabled": false,
diff --git a/docker/Dockerfile.aarch64 b/docker/Dockerfile.aarch64
deleted file mode 100644
index e5d3f0ee9..000000000
--- a/docker/Dockerfile.aarch64
+++ /dev/null
@@ -1,58 +0,0 @@
-FROM --platform=linux/arm64/v8 python:3.9.4-slim-buster as base
-
-# Setup env
-ENV LANG C.UTF-8
-ENV LC_ALL C.UTF-8
-ENV PYTHONDONTWRITEBYTECODE 1
-ENV PYTHONFAULTHANDLER 1
-ENV PATH=/home/ftuser/.local/bin:$PATH
-ENV FT_APP_ENV="docker"
-
-# Prepare environment
-RUN mkdir /freqtrade \
- && apt-get update \
- && apt-get -y install sudo libatlas3-base curl sqlite3 libhdf5-serial-dev \
- && apt-get clean \
- && useradd -u 1000 -G sudo -U -m ftuser \
- && chown ftuser:ftuser /freqtrade \
- # Allow sudoers
- && echo "ftuser ALL=(ALL) NOPASSWD: /bin/chown" >> /etc/sudoers
-
-WORKDIR /freqtrade
-
-# Install dependencies
-FROM base as python-deps
-RUN apt-get update \
- && apt-get -y install build-essential libssl-dev git libffi-dev libgfortran5 pkg-config cmake gcc \
- && apt-get clean \
- && pip install --upgrade pip
-
-# Install TA-lib
-COPY build_helpers/* /tmp/
-RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
-ENV LD_LIBRARY_PATH /usr/local/lib
-
-# Install dependencies
-COPY --chown=ftuser:ftuser requirements.txt requirements-hyperopt.txt /freqtrade/
-USER ftuser
-RUN pip install --user --no-cache-dir numpy \
- && pip install --user --no-cache-dir -r requirements-hyperopt.txt
-
-# Copy dependencies to runtime-image
-FROM base as runtime-image
-COPY --from=python-deps /usr/local/lib /usr/local/lib
-ENV LD_LIBRARY_PATH /usr/local/lib
-
-COPY --from=python-deps --chown=ftuser:ftuser /home/ftuser/.local /home/ftuser/.local
-
-USER ftuser
-# Install and execute
-COPY --chown=ftuser:ftuser . /freqtrade/
-
-RUN pip install -e . --user --no-cache-dir --no-build-isolation\
- && mkdir /freqtrade/user_data/ \
- && freqtrade install-ui
-
-ENTRYPOINT ["freqtrade"]
-# Default to trade mode
-CMD [ "trade" ]
diff --git a/docs/backtesting.md b/docs/backtesting.md
index 26642ef8c..4899b1dad 100644
--- a/docs/backtesting.md
+++ b/docs/backtesting.md
@@ -284,7 +284,7 @@ A backtesting result will look like that:
| Backtesting to | 2019-05-01 00:00:00 |
| Max open trades | 3 |
| | |
-| Total trades | 429 |
+| Total/Daily Avg Trades| 429 / 3.575 |
| Starting balance | 0.01000000 BTC |
| Final balance | 0.01762792 BTC |
| Absolute profit | 0.00762792 BTC |
@@ -373,12 +373,11 @@ It contains some useful key metrics about performance of your strategy on backte
| Backtesting to | 2019-05-01 00:00:00 |
| Max open trades | 3 |
| | |
-| Total trades | 429 |
+| Total/Daily Avg Trades| 429 / 3.575 |
| Starting balance | 0.01000000 BTC |
| Final balance | 0.01762792 BTC |
| Absolute profit | 0.00762792 BTC |
| Total profit % | 76.2% |
-| Trades per day | 3.575 |
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | |
@@ -409,12 +408,11 @@ It contains some useful key metrics about performance of your strategy on backte
- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option).
- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - or number of pairs in the pairlist (whatever is lower).
-- `Total trades`: Identical to the total trades of the backtest output table.
+- `Total/Daily Avg Trades`: Identical to the total trades of the backtest output table / Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line).
- `Final balance`: Final balance - starting balance + absolute profit.
- `Absolute profit`: Profit made in stake currency.
- `Total profit %`: Total profit. Aligned to the `TOTAL` row's `Tot Profit %` from the first table. Calculated as `(End capital − Starting capital) / Starting capital`.
-- `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy).
- `Avg. stake amount`: Average stake amount, either `stake_amount` or the average when using dynamic stake amount.
- `Total trade volume`: Volume generated on the exchange to reach the above profit.
- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`.
@@ -446,6 +444,7 @@ Since backtesting lacks some detailed information about what happens within a ca
- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes
- Low happens before high for stoploss, protecting capital first
- Trailing stoploss
+ - Trailing Stoploss is only adjusted if it's below the candle's low (otherwise it would be triggered)
- High happens first - adjusting stoploss
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly)
- ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies
diff --git a/docs/configuration.md b/docs/configuration.md
index ef6f34094..8b85e9e96 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -102,10 +102,11 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
*Defaults to `60` minutes.*
**Datatype:** Positive Integer
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
*Defaults to `false`
**Datatype:** Boolean
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.
*Defaults to `false`
**Datatype:** Boolean
+| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.
*Defaults to `false`
**Datatype:** Boolean
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
| `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
*Defaults to `true`.*
**Datatype:** Boolean
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers).
*Defaults to `StaticPairList`.*
**Datatype:** List of Dicts
-| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** List of Dicts
+| `protections` | Define one or more protections to be used. [More information](plugins.md#protections).
**Datatype:** List of Dicts
| `telegram.enabled` | Enable the usage of Telegram.
**Datatype:** Boolean
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
**Keep it in secret, do not disclose publicly.**
**Datatype:** String
@@ -156,7 +157,6 @@ Values set in the configuration file always overwrite values set in the strategy
* `order_time_in_force`
* `unfilledtimeout`
* `disable_dataframe_checks`
-* `protections`
* `use_sell_signal` (ask_strategy)
* `sell_profit_only` (ask_strategy)
* `sell_profit_offset` (ask_strategy)
diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md
index 3a85aa885..cb66fc7e2 100644
--- a/docs/docker_quickstart.md
+++ b/docs/docker_quickstart.md
@@ -98,7 +98,7 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
image: freqtradeorg/freqtrade:custom_arm64
build:
context: .
- dockerfile: "./docker/Dockerfile.aarch64"
+ dockerfile: "Dockerfile"
```
The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image.
diff --git a/docs/includes/protections.md b/docs/includes/protections.md
index 6bc57153e..3ea2dde61 100644
--- a/docs/includes/protections.md
+++ b/docs/includes/protections.md
@@ -8,7 +8,6 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
!!! Note
Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance.
- To align your protection with your strategy, you can define protections in the strategy.
!!! Tip
Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term).
@@ -47,16 +46,16 @@ This applies across all pairs, unless `only_per_pair` is set to true, which will
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.
-```json
-"protections": [
+``` python
+protections = [
{
"method": "StoplossGuard",
"lookback_period_candles": 24,
"trade_limit": 4,
"stop_duration_candles": 4,
- "only_per_pair": false
+ "only_per_pair": False
}
-],
+]
```
!!! Note
@@ -69,8 +68,8 @@ The below example stops trading for all pairs for 4 candles after the last trade
The below sample stops trading for 12 candles if max-drawdown is > 20% considering all pairs - with a minimum of `trade_limit` trades - within the last 48 candles. If desired, `lookback_period` and/or `stop_duration` can be used.
-```json
-"protections": [
+``` python
+protections = [
{
"method": "MaxDrawdown",
"lookback_period_candles": 48,
@@ -78,7 +77,7 @@ The below sample stops trading for 12 candles if max-drawdown is > 20% consideri
"stop_duration_candles": 12,
"max_allowed_drawdown": 0.2
},
-],
+]
```
#### Low Profit Pairs
@@ -88,8 +87,8 @@ If that ratio is below `required_profit`, that pair will be locked for `stop_dur
The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles.
-```json
-"protections": [
+``` python
+protections = [
{
"method": "LowProfitPairs",
"lookback_period_candles": 6,
@@ -97,7 +96,7 @@ The below example will stop trading a pair for 60 minutes if the pair does not h
"stop_duration": 60,
"required_profit": 0.02
}
-],
+]
```
#### Cooldown Period
@@ -106,13 +105,13 @@ The below example will stop trading a pair for 60 minutes if the pair does not h
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".
-```json
-"protections": [
+``` python
+protections = [
{
"method": "CooldownPeriod",
"stop_duration_candles": 2
}
-],
+]
```
!!! Note
@@ -132,46 +131,6 @@ The below example assumes a timeframe of 1 hour:
* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`).
* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades.
-```json
-"timeframe": "1h",
-"protections": [
- {
- "method": "CooldownPeriod",
- "stop_duration_candles": 5
- },
- {
- "method": "MaxDrawdown",
- "lookback_period_candles": 48,
- "trade_limit": 20,
- "stop_duration_candles": 4,
- "max_allowed_drawdown": 0.2
- },
- {
- "method": "StoplossGuard",
- "lookback_period_candles": 24,
- "trade_limit": 4,
- "stop_duration_candles": 2,
- "only_per_pair": false
- },
- {
- "method": "LowProfitPairs",
- "lookback_period_candles": 6,
- "trade_limit": 2,
- "stop_duration_candles": 60,
- "required_profit": 0.02
- },
- {
- "method": "LowProfitPairs",
- "lookback_period_candles": 24,
- "trade_limit": 4,
- "stop_duration_candles": 2,
- "required_profit": 0.01
- }
- ],
-```
-
-You can use the same in your strategy, the syntax is only slightly different:
-
``` python
from freqtrade.strategy import IStrategy
diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
index 87ff38881..f5d9744b4 100644
--- a/docs/telegram-usage.md
+++ b/docs/telegram-usage.md
@@ -95,6 +95,7 @@ Example configuration showing the different settings:
"buy_fill": "off",
"sell_fill": "off"
},
+ "reload": true,
"balance_dust_level": 0.01
},
```
@@ -105,6 +106,7 @@ Example configuration showing the different settings:
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
+`reload` allows you to disable reload-buttons on selected messages.
## Create a custom keyboard (command shortcut buttons)
diff --git a/freqtrade/constants.py b/freqtrade/constants.py
index 259aa0e03..013e9df41 100644
--- a/freqtrade/constants.py
+++ b/freqtrade/constants.py
@@ -275,7 +275,8 @@ CONF_SCHEMA = {
'default': 'off'
},
}
- }
+ },
+ 'reload': {'type': 'boolean'},
},
'required': ['enabled', 'token', 'chat_id'],
},
diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py
index 0bcfa5e17..0c470cb24 100644
--- a/freqtrade/exchange/binance.py
+++ b/freqtrade/exchange/binance.py
@@ -68,6 +68,7 @@ class Binance(Exchange):
amount=amount, price=rate, params=params)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
+ self._log_exchange_response('create_stoploss_order', order)
return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 67676d4e0..07ac337fc 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -104,6 +104,7 @@ class Exchange:
logger.info('Instance is running with dry_run enabled')
logger.info(f"Using CCXT {ccxt.__version__}")
exchange_config = config['exchange']
+ self.log_responses = exchange_config.get('log_responses', False)
# Deep merge ft_has with default ft_has options
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
@@ -226,6 +227,11 @@ class Exchange:
"""exchange ccxt precisionMode"""
return self._api.precisionMode
+ def _log_exchange_response(self, endpoint, response) -> None:
+ """ Log exchange responses """
+ if self.log_responses:
+ logger.info(f"API {endpoint}: {response}")
+
def ohlcv_candle_limit(self, timeframe: str) -> int:
"""
Exchange ohlcv candle limit
@@ -622,8 +628,10 @@ class Exchange:
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
- return self._api.create_order(pair, ordertype, side,
- amount, rate_for_order, params)
+ order = self._api.create_order(pair, ordertype, side,
+ amount, rate_for_order, params)
+ self._log_exchange_response('create_order', order)
+ return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(
@@ -694,7 +702,9 @@ class Exchange:
if self._config['dry_run']:
return self.fetch_dry_run_order(order_id)
try:
- return self._api.fetch_order(order_id, pair)
+ order = self._api.fetch_order(order_id, pair)
+ self._log_exchange_response('fetch_order', order)
+ return order
except ccxt.OrderNotFound as e:
raise RetryableOrderError(
f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
@@ -744,7 +754,9 @@ class Exchange:
return {}
try:
- return self._api.cancel_order(order_id, pair)
+ order = self._api.cancel_order(order_id, pair)
+ self._log_exchange_response('cancel_order', order)
+ return order
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Could not cancel order. Message: {e}') from e
@@ -1042,6 +1054,7 @@ class Exchange:
pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000))
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
+ self._log_exchange_response('get_trades_for_order', matched_trades)
return matched_trades
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py
index 3184c2524..6cd549d60 100644
--- a/freqtrade/exchange/ftx.py
+++ b/freqtrade/exchange/ftx.py
@@ -69,6 +69,7 @@ class Ftx(Exchange):
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
amount=amount, params=params)
+ self._log_exchange_response('create_stoploss_order', order)
logger.info('stoploss order added for %s. '
'stop price: %s.', pair, stop_price)
return order
@@ -99,12 +100,14 @@ class Ftx(Exchange):
orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
order = [order for order in orders if order['id'] == order_id]
+ self._log_exchange_response('fetch_stoploss_order', order)
if len(order) == 1:
if order[0].get('status') == 'closed':
# Trigger order was triggered ...
real_order_id = order[0].get('info', {}).get('orderId')
order1 = self._api.fetch_order(real_order_id, pair)
+ self._log_exchange_response('fetch_stoploss_order1', order1)
# Fake type to stop - as this was really a stop order.
order1['id_stop'] = order1['id']
order1['id'] = order_id
@@ -131,7 +134,9 @@ class Ftx(Exchange):
if self._config['dry_run']:
return {}
try:
- return self._api.cancel_order(order_id, pair, params={'type': 'stop'})
+ order = self._api.cancel_order(order_id, pair, params={'type': 'stop'})
+ self._log_exchange_response('cancel_stoploss_order', order)
+ return order
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Could not cancel order. Message: {e}') from e
diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py
index 6f1fa409a..8f7cbe590 100644
--- a/freqtrade/exchange/kraken.py
+++ b/freqtrade/exchange/kraken.py
@@ -103,6 +103,7 @@ class Kraken(Exchange):
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
amount=amount, price=stop_price, params=params)
+ self._log_exchange_response('create_stoploss_order', order)
logger.info('stoploss order added for %s. '
'stop price: %s.', pair, stop_price)
return order
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index eb3193300..050880024 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -70,7 +70,7 @@ class FreqtradeBot(LoggingMixin):
PairLocks.timeframe = self.config['timeframe']
- self.protections = ProtectionManager(self.config)
+ self.protections = ProtectionManager(self.config, self.strategy.protections)
# RPC runs in separate threads, can start handling external commands just after
# initialization, even before Freqtradebot has a chance to start its throttling,
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index 5282b8588..5bc384e72 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -137,7 +137,7 @@ class Backtesting:
if hasattr(strategy, 'protections'):
conf = deepcopy(conf)
conf['protections'] = strategy.protections
- self.protections = ProtectionManager(conf)
+ self.protections = ProtectionManager(self.config, strategy.protections)
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
"""
@@ -225,6 +225,22 @@ class Backtesting:
# sell at open price.
return sell_row[OPEN_IDX]
+ # Special case: trailing triggers within same candle as trade opened. Assume most
+ # pessimistic price movement, which is moving just enough to arm stoploss and
+ # immediately going down to stop price.
+ if (sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0
+ and self.strategy.trailing_stop_positive):
+ if self.strategy.trailing_only_offset_is_reached:
+ # Worst case: price reaches stop_positive_offset and dives down.
+ stop_rate = (sell_row[OPEN_IDX] *
+ (1 + abs(self.strategy.trailing_stop_positive_offset) -
+ abs(self.strategy.trailing_stop_positive)))
+ else:
+ # Worst case: price ticks tiny bit above open and dives down.
+ stop_rate = sell_row[OPEN_IDX] * (1 - abs(self.strategy.trailing_stop_positive))
+ assert stop_rate < sell_row[HIGH_IDX]
+ return stop_rate
+
# Set close_rate to stoploss
return trade.stop_loss
elif sell.sell_type == (SellType.ROI):
diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py
old mode 100644
new mode 100755
index fb1a0f56a..9eee42a8d
--- a/freqtrade/optimize/hyperopt_tools.py
+++ b/freqtrade/optimize/hyperopt_tools.py
@@ -91,7 +91,7 @@ class HyperoptTools():
if print_json:
result_dict: Dict = {}
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
- HyperoptTools._params_update_for_json(result_dict, params, s)
+ HyperoptTools._params_update_for_json(result_dict, params, non_optimized, s)
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
else:
@@ -104,17 +104,24 @@ class HyperoptTools():
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
@staticmethod
- def _params_update_for_json(result_dict, params, space: str) -> None:
- if space in params:
+ def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
+ if (space in params) or (space in non_optimized):
space_params = HyperoptTools._space_params(params, space)
+ space_non_optimized = HyperoptTools._space_params(non_optimized, space)
+ all_space_params = space_params
+
+ # Merge non optimized params if there are any
+ if len(space_non_optimized) > 0:
+ all_space_params = {**space_params, **space_non_optimized}
+
if space in ['buy', 'sell']:
- result_dict.setdefault('params', {}).update(space_params)
+ result_dict.setdefault('params', {}).update(all_space_params)
elif space == 'roi':
# Convert keys in min_roi dict to strings because
# rapidjson cannot dump dicts with integer keys...
- result_dict['minimal_roi'] = {str(k): v for k, v in space_params.items()}
+ result_dict['minimal_roi'] = {str(k): v for k, v in all_space_params.items()}
else: # 'stoploss', 'trailing'
- result_dict.update(space_params)
+ result_dict.update(all_space_params)
@staticmethod
def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None:
diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py
index 6a5f8da5c..0c96d9da5 100644
--- a/freqtrade/optimize/optimize_reports.py
+++ b/freqtrade/optimize/optimize_reports.py
@@ -556,7 +556,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Backtesting to', strat_results['backtest_end']),
('Max open trades', strat_results['max_open_trades']),
('', ''), # Empty line to improve readability
- ('Total trades', strat_results['total_trades']),
+ ('Total/Daily Avg Trades',
+ f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"),
('Starting balance', round_coin_value(strat_results['starting_balance'],
strat_results['stake_currency'])),
('Final balance', round_coin_value(strat_results['final_balance'],
@@ -564,7 +565,6 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
strat_results['stake_currency'])),
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"),
- ('Trades per day', strat_results['trades_per_day']),
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
strat_results['stake_currency'])),
('Total trade volume', round_coin_value(strat_results['total_volume'],
diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py
index d1cdd2c5b..03f4760b8 100644
--- a/freqtrade/plugins/pairlistmanager.py
+++ b/freqtrade/plugins/pairlistmanager.py
@@ -83,7 +83,8 @@ class PairListManager():
pairlist = self._pairlist_handlers[0].gen_pairlist(tickers)
# Process all Pairlist Handlers in the chain
- for pairlist_handler in self._pairlist_handlers:
+ # except for the first one, which is the generator.
+ for pairlist_handler in self._pairlist_handlers[1:]:
pairlist = pairlist_handler.filter_pairlist(pairlist, tickers)
# Validation against blacklist happens after the chain of Pairlist Handlers
diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py
index a8edd4e4b..f33e5b4bc 100644
--- a/freqtrade/plugins/protectionmanager.py
+++ b/freqtrade/plugins/protectionmanager.py
@@ -15,11 +15,11 @@ logger = logging.getLogger(__name__)
class ProtectionManager():
- def __init__(self, config: dict) -> None:
+ def __init__(self, config: Dict, protections: List) -> None:
self._config = config
self._protection_handlers: List[IProtection] = []
- for protection_handler_config in self._config.get('protections', []):
+ for protection_handler_config in protections:
protection_handler = ProtectionResolver.load_protection(
protection_handler_config['method'],
config=config,
diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py
index 6484f900b..e76d1e3e5 100644
--- a/freqtrade/resolvers/strategy_resolver.py
+++ b/freqtrade/resolvers/strategy_resolver.py
@@ -113,7 +113,9 @@ class StrategyResolver(IResolver):
- Strategy
- default (if not None)
"""
- if attribute in config:
+ if (attribute in config
+ and not isinstance(getattr(type(strategy), 'my_property', None), property)):
+ # Ensure Properties are not overwritten
setattr(strategy, attribute, config[attribute])
logger.info("Override strategy '%s' with value in config file: %s.",
attribute, config[attribute])
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 6c97b3cda..0389c45aa 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -10,13 +10,13 @@ from datetime import date, datetime, timedelta
from html import escape
from itertools import chain
from math import isnan
-from typing import Any, Callable, Dict, List, Optional, Union, cast
+from typing import Any, Callable, Dict, List, Optional, Union
import arrow
from tabulate import tabulate
-from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ParseMode,
- ReplyKeyboardMarkup, Update)
-from telegram.error import NetworkError, TelegramError
+from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton,
+ ParseMode, ReplyKeyboardMarkup, Update)
+from telegram.error import BadRequest, NetworkError, TelegramError
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
from telegram.utils.helpers import escape_markdown
@@ -47,9 +47,13 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
update = kwargs.get('update') or args[0]
# Reject unauthorized messages
- chat_id = int(self._config['telegram']['chat_id'])
+ if update.callback_query:
+ cchat_id = int(update.callback_query.message.chat.id)
+ else:
+ cchat_id = int(update.message.chat_id)
- if int(update.message.chat_id) != chat_id:
+ chat_id = int(self._config['telegram']['chat_id'])
+ if cchat_id != chat_id:
logger.info(
'Rejected unauthorized message from: %s',
update.message.chat_id
@@ -91,7 +95,7 @@ class Telegram(RPCHandler):
Validates the keyboard configuration from telegram config
section.
"""
- self._keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = [
+ self._keyboard: List[List[Union[str, KeyboardButton]]] = [
['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'],
['/count', '/start', '/stop', '/help']
@@ -164,8 +168,21 @@ class Telegram(RPCHandler):
CommandHandler('help', self._help),
CommandHandler('version', self._version),
]
+ callbacks = [
+ CallbackQueryHandler(self._status_table, pattern='update_status_table'),
+ CallbackQueryHandler(self._daily, pattern='update_daily'),
+ CallbackQueryHandler(self._profit, pattern='update_profit'),
+ CallbackQueryHandler(self._balance, pattern='update_balance'),
+ CallbackQueryHandler(self._performance, pattern='update_performance'),
+ CallbackQueryHandler(self._count, pattern='update_count'),
+ CallbackQueryHandler(self._forcebuy_inline),
+ ]
for handle in handles:
self._updater.dispatcher.add_handler(handle)
+
+ for callback in callbacks:
+ self._updater.dispatcher.add_handler(callback)
+
self._updater.start_polling(
bootstrap_retries=-1,
timeout=30,
@@ -177,11 +194,6 @@ class Telegram(RPCHandler):
[h.command for h in handles]
)
- self._current_callback_query_handler: Optional[CallbackQueryHandler] = None
- self._callback_query_handlers = {
- 'forcebuy': CallbackQueryHandler(self._forcebuy_inline)
- }
-
def cleanup(self) -> None:
"""
Stops all running telegram threads.
@@ -409,7 +421,9 @@ class Telegram(RPCHandler):
# insert separators line between Total
lines = message.split("\n")
message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
- self._send_msg(f"
{message}", parse_mode=ParseMode.HTML) + self._send_msg(f"
{message}", parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_status_table", + query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -447,7 +461,8 @@ class Telegram(RPCHandler): ], tablefmt='simple') message = f'Daily Profit over the last {timescale} days:\n
{stats_tab}' - self._send_msg(message, parse_mode=ParseMode.HTML) + self._send_msg(message, parse_mode=ParseMode.HTML, reload_able=True, + callback_path="update_daily", query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -519,7 +534,8 @@ class Telegram(RPCHandler): if stats['closed_trade_count'] > 0: markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") - self._send_msg(markdown_msg) + self._send_msg(markdown_msg, reload_able=True, callback_path="update_profit", + query=update.callback_query) @authorized_only def _stats(self, update: Update, context: CallbackContext) -> None: @@ -606,7 +622,8 @@ class Telegram(RPCHandler): f"\t`{result['stake']}: {result['total']: .8f}`\n" f"\t`{result['symbol']}: " f"{round_coin_value(result['value'], result['symbol'], False)}`\n") - self._send_msg(output) + self._send_msg(output, reload_able=True, callback_path="update_balance", + query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -713,10 +730,10 @@ class Telegram(RPCHandler): self._forcebuy_action(pair, price) else: whitelist = self._rpc._rpc_whitelist()['whitelist'] - pairs = [InlineKeyboardButton(pair, callback_data=pair) for pair in whitelist] - self._send_inline_msg("Which pair?", - keyboard=self._layout_inline_keyboard(pairs), - callback_query_handler='forcebuy') + pairs = [InlineKeyboardButton(text=pair, callback_data=pair) for pair in whitelist] + + self._send_msg(msg="Which pair?", + keyboard=self._layout_inline_keyboard(pairs)) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: @@ -800,7 +817,9 @@ class Telegram(RPCHandler): else: output += stat_line - self._send_msg(output, parse_mode=ParseMode.HTML) + self._send_msg(output, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_performance", + query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -820,7 +839,9 @@ class Telegram(RPCHandler): tablefmt='simple') message = "
{}".format(message) logger.debug(message) - self._send_msg(message, parse_mode=ParseMode.HTML) + self._send_msg(message, parse_mode=ParseMode.HTML, + reload_able=True, callback_path="update_count", + query=update.callback_query) except RPCException as e: self._send_msg(str(e)) @@ -1052,29 +1073,42 @@ class Telegram(RPCHandler): f"*Current state:* `{val['state']}`" ) - def _send_inline_msg(self, msg: str, callback_query_handler, - parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, - keyboard: List[List[InlineKeyboardButton]] = None, ) -> None: - """ - Send given markdown message - :param msg: message - :param bot: alternative bot - :param parse_mode: telegram parse mode - :return: None - """ - if self._current_callback_query_handler: - self._updater.dispatcher.remove_handler(self._current_callback_query_handler) - self._current_callback_query_handler = self._callback_query_handlers[callback_query_handler] - self._updater.dispatcher.add_handler(self._current_callback_query_handler) + def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "", + reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None: + if reload_able: + reply_markup = InlineKeyboardMarkup([ + [InlineKeyboardButton("Refresh", callback_data=callback_path)], + ]) + else: + reply_markup = InlineKeyboardMarkup([[]]) + msg += "\nUpdated: {}".format(datetime.now().ctime()) + if not query.message: + return + chat_id = query.message.chat_id + message_id = query.message.message_id - self._send_msg(msg, parse_mode, disable_notification, - cast(List[List[Union[str, KeyboardButton, InlineKeyboardButton]]], keyboard), - reply_markup=InlineKeyboardMarkup) + try: + self._updater.bot.edit_message_text( + chat_id=chat_id, + message_id=message_id, + text=msg, + parse_mode=parse_mode, + reply_markup=reply_markup + ) + except BadRequest as e: + if 'not modified' in e.message.lower(): + pass + else: + logger.warning('TelegramError: %s', e.message) + except TelegramError as telegram_err: + logger.warning('TelegramError: %s! Giving up on that message.', telegram_err.message) def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False, - keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = None, - reply_markup=ReplyKeyboardMarkup) -> None: + keyboard: List[List[InlineKeyboardButton]] = None, + callback_path: str = "", + reload_able: bool = False, + query: Optional[CallbackQuery] = None) -> None: """ Send given markdown message :param msg: message @@ -1082,9 +1116,19 @@ class Telegram(RPCHandler): :param parse_mode: telegram parse mode :return: None """ - if keyboard is None: - keyboard = self._keyboard - reply_markup = reply_markup(keyboard, resize_keyboard=True) + reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup] + if query: + self._update_msg(query=query, msg=msg, parse_mode=parse_mode, + callback_path=callback_path, reload_able=reload_able) + return + if reload_able and self._config['telegram'].get('reload', True): + reply_markup = InlineKeyboardMarkup([ + [InlineKeyboardButton("Refresh", callback_data=callback_path)]]) + else: + if keyboard is not None: + reply_markup = InlineKeyboardMarkup(keyboard, resize_keyboard=True) + else: + reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True) try: try: self._updater.bot.send_message( diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b03003728..a46a8f952 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -107,7 +107,7 @@ class IStrategy(ABC, HyperStrategyMixin): startup_candle_count: int = 0 # Protections - protections: List + protections: List = [] # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) @@ -453,18 +453,25 @@ class IStrategy(ABC, HyperStrategyMixin): """ Ensure dataframe (length, last candle) was not modified, and has all elements we need. """ + message_template = "Dataframe returned from strategy has mismatching {}." message = "" - if df_len != len(dataframe): - message = "length" + if dataframe is None: + message = "No dataframe returned (return statement missing?)." + elif 'buy' not in dataframe: + message = "Buy column not set." + elif 'sell' not in dataframe: + message = "Sell column not set." + elif df_len != len(dataframe): + message = message_template.format("length") elif df_close != dataframe["close"].iloc[-1]: - message = "last close price" + message = message_template.format("last close price") elif df_date != dataframe["date"].iloc[-1]: - message = "last date" + message = message_template.format("last date") if message: if self.disable_dataframe_checks: - logger.warning(f"Dataframe returned from strategy has mismatching {message}.") + logger.warning(message) else: - raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.") + raise StrategyError(message) def get_signal(self, pair: str, timeframe: str, dataframe: DataFrame) -> Tuple[bool, bool]: """ @@ -524,15 +531,14 @@ class IStrategy(ABC, HyperStrategyMixin): :param force_stoploss: Externally provided stoploss :return: True if trade should be sold, False otherwise """ - # Set current rate to low for backtesting sell - current_rate = low or rate + current_rate = rate current_profit = trade.calc_profit_ratio(current_rate) trade.adjust_min_max_rates(high or current_rate) stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, - force_stoploss=force_stoploss, high=high) + force_stoploss=force_stoploss, low=low, high=high) # Set current rate to high for backtesting sell current_rate = high or rate @@ -599,18 +605,21 @@ class IStrategy(ABC, HyperStrategyMixin): def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, - force_stoploss: float, high: float = None) -> SellCheckTuple: + force_stoploss: float, low: float = None, + high: float = None) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not :param current_profit: current profit as ratio + :param low: Low value of this candle, only set in backtesting + :param high: High value of this candle, only set in backtesting """ stop_loss_value = force_stoploss if force_stoploss else self.stoploss # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) - if self.use_custom_stoploss: + if self.use_custom_stoploss and trade.stop_loss < (low or current_rate): stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None )(pair=trade.pair, trade=trade, current_time=current_time, @@ -623,7 +632,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: logger.warning("CustomStoploss function did not return valid stoploss") - if self.trailing_stop: + if self.trailing_stop and trade.stop_loss < (low or current_rate): # trailing stoploss handling sl_offset = self.trailing_stop_positive_offset @@ -643,7 +652,7 @@ class IStrategy(ABC, HyperStrategyMixin): # evaluate if the stoploss was hit if stoploss is not on exchange # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # regular stoploss handling. - if ((trade.stop_loss >= current_rate) and + if ((trade.stop_loss >= (low or current_rate)) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): sell_type = SellType.STOP_LOSS @@ -652,7 +661,7 @@ class IStrategy(ABC, HyperStrategyMixin): if trade.initial_stop_loss != trade.stop_loss: sell_type = SellType.TRAILING_STOP_LOSS logger.debug( - f"{trade.pair} - HIT STOP: current price at {current_rate:.6f}, " + f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, " f"stoploss is {trade.stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, " f"trade opened at {trade.open_rate:.6f}") diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5fa94e6c1..f5becc274 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2271,8 +2271,9 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_fetch_order(default_conf, mocker, exchange_name): +def test_fetch_order(default_conf, mocker, exchange_name, caplog): default_conf['dry_run'] = True + default_conf['exchange']['log_responses'] = True order = MagicMock() order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) @@ -2287,6 +2288,7 @@ def test_fetch_order(default_conf, mocker, exchange_name): api_mock.fetch_order = MagicMock(return_value=456) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) assert exchange.fetch_order('X', 'TKN/BTC') == 456 + assert log_has("API fetch_order: 456", caplog) with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index e5b969383..488425323 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -457,6 +457,50 @@ tc28 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] ) +# Test 29: trailing_stop should be triggered by low of next candle, without adjusting stoploss using +# high of stoploss candle. +# stop-loss: 10%, ROI: 10% (should not apply) +tc29 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) + [2, 4900, 5250, 4500, 5100, 6172, 0, 0], # Triggers trailing-stoploss + [3, 5100, 5100, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.02, trailing_stop=True, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=2)] +) + +# Test 30: trailing_stop should be triggered immediately on trade open candle. +# stop-loss: 10%, ROI: 10% (should not apply) +tc30 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5500, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop + [2, 4900, 5250, 4500, 5100, 6172, 0, 0], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, + trailing_stop_positive=0.01, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] +) + +# Test 31: trailing_stop should be triggered immediately on trade open candle. +# stop-loss: 10%, ROI: 10% (should not apply) +tc31 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5500, 5000, 4900, 6172, 0, 0], # enter trade (signal on last candle) and stop + [2, 4900, 5250, 4500, 5100, 6172, 0, 0], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10}, profit_perc=0.01, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, + trailing_stop_positive=0.01, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] +) + TESTS = [ tc0, tc1, @@ -487,6 +531,9 @@ TESTS = [ tc26, tc27, tc28, + tc29, + tc30, + tc31, ] diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 5e2274ce3..ae8f6e958 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -75,7 +75,7 @@ def whitelist_conf_agefilter(default_conf): "method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", - "refresh_period": 0, + "refresh_period": -1, }, { "method": "AgeFilter", @@ -687,7 +687,6 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 3 assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 - # freqtrade.config['exchange']['pair_whitelist'].append('HOT/BTC') previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count freqtrade.pairlists.refresh_pairlist() diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 10ab64690..9ec47dade 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -70,8 +70,7 @@ def test_protectionmanager(mocker, default_conf): ]) def test_protections_init(mocker, default_conf, timeframe, expected, protconf): default_conf['timeframe'] = timeframe - default_conf['protections'] = protconf - man = ProtectionManager(default_conf) + man = ProtectionManager(default_conf, protconf) assert len(man._protection_handlers) == len(protconf) assert man._protection_handlers[0]._lookback_period == expected[0] assert man._protection_handlers[0]._stop_duration == expected[1] diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index d091f3837..39ef6a1ab 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -13,7 +13,7 @@ from unittest.mock import ANY, MagicMock import arrow import pytest from telegram import Chat, Message, ReplyKeyboardMarkup, Update -from telegram.error import NetworkError +from telegram.error import BadRequest, NetworkError, TelegramError from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON @@ -25,8 +25,8 @@ from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC from freqtrade.rpc.telegram import Telegram, authorized_only -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange, - patch_get_signal, patch_whitelist) +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, + patch_exchange, patch_get_signal, patch_whitelist) class DummyCls(Telegram): @@ -55,14 +55,6 @@ class DummyCls(Telegram): raise Exception('test') -def get_telegram_testobject_with_inline(mocker, default_conf, mock=True, ftbot=None): - inline_msg_mock = MagicMock() - telegram, ftbot, msg_mock = get_telegram_testobject(mocker, default_conf) - mocker.patch('freqtrade.rpc.telegram.Telegram._send_inline_msg', inline_msg_mock) - - return telegram, ftbot, msg_mock, inline_msg_mock - - def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None): msg_mock = MagicMock() if mock: @@ -920,8 +912,8 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: fbuy_mock = MagicMock(return_value=None) mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) - telegram, freqtradebot, _, inline_msg_mock = get_telegram_testobject_with_inline(mocker, - default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) context = MagicMock() @@ -929,10 +921,10 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: telegram._forcebuy(update=update, context=context) assert fbuy_mock.call_count == 0 - assert inline_msg_mock.call_count == 1 - assert inline_msg_mock.call_args_list[0][0][0] == 'Which pair?' - assert inline_msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy' - keyboard = inline_msg_mock.call_args_list[0][1]['keyboard'] + assert msg_mock.call_count == 1 + assert msg_mock.call_args_list[0][1]['msg'] == 'Which pair?' + # assert msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy' + keyboard = msg_mock.call_args_list[0][1]['keyboard'] assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4 update = MagicMock() update.callback_query = MagicMock() @@ -1569,7 +1561,7 @@ def test__sell_emoji(default_conf, mocker, msg, expected): assert telegram._get_sell_emoji(msg) == expected -def test__send_msg(default_conf, mocker) -> None: +def test_telegram__send_msg(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) @@ -1580,6 +1572,28 @@ def test__send_msg(default_conf, mocker) -> None: telegram._send_msg('test') assert len(bot.method_calls) == 1 + # Test update + query = MagicMock() + telegram._send_msg('test', callback_path="DeadBeef", query=query, reload_able=True) + edit_message_text = telegram._updater.bot.edit_message_text + assert edit_message_text.call_count == 1 + assert "Updated: " in edit_message_text.call_args_list[0][1]['text'] + + telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest("not modified")) + telegram._send_msg('test', callback_path="DeadBeef", query=query) + assert telegram._updater.bot.edit_message_text.call_count == 1 + assert not log_has_re(r"TelegramError: .*", caplog) + + telegram._updater.bot.edit_message_text = MagicMock(side_effect=BadRequest("")) + telegram._send_msg('test2', callback_path="DeadBeef", query=query) + assert telegram._updater.bot.edit_message_text.call_count == 1 + assert log_has_re(r"TelegramError: .*", caplog) + + telegram._updater.bot.edit_message_text = MagicMock(side_effect=TelegramError("DeadBEEF")) + telegram._send_msg('test3', callback_path="DeadBeef", query=query) + + assert log_has_re(r"TelegramError: DeadBEEF! Giving up.*", caplog) + def test__send_msg_network_error(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 64081fa37..04d12a51f 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -153,6 +153,8 @@ def test_assert_df_raise(mocker, caplog, ohlcv_history): def test_assert_df(ohlcv_history, caplog): df_len = len(ohlcv_history) - 1 + ohlcv_history.loc[:, 'buy'] = 0 + ohlcv_history.loc[:, 'sell'] = 0 # Ensure it's running when passed correctly _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date']) @@ -170,6 +172,18 @@ def test_assert_df(ohlcv_history, caplog): match=r"Dataframe returned from strategy.*last date\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) + with pytest.raises(StrategyError, + match=r"No dataframe returned \(return statement missing\?\)."): + _STRATEGY.assert_df(None, len(ohlcv_history), + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) + with pytest.raises(StrategyError, + match="Buy column not set"): + _STRATEGY.assert_df(ohlcv_history.drop('buy', axis=1), len(ohlcv_history), + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) + with pytest.raises(StrategyError, + match="Sell column not set"): + _STRATEGY.assert_df(ohlcv_history.drop('sell', axis=1), len(ohlcv_history), + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) _STRATEGY.disable_dataframe_checks = True caplog.clear()