commit
767332405e
@ -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
13
.github/dependabot.yml
vendored
Normal 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
|
@ -1,7 +1,7 @@
|
||||
FROM python:3.8.3-slim-buster
|
||||
FROM python:3.8.5-slim-buster
|
||||
|
||||
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 \
|
||||
&& pip install --upgrade pip
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
FROM --platform=linux/arm/v7 python:3.7.7-slim-buster
|
||||
|
||||
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 \
|
||||
&& pip install --upgrade pip \
|
||||
&& echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf
|
||||
|
@ -66,7 +66,7 @@
|
||||
},
|
||||
{"method": "AgeFilter", "min_days_listed": 10},
|
||||
{"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}
|
||||
],
|
||||
"exchange": {
|
||||
|
@ -66,7 +66,7 @@ Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies
|
||||
#### Exporting trades to file
|
||||
|
||||
```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.
|
||||
|
58
docs/bot-basics.md
Normal file
58
docs/bot-basics.md
Normal 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.
|
@ -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.
|
||||
|
||||
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
|
||||
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.
|
||||
@ -662,16 +662,25 @@ Filters low-value coins which would not allow setting stoplosses.
|
||||
|
||||
#### 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.
|
||||
|
||||
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.
|
||||
|
||||
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
|
||||
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
||||
|
@ -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.
|
||||
|
||||
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).
|
||||
|
@ -1,2 +1,2 @@
|
||||
mkdocs-material==5.3.3
|
||||
mkdocs-material==5.5.0
|
||||
mdx_truly_sane_lists==1.2
|
||||
|
@ -13,6 +13,15 @@ Feel free to use a visual Database editor like SqliteBrowser if you feel more co
|
||||
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
|
||||
|
||||
```bash
|
||||
@ -100,8 +109,8 @@ UPDATE trades
|
||||
SET is_open=0,
|
||||
close_date=<close_date>,
|
||||
close_rate=<close_rate>,
|
||||
close_profit=close_rate/open_rate-1,
|
||||
close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * open_rate * 1 - fee_open)),
|
||||
close_profit = close_rate / open_rate - 1,
|
||||
close_profit_abs = (amount * <close_rate> * (1 - fee_close) - (amount * (open_rate * 1 - fee_open))),
|
||||
sell_reason=<sell_reason>
|
||||
WHERE id=<trade_ID_to_update>;
|
||||
```
|
||||
@ -111,24 +120,39 @@ WHERE id=<trade_ID_to_update>;
|
||||
```sql
|
||||
UPDATE trades
|
||||
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_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'
|
||||
WHERE id=31;
|
||||
```
|
||||
|
||||
## Insert manually a new trade
|
||||
## Manually insert a new trade
|
||||
|
||||
```sql
|
||||
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
|
||||
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.
|
||||
|
@ -84,7 +84,7 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai
|
||||
|
||||
``` python
|
||||
trailing_stop_positive_offset = 0.011
|
||||
trailing_only_offset_is_reached = true
|
||||
trailing_only_offset_is_reached = True
|
||||
```
|
||||
|
||||
Simplified example:
|
||||
|
@ -1,7 +1,12 @@
|
||||
# Advanced 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
|
||||
|
||||
@ -89,3 +94,108 @@ class Awesomestrategy(IStrategy):
|
||||
return True
|
||||
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
|
||||
|
||||
```
|
||||
|
@ -1,6 +1,8 @@
|
||||
# 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
|
||||
|
||||
@ -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).
|
||||
- [`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_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.
|
||||
- `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.
|
||||
@ -384,13 +387,14 @@ if self.dp:
|
||||
```
|
||||
|
||||
#### *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.
|
||||
|
||||
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.
|
||||
|
||||
@ -406,18 +410,49 @@ class SampleStrategy(IStrategy):
|
||||
|
||||
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()
|
||||
# Assign tf to each pair so they can be downloaded and cached for strategy.
|
||||
informative_pairs = [(pair, '1d') for pair in 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
|
||||
informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d')
|
||||
# Get the 14 day ATR.
|
||||
atr = ta.ATR(informative, timeperiod=14)
|
||||
# Get the 14 day rsi
|
||||
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
|
||||
# ...
|
||||
|
||||
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)*
|
||||
@ -431,13 +466,32 @@ if self.dp:
|
||||
```
|
||||
|
||||
!!! 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,
|
||||
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"
|
||||
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)*
|
||||
|
||||
``` python
|
||||
@ -470,6 +524,7 @@ if self.dp:
|
||||
data returned from the exchange and add appropriate error handling / defaults.
|
||||
|
||||
***
|
||||
|
||||
### Additional data (Wallets)
|
||||
|
||||
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
|
||||
|
||||
***
|
||||
|
||||
### Additional data (Trades)
|
||||
|
||||
A history of Trades can be retrieved in the strategy by querying the database.
|
||||
|
@ -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
|
||||
| `/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 (**)
|
||||
| `/trades [limit]` | | List all recently closed trades in a table format.
|
||||
| `/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
|
||||
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||
|
@ -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.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `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.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `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.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `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.
|
||||
Possible parameters are:
|
||||
|
||||
* `trade_id`
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `gain`
|
||||
|
@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2020.6'
|
||||
__version__ = '2020.7'
|
||||
|
||||
if __version__ == 'develop':
|
||||
|
||||
|
@ -9,7 +9,8 @@ Note: Be careful with file-scoped imports in these subfiles.
|
||||
from freqtrade.commands.arguments import Arguments
|
||||
from freqtrade.commands.build_config_commands import start_new_config
|
||||
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,
|
||||
start_new_hyperopt,
|
||||
start_new_strategy)
|
||||
|
@ -54,6 +54,8 @@ ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"]
|
||||
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
|
||||
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",
|
||||
"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"]
|
||||
|
||||
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",
|
||||
"plot-dataframe", "plot-profit", "show-trades"]
|
||||
|
||||
@ -159,7 +161,7 @@ class Arguments:
|
||||
self._build_args(optionlist=['version'], parser=self.parser)
|
||||
|
||||
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_list_exchanges, start_list_hyperopts,
|
||||
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))
|
||||
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
|
||||
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.',
|
||||
parents=[_common_parser, _strategy_parser])
|
||||
|
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import arrow
|
||||
@ -11,6 +12,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv,
|
||||
refresh_backtest_ohlcv_data,
|
||||
refresh_backtest_trades_data)
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_minutes
|
||||
from freqtrade.resolvers import ExchangeResolver
|
||||
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_from=args['format_from'], convert_to=args['format_to'],
|
||||
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'))
|
||||
|
@ -156,7 +156,9 @@ CONF_SCHEMA = {
|
||||
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||
'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']
|
||||
},
|
||||
@ -339,4 +341,5 @@ CANCEL_REASON = {
|
||||
}
|
||||
|
||||
# List of pairs with their timeframes
|
||||
ListPairsWithTimeframes = List[Tuple[str, str]]
|
||||
PairWithTimeframe = Tuple[str, str]
|
||||
ListPairsWithTimeframes = List[PairWithTimeframe]
|
||||
|
@ -5,16 +5,17 @@ including ticker and orderbook data, live and historical candle (OHLCV) data
|
||||
Common Interface for bot and strategy to access data.
|
||||
"""
|
||||
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 freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
||||
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.state import RunMode
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -25,6 +26,18 @@ class DataProvider:
|
||||
self._config = config
|
||||
self._exchange = exchange
|
||||
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,
|
||||
pairlist: ListPairsWithTimeframes,
|
||||
@ -89,6 +102,20 @@ class DataProvider:
|
||||
logger.warning(f"No data found for ({pair}, {timeframe}).")
|
||||
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]]:
|
||||
"""
|
||||
Return market data for the pair
|
||||
@ -105,7 +132,7 @@ class DataProvider:
|
||||
"""
|
||||
try:
|
||||
return self._exchange.fetch_ticker(pair)
|
||||
except DependencyException:
|
||||
except ExchangeError:
|
||||
return {}
|
||||
|
||||
def orderbook(self, pair: str, maximum: int) -> Dict[str, List]:
|
||||
|
@ -13,6 +13,7 @@ from typing import List, Optional, Type
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.configuration import TimeRange
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.data.converter import (clean_ohlcv_dataframe,
|
||||
trades_remove_duplicates, trim_dataframe)
|
||||
from freqtrade.exchange import timeframe_to_seconds
|
||||
@ -28,6 +29,14 @@ class IDataHandler(ABC):
|
||||
def __init__(self, datadir: Path) -> None:
|
||||
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
|
||||
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
|
||||
"""
|
||||
|
@ -8,7 +8,8 @@ from pandas import DataFrame, read_json, to_datetime
|
||||
|
||||
from freqtrade import misc
|
||||
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 .idatahandler import IDataHandler, TradeList
|
||||
@ -21,6 +22,18 @@ class JsonDataHandler(IDataHandler):
|
||||
_use_zip = False
|
||||
_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
|
||||
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
|
||||
"""
|
||||
|
@ -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.
|
||||
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):
|
||||
"""
|
||||
Errors with custom user-code deteced.
|
||||
|
@ -4,9 +4,11 @@ from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError,
|
||||
InvalidOrderException, OperationalException,
|
||||
TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -39,6 +41,7 @@ class Binance(Exchange):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
@ -77,8 +80,8 @@ class Binance(Exchange):
|
||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
||||
raise ExchangeError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
@ -88,6 +91,8 @@ class Binance(Exchange):
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
|
@ -1,6 +1,10 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from functools import wraps
|
||||
|
||||
from freqtrade.exceptions import TemporaryError
|
||||
from freqtrade.exceptions import (DDosProtection, RetryableOrderError,
|
||||
TemporaryError)
|
||||
|
||||
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):
|
||||
async def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||
@ -99,6 +110,10 @@ def retrier_async(f):
|
||||
count -= 1
|
||||
kwargs.update({'count': 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)
|
||||
else:
|
||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||
@ -106,19 +121,31 @@ def retrier_async(f):
|
||||
return wrapper
|
||||
|
||||
|
||||
def retrier(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except TemporaryError as ex:
|
||||
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||
if count > 0:
|
||||
count -= 1
|
||||
kwargs.update({'count': count})
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
return wrapper(*args, **kwargs)
|
||||
else:
|
||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||
raise ex
|
||||
return wrapper
|
||||
def retrier(_func=None, retries=API_RETRY_COUNT):
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def wrapper(*args, **kwargs):
|
||||
count = kwargs.pop('count', retries)
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except (TemporaryError, RetryableOrderError) as ex:
|
||||
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||
if count > 0:
|
||||
count -= 1
|
||||
kwargs.update({'count': count})
|
||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||
if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError):
|
||||
# increasing backoff
|
||||
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)
|
||||
|
@ -18,12 +18,13 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE,
|
||||
TRUNCATE, decimal_to_precision)
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError,
|
||||
InvalidOrderException, OperationalException,
|
||||
RetryableOrderError, TemporaryError)
|
||||
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
|
||||
from freqtrade.misc import deep_merge_dicts, safe_value_fallback
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
|
||||
CcxtModuleType = Any
|
||||
|
||||
@ -186,6 +187,11 @@ class Exchange:
|
||||
def timeframes(self) -> List[str]:
|
||||
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
|
||||
def markets(self) -> Dict:
|
||||
"""exchange ccxt markets"""
|
||||
@ -351,7 +357,7 @@ class Exchange:
|
||||
for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]:
|
||||
if pair in self.markets and self.markets[pair].get('active'):
|
||||
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:
|
||||
"""
|
||||
@ -518,15 +524,17 @@ class Exchange:
|
||||
amount, rate_for_order, params)
|
||||
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}.'
|
||||
raise ExchangeError(
|
||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise DependencyException(
|
||||
f'Could not create {ordertype} {side} order on market {pair}.'
|
||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||
raise ExchangeError(
|
||||
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||
f'Tried to {side} amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
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)
|
||||
|
||||
return balances
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@ -620,6 +630,8 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching tickers in batch. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
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:
|
||||
try:
|
||||
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)
|
||||
return data
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e
|
||||
@ -766,6 +780,8 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching historical '
|
||||
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:
|
||||
raise TemporaryError(f'Could not fetch historical candle (OHLCV) data '
|
||||
f'for pair {pair} due to {e.__class__.__name__}. '
|
||||
@ -802,6 +818,8 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching historical trade data.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. '
|
||||
f'Message: {e}') from e
|
||||
@ -933,7 +951,7 @@ class Exchange:
|
||||
def check_order_canceled_empty(self, order: Dict) -> bool:
|
||||
"""
|
||||
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 order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0
|
||||
@ -948,13 +966,15 @@ class Exchange:
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
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:
|
||||
raise TemporaryError(
|
||||
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as 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
|
||||
|
||||
def is_cancel_order_result_suitable(self, corder) -> bool:
|
||||
@ -968,7 +988,7 @@ class Exchange:
|
||||
"""
|
||||
Cancel order returning a 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 pair: Pair corresponding to order_id
|
||||
:param amount: Amount to use for fake response
|
||||
@ -981,7 +1001,7 @@ class Exchange:
|
||||
except InvalidOrderException:
|
||||
logger.warning(f"Could not cancel order {order_id}.")
|
||||
try:
|
||||
order = self.get_order(order_id, pair)
|
||||
order = self.fetch_order(order_id, pair)
|
||||
except InvalidOrderException:
|
||||
logger.warning(f"Could not fetch cancelled order {order_id}.")
|
||||
order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}}
|
||||
@ -989,7 +1009,7 @@ class Exchange:
|
||||
return order
|
||||
|
||||
@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']:
|
||||
try:
|
||||
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
|
||||
try:
|
||||
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:
|
||||
raise InvalidOrderException(
|
||||
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:
|
||||
raise TemporaryError(
|
||||
f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
# Assign method to get_stoploss_order to allow easy overriding in other classes
|
||||
get_stoploss_order = get_order
|
||||
# Assign method to fetch_stoploss_order to allow easy overriding in other classes
|
||||
fetch_stoploss_order = fetch_order
|
||||
|
||||
@retrier
|
||||
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||
@ -1027,6 +1052,8 @@ class Exchange:
|
||||
raise OperationalException(
|
||||
f'Exchange {self._api.name} does not support fetching order book.'
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
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]
|
||||
|
||||
return matched_trades
|
||||
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
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,
|
||||
price=price, takerOrMaker=taker_or_maker)['rate']
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
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')
|
||||
return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8)
|
||||
except DependencyException:
|
||||
except ExchangeError:
|
||||
return None
|
||||
|
||||
def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]:
|
||||
|
@ -4,8 +4,9 @@ from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError,
|
||||
InvalidOrderException, OperationalException,
|
||||
TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
@ -26,6 +27,7 @@ class Ftx(Exchange):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
Creates a stoploss order.
|
||||
@ -59,7 +61,7 @@ class Ftx(Exchange):
|
||||
'stop price: %s.', pair, stop_price)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
raise ExchangeError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
@ -68,6 +70,8 @@ class Ftx(Exchange):
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
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
|
||||
|
||||
@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']:
|
||||
try:
|
||||
order = self._dry_run_open_orders[order_id]
|
||||
@ -96,6 +100,8 @@ class Ftx(Exchange):
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise InvalidOrderException(
|
||||
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:
|
||||
raise TemporaryError(
|
||||
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:
|
||||
raise InvalidOrderException(
|
||||
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:
|
||||
raise TemporaryError(
|
||||
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
|
@ -4,8 +4,9 @@ from typing import Dict
|
||||
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, ExchangeError,
|
||||
InvalidOrderException, OperationalException,
|
||||
TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
@ -45,6 +46,8 @@ class Kraken(Exchange):
|
||||
balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used']
|
||||
|
||||
return balances
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
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'])
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
Creates a stoploss market order.
|
||||
@ -84,8 +88,8 @@ class Kraken(Exchange):
|
||||
'stop price: %s.', pair, stop_price)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
||||
raise ExchangeError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
@ -93,6 +97,8 @@ class Kraken(Exchange):
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
|
@ -11,14 +11,14 @@ from typing import Any, Dict, List, Optional
|
||||
|
||||
import arrow
|
||||
from cachetools import TTLCache
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from freqtrade import __version__, constants, persistence
|
||||
from freqtrade.configuration import validate_config_consistency
|
||||
from freqtrade.data.converter import order_book_to_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
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.misc import safe_value_fallback
|
||||
from freqtrade.pairlist.pairlistmanager import PairListManager
|
||||
@ -119,6 +119,8 @@ class FreqtradeBot:
|
||||
if self.config['cancel_open_orders_on_exit']:
|
||||
self.cancel_all_open_orders()
|
||||
|
||||
self.check_for_open_trades()
|
||||
|
||||
self.rpc.cleanup()
|
||||
persistence.cleanup()
|
||||
|
||||
@ -151,6 +153,10 @@ class FreqtradeBot:
|
||||
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
||||
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:
|
||||
# Check and handle any timed out open orders
|
||||
self.check_handle_timedout()
|
||||
@ -175,6 +181,24 @@ class FreqtradeBot:
|
||||
if self.config['cancel_open_orders_on_exit']:
|
||||
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]:
|
||||
"""
|
||||
Refresh active whitelist from pairlist or edge and extend it with
|
||||
@ -420,9 +444,8 @@ class FreqtradeBot:
|
||||
return False
|
||||
|
||||
# running get_signal on historical data fetched
|
||||
(buy, sell) = self.strategy.get_signal(
|
||||
pair, self.strategy.timeframe,
|
||||
self.dataprovider.ohlcv(pair, self.strategy.timeframe))
|
||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe)
|
||||
(buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df)
|
||||
|
||||
if buy and not sell:
|
||||
stake_amount = self.get_trade_stake_amount(pair)
|
||||
@ -495,6 +518,12 @@ class FreqtradeBot:
|
||||
|
||||
amount = stake_amount / buy_limit_requested
|
||||
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,
|
||||
amount=amount, rate=buy_limit_requested,
|
||||
time_in_force=time_in_force)
|
||||
@ -569,6 +598,7 @@ class FreqtradeBot:
|
||||
Sends rpc notification when a buy occured.
|
||||
"""
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
@ -592,6 +622,7 @@ class FreqtradeBot:
|
||||
current_rate = self.get_buy_rate(trade.pair, False)
|
||||
|
||||
msg = {
|
||||
'trade_id': trade.id,
|
||||
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
@ -697,9 +728,10 @@ class FreqtradeBot:
|
||||
|
||||
if (config_ask_strategy.get('use_sell_signal', True) or
|
||||
config_ask_strategy.get('ignore_roi_if_buy_signal', False)):
|
||||
(buy, sell) = self.strategy.get_signal(
|
||||
trade.pair, self.strategy.timeframe,
|
||||
self.dataprovider.ohlcv(trade.pair, self.strategy.timeframe))
|
||||
analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(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):
|
||||
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
||||
@ -755,7 +787,7 @@ class FreqtradeBot:
|
||||
logger.warning('Selling the trade forcefully')
|
||||
self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL)
|
||||
|
||||
except DependencyException:
|
||||
except ExchangeError:
|
||||
trade.stoploss_order_id = None
|
||||
logger.exception('Unable to place a stoploss order on exchange.')
|
||||
return False
|
||||
@ -773,8 +805,8 @@ class FreqtradeBot:
|
||||
|
||||
try:
|
||||
# First we check if there is already a stoploss on exchange
|
||||
stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \
|
||||
if trade.stoploss_order_id else None
|
||||
stoploss_order = self.exchange.fetch_stoploss_order(
|
||||
trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None
|
||||
except InvalidOrderException as exception:
|
||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||
|
||||
@ -795,10 +827,8 @@ class FreqtradeBot:
|
||||
return False
|
||||
|
||||
# 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
|
||||
|
||||
stop_price = trade.open_rate * (1 + stoploss)
|
||||
|
||||
if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price):
|
||||
@ -890,8 +920,8 @@ class FreqtradeBot:
|
||||
try:
|
||||
if not trade.open_order_id:
|
||||
continue
|
||||
order = self.exchange.get_order(trade.open_order_id, trade.pair)
|
||||
except (RequestException, DependencyException, InvalidOrderException):
|
||||
order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||
except (ExchangeError, InvalidOrderException):
|
||||
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
||||
continue
|
||||
|
||||
@ -923,7 +953,7 @@ class FreqtradeBot:
|
||||
|
||||
for trade in Trade.get_open_order_trades():
|
||||
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):
|
||||
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
||||
continue
|
||||
@ -1077,12 +1107,20 @@ class FreqtradeBot:
|
||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||
|
||||
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
|
||||
order = self.exchange.sell(pair=str(trade.pair),
|
||||
ordertype=order_type,
|
||||
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']
|
||||
@ -1113,6 +1151,7 @@ class FreqtradeBot:
|
||||
|
||||
msg = {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'trade_id': trade.id,
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
@ -1155,6 +1194,7 @@ class FreqtradeBot:
|
||||
|
||||
msg = {
|
||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
||||
'trade_id': trade.id,
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
@ -1202,7 +1242,7 @@ class FreqtradeBot:
|
||||
# Update trade with order values
|
||||
logger.info('Found open order for %s', trade)
|
||||
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:
|
||||
logger.warning('Unable to fetch order %s: %s', order_id, exception)
|
||||
return False
|
||||
|
@ -101,7 +101,7 @@ class Backtesting:
|
||||
if len(self.pairlists.whitelist) == 0:
|
||||
raise OperationalException("No pair in whitelist.")
|
||||
|
||||
if config.get('fee'):
|
||||
if config.get('fee', None) is not None:
|
||||
self.fee = config['fee']
|
||||
else:
|
||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||
|
@ -5,6 +5,7 @@ import logging
|
||||
import arrow
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import plural
|
||||
from freqtrade.pairlist.IPairList import IPairList
|
||||
|
||||
@ -23,6 +24,13 @@ class AgeFilter(IPairList):
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
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
|
||||
|
||||
@property
|
||||
@ -69,7 +77,7 @@ class AgeFilter(IPairList):
|
||||
return True
|
||||
else:
|
||||
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"{plural(self._min_days_listed, 'day')}")
|
||||
return False
|
||||
|
@ -18,7 +18,11 @@ class PriceFilter(IPairList):
|
||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||
|
||||
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
|
||||
def needstickers(self) -> bool:
|
||||
@ -33,7 +37,18 @@ class PriceFilter(IPairList):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
@ -41,15 +56,33 @@ class PriceFilter(IPairList):
|
||||
:param ticker: ticker dict as returned from ccxt.load_markets()
|
||||
: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,
|
||||
f"Removed {ticker['symbol']} from whitelist, because "
|
||||
"ticker['last'] is empty (Usually no trade in the last 24h).")
|
||||
return False
|
||||
compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last'])
|
||||
changeperc = compare / ticker['last']
|
||||
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 low_price_ratio check.
|
||||
if self._low_price_ratio != 0:
|
||||
compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last'])
|
||||
changeperc = compare / ticker['last']
|
||||
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
|
||||
|
@ -360,7 +360,7 @@ class Trade(_DECL_BASE):
|
||||
def update(self, order: Dict) -> None:
|
||||
"""
|
||||
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
|
||||
"""
|
||||
order_type = order['type']
|
||||
|
@ -10,11 +10,13 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown,
|
||||
create_cum_profit,
|
||||
extract_trades_of_period, load_trades)
|
||||
from freqtrade.data.converter import trim_dataframe
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.data.history import load_data
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import timeframe_to_prev_date
|
||||
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__)
|
||||
|
||||
@ -467,6 +469,8 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
||||
"""
|
||||
strategy = StrategyResolver.load_strategy(config)
|
||||
|
||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
||||
IStrategy.dp = DataProvider(config, exchange)
|
||||
plot_elements = init_plotscript(config)
|
||||
trades = plot_elements['trades']
|
||||
pair_counter = 0
|
||||
|
@ -42,14 +42,14 @@ class HyperOptResolver(IResolver):
|
||||
extra_dir=config.get('hyperopt_path'))
|
||||
|
||||
if not hasattr(hyperopt, 'populate_indicators'):
|
||||
logger.warning("Hyperopt class does not provide populate_indicators() method. "
|
||||
"Using populate_indicators from the strategy.")
|
||||
logger.info("Hyperopt class does not provide populate_indicators() method. "
|
||||
"Using populate_indicators from the strategy.")
|
||||
if not hasattr(hyperopt, 'populate_buy_trend'):
|
||||
logger.warning("Hyperopt class does not provide populate_buy_trend() method. "
|
||||
"Using populate_buy_trend from the strategy.")
|
||||
logger.info("Hyperopt class does not provide populate_buy_trend() method. "
|
||||
"Using populate_buy_trend from the strategy.")
|
||||
if not hasattr(hyperopt, 'populate_sell_trend'):
|
||||
logger.warning("Hyperopt class does not provide populate_sell_trend() method. "
|
||||
"Using populate_sell_trend from the strategy.")
|
||||
logger.info("Hyperopt class does not provide populate_sell_trend() method. "
|
||||
"Using populate_sell_trend from the strategy.")
|
||||
return hyperopt
|
||||
|
||||
|
||||
|
@ -17,6 +17,7 @@ from werkzeug.serving import make_server
|
||||
|
||||
from freqtrade.__init__ import __version__
|
||||
from freqtrade.rpc.rpc import RPC, RPCException
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -105,6 +106,9 @@ class ApiServer(RPC):
|
||||
# Register application handling
|
||||
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.start()
|
||||
|
||||
|
@ -11,7 +11,9 @@ from typing import Any, Dict, List, Optional, Tuple
|
||||
import arrow
|
||||
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.persistence import Trade
|
||||
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'),
|
||||
'ticker_interval': config['timeframe'], # DEPRECATED
|
||||
'timeframe': config['timeframe'],
|
||||
'timeframe_ms': timeframe_to_msecs(config['timeframe']),
|
||||
'timeframe_min': timeframe_to_minutes(config['timeframe']),
|
||||
'exchange': config['exchange']['name'],
|
||||
'strategy': config['strategy'],
|
||||
'forcebuy_enabled': config.get('forcebuy_enable', False),
|
||||
@ -126,11 +130,11 @@ class RPC:
|
||||
for trade in trades:
|
||||
order = None
|
||||
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
|
||||
try:
|
||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||
except DependencyException:
|
||||
except (ExchangeError, PricingError):
|
||||
current_rate = NAN
|
||||
current_profit = trade.calc_profit_ratio(current_rate)
|
||||
current_profit_abs = trade.calc_profit(current_rate)
|
||||
@ -174,7 +178,7 @@ class RPC:
|
||||
# calculate profit and send message to user
|
||||
try:
|
||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||
except DependencyException:
|
||||
except (PricingError, ExchangeError):
|
||||
current_rate = NAN
|
||||
trade_percent = (100 * trade.calc_profit_ratio(current_rate))
|
||||
trade_profit = trade.calc_profit(current_rate)
|
||||
@ -248,9 +252,10 @@ class RPC:
|
||||
def _rpc_trade_history(self, limit: int) -> Dict:
|
||||
""" Returns the X last trades """
|
||||
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:
|
||||
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]
|
||||
|
||||
@ -269,6 +274,8 @@ class RPC:
|
||||
profit_closed_coin = []
|
||||
profit_closed_ratio = []
|
||||
durations = []
|
||||
winning_trades = 0
|
||||
losing_trades = 0
|
||||
|
||||
for trade in trades:
|
||||
current_rate: float = 0.0
|
||||
@ -282,11 +289,15 @@ class RPC:
|
||||
profit_ratio = trade.close_profit
|
||||
profit_closed_coin.append(trade.close_profit_abs)
|
||||
profit_closed_ratio.append(profit_ratio)
|
||||
if trade.close_profit >= 0:
|
||||
winning_trades += 1
|
||||
else:
|
||||
losing_trades += 1
|
||||
else:
|
||||
# Get current rate
|
||||
try:
|
||||
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
|
||||
except DependencyException:
|
||||
except (PricingError, ExchangeError):
|
||||
current_rate = NAN
|
||||
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],
|
||||
'best_pair': best_pair[0] if best_pair else '',
|
||||
'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:
|
||||
@ -352,7 +365,7 @@ class RPC:
|
||||
total = 0.0
|
||||
try:
|
||||
tickers = self._freqtrade.exchange.get_tickers()
|
||||
except (TemporaryError, DependencyException):
|
||||
except (ExchangeError):
|
||||
raise RPCException('Error getting current tickers.')
|
||||
|
||||
self._freqtrade.wallets.update(require_update=False)
|
||||
@ -373,7 +386,7 @@ class RPC:
|
||||
if pair.startswith(stake_currency):
|
||||
rate = 1.0 / rate
|
||||
est_stake = rate * balance.total
|
||||
except (TemporaryError, DependencyException):
|
||||
except (ExchangeError):
|
||||
logger.warning(f" Could not get rate for pair {coin}.")
|
||||
continue
|
||||
total = total + (est_stake or 0)
|
||||
@ -442,7 +455,7 @@ class RPC:
|
||||
def _exec_forcesell(trade: Trade) -> None:
|
||||
# Check if there is there is an open order
|
||||
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
|
||||
if order and order['status'] == 'open' \
|
||||
@ -511,7 +524,7 @@ class RPC:
|
||||
# check if valid 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:
|
||||
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
||||
|
||||
@ -520,7 +533,7 @@ class RPC:
|
||||
|
||||
# execute buy
|
||||
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
|
||||
else:
|
||||
return None
|
||||
|
@ -5,6 +5,7 @@ This module manage Telegram communication
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import arrow
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
from tabulate import tabulate
|
||||
@ -92,6 +93,7 @@ class Telegram(RPC):
|
||||
CommandHandler('stop', self._stop),
|
||||
CommandHandler('forcesell', self._forcesell),
|
||||
CommandHandler('forcebuy', self._forcebuy),
|
||||
CommandHandler('trades', self._trades),
|
||||
CommandHandler('performance', self._performance),
|
||||
CommandHandler('daily', self._daily),
|
||||
CommandHandler('count', self._count),
|
||||
@ -366,7 +368,9 @@ class Telegram(RPC):
|
||||
f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n"
|
||||
f"*Total Trade Count:* `{trade_count}`\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:
|
||||
markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n"
|
||||
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`")
|
||||
@ -494,6 +498,41 @@ class Telegram(RPC):
|
||||
except RPCException as 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
|
||||
def _performance(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
@ -607,6 +646,7 @@ class Telegram(RPC):
|
||||
" *table :* `will display trades in a table`\n"
|
||||
" `pending buy orders are marked with an 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"
|
||||
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
|
||||
"regardless of profit`\n"
|
||||
|
@ -7,20 +7,19 @@ import warnings
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from typing import Dict, NamedTuple, Optional, Tuple
|
||||
from typing import Dict, List, NamedTuple, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
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.persistence import Trade
|
||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||
from freqtrade.constants import ListPairsWithTimeframes
|
||||
from freqtrade.wallets import Wallets
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -191,6 +190,63 @@ class IStrategy(ABC):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||
@ -204,6 +260,10 @@ class IStrategy(ABC):
|
||||
"""
|
||||
return []
|
||||
|
||||
###
|
||||
# END - Intended to be overridden by strategy
|
||||
###
|
||||
|
||||
def get_strategy_name(self) -> str:
|
||||
"""
|
||||
Returns strategy class name
|
||||
@ -273,6 +333,8 @@ class IStrategy(ABC):
|
||||
# Defs that only make change on new candle data.
|
||||
dataframe = self.analyze_ticker(dataframe, metadata)
|
||||
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
|
||||
if self.dp:
|
||||
self.dp._set_cached_df(pair, self.timeframe, dataframe)
|
||||
else:
|
||||
logger.debug("Skipping TA Analysis for already analyzed candle")
|
||||
dataframe['buy'] = 0
|
||||
@ -284,13 +346,53 @@ class IStrategy(ABC):
|
||||
|
||||
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
|
||||
def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]:
|
||||
""" keep some data for dataframes """
|
||||
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):
|
||||
""" make sure data is unmodified """
|
||||
"""
|
||||
Ensure dataframe (length, last candle) was not modified, and has all elements we need.
|
||||
"""
|
||||
message = ""
|
||||
if df_len != len(dataframe):
|
||||
message = "length"
|
||||
@ -304,31 +406,17 @@ class IStrategy(ABC):
|
||||
else:
|
||||
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 interval: Interval to use (in min)
|
||||
:param dataframe: Dataframe to analyze
|
||||
:param timeframe: timeframe to use
|
||||
:param dataframe: Analyzed dataframe to get signal from.
|
||||
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
|
||||
"""
|
||||
if not isinstance(dataframe, DataFrame) or dataframe.empty:
|
||||
logger.warning('Empty candle (OHLCV) data for pair %s', 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)
|
||||
logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
|
||||
return False, False
|
||||
|
||||
latest_date = dataframe['date'].max()
|
||||
@ -337,24 +425,18 @@ class IStrategy(ABC):
|
||||
latest_date = arrow.get(latest_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)
|
||||
if latest_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))):
|
||||
if latest_date < (arrow.utcnow().shift(minutes=-(timeframe_minutes * 2 + offset))):
|
||||
logger.warning(
|
||||
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||
pair,
|
||||
(arrow.utcnow() - latest_date).seconds // 60
|
||||
pair, (arrow.utcnow() - latest_date).seconds // 60
|
||||
)
|
||||
return False, False
|
||||
|
||||
(buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1
|
||||
logger.debug(
|
||||
'trigger: %s (pair=%s) buy=%s sell=%s',
|
||||
latest['date'],
|
||||
pair,
|
||||
str(buy),
|
||||
str(sell)
|
||||
)
|
||||
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
|
||||
latest['date'], pair, str(buy), str(sell))
|
||||
return buy, sell
|
||||
|
||||
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]:
|
||||
"""
|
||||
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.
|
||||
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
|
||||
|
@ -5,7 +5,7 @@ from freqtrade.exceptions import StrategyError
|
||||
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.
|
||||
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"{f}"
|
||||
)
|
||||
if default_retval is None:
|
||||
if default_retval is None and not supress_error:
|
||||
raise StrategyError(str(error)) from error
|
||||
return default_retval
|
||||
except Exception as error:
|
||||
@ -28,7 +28,7 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None):
|
||||
f"{message}"
|
||||
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
|
||||
return default_retval
|
||||
|
||||
|
@ -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:
|
||||
"""
|
||||
Check buy timeout function callback.
|
||||
|
@ -90,6 +90,9 @@ class Worker:
|
||||
if state == State.RUNNING:
|
||||
self.freqtrade.startup()
|
||||
|
||||
if state == State.STOPPED:
|
||||
self.freqtrade.check_for_open_trades()
|
||||
|
||||
# Reset heartbeat timestamp to log the heartbeat message at
|
||||
# first throttling iteration when the state changes
|
||||
self._heartbeat_msg = 0
|
||||
|
@ -3,6 +3,7 @@ nav:
|
||||
- Home: index.md
|
||||
- Installation Docker: docker.md
|
||||
- Installation: installation.md
|
||||
- Freqtrade Basics: bot-basics.md
|
||||
- Configuration: configuration.md
|
||||
- Strategy Customization: strategy-customization.md
|
||||
- Stoploss: stoploss.md
|
||||
|
@ -1,17 +1,17 @@
|
||||
# requirements without requirements installable via conda
|
||||
# mainly used for Raspberry pi installs
|
||||
ccxt==1.30.48
|
||||
ccxt==1.32.7
|
||||
SQLAlchemy==1.3.18
|
||||
python-telegram-bot==12.8
|
||||
arrow==0.15.7
|
||||
arrow==0.15.8
|
||||
cachetools==4.1.1
|
||||
requests==2.24.0
|
||||
urllib3==1.25.9
|
||||
urllib3==1.25.10
|
||||
wrapt==1.12.1
|
||||
jsonschema==3.2.0
|
||||
TA-Lib==0.4.18
|
||||
tabulate==0.8.7
|
||||
pycoingecko==1.2.0
|
||||
pycoingecko==1.3.0
|
||||
jinja2==2.11.2
|
||||
|
||||
# find first, C search in arrays
|
||||
|
@ -3,7 +3,7 @@
|
||||
-r requirements-plot.txt
|
||||
-r requirements-hyperopt.txt
|
||||
|
||||
coveralls==2.0.0
|
||||
coveralls==2.1.1
|
||||
flake8==3.8.3
|
||||
flake8-type-annotations==0.1.0
|
||||
flake8-tidy-imports==4.1.0
|
||||
@ -11,7 +11,7 @@ mypy==0.782
|
||||
pytest==5.4.3
|
||||
pytest-asyncio==0.14.0
|
||||
pytest-cov==2.10.0
|
||||
pytest-mock==3.1.1
|
||||
pytest-mock==3.2.0
|
||||
pytest-random-order==1.0.4
|
||||
|
||||
# Convert jupyter notebooks to markdown documents
|
||||
|
@ -2,9 +2,9 @@
|
||||
-r requirements.txt
|
||||
|
||||
# Required for hyperopt
|
||||
scipy==1.5.0
|
||||
scipy==1.5.2
|
||||
scikit-learn==0.23.1
|
||||
scikit-optimize==0.7.4
|
||||
filelock==3.0.12
|
||||
joblib==0.15.1
|
||||
joblib==0.16.0
|
||||
progressbar2==3.51.4
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
plotly==4.8.2
|
||||
plotly==4.9.0
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
# Load common requirements
|
||||
-r requirements-common.txt
|
||||
|
||||
numpy==1.19.0
|
||||
numpy==1.19.1
|
||||
pandas==1.0.5
|
||||
|
@ -6,12 +6,12 @@ import pytest
|
||||
|
||||
from freqtrade.commands import (start_convert_data, start_create_userdir,
|
||||
start_download_data, start_hyperopt_list,
|
||||
start_hyperopt_show, start_list_exchanges,
|
||||
start_list_hyperopts, start_list_markets,
|
||||
start_list_strategies, start_list_timeframes,
|
||||
start_new_hyperopt, start_new_strategy,
|
||||
start_show_trades, start_test_pairlist,
|
||||
start_trading)
|
||||
start_hyperopt_show, start_list_data,
|
||||
start_list_exchanges, start_list_hyperopts,
|
||||
start_list_markets, start_list_strategies,
|
||||
start_list_timeframes, start_new_hyperopt,
|
||||
start_new_strategy, start_show_trades,
|
||||
start_test_pairlist, start_trading)
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.exceptions import OperationalException
|
||||
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
|
||||
|
||||
|
||||
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")
|
||||
def test_show_trades(mocker, fee, capsys, caplog):
|
||||
mocker.patch("freqtrade.persistence.init")
|
||||
@ -1055,7 +1089,7 @@ def test_show_trades(mocker, fee, capsys, caplog):
|
||||
pargs = get_args(args)
|
||||
pargs['config'] = None
|
||||
start_show_trades(pargs)
|
||||
assert log_has("Printing 3 Trades: ", caplog)
|
||||
assert log_has("Printing 4 Trades: ", caplog)
|
||||
captured = capsys.readouterr()
|
||||
assert "Trade(id=1" in captured.out
|
||||
assert "Trade(id=2" in captured.out
|
||||
|
@ -163,7 +163,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
|
||||
:param value: which value IStrategy.get_signal() must return
|
||||
: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
|
||||
|
||||
|
||||
@ -199,6 +199,20 @@ def create_mock_trades(fee):
|
||||
)
|
||||
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
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
@ -661,7 +675,8 @@ def shitcoinmarkets(markets):
|
||||
Fixture with shitcoin markets - used to test filters in pairlists
|
||||
"""
|
||||
shitmarkets = deepcopy(markets)
|
||||
shitmarkets.update({'HOT/BTC': {
|
||||
shitmarkets.update({
|
||||
'HOT/BTC': {
|
||||
'id': 'HOTBTC',
|
||||
'symbol': 'HOT/BTC',
|
||||
'base': 'HOT',
|
||||
@ -766,7 +781,32 @@ def shitcoinmarkets(markets):
|
||||
"spot": True,
|
||||
"future": False,
|
||||
"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
|
||||
|
||||
@ -787,6 +827,7 @@ def limit_buy_order():
|
||||
'price': 0.00001099,
|
||||
'amount': 90.99181073,
|
||||
'filled': 90.99181073,
|
||||
'cost': 0.0009999,
|
||||
'remaining': 0.0,
|
||||
'status': 'closed'
|
||||
}
|
||||
@ -1387,6 +1428,28 @@ def tickers():
|
||||
"quoteVolume": 0.0,
|
||||
"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": {}
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
|
@ -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'])
|
||||
assert init_mock.call_count == 1
|
||||
assert len(trades) == 3
|
||||
assert len(trades) == 4
|
||||
assert isinstance(trades, DataFrame)
|
||||
assert "pair" in trades.columns
|
||||
assert "open_time" in trades.columns
|
||||
|
@ -1,11 +1,12 @@
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pandas import DataFrame
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.data.dataprovider import DataProvider
|
||||
from freqtrade.exceptions import ExchangeError, OperationalException
|
||||
from freqtrade.pairlist.pairlistmanager import PairListManager
|
||||
from freqtrade.exceptions import DependencyException, OperationalException
|
||||
from freqtrade.state import RunMode
|
||||
from tests.conftest import get_patched_exchange
|
||||
|
||||
@ -164,7 +165,7 @@ def test_ticker(mocker, default_conf, tickers):
|
||||
assert 'symbol' in res
|
||||
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)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
@ -194,3 +195,29 @@ def test_current_whitelist(mocker, default_conf, tickers):
|
||||
with pytest.raises(OperationalException):
|
||||
dp = DataProvider(default_conf, exchange)
|
||||
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)
|
||||
|
@ -631,6 +631,20 @@ def test_jsondatahandler_ohlcv_get_pairs(testdatadir):
|
||||
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):
|
||||
pairs = JsonGzDataHandler.trades_get_pairs(testdatadir)
|
||||
# Convert to set to avoid failures due to sorting
|
||||
|
@ -5,8 +5,9 @@ import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
OperationalException)
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
|
||||
@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.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
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={})
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance",
|
||||
"stoploss", "create_order", retries=1,
|
||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
|
||||
def test_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||
|
@ -4,17 +4,17 @@ import copy
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock, Mock, PropertyMock
|
||||
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection,
|
||||
OperationalException, TemporaryError)
|
||||
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,
|
||||
timeframe_to_minutes,
|
||||
timeframe_to_msecs,
|
||||
@ -37,12 +37,20 @@ def get_mock_coro(return_value):
|
||||
|
||||
|
||||
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):
|
||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef"))
|
||||
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 == API_RETRY_COUNT + 1
|
||||
assert api_mock.__dict__[mock_ccxt_fun].call_count == retries
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
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):
|
||||
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_stakecurrency')
|
||||
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
|
||||
|
||||
default_conf['order_types'] = {
|
||||
'buy': 'limit',
|
||||
'sell': 'limit',
|
||||
'stoploss': 'market',
|
||||
'stoploss_on_exchange': False
|
||||
}
|
||||
|
||||
Exchange(default_conf)
|
||||
|
||||
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False})
|
||||
@ -713,9 +730,8 @@ def test_validate_order_types(default_conf, mocker):
|
||||
'buy': 'limit',
|
||||
'sell': 'limit',
|
||||
'stoploss': 'market',
|
||||
'stoploss_on_exchange': 'false'
|
||||
'stoploss_on_exchange': False
|
||||
}
|
||||
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'Exchange .* does not support market orders.'):
|
||||
Exchange(default_conf)
|
||||
@ -726,7 +742,6 @@ def test_validate_order_types(default_conf, mocker):
|
||||
'stoploss': 'limit',
|
||||
'stoploss_on_exchange': True
|
||||
}
|
||||
|
||||
with pytest.raises(OperationalException,
|
||||
match=r'On exchange stoploss is not supported for .*'):
|
||||
Exchange(default_conf)
|
||||
@ -1127,9 +1142,10 @@ def test_get_balance_prod(default_conf, mocker, exchange_name):
|
||||
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
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
assert exchange.get_balances() == {}
|
||||
|
||||
|
||||
@ -1847,36 +1863,48 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name):
|
||||
|
||||
|
||||
@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
|
||||
order = MagicMock()
|
||||
order.myid = 123
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
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.*'):
|
||||
exchange.get_order('Y', 'TKN/BTC')
|
||||
exchange.fetch_order('Y', 'TKN/BTC')
|
||||
|
||||
default_conf['dry_run'] = False
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_order = MagicMock(return_value=456)
|
||||
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):
|
||||
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_order(order_id='_', pair='TKN/BTC')
|
||||
exchange.fetch_order(order_id='_', pair='TKN/BTC')
|
||||
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,
|
||||
'get_order', 'fetch_order',
|
||||
'fetch_order', 'fetch_order',
|
||||
order_id='_', pair='TKN/BTC')
|
||||
|
||||
|
||||
@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
|
||||
if exchange_name == 'ftx':
|
||||
return
|
||||
@ -1885,25 +1913,25 @@ def test_get_stoploss_order(default_conf, mocker, exchange_name):
|
||||
order.myid = 123
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
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.*'):
|
||||
exchange.get_stoploss_order('Y', 'TKN/BTC')
|
||||
exchange.fetch_stoploss_order('Y', 'TKN/BTC')
|
||||
|
||||
default_conf['dry_run'] = False
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_order = MagicMock(return_value=456)
|
||||
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):
|
||||
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_stoploss_order(order_id='_', pair='TKN/BTC')
|
||||
exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC')
|
||||
assert api_mock.fetch_order.call_count == 1
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
'get_stoploss_order', 'fetch_order',
|
||||
'fetch_stoploss_order', 'fetch_order',
|
||||
order_id='_', pair='TKN/BTC')
|
||||
|
||||
|
||||
@ -2111,6 +2139,13 @@ def test_get_markets(default_conf, mocker, markets,
|
||||
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():
|
||||
assert timeframe_to_minutes("5m") == 5
|
||||
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)
|
||||
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
|
||||
|
@ -6,9 +6,9 @@ from unittest.mock import MagicMock
|
||||
import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException
|
||||
from tests.conftest import get_patched_exchange
|
||||
|
||||
from .test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
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.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||
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={})
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx",
|
||||
"stoploss", "create_order", retries=1,
|
||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def test_get_stoploss_order(default_conf, mocker):
|
||||
def test_fetch_stoploss_order(default_conf, mocker):
|
||||
default_conf['dry_run'] = True
|
||||
order = MagicMock()
|
||||
order.myid = 123
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
|
||||
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.*'):
|
||||
exchange.get_stoploss_order('Y', 'TKN/BTC')
|
||||
exchange.fetch_stoploss_order('Y', 'TKN/BTC')
|
||||
|
||||
default_conf['dry_run'] = False
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}])
|
||||
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'}])
|
||||
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"):
|
||||
exchange.get_stoploss_order('X', 'TKN/BTC')['status']
|
||||
exchange.fetch_stoploss_order('X', 'TKN/BTC')['status']
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
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_stoploss_order(order_id='_', pair='TKN/BTC')
|
||||
exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC')
|
||||
assert api_mock.fetch_orders.call_count == 1
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx',
|
||||
'get_stoploss_order', 'fetch_orders',
|
||||
'fetch_stoploss_order', 'fetch_orders',
|
||||
order_id='_', pair='TKN/BTC')
|
||||
|
@ -6,8 +6,7 @@ from unittest.mock import MagicMock
|
||||
import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException
|
||||
from tests.conftest import get_patched_exchange
|
||||
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.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||
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={})
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
|
||||
"stoploss", "create_order", retries=1,
|
||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
|
||||
def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
||||
|
@ -308,6 +308,11 @@ def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
|
||||
assert backtesting.fee == 0.1234
|
||||
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:
|
||||
patch_exchange(mocker)
|
||||
|
@ -235,7 +235,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
|
||||
"BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
|
||||
([{"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
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
||||
"ETH", []),
|
||||
@ -275,11 +275,16 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||
{"method": "PriceFilter", "low_price_ratio": 0.03}],
|
||||
"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": "PrecisionFilter"},
|
||||
{"method": "PriceFilter", "low_price_ratio": 0.02}],
|
||||
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
|
||||
{"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.01}],
|
||||
"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
|
||||
([{"method": "VolumePairList", "number_assets": 5,
|
||||
"sort_key": "quoteVolume", "min_value": 1250}],
|
||||
@ -298,11 +303,11 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||
# ShuffleFilter
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||
{"method": "ShuffleFilter", "seed": 77}],
|
||||
"USDT", ['ETH/USDT', 'ADAHALF/USDT', 'NANO/USDT']),
|
||||
"USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']),
|
||||
# ShuffleFilter, other seed
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||
{"method": "ShuffleFilter", "seed": 42}],
|
||||
"USDT", ['NANO/USDT', 'ETH/USDT', 'ADAHALF/USDT']),
|
||||
"USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']),
|
||||
# ShuffleFilter, no seed
|
||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||
{"method": "ShuffleFilter"}],
|
||||
@ -319,7 +324,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
||||
"BTC", 'filter_at_the_beginning'), # OperationalException expected
|
||||
# PriceFilter after 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']),
|
||||
# PriceFilter only
|
||||
([{"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": "StaticPairList"}],
|
||||
"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,
|
||||
ohlcv_history_list, pairlists, base_currency,
|
||||
@ -389,13 +397,17 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
|
||||
for pairlist in pairlists:
|
||||
if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \
|
||||
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)
|
||||
if pairlist['method'] == 'PrecisionFilter' and whitelist_result:
|
||||
assert log_has_re(r'^Removed .* from whitelist, because stop price .* '
|
||||
r'would be <= stop limit.*', caplog)
|
||||
if pairlist['method'] == 'PriceFilter' and whitelist_result:
|
||||
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'\] "
|
||||
r"is empty.*", caplog))
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
|
||||
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
|
||||
|
||||
|
||||
@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):
|
||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||
|
||||
|
@ -8,12 +8,13 @@ import pytest
|
||||
from numpy import isnan
|
||||
|
||||
from freqtrade.edge import PairInfo
|
||||
from freqtrade.exceptions import DependencyException, TemporaryError
|
||||
from freqtrade.exceptions import ExchangeError, TemporaryError
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import RPC, RPCException
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
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
|
||||
@ -106,7 +107,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
}
|
||||
|
||||
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()
|
||||
assert isnan(results[0]['current_profit'])
|
||||
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]
|
||||
|
||||
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')
|
||||
assert 'instantly' == result[0][2]
|
||||
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)
|
||||
|
||||
trades = rpc._rpc_trade_history(0)
|
||||
assert len(trades['trades']) == 3
|
||||
assert trades['trades_count'] == 3
|
||||
# The first trade is for ETH ... sorting is descending
|
||||
assert trades['trades'][-1]['pair'] == 'ETH/BTC'
|
||||
assert trades['trades'][0]['pair'] == 'ETC/BTC'
|
||||
assert trades['trades'][1]['pair'] == 'ETC/BTC'
|
||||
assert len(trades['trades']) == 2
|
||||
assert trades['trades_count'] == 2
|
||||
# The first closed trade is for ETC ... sorting is descending
|
||||
assert trades['trades'][-1]['pair'] == 'ETC/BTC'
|
||||
assert trades['trades'][0]['pair'] == 'XRP/BTC'
|
||||
|
||||
|
||||
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
|
||||
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)
|
||||
assert stats['trade_count'] == 2
|
||||
assert stats['first_trade_date'] == 'just now'
|
||||
@ -606,7 +606,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
cancel_order=cancel_order_mock,
|
||||
get_order=MagicMock(
|
||||
fetch_order=MagicMock(
|
||||
return_value={
|
||||
'status': 'closed',
|
||||
'type': 'limit',
|
||||
@ -652,7 +652,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
||||
trade = Trade.query.filter(Trade.id == '1').first()
|
||||
filled_amount = trade.amount / 2
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.get_order',
|
||||
'freqtrade.exchange.Exchange.fetch_order',
|
||||
return_value={
|
||||
'status': 'open',
|
||||
'type': 'limit',
|
||||
@ -671,7 +671,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
||||
amount = trade.amount
|
||||
# make an limit-buy open trade, if there is no 'filled', don't sell it
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.get_order',
|
||||
'freqtrade.exchange.Exchange.fetch_order',
|
||||
return_value={
|
||||
'status': 'open',
|
||||
'type': 'limit',
|
||||
@ -688,7 +688,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
||||
freqtradebot.enter_positions()
|
||||
# make an limit-sell open trade
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.get_order',
|
||||
'freqtrade.exchange.Exchange.fetch_order',
|
||||
return_value={
|
||||
'status': 'open',
|
||||
'type': 'limit',
|
||||
|
@ -326,6 +326,8 @@ def test_api_show_config(botclient, mocker):
|
||||
assert rc.json['exchange'] == 'bittrex'
|
||||
assert rc.json['ticker_interval'] == '5m'
|
||||
assert rc.json['timeframe'] == '5m'
|
||||
assert rc.json['timeframe_ms'] == 300000
|
||||
assert rc.json['timeframe_min'] == 5
|
||||
assert rc.json['state'] == 'running'
|
||||
assert not rc.json['trailing_stop']
|
||||
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")
|
||||
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 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):
|
||||
@ -431,14 +433,14 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li
|
||||
'latest_trade_date': 'just now',
|
||||
'latest_trade_timestamp': ANY,
|
||||
'profit_all_coin': 6.217e-05,
|
||||
'profit_all_fiat': 0,
|
||||
'profit_all_fiat': 0.76748865,
|
||||
'profit_all_percent': 6.2,
|
||||
'profit_all_percent_mean': 6.2,
|
||||
'profit_all_ratio_mean': 0.06201058,
|
||||
'profit_all_percent_sum': 6.2,
|
||||
'profit_all_ratio_sum': 0.06201058,
|
||||
'profit_closed_coin': 6.217e-05,
|
||||
'profit_closed_fiat': 0,
|
||||
'profit_closed_fiat': 0.76748865,
|
||||
'profit_closed_percent': 6.2,
|
||||
'profit_closed_ratio_mean': 0.06201058,
|
||||
'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,
|
||||
'trade_count': 1,
|
||||
'closed_trade_count': 1,
|
||||
'winning_trades': 1,
|
||||
'losing_trades': 0,
|
||||
}
|
||||
|
||||
|
||||
|
@ -21,8 +21,9 @@ from freqtrade.rpc import RPCMessageType
|
||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||
from freqtrade.state import State
|
||||
from freqtrade.strategy.interface import SellType
|
||||
from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange,
|
||||
patch_get_signal, patch_whitelist)
|
||||
from tests.conftest import (create_mock_trades, get_patched_freqtradebot,
|
||||
log_has, patch_exchange, patch_get_signal,
|
||||
patch_whitelist)
|
||||
|
||||
|
||||
class DummyCls(Telegram):
|
||||
@ -60,7 +61,7 @@ def test__init__(default_conf, mocker) -> None:
|
||||
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()
|
||||
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
|
||||
|
||||
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'], "
|
||||
"['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], "
|
||||
"['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]
|
||||
assert {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'trade_id': 1,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'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]
|
||||
assert {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'trade_id': 1,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'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]
|
||||
assert {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'trade_id': 1,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'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]
|
||||
|
||||
|
||||
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:
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
|
@ -13,12 +13,14 @@ from freqtrade.exceptions import StrategyError
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.resolvers import StrategyResolver
|
||||
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
|
||||
|
||||
# Avoid to reinit the same object again and again
|
||||
_STRATEGY = DefaultStrategy(config={})
|
||||
_STRATEGY.dp = DataProvider({}, None, None)
|
||||
|
||||
|
||||
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.loc[1, 'sell'] = 1
|
||||
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=mocked_history
|
||||
)
|
||||
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True)
|
||||
mocked_history.loc[1, 'sell'] = 0
|
||||
mocked_history.loc[1, 'buy'] = 1
|
||||
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=mocked_history
|
||||
)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False)
|
||||
mocked_history.loc[1, 'sell'] = 0
|
||||
mocked_history.loc[1, 'buy'] = 0
|
||||
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=mocked_history
|
||||
)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, False)
|
||||
assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False)
|
||||
|
||||
|
||||
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'],
|
||||
[])
|
||||
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)
|
||||
def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history):
|
||||
mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history)
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=DataFrame([])
|
||||
)
|
||||
mocker.patch.object(_STRATEGY, 'assert_df')
|
||||
|
||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'],
|
||||
ohlcv_history)
|
||||
assert log_has('Empty dataframe for pair xyz', caplog)
|
||||
_STRATEGY.analyze_pair('ETH/BTC')
|
||||
|
||||
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):
|
||||
@ -99,13 +98,9 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history):
|
||||
mocked_history.loc[1, 'buy'] = 1
|
||||
|
||||
caplog.set_level(logging.INFO)
|
||||
mocker.patch.object(
|
||||
_STRATEGY, '_analyze_ticker_internal',
|
||||
return_value=mocked_history
|
||||
)
|
||||
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)
|
||||
|
||||
|
||||
@ -120,12 +115,13 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history):
|
||||
mocked_history.loc[1, 'buy'] = 1
|
||||
|
||||
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(
|
||||
_STRATEGY, 'assert_df',
|
||||
side_effect=StrategyError('Dataframe returned...')
|
||||
)
|
||||
assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'],
|
||||
ohlcv_history)
|
||||
_STRATEGY.analyze_pair('xyz')
|
||||
assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...',
|
||||
caplog)
|
||||
|
||||
@ -157,15 +153,6 @@ def test_assert_df(default_conf, mocker, ohlcv_history, caplog):
|
||||
_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:
|
||||
default_conf.update({'strategy': 'DefaultStrategy'})
|
||||
strategy = StrategyResolver.load_strategy(default_conf)
|
||||
@ -342,6 +329,7 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) ->
|
||||
|
||||
)
|
||||
strategy = DefaultStrategy({})
|
||||
strategy.dp = DataProvider({}, None, None)
|
||||
strategy.process_only_new_candles = True
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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', [
|
||||
ValueError, KeyError, Exception,
|
||||
])
|
||||
@ -419,6 +415,11 @@ def test_strategy_safe_wrapper_error(caplog, error):
|
||||
assert isinstance(ret, bool)
|
||||
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', [
|
||||
1, 22, 55, True, False, {'a': 1, 'b': '112'},
|
||||
|
@ -871,6 +871,14 @@ def test_load_config_default_exchange_name(all_conf) -> None:
|
||||
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),
|
||||
("exchange", "key", ""),
|
||||
("exchange", "secret", ""),
|
||||
|
@ -9,13 +9,12 @@ from unittest.mock import ANY, MagicMock, PropertyMock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC,
|
||||
UNLIMITED_STAKE_AMOUNT)
|
||||
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||
OperationalException, PricingError,
|
||||
TemporaryError)
|
||||
from freqtrade.exceptions import (DependencyException, ExchangeError,
|
||||
InvalidOrderException, OperationalException,
|
||||
PricingError, TemporaryError)
|
||||
from freqtrade.freqtradebot import FreqtradeBot
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc import RPCMessageType
|
||||
@ -763,7 +762,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
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,
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
@ -832,7 +831,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mock
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
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,
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
@ -859,7 +858,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order,
|
||||
'freqtrade.exchange.Exchange',
|
||||
fetch_ticker=ticker,
|
||||
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,
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
@ -912,6 +911,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None:
|
||||
refresh_latest_ohlcv=refresh_mock,
|
||||
)
|
||||
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)
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
@ -974,6 +974,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False)
|
||||
stake_amount = 2
|
||||
bid = 0.11
|
||||
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'
|
||||
|
||||
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 buy_rate_mock.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['rate'] == bid
|
||||
assert call_args['amount'] == stake_amount / bid
|
||||
buy_rate_mock.reset_mock()
|
||||
|
||||
# Should create an open trade with an open order id
|
||||
# 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
|
||||
assert freqtrade.execute_buy(pair, stake_amount, fix_price)
|
||||
# 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
|
||||
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)
|
||||
|
||||
|
||||
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:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
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.freqtradebot.FreqtradeBot.get_real_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
|
||||
|
||||
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 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
|
||||
|
||||
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()
|
||||
|
||||
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,
|
||||
'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 log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog)
|
||||
assert trade.stoploss_order_id is None
|
||||
@ -1172,18 +1214,18 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||
|
||||
mocker.patch(
|
||||
'freqtrade.exchange.Exchange.stoploss',
|
||||
side_effect=DependencyException()
|
||||
side_effect=ExchangeError()
|
||||
)
|
||||
trade.is_open = True
|
||||
freqtrade.handle_stoploss_on_exchange(trade)
|
||||
assert log_has('Unable to place a stoploss order on exchange.', caplog)
|
||||
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
|
||||
trade.stoploss_order_id = 100
|
||||
stoploss.reset_mock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order',
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order',
|
||||
side_effect=InvalidOrderException())
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||
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.is_open = False
|
||||
stoploss.reset_mock()
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order')
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order')
|
||||
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||
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']}),
|
||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||
get_fee=fee,
|
||||
get_stoploss_order=MagicMock(return_value={'status': 'canceled'}),
|
||||
stoploss=MagicMock(side_effect=DependencyException()),
|
||||
fetch_stoploss_order=MagicMock(return_value={'status': 'canceled'}),
|
||||
stoploss=MagicMock(side_effect=ExchangeError()),
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
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']}),
|
||||
sell=sell_mock,
|
||||
get_fee=fee,
|
||||
get_order=MagicMock(return_value={'status': 'canceled'}),
|
||||
fetch_order=MagicMock(return_value={'status': 'canceled'}),
|
||||
stoploss=MagicMock(side_effect=InvalidOrderException()),
|
||||
)
|
||||
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%
|
||||
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',
|
||||
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)
|
||||
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
|
||||
caplog.clear()
|
||||
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)
|
||||
assert cancel_mock.call_count == 1
|
||||
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.
|
||||
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)
|
||||
|
||||
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.freqtradebot.FreqtradeBot.get_real_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:
|
||||
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.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)
|
||||
|
||||
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.freqtradebot.FreqtradeBot.get_real_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,
|
||||
mocker):
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||
# get_order should not be called!!
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError))
|
||||
# fetch_order should not be called!!
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
|
||||
patch_exchange(mocker)
|
||||
Trade.session = MagicMock()
|
||||
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):
|
||||
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)
|
||||
# get_order should not be called!!
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError))
|
||||
# fetch_order should not be called!!
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
|
||||
patch_exchange(mocker)
|
||||
Trade.session = MagicMock()
|
||||
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,
|
||||
limit_buy_order, caplog) -> None:
|
||||
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.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:
|
||||
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))
|
||||
|
||||
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):
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||
# get_order should not be called!!
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError))
|
||||
# fetch_order should not be called!!
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
|
||||
wallet_mock = MagicMock()
|
||||
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)
|
||||
|
||||
|
||||
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,
|
||||
fee, mocker) -> None:
|
||||
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(
|
||||
'freqtrade.exchange.Exchange',
|
||||
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,
|
||||
get_fee=fee
|
||||
)
|
||||
@ -2022,7 +2076,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
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,
|
||||
get_fee=fee
|
||||
)
|
||||
@ -2052,7 +2106,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
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,
|
||||
get_fee=fee
|
||||
)
|
||||
@ -2079,7 +2133,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord
|
||||
'freqtrade.exchange.Exchange',
|
||||
validate_pairs=MagicMock(),
|
||||
fetch_ticker=ticker,
|
||||
get_order=MagicMock(side_effect=DependencyException),
|
||||
fetch_order=MagicMock(side_effect=ExchangeError),
|
||||
cancel_order=cancel_order_mock,
|
||||
get_fee=fee
|
||||
)
|
||||
@ -2105,7 +2159,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
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
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
@ -2152,7 +2206,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
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
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
@ -2183,7 +2237,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old,
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
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
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
@ -2210,7 +2264,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
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
|
||||
)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
@ -2238,7 +2292,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
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,
|
||||
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(
|
||||
'freqtrade.exchange.Exchange',
|
||||
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,
|
||||
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(
|
||||
'freqtrade.exchange.Exchange',
|
||||
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
|
||||
)
|
||||
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)
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
patch_get_signal(freqtrade)
|
||||
freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=False)
|
||||
|
||||
# Create some test data
|
||||
freqtrade.enter_positions()
|
||||
rpc_mock.reset_mock()
|
||||
|
||||
trade = Trade.query.first()
|
||||
assert trade
|
||||
assert freqtrade.strategy.confirm_trade_exit.call_count == 0
|
||||
|
||||
# Increase the price and sell it
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
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)
|
||||
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]
|
||||
assert {
|
||||
'trade_id': 1,
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'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]
|
||||
assert {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'trade_id': 1,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'loss',
|
||||
@ -2613,6 +2680,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
|
||||
|
||||
assert {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'trade_id': 1,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'loss',
|
||||
@ -2774,7 +2842,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
||||
"fee": 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)
|
||||
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]
|
||||
assert {
|
||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||
'trade_id': 1,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'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")
|
||||
def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order):
|
||||
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])
|
||||
buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy')
|
||||
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)
|
||||
create_mock_trades(fee)
|
||||
trades = Trade.query.all()
|
||||
assert len(trades) == 3
|
||||
assert len(trades) == 4
|
||||
freqtrade.cancel_all_open_orders()
|
||||
assert buy_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']
|
||||
|
@ -62,7 +62,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
get_fee=fee,
|
||||
amount_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,
|
||||
)
|
||||
|
||||
@ -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
|
||||
# Switch ordertype to market to close trade immediately
|
||||
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)
|
||||
|
||||
# Create some test data
|
||||
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()
|
||||
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)
|
||||
assert n == 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
|
||||
assert cancel_order_mock.call_count == 1
|
||||
|
@ -989,7 +989,7 @@ def test_get_overall_performance(fee):
|
||||
create_mock_trades(fee)
|
||||
res = Trade.get_overall_performance()
|
||||
|
||||
assert len(res) == 1
|
||||
assert len(res) == 2
|
||||
assert 'pair' in res[0]
|
||||
assert 'profit' in res[0]
|
||||
assert 'count' in res[0]
|
||||
@ -1004,5 +1004,5 @@ def test_get_best_pair(fee):
|
||||
create_mock_trades(fee)
|
||||
res = Trade.get_best_pair()
|
||||
assert len(res) == 2
|
||||
assert res[0] == 'ETC/BTC'
|
||||
assert res[1] == 0.005
|
||||
assert res[0] == 'XRP/BTC'
|
||||
assert res[1] == 0.01
|
||||
|
@ -21,7 +21,7 @@ from freqtrade.plot.plotting import (add_indicators, add_profit,
|
||||
load_and_plot_trades, plot_profit,
|
||||
plot_trades, store_plot_file)
|
||||
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):
|
||||
@ -316,6 +316,8 @@ def test_start_plot_dataframe(mocker):
|
||||
|
||||
|
||||
def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
|
||||
patch_exchange(mocker)
|
||||
|
||||
default_conf['trade_source'] = 'file'
|
||||
default_conf["datadir"] = testdatadir
|
||||
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
|
||||
|
Loading…
Reference in New Issue
Block a user