Merge branch 'margin-db' into margin-commands
This commit is contained in:
commit
e99b163985
@ -178,7 +178,9 @@
|
||||
"sell_fill": "on",
|
||||
"buy_cancel": "on",
|
||||
"sell_cancel": "on"
|
||||
}
|
||||
},
|
||||
"reload": true,
|
||||
"balance_dust_level": 0.01
|
||||
},
|
||||
"api_server": {
|
||||
"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 |
|
||||
| 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
|
||||
|
@ -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.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.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.
|
||||
| `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
|
||||
| `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.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
|
||||
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -275,7 +275,8 @@ CONF_SCHEMA = {
|
||||
'default': 'off'
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
'reload': {'type': 'boolean'},
|
||||
},
|
||||
'required': ['enabled', 'token', 'chat_id'],
|
||||
},
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
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:
|
||||
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:
|
||||
|
@ -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'],
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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])
|
||||
|
@ -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"<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:
|
||||
self._send_msg(str(e))
|
||||
|
||||
@ -447,7 +461,8 @@ class Telegram(RPCHandler):
|
||||
],
|
||||
tablefmt='simple')
|
||||
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:
|
||||
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 = "<pre>{}</pre>".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(
|
||||
|
@ -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}")
|
||||
|
@ -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"))
|
||||
|
@ -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,
|
||||
]
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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]
|
||||
|
@ -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())
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user