Merge branch 'develop' into margin-db
This commit is contained in:
commit
1dd2a6a895
@ -178,7 +178,9 @@
|
|||||||
"sell_fill": "on",
|
"sell_fill": "on",
|
||||||
"buy_cancel": "on",
|
"buy_cancel": "on",
|
||||||
"sell_cancel": "on"
|
"sell_cancel": "on"
|
||||||
}
|
},
|
||||||
|
"reload": true,
|
||||||
|
"balance_dust_level": 0.01
|
||||||
},
|
},
|
||||||
"api_server": {
|
"api_server": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
@ -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" ]
|
|
@ -284,7 +284,7 @@ A backtesting result will look like that:
|
|||||||
| Backtesting to | 2019-05-01 00:00:00 |
|
| Backtesting to | 2019-05-01 00:00:00 |
|
||||||
| Max open trades | 3 |
|
| Max open trades | 3 |
|
||||||
| | |
|
| | |
|
||||||
| Total trades | 429 |
|
| Total/Daily Avg Trades| 429 / 3.575 |
|
||||||
| Starting balance | 0.01000000 BTC |
|
| Starting balance | 0.01000000 BTC |
|
||||||
| Final balance | 0.01762792 BTC |
|
| Final balance | 0.01762792 BTC |
|
||||||
| Absolute profit | 0.00762792 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 |
|
| Backtesting to | 2019-05-01 00:00:00 |
|
||||||
| Max open trades | 3 |
|
| Max open trades | 3 |
|
||||||
| | |
|
| | |
|
||||||
| Total trades | 429 |
|
| Total/Daily Avg Trades| 429 / 3.575 |
|
||||||
| Starting balance | 0.01000000 BTC |
|
| Starting balance | 0.01000000 BTC |
|
||||||
| Final balance | 0.01762792 BTC |
|
| Final balance | 0.01762792 BTC |
|
||||||
| Absolute profit | 0.00762792 BTC |
|
| Absolute profit | 0.00762792 BTC |
|
||||||
| Total profit % | 76.2% |
|
| Total profit % | 76.2% |
|
||||||
| Trades per day | 3.575 |
|
|
||||||
| Avg. stake amount | 0.001 BTC |
|
| Avg. stake amount | 0.001 BTC |
|
||||||
| Total trade volume | 0.429 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).
|
- `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).
|
- `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).
|
- `Starting balance`: Start balance - as given by dry-run-wallet (config or command line).
|
||||||
- `Final balance`: Final balance - starting balance + absolute profit.
|
- `Final balance`: Final balance - starting balance + absolute profit.
|
||||||
- `Absolute profit`: Profit made in stake currency.
|
- `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`.
|
- `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.
|
- `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.
|
- `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 %`.
|
- `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
|
- 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
|
- Low happens before high for stoploss, protecting capital first
|
||||||
- Trailing stoploss
|
- 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
|
- High happens first - adjusting stoploss
|
||||||
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly)
|
- 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
|
- ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies
|
||||||
|
@ -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. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
| `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded. <br>*Defaults to `60` minutes.* <br> **Datatype:** Positive Integer
|
||||||
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
| `exchange.skip_pair_validation` | Skip pairlist validation on startup.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||||
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
| `exchange.skip_open_order_update` | Skips open order updates on startup should the exchange cause problems. Only relevant in live conditions.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||||
|
| `exchange.log_responses` | Log relevant exchange responses. For debug mode only - use with care.<br>*Defaults to `false`<br> **Datatype:** Boolean
|
||||||
| `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation.
|
| `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. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `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. <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
| `pairlists` | Define one or more pairlists to be used. [More information](plugins.md#pairlists-and-pairlist-handlers). <br>*Defaults to `StaticPairList`.* <br> **Datatype:** List of Dicts
|
||||||
| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** List of Dicts
|
| `protections` | Define one or more protections to be used. [More information](plugins.md#protections). <br> **Datatype:** List of Dicts
|
||||||
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
|
| `telegram.enabled` | Enable the usage of Telegram. <br> **Datatype:** Boolean
|
||||||
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
| `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. <br>**Keep it in secret, do not disclose publicly.** <br> **Datatype:** String
|
||||||
@ -156,7 +157,6 @@ Values set in the configuration file always overwrite values set in the strategy
|
|||||||
* `order_time_in_force`
|
* `order_time_in_force`
|
||||||
* `unfilledtimeout`
|
* `unfilledtimeout`
|
||||||
* `disable_dataframe_checks`
|
* `disable_dataframe_checks`
|
||||||
* `protections`
|
|
||||||
* `use_sell_signal` (ask_strategy)
|
* `use_sell_signal` (ask_strategy)
|
||||||
* `sell_profit_only` (ask_strategy)
|
* `sell_profit_only` (ask_strategy)
|
||||||
* `sell_profit_offset` (ask_strategy)
|
* `sell_profit_offset` (ask_strategy)
|
||||||
|
@ -98,7 +98,7 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse
|
|||||||
image: freqtradeorg/freqtrade:custom_arm64
|
image: freqtradeorg/freqtrade:custom_arm64
|
||||||
build:
|
build:
|
||||||
context: .
|
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.
|
The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image.
|
||||||
|
@ -8,7 +8,6 @@ All protection end times are rounded up to the next candle to avoid sudden, unex
|
|||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance.
|
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
|
!!! Tip
|
||||||
Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term).
|
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.
|
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
|
``` python
|
||||||
"protections": [
|
protections = [
|
||||||
{
|
{
|
||||||
"method": "StoplossGuard",
|
"method": "StoplossGuard",
|
||||||
"lookback_period_candles": 24,
|
"lookback_period_candles": 24,
|
||||||
"trade_limit": 4,
|
"trade_limit": 4,
|
||||||
"stop_duration_candles": 4,
|
"stop_duration_candles": 4,
|
||||||
"only_per_pair": false
|
"only_per_pair": False
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! 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.
|
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
|
``` python
|
||||||
"protections": [
|
protections = [
|
||||||
{
|
{
|
||||||
"method": "MaxDrawdown",
|
"method": "MaxDrawdown",
|
||||||
"lookback_period_candles": 48,
|
"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,
|
"stop_duration_candles": 12,
|
||||||
"max_allowed_drawdown": 0.2
|
"max_allowed_drawdown": 0.2
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Low Profit Pairs
|
#### 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.
|
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
|
``` python
|
||||||
"protections": [
|
protections = [
|
||||||
{
|
{
|
||||||
"method": "LowProfitPairs",
|
"method": "LowProfitPairs",
|
||||||
"lookback_period_candles": 6,
|
"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,
|
"stop_duration": 60,
|
||||||
"required_profit": 0.02
|
"required_profit": 0.02
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Cooldown Period
|
#### 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".
|
The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down".
|
||||||
|
|
||||||
```json
|
``` python
|
||||||
"protections": [
|
protections = [
|
||||||
{
|
{
|
||||||
"method": "CooldownPeriod",
|
"method": "CooldownPeriod",
|
||||||
"stop_duration_candles": 2
|
"stop_duration_candles": 2
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! 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 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.
|
* 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
|
``` python
|
||||||
from freqtrade.strategy import IStrategy
|
from freqtrade.strategy import IStrategy
|
||||||
|
|
||||||
|
@ -95,6 +95,7 @@ Example configuration showing the different settings:
|
|||||||
"buy_fill": "off",
|
"buy_fill": "off",
|
||||||
"sell_fill": "off"
|
"sell_fill": "off"
|
||||||
},
|
},
|
||||||
|
"reload": true,
|
||||||
"balance_dust_level": 0.01
|
"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.
|
`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)
|
## Create a custom keyboard (command shortcut buttons)
|
||||||
|
|
||||||
|
@ -275,7 +275,8 @@ CONF_SCHEMA = {
|
|||||||
'default': 'off'
|
'default': 'off'
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
'reload': {'type': 'boolean'},
|
||||||
},
|
},
|
||||||
'required': ['enabled', 'token', 'chat_id'],
|
'required': ['enabled', 'token', 'chat_id'],
|
||||||
},
|
},
|
||||||
|
@ -68,6 +68,7 @@ class Binance(Exchange):
|
|||||||
amount=amount, price=rate, params=params)
|
amount=amount, price=rate, params=params)
|
||||||
logger.info('stoploss limit order added for %s. '
|
logger.info('stoploss limit order added for %s. '
|
||||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||||
|
self._log_exchange_response('create_stoploss_order', order)
|
||||||
return order
|
return order
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise InsufficientFundsError(
|
raise InsufficientFundsError(
|
||||||
|
@ -104,6 +104,7 @@ class Exchange:
|
|||||||
logger.info('Instance is running with dry_run enabled')
|
logger.info('Instance is running with dry_run enabled')
|
||||||
logger.info(f"Using CCXT {ccxt.__version__}")
|
logger.info(f"Using CCXT {ccxt.__version__}")
|
||||||
exchange_config = config['exchange']
|
exchange_config = config['exchange']
|
||||||
|
self.log_responses = exchange_config.get('log_responses', False)
|
||||||
|
|
||||||
# Deep merge ft_has with default ft_has options
|
# Deep merge ft_has with default ft_has options
|
||||||
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
|
self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default))
|
||||||
@ -226,6 +227,11 @@ class Exchange:
|
|||||||
"""exchange ccxt precisionMode"""
|
"""exchange ccxt precisionMode"""
|
||||||
return self._api.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:
|
def ohlcv_candle_limit(self, timeframe: str) -> int:
|
||||||
"""
|
"""
|
||||||
Exchange ohlcv candle limit
|
Exchange ohlcv candle limit
|
||||||
@ -622,8 +628,10 @@ class Exchange:
|
|||||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
||||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||||
|
|
||||||
return self._api.create_order(pair, ordertype, side,
|
order = self._api.create_order(pair, ordertype, side,
|
||||||
amount, rate_for_order, params)
|
amount, rate_for_order, params)
|
||||||
|
self._log_exchange_response('create_order', order)
|
||||||
|
return order
|
||||||
|
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise InsufficientFundsError(
|
raise InsufficientFundsError(
|
||||||
@ -694,7 +702,9 @@ class Exchange:
|
|||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
return self.fetch_dry_run_order(order_id)
|
return self.fetch_dry_run_order(order_id)
|
||||||
try:
|
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:
|
except ccxt.OrderNotFound as e:
|
||||||
raise RetryableOrderError(
|
raise RetryableOrderError(
|
||||||
f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
|
f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
|
||||||
@ -744,7 +754,9 @@ class Exchange:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
try:
|
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:
|
except ccxt.InvalidOrder as e:
|
||||||
raise InvalidOrderException(
|
raise InvalidOrderException(
|
||||||
f'Could not cancel order. Message: {e}') from e
|
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))
|
pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000))
|
||||||
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
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
|
return matched_trades
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
|
@ -69,6 +69,7 @@ class Ftx(Exchange):
|
|||||||
|
|
||||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||||
amount=amount, params=params)
|
amount=amount, params=params)
|
||||||
|
self._log_exchange_response('create_stoploss_order', order)
|
||||||
logger.info('stoploss order added for %s. '
|
logger.info('stoploss order added for %s. '
|
||||||
'stop price: %s.', pair, stop_price)
|
'stop price: %s.', pair, stop_price)
|
||||||
return order
|
return order
|
||||||
@ -99,12 +100,14 @@ class Ftx(Exchange):
|
|||||||
orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
|
orders = self._api.fetch_orders(pair, None, params={'type': 'stop'})
|
||||||
|
|
||||||
order = [order for order in orders if order['id'] == order_id]
|
order = [order for order in orders if order['id'] == order_id]
|
||||||
|
self._log_exchange_response('fetch_stoploss_order', order)
|
||||||
if len(order) == 1:
|
if len(order) == 1:
|
||||||
if order[0].get('status') == 'closed':
|
if order[0].get('status') == 'closed':
|
||||||
# Trigger order was triggered ...
|
# Trigger order was triggered ...
|
||||||
real_order_id = order[0].get('info', {}).get('orderId')
|
real_order_id = order[0].get('info', {}).get('orderId')
|
||||||
|
|
||||||
order1 = self._api.fetch_order(real_order_id, pair)
|
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.
|
# Fake type to stop - as this was really a stop order.
|
||||||
order1['id_stop'] = order1['id']
|
order1['id_stop'] = order1['id']
|
||||||
order1['id'] = order_id
|
order1['id'] = order_id
|
||||||
@ -131,7 +134,9 @@ class Ftx(Exchange):
|
|||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
return {}
|
return {}
|
||||||
try:
|
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:
|
except ccxt.InvalidOrder as e:
|
||||||
raise InvalidOrderException(
|
raise InvalidOrderException(
|
||||||
f'Could not cancel order. Message: {e}') from e
|
f'Could not cancel order. Message: {e}') from e
|
||||||
|
@ -103,6 +103,7 @@ class Kraken(Exchange):
|
|||||||
|
|
||||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||||
amount=amount, price=stop_price, params=params)
|
amount=amount, price=stop_price, params=params)
|
||||||
|
self._log_exchange_response('create_stoploss_order', order)
|
||||||
logger.info('stoploss order added for %s. '
|
logger.info('stoploss order added for %s. '
|
||||||
'stop price: %s.', pair, stop_price)
|
'stop price: %s.', pair, stop_price)
|
||||||
return order
|
return order
|
||||||
|
@ -70,7 +70,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
PairLocks.timeframe = self.config['timeframe']
|
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
|
# RPC runs in separate threads, can start handling external commands just after
|
||||||
# initialization, even before Freqtradebot has a chance to start its throttling,
|
# initialization, even before Freqtradebot has a chance to start its throttling,
|
||||||
|
@ -137,7 +137,7 @@ class Backtesting:
|
|||||||
if hasattr(strategy, 'protections'):
|
if hasattr(strategy, 'protections'):
|
||||||
conf = deepcopy(conf)
|
conf = deepcopy(conf)
|
||||||
conf['protections'] = strategy.protections
|
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]:
|
def load_bt_data(self) -> Tuple[Dict[str, DataFrame], TimeRange]:
|
||||||
"""
|
"""
|
||||||
@ -225,6 +225,22 @@ class Backtesting:
|
|||||||
# sell at open price.
|
# sell at open price.
|
||||||
return sell_row[OPEN_IDX]
|
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
|
# Set close_rate to stoploss
|
||||||
return trade.stop_loss
|
return trade.stop_loss
|
||||||
elif sell.sell_type == (SellType.ROI):
|
elif sell.sell_type == (SellType.ROI):
|
||||||
|
19
freqtrade/optimize/hyperopt_tools.py
Normal file → Executable file
19
freqtrade/optimize/hyperopt_tools.py
Normal file → Executable file
@ -91,7 +91,7 @@ class HyperoptTools():
|
|||||||
if print_json:
|
if print_json:
|
||||||
result_dict: Dict = {}
|
result_dict: Dict = {}
|
||||||
for s in ['buy', 'sell', 'roi', 'stoploss', 'trailing']:
|
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))
|
print(rapidjson.dumps(result_dict, default=str, number_mode=rapidjson.NM_NATIVE))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -104,17 +104,24 @@ class HyperoptTools():
|
|||||||
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
|
HyperoptTools._params_pretty_print(params, 'trailing', "Trailing stop:")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _params_update_for_json(result_dict, params, space: str) -> None:
|
def _params_update_for_json(result_dict, params, non_optimized, space: str) -> None:
|
||||||
if space in params:
|
if (space in params) or (space in non_optimized):
|
||||||
space_params = HyperoptTools._space_params(params, space)
|
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']:
|
if space in ['buy', 'sell']:
|
||||||
result_dict.setdefault('params', {}).update(space_params)
|
result_dict.setdefault('params', {}).update(all_space_params)
|
||||||
elif space == 'roi':
|
elif space == 'roi':
|
||||||
# Convert keys in min_roi dict to strings because
|
# Convert keys in min_roi dict to strings because
|
||||||
# rapidjson cannot dump dicts with integer keys...
|
# 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'
|
else: # 'stoploss', 'trailing'
|
||||||
result_dict.update(space_params)
|
result_dict.update(all_space_params)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None:
|
def _params_pretty_print(params, space: str, header: str, non_optimized={}) -> None:
|
||||||
|
@ -556,7 +556,8 @@ def text_table_add_metrics(strat_results: Dict) -> str:
|
|||||||
('Backtesting to', strat_results['backtest_end']),
|
('Backtesting to', strat_results['backtest_end']),
|
||||||
('Max open trades', strat_results['max_open_trades']),
|
('Max open trades', strat_results['max_open_trades']),
|
||||||
('', ''), # Empty line to improve readability
|
('', ''), # 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'],
|
('Starting balance', round_coin_value(strat_results['starting_balance'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Final balance', round_coin_value(strat_results['final_balance'],
|
('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'],
|
('Absolute profit ', round_coin_value(strat_results['profit_total_abs'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Total profit %', f"{round(strat_results['profit_total'] * 100, 2):}%"),
|
('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'],
|
('Avg. stake amount', round_coin_value(strat_results['avg_stake_amount'],
|
||||||
strat_results['stake_currency'])),
|
strat_results['stake_currency'])),
|
||||||
('Total trade volume', round_coin_value(strat_results['total_volume'],
|
('Total trade volume', round_coin_value(strat_results['total_volume'],
|
||||||
|
@ -83,7 +83,8 @@ class PairListManager():
|
|||||||
pairlist = self._pairlist_handlers[0].gen_pairlist(tickers)
|
pairlist = self._pairlist_handlers[0].gen_pairlist(tickers)
|
||||||
|
|
||||||
# Process all Pairlist Handlers in the chain
|
# 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)
|
pairlist = pairlist_handler.filter_pairlist(pairlist, tickers)
|
||||||
|
|
||||||
# Validation against blacklist happens after the chain of Pairlist Handlers
|
# Validation against blacklist happens after the chain of Pairlist Handlers
|
||||||
|
@ -15,11 +15,11 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class ProtectionManager():
|
class ProtectionManager():
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: Dict, protections: List) -> None:
|
||||||
self._config = config
|
self._config = config
|
||||||
|
|
||||||
self._protection_handlers: List[IProtection] = []
|
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 = ProtectionResolver.load_protection(
|
||||||
protection_handler_config['method'],
|
protection_handler_config['method'],
|
||||||
config=config,
|
config=config,
|
||||||
|
@ -113,7 +113,9 @@ class StrategyResolver(IResolver):
|
|||||||
- Strategy
|
- Strategy
|
||||||
- default (if not None)
|
- 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])
|
setattr(strategy, attribute, config[attribute])
|
||||||
logger.info("Override strategy '%s' with value in config file: %s.",
|
logger.info("Override strategy '%s' with value in config file: %s.",
|
||||||
attribute, config[attribute])
|
attribute, config[attribute])
|
||||||
|
@ -10,13 +10,13 @@ from datetime import date, datetime, timedelta
|
|||||||
from html import escape
|
from html import escape
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from math import isnan
|
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
|
import arrow
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ParseMode,
|
from telegram import (CallbackQuery, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton,
|
||||||
ReplyKeyboardMarkup, Update)
|
ParseMode, ReplyKeyboardMarkup, Update)
|
||||||
from telegram.error import NetworkError, TelegramError
|
from telegram.error import BadRequest, NetworkError, TelegramError
|
||||||
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
|
from telegram.ext import CallbackContext, CallbackQueryHandler, CommandHandler, Updater
|
||||||
from telegram.utils.helpers import escape_markdown
|
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]
|
update = kwargs.get('update') or args[0]
|
||||||
|
|
||||||
# Reject unauthorized messages
|
# 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(
|
logger.info(
|
||||||
'Rejected unauthorized message from: %s',
|
'Rejected unauthorized message from: %s',
|
||||||
update.message.chat_id
|
update.message.chat_id
|
||||||
@ -91,7 +95,7 @@ class Telegram(RPCHandler):
|
|||||||
Validates the keyboard configuration from telegram config
|
Validates the keyboard configuration from telegram config
|
||||||
section.
|
section.
|
||||||
"""
|
"""
|
||||||
self._keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = [
|
self._keyboard: List[List[Union[str, KeyboardButton]]] = [
|
||||||
['/daily', '/profit', '/balance'],
|
['/daily', '/profit', '/balance'],
|
||||||
['/status', '/status table', '/performance'],
|
['/status', '/status table', '/performance'],
|
||||||
['/count', '/start', '/stop', '/help']
|
['/count', '/start', '/stop', '/help']
|
||||||
@ -164,8 +168,21 @@ class Telegram(RPCHandler):
|
|||||||
CommandHandler('help', self._help),
|
CommandHandler('help', self._help),
|
||||||
CommandHandler('version', self._version),
|
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:
|
for handle in handles:
|
||||||
self._updater.dispatcher.add_handler(handle)
|
self._updater.dispatcher.add_handler(handle)
|
||||||
|
|
||||||
|
for callback in callbacks:
|
||||||
|
self._updater.dispatcher.add_handler(callback)
|
||||||
|
|
||||||
self._updater.start_polling(
|
self._updater.start_polling(
|
||||||
bootstrap_retries=-1,
|
bootstrap_retries=-1,
|
||||||
timeout=30,
|
timeout=30,
|
||||||
@ -177,11 +194,6 @@ class Telegram(RPCHandler):
|
|||||||
[h.command for h in handles]
|
[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:
|
def cleanup(self) -> None:
|
||||||
"""
|
"""
|
||||||
Stops all running telegram threads.
|
Stops all running telegram threads.
|
||||||
@ -409,7 +421,9 @@ class Telegram(RPCHandler):
|
|||||||
# insert separators line between Total
|
# insert separators line between Total
|
||||||
lines = message.split("\n")
|
lines = message.split("\n")
|
||||||
message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
|
message = "\n".join(lines[:-1] + [lines[1]] + [lines[-1]])
|
||||||
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
|
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML,
|
||||||
|
reload_able=True, callback_path="update_status_table",
|
||||||
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@ -447,7 +461,8 @@ class Telegram(RPCHandler):
|
|||||||
],
|
],
|
||||||
tablefmt='simple')
|
tablefmt='simple')
|
||||||
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
|
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
|
||||||
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:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@ -519,7 +534,8 @@ class Telegram(RPCHandler):
|
|||||||
if stats['closed_trade_count'] > 0:
|
if stats['closed_trade_count'] > 0:
|
||||||
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
|
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
|
||||||
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
|
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
|
@authorized_only
|
||||||
def _stats(self, update: Update, context: CallbackContext) -> None:
|
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['stake']}: {result['total']: .8f}`\n"
|
||||||
f"\t`{result['symbol']}: "
|
f"\t`{result['symbol']}: "
|
||||||
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
|
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
|
||||||
self._send_msg(output)
|
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
||||||
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@ -713,10 +730,10 @@ class Telegram(RPCHandler):
|
|||||||
self._forcebuy_action(pair, price)
|
self._forcebuy_action(pair, price)
|
||||||
else:
|
else:
|
||||||
whitelist = self._rpc._rpc_whitelist()['whitelist']
|
whitelist = self._rpc._rpc_whitelist()['whitelist']
|
||||||
pairs = [InlineKeyboardButton(pair, callback_data=pair) for pair in whitelist]
|
pairs = [InlineKeyboardButton(text=pair, callback_data=pair) for pair in whitelist]
|
||||||
self._send_inline_msg("Which pair?",
|
|
||||||
keyboard=self._layout_inline_keyboard(pairs),
|
self._send_msg(msg="Which pair?",
|
||||||
callback_query_handler='forcebuy')
|
keyboard=self._layout_inline_keyboard(pairs))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _trades(self, update: Update, context: CallbackContext) -> None:
|
def _trades(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -800,7 +817,9 @@ class Telegram(RPCHandler):
|
|||||||
else:
|
else:
|
||||||
output += stat_line
|
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:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@ -820,7 +839,9 @@ class Telegram(RPCHandler):
|
|||||||
tablefmt='simple')
|
tablefmt='simple')
|
||||||
message = "<pre>{}</pre>".format(message)
|
message = "<pre>{}</pre>".format(message)
|
||||||
logger.debug(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:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@ -1052,29 +1073,42 @@ class Telegram(RPCHandler):
|
|||||||
f"*Current state:* `{val['state']}`"
|
f"*Current state:* `{val['state']}`"
|
||||||
)
|
)
|
||||||
|
|
||||||
def _send_inline_msg(self, msg: str, callback_query_handler,
|
def _update_msg(self, query: CallbackQuery, msg: str, callback_path: str = "",
|
||||||
parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False,
|
reload_able: bool = False, parse_mode: str = ParseMode.MARKDOWN) -> None:
|
||||||
keyboard: List[List[InlineKeyboardButton]] = None, ) -> None:
|
if reload_able:
|
||||||
"""
|
reply_markup = InlineKeyboardMarkup([
|
||||||
Send given markdown message
|
[InlineKeyboardButton("Refresh", callback_data=callback_path)],
|
||||||
:param msg: message
|
])
|
||||||
:param bot: alternative bot
|
else:
|
||||||
:param parse_mode: telegram parse mode
|
reply_markup = InlineKeyboardMarkup([[]])
|
||||||
:return: None
|
msg += "\nUpdated: {}".format(datetime.now().ctime())
|
||||||
"""
|
if not query.message:
|
||||||
if self._current_callback_query_handler:
|
return
|
||||||
self._updater.dispatcher.remove_handler(self._current_callback_query_handler)
|
chat_id = query.message.chat_id
|
||||||
self._current_callback_query_handler = self._callback_query_handlers[callback_query_handler]
|
message_id = query.message.message_id
|
||||||
self._updater.dispatcher.add_handler(self._current_callback_query_handler)
|
|
||||||
|
|
||||||
self._send_msg(msg, parse_mode, disable_notification,
|
try:
|
||||||
cast(List[List[Union[str, KeyboardButton, InlineKeyboardButton]]], keyboard),
|
self._updater.bot.edit_message_text(
|
||||||
reply_markup=InlineKeyboardMarkup)
|
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,
|
def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN,
|
||||||
disable_notification: bool = False,
|
disable_notification: bool = False,
|
||||||
keyboard: List[List[Union[str, KeyboardButton, InlineKeyboardButton]]] = None,
|
keyboard: List[List[InlineKeyboardButton]] = None,
|
||||||
reply_markup=ReplyKeyboardMarkup) -> None:
|
callback_path: str = "",
|
||||||
|
reload_able: bool = False,
|
||||||
|
query: Optional[CallbackQuery] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Send given markdown message
|
Send given markdown message
|
||||||
:param msg: message
|
:param msg: message
|
||||||
@ -1082,9 +1116,19 @@ class Telegram(RPCHandler):
|
|||||||
:param parse_mode: telegram parse mode
|
:param parse_mode: telegram parse mode
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if keyboard is None:
|
reply_markup: Union[InlineKeyboardMarkup, ReplyKeyboardMarkup]
|
||||||
keyboard = self._keyboard
|
if query:
|
||||||
reply_markup = reply_markup(keyboard, resize_keyboard=True)
|
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:
|
||||||
try:
|
try:
|
||||||
self._updater.bot.send_message(
|
self._updater.bot.send_message(
|
||||||
|
@ -107,7 +107,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
startup_candle_count: int = 0
|
startup_candle_count: int = 0
|
||||||
|
|
||||||
# Protections
|
# Protections
|
||||||
protections: List
|
protections: List = []
|
||||||
|
|
||||||
# Class level variables (intentional) containing
|
# Class level variables (intentional) containing
|
||||||
# the dataprovider (dp) (access to other candles, historic data, ...)
|
# the dataprovider (dp) (access to other candles, historic data, ...)
|
||||||
@ -453,18 +453,25 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
"""
|
"""
|
||||||
Ensure dataframe (length, last candle) was not modified, and has all elements we need.
|
Ensure dataframe (length, last candle) was not modified, and has all elements we need.
|
||||||
"""
|
"""
|
||||||
|
message_template = "Dataframe returned from strategy has mismatching {}."
|
||||||
message = ""
|
message = ""
|
||||||
if df_len != len(dataframe):
|
if dataframe is None:
|
||||||
message = "length"
|
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]:
|
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]:
|
elif df_date != dataframe["date"].iloc[-1]:
|
||||||
message = "last date"
|
message = message_template.format("last date")
|
||||||
if message:
|
if message:
|
||||||
if self.disable_dataframe_checks:
|
if self.disable_dataframe_checks:
|
||||||
logger.warning(f"Dataframe returned from strategy has mismatching {message}.")
|
logger.warning(message)
|
||||||
else:
|
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]:
|
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
|
:param force_stoploss: Externally provided stoploss
|
||||||
:return: True if trade should be sold, False otherwise
|
:return: True if trade should be sold, False otherwise
|
||||||
"""
|
"""
|
||||||
# Set current rate to low for backtesting sell
|
current_rate = rate
|
||||||
current_rate = low or rate
|
|
||||||
current_profit = trade.calc_profit_ratio(current_rate)
|
current_profit = trade.calc_profit_ratio(current_rate)
|
||||||
|
|
||||||
trade.adjust_min_max_rates(high or current_rate)
|
trade.adjust_min_max_rates(high or current_rate)
|
||||||
|
|
||||||
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade,
|
||||||
current_time=date, current_profit=current_profit,
|
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
|
# Set current rate to high for backtesting sell
|
||||||
current_rate = high or rate
|
current_rate = high or rate
|
||||||
@ -599,18 +605,21 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
def stop_loss_reached(self, current_rate: float, trade: Trade,
|
||||||
current_time: datetime, current_profit: float,
|
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,
|
Based on current profit of the trade and configured (trailing) stoploss,
|
||||||
decides to sell or not
|
decides to sell or not
|
||||||
:param current_profit: current profit as ratio
|
: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
|
stop_loss_value = force_stoploss if force_stoploss else self.stoploss
|
||||||
|
|
||||||
# Initiate stoploss with open_rate. Does nothing if stoploss is already set.
|
# Initiate stoploss with open_rate. Does nothing if stoploss is already set.
|
||||||
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
|
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
|
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None
|
||||||
)(pair=trade.pair, trade=trade,
|
)(pair=trade.pair, trade=trade,
|
||||||
current_time=current_time,
|
current_time=current_time,
|
||||||
@ -623,7 +632,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
else:
|
else:
|
||||||
logger.warning("CustomStoploss function did not return valid stoploss")
|
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
|
# trailing stoploss handling
|
||||||
sl_offset = self.trailing_stop_positive_offset
|
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
|
# 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
|
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
||||||
# regular stoploss handling.
|
# 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'])):
|
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
|
||||||
|
|
||||||
sell_type = SellType.STOP_LOSS
|
sell_type = SellType.STOP_LOSS
|
||||||
@ -652,7 +661,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
if trade.initial_stop_loss != trade.stop_loss:
|
if trade.initial_stop_loss != trade.stop_loss:
|
||||||
sell_type = SellType.TRAILING_STOP_LOSS
|
sell_type = SellType.TRAILING_STOP_LOSS
|
||||||
logger.debug(
|
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"stoploss is {trade.stop_loss:.6f}, "
|
||||||
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
|
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
|
||||||
f"trade opened at {trade.open_rate:.6f}")
|
f"trade opened at {trade.open_rate:.6f}")
|
||||||
|
@ -2271,8 +2271,9 @@ def test_cancel_stoploss_order_with_result(default_conf, mocker, exchange_name):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
@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['dry_run'] = True
|
||||||
|
default_conf['exchange']['log_responses'] = True
|
||||||
order = MagicMock()
|
order = MagicMock()
|
||||||
order.myid = 123
|
order.myid = 123
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
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)
|
api_mock.fetch_order = MagicMock(return_value=456)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
assert exchange.fetch_order('X', 'TKN/BTC') == 456
|
assert exchange.fetch_order('X', 'TKN/BTC') == 456
|
||||||
|
assert log_has("API fetch_order: 456", caplog)
|
||||||
|
|
||||||
with pytest.raises(InvalidOrderException):
|
with pytest.raises(InvalidOrderException):
|
||||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
|
@ -457,6 +457,50 @@ tc28 = BTContainer(data=[
|
|||||||
trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)]
|
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 = [
|
TESTS = [
|
||||||
tc0,
|
tc0,
|
||||||
tc1,
|
tc1,
|
||||||
@ -487,6 +531,9 @@ TESTS = [
|
|||||||
tc26,
|
tc26,
|
||||||
tc27,
|
tc27,
|
||||||
tc28,
|
tc28,
|
||||||
|
tc29,
|
||||||
|
tc30,
|
||||||
|
tc31,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -75,7 +75,7 @@ def whitelist_conf_agefilter(default_conf):
|
|||||||
"method": "VolumePairList",
|
"method": "VolumePairList",
|
||||||
"number_assets": 5,
|
"number_assets": 5,
|
||||||
"sort_key": "quoteVolume",
|
"sort_key": "quoteVolume",
|
||||||
"refresh_period": 0,
|
"refresh_period": -1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"method": "AgeFilter",
|
"method": "AgeFilter",
|
||||||
@ -687,7 +687,6 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o
|
|||||||
freqtrade.pairlists.refresh_pairlist()
|
freqtrade.pairlists.refresh_pairlist()
|
||||||
assert len(freqtrade.pairlists.whitelist) == 3
|
assert len(freqtrade.pairlists.whitelist) == 3
|
||||||
assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0
|
assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0
|
||||||
# freqtrade.config['exchange']['pair_whitelist'].append('HOT/BTC')
|
|
||||||
|
|
||||||
previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count
|
previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count
|
||||||
freqtrade.pairlists.refresh_pairlist()
|
freqtrade.pairlists.refresh_pairlist()
|
||||||
|
@ -70,8 +70,7 @@ def test_protectionmanager(mocker, default_conf):
|
|||||||
])
|
])
|
||||||
def test_protections_init(mocker, default_conf, timeframe, expected, protconf):
|
def test_protections_init(mocker, default_conf, timeframe, expected, protconf):
|
||||||
default_conf['timeframe'] = timeframe
|
default_conf['timeframe'] = timeframe
|
||||||
default_conf['protections'] = protconf
|
man = ProtectionManager(default_conf, protconf)
|
||||||
man = ProtectionManager(default_conf)
|
|
||||||
assert len(man._protection_handlers) == len(protconf)
|
assert len(man._protection_handlers) == len(protconf)
|
||||||
assert man._protection_handlers[0]._lookback_period == expected[0]
|
assert man._protection_handlers[0]._lookback_period == expected[0]
|
||||||
assert man._protection_handlers[0]._stop_duration == expected[1]
|
assert man._protection_handlers[0]._stop_duration == expected[1]
|
||||||
|
@ -13,7 +13,7 @@ from unittest.mock import ANY, MagicMock
|
|||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
from telegram import Chat, Message, ReplyKeyboardMarkup, Update
|
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 import __version__
|
||||||
from freqtrade.constants import CANCEL_REASON
|
from freqtrade.constants import CANCEL_REASON
|
||||||
@ -25,8 +25,8 @@ from freqtrade.loggers import setup_logging
|
|||||||
from freqtrade.persistence import PairLocks, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC
|
||||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||||
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange,
|
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re,
|
||||||
patch_get_signal, patch_whitelist)
|
patch_exchange, patch_get_signal, patch_whitelist)
|
||||||
|
|
||||||
|
|
||||||
class DummyCls(Telegram):
|
class DummyCls(Telegram):
|
||||||
@ -55,14 +55,6 @@ class DummyCls(Telegram):
|
|||||||
raise Exception('test')
|
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):
|
def get_telegram_testobject(mocker, default_conf, mock=True, ftbot=None):
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
if mock:
|
if mock:
|
||||||
@ -920,8 +912,8 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None:
|
|||||||
fbuy_mock = MagicMock(return_value=None)
|
fbuy_mock = MagicMock(return_value=None)
|
||||||
mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
|
mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock)
|
||||||
|
|
||||||
telegram, freqtradebot, _, inline_msg_mock = get_telegram_testobject_with_inline(mocker,
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
default_conf)
|
|
||||||
patch_get_signal(freqtradebot, (True, False))
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
@ -929,10 +921,10 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None:
|
|||||||
telegram._forcebuy(update=update, context=context)
|
telegram._forcebuy(update=update, context=context)
|
||||||
|
|
||||||
assert fbuy_mock.call_count == 0
|
assert fbuy_mock.call_count == 0
|
||||||
assert inline_msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert inline_msg_mock.call_args_list[0][0][0] == 'Which pair?'
|
assert msg_mock.call_args_list[0][1]['msg'] == 'Which pair?'
|
||||||
assert inline_msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy'
|
# assert msg_mock.call_args_list[0][1]['callback_query_handler'] == 'forcebuy'
|
||||||
keyboard = inline_msg_mock.call_args_list[0][1]['keyboard']
|
keyboard = msg_mock.call_args_list[0][1]['keyboard']
|
||||||
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4
|
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4
|
||||||
update = MagicMock()
|
update = MagicMock()
|
||||||
update.callback_query = 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
|
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())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
bot = MagicMock()
|
bot = MagicMock()
|
||||||
telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False)
|
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')
|
telegram._send_msg('test')
|
||||||
assert len(bot.method_calls) == 1
|
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:
|
def test__send_msg_network_error(default_conf, mocker, caplog) -> None:
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
|
@ -153,6 +153,8 @@ def test_assert_df_raise(mocker, caplog, ohlcv_history):
|
|||||||
|
|
||||||
def test_assert_df(ohlcv_history, caplog):
|
def test_assert_df(ohlcv_history, caplog):
|
||||||
df_len = len(ohlcv_history) - 1
|
df_len = len(ohlcv_history) - 1
|
||||||
|
ohlcv_history.loc[:, 'buy'] = 0
|
||||||
|
ohlcv_history.loc[:, 'sell'] = 0
|
||||||
# Ensure it's running when passed correctly
|
# Ensure it's running when passed correctly
|
||||||
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
|
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
|
||||||
ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date'])
|
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\."):
|
match=r"Dataframe returned from strategy.*last date\."):
|
||||||
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
|
_STRATEGY.assert_df(ohlcv_history, len(ohlcv_history),
|
||||||
ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date'])
|
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
|
_STRATEGY.disable_dataframe_checks = True
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
Loading…
Reference in New Issue
Block a user