Merge pull request #3642 from freqtrade/new_release

New release 2020.7
This commit is contained in:
Matthias 2020-08-06 06:50:08 +02:00 committed by GitHub
commit 767332405e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1610 additions and 438 deletions

View File

@ -1,17 +0,0 @@
version: 1
update_configs:
- package_manager: "python"
directory: "/"
update_schedule: "weekly"
allowed_updates:
- match:
update_type: "all"
target_branch: "develop"
- package_manager: "docker"
directory: "/"
update_schedule: "daily"
allowed_updates:
- match:
update_type: "all"

13
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: docker
directory: "/"
schedule:
interval: daily
open-pull-requests-limit: 10
- package-ecosystem: pip
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
target-branch: develop

View File

@ -1,7 +1,7 @@
FROM python:3.8.3-slim-buster FROM python:3.8.5-slim-buster
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install curl build-essential libssl-dev \ && apt-get -y install curl build-essential libssl-dev sqlite3 \
&& apt-get clean \ && apt-get clean \
&& pip install --upgrade pip && pip install --upgrade pip

View File

@ -1,7 +1,7 @@
FROM --platform=linux/arm/v7 python:3.7.7-slim-buster FROM --platform=linux/arm/v7 python:3.7.7-slim-buster
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install curl build-essential libssl-dev libatlas3-base libgfortran5 \ && apt-get -y install curl build-essential libssl-dev libatlas3-base libgfortran5 sqlite3 \
&& apt-get clean \ && apt-get clean \
&& pip install --upgrade pip \ && pip install --upgrade pip \
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf && echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf

View File

@ -66,7 +66,7 @@
}, },
{"method": "AgeFilter", "min_days_listed": 10}, {"method": "AgeFilter", "min_days_listed": 10},
{"method": "PrecisionFilter"}, {"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.01}, {"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010},
{"method": "SpreadFilter", "max_spread_ratio": 0.005} {"method": "SpreadFilter", "max_spread_ratio": 0.005}
], ],
"exchange": { "exchange": {

View File

@ -66,7 +66,7 @@ Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies
#### Exporting trades to file #### Exporting trades to file
```bash ```bash
freqtrade backtesting --export trades freqtrade backtesting --export trades --config config.json --strategy SampleStrategy
``` ```
The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory.

58
docs/bot-basics.md Normal file
View File

@ -0,0 +1,58 @@
# Freqtrade basics
This page provides you some basic concepts on how Freqtrade works and operates.
## Freqtrade terminology
* Trade: Open position.
* Open Order: Order which is currently placed on the exchange, and is not yet complete.
* Pair: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT).
* Timeframe: Candle length to use (e.g. `"5m"`, `"1h"`, ...).
* Indicators: Technical indicators (SMA, EMA, RSI, ...).
* Limit order: Limit orders which execute at the defined limit price or better.
* Market order: Guaranteed to fill, may move price depending on the order size.
## Fee handling
All profit calculations of Freqtrade include fees. For Backtesting / Hyperopt / Dry-run modes, the exchange default fee is used (lowest tier on the exchange). For live operations, fees are used as applied by the exchange (this includes BNB rebates etc.).
## Bot execution logic
Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop.
By default, loop runs every few seconds (`internals.process_throttle_secs`) and does roughly the following in the following sequence:
* Fetch open trades from persistence.
* Calculate current list of tradable pairs.
* Download ohlcv data for the pairlist including all [informative pairs](strategy-customization.md#get-data-for-non-tradeable-pairs)
This step is only executed once per Candle to avoid unnecessary network traffic.
* Call `bot_loop_start()` strategy callback.
* Analyze strategy per pair.
* Call `populate_indicators()`
* Call `populate_buy_trend()`
* Call `populate_sell_trend()`
* Check timeouts for open orders.
* Calls `check_buy_timeout()` strategy callback for open buy orders.
* Calls `check_sell_timeout()` strategy callback for open sell orders.
* Verifies existing positions and eventually places sell orders.
* Considers stoploss, ROI and sell-signal.
* Determine sell-price based on `ask_strategy` configuration setting.
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
* Check if trade-slots are still available (if `max_open_trades` is reached).
* Verifies buy signal trying to enter new positions.
* Determine buy-price based on `bid_strategy` configuration setting.
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
This loop will be repeated again and again until the bot is stopped.
## Backtesting / Hyperopt execution logic
[backtesting](backtesting.md) or [hyperopt](hyperopt.md) do only part of the above logic, since most of the trading operations are fully simulated.
* Load historic data for configured pairlist.
* Calculate indicators (calls `populate_indicators()`).
* Calls `populate_buy_trend()` and `populate_sell_trend()`
* Loops per candle simulating entry and exit points.
* Generate backtest report output
!!! Note
Both Backtesting and Hyperopt include exchange default Fees in the calculation. Custom fees can be passed to backtesting / hyperopt by specifying the `--fee` argument.

View File

@ -275,7 +275,7 @@ the static list of pairs) if we should buy.
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
This allows to buy using limit orders, sell using This allows to buy using limit orders, sell using
limit-orders, and create stoplosses using using market orders. It also allows to set the limit-orders, and create stoplosses using market orders. It also allows to set the
stoploss "on exchange" which means stoploss order would be placed immediately once stoploss "on exchange" which means stoploss order would be placed immediately once
the buy order is fulfilled. the buy order is fulfilled.
If `stoploss_on_exchange` and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check and update the stoploss on exchange periodically. If `stoploss_on_exchange` and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check and update the stoploss on exchange periodically.
@ -662,16 +662,25 @@ Filters low-value coins which would not allow setting stoplosses.
#### PriceFilter #### PriceFilter
The `PriceFilter` allows filtering of pairs by price. The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
* `min_price`
* `max_price`
* `low_price_ratio`
Currently, only `low_price_ratio` setting is implemented, where a raise of 1 price unit (pip) is below the `low_price_ratio` ratio. The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs.
This option is disabled by default, and will only apply if set to <> 0.
The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs.
This option is disabled by default, and will only apply if set to <> 0.
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
This option is disabled by default, and will only apply if set to <> 0. This option is disabled by default, and will only apply if set to <> 0.
Calculation example: Calculation example:
Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value. Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value.
These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. Here is what the PriceFilters takes over. These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses.
#### ShuffleFilter #### ShuffleFilter

View File

@ -158,6 +158,58 @@ It'll also remove original jsongz data files (`--erase` parameter).
freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase
``` ```
### Subcommand list-data
You can get a list of downloaded data using the `list-data` subcommand.
```
usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [--exchange EXCHANGE]
[--data-format-ohlcv {json,jsongz}]
[-p PAIRS [PAIRS ...]]
optional arguments:
-h, --help show this help message and exit
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
config is provided.
--data-format-ohlcv {json,jsongz}
Storage format for downloaded candle (OHLCV) data.
(default: `json`).
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Show profits for only these pairs. Pairs are space-
separated.
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
--logfile FILE Log to the file specified. Special values are:
'syslog', 'journald'. See the documentation for more
details.
-V, --version show program's version number and exit
-c PATH, --config PATH
Specify configuration file (default:
`userdir/config.json` or `config.json` whichever
exists). Multiple --config options may be used. Can be
set to `-` to read config from stdin.
-d PATH, --datadir PATH
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
```
#### Example list-data
```bash
> freqtrade list-data --userdir ~/.freqtrade/user_data/
Found 33 pair / timeframe combinations.
pairs timeframe
---------- -----------------------------------------
ADA/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
ADA/ETH 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
```
### Pairs file ### Pairs file
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used. In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.

View File

@ -498,8 +498,3 @@ After you run Hyperopt for the desired amount of epochs, you can later list all
Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected.
To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting.
## Next Step
Now you have a perfect bot and want to control it from Telegram. Your
next step is to learn the [Telegram usage](telegram-usage.md).

View File

@ -1,2 +1,2 @@
mkdocs-material==5.3.3 mkdocs-material==5.5.0
mdx_truly_sane_lists==1.2 mdx_truly_sane_lists==1.2

View File

@ -13,6 +13,15 @@ Feel free to use a visual Database editor like SqliteBrowser if you feel more co
sudo apt-get install sqlite3 sudo apt-get install sqlite3
``` ```
### Using sqlite3 via docker-compose
The freqtrade docker image does contain sqlite3, so you can edit the database without having to install anything on the host system.
``` bash
docker-compose exec freqtrade /bin/bash
sqlite3 <databasefile>.sqlite
```
## Open the DB ## Open the DB
```bash ```bash
@ -100,8 +109,8 @@ UPDATE trades
SET is_open=0, SET is_open=0,
close_date=<close_date>, close_date=<close_date>,
close_rate=<close_rate>, close_rate=<close_rate>,
close_profit=close_rate/open_rate-1, close_profit = close_rate / open_rate - 1,
close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * open_rate * 1 - fee_open)), close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * (open_rate * 1 - fee_open))),
sell_reason=<sell_reason> sell_reason=<sell_reason>
WHERE id=<trade_ID_to_update>; WHERE id=<trade_ID_to_update>;
``` ```
@ -111,24 +120,39 @@ WHERE id=<trade_ID_to_update>;
```sql ```sql
UPDATE trades UPDATE trades
SET is_open=0, SET is_open=0,
close_date='2017-12-20 03:08:45.103418', close_date='2020-06-20 03:08:45.103418',
close_rate=0.19638016, close_rate=0.19638016,
close_profit=0.0496, close_profit=0.0496,
close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * 1 - fee_open)) close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open))),
sell_reason='force_sell' sell_reason='force_sell'
WHERE id=31; WHERE id=31;
``` ```
## Insert manually a new trade ## Manually insert a new trade
```sql ```sql
INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date)
VALUES ('bittrex', 'ETH/BTC', 1, 0.0025, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>') VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, <open_rate>, <stake_amount>, <amount>, '<datetime>')
``` ```
##### Example: ### Insert trade example
```sql ```sql
INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date)
VALUES ('bittrex', 'ETH/BTC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000') VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2020-06-28 12:44:24.000000')
``` ```
## Remove trade from the database
Maybe you'd like to remove a trade from the database, because something went wrong.
```sql
DELETE FROM trades WHERE id = <tradeid>;
```
```sql
DELETE FROM trades WHERE id = 31;
```
!!! Warning
This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause.

View File

@ -84,7 +84,7 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai
``` python ``` python
trailing_stop_positive_offset = 0.011 trailing_stop_positive_offset = 0.011
trailing_only_offset_is_reached = true trailing_only_offset_is_reached = True
``` ```
Simplified example: Simplified example:

View File

@ -1,7 +1,12 @@
# Advanced Strategies # Advanced Strategies
This page explains some advanced concepts available for strategies. This page explains some advanced concepts available for strategies.
If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation first. If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first.
[Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs.
!!! Note
All callback methods described below should only be implemented in a strategy if they are actually used.
## Custom order timeout rules ## Custom order timeout rules
@ -89,3 +94,108 @@ class Awesomestrategy(IStrategy):
return True return True
return False return False
``` ```
## Bot loop start callback
A simple callback which is called once at the start of every bot throttling iteration.
This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc.
``` python
import requests
class Awesomestrategy(IStrategy):
# ... populate_* methods
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
if self.config['runmode'].value in ('live', 'dry_run'):
# Assign this to the class by using self.*
# can then be used by populate_* methods
self.remote_data = requests.get('https://some_remote_source.example.com')
```
## Bot order confirmation
### Trade entry (buy order) confirmation
`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect).
``` python
class Awesomestrategy(IStrategy):
# ... populate_* methods
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, **kwargs) -> bool:
"""
Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be bought.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
"""
return True
```
### Trade exit (sell order) confirmation
`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect).
``` python
from freqtrade.persistence import Trade
class Awesomestrategy(IStrategy):
# ... populate_* methods
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
"""
Called right before placing a regular sell order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be sold.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell']
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange.
False aborts the process
"""
if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0:
# Reject force-sells with negative profit
# This is just a sample, please adjust to your needs
# (this does not necessarily make sense, assuming you know when you're force-selling)
return False
return True
```

View File

@ -1,6 +1,8 @@
# Strategy Customization # Strategy Customization
This page explains where to customize your strategies, and add new indicators. This page explains how to customize your strategies, add new indicators and set up trading rules.
Please familiarize yourself with [Freqtrade basics](bot-basics.md) first, which provides overall info on how the bot operates.
## Install a custom strategy file ## Install a custom strategy file
@ -366,6 +368,7 @@ Please always check the mode of operation to select the correct method to get da
- [`available_pairs`](#available_pairs) - Property with tuples listing cached pairs with their intervals (pair, interval). - [`available_pairs`](#available_pairs) - Property with tuples listing cached pairs with their intervals (pair, interval).
- [`current_whitelist()`](#current_whitelist) - Returns a current list of whitelisted pairs. Useful for accessing dynamic whitelists (ie. VolumePairlist) - [`current_whitelist()`](#current_whitelist) - Returns a current list of whitelisted pairs. Useful for accessing dynamic whitelists (ie. VolumePairlist)
- [`get_pair_dataframe(pair, timeframe)`](#get_pair_dataframepair-timeframe) - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes). - [`get_pair_dataframe(pair, timeframe)`](#get_pair_dataframepair-timeframe) - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes).
- [`get_analyzed_dataframe(pair, timeframe)`](#get_analyzed_dataframepair-timeframe) - Returns the analyzed dataframe (after calling `populate_indicators()`, `populate_buy()`, `populate_sell()`) and the time of the latest analysis.
- `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk. - `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk.
- `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on the Market data structure. - `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on the Market data structure.
- `ohlcv(pair, timeframe)` - Currently cached candle (OHLCV) data for the pair, returns DataFrame or empty DataFrame. - `ohlcv(pair, timeframe)` - Currently cached candle (OHLCV) data for the pair, returns DataFrame or empty DataFrame.
@ -384,13 +387,14 @@ if self.dp:
``` ```
#### *current_whitelist()* #### *current_whitelist()*
Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume. Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume.
The strategy might look something like this: The strategy might look something like this:
*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day ATR to buy and sell.* *Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.*
Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day ATR. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least! Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
Since we can't resample our data we will have to use an informative pair; and since our whitelist will be dynamic we don't know which pair(s) to use. Since we can't resample our data we will have to use an informative pair; and since our whitelist will be dynamic we don't know which pair(s) to use.
@ -406,18 +410,49 @@ class SampleStrategy(IStrategy):
def informative_pairs(self): def informative_pairs(self):
# get access to all pairs available in whitelist. # get access to all pairs available in whitelist.
pairs = self.dp.current_whitelist() pairs = self.dp.current_whitelist()
# Assign tf to each pair so they can be downloaded and cached for strategy. # Assign tf to each pair so they can be downloaded and cached for strategy.
informative_pairs = [(pair, '1d') for pair in pairs] informative_pairs = [(pair, '1d') for pair in pairs]
return informative_pairs return informative_pairs
def populate_indicators(self, dataframe, metadata): def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
inf_tf = '1d'
# Get the informative pair # Get the informative pair
informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d') informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d')
# Get the 14 day ATR. # Get the 14 day rsi
atr = ta.ATR(informative, timeperiod=14) informative['rsi'] = ta.RSI(informative, timeperiod=14)
# Rename columns to be unique
informative.columns = [f"{col}_{inf_tf}" for col in informative.columns]
# Assuming inf_tf = '1d' - then the columns will now be:
# date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d
# Combine the 2 dataframes
# all indicators on the informative sample MUST be calculated before this point
dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_{inf_tf}', how='left')
# FFill to have the 1d value available in every row throughout the day.
# Without this, comparisons would only work once per day.
dataframe = dataframe.ffill()
# Calculate rsi of the original dataframe (5m timeframe)
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
# Do other stuff # Do other stuff
# ...
return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
(dataframe['rsi_1d'] < 30) & # Ensure daily RSI is < 30
(dataframe['volume'] > 0) # Ensure this candle had volume (important for backtesting)
),
'buy'] = 1
``` ```
#### *get_pair_dataframe(pair, timeframe)* #### *get_pair_dataframe(pair, timeframe)*
@ -431,13 +466,32 @@ if self.dp:
``` ```
!!! Warning "Warning about backtesting" !!! Warning "Warning about backtesting"
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()` Be careful when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()`
for the backtesting runmode) provides the full time-range in one go, for the backtesting runmode) provides the full time-range in one go,
so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode). so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
!!! Warning "Warning in hyperopt" !!! Warning "Warning in hyperopt"
This option cannot currently be used during hyperopt. This option cannot currently be used during hyperopt.
#### *get_analyzed_dataframe(pair, timeframe)*
This method is used by freqtrade internally to determine the last signal.
It can also be used in specific callbacks to get the signal that caused the action (see [Advanced Strategy Documentation](strategy-advanced.md) for more details on available callbacks).
``` python
# fetch current dataframe
if self.dp:
dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'],
timeframe=self.ticker_interval)
```
!!! Note "No data available"
Returns an empty dataframe if the requested pair was not cached.
This should not happen when using whitelisted pairs.
!!! Warning "Warning in hyperopt"
This option cannot currently be used during hyperopt.
#### *orderbook(pair, maximum)* #### *orderbook(pair, maximum)*
``` python ``` python
@ -470,6 +524,7 @@ if self.dp:
data returned from the exchange and add appropriate error handling / defaults. data returned from the exchange and add appropriate error handling / defaults.
*** ***
### Additional data (Wallets) ### Additional data (Wallets)
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
@ -493,6 +548,7 @@ if self.wallets:
- `get_total(asset)` - total available balance - sum of the 2 above - `get_total(asset)` - total available balance - sum of the 2 above
*** ***
### Additional data (Trades) ### Additional data (Trades)
A history of Trades can be retrieved in the strategy by querying the database. A history of Trades can be retrieved in the strategy by querying the database.

View File

@ -56,6 +56,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/show_config` | | Shows part of the current configuration with relevant settings to operation | `/show_config` | | Shows part of the current configuration with relevant settings to operation
| `/status` | | Lists all open trades | `/status` | | Lists all open trades
| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) | `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
| `/trades [limit]` | | List all recently closed trades in a table format.
| `/count` | | Displays number of trades used and available | `/count` | | Displays number of trades used and available
| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance | `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).

View File

@ -47,6 +47,7 @@ Different payloads can be configured for different events. Not all fields are ne
The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format. The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id`
* `exchange` * `exchange`
* `pair` * `pair`
* `limit` * `limit`
@ -63,6 +64,7 @@ Possible parameters are:
The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format. The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id`
* `exchange` * `exchange`
* `pair` * `pair`
* `limit` * `limit`
@ -79,6 +81,7 @@ Possible parameters are:
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id`
* `exchange` * `exchange`
* `pair` * `pair`
* `gain` * `gain`
@ -100,6 +103,7 @@ Possible parameters are:
The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format. The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format.
Possible parameters are: Possible parameters are:
* `trade_id`
* `exchange` * `exchange`
* `pair` * `pair`
* `gain` * `gain`

View File

@ -1,5 +1,5 @@
""" Freqtrade bot """ """ Freqtrade bot """
__version__ = '2020.6' __version__ = '2020.7'
if __version__ == 'develop': if __version__ == 'develop':

View File

@ -9,7 +9,8 @@ Note: Be careful with file-scoped imports in these subfiles.
from freqtrade.commands.arguments import Arguments from freqtrade.commands.arguments import Arguments
from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.build_config_commands import start_new_config
from freqtrade.commands.data_commands import (start_convert_data, from freqtrade.commands.data_commands import (start_convert_data,
start_download_data) start_download_data,
start_list_data)
from freqtrade.commands.deploy_commands import (start_create_userdir, from freqtrade.commands.deploy_commands import (start_create_userdir,
start_new_hyperopt, start_new_hyperopt,
start_new_strategy) start_new_strategy)

View File

@ -54,6 +54,8 @@ ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"]
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"] ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange",
"timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"]
@ -78,7 +80,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
"print_json", "hyperopt_show_no_header"] "print_json", "hyperopt_show_no_header"]
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-markets", "list-pairs", "list-strategies", "list-markets", "list-pairs", "list-strategies", "list-data",
"list-hyperopts", "hyperopt-list", "hyperopt-show", "list-hyperopts", "hyperopt-list", "hyperopt-show",
"plot-dataframe", "plot-profit", "show-trades"] "plot-dataframe", "plot-profit", "show-trades"]
@ -159,7 +161,7 @@ class Arguments:
self._build_args(optionlist=['version'], parser=self.parser) self._build_args(optionlist=['version'], parser=self.parser)
from freqtrade.commands import (start_create_userdir, start_convert_data, from freqtrade.commands import (start_create_userdir, start_convert_data,
start_download_data, start_download_data, start_list_data,
start_hyperopt_list, start_hyperopt_show, start_hyperopt_list, start_hyperopt_show,
start_list_exchanges, start_list_hyperopts, start_list_exchanges, start_list_hyperopts,
start_list_markets, start_list_strategies, start_list_markets, start_list_strategies,
@ -233,6 +235,15 @@ class Arguments:
convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False)) convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False))
self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd) self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd)
# Add list-data subcommand
list_data_cmd = subparsers.add_parser(
'list-data',
help='List downloaded data.',
parents=[_common_parser],
)
list_data_cmd.set_defaults(func=start_list_data)
self._build_args(optionlist=ARGS_LIST_DATA, parser=list_data_cmd)
# Add backtesting subcommand # Add backtesting subcommand
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.', backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.',
parents=[_common_parser, _strategy_parser]) parents=[_common_parser, _strategy_parser])

View File

@ -1,5 +1,6 @@
import logging import logging
import sys import sys
from collections import defaultdict
from typing import Any, Dict, List from typing import Any, Dict, List
import arrow import arrow
@ -11,6 +12,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv,
refresh_backtest_ohlcv_data, refresh_backtest_ohlcv_data,
refresh_backtest_trades_data) refresh_backtest_trades_data)
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.resolvers import ExchangeResolver from freqtrade.resolvers import ExchangeResolver
from freqtrade.state import RunMode from freqtrade.state import RunMode
@ -88,3 +90,30 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
convert_trades_format(config, convert_trades_format(config,
convert_from=args['format_from'], convert_to=args['format_to'], convert_from=args['format_from'], convert_to=args['format_to'],
erase=args['erase']) erase=args['erase'])
def start_list_data(args: Dict[str, Any]) -> None:
"""
List available backtest data
"""
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
from freqtrade.data.history.idatahandler import get_datahandler
from tabulate import tabulate
dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv'])
paircombs = dhc.ohlcv_get_available_data(config['datadir'])
if args['pairs']:
paircombs = [comb for comb in paircombs if comb[0] in args['pairs']]
print(f"Found {len(paircombs)} pair / timeframe combinations.")
groupedpair = defaultdict(list)
for pair, timeframe in sorted(paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]))):
groupedpair[pair].append(timeframe)
if groupedpair:
print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()],
headers=("Pair", "Timeframe"),
tablefmt='psql', stralign='right'))

View File

@ -156,7 +156,9 @@ CONF_SCHEMA = {
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss_on_exchange': {'type': 'boolean'}, 'stoploss_on_exchange': {'type': 'boolean'},
'stoploss_on_exchange_interval': {'type': 'number'} 'stoploss_on_exchange_interval': {'type': 'number'},
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
'maximum': 1.0}
}, },
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] 'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
}, },
@ -339,4 +341,5 @@ CANCEL_REASON = {
} }
# List of pairs with their timeframes # List of pairs with their timeframes
ListPairsWithTimeframes = List[Tuple[str, str]] PairWithTimeframe = Tuple[str, str]
ListPairsWithTimeframes = List[PairWithTimeframe]

View File

@ -5,16 +5,17 @@ including ticker and orderbook data, live and historical candle (OHLCV) data
Common Interface for bot and strategy to access data. Common Interface for bot and strategy to access data.
""" """
import logging import logging
from typing import Any, Dict, List, Optional from datetime import datetime, timezone
from typing import Any, Dict, List, Optional, Tuple
from arrow import Arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
from freqtrade.data.history import load_pair_history from freqtrade.data.history import load_pair_history
from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.constants import ListPairsWithTimeframes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -25,6 +26,18 @@ class DataProvider:
self._config = config self._config = config
self._exchange = exchange self._exchange = exchange
self._pairlists = pairlists self._pairlists = pairlists
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None:
"""
Store cached Dataframe.
Using private method as this should never be used by a user
(but the class is exposed via `self.dp` to the strategy)
:param pair: pair to get the data for
:param timeframe: Timeframe to get data for
:param dataframe: analyzed dataframe
"""
self.__cached_pairs[(pair, timeframe)] = (dataframe, Arrow.utcnow().datetime)
def refresh(self, def refresh(self,
pairlist: ListPairsWithTimeframes, pairlist: ListPairsWithTimeframes,
@ -89,6 +102,20 @@ class DataProvider:
logger.warning(f"No data found for ({pair}, {timeframe}).") logger.warning(f"No data found for ({pair}, {timeframe}).")
return data return data
def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]:
"""
:param pair: pair to get the data for
:param timeframe: timeframe to get data for
:return: Tuple of (Analyzed Dataframe, lastrefreshed) for the requested pair / timeframe
combination.
Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached.
"""
if (pair, timeframe) in self.__cached_pairs:
return self.__cached_pairs[(pair, timeframe)]
else:
return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc))
def market(self, pair: str) -> Optional[Dict[str, Any]]: def market(self, pair: str) -> Optional[Dict[str, Any]]:
""" """
Return market data for the pair Return market data for the pair
@ -105,7 +132,7 @@ class DataProvider:
""" """
try: try:
return self._exchange.fetch_ticker(pair) return self._exchange.fetch_ticker(pair)
except DependencyException: except ExchangeError:
return {} return {}
def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: def orderbook(self, pair: str, maximum: int) -> Dict[str, List]:

View File

@ -13,6 +13,7 @@ from typing import List, Optional, Type
from pandas import DataFrame from pandas import DataFrame
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.data.converter import (clean_ohlcv_dataframe, from freqtrade.data.converter import (clean_ohlcv_dataframe,
trades_remove_duplicates, trim_dataframe) trades_remove_duplicates, trim_dataframe)
from freqtrade.exchange import timeframe_to_seconds from freqtrade.exchange import timeframe_to_seconds
@ -28,6 +29,14 @@ class IDataHandler(ABC):
def __init__(self, datadir: Path) -> None: def __init__(self, datadir: Path) -> None:
self._datadir = datadir self._datadir = datadir
@abstractclassmethod
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
"""
Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files
:return: List of Tuples of (pair, timeframe)
"""
@abstractclassmethod @abstractclassmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]: def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
""" """

View File

@ -8,7 +8,8 @@ from pandas import DataFrame, read_json, to_datetime
from freqtrade import misc from freqtrade import misc
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS,
ListPairsWithTimeframes)
from freqtrade.data.converter import trades_dict_to_list from freqtrade.data.converter import trades_dict_to_list
from .idatahandler import IDataHandler, TradeList from .idatahandler import IDataHandler, TradeList
@ -21,6 +22,18 @@ class JsonDataHandler(IDataHandler):
_use_zip = False _use_zip = False
_columns = DEFAULT_DATAFRAME_COLUMNS _columns = DEFAULT_DATAFRAME_COLUMNS
@classmethod
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
"""
Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files
:return: List of Tuples of (pair, timeframe)
"""
_tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.json)', p.name)
for p in datadir.glob(f"*.{cls._get_file_extension()}")]
return [(match[1].replace('_', '/'), match[2]) for match in _tmp
if match and len(match.groups()) > 1]
@classmethod @classmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]: def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
""" """

View File

@ -37,7 +37,21 @@ class InvalidOrderException(FreqtradeException):
""" """
class TemporaryError(FreqtradeException): class RetryableOrderError(InvalidOrderException):
"""
This is returned when the order is not found.
This Error will be repeated with increasing backof (in line with DDosError).
"""
class ExchangeError(DependencyException):
"""
Error raised out of the exchange.
Has multiple Errors to determine the appropriate error.
"""
class TemporaryError(ExchangeError):
""" """
Temporary network or exchange related error. Temporary network or exchange related error.
This could happen when an exchange is congested, unavailable, or the user This could happen when an exchange is congested, unavailable, or the user
@ -45,6 +59,13 @@ class TemporaryError(FreqtradeException):
""" """
class DDosProtection(TemporaryError):
"""
Temporary error caused by DDOS protection.
Bot will wait for a second and then retry.
"""
class StrategyError(FreqtradeException): class StrategyError(FreqtradeException):
""" """
Errors with custom user-code deteced. Errors with custom user-code deteced.

View File

@ -4,9 +4,11 @@ from typing import Dict
import ccxt import ccxt
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, ExchangeError,
OperationalException, TemporaryError) InvalidOrderException, OperationalException,
TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -39,6 +41,7 @@ class Binance(Exchange):
""" """
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
""" """
creates a stoploss limit order. creates a stoploss limit order.
@ -77,8 +80,8 @@ class Binance(Exchange):
'stop price: %s. limit: %s', pair, stop_price, rate) 'stop price: %s. limit: %s', pair, stop_price, rate)
return order return order
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise ExchangeError(
f'Insufficient funds to create {ordertype} sell order on market {pair}.' f'Insufficient funds to create {ordertype} sell order on market {pair}. '
f'Tried to sell amount {amount} at rate {rate}. ' f'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
@ -88,6 +91,8 @@ class Binance(Exchange):
f'Could not create {ordertype} sell order on market {pair}. ' f'Could not create {ordertype} sell order on market {pair}. '
f'Tried to sell amount {amount} at rate {rate}. ' f'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e

View File

@ -1,6 +1,10 @@
import asyncio
import logging import logging
import time
from functools import wraps
from freqtrade.exceptions import TemporaryError from freqtrade.exceptions import (DDosProtection, RetryableOrderError,
TemporaryError)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -88,6 +92,13 @@ MAP_EXCHANGE_CHILDCLASS = {
} }
def calculate_backoff(retrycount, max_retries):
"""
Calculate backoff
"""
return (max_retries - retrycount) ** 2 + 1
def retrier_async(f): def retrier_async(f):
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
count = kwargs.pop('count', API_RETRY_COUNT) count = kwargs.pop('count', API_RETRY_COUNT)
@ -99,6 +110,10 @@ def retrier_async(f):
count -= 1 count -= 1
kwargs.update({'count': count}) kwargs.update({'count': count})
logger.warning('retrying %s() still for %s times', f.__name__, count) logger.warning('retrying %s() still for %s times', f.__name__, count)
if isinstance(ex, DDosProtection):
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}")
await asyncio.sleep(backoff_delay)
return await wrapper(*args, **kwargs) return await wrapper(*args, **kwargs)
else: else:
logger.warning('Giving up retrying: %s()', f.__name__) logger.warning('Giving up retrying: %s()', f.__name__)
@ -106,19 +121,31 @@ def retrier_async(f):
return wrapper return wrapper
def retrier(f): def retrier(_func=None, retries=API_RETRY_COUNT):
def wrapper(*args, **kwargs): def decorator(f):
count = kwargs.pop('count', API_RETRY_COUNT) @wraps(f)
try: def wrapper(*args, **kwargs):
return f(*args, **kwargs) count = kwargs.pop('count', retries)
except TemporaryError as ex: try:
logger.warning('%s() returned exception: "%s"', f.__name__, ex) return f(*args, **kwargs)
if count > 0: except (TemporaryError, RetryableOrderError) as ex:
count -= 1 logger.warning('%s() returned exception: "%s"', f.__name__, ex)
kwargs.update({'count': count}) if count > 0:
logger.warning('retrying %s() still for %s times', f.__name__, count) count -= 1
return wrapper(*args, **kwargs) kwargs.update({'count': count})
else: logger.warning('retrying %s() still for %s times', f.__name__, count)
logger.warning('Giving up retrying: %s()', f.__name__) if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError):
raise ex # increasing backoff
return wrapper backoff_delay = calculate_backoff(count + 1, retries)
logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}")
time.sleep(backoff_delay)
return wrapper(*args, **kwargs)
else:
logger.warning('Giving up retrying: %s()', f.__name__)
raise ex
return wrapper
# Support both @retrier and @retrier(retries=2) syntax
if _func is None:
return decorator
else:
return decorator(_func)

View File

@ -18,12 +18,13 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE,
TRUNCATE, decimal_to_precision) TRUNCATE, decimal_to_precision)
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, ExchangeError,
OperationalException, TemporaryError) InvalidOrderException, OperationalException,
RetryableOrderError, TemporaryError)
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
from freqtrade.misc import deep_merge_dicts, safe_value_fallback from freqtrade.misc import deep_merge_dicts, safe_value_fallback
from freqtrade.constants import ListPairsWithTimeframes
CcxtModuleType = Any CcxtModuleType = Any
@ -186,6 +187,11 @@ class Exchange:
def timeframes(self) -> List[str]: def timeframes(self) -> List[str]:
return list((self._api.timeframes or {}).keys()) return list((self._api.timeframes or {}).keys())
@property
def ohlcv_candle_limit(self) -> int:
"""exchange ohlcv candle limit"""
return int(self._ohlcv_candle_limit)
@property @property
def markets(self) -> Dict: def markets(self) -> Dict:
"""exchange ccxt markets""" """exchange ccxt markets"""
@ -351,7 +357,7 @@ class Exchange:
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]: for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
if pair in self.markets and self.markets[pair].get('active'): if pair in self.markets and self.markets[pair].get('active'):
return pair return pair
raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") raise ExchangeError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
def validate_timeframes(self, timeframe: Optional[str]) -> None: def validate_timeframes(self, timeframe: Optional[str]) -> None:
""" """
@ -518,15 +524,17 @@ class Exchange:
amount, rate_for_order, params) amount, rate_for_order, params)
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise ExchangeError(
f'Insufficient funds to create {ordertype} {side} order on market {pair}.' f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}.' f'Tried to {side} amount {amount} at rate {rate}.'
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise DependencyException( raise ExchangeError(
f'Could not create {ordertype} {side} order on market {pair}.' f'Could not create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}.' f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
@ -606,6 +614,8 @@ class Exchange:
balances.pop("used", None) balances.pop("used", None)
return balances return balances
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
@ -620,6 +630,8 @@ class Exchange:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching tickers in batch. ' f'Exchange {self._api.name} does not support fetching tickers in batch. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e
@ -630,9 +642,11 @@ class Exchange:
def fetch_ticker(self, pair: str) -> dict: def fetch_ticker(self, pair: str) -> dict:
try: try:
if pair not in self._api.markets or not self._api.markets[pair].get('active'): if pair not in self._api.markets or not self._api.markets[pair].get('active'):
raise DependencyException(f"Pair {pair} not available") raise ExchangeError(f"Pair {pair} not available")
data = self._api.fetch_ticker(pair) data = self._api.fetch_ticker(pair)
return data return data
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e
@ -766,6 +780,8 @@ class Exchange:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical ' f'Exchange {self._api.name} does not support fetching historical '
f'candle (OHLCV) data. Message: {e}') from e f'candle (OHLCV) data. Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(f'Could not fetch historical candle (OHLCV) data ' raise TemporaryError(f'Could not fetch historical candle (OHLCV) data '
f'for pair {pair} due to {e.__class__.__name__}. ' f'for pair {pair} due to {e.__class__.__name__}. '
@ -802,6 +818,8 @@ class Exchange:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical trade data.' f'Exchange {self._api.name} does not support fetching historical trade data.'
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. ' raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. '
f'Message: {e}') from e f'Message: {e}') from e
@ -933,7 +951,7 @@ class Exchange:
def check_order_canceled_empty(self, order: Dict) -> bool: def check_order_canceled_empty(self, order: Dict) -> bool:
""" """
Verify if an order has been cancelled without being partially filled Verify if an order has been cancelled without being partially filled
:param order: Order dict as returned from get_order() :param order: Order dict as returned from fetch_order()
:return: True if order has been cancelled without being filled, False otherwise. :return: True if order has been cancelled without being filled, False otherwise.
""" """
return order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0 return order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0
@ -948,13 +966,15 @@ class Exchange:
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
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
# Assign method to get_stoploss_order to allow easy overriding in other classes # Assign method to fetch_stoploss_order to allow easy overriding in other classes
cancel_stoploss_order = cancel_order cancel_stoploss_order = cancel_order
def is_cancel_order_result_suitable(self, corder) -> bool: def is_cancel_order_result_suitable(self, corder) -> bool:
@ -968,7 +988,7 @@ class Exchange:
""" """
Cancel order returning a result. Cancel order returning a result.
Creates a fake result if cancel order returns a non-usable result Creates a fake result if cancel order returns a non-usable result
and get_order does not work (certain exchanges don't return cancelled orders) and fetch_order does not work (certain exchanges don't return cancelled orders)
:param order_id: Orderid to cancel :param order_id: Orderid to cancel
:param pair: Pair corresponding to order_id :param pair: Pair corresponding to order_id
:param amount: Amount to use for fake response :param amount: Amount to use for fake response
@ -981,7 +1001,7 @@ class Exchange:
except InvalidOrderException: except InvalidOrderException:
logger.warning(f"Could not cancel order {order_id}.") logger.warning(f"Could not cancel order {order_id}.")
try: try:
order = self.get_order(order_id, pair) order = self.fetch_order(order_id, pair)
except InvalidOrderException: except InvalidOrderException:
logger.warning(f"Could not fetch cancelled order {order_id}.") logger.warning(f"Could not fetch cancelled order {order_id}.")
order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
@ -989,7 +1009,7 @@ class Exchange:
return order return order
@retrier @retrier
def get_order(self, order_id: str, pair: str) -> Dict: def fetch_order(self, order_id: str, pair: str) -> Dict:
if self._config['dry_run']: if self._config['dry_run']:
try: try:
order = self._dry_run_open_orders[order_id] order = self._dry_run_open_orders[order_id]
@ -1000,17 +1020,22 @@ class Exchange:
f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e
try: try:
return self._api.fetch_order(order_id, pair) return self._api.fetch_order(order_id, pair)
except ccxt.OrderNotFound as e:
raise RetryableOrderError(
f'Order not found (id: {order_id}). Message: {e}') from e
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise InvalidOrderException( raise InvalidOrderException(
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(e) from e raise OperationalException(e) from e
# Assign method to get_stoploss_order to allow easy overriding in other classes # Assign method to fetch_stoploss_order to allow easy overriding in other classes
get_stoploss_order = get_order fetch_stoploss_order = fetch_order
@retrier @retrier
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
@ -1027,6 +1052,8 @@ class Exchange:
raise OperationalException( raise OperationalException(
f'Exchange {self._api.name} does not support fetching order book.' f'Exchange {self._api.name} does not support fetching order book.'
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e
@ -1063,7 +1090,8 @@ class Exchange:
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]
return matched_trades return matched_trades
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e
@ -1080,6 +1108,8 @@ class Exchange:
return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount,
price=price, takerOrMaker=taker_or_maker)['rate'] price=price, takerOrMaker=taker_or_maker)['rate']
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e
@ -1129,7 +1159,7 @@ class Exchange:
fee_to_quote_rate = safe_value_fallback(tick, tick, 'last', 'ask') fee_to_quote_rate = safe_value_fallback(tick, tick, 'last', 'ask')
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8) return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
except DependencyException: except ExchangeError:
return None return None
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]: def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:

View File

@ -4,8 +4,9 @@ from typing import Dict
import ccxt import ccxt
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, ExchangeError,
OperationalException, TemporaryError) InvalidOrderException, OperationalException,
TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
@ -26,6 +27,7 @@ class Ftx(Exchange):
""" """
return order['type'] == 'stop' and stop_loss > float(order['price']) return order['type'] == 'stop' and stop_loss > float(order['price'])
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
""" """
Creates a stoploss order. Creates a stoploss order.
@ -59,7 +61,7 @@ class Ftx(Exchange):
'stop price: %s.', pair, stop_price) 'stop price: %s.', pair, stop_price)
return order return order
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise ExchangeError(
f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Insufficient funds to create {ordertype} sell order on market {pair}. '
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e f'Message: {e}') from e
@ -68,6 +70,8 @@ class Ftx(Exchange):
f'Could not create {ordertype} sell order on market {pair}. ' f'Could not create {ordertype} sell order on market {pair}. '
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
@ -75,7 +79,7 @@ class Ftx(Exchange):
raise OperationalException(e) from e raise OperationalException(e) from e
@retrier @retrier
def get_stoploss_order(self, order_id: str, pair: str) -> Dict: def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict:
if self._config['dry_run']: if self._config['dry_run']:
try: try:
order = self._dry_run_open_orders[order_id] order = self._dry_run_open_orders[order_id]
@ -96,6 +100,8 @@ class Ftx(Exchange):
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise InvalidOrderException( raise InvalidOrderException(
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
@ -111,6 +117,8 @@ class Ftx(Exchange):
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
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e

View File

@ -4,8 +4,9 @@ from typing import Dict
import ccxt import ccxt
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import (DDosProtection, ExchangeError,
OperationalException, TemporaryError) InvalidOrderException, OperationalException,
TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
@ -45,6 +46,8 @@ class Kraken(Exchange):
balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used'] balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used']
return balances return balances
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
@ -58,6 +61,7 @@ class Kraken(Exchange):
""" """
return order['type'] == 'stop-loss' and stop_loss > float(order['price']) return order['type'] == 'stop-loss' and stop_loss > float(order['price'])
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
""" """
Creates a stoploss market order. Creates a stoploss market order.
@ -84,8 +88,8 @@ class Kraken(Exchange):
'stop price: %s.', pair, stop_price) 'stop price: %s.', pair, stop_price)
return order return order
except ccxt.InsufficientFunds as e: except ccxt.InsufficientFunds as e:
raise DependencyException( raise ExchangeError(
f'Insufficient funds to create {ordertype} sell order on market {pair}.' f'Insufficient funds to create {ordertype} sell order on market {pair}. '
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
@ -93,6 +97,8 @@ class Kraken(Exchange):
f'Could not create {ordertype} sell order on market {pair}. ' f'Could not create {ordertype} sell order on market {pair}. '
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e: except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError( raise TemporaryError(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e

View File

@ -11,14 +11,14 @@ from typing import Any, Dict, List, Optional
import arrow import arrow
from cachetools import TTLCache from cachetools import TTLCache
from requests.exceptions import RequestException
from freqtrade import __version__, constants, persistence from freqtrade import __version__, constants, persistence
from freqtrade.configuration import validate_config_consistency from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.exceptions import DependencyException, InvalidOrderException, PricingError from freqtrade.exceptions import (DependencyException, ExchangeError,
InvalidOrderException, PricingError)
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
from freqtrade.misc import safe_value_fallback from freqtrade.misc import safe_value_fallback
from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.pairlist.pairlistmanager import PairListManager
@ -119,6 +119,8 @@ class FreqtradeBot:
if self.config['cancel_open_orders_on_exit']: if self.config['cancel_open_orders_on_exit']:
self.cancel_all_open_orders() self.cancel_all_open_orders()
self.check_for_open_trades()
self.rpc.cleanup() self.rpc.cleanup()
persistence.cleanup() persistence.cleanup()
@ -151,6 +153,10 @@ class FreqtradeBot:
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
self.strategy.informative_pairs()) self.strategy.informative_pairs())
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
self.strategy.analyze(self.active_pair_whitelist)
with self._sell_lock: with self._sell_lock:
# Check and handle any timed out open orders # Check and handle any timed out open orders
self.check_handle_timedout() self.check_handle_timedout()
@ -175,6 +181,24 @@ class FreqtradeBot:
if self.config['cancel_open_orders_on_exit']: if self.config['cancel_open_orders_on_exit']:
self.cancel_all_open_orders() self.cancel_all_open_orders()
def check_for_open_trades(self):
"""
Notify the user when the bot is stopped
and there are still open trades active.
"""
open_trades = Trade.get_trades([Trade.is_open == 1]).all()
if len(open_trades) != 0:
msg = {
'type': RPCMessageType.WARNING_NOTIFICATION,
'status': f"{len(open_trades)} open trades active.\n\n"
f"Handle these trades manually on {self.exchange.name}, "
f"or '/start' the bot again and use '/stopbuy' "
f"to handle open trades gracefully. \n"
f"{'Trades are simulated.' if self.config['dry_run'] else ''}",
}
self.rpc.send_msg(msg)
def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]: def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]:
""" """
Refresh active whitelist from pairlist or edge and extend it with Refresh active whitelist from pairlist or edge and extend it with
@ -420,9 +444,8 @@ class FreqtradeBot:
return False return False
# running get_signal on historical data fetched # running get_signal on historical data fetched
(buy, sell) = self.strategy.get_signal( analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe)
pair, self.strategy.timeframe, (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df)
self.dataprovider.ohlcv(pair, self.strategy.timeframe))
if buy and not sell: if buy and not sell:
stake_amount = self.get_trade_stake_amount(pair) stake_amount = self.get_trade_stake_amount(pair)
@ -495,6 +518,12 @@ class FreqtradeBot:
amount = stake_amount / buy_limit_requested amount = stake_amount / buy_limit_requested
order_type = self.strategy.order_types['buy'] order_type = self.strategy.order_types['buy']
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested,
time_in_force=time_in_force):
logger.info(f"User requested abortion of buying {pair}")
return False
order = self.exchange.buy(pair=pair, ordertype=order_type, order = self.exchange.buy(pair=pair, ordertype=order_type,
amount=amount, rate=buy_limit_requested, amount=amount, rate=buy_limit_requested,
time_in_force=time_in_force) time_in_force=time_in_force)
@ -569,6 +598,7 @@ class FreqtradeBot:
Sends rpc notification when a buy occured. Sends rpc notification when a buy occured.
""" """
msg = { msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY_NOTIFICATION, 'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': self.exchange.name.capitalize(), 'exchange': self.exchange.name.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -592,6 +622,7 @@ class FreqtradeBot:
current_rate = self.get_buy_rate(trade.pair, False) current_rate = self.get_buy_rate(trade.pair, False)
msg = { msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
'exchange': self.exchange.name.capitalize(), 'exchange': self.exchange.name.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
@ -697,9 +728,10 @@ class FreqtradeBot:
if (config_ask_strategy.get('use_sell_signal', True) or if (config_ask_strategy.get('use_sell_signal', True) or
config_ask_strategy.get('ignore_roi_if_buy_signal', False)): config_ask_strategy.get('ignore_roi_if_buy_signal', False)):
(buy, sell) = self.strategy.get_signal( analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair,
trade.pair, self.strategy.timeframe, self.strategy.timeframe)
self.dataprovider.ohlcv(trade.pair, self.strategy.timeframe))
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df)
if config_ask_strategy.get('use_order_book', False): if config_ask_strategy.get('use_order_book', False):
order_book_min = config_ask_strategy.get('order_book_min', 1) order_book_min = config_ask_strategy.get('order_book_min', 1)
@ -755,7 +787,7 @@ class FreqtradeBot:
logger.warning('Selling the trade forcefully') logger.warning('Selling the trade forcefully')
self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL) self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL)
except DependencyException: except ExchangeError:
trade.stoploss_order_id = None trade.stoploss_order_id = None
logger.exception('Unable to place a stoploss order on exchange.') logger.exception('Unable to place a stoploss order on exchange.')
return False return False
@ -773,8 +805,8 @@ class FreqtradeBot:
try: try:
# First we check if there is already a stoploss on exchange # First we check if there is already a stoploss on exchange
stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \ stoploss_order = self.exchange.fetch_stoploss_order(
if trade.stoploss_order_id else None trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None
except InvalidOrderException as exception: except InvalidOrderException as exception:
logger.warning('Unable to fetch stoploss order: %s', exception) logger.warning('Unable to fetch stoploss order: %s', exception)
@ -795,10 +827,8 @@ class FreqtradeBot:
return False return False
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
if (not stoploss_order): if not stoploss_order:
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
stop_price = trade.open_rate * (1 + stoploss) stop_price = trade.open_rate * (1 + stoploss)
if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price): if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price):
@ -890,8 +920,8 @@ class FreqtradeBot:
try: try:
if not trade.open_order_id: if not trade.open_order_id:
continue continue
order = self.exchange.get_order(trade.open_order_id, trade.pair) order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
except (RequestException, DependencyException, InvalidOrderException): except (ExchangeError, InvalidOrderException):
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue continue
@ -923,7 +953,7 @@ class FreqtradeBot:
for trade in Trade.get_open_order_trades(): for trade in Trade.get_open_order_trades():
try: try:
order = self.exchange.get_order(trade.open_order_id, trade.pair) order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
except (DependencyException, InvalidOrderException): except (DependencyException, InvalidOrderException):
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
continue continue
@ -1077,12 +1107,20 @@ class FreqtradeBot:
order_type = self.strategy.order_types.get("emergencysell", "market") order_type = self.strategy.order_types.get("emergencysell", "market")
amount = self._safe_sell_amount(trade.pair, trade.amount) amount = self._safe_sell_amount(trade.pair, trade.amount)
time_in_force = self.strategy.order_time_in_force['sell']
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
time_in_force=time_in_force,
sell_reason=sell_reason.value):
logger.info(f"User requested abortion of selling {trade.pair}")
return False
# Execute sell and update trade record # Execute sell and update trade record
order = self.exchange.sell(pair=str(trade.pair), order = self.exchange.sell(pair=str(trade.pair),
ordertype=order_type, ordertype=order_type,
amount=amount, rate=limit, amount=amount, rate=limit,
time_in_force=self.strategy.order_time_in_force['sell'] time_in_force=time_in_force
) )
trade.open_order_id = order['id'] trade.open_order_id = order['id']
@ -1113,6 +1151,7 @@ class FreqtradeBot:
msg = { msg = {
'type': RPCMessageType.SELL_NOTIFICATION, 'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': trade.id,
'exchange': trade.exchange.capitalize(), 'exchange': trade.exchange.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
'gain': gain, 'gain': gain,
@ -1155,6 +1194,7 @@ class FreqtradeBot:
msg = { msg = {
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'trade_id': trade.id,
'exchange': trade.exchange.capitalize(), 'exchange': trade.exchange.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
'gain': gain, 'gain': gain,
@ -1202,7 +1242,7 @@ class FreqtradeBot:
# Update trade with order values # Update trade with order values
logger.info('Found open order for %s', trade) logger.info('Found open order for %s', trade)
try: try:
order = action_order or self.exchange.get_order(order_id, trade.pair) order = action_order or self.exchange.fetch_order(order_id, trade.pair)
except InvalidOrderException as exception: except InvalidOrderException as exception:
logger.warning('Unable to fetch order %s: %s', order_id, exception) logger.warning('Unable to fetch order %s: %s', order_id, exception)
return False return False

View File

@ -101,7 +101,7 @@ class Backtesting:
if len(self.pairlists.whitelist) == 0: if len(self.pairlists.whitelist) == 0:
raise OperationalException("No pair in whitelist.") raise OperationalException("No pair in whitelist.")
if config.get('fee'): if config.get('fee', None) is not None:
self.fee = config['fee'] self.fee = config['fee']
else: else:
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])

View File

@ -5,6 +5,7 @@ import logging
import arrow import arrow
from typing import Any, Dict from typing import Any, Dict
from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural from freqtrade.misc import plural
from freqtrade.pairlist.IPairList import IPairList from freqtrade.pairlist.IPairList import IPairList
@ -23,6 +24,13 @@ class AgeFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._min_days_listed = pairlistconfig.get('min_days_listed', 10) self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
if self._min_days_listed < 1:
raise OperationalException("AgeFilter requires min_days_listed must be >= 1")
if self._min_days_listed > exchange.ohlcv_candle_limit:
raise OperationalException("AgeFilter requires min_days_listed must not exceed "
"exchange max request size "
f"({exchange.ohlcv_candle_limit})")
self._enabled = self._min_days_listed >= 1 self._enabled = self._min_days_listed >= 1
@property @property
@ -69,7 +77,7 @@ class AgeFilter(IPairList):
return True return True
else: else:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because age is less than " f"because age {len(daily_candles)} is less than "
f"{self._min_days_listed} " f"{self._min_days_listed} "
f"{plural(self._min_days_listed, 'day')}") f"{plural(self._min_days_listed, 'day')}")
return False return False

View File

@ -18,7 +18,11 @@ class PriceFilter(IPairList):
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0) self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0)
self._enabled = self._low_price_ratio != 0 self._min_price = pairlistconfig.get('min_price', 0)
self._max_price = pairlistconfig.get('max_price', 0)
self._enabled = ((self._low_price_ratio != 0) or
(self._min_price != 0) or
(self._max_price != 0))
@property @property
def needstickers(self) -> bool: def needstickers(self) -> bool:
@ -33,7 +37,18 @@ class PriceFilter(IPairList):
""" """
Short whitelist method description - used for startup-messages Short whitelist method description - used for startup-messages
""" """
return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%." active_price_filters = []
if self._low_price_ratio != 0:
active_price_filters.append(f"below {self._low_price_ratio * 100}%")
if self._min_price != 0:
active_price_filters.append(f"below {self._min_price:.8f}")
if self._max_price != 0:
active_price_filters.append(f"above {self._max_price:.8f}")
if len(active_price_filters):
return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}."
return f"{self.name} - No price filters configured."
def _validate_pair(self, ticker) -> bool: def _validate_pair(self, ticker) -> bool:
""" """
@ -41,15 +56,33 @@ class PriceFilter(IPairList):
:param ticker: ticker dict as returned from ccxt.load_markets() :param ticker: ticker dict as returned from ccxt.load_markets()
:return: True if the pair can stay, false if it should be removed :return: True if the pair can stay, false if it should be removed
""" """
if ticker['last'] is None: if ticker['last'] is None or ticker['last'] == 0:
self.log_on_refresh(logger.info, self.log_on_refresh(logger.info,
f"Removed {ticker['symbol']} from whitelist, because " f"Removed {ticker['symbol']} from whitelist, because "
"ticker['last'] is empty (Usually no trade in the last 24h).") "ticker['last'] is empty (Usually no trade in the last 24h).")
return False return False
compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last'])
changeperc = compare / ticker['last'] # Perform low_price_ratio check.
if changeperc > self._low_price_ratio: if self._low_price_ratio != 0:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last'])
f"because 1 unit is {changeperc * 100:.3f}%") changeperc = compare / ticker['last']
return False if changeperc > self._low_price_ratio:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because 1 unit is {changeperc * 100:.3f}%")
return False
# Perform min_price check.
if self._min_price != 0:
if ticker['last'] < self._min_price:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because last price < {self._min_price:.8f}")
return False
# Perform max_price check.
if self._max_price != 0:
if ticker['last'] > self._max_price:
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
f"because last price > {self._max_price:.8f}")
return False
return True return True

View File

@ -360,7 +360,7 @@ class Trade(_DECL_BASE):
def update(self, order: Dict) -> None: def update(self, order: Dict) -> None:
""" """
Updates this entity with amount and actual open/close rates. Updates this entity with amount and actual open/close rates.
:param order: order retrieved by exchange.get_order() :param order: order retrieved by exchange.fetch_order()
:return: None :return: None
""" """
order_type = order['type'] order_type = order['type']

View File

@ -10,11 +10,13 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown,
create_cum_profit, create_cum_profit,
extract_trades_of_period, load_trades) extract_trades_of_period, load_trades)
from freqtrade.data.converter import trim_dataframe from freqtrade.data.converter import trim_dataframe
from freqtrade.data.dataprovider import DataProvider
from freqtrade.data.history import load_data from freqtrade.data.history import load_data
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_prev_date from freqtrade.exchange import timeframe_to_prev_date
from freqtrade.misc import pair_to_filename from freqtrade.misc import pair_to_filename
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy import IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -467,6 +469,8 @@ def load_and_plot_trades(config: Dict[str, Any]):
""" """
strategy = StrategyResolver.load_strategy(config) strategy = StrategyResolver.load_strategy(config)
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
IStrategy.dp = DataProvider(config, exchange)
plot_elements = init_plotscript(config) plot_elements = init_plotscript(config)
trades = plot_elements['trades'] trades = plot_elements['trades']
pair_counter = 0 pair_counter = 0

View File

@ -42,14 +42,14 @@ class HyperOptResolver(IResolver):
extra_dir=config.get('hyperopt_path')) extra_dir=config.get('hyperopt_path'))
if not hasattr(hyperopt, 'populate_indicators'): if not hasattr(hyperopt, 'populate_indicators'):
logger.warning("Hyperopt class does not provide populate_indicators() method. " logger.info("Hyperopt class does not provide populate_indicators() method. "
"Using populate_indicators from the strategy.") "Using populate_indicators from the strategy.")
if not hasattr(hyperopt, 'populate_buy_trend'): if not hasattr(hyperopt, 'populate_buy_trend'):
logger.warning("Hyperopt class does not provide populate_buy_trend() method. " logger.info("Hyperopt class does not provide populate_buy_trend() method. "
"Using populate_buy_trend from the strategy.") "Using populate_buy_trend from the strategy.")
if not hasattr(hyperopt, 'populate_sell_trend'): if not hasattr(hyperopt, 'populate_sell_trend'):
logger.warning("Hyperopt class does not provide populate_sell_trend() method. " logger.info("Hyperopt class does not provide populate_sell_trend() method. "
"Using populate_sell_trend from the strategy.") "Using populate_sell_trend from the strategy.")
return hyperopt return hyperopt

View File

@ -17,6 +17,7 @@ from werkzeug.serving import make_server
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -105,6 +106,9 @@ class ApiServer(RPC):
# Register application handling # Register application handling
self.register_rest_rpc_urls() self.register_rest_rpc_urls()
if self._config.get('fiat_display_currency', None):
self._fiat_converter = CryptoToFiatConverter()
thread = threading.Thread(target=self.run, daemon=True) thread = threading.Thread(target=self.run, daemon=True)
thread.start() thread.start()

View File

@ -11,7 +11,9 @@ from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
from numpy import NAN, mean from numpy import NAN, mean
from freqtrade.exceptions import DependencyException, TemporaryError from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import timeframe_to_msecs, timeframe_to_minutes
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
@ -103,6 +105,8 @@ class RPC:
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
'ticker_interval': config['timeframe'], # DEPRECATED 'ticker_interval': config['timeframe'], # DEPRECATED
'timeframe': config['timeframe'], 'timeframe': config['timeframe'],
'timeframe_ms': timeframe_to_msecs(config['timeframe']),
'timeframe_min': timeframe_to_minutes(config['timeframe']),
'exchange': config['exchange']['name'], 'exchange': config['exchange']['name'],
'strategy': config['strategy'], 'strategy': config['strategy'],
'forcebuy_enabled': config.get('forcebuy_enable', False), 'forcebuy_enabled': config.get('forcebuy_enable', False),
@ -126,11 +130,11 @@ class RPC:
for trade in trades: for trade in trades:
order = None order = None
if trade.open_order_id: if trade.open_order_id:
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
# calculate profit and send message to user # calculate profit and send message to user
try: try:
current_rate = self._freqtrade.get_sell_rate(trade.pair, False) current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException: except (ExchangeError, PricingError):
current_rate = NAN current_rate = NAN
current_profit = trade.calc_profit_ratio(current_rate) current_profit = trade.calc_profit_ratio(current_rate)
current_profit_abs = trade.calc_profit(current_rate) current_profit_abs = trade.calc_profit(current_rate)
@ -174,7 +178,7 @@ class RPC:
# calculate profit and send message to user # calculate profit and send message to user
try: try:
current_rate = self._freqtrade.get_sell_rate(trade.pair, False) current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException: except (PricingError, ExchangeError):
current_rate = NAN current_rate = NAN
trade_percent = (100 * trade.calc_profit_ratio(current_rate)) trade_percent = (100 * trade.calc_profit_ratio(current_rate))
trade_profit = trade.calc_profit(current_rate) trade_profit = trade.calc_profit(current_rate)
@ -248,9 +252,10 @@ class RPC:
def _rpc_trade_history(self, limit: int) -> Dict: def _rpc_trade_history(self, limit: int) -> Dict:
""" Returns the X last trades """ """ Returns the X last trades """
if limit > 0: if limit > 0:
trades = Trade.get_trades().order_by(Trade.id.desc()).limit(limit) trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
Trade.id.desc()).limit(limit)
else: else:
trades = Trade.get_trades().order_by(Trade.id.desc()).all() trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all()
output = [trade.to_json() for trade in trades] output = [trade.to_json() for trade in trades]
@ -269,6 +274,8 @@ class RPC:
profit_closed_coin = [] profit_closed_coin = []
profit_closed_ratio = [] profit_closed_ratio = []
durations = [] durations = []
winning_trades = 0
losing_trades = 0
for trade in trades: for trade in trades:
current_rate: float = 0.0 current_rate: float = 0.0
@ -282,11 +289,15 @@ class RPC:
profit_ratio = trade.close_profit profit_ratio = trade.close_profit
profit_closed_coin.append(trade.close_profit_abs) profit_closed_coin.append(trade.close_profit_abs)
profit_closed_ratio.append(profit_ratio) profit_closed_ratio.append(profit_ratio)
if trade.close_profit >= 0:
winning_trades += 1
else:
losing_trades += 1
else: else:
# Get current rate # Get current rate
try: try:
current_rate = self._freqtrade.get_sell_rate(trade.pair, False) current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except DependencyException: except (PricingError, ExchangeError):
current_rate = NAN current_rate = NAN
profit_ratio = trade.calc_profit_ratio(rate=current_rate) profit_ratio = trade.calc_profit_ratio(rate=current_rate)
@ -344,6 +355,8 @@ class RPC:
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
'best_pair': best_pair[0] if best_pair else '', 'best_pair': best_pair[0] if best_pair else '',
'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0, 'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0,
'winning_trades': winning_trades,
'losing_trades': losing_trades,
} }
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
@ -352,7 +365,7 @@ class RPC:
total = 0.0 total = 0.0
try: try:
tickers = self._freqtrade.exchange.get_tickers() tickers = self._freqtrade.exchange.get_tickers()
except (TemporaryError, DependencyException): except (ExchangeError):
raise RPCException('Error getting current tickers.') raise RPCException('Error getting current tickers.')
self._freqtrade.wallets.update(require_update=False) self._freqtrade.wallets.update(require_update=False)
@ -373,7 +386,7 @@ class RPC:
if pair.startswith(stake_currency): if pair.startswith(stake_currency):
rate = 1.0 / rate rate = 1.0 / rate
est_stake = rate * balance.total est_stake = rate * balance.total
except (TemporaryError, DependencyException): except (ExchangeError):
logger.warning(f" Could not get rate for pair {coin}.") logger.warning(f" Could not get rate for pair {coin}.")
continue continue
total = total + (est_stake or 0) total = total + (est_stake or 0)
@ -442,7 +455,7 @@ class RPC:
def _exec_forcesell(trade: Trade) -> None: def _exec_forcesell(trade: Trade) -> None:
# Check if there is there is an open order # Check if there is there is an open order
if trade.open_order_id: if trade.open_order_id:
order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
# Cancel open LIMIT_BUY orders and close trade # Cancel open LIMIT_BUY orders and close trade
if order and order['status'] == 'open' \ if order and order['status'] == 'open' \
@ -511,7 +524,7 @@ class RPC:
# check if valid pair # check if valid pair
# check if pair already has an open pair # check if pair already has an open pair
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
if trade: if trade:
raise RPCException(f'position for {pair} already open - id: {trade.id}') raise RPCException(f'position for {pair} already open - id: {trade.id}')
@ -520,7 +533,7 @@ class RPC:
# execute buy # execute buy
if self._freqtrade.execute_buy(pair, stakeamount, price): if self._freqtrade.execute_buy(pair, stakeamount, price):
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade return trade
else: else:
return None return None

View File

@ -5,6 +5,7 @@ This module manage Telegram communication
""" """
import json import json
import logging import logging
import arrow
from typing import Any, Callable, Dict from typing import Any, Callable, Dict
from tabulate import tabulate from tabulate import tabulate
@ -92,6 +93,7 @@ class Telegram(RPC):
CommandHandler('stop', self._stop), CommandHandler('stop', self._stop),
CommandHandler('forcesell', self._forcesell), CommandHandler('forcesell', self._forcesell),
CommandHandler('forcebuy', self._forcebuy), CommandHandler('forcebuy', self._forcebuy),
CommandHandler('trades', self._trades),
CommandHandler('performance', self._performance), CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily), CommandHandler('daily', self._daily),
CommandHandler('count', self._count), CommandHandler('count', self._count),
@ -366,7 +368,9 @@ class Telegram(RPC):
f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n"
f"*Total Trade Count:* `{trade_count}`\n" f"*Total Trade Count:* `{trade_count}`\n"
f"*First Trade opened:* `{first_trade_date}`\n" f"*First Trade opened:* `{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}`") f"*Latest Trade opened:* `{latest_trade_date}\n`"
f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`"
)
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}%`")
@ -494,6 +498,41 @@ class Telegram(RPC):
except RPCException as e: except RPCException as e:
self._send_msg(str(e)) self._send_msg(str(e))
@authorized_only
def _trades(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /trades <n>
Returns last n recent trades.
:param bot: telegram bot
:param update: message update
:return: None
"""
stake_cur = self._config['stake_currency']
try:
nrecent = int(context.args[0])
except (TypeError, ValueError, IndexError):
nrecent = 10
try:
trades = self._rpc_trade_history(
nrecent
)
trades_tab = tabulate(
[[arrow.get(trade['open_date']).humanize(),
trade['pair'],
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
for trade in trades['trades']],
headers=[
'Open Date',
'Pair',
f'Profit ({stake_cur})',
],
tablefmt='simple')
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e:
self._send_msg(str(e))
@authorized_only @authorized_only
def _performance(self, update: Update, context: CallbackContext) -> None: def _performance(self, update: Update, context: CallbackContext) -> None:
""" """
@ -607,6 +646,7 @@ class Telegram(RPC):
" *table :* `will display trades in a table`\n" " *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an asterisk (*)`\n" " `pending buy orders are marked with an asterisk (*)`\n"
" `pending sell orders are marked with a double asterisk (**)`\n" " `pending sell orders are marked with a double asterisk (**)`\n"
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit:* `Lists cumulative profit from all finished trades`\n" "*/profit:* `Lists cumulative profit from all finished trades`\n"
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " "*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
"regardless of profit`\n" "regardless of profit`\n"

View File

@ -7,20 +7,19 @@ import warnings
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from datetime import datetime, timezone from datetime import datetime, timezone
from enum import Enum from enum import Enum
from typing import Dict, NamedTuple, Optional, Tuple from typing import Dict, List, NamedTuple, Optional, Tuple
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import StrategyError from freqtrade.exceptions import StrategyError, OperationalException
from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange import timeframe_to_minutes
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -191,6 +190,63 @@ class IStrategy(ABC):
""" """
return False return False
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote resource for comparison)
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, **kwargs) -> bool:
"""
Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be bought.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
"""
return True
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
"""
Called right before placing a regular sell order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be sold.
:param trade: trade object.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell']
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange.
False aborts the process
"""
return True
def informative_pairs(self) -> ListPairsWithTimeframes: def informative_pairs(self) -> ListPairsWithTimeframes:
""" """
Define additional, informative pair/interval combinations to be cached from the exchange. Define additional, informative pair/interval combinations to be cached from the exchange.
@ -204,6 +260,10 @@ class IStrategy(ABC):
""" """
return [] return []
###
# END - Intended to be overridden by strategy
###
def get_strategy_name(self) -> str: def get_strategy_name(self) -> str:
""" """
Returns strategy class name Returns strategy class name
@ -273,6 +333,8 @@ class IStrategy(ABC):
# Defs that only make change on new candle data. # Defs that only make change on new candle data.
dataframe = self.analyze_ticker(dataframe, metadata) dataframe = self.analyze_ticker(dataframe, metadata)
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
if self.dp:
self.dp._set_cached_df(pair, self.timeframe, dataframe)
else: else:
logger.debug("Skipping TA Analysis for already analyzed candle") logger.debug("Skipping TA Analysis for already analyzed candle")
dataframe['buy'] = 0 dataframe['buy'] = 0
@ -284,13 +346,53 @@ class IStrategy(ABC):
return dataframe return dataframe
def analyze_pair(self, pair: str) -> None:
"""
Fetch data for this pair from dataprovider and analyze.
Stores the dataframe into the dataprovider.
The analyzed dataframe is then accessible via `dp.get_analyzed_dataframe()`.
:param pair: Pair to analyze.
"""
if not self.dp:
raise OperationalException("DataProvider not found.")
dataframe = self.dp.ohlcv(pair, self.timeframe)
if not isinstance(dataframe, DataFrame) or dataframe.empty:
logger.warning('Empty candle (OHLCV) data for pair %s', pair)
return
try:
df_len, df_close, df_date = self.preserve_df(dataframe)
dataframe = strategy_safe_wrapper(
self._analyze_ticker_internal, message=""
)(dataframe, {'pair': pair})
self.assert_df(dataframe, df_len, df_close, df_date)
except StrategyError as error:
logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}")
return
if dataframe.empty:
logger.warning('Empty dataframe for pair %s', pair)
return
def analyze(self, pairs: List[str]) -> None:
"""
Analyze all pairs using analyze_pair().
:param pairs: List of pairs to analyze
"""
for pair in pairs:
self.analyze_pair(pair)
@staticmethod @staticmethod
def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]: def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]:
""" keep some data for dataframes """ """ keep some data for dataframes """
return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1] return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1]
def assert_df(self, dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime): def assert_df(self, dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime):
""" make sure data is unmodified """ """
Ensure dataframe (length, last candle) was not modified, and has all elements we need.
"""
message = "" message = ""
if df_len != len(dataframe): if df_len != len(dataframe):
message = "length" message = "length"
@ -304,31 +406,17 @@ class IStrategy(ABC):
else: else:
raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.") raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.")
def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]: def get_signal(self, pair: str, timeframe: str, dataframe: DataFrame) -> Tuple[bool, bool]:
""" """
Calculates current signal based several technical analysis indicators Calculates current signal based based on the buy / sell columns of the dataframe.
Used by Bot to get the signal to buy or sell
:param pair: pair in format ANT/BTC :param pair: pair in format ANT/BTC
:param interval: Interval to use (in min) :param timeframe: timeframe to use
:param dataframe: Dataframe to analyze :param dataframe: Analyzed dataframe to get signal from.
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal :return: (Buy, Sell) A bool-tuple indicating buy/sell signal
""" """
if not isinstance(dataframe, DataFrame) or dataframe.empty: if not isinstance(dataframe, DataFrame) or dataframe.empty:
logger.warning('Empty candle (OHLCV) data for pair %s', pair) logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
return False, False
try:
df_len, df_close, df_date = self.preserve_df(dataframe)
dataframe = strategy_safe_wrapper(
self._analyze_ticker_internal, message=""
)(dataframe, {'pair': pair})
self.assert_df(dataframe, df_len, df_close, df_date)
except StrategyError as error:
logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}")
return False, False
if dataframe.empty:
logger.warning('Empty dataframe for pair %s', pair)
return False, False return False, False
latest_date = dataframe['date'].max() latest_date = dataframe['date'].max()
@ -337,24 +425,18 @@ class IStrategy(ABC):
latest_date = arrow.get(latest_date) latest_date = arrow.get(latest_date)
# Check if dataframe is out of date # Check if dataframe is out of date
interval_minutes = timeframe_to_minutes(interval) timeframe_minutes = timeframe_to_minutes(timeframe)
offset = self.config.get('exchange', {}).get('outdated_offset', 5) offset = self.config.get('exchange', {}).get('outdated_offset', 5)
if latest_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): if latest_date < (arrow.utcnow().shift(minutes=-(timeframe_minutes * 2 + offset))):
logger.warning( logger.warning(
'Outdated history for pair %s. Last tick is %s minutes old', 'Outdated history for pair %s. Last tick is %s minutes old',
pair, pair, (arrow.utcnow() - latest_date).seconds // 60
(arrow.utcnow() - latest_date).seconds // 60
) )
return False, False return False, False
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
logger.debug( logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
'trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell))
latest['date'],
pair,
str(buy),
str(sell)
)
return buy, sell return buy, sell
def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool,
@ -500,7 +582,8 @@ class IStrategy(ABC):
def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
""" """
Creates a dataframe and populates indicators for given candle (OHLCV) data Populates indicators for given candle (OHLCV) data (for multiple pairs)
Does not run advice_buy or advise_sell!
Used by optimize operations only, not during dry / live runs. Used by optimize operations only, not during dry / live runs.
Using .copy() to get a fresh copy of the dataframe for every strategy run. Using .copy() to get a fresh copy of the dataframe for every strategy run.
Has positive effects on memory usage for whatever reason - also when Has positive effects on memory usage for whatever reason - also when

View File

@ -5,7 +5,7 @@ from freqtrade.exceptions import StrategyError
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def strategy_safe_wrapper(f, message: str = "", default_retval=None): def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_error=False):
""" """
Wrapper around user-provided methods and functions. Wrapper around user-provided methods and functions.
Caches all exceptions and returns either the default_retval (if it's not None) or raises Caches all exceptions and returns either the default_retval (if it's not None) or raises
@ -20,7 +20,7 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None):
f"Strategy caused the following exception: {error}" f"Strategy caused the following exception: {error}"
f"{f}" f"{f}"
) )
if default_retval is None: if default_retval is None and not supress_error:
raise StrategyError(str(error)) from error raise StrategyError(str(error)) from error
return default_retval return default_retval
except Exception as error: except Exception as error:
@ -28,7 +28,7 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None):
f"{message}" f"{message}"
f"Unexpected error {error} calling {f}" f"Unexpected error {error} calling {f}"
) )
if default_retval is None: if default_retval is None and not supress_error:
raise StrategyError(str(error)) from error raise StrategyError(str(error)) from error
return default_retval return default_retval

View File

@ -1,4 +1,65 @@
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
Might be used to perform pair-independent tasks
(e.g. gather some remote ressource for comparison)
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, this simply does nothing.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
"""
pass
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, **kwargs) -> bool:
"""
Called right before placing a buy order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be bought.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
"""
return True
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
"""
Called right before placing a regular sell order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be sold.
:param trade: trade object.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell']
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange.
False aborts the process
"""
return True
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool:
""" """
Check buy timeout function callback. Check buy timeout function callback.

View File

@ -90,6 +90,9 @@ class Worker:
if state == State.RUNNING: if state == State.RUNNING:
self.freqtrade.startup() self.freqtrade.startup()
if state == State.STOPPED:
self.freqtrade.check_for_open_trades()
# Reset heartbeat timestamp to log the heartbeat message at # Reset heartbeat timestamp to log the heartbeat message at
# first throttling iteration when the state changes # first throttling iteration when the state changes
self._heartbeat_msg = 0 self._heartbeat_msg = 0

View File

@ -3,6 +3,7 @@ nav:
- Home: index.md - Home: index.md
- Installation Docker: docker.md - Installation Docker: docker.md
- Installation: installation.md - Installation: installation.md
- Freqtrade Basics: bot-basics.md
- Configuration: configuration.md - Configuration: configuration.md
- Strategy Customization: strategy-customization.md - Strategy Customization: strategy-customization.md
- Stoploss: stoploss.md - Stoploss: stoploss.md

View File

@ -1,17 +1,17 @@
# requirements without requirements installable via conda # requirements without requirements installable via conda
# mainly used for Raspberry pi installs # mainly used for Raspberry pi installs
ccxt==1.30.48 ccxt==1.32.7
SQLAlchemy==1.3.18 SQLAlchemy==1.3.18
python-telegram-bot==12.8 python-telegram-bot==12.8
arrow==0.15.7 arrow==0.15.8
cachetools==4.1.1 cachetools==4.1.1
requests==2.24.0 requests==2.24.0
urllib3==1.25.9 urllib3==1.25.10
wrapt==1.12.1 wrapt==1.12.1
jsonschema==3.2.0 jsonschema==3.2.0
TA-Lib==0.4.18 TA-Lib==0.4.18
tabulate==0.8.7 tabulate==0.8.7
pycoingecko==1.2.0 pycoingecko==1.3.0
jinja2==2.11.2 jinja2==2.11.2
# find first, C search in arrays # find first, C search in arrays

View File

@ -3,7 +3,7 @@
-r requirements-plot.txt -r requirements-plot.txt
-r requirements-hyperopt.txt -r requirements-hyperopt.txt
coveralls==2.0.0 coveralls==2.1.1
flake8==3.8.3 flake8==3.8.3
flake8-type-annotations==0.1.0 flake8-type-annotations==0.1.0
flake8-tidy-imports==4.1.0 flake8-tidy-imports==4.1.0
@ -11,7 +11,7 @@ mypy==0.782
pytest==5.4.3 pytest==5.4.3
pytest-asyncio==0.14.0 pytest-asyncio==0.14.0
pytest-cov==2.10.0 pytest-cov==2.10.0
pytest-mock==3.1.1 pytest-mock==3.2.0
pytest-random-order==1.0.4 pytest-random-order==1.0.4
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents

View File

@ -2,9 +2,9 @@
-r requirements.txt -r requirements.txt
# Required for hyperopt # Required for hyperopt
scipy==1.5.0 scipy==1.5.2
scikit-learn==0.23.1 scikit-learn==0.23.1
scikit-optimize==0.7.4 scikit-optimize==0.7.4
filelock==3.0.12 filelock==3.0.12
joblib==0.15.1 joblib==0.16.0
progressbar2==3.51.4 progressbar2==3.51.4

View File

@ -1,5 +1,5 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==4.8.2 plotly==4.9.0

View File

@ -1,5 +1,5 @@
# Load common requirements # Load common requirements
-r requirements-common.txt -r requirements-common.txt
numpy==1.19.0 numpy==1.19.1
pandas==1.0.5 pandas==1.0.5

View File

@ -6,12 +6,12 @@ import pytest
from freqtrade.commands import (start_convert_data, start_create_userdir, from freqtrade.commands import (start_convert_data, start_create_userdir,
start_download_data, start_hyperopt_list, start_download_data, start_hyperopt_list,
start_hyperopt_show, start_list_exchanges, start_hyperopt_show, start_list_data,
start_list_hyperopts, start_list_markets, start_list_exchanges, start_list_hyperopts,
start_list_strategies, start_list_timeframes, start_list_markets, start_list_strategies,
start_new_hyperopt, start_new_strategy, start_list_timeframes, start_new_hyperopt,
start_show_trades, start_test_pairlist, start_new_strategy, start_show_trades,
start_trading) start_test_pairlist, start_trading)
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.state import RunMode from freqtrade.state import RunMode
@ -1043,6 +1043,40 @@ def test_convert_data_trades(mocker, testdatadir):
assert trades_mock.call_args[1]['erase'] is False assert trades_mock.call_args[1]['erase'] is False
def test_start_list_data(testdatadir, capsys):
args = [
"list-data",
"--data-format-ohlcv",
"json",
"--datadir",
str(testdatadir),
]
pargs = get_args(args)
pargs['config'] = None
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 16 pair / timeframe combinations." in captured.out
assert "\n| Pair | Timeframe |\n" in captured.out
assert "\n| UNITTEST/BTC | 1m, 5m, 8m, 30m |\n" in captured.out
args = [
"list-data",
"--data-format-ohlcv",
"json",
"--pairs", "XRP/ETH",
"--datadir",
str(testdatadir),
]
pargs = get_args(args)
pargs['config'] = None
start_list_data(pargs)
captured = capsys.readouterr()
assert "Found 2 pair / timeframe combinations." in captured.out
assert "\n| Pair | Timeframe |\n" in captured.out
assert "UNITTEST/BTC" not in captured.out
assert "\n| XRP/ETH | 1m, 5m |\n" in captured.out
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_show_trades(mocker, fee, capsys, caplog): def test_show_trades(mocker, fee, capsys, caplog):
mocker.patch("freqtrade.persistence.init") mocker.patch("freqtrade.persistence.init")
@ -1055,7 +1089,7 @@ def test_show_trades(mocker, fee, capsys, caplog):
pargs = get_args(args) pargs = get_args(args)
pargs['config'] = None pargs['config'] = None
start_show_trades(pargs) start_show_trades(pargs)
assert log_has("Printing 3 Trades: ", caplog) assert log_has("Printing 4 Trades: ", caplog)
captured = capsys.readouterr() captured = capsys.readouterr()
assert "Trade(id=1" in captured.out assert "Trade(id=1" in captured.out
assert "Trade(id=2" in captured.out assert "Trade(id=2" in captured.out

View File

@ -163,7 +163,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
:param value: which value IStrategy.get_signal() must return :param value: which value IStrategy.get_signal() must return
:return: None :return: None
""" """
freqtrade.strategy.get_signal = lambda e, s, t: value freqtrade.strategy.get_signal = lambda e, s, x: value
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
@ -199,6 +199,20 @@ def create_mock_trades(fee):
) )
Trade.session.add(trade) Trade.session.add(trade)
trade = Trade(
pair='XRP/BTC',
stake_amount=0.001,
amount=123.0,
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.05,
close_rate=0.06,
close_profit=0.01,
exchange='bittrex',
is_open=False,
)
Trade.session.add(trade)
# Simulate prod entry # Simulate prod entry
trade = Trade( trade = Trade(
pair='ETC/BTC', pair='ETC/BTC',
@ -661,7 +675,8 @@ def shitcoinmarkets(markets):
Fixture with shitcoin markets - used to test filters in pairlists Fixture with shitcoin markets - used to test filters in pairlists
""" """
shitmarkets = deepcopy(markets) shitmarkets = deepcopy(markets)
shitmarkets.update({'HOT/BTC': { shitmarkets.update({
'HOT/BTC': {
'id': 'HOTBTC', 'id': 'HOTBTC',
'symbol': 'HOT/BTC', 'symbol': 'HOT/BTC',
'base': 'HOT', 'base': 'HOT',
@ -766,7 +781,32 @@ def shitcoinmarkets(markets):
"spot": True, "spot": True,
"future": False, "future": False,
"active": True "active": True
}, },
'ADADOUBLE/USDT': {
"percentage": True,
"tierBased": False,
"taker": 0.001,
"maker": 0.001,
"precision": {
"base": 8,
"quote": 8,
"amount": 2,
"price": 4
},
"limits": {
},
"id": "ADADOUBLEUSDT",
"symbol": "ADADOUBLE/USDT",
"base": "ADADOUBLE",
"quote": "USDT",
"baseId": "ADADOUBLE",
"quoteId": "USDT",
"info": {},
"type": "spot",
"spot": True,
"future": False,
"active": True
},
}) })
return shitmarkets return shitmarkets
@ -787,6 +827,7 @@ def limit_buy_order():
'price': 0.00001099, 'price': 0.00001099,
'amount': 90.99181073, 'amount': 90.99181073,
'filled': 90.99181073, 'filled': 90.99181073,
'cost': 0.0009999,
'remaining': 0.0, 'remaining': 0.0,
'status': 'closed' 'status': 'closed'
} }
@ -1387,6 +1428,28 @@ def tickers():
"quoteVolume": 0.0, "quoteVolume": 0.0,
"info": {} "info": {}
}, },
"ADADOUBLE/USDT": {
"symbol": "ADADOUBLE/USDT",
"timestamp": 1580469388244,
"datetime": "2020-01-31T11:16:28.244Z",
"high": None,
"low": None,
"bid": 0.7305,
"bidVolume": None,
"ask": 0.7342,
"askVolume": None,
"vwap": None,
"open": None,
"close": None,
"last": 0,
"previousClose": None,
"change": None,
"percentage": 2.628,
"average": None,
"baseVolume": 0.0,
"quoteVolume": 0.0,
"info": {}
},
}) })

View File

@ -43,7 +43,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
trades = load_trades_from_db(db_url=default_conf['db_url']) trades = load_trades_from_db(db_url=default_conf['db_url'])
assert init_mock.call_count == 1 assert init_mock.call_count == 1
assert len(trades) == 3 assert len(trades) == 4
assert isinstance(trades, DataFrame) assert isinstance(trades, DataFrame)
assert "pair" in trades.columns assert "pair" in trades.columns
assert "open_time" in trades.columns assert "open_time" in trades.columns

View File

@ -1,11 +1,12 @@
from datetime import datetime, timezone
from unittest.mock import MagicMock from unittest.mock import MagicMock
from pandas import DataFrame
import pytest import pytest
from pandas import DataFrame
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.pairlist.pairlistmanager import PairListManager
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.state import RunMode from freqtrade.state import RunMode
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
@ -164,7 +165,7 @@ def test_ticker(mocker, default_conf, tickers):
assert 'symbol' in res assert 'symbol' in res
assert res['symbol'] == 'ETH/BTC' assert res['symbol'] == 'ETH/BTC'
ticker_mock = MagicMock(side_effect=DependencyException('Pair not found')) ticker_mock = MagicMock(side_effect=ExchangeError('Pair not found'))
mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock) mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock)
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
@ -194,3 +195,29 @@ def test_current_whitelist(mocker, default_conf, tickers):
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
dp = DataProvider(default_conf, exchange) dp = DataProvider(default_conf, exchange)
dp.current_whitelist() dp.current_whitelist()
def test_get_analyzed_dataframe(mocker, default_conf, ohlcv_history):
default_conf["runmode"] = RunMode.DRY_RUN
timeframe = default_conf["timeframe"]
exchange = get_patched_exchange(mocker, default_conf)
dp = DataProvider(default_conf, exchange)
dp._set_cached_df("XRP/BTC", timeframe, ohlcv_history)
dp._set_cached_df("UNITTEST/BTC", timeframe, ohlcv_history)
assert dp.runmode == RunMode.DRY_RUN
dataframe, time = dp.get_analyzed_dataframe("UNITTEST/BTC", timeframe)
assert ohlcv_history.equals(dataframe)
assert isinstance(time, datetime)
dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe)
assert ohlcv_history.equals(dataframe)
assert isinstance(time, datetime)
dataframe, time = dp.get_analyzed_dataframe("NOTHING/BTC", timeframe)
assert dataframe.empty
assert isinstance(time, datetime)
assert time == datetime(1970, 1, 1, tzinfo=timezone.utc)

View File

@ -631,6 +631,20 @@ def test_jsondatahandler_ohlcv_get_pairs(testdatadir):
assert set(pairs) == {'UNITTEST/BTC'} assert set(pairs) == {'UNITTEST/BTC'}
def test_jsondatahandler_ohlcv_get_available_data(testdatadir):
paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir)
# Convert to set to avoid failures due to sorting
assert set(paircombs) == {('UNITTEST/BTC', '5m'), ('ETH/BTC', '5m'), ('XLM/BTC', '5m'),
('TRX/BTC', '5m'), ('LTC/BTC', '5m'), ('XMR/BTC', '5m'),
('ZEC/BTC', '5m'), ('UNITTEST/BTC', '1m'), ('ADA/BTC', '5m'),
('ETC/BTC', '5m'), ('NXT/BTC', '5m'), ('DASH/BTC', '5m'),
('XRP/ETH', '1m'), ('XRP/ETH', '5m'), ('UNITTEST/BTC', '30m'),
('UNITTEST/BTC', '8m')}
paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir)
assert set(paircombs) == {('UNITTEST/BTC', '8m')}
def test_jsondatahandler_trades_get_pairs(testdatadir): def test_jsondatahandler_trades_get_pairs(testdatadir):
pairs = JsonGzDataHandler.trades_get_pairs(testdatadir) pairs = JsonGzDataHandler.trades_get_pairs(testdatadir)
# Convert to set to avoid failures due to sorting # Convert to set to avoid failures due to sorting

View File

@ -5,8 +5,9 @@ import ccxt
import pytest import pytest
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError) OperationalException)
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@pytest.mark.parametrize('limitratio,expected', [ @pytest.mark.parametrize('limitratio,expected', [
@ -62,15 +63,9 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(TemporaryError): ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance",
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) "stoploss", "create_order", retries=1,
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') pair='ETH/BTC', amount=1, stop_price=220, order_types={})
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
def test_stoploss_order_dry_run_binance(default_conf, mocker): def test_stoploss_order_dry_run_binance(default_conf, mocker):

View File

@ -4,17 +4,17 @@ import copy
import logging import logging
from datetime import datetime, timezone from datetime import datetime, timezone
from random import randint from random import randint
from unittest.mock import MagicMock, Mock, PropertyMock from unittest.mock import MagicMock, Mock, PropertyMock, patch
import arrow import arrow
import ccxt import ccxt
import pytest import pytest
from pandas import DataFrame from pandas import DataFrame
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection,
OperationalException, TemporaryError) OperationalException, TemporaryError)
from freqtrade.exchange import Binance, Exchange, Kraken from freqtrade.exchange import Binance, Exchange, Kraken
from freqtrade.exchange.common import API_RETRY_COUNT from freqtrade.exchange.common import API_RETRY_COUNT, calculate_backoff
from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair, from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair,
timeframe_to_minutes, timeframe_to_minutes,
timeframe_to_msecs, timeframe_to_msecs,
@ -37,12 +37,20 @@ def get_mock_coro(return_value):
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
fun, mock_ccxt_fun, **kwargs): fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs):
with patch('freqtrade.exchange.common.time.sleep'):
with pytest.raises(DDosProtection):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DDos"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
getattr(exchange, fun)(**kwargs) getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
@ -51,12 +59,21 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun,
retries=API_RETRY_COUNT + 1, **kwargs):
with patch('freqtrade.exchange.common.asyncio.sleep', get_mock_coro(None)):
with pytest.raises(DDosProtection):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("Dooh"))
exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
with pytest.raises(TemporaryError): with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
await getattr(exchange, fun)(**kwargs) await getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
@ -697,13 +714,13 @@ def test_validate_order_types(default_conf, mocker):
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
default_conf['order_types'] = { default_conf['order_types'] = {
'buy': 'limit', 'buy': 'limit',
'sell': 'limit', 'sell': 'limit',
'stoploss': 'market', 'stoploss': 'market',
'stoploss_on_exchange': False 'stoploss_on_exchange': False
} }
Exchange(default_conf) Exchange(default_conf)
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False})
@ -713,9 +730,8 @@ def test_validate_order_types(default_conf, mocker):
'buy': 'limit', 'buy': 'limit',
'sell': 'limit', 'sell': 'limit',
'stoploss': 'market', 'stoploss': 'market',
'stoploss_on_exchange': 'false' 'stoploss_on_exchange': False
} }
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r'Exchange .* does not support market orders.'): match=r'Exchange .* does not support market orders.'):
Exchange(default_conf) Exchange(default_conf)
@ -726,7 +742,6 @@ def test_validate_order_types(default_conf, mocker):
'stoploss': 'limit', 'stoploss': 'limit',
'stoploss_on_exchange': True 'stoploss_on_exchange': True
} }
with pytest.raises(OperationalException, with pytest.raises(OperationalException,
match=r'On exchange stoploss is not supported for .*'): match=r'On exchange stoploss is not supported for .*'):
Exchange(default_conf) Exchange(default_conf)
@ -1127,9 +1142,10 @@ def test_get_balance_prod(default_conf, mocker, exchange_name):
exchange.get_balance(currency='BTC') exchange.get_balance(currency='BTC')
def test_get_balances_dry_run(default_conf, mocker): @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_balances_dry_run(default_conf, mocker, exchange_name):
default_conf['dry_run'] = True default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
assert exchange.get_balances() == {} assert exchange.get_balances() == {}
@ -1847,36 +1863,48 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_order(default_conf, mocker, exchange_name): def test_fetch_order(default_conf, mocker, exchange_name):
default_conf['dry_run'] = True default_conf['dry_run'] = 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)
exchange._dry_run_open_orders['X'] = order exchange._dry_run_open_orders['X'] = order
assert exchange.get_order('X', 'TKN/BTC').myid == 123 assert exchange.fetch_order('X', 'TKN/BTC').myid == 123
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
exchange.get_order('Y', 'TKN/BTC') exchange.fetch_order('Y', 'TKN/BTC')
default_conf['dry_run'] = False default_conf['dry_run'] = False
api_mock = MagicMock() api_mock = MagicMock()
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.get_order('X', 'TKN/BTC') == 456 assert exchange.fetch_order('X', 'TKN/BTC') == 456
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"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_order(order_id='_', pair='TKN/BTC') exchange.fetch_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1 assert api_mock.fetch_order.call_count == 1
api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
with patch('freqtrade.exchange.common.time.sleep') as tm:
with pytest.raises(InvalidOrderException):
exchange.fetch_order(order_id='_', pair='TKN/BTC')
# Ensure backoff is called
assert tm.call_args_list[0][0][0] == 1
assert tm.call_args_list[1][0][0] == 2
assert tm.call_args_list[2][0][0] == 5
assert tm.call_args_list[3][0][0] == 10
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
'get_order', 'fetch_order', 'fetch_order', 'fetch_order',
order_id='_', pair='TKN/BTC') order_id='_', pair='TKN/BTC')
@pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("exchange_name", EXCHANGES)
def test_get_stoploss_order(default_conf, mocker, exchange_name): def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
# Don't test FTX here - that needs a seperate test # Don't test FTX here - that needs a seperate test
if exchange_name == 'ftx': if exchange_name == 'ftx':
return return
@ -1885,25 +1913,25 @@ def test_get_stoploss_order(default_conf, mocker, exchange_name):
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)
exchange._dry_run_open_orders['X'] = order exchange._dry_run_open_orders['X'] = order
assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
exchange.get_stoploss_order('Y', 'TKN/BTC') exchange.fetch_stoploss_order('Y', 'TKN/BTC')
default_conf['dry_run'] = False default_conf['dry_run'] = False
api_mock = MagicMock() api_mock = MagicMock()
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.get_stoploss_order('X', 'TKN/BTC') == 456 assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == 456
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"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1 assert api_mock.fetch_order.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
'get_stoploss_order', 'fetch_order', 'fetch_stoploss_order', 'fetch_order',
order_id='_', pair='TKN/BTC') order_id='_', pair='TKN/BTC')
@ -2111,6 +2139,13 @@ def test_get_markets(default_conf, mocker, markets,
assert sorted(pairs.keys()) == sorted(expected_keys) assert sorted(pairs.keys()) == sorted(expected_keys)
def test_get_markets_error(default_conf, mocker):
ex = get_patched_exchange(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=None))
with pytest.raises(OperationalException, match="Markets were not loaded."):
ex.get_markets('LTC', 'USDT', True, False)
def test_timeframe_to_minutes(): def test_timeframe_to_minutes():
assert timeframe_to_minutes("5m") == 5 assert timeframe_to_minutes("5m") == 5
assert timeframe_to_minutes("10m") == 10 assert timeframe_to_minutes("10m") == 10
@ -2271,3 +2306,15 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
ex = get_patched_exchange(mocker, default_conf) ex = get_patched_exchange(mocker, default_conf)
assert ex.calculate_fee_rate(order) == expected assert ex.calculate_fee_rate(order) == expected
@pytest.mark.parametrize('retrycount,max_retries,expected', [
(0, 3, 10),
(1, 3, 5),
(2, 3, 2),
(3, 3, 1),
(0, 1, 2),
(1, 1, 1),
])
def test_calculate_backoff(retrycount, max_retries, expected):
assert calculate_backoff(retrycount, max_retries) == expected

View File

@ -6,9 +6,9 @@ from unittest.mock import MagicMock
import ccxt import ccxt
import pytest import pytest
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import DependencyException, InvalidOrderException
OperationalException, TemporaryError)
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
from .test_exchange import ccxt_exceptionhandlers from .test_exchange import ccxt_exceptionhandlers
STOPLOSS_ORDERTYPE = 'stop' STOPLOSS_ORDERTYPE = 'stop'
@ -85,15 +85,9 @@ def test_stoploss_order_ftx(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(TemporaryError): ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx",
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) "stoploss", "create_order", retries=1,
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') pair='ETH/BTC', amount=1, stop_price=220, order_types={})
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
def test_stoploss_order_dry_run_ftx(default_conf, mocker): def test_stoploss_order_dry_run_ftx(default_conf, mocker):
@ -130,34 +124,34 @@ def test_stoploss_adjust_ftx(mocker, default_conf):
assert not exchange.stoploss_adjust(1501, order) assert not exchange.stoploss_adjust(1501, order)
def test_get_stoploss_order(default_conf, mocker): def test_fetch_stoploss_order(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
order = MagicMock() order = MagicMock()
order.myid = 123 order.myid = 123
exchange = get_patched_exchange(mocker, default_conf, id='ftx') exchange = get_patched_exchange(mocker, default_conf, id='ftx')
exchange._dry_run_open_orders['X'] = order exchange._dry_run_open_orders['X'] = order
assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123
with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'):
exchange.get_stoploss_order('Y', 'TKN/BTC') exchange.fetch_stoploss_order('Y', 'TKN/BTC')
default_conf['dry_run'] = False default_conf['dry_run'] = False
api_mock = MagicMock() api_mock = MagicMock()
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}]) api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}])
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
assert exchange.get_stoploss_order('X', 'TKN/BTC')['status'] == '456' assert exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] == '456'
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}]) api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}])
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"):
exchange.get_stoploss_order('X', 'TKN/BTC')['status'] exchange.fetch_stoploss_order('X', 'TKN/BTC')['status']
with pytest.raises(InvalidOrderException): with pytest.raises(InvalidOrderException):
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_orders.call_count == 1 assert api_mock.fetch_orders.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx', ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx',
'get_stoploss_order', 'fetch_orders', 'fetch_stoploss_order', 'fetch_orders',
order_id='_', pair='TKN/BTC') order_id='_', pair='TKN/BTC')

View File

@ -6,8 +6,7 @@ from unittest.mock import MagicMock
import ccxt import ccxt
import pytest import pytest
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import DependencyException, InvalidOrderException
OperationalException, TemporaryError)
from tests.conftest import get_patched_exchange from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers from tests.exchange.test_exchange import ccxt_exceptionhandlers
@ -206,15 +205,9 @@ def test_stoploss_order_kraken(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(TemporaryError): ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) "stoploss", "create_order", retries=1,
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') pair='ETH/BTC', amount=1, stop_price=220, order_types={})
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
def test_stoploss_order_dry_run_kraken(default_conf, mocker): def test_stoploss_order_dry_run_kraken(default_conf, mocker):

View File

@ -308,6 +308,11 @@ def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
assert backtesting.fee == 0.1234 assert backtesting.fee == 0.1234
assert fee_mock.call_count == 0 assert fee_mock.call_count == 0
default_conf['fee'] = 0.0
backtesting = Backtesting(default_conf)
assert backtesting.fee == 0.0
assert fee_mock.call_count == 0
def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
patch_exchange(mocker) patch_exchange(mocker)

View File

@ -235,7 +235,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
"BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), "BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
"USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']),
# No pair for ETH, VolumePairList # No pair for ETH, VolumePairList
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
"ETH", []), "ETH", []),
@ -275,11 +275,16 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "low_price_ratio": 0.03}], {"method": "PriceFilter", "low_price_ratio": 0.03}],
"USDT", ['ETH/USDT', 'NANO/USDT']), "USDT", ['ETH/USDT', 'NANO/USDT']),
# Hot is removed by precision_filter, Fuel by low_price_filter. # Hot is removed by precision_filter, Fuel by low_price_ratio, Ripple by min_price.
([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"}, {"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.02}], {"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.01}],
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
# Hot is removed by precision_filter, Fuel by low_price_ratio, Ethereum by max_price.
([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"},
{"method": "PrecisionFilter"},
{"method": "PriceFilter", "low_price_ratio": 0.02, "max_price": 0.05}],
"BTC", ['TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
# HOT and XRP are removed because below 1250 quoteVolume # HOT and XRP are removed because below 1250 quoteVolume
([{"method": "VolumePairList", "number_assets": 5, ([{"method": "VolumePairList", "number_assets": 5,
"sort_key": "quoteVolume", "min_value": 1250}], "sort_key": "quoteVolume", "min_value": 1250}],
@ -298,11 +303,11 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
# ShuffleFilter # ShuffleFilter
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "ShuffleFilter", "seed": 77}], {"method": "ShuffleFilter", "seed": 77}],
"USDT", ['ETH/USDT', 'ADAHALF/USDT', 'NANO/USDT']), "USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']),
# ShuffleFilter, other seed # ShuffleFilter, other seed
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "ShuffleFilter", "seed": 42}], {"method": "ShuffleFilter", "seed": 42}],
"USDT", ['NANO/USDT', 'ETH/USDT', 'ADAHALF/USDT']), "USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']),
# ShuffleFilter, no seed # ShuffleFilter, no seed
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
{"method": "ShuffleFilter"}], {"method": "ShuffleFilter"}],
@ -319,7 +324,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
"BTC", 'filter_at_the_beginning'), # OperationalException expected "BTC", 'filter_at_the_beginning'), # OperationalException expected
# PriceFilter after StaticPairList # PriceFilter after StaticPairList
([{"method": "StaticPairList"}, ([{"method": "StaticPairList"},
{"method": "PriceFilter", "low_price_ratio": 0.02}], {"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.000001, "max_price": 0.1}],
"BTC", ['ETH/BTC', 'TKN/BTC']), "BTC", ['ETH/BTC', 'TKN/BTC']),
# PriceFilter only # PriceFilter only
([{"method": "PriceFilter", "low_price_ratio": 0.02}], ([{"method": "PriceFilter", "low_price_ratio": 0.02}],
@ -342,6 +347,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
{"method": "StaticPairList"}], {"method": "StaticPairList"}],
"BTC", 'static_in_the_middle'), "BTC", 'static_in_the_middle'),
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
{"method": "PriceFilter", "low_price_ratio": 0.02}],
"USDT", ['ETH/USDT', 'NANO/USDT']),
]) ])
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
ohlcv_history_list, pairlists, base_currency, ohlcv_history_list, pairlists, base_currency,
@ -389,13 +397,17 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
for pairlist in pairlists: for pairlist in pairlists:
if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \
len(ohlcv_history_list) <= pairlist['min_days_listed']: len(ohlcv_history_list) <= pairlist['min_days_listed']:
assert log_has_re(r'^Removed .* from whitelist, because age is less than ' assert log_has_re(r'^Removed .* from whitelist, because age .* is less than '
r'.* day.*', caplog) r'.* day.*', caplog)
if pairlist['method'] == 'PrecisionFilter' and whitelist_result: if pairlist['method'] == 'PrecisionFilter' and whitelist_result:
assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' assert log_has_re(r'^Removed .* from whitelist, because stop price .* '
r'would be <= stop limit.*', caplog) r'would be <= stop limit.*', caplog)
if pairlist['method'] == 'PriceFilter' and whitelist_result: if pairlist['method'] == 'PriceFilter' and whitelist_result:
assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or
log_has_re(r'^Removed .* from whitelist, '
r'because last price < .*%$', caplog) or
log_has_re(r'^Removed .* from whitelist, '
r'because last price > .*%$', caplog) or
log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] " log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] "
r"is empty.*", caplog)) r"is empty.*", caplog))
if pairlist['method'] == 'VolumePairList': if pairlist['method'] == 'VolumePairList':
@ -524,6 +536,37 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers):
assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf
def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog):
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
{'method': 'AgeFilter', 'min_days_listed': -1}]
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
with pytest.raises(OperationalException,
match=r'AgeFilter requires min_days_listed must be >= 1'):
get_patched_freqtradebot(mocker, default_conf)
def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog):
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
{'method': 'AgeFilter', 'min_days_listed': 99999}]
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True),
get_tickers=tickers
)
with pytest.raises(OperationalException,
match=r'AgeFilter requires min_days_listed must not exceed '
r'exchange max request size \([0-9]+\)'):
get_patched_freqtradebot(mocker, default_conf)
def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list): def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list):
mocker.patch.multiple('freqtrade.exchange.Exchange', mocker.patch.multiple('freqtrade.exchange.Exchange',
@ -547,6 +590,36 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his
assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count
@pytest.mark.parametrize("pairlistconfig,expected", [
({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010,
"max_price": 1.0}, "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below "
"0.1% or below 0.00000010 or above 1.00000000.'}]"
),
({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010},
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or below 0.00000010.'}]"
),
({"method": "PriceFilter", "low_price_ratio": 0.001, "max_price": 1.00010000},
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or above 1.00010000.'}]"
),
({"method": "PriceFilter", "min_price": 0.00002000},
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]"
),
({"method": "PriceFilter"},
"[{'PriceFilter': 'PriceFilter - No price filters configured.'}]"
),
])
def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, expected):
mocker.patch.multiple('freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
exchange_has=MagicMock(return_value=True)
)
whitelist_conf['pairlists'] = [pairlistconfig]
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
short_desc = str(freqtrade.pairlists.short_desc())
assert short_desc == expected
def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))

View File

@ -8,12 +8,13 @@ import pytest
from numpy import isnan from numpy import isnan
from freqtrade.edge import PairInfo from freqtrade.edge import PairInfo
from freqtrade.exceptions import DependencyException, TemporaryError from freqtrade.exceptions import ExchangeError, TemporaryError
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import RPC, RPCException from freqtrade.rpc import RPC, RPCException
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.state import State from freqtrade.state import State
from tests.conftest import get_patched_freqtradebot, patch_get_signal, create_mock_trades from tests.conftest import (create_mock_trades, get_patched_freqtradebot,
patch_get_signal)
# Functions for recurrent object patching # Functions for recurrent object patching
@ -106,7 +107,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
} }
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
results = rpc._rpc_trade_status() results = rpc._rpc_trade_status()
assert isnan(results[0]['current_profit']) assert isnan(results[0]['current_profit'])
assert isnan(results[0]['current_rate']) assert isnan(results[0]['current_rate'])
@ -209,7 +210,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert '-0.41% (-0.06)' == result[0][3] assert '-0.41% (-0.06)' == result[0][3]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert 'instantly' == result[0][2] assert 'instantly' == result[0][2]
assert 'ETH/BTC' in result[0][1] assert 'ETH/BTC' in result[0][1]
@ -283,12 +284,11 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
assert isinstance(trades['trades'][1], dict) assert isinstance(trades['trades'][1], dict)
trades = rpc._rpc_trade_history(0) trades = rpc._rpc_trade_history(0)
assert len(trades['trades']) == 3 assert len(trades['trades']) == 2
assert trades['trades_count'] == 3 assert trades['trades_count'] == 2
# The first trade is for ETH ... sorting is descending # The first closed trade is for ETC ... sorting is descending
assert trades['trades'][-1]['pair'] == 'ETH/BTC' assert trades['trades'][-1]['pair'] == 'ETC/BTC'
assert trades['trades'][0]['pair'] == 'ETC/BTC' assert trades['trades'][0]['pair'] == 'XRP/BTC'
assert trades['trades'][1]['pair'] == 'ETC/BTC'
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
@ -365,7 +365,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
# Test non-available pair # Test non-available pair
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available")))
stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert stats['trade_count'] == 2 assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now' assert stats['first_trade_date'] == 'just now'
@ -606,7 +606,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
cancel_order=cancel_order_mock, cancel_order=cancel_order_mock,
get_order=MagicMock( fetch_order=MagicMock(
return_value={ return_value={
'status': 'closed', 'status': 'closed',
'type': 'limit', 'type': 'limit',
@ -652,7 +652,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
trade = Trade.query.filter(Trade.id == '1').first() trade = Trade.query.filter(Trade.id == '1').first()
filled_amount = trade.amount / 2 filled_amount = trade.amount / 2
mocker.patch( mocker.patch(
'freqtrade.exchange.Exchange.get_order', 'freqtrade.exchange.Exchange.fetch_order',
return_value={ return_value={
'status': 'open', 'status': 'open',
'type': 'limit', 'type': 'limit',
@ -671,7 +671,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
amount = trade.amount amount = trade.amount
# make an limit-buy open trade, if there is no 'filled', don't sell it # make an limit-buy open trade, if there is no 'filled', don't sell it
mocker.patch( mocker.patch(
'freqtrade.exchange.Exchange.get_order', 'freqtrade.exchange.Exchange.fetch_order',
return_value={ return_value={
'status': 'open', 'status': 'open',
'type': 'limit', 'type': 'limit',
@ -688,7 +688,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
freqtradebot.enter_positions() freqtradebot.enter_positions()
# make an limit-sell open trade # make an limit-sell open trade
mocker.patch( mocker.patch(
'freqtrade.exchange.Exchange.get_order', 'freqtrade.exchange.Exchange.fetch_order',
return_value={ return_value={
'status': 'open', 'status': 'open',
'type': 'limit', 'type': 'limit',

View File

@ -326,6 +326,8 @@ def test_api_show_config(botclient, mocker):
assert rc.json['exchange'] == 'bittrex' assert rc.json['exchange'] == 'bittrex'
assert rc.json['ticker_interval'] == '5m' assert rc.json['ticker_interval'] == '5m'
assert rc.json['timeframe'] == '5m' assert rc.json['timeframe'] == '5m'
assert rc.json['timeframe_ms'] == 300000
assert rc.json['timeframe_min'] == 5
assert rc.json['state'] == 'running' assert rc.json['state'] == 'running'
assert not rc.json['trailing_stop'] assert not rc.json['trailing_stop']
assert 'bid_strategy' in rc.json assert 'bid_strategy' in rc.json
@ -366,12 +368,12 @@ def test_api_trades(botclient, mocker, ticker, fee, markets):
rc = client_get(client, f"{BASE_URI}/trades") rc = client_get(client, f"{BASE_URI}/trades")
assert_response(rc) assert_response(rc)
assert len(rc.json['trades']) == 3
assert rc.json['trades_count'] == 3
rc = client_get(client, f"{BASE_URI}/trades?limit=2")
assert_response(rc)
assert len(rc.json['trades']) == 2 assert len(rc.json['trades']) == 2
assert rc.json['trades_count'] == 2 assert rc.json['trades_count'] == 2
rc = client_get(client, f"{BASE_URI}/trades?limit=1")
assert_response(rc)
assert len(rc.json['trades']) == 1
assert rc.json['trades_count'] == 1
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
@ -431,14 +433,14 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
'latest_trade_date': 'just now', 'latest_trade_date': 'just now',
'latest_trade_timestamp': ANY, 'latest_trade_timestamp': ANY,
'profit_all_coin': 6.217e-05, 'profit_all_coin': 6.217e-05,
'profit_all_fiat': 0, 'profit_all_fiat': 0.76748865,
'profit_all_percent': 6.2, 'profit_all_percent': 6.2,
'profit_all_percent_mean': 6.2, 'profit_all_percent_mean': 6.2,
'profit_all_ratio_mean': 0.06201058, 'profit_all_ratio_mean': 0.06201058,
'profit_all_percent_sum': 6.2, 'profit_all_percent_sum': 6.2,
'profit_all_ratio_sum': 0.06201058, 'profit_all_ratio_sum': 0.06201058,
'profit_closed_coin': 6.217e-05, 'profit_closed_coin': 6.217e-05,
'profit_closed_fiat': 0, 'profit_closed_fiat': 0.76748865,
'profit_closed_percent': 6.2, 'profit_closed_percent': 6.2,
'profit_closed_ratio_mean': 0.06201058, 'profit_closed_ratio_mean': 0.06201058,
'profit_closed_percent_mean': 6.2, 'profit_closed_percent_mean': 6.2,
@ -446,6 +448,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
'profit_closed_percent_sum': 6.2, 'profit_closed_percent_sum': 6.2,
'trade_count': 1, 'trade_count': 1,
'closed_trade_count': 1, 'closed_trade_count': 1,
'winning_trades': 1,
'losing_trades': 0,
} }

View File

@ -21,8 +21,9 @@ from freqtrade.rpc import RPCMessageType
from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.rpc.telegram import Telegram, authorized_only
from freqtrade.state import State from freqtrade.state import State
from freqtrade.strategy.interface import SellType from freqtrade.strategy.interface import SellType
from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange, from tests.conftest import (create_mock_trades, get_patched_freqtradebot,
patch_get_signal, patch_whitelist) log_has, patch_exchange, patch_get_signal,
patch_whitelist)
class DummyCls(Telegram): class DummyCls(Telegram):
@ -60,7 +61,7 @@ def test__init__(default_conf, mocker) -> None:
assert telegram._config == default_conf assert telegram._config == default_conf
def test_init(default_conf, mocker, caplog) -> None: def test_telegram_init(default_conf, mocker, caplog) -> None:
start_polling = MagicMock() start_polling = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
@ -72,7 +73,7 @@ def test_init(default_conf, mocker, caplog) -> None:
assert start_polling.start_polling.call_count == 1 assert start_polling.start_polling.call_count == 1
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
"['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], " "['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], "
"['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], " "['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], "
"['edge'], ['help'], ['version']]") "['edge'], ['help'], ['version']]")
@ -725,6 +726,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'type': RPCMessageType.SELL_NOTIFICATION, 'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'profit', 'gain': 'profit',
@ -784,6 +786,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'type': RPCMessageType.SELL_NOTIFICATION, 'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'loss', 'gain': 'loss',
@ -832,6 +835,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
msg = rpc_mock.call_args_list[0][0][0] msg = rpc_mock.call_args_list[0][0][0]
assert { assert {
'type': RPCMessageType.SELL_NOTIFICATION, 'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'loss', 'gain': 'loss',
@ -1143,6 +1147,36 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0] assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0]
def test_telegram_trades(mocker, update, default_conf, fee):
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
telegram = Telegram(freqtradebot)
context = MagicMock()
context.args = []
telegram._trades(update=update, context=context)
assert "<b>0 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
assert "<pre>" not in msg_mock.call_args_list[0][0][0]
msg_mock.reset_mock()
create_mock_trades(fee)
context = MagicMock()
context.args = [5]
telegram._trades(update=update, context=context)
msg_mock.call_count == 1
assert "2 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
assert "Profit (" in msg_mock.call_args_list[0][0][0]
assert "Open Date" in msg_mock.call_args_list[0][0][0]
assert "<pre>" in msg_mock.call_args_list[0][0][0]
def test_help_handle(default_conf, update, mocker) -> None: def test_help_handle(default_conf, update, mocker) -> None:
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(

View File

@ -13,12 +13,14 @@ from freqtrade.exceptions import StrategyError
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from tests.conftest import get_patched_exchange, log_has, log_has_re from freqtrade.data.dataprovider import DataProvider
from tests.conftest import log_has, log_has_re
from .strats.default_strategy import DefaultStrategy from .strats.default_strategy import DefaultStrategy
# Avoid to reinit the same object again and again # Avoid to reinit the same object again and again
_STRATEGY = DefaultStrategy(config={}) _STRATEGY = DefaultStrategy(config={})
_STRATEGY.dp = DataProvider({}, None, None)
def test_returns_latest_signal(mocker, default_conf, ohlcv_history): def test_returns_latest_signal(mocker, default_conf, ohlcv_history):
@ -29,63 +31,60 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history):
mocked_history['buy'] = 0 mocked_history['buy'] = 0
mocked_history.loc[1, 'sell'] = 1 mocked_history.loc[1, 'sell'] = 1
mocker.patch.object( assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True)
_STRATEGY, '_analyze_ticker_internal',
return_value=mocked_history
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True)
mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 1 mocked_history.loc[1, 'buy'] = 1
mocker.patch.object( assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False)
_STRATEGY, '_analyze_ticker_internal',
return_value=mocked_history
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False)
mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'sell'] = 0
mocked_history.loc[1, 'buy'] = 0 mocked_history.loc[1, 'buy'] = 0
mocker.patch.object( assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False)
_STRATEGY, '_analyze_ticker_internal',
return_value=mocked_history
)
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, False)
def test_get_signal_empty(default_conf, mocker, caplog): def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history):
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history)
DataFrame())
assert log_has('Empty candle (OHLCV) data for pair foo', caplog)
caplog.clear()
assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe'],
[])
assert log_has('Empty candle (OHLCV) data for pair bar', caplog)
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
side_effect=ValueError('xyz')
)
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'],
ohlcv_history)
assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog)
def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
mocker.patch.object( mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal', _STRATEGY, '_analyze_ticker_internal',
return_value=DataFrame([]) return_value=DataFrame([])
) )
mocker.patch.object(_STRATEGY, 'assert_df') mocker.patch.object(_STRATEGY, 'assert_df')
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], _STRATEGY.analyze_pair('ETH/BTC')
ohlcv_history)
assert log_has('Empty dataframe for pair xyz', caplog) assert log_has('Empty dataframe for pair ETH/BTC', caplog)
def test_get_signal_empty(default_conf, mocker, caplog):
assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], DataFrame())
assert log_has('Empty candle (OHLCV) data for pair foo', caplog)
caplog.clear()
assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None)
assert log_has('Empty candle (OHLCV) data for pair bar', caplog)
caplog.clear()
assert (False, False) == _STRATEGY.get_signal('baz', default_conf['timeframe'], DataFrame([]))
assert log_has('Empty candle (OHLCV) data for pair baz', caplog)
def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history):
caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history)
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
side_effect=ValueError('xyz')
)
_STRATEGY.analyze_pair('foo')
assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog)
caplog.clear()
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
side_effect=Exception('invalid ticker history ')
)
_STRATEGY.analyze_pair('foo')
assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog)
def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
@ -99,13 +98,9 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
mocked_history.loc[1, 'buy'] = 1 mocked_history.loc[1, 'buy'] = 1
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
mocker.patch.object(
_STRATEGY, '_analyze_ticker_internal',
return_value=mocked_history
)
mocker.patch.object(_STRATEGY, 'assert_df') mocker.patch.object(_STRATEGY, 'assert_df')
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'],
ohlcv_history) assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], mocked_history)
assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog)
@ -120,12 +115,13 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
mocked_history.loc[1, 'buy'] = 1 mocked_history.loc[1, 'buy'] = 1
caplog.set_level(logging.INFO) caplog.set_level(logging.INFO)
mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history)
mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(mocked_history, 0))
mocker.patch.object( mocker.patch.object(
_STRATEGY, 'assert_df', _STRATEGY, 'assert_df',
side_effect=StrategyError('Dataframe returned...') side_effect=StrategyError('Dataframe returned...')
) )
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], _STRATEGY.analyze_pair('xyz')
ohlcv_history)
assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...', assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...',
caplog) caplog)
@ -157,15 +153,6 @@ def test_assert_df(default_conf, mocker, ohlcv_history, caplog):
_STRATEGY.disable_dataframe_checks = False _STRATEGY.disable_dataframe_checks = False
def test_get_signal_handles_exceptions(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf)
mocker.patch.object(
_STRATEGY, 'analyze_ticker',
side_effect=Exception('invalid ticker history ')
)
assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False)
def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None: def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None:
default_conf.update({'strategy': 'DefaultStrategy'}) default_conf.update({'strategy': 'DefaultStrategy'})
strategy = StrategyResolver.load_strategy(default_conf) strategy = StrategyResolver.load_strategy(default_conf)
@ -342,6 +329,7 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
) )
strategy = DefaultStrategy({}) strategy = DefaultStrategy({})
strategy.dp = DataProvider({}, None, None)
strategy.process_only_new_candles = True strategy.process_only_new_candles = True
ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'}) ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'})
@ -400,6 +388,14 @@ def test_is_pair_locked(default_conf):
assert not strategy.is_pair_locked(pair) assert not strategy.is_pair_locked(pair)
def test_is_informative_pairs_callback(default_conf):
default_conf.update({'strategy': 'TestStrategyLegacy'})
strategy = StrategyResolver.load_strategy(default_conf)
# Should return empty
# Uses fallback to base implementation
assert [] == strategy.informative_pairs()
@pytest.mark.parametrize('error', [ @pytest.mark.parametrize('error', [
ValueError, KeyError, Exception, ValueError, KeyError, Exception,
]) ])
@ -419,6 +415,11 @@ def test_strategy_safe_wrapper_error(caplog, error):
assert isinstance(ret, bool) assert isinstance(ret, bool)
assert ret assert ret
caplog.clear()
# Test supressing error
ret = strategy_safe_wrapper(failing_method, message='DeadBeef', supress_error=True)()
assert log_has_re(r'DeadBeef.*', caplog)
@pytest.mark.parametrize('value', [ @pytest.mark.parametrize('value', [
1, 22, 55, True, False, {'a': 1, 'b': '112'}, 1, 22, 55, True, False, {'a': 1, 'b': '112'},

View File

@ -871,6 +871,14 @@ def test_load_config_default_exchange_name(all_conf) -> None:
validate_config_schema(all_conf) validate_config_schema(all_conf)
def test_load_config_stoploss_exchange_limit_ratio(all_conf) -> None:
all_conf['order_types']['stoploss_on_exchange_limit_ratio'] = 1.15
with pytest.raises(ValidationError,
match=r"1.15 is greater than the maximum"):
validate_config_schema(all_conf)
@pytest.mark.parametrize("keys", [("exchange", "sandbox", False), @pytest.mark.parametrize("keys", [("exchange", "sandbox", False),
("exchange", "key", ""), ("exchange", "key", ""),
("exchange", "secret", ""), ("exchange", "secret", ""),

View File

@ -9,13 +9,12 @@ from unittest.mock import ANY, MagicMock, PropertyMock
import arrow import arrow
import pytest import pytest
import requests
from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC, from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC,
UNLIMITED_STAKE_AMOUNT) UNLIMITED_STAKE_AMOUNT)
from freqtrade.exceptions import (DependencyException, InvalidOrderException, from freqtrade.exceptions import (DependencyException, ExchangeError,
OperationalException, PricingError, InvalidOrderException, OperationalException,
TemporaryError) PricingError, TemporaryError)
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc import RPCMessageType from freqtrade.rpc import RPCMessageType
@ -763,7 +762,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}), buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_order=MagicMock(return_value=limit_buy_order), fetch_order=MagicMock(return_value=limit_buy_order),
get_fee=fee, get_fee=fee,
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -832,7 +831,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mock
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}), buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_order=MagicMock(return_value=limit_buy_order), fetch_order=MagicMock(return_value=limit_buy_order),
get_fee=fee, get_fee=fee,
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -859,7 +858,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order,
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
buy=MagicMock(return_value={'id': limit_buy_order['id']}), buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_order=MagicMock(return_value=limit_buy_order), fetch_order=MagicMock(return_value=limit_buy_order),
get_fee=fee, get_fee=fee,
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -912,6 +911,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None:
refresh_latest_ohlcv=refresh_mock, refresh_latest_ohlcv=refresh_mock,
) )
inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")]) inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")])
mocker.patch('freqtrade.strategy.interface.IStrategy.get_signal', return_value=(False, False))
mocker.patch('time.sleep', return_value=None) mocker.patch('time.sleep', return_value=None)
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -974,6 +974,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
stake_amount = 2 stake_amount = 2
bid = 0.11 bid = 0.11
buy_rate_mock = MagicMock(return_value=bid) buy_rate_mock = MagicMock(return_value=bid)
@ -995,6 +996,13 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None:
) )
pair = 'ETH/BTC' pair = 'ETH/BTC'
assert not freqtrade.execute_buy(pair, stake_amount)
assert buy_rate_mock.call_count == 1
assert buy_mm.call_count == 0
assert freqtrade.strategy.confirm_trade_entry.call_count == 1
buy_rate_mock.reset_mock()
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
assert freqtrade.execute_buy(pair, stake_amount) assert freqtrade.execute_buy(pair, stake_amount)
assert buy_rate_mock.call_count == 1 assert buy_rate_mock.call_count == 1
assert buy_mm.call_count == 1 assert buy_mm.call_count == 1
@ -1002,6 +1010,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None:
assert call_args['pair'] == pair assert call_args['pair'] == pair
assert call_args['rate'] == bid assert call_args['rate'] == bid
assert call_args['amount'] == stake_amount / bid assert call_args['amount'] == stake_amount / bid
buy_rate_mock.reset_mock()
# Should create an open trade with an open order id # Should create an open trade with an open order id
# As the order is not fulfilled yet # As the order is not fulfilled yet
@ -1014,7 +1023,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None:
fix_price = 0.06 fix_price = 0.06
assert freqtrade.execute_buy(pair, stake_amount, fix_price) assert freqtrade.execute_buy(pair, stake_amount, fix_price)
# Make sure get_buy_rate wasn't called again # Make sure get_buy_rate wasn't called again
assert buy_rate_mock.call_count == 1 assert buy_rate_mock.call_count == 0
assert buy_mm.call_count == 2 assert buy_mm.call_count == 2
call_args = buy_mm.call_args_list[1][1] call_args = buy_mm.call_args_list[1][1]
@ -1060,11 +1069,44 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None:
assert not freqtrade.execute_buy(pair, stake_amount) assert not freqtrade.execute_buy(pair, stake_amount)
def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
get_buy_rate=MagicMock(return_value=0.11),
_get_min_pair_stake_amount=MagicMock(return_value=1)
)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=MagicMock(return_value={
'bid': 0.00001172,
'ask': 0.00001173,
'last': 0.00001172
}),
buy=MagicMock(return_value=limit_buy_order),
get_fee=fee,
)
stake_amount = 2
pair = 'ETH/BTC'
freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError)
assert freqtrade.execute_buy(pair, stake_amount)
freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception)
assert freqtrade.execute_buy(pair, stake_amount)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
assert freqtrade.execute_buy(pair, stake_amount)
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
assert not freqtrade.execute_buy(pair, stake_amount)
def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None: def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None:
patch_RPCManager(mocker) patch_RPCManager(mocker)
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
return_value=limit_buy_order['amount']) return_value=limit_buy_order['amount'])
@ -1126,7 +1168,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
trade.stoploss_order_id = 100 trade.stoploss_order_id = 100
hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', hanging_stoploss_order) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order)
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id == 100 assert trade.stoploss_order_id == 100
@ -1139,7 +1181,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
trade.stoploss_order_id = 100 trade.stoploss_order_id = 100
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', canceled_stoploss_order) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order)
stoploss.reset_mock() stoploss.reset_mock()
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
@ -1164,7 +1206,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
'average': 2, 'average': 2,
'amount': limit_buy_order['amount'], 'amount': limit_buy_order['amount'],
}) })
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hit) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit)
assert freqtrade.handle_stoploss_on_exchange(trade) is True assert freqtrade.handle_stoploss_on_exchange(trade) is True
assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog) assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog)
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
@ -1172,18 +1214,18 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
mocker.patch( mocker.patch(
'freqtrade.exchange.Exchange.stoploss', 'freqtrade.exchange.Exchange.stoploss',
side_effect=DependencyException() side_effect=ExchangeError()
) )
trade.is_open = True trade.is_open = True
freqtrade.handle_stoploss_on_exchange(trade) freqtrade.handle_stoploss_on_exchange(trade)
assert log_has('Unable to place a stoploss order on exchange.', caplog) assert log_has('Unable to place a stoploss order on exchange.', caplog)
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
# Fifth case: get_order returns InvalidOrder # Fifth case: fetch_order returns InvalidOrder
# It should try to add stoploss order # It should try to add stoploss order
trade.stoploss_order_id = 100 trade.stoploss_order_id = 100
stoploss.reset_mock() stoploss.reset_mock()
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order',
side_effect=InvalidOrderException()) side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
freqtrade.handle_stoploss_on_exchange(trade) freqtrade.handle_stoploss_on_exchange(trade)
@ -1194,7 +1236,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
trade.stoploss_order_id = None trade.stoploss_order_id = None
trade.is_open = False trade.is_open = False
stoploss.reset_mock() stoploss.reset_mock()
mocker.patch('freqtrade.exchange.Exchange.get_order') mocker.patch('freqtrade.exchange.Exchange.fetch_order')
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert stoploss.call_count == 0 assert stoploss.call_count == 0
@ -1215,8 +1257,8 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
buy=MagicMock(return_value={'id': limit_buy_order['id']}), buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee, get_fee=fee,
get_stoploss_order=MagicMock(return_value={'status': 'canceled'}), fetch_stoploss_order=MagicMock(return_value={'status': 'canceled'}),
stoploss=MagicMock(side_effect=DependencyException()), stoploss=MagicMock(side_effect=ExchangeError()),
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
@ -1249,7 +1291,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
buy=MagicMock(return_value={'id': limit_buy_order['id']}), buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=sell_mock, sell=sell_mock,
get_fee=fee, get_fee=fee,
get_order=MagicMock(return_value={'status': 'canceled'}), fetch_order=MagicMock(return_value={'status': 'canceled'}),
stoploss=MagicMock(side_effect=InvalidOrderException()), stoploss=MagicMock(side_effect=InvalidOrderException()),
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -1332,7 +1374,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
} }
}) })
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging)
# stoploss initially at 5% # stoploss initially at 5%
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
@ -1432,7 +1474,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
} }
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
side_effect=InvalidOrderException()) side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging)
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
@ -1442,7 +1484,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
# Fail creating stoploss order # Fail creating stoploss order
caplog.clear() caplog.clear()
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock()) cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock())
mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException()) mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=ExchangeError())
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert cancel_mock.call_count == 1 assert cancel_mock.call_count == 1
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog)
@ -1512,7 +1554,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
} }
}) })
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging)
# stoploss initially at 20% as edge dictated it. # stoploss initially at 20% as edge dictated it.
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
@ -1589,7 +1631,7 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
return_value=limit_buy_order['amount']) return_value=limit_buy_order['amount'])
@ -1613,7 +1655,7 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None:
def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) -> None: def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order)
trade = MagicMock() trade = MagicMock()
trade.open_order_id = None trade.open_order_id = None
@ -1634,7 +1676,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order)
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
return_value=limit_buy_order['amount']) return_value=limit_buy_order['amount'])
@ -1673,8 +1715,8 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No
def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee,
mocker): mocker):
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
# get_order should not be called!! # fetch_order should not be called!!
mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
patch_exchange(mocker) patch_exchange(mocker)
Trade.session = MagicMock() Trade.session = MagicMock()
amount = sum(x['amount'] for x in trades_for_order) amount = sum(x['amount'] for x in trades_for_order)
@ -1698,8 +1740,8 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_
limit_buy_order, mocker, caplog): limit_buy_order, mocker, caplog):
trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14 trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
# get_order should not be called!! # fetch_order should not be called!!
mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
patch_exchange(mocker) patch_exchange(mocker)
Trade.session = MagicMock() Trade.session = MagicMock()
amount = sum(x['amount'] for x in trades_for_order) amount = sum(x['amount'] for x in trades_for_order)
@ -1724,7 +1766,7 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_
def test_update_trade_state_exception(mocker, default_conf, def test_update_trade_state_exception(mocker, default_conf,
limit_buy_order, caplog) -> None: limit_buy_order, caplog) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order)
trade = MagicMock() trade = MagicMock()
trade.open_order_id = '123' trade.open_order_id = '123'
@ -1741,7 +1783,7 @@ def test_update_trade_state_exception(mocker, default_conf,
def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None: def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None:
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.get_order', mocker.patch('freqtrade.exchange.Exchange.fetch_order',
MagicMock(side_effect=InvalidOrderException)) MagicMock(side_effect=InvalidOrderException))
trade = MagicMock() trade = MagicMock()
@ -1757,8 +1799,8 @@ def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None
def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker): def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker):
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
# get_order should not be called!! # fetch_order should not be called!!
mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
wallet_mock = MagicMock() wallet_mock = MagicMock()
mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock)
@ -1963,6 +2005,18 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order,
freqtrade.handle_trade(trade) freqtrade.handle_trade(trade)
def test_bot_loop_start_called_once(mocker, default_conf, caplog):
ftbot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(ftbot)
ftbot.strategy.bot_loop_start = MagicMock(side_effect=ValueError)
ftbot.strategy.analyze = MagicMock()
ftbot.process()
assert log_has_re(r'Strategy caused the following exception.*', caplog)
assert ftbot.strategy.bot_loop_start.call_count == 1
assert ftbot.strategy.analyze.call_count == 1
def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_order_old, open_trade, def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_order_old, open_trade,
fee, mocker) -> None: fee, mocker) -> None:
default_conf["unfilledtimeout"] = {"buy": 1400, "sell": 30} default_conf["unfilledtimeout"] = {"buy": 1400, "sell": 30}
@ -1973,7 +2027,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old), fetch_order=MagicMock(return_value=limit_buy_order_old),
cancel_order=cancel_order_mock, cancel_order=cancel_order_mock,
get_fee=fee get_fee=fee
) )
@ -2022,7 +2076,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old), fetch_order=MagicMock(return_value=limit_buy_order_old),
cancel_order_with_result=cancel_order_mock, cancel_order_with_result=cancel_order_mock,
get_fee=fee get_fee=fee
) )
@ -2052,7 +2106,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old), fetch_order=MagicMock(return_value=limit_buy_order_old),
cancel_order=cancel_order_mock, cancel_order=cancel_order_mock,
get_fee=fee get_fee=fee
) )
@ -2079,7 +2133,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
fetch_ticker=ticker, fetch_ticker=ticker,
get_order=MagicMock(side_effect=DependencyException), fetch_order=MagicMock(side_effect=ExchangeError),
cancel_order=cancel_order_mock, cancel_order=cancel_order_mock,
get_fee=fee get_fee=fee
) )
@ -2105,7 +2159,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_sell_order_old), fetch_order=MagicMock(return_value=limit_sell_order_old),
cancel_order=cancel_order_mock cancel_order=cancel_order_mock
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -2152,7 +2206,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_sell_order_old), fetch_order=MagicMock(return_value=limit_sell_order_old),
cancel_order=cancel_order_mock cancel_order=cancel_order_mock
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -2183,7 +2237,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_sell_order_old), fetch_order=MagicMock(return_value=limit_sell_order_old),
cancel_order_with_result=cancel_order_mock cancel_order_with_result=cancel_order_mock
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -2210,7 +2264,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old_partial), fetch_order=MagicMock(return_value=limit_buy_order_old_partial),
cancel_order_with_result=cancel_order_mock cancel_order_with_result=cancel_order_mock
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -2238,7 +2292,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old_partial), fetch_order=MagicMock(return_value=limit_buy_order_old_partial),
cancel_order_with_result=cancel_order_mock, cancel_order_with_result=cancel_order_mock,
get_trades_for_order=MagicMock(return_value=trades_for_order), get_trades_for_order=MagicMock(return_value=trades_for_order),
) )
@ -2276,7 +2330,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_order=MagicMock(return_value=limit_buy_order_old_partial), fetch_order=MagicMock(return_value=limit_buy_order_old_partial),
cancel_order_with_result=cancel_order_mock, cancel_order_with_result=cancel_order_mock,
get_trades_for_order=MagicMock(return_value=trades_for_order), get_trades_for_order=MagicMock(return_value=trades_for_order),
) )
@ -2320,7 +2374,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker, fetch_ticker=ticker,
get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')), fetch_order=MagicMock(side_effect=ExchangeError('Oh snap')),
cancel_order=cancel_order_mock cancel_order=cancel_order_mock
) )
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
@ -2489,24 +2543,36 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
patch_whitelist(mocker, default_conf) patch_whitelist(mocker, default_conf)
freqtrade = FreqtradeBot(default_conf) freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=False)
# Create some test data # Create some test data
freqtrade.enter_positions() freqtrade.enter_positions()
rpc_mock.reset_mock()
trade = Trade.query.first() trade = Trade.query.first()
assert trade assert trade
assert freqtrade.strategy.confirm_trade_exit.call_count == 0
# Increase the price and sell it # Increase the price and sell it
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.exchange.Exchange', 'freqtrade.exchange.Exchange',
fetch_ticker=ticker_sell_up fetch_ticker=ticker_sell_up
) )
# Prevented sell ...
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
assert rpc_mock.call_count == 0
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
# Repatch with true
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 1
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'trade_id': 1,
'type': RPCMessageType.SELL_NOTIFICATION, 'type': RPCMessageType.SELL_NOTIFICATION,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
@ -2557,6 +2623,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'type': RPCMessageType.SELL_NOTIFICATION, 'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'loss', 'gain': 'loss',
@ -2613,6 +2680,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
assert { assert {
'type': RPCMessageType.SELL_NOTIFICATION, 'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'loss', 'gain': 'loss',
@ -2774,7 +2842,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
"fee": None, "fee": None,
"trades": None "trades": None
}) })
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_executed) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_executed)
freqtrade.exit_positions(trades) freqtrade.exit_positions(trades)
assert trade.stoploss_order_id is None assert trade.stoploss_order_id is None
@ -2818,6 +2886,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
assert { assert {
'type': RPCMessageType.SELL_NOTIFICATION, 'type': RPCMessageType.SELL_NOTIFICATION,
'trade_id': 1,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'profit', 'gain': 'profit',
@ -4017,7 +4086,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order,
@pytest.mark.usefixtures("init_persistence") @pytest.mark.usefixtures("init_persistence")
def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order): def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order):
default_conf['cancel_open_orders_on_exit'] = True default_conf['cancel_open_orders_on_exit'] = True
mocker.patch('freqtrade.exchange.Exchange.get_order', mocker.patch('freqtrade.exchange.Exchange.fetch_order',
side_effect=[DependencyException(), limit_sell_order, limit_buy_order]) side_effect=[DependencyException(), limit_sell_order, limit_buy_order])
buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy')
sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell')
@ -4025,7 +4094,23 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
create_mock_trades(fee) create_mock_trades(fee)
trades = Trade.query.all() trades = Trade.query.all()
assert len(trades) == 3 assert len(trades) == 4
freqtrade.cancel_all_open_orders() freqtrade.cancel_all_open_orders()
assert buy_mock.call_count == 1 assert buy_mock.call_count == 1
assert sell_mock.call_count == 1 assert sell_mock.call_count == 1
@pytest.mark.usefixtures("init_persistence")
def test_check_for_open_trades(mocker, default_conf, fee, limit_buy_order, limit_sell_order):
freqtrade = get_patched_freqtradebot(mocker, default_conf)
freqtrade.check_for_open_trades()
assert freqtrade.rpc.send_msg.call_count == 0
create_mock_trades(fee)
trade = Trade.query.first()
trade.is_open = True
freqtrade.check_for_open_trades()
assert freqtrade.rpc.send_msg.call_count == 1
assert 'Handle these trades manually' in freqtrade.rpc.send_msg.call_args[0][0]['status']

View File

@ -62,7 +62,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
get_fee=fee, get_fee=fee,
amount_to_precision=lambda s, x, y: y, amount_to_precision=lambda s, x, y: y,
price_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y,
get_stoploss_order=stoploss_order_mock, fetch_stoploss_order=stoploss_order_mock,
cancel_stoploss_order=cancel_order_mock, cancel_stoploss_order=cancel_order_mock,
) )
@ -79,10 +79,15 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
freqtrade.strategy.order_types['stoploss_on_exchange'] = True freqtrade.strategy.order_types['stoploss_on_exchange'] = True
# Switch ordertype to market to close trade immediately # Switch ordertype to market to close trade immediately
freqtrade.strategy.order_types['sell'] = 'market' freqtrade.strategy.order_types['sell'] = 'market'
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
patch_get_signal(freqtrade) patch_get_signal(freqtrade)
# Create some test data # Create some test data
freqtrade.enter_positions() freqtrade.enter_positions()
assert freqtrade.strategy.confirm_trade_entry.call_count == 3
freqtrade.strategy.confirm_trade_entry.reset_mock()
assert freqtrade.strategy.confirm_trade_exit.call_count == 0
wallets_mock.reset_mock() wallets_mock.reset_mock()
Trade.session = MagicMock() Trade.session = MagicMock()
@ -95,6 +100,9 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
n = freqtrade.exit_positions(trades) n = freqtrade.exit_positions(trades)
assert n == 2 assert n == 2
assert should_sell_mock.call_count == 2 assert should_sell_mock.call_count == 2
assert freqtrade.strategy.confirm_trade_entry.call_count == 0
assert freqtrade.strategy.confirm_trade_exit.call_count == 1
freqtrade.strategy.confirm_trade_exit.reset_mock()
# Only order for 3rd trade needs to be cancelled # Only order for 3rd trade needs to be cancelled
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1

View File

@ -989,7 +989,7 @@ def test_get_overall_performance(fee):
create_mock_trades(fee) create_mock_trades(fee)
res = Trade.get_overall_performance() res = Trade.get_overall_performance()
assert len(res) == 1 assert len(res) == 2
assert 'pair' in res[0] assert 'pair' in res[0]
assert 'profit' in res[0] assert 'profit' in res[0]
assert 'count' in res[0] assert 'count' in res[0]
@ -1004,5 +1004,5 @@ def test_get_best_pair(fee):
create_mock_trades(fee) create_mock_trades(fee)
res = Trade.get_best_pair() res = Trade.get_best_pair()
assert len(res) == 2 assert len(res) == 2
assert res[0] == 'ETC/BTC' assert res[0] == 'XRP/BTC'
assert res[1] == 0.005 assert res[1] == 0.01

View File

@ -21,7 +21,7 @@ from freqtrade.plot.plotting import (add_indicators, add_profit,
load_and_plot_trades, plot_profit, load_and_plot_trades, plot_profit,
plot_trades, store_plot_file) plot_trades, store_plot_file)
from freqtrade.resolvers import StrategyResolver from freqtrade.resolvers import StrategyResolver
from tests.conftest import get_args, log_has, log_has_re from tests.conftest import get_args, log_has, log_has_re, patch_exchange
def fig_generating_mock(fig, *args, **kwargs): def fig_generating_mock(fig, *args, **kwargs):
@ -316,6 +316,8 @@ def test_start_plot_dataframe(mocker):
def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir): def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
patch_exchange(mocker)
default_conf['trade_source'] = 'file' default_conf['trade_source'] = 'file'
default_conf["datadir"] = testdatadir default_conf["datadir"] = testdatadir
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json" default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"