Merge branch 'develop' into fix/3579
This commit is contained in:
commit
9999d0ffb5
@ -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,4 +1,4 @@
|
|||||||
FROM python:3.8.4-slim-buster
|
FROM python:3.8.5-slim-buster
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install curl build-essential libssl-dev sqlite3 \
|
&& apt-get -y install curl build-essential libssl-dev sqlite3 \
|
||||||
|
@ -66,7 +66,7 @@
|
|||||||
},
|
},
|
||||||
{"method": "AgeFilter", "min_days_listed": 10},
|
{"method": "AgeFilter", "min_days_listed": 10},
|
||||||
{"method": "PrecisionFilter"},
|
{"method": "PrecisionFilter"},
|
||||||
{"method": "PriceFilter", "low_price_ratio": 0.01},
|
{"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010},
|
||||||
{"method": "SpreadFilter", "max_spread_ratio": 0.005}
|
{"method": "SpreadFilter", "max_spread_ratio": 0.005}
|
||||||
],
|
],
|
||||||
"exchange": {
|
"exchange": {
|
||||||
|
@ -662,16 +662,25 @@ Filters low-value coins which would not allow setting stoplosses.
|
|||||||
|
|
||||||
#### PriceFilter
|
#### PriceFilter
|
||||||
|
|
||||||
The `PriceFilter` allows filtering of pairs by price.
|
The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported:
|
||||||
|
* `min_price`
|
||||||
|
* `max_price`
|
||||||
|
* `low_price_ratio`
|
||||||
|
|
||||||
Currently, only `low_price_ratio` setting is implemented, where a raise of 1 price unit (pip) is below the `low_price_ratio` ratio.
|
The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs.
|
||||||
|
This option is disabled by default, and will only apply if set to <> 0.
|
||||||
|
|
||||||
|
The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs.
|
||||||
|
This option is disabled by default, and will only apply if set to <> 0.
|
||||||
|
|
||||||
|
The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio.
|
||||||
This option is disabled by default, and will only apply if set to <> 0.
|
This option is disabled by default, and will only apply if set to <> 0.
|
||||||
|
|
||||||
Calculation example:
|
Calculation example:
|
||||||
|
|
||||||
Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value.
|
Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value.
|
||||||
|
|
||||||
These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. Here is what the PriceFilters takes over.
|
These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses.
|
||||||
|
|
||||||
#### ShuffleFilter
|
#### ShuffleFilter
|
||||||
|
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
mkdocs-material==5.4.0
|
mkdocs-material==5.5.3
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
|
@ -46,7 +46,7 @@ secrets.token_hex()
|
|||||||
|
|
||||||
### Configuration with docker
|
### Configuration with docker
|
||||||
|
|
||||||
If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker.
|
If you run your bot using docker, you'll need to have the bot listen to incoming connections. The security is then handled by docker.
|
||||||
|
|
||||||
``` json
|
``` json
|
||||||
"api_server": {
|
"api_server": {
|
||||||
@ -106,26 +106,29 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
|
|||||||
|
|
||||||
## Available commands
|
## Available commands
|
||||||
|
|
||||||
| Command | Default | Description |
|
| Command | Description |
|
||||||
|----------|---------|-------------|
|
|----------|-------------|
|
||||||
| `start` | | Starts the trader
|
| `ping` | Simple command testing the API Readiness - requires no authentication.
|
||||||
| `stop` | | Stops the trader
|
| `start` | Starts the trader
|
||||||
| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
| `stop` | Stops the trader
|
||||||
| `reload_config` | | Reloads the configuration file
|
| `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||||
| `show_config` | | Shows part of the current configuration with relevant settings to operation
|
| `reload_config` | Reloads the configuration file
|
||||||
| `status` | | Lists all open trades
|
| `trades` | List last trades.
|
||||||
| `count` | | Displays number of trades used and available
|
| `delete_trade <trade_id>` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
||||||
| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
|
| `show_config` | Shows part of the current configuration with relevant settings to operation
|
||||||
| `forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
|
| `status` | Lists all open trades
|
||||||
| `forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`).
|
| `count` | Displays number of trades used and available
|
||||||
| `forcebuy <pair> [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
|
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance
|
||||||
| `performance` | | Show performance of each finished trade grouped by pair
|
| `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
| `balance` | | Show account balance per currency
|
| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
|
||||||
| `daily <n>` | 7 | Shows profit or loss per day, over the last n days
|
| `forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
|
||||||
| `whitelist` | | Show the current whitelist
|
| `performance` | Show performance of each finished trade grouped by pair
|
||||||
| `blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist.
|
| `balance` | Show account balance per currency
|
||||||
| `edge` | | Show validated pairs by Edge if it is enabled.
|
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
||||||
| `version` | | Show version
|
| `whitelist` | Show the current whitelist
|
||||||
|
| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||||
|
| `edge` | Show validated pairs by Edge if it is enabled.
|
||||||
|
| `version` | Show version
|
||||||
|
|
||||||
Possible commands can be listed from the rest-client script using the `help` command.
|
Possible commands can be listed from the rest-client script using the `help` command.
|
||||||
|
|
||||||
|
@ -123,7 +123,7 @@ SET is_open=0,
|
|||||||
close_date='2020-06-20 03:08:45.103418',
|
close_date='2020-06-20 03:08:45.103418',
|
||||||
close_rate=0.19638016,
|
close_rate=0.19638016,
|
||||||
close_profit=0.0496,
|
close_profit=0.0496,
|
||||||
close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open)))
|
close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open))),
|
||||||
sell_reason='force_sell'
|
sell_reason='force_sell'
|
||||||
WHERE id=31;
|
WHERE id=31;
|
||||||
```
|
```
|
||||||
|
@ -84,7 +84,7 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai
|
|||||||
|
|
||||||
``` python
|
``` python
|
||||||
trailing_stop_positive_offset = 0.011
|
trailing_stop_positive_offset = 0.011
|
||||||
trailing_only_offset_is_reached = true
|
trailing_only_offset_is_reached = True
|
||||||
```
|
```
|
||||||
|
|
||||||
Simplified example:
|
Simplified example:
|
||||||
|
@ -392,9 +392,9 @@ Imagine you've developed a strategy that trades the `5m` timeframe using signals
|
|||||||
|
|
||||||
The strategy might look something like this:
|
The strategy might look something like this:
|
||||||
|
|
||||||
*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day ATR to buy and sell.*
|
*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.*
|
||||||
|
|
||||||
Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day ATR. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
|
Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least!
|
||||||
|
|
||||||
Since we can't resample our data we will have to use an informative pair; and since our whitelist will be dynamic we don't know which pair(s) to use.
|
Since we can't resample our data we will have to use an informative pair; and since our whitelist will be dynamic we don't know which pair(s) to use.
|
||||||
|
|
||||||
@ -416,12 +416,43 @@ class SampleStrategy(IStrategy):
|
|||||||
informative_pairs = [(pair, '1d') for pair in pairs]
|
informative_pairs = [(pair, '1d') for pair in pairs]
|
||||||
return informative_pairs
|
return informative_pairs
|
||||||
|
|
||||||
def populate_indicators(self, dataframe, metadata):
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
|
inf_tf = '1d'
|
||||||
# Get the informative pair
|
# Get the informative pair
|
||||||
informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d')
|
informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d')
|
||||||
# Get the 14 day ATR.
|
# Get the 14 day rsi
|
||||||
atr = ta.ATR(informative, timeperiod=14)
|
informative['rsi'] = ta.RSI(informative, timeperiod=14)
|
||||||
|
|
||||||
|
# Rename columns to be unique
|
||||||
|
informative.columns = [f"{col}_{inf_tf}" for col in informative.columns]
|
||||||
|
# Assuming inf_tf = '1d' - then the columns will now be:
|
||||||
|
# date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d
|
||||||
|
|
||||||
|
# Combine the 2 dataframes
|
||||||
|
# all indicators on the informative sample MUST be calculated before this point
|
||||||
|
dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_{inf_tf}', how='left')
|
||||||
|
# FFill to have the 1d value available in every row throughout the day.
|
||||||
|
# Without this, comparisons would only work once per day.
|
||||||
|
dataframe = dataframe.ffill()
|
||||||
|
# Calculate rsi of the original dataframe (5m timeframe)
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
|
||||||
# Do other stuff
|
# Do other stuff
|
||||||
|
# ...
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
|
||||||
|
(dataframe['rsi_1d'] < 30) & # Ensure daily RSI is < 30
|
||||||
|
(dataframe['volume'] > 0) # Ensure this candle had volume (important for backtesting)
|
||||||
|
),
|
||||||
|
'buy'] = 1
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### *get_pair_dataframe(pair, timeframe)*
|
#### *get_pair_dataframe(pair, timeframe)*
|
||||||
@ -493,6 +524,7 @@ if self.dp:
|
|||||||
data returned from the exchange and add appropriate error handling / defaults.
|
data returned from the exchange and add appropriate error handling / defaults.
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
### Additional data (Wallets)
|
### Additional data (Wallets)
|
||||||
|
|
||||||
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
|
||||||
@ -516,6 +548,7 @@ if self.wallets:
|
|||||||
- `get_total(asset)` - total available balance - sum of the 2 above
|
- `get_total(asset)` - total available balance - sum of the 2 above
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
### Additional data (Trades)
|
### Additional data (Trades)
|
||||||
|
|
||||||
A history of Trades can be retrieved in the strategy by querying the database.
|
A history of Trades can be retrieved in the strategy by querying the database.
|
||||||
|
@ -47,28 +47,30 @@ Per default, the Telegram bot shows predefined commands. Some commands
|
|||||||
are only available by sending them to the bot. The table below list the
|
are only available by sending them to the bot. The table below list the
|
||||||
official commands. You can ask at any moment for help with `/help`.
|
official commands. You can ask at any moment for help with `/help`.
|
||||||
|
|
||||||
| Command | Default | Description |
|
| Command | Description |
|
||||||
|----------|---------|-------------|
|
|----------|-------------|
|
||||||
| `/start` | | Starts the trader
|
| `/start` | Starts the trader
|
||||||
| `/stop` | | Stops the trader
|
| `/stop` | Stops the trader
|
||||||
| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
| `/stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
|
||||||
| `/reload_config` | | Reloads the configuration file
|
| `/reload_config` | Reloads the configuration file
|
||||||
| `/show_config` | | Shows part of the current configuration with relevant settings to operation
|
| `/show_config` | Shows part of the current configuration with relevant settings to operation
|
||||||
| `/status` | | Lists all open trades
|
| `/status` | Lists all open trades
|
||||||
| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
||||||
| `/count` | | Displays number of trades used and available
|
| `/trades [limit]` | List all recently closed trades in a table format.
|
||||||
| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
|
| `/delete <trade_id>` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
|
||||||
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
|
| `/count` | Displays number of trades used and available
|
||||||
| `/forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`).
|
| `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance
|
||||||
| `/forcebuy <pair> [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
|
| `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
| `/performance` | | Show performance of each finished trade grouped by pair
|
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
|
||||||
| `/balance` | | Show account balance per currency
|
| `/forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
|
||||||
| `/daily <n>` | 7 | Shows profit or loss per day, over the last n days
|
| `/performance` | Show performance of each finished trade grouped by pair
|
||||||
| `/whitelist` | | Show the current whitelist
|
| `/balance` | Show account balance per currency
|
||||||
| `/blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist.
|
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
||||||
| `/edge` | | Show validated pairs by Edge if it is enabled.
|
| `/whitelist` | Show the current whitelist
|
||||||
| `/help` | | Show help message
|
| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
|
||||||
| `/version` | | Show version
|
| `/edge` | Show validated pairs by Edge if it is enabled.
|
||||||
|
| `/help` | Show help message
|
||||||
|
| `/version` | Show version
|
||||||
|
|
||||||
## Telegram commands in action
|
## Telegram commands in action
|
||||||
|
|
||||||
@ -113,6 +115,7 @@ For each open trade, the bot will send you the following message.
|
|||||||
### /status table
|
### /status table
|
||||||
|
|
||||||
Return the status of all open trades in a table format.
|
Return the status of all open trades in a table format.
|
||||||
|
|
||||||
```
|
```
|
||||||
ID Pair Since Profit
|
ID Pair Since Profit
|
||||||
---- -------- ------- --------
|
---- -------- ------- --------
|
||||||
@ -123,6 +126,7 @@ Return the status of all open trades in a table format.
|
|||||||
### /count
|
### /count
|
||||||
|
|
||||||
Return the number of trades used and available.
|
Return the number of trades used and available.
|
||||||
|
|
||||||
```
|
```
|
||||||
current max
|
current max
|
||||||
--------- -----
|
--------- -----
|
||||||
@ -208,7 +212,7 @@ Shows the current whitelist
|
|||||||
|
|
||||||
Shows the current blacklist.
|
Shows the current blacklist.
|
||||||
If Pair is set, then this pair will be added to the pairlist.
|
If Pair is set, then this pair will be added to the pairlist.
|
||||||
Also supports multiple pairs, seperated by a space.
|
Also supports multiple pairs, separated by a space.
|
||||||
Use `/reload_config` to reset the blacklist.
|
Use `/reload_config` to reset the blacklist.
|
||||||
|
|
||||||
> Using blacklist `StaticPairList` with 2 pairs
|
> Using blacklist `StaticPairList` with 2 pairs
|
||||||
@ -216,7 +220,7 @@ Use `/reload_config` to reset the blacklist.
|
|||||||
|
|
||||||
### /edge
|
### /edge
|
||||||
|
|
||||||
Shows pairs validated by Edge along with their corresponding winrate, expectancy and stoploss values.
|
Shows pairs validated by Edge along with their corresponding win-rate, expectancy and stoploss values.
|
||||||
|
|
||||||
> **Edge only validated following pairs:**
|
> **Edge only validated following pairs:**
|
||||||
```
|
```
|
||||||
|
@ -432,9 +432,9 @@ usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[--max-trades INT] [--min-avg-time FLOAT]
|
[--max-trades INT] [--min-avg-time FLOAT]
|
||||||
[--max-avg-time FLOAT] [--min-avg-profit FLOAT]
|
[--max-avg-time FLOAT] [--min-avg-profit FLOAT]
|
||||||
[--max-avg-profit FLOAT]
|
[--max-avg-profit FLOAT]
|
||||||
[--min-total-profit FLOAT]
|
[--min-total-profit FLOAT] [--max-total-profit FLOAT]
|
||||||
[--max-total-profit FLOAT] [--no-color]
|
[--min-objective FLOAT] [--max-objective FLOAT]
|
||||||
[--print-json] [--no-details]
|
[--no-color] [--print-json] [--no-details]
|
||||||
[--export-csv FILE]
|
[--export-csv FILE]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
@ -453,6 +453,10 @@ optional arguments:
|
|||||||
Select epochs on above total profit.
|
Select epochs on above total profit.
|
||||||
--max-total-profit FLOAT
|
--max-total-profit FLOAT
|
||||||
Select epochs on below total profit.
|
Select epochs on below total profit.
|
||||||
|
--min-objective FLOAT
|
||||||
|
Select epochs on above objective (- is added by default).
|
||||||
|
--max-objective FLOAT
|
||||||
|
Select epochs on below objective (- is added by default).
|
||||||
--no-color Disable colorization of hyperopt results. May be
|
--no-color Disable colorization of hyperopt results. May be
|
||||||
useful if you are redirecting output to a file.
|
useful if you are redirecting output to a file.
|
||||||
--print-json Print best result detailization in JSON format.
|
--print-json Print best result detailization in JSON format.
|
||||||
|
@ -47,6 +47,7 @@ Different payloads can be configured for different events. Not all fields are ne
|
|||||||
The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format.
|
The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
|
* `trade_id`
|
||||||
* `exchange`
|
* `exchange`
|
||||||
* `pair`
|
* `pair`
|
||||||
* `limit`
|
* `limit`
|
||||||
@ -63,6 +64,7 @@ Possible parameters are:
|
|||||||
The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format.
|
The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
|
* `trade_id`
|
||||||
* `exchange`
|
* `exchange`
|
||||||
* `pair`
|
* `pair`
|
||||||
* `limit`
|
* `limit`
|
||||||
@ -79,6 +81,7 @@ Possible parameters are:
|
|||||||
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format.
|
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
|
* `trade_id`
|
||||||
* `exchange`
|
* `exchange`
|
||||||
* `pair`
|
* `pair`
|
||||||
* `gain`
|
* `gain`
|
||||||
@ -100,6 +103,7 @@ Possible parameters are:
|
|||||||
The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format.
|
The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format.
|
||||||
Possible parameters are:
|
Possible parameters are:
|
||||||
|
|
||||||
|
* `trade_id`
|
||||||
* `exchange`
|
* `exchange`
|
||||||
* `pair`
|
* `pair`
|
||||||
* `gain`
|
* `gain`
|
||||||
|
@ -73,6 +73,7 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable",
|
|||||||
"hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time",
|
"hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time",
|
||||||
"hyperopt_list_min_avg_profit", "hyperopt_list_max_avg_profit",
|
"hyperopt_list_min_avg_profit", "hyperopt_list_max_avg_profit",
|
||||||
"hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit",
|
"hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit",
|
||||||
|
"hyperopt_list_min_objective", "hyperopt_list_max_objective",
|
||||||
"print_colorized", "print_json", "hyperopt_list_no_details",
|
"print_colorized", "print_json", "hyperopt_list_no_details",
|
||||||
"export_csv"]
|
"export_csv"]
|
||||||
|
|
||||||
|
@ -455,37 +455,49 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
),
|
),
|
||||||
"hyperopt_list_min_avg_time": Arg(
|
"hyperopt_list_min_avg_time": Arg(
|
||||||
'--min-avg-time',
|
'--min-avg-time',
|
||||||
help='Select epochs on above average time.',
|
help='Select epochs above average time.',
|
||||||
type=float,
|
type=float,
|
||||||
metavar='FLOAT',
|
metavar='FLOAT',
|
||||||
),
|
),
|
||||||
"hyperopt_list_max_avg_time": Arg(
|
"hyperopt_list_max_avg_time": Arg(
|
||||||
'--max-avg-time',
|
'--max-avg-time',
|
||||||
help='Select epochs on under average time.',
|
help='Select epochs below average time.',
|
||||||
type=float,
|
type=float,
|
||||||
metavar='FLOAT',
|
metavar='FLOAT',
|
||||||
),
|
),
|
||||||
"hyperopt_list_min_avg_profit": Arg(
|
"hyperopt_list_min_avg_profit": Arg(
|
||||||
'--min-avg-profit',
|
'--min-avg-profit',
|
||||||
help='Select epochs on above average profit.',
|
help='Select epochs above average profit.',
|
||||||
type=float,
|
type=float,
|
||||||
metavar='FLOAT',
|
metavar='FLOAT',
|
||||||
),
|
),
|
||||||
"hyperopt_list_max_avg_profit": Arg(
|
"hyperopt_list_max_avg_profit": Arg(
|
||||||
'--max-avg-profit',
|
'--max-avg-profit',
|
||||||
help='Select epochs on below average profit.',
|
help='Select epochs below average profit.',
|
||||||
type=float,
|
type=float,
|
||||||
metavar='FLOAT',
|
metavar='FLOAT',
|
||||||
),
|
),
|
||||||
"hyperopt_list_min_total_profit": Arg(
|
"hyperopt_list_min_total_profit": Arg(
|
||||||
'--min-total-profit',
|
'--min-total-profit',
|
||||||
help='Select epochs on above total profit.',
|
help='Select epochs above total profit.',
|
||||||
type=float,
|
type=float,
|
||||||
metavar='FLOAT',
|
metavar='FLOAT',
|
||||||
),
|
),
|
||||||
"hyperopt_list_max_total_profit": Arg(
|
"hyperopt_list_max_total_profit": Arg(
|
||||||
'--max-total-profit',
|
'--max-total-profit',
|
||||||
help='Select epochs on below total profit.',
|
help='Select epochs below total profit.',
|
||||||
|
type=float,
|
||||||
|
metavar='FLOAT',
|
||||||
|
),
|
||||||
|
"hyperopt_list_min_objective": Arg(
|
||||||
|
'--min-objective',
|
||||||
|
help='Select epochs above objective.',
|
||||||
|
type=float,
|
||||||
|
metavar='FLOAT',
|
||||||
|
),
|
||||||
|
"hyperopt_list_max_objective": Arg(
|
||||||
|
'--max-objective',
|
||||||
|
help='Select epochs below objective.',
|
||||||
type=float,
|
type=float,
|
||||||
metavar='FLOAT',
|
metavar='FLOAT',
|
||||||
),
|
),
|
||||||
|
@ -35,7 +35,9 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
|||||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None)
|
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
||||||
|
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
||||||
|
'filter_max_objective': config.get('hyperopt_list_max_objective', None),
|
||||||
}
|
}
|
||||||
|
|
||||||
results_file = (config['user_data_dir'] /
|
results_file = (config['user_data_dir'] /
|
||||||
@ -45,7 +47,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
|||||||
epochs = Hyperopt.load_previous_results(results_file)
|
epochs = Hyperopt.load_previous_results(results_file)
|
||||||
total_epochs = len(epochs)
|
total_epochs = len(epochs)
|
||||||
|
|
||||||
epochs = _hyperopt_filter_epochs(epochs, filteroptions)
|
epochs = hyperopt_filter_epochs(epochs, filteroptions)
|
||||||
|
|
||||||
if print_colorized:
|
if print_colorized:
|
||||||
colorama_init(autoreset=True)
|
colorama_init(autoreset=True)
|
||||||
@ -92,14 +94,16 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
|||||||
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None),
|
||||||
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None),
|
||||||
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None),
|
||||||
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None)
|
'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None),
|
||||||
|
'filter_min_objective': config.get('hyperopt_list_min_objective', None),
|
||||||
|
'filter_max_objective': config.get('hyperopt_list_max_objective', None)
|
||||||
}
|
}
|
||||||
|
|
||||||
# Previous evaluations
|
# Previous evaluations
|
||||||
epochs = Hyperopt.load_previous_results(results_file)
|
epochs = Hyperopt.load_previous_results(results_file)
|
||||||
total_epochs = len(epochs)
|
total_epochs = len(epochs)
|
||||||
|
|
||||||
epochs = _hyperopt_filter_epochs(epochs, filteroptions)
|
epochs = hyperopt_filter_epochs(epochs, filteroptions)
|
||||||
filtered_epochs = len(epochs)
|
filtered_epochs = len(epochs)
|
||||||
|
|
||||||
if n > filtered_epochs:
|
if n > filtered_epochs:
|
||||||
@ -119,7 +123,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
|
|||||||
header_str="Epoch details")
|
header_str="Epoch details")
|
||||||
|
|
||||||
|
|
||||||
def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
||||||
"""
|
"""
|
||||||
Filter our items from the list of hyperopt results
|
Filter our items from the list of hyperopt results
|
||||||
"""
|
"""
|
||||||
@ -127,6 +131,24 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
|||||||
epochs = [x for x in epochs if x['is_best']]
|
epochs = [x for x in epochs if x['is_best']]
|
||||||
if filteroptions['only_profitable']:
|
if filteroptions['only_profitable']:
|
||||||
epochs = [x for x in epochs if x['results_metrics']['profit'] > 0]
|
epochs = [x for x in epochs if x['results_metrics']['profit'] > 0]
|
||||||
|
|
||||||
|
epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
|
||||||
|
|
||||||
|
epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
|
||||||
|
|
||||||
|
epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
|
||||||
|
|
||||||
|
epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
|
||||||
|
|
||||||
|
logger.info(f"{len(epochs)} " +
|
||||||
|
("best " if filteroptions['only_best'] else "") +
|
||||||
|
("profitable " if filteroptions['only_profitable'] else "") +
|
||||||
|
"epochs found.")
|
||||||
|
return epochs
|
||||||
|
|
||||||
|
|
||||||
|
def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
|
||||||
|
|
||||||
if filteroptions['filter_min_trades'] > 0:
|
if filteroptions['filter_min_trades'] > 0:
|
||||||
epochs = [
|
epochs = [
|
||||||
x for x in epochs
|
x for x in epochs
|
||||||
@ -137,6 +159,11 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
|||||||
x for x in epochs
|
x for x in epochs
|
||||||
if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades']
|
if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades']
|
||||||
]
|
]
|
||||||
|
return epochs
|
||||||
|
|
||||||
|
|
||||||
|
def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
|
||||||
|
|
||||||
if filteroptions['filter_min_avg_time'] is not None:
|
if filteroptions['filter_min_avg_time'] is not None:
|
||||||
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
|
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
|
||||||
epochs = [
|
epochs = [
|
||||||
@ -149,6 +176,12 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
|||||||
x for x in epochs
|
x for x in epochs
|
||||||
if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time']
|
if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time']
|
||||||
]
|
]
|
||||||
|
|
||||||
|
return epochs
|
||||||
|
|
||||||
|
|
||||||
|
def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
|
||||||
|
|
||||||
if filteroptions['filter_min_avg_profit'] is not None:
|
if filteroptions['filter_min_avg_profit'] is not None:
|
||||||
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
|
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
|
||||||
epochs = [
|
epochs = [
|
||||||
@ -173,10 +206,18 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
|
|||||||
x for x in epochs
|
x for x in epochs
|
||||||
if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit']
|
if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit']
|
||||||
]
|
]
|
||||||
|
return epochs
|
||||||
|
|
||||||
logger.info(f"{len(epochs)} " +
|
|
||||||
("best " if filteroptions['only_best'] else "") +
|
def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
|
||||||
("profitable " if filteroptions['only_profitable'] else "") +
|
|
||||||
"epochs found.")
|
if filteroptions['filter_min_objective'] is not None:
|
||||||
|
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
|
||||||
|
|
||||||
|
epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
|
||||||
|
if filteroptions['filter_max_objective'] is not None:
|
||||||
|
epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
|
||||||
|
|
||||||
|
epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
|
||||||
|
|
||||||
return epochs
|
return epochs
|
||||||
|
@ -334,6 +334,12 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='hyperopt_list_max_total_profit',
|
self._args_to_config(config, argname='hyperopt_list_max_total_profit',
|
||||||
logstring='Parameter --max-total-profit detected: {}')
|
logstring='Parameter --max-total-profit detected: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='hyperopt_list_min_objective',
|
||||||
|
logstring='Parameter --min-objective detected: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='hyperopt_list_max_objective',
|
||||||
|
logstring='Parameter --max-objective detected: {}')
|
||||||
|
|
||||||
self._args_to_config(config, argname='hyperopt_list_no_details',
|
self._args_to_config(config, argname='hyperopt_list_no_details',
|
||||||
logstring='Parameter --no-details detected: {}')
|
logstring='Parameter --no-details detected: {}')
|
||||||
|
|
||||||
|
@ -156,7 +156,9 @@ CONF_SCHEMA = {
|
|||||||
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
|
||||||
'stoploss_on_exchange': {'type': 'boolean'},
|
'stoploss_on_exchange': {'type': 'boolean'},
|
||||||
'stoploss_on_exchange_interval': {'type': 'number'}
|
'stoploss_on_exchange_interval': {'type': 'number'},
|
||||||
|
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
|
||||||
|
'maximum': 1.0}
|
||||||
},
|
},
|
||||||
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||||
},
|
},
|
||||||
|
@ -281,8 +281,8 @@ class Edge:
|
|||||||
#
|
#
|
||||||
# Removing Pumps
|
# Removing Pumps
|
||||||
if self.edge_config.get('remove_pumps', False):
|
if self.edge_config.get('remove_pumps', False):
|
||||||
results = results.groupby(['pair', 'stoploss']).apply(
|
results = results[results['profit_abs'] < 2 * results['profit_abs'].std()
|
||||||
lambda x: x[x['profit_abs'] < 2 * x['profit_abs'].std() + x['profit_abs'].mean()])
|
+ results['profit_abs'].mean()]
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
# Removing trades having a duration more than X minutes (set in config)
|
# Removing trades having a duration more than X minutes (set in config)
|
||||||
|
@ -81,7 +81,7 @@ class Binance(Exchange):
|
|||||||
return order
|
return order
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise ExchangeError(
|
raise ExchangeError(
|
||||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||||
f'Tried to sell amount {amount} at rate {rate}. '
|
f'Tried to sell amount {amount} at rate {rate}. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
|
@ -107,12 +107,12 @@ def retrier_async(f):
|
|||||||
except TemporaryError as ex:
|
except TemporaryError as ex:
|
||||||
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
|
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||||
count -= 1
|
count -= 1
|
||||||
kwargs.update({'count': count})
|
kwargs.update({'count': count})
|
||||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
|
||||||
if isinstance(ex, DDosProtection):
|
if isinstance(ex, DDosProtection):
|
||||||
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
|
backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
|
||||||
logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||||
await asyncio.sleep(backoff_delay)
|
await asyncio.sleep(backoff_delay)
|
||||||
return await wrapper(*args, **kwargs)
|
return await wrapper(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
@ -131,13 +131,13 @@ def retrier(_func=None, retries=API_RETRY_COUNT):
|
|||||||
except (TemporaryError, RetryableOrderError) as ex:
|
except (TemporaryError, RetryableOrderError) as ex:
|
||||||
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
|
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||||
count -= 1
|
count -= 1
|
||||||
kwargs.update({'count': count})
|
kwargs.update({'count': count})
|
||||||
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
|
||||||
if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError):
|
if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError):
|
||||||
# increasing backoff
|
# increasing backoff
|
||||||
backoff_delay = calculate_backoff(count + 1, retries)
|
backoff_delay = calculate_backoff(count + 1, retries)
|
||||||
logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
|
||||||
time.sleep(backoff_delay)
|
time.sleep(backoff_delay)
|
||||||
return wrapper(*args, **kwargs)
|
return wrapper(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
|
@ -187,6 +187,11 @@ class Exchange:
|
|||||||
def timeframes(self) -> List[str]:
|
def timeframes(self) -> List[str]:
|
||||||
return list((self._api.timeframes or {}).keys())
|
return list((self._api.timeframes or {}).keys())
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ohlcv_candle_limit(self) -> int:
|
||||||
|
"""exchange ohlcv candle limit"""
|
||||||
|
return int(self._ohlcv_candle_limit)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def markets(self) -> Dict:
|
def markets(self) -> Dict:
|
||||||
"""exchange ccxt markets"""
|
"""exchange ccxt markets"""
|
||||||
@ -253,8 +258,8 @@ class Exchange:
|
|||||||
api.urls['api'] = api.urls['test']
|
api.urls['api'] = api.urls['test']
|
||||||
logger.info("Enabled Sandbox API on %s", name)
|
logger.info("Enabled Sandbox API on %s", name)
|
||||||
else:
|
else:
|
||||||
logger.warning(name, "No Sandbox URL in CCXT, exiting. "
|
logger.warning(
|
||||||
"Please check your config.json")
|
f"No Sandbox URL in CCXT for {name}, exiting. Please check your config.json")
|
||||||
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
||||||
|
|
||||||
def _load_async_markets(self, reload: bool = False) -> None:
|
def _load_async_markets(self, reload: bool = False) -> None:
|
||||||
@ -521,13 +526,13 @@ class Exchange:
|
|||||||
|
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise ExchangeError(
|
raise ExchangeError(
|
||||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}.'
|
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
f'Tried to {side} amount {amount} at rate {rate}.'
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise ExchangeError(
|
raise ExchangeError(
|
||||||
f'Could not create {ordertype} {side} order on market {pair}.'
|
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to {side} amount {amount} at rate {rate}.'
|
f'Tried to {side} amount {amount} at rate {rate}. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
@ -995,7 +1000,7 @@ class Exchange:
|
|||||||
if self.is_cancel_order_result_suitable(corder):
|
if self.is_cancel_order_result_suitable(corder):
|
||||||
return corder
|
return corder
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.warning(f"Could not cancel order {order_id}.")
|
logger.warning(f"Could not cancel order {order_id} for {pair}.")
|
||||||
try:
|
try:
|
||||||
order = self.fetch_order(order_id, pair)
|
order = self.fetch_order(order_id, pair)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
@ -1004,7 +1009,7 @@ class Exchange:
|
|||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
@retrier
|
@retrier(retries=5)
|
||||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
try:
|
try:
|
||||||
@ -1018,10 +1023,10 @@ class Exchange:
|
|||||||
return self._api.fetch_order(order_id, pair)
|
return self._api.fetch_order(order_id, pair)
|
||||||
except ccxt.OrderNotFound as e:
|
except ccxt.OrderNotFound as e:
|
||||||
raise RetryableOrderError(
|
raise RetryableOrderError(
|
||||||
f'Order not found (id: {order_id}). Message: {e}') from e
|
f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise InvalidOrderException(
|
raise InvalidOrderException(
|
||||||
f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e
|
f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
@ -78,7 +78,7 @@ class Ftx(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@retrier(retries=5)
|
||||||
def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
try:
|
try:
|
||||||
|
@ -89,7 +89,7 @@ class Kraken(Exchange):
|
|||||||
return order
|
return order
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise ExchangeError(
|
raise ExchangeError(
|
||||||
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
|
@ -600,6 +600,7 @@ class FreqtradeBot:
|
|||||||
Sends rpc notification when a buy occured.
|
Sends rpc notification when a buy occured.
|
||||||
"""
|
"""
|
||||||
msg = {
|
msg = {
|
||||||
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
@ -623,6 +624,7 @@ class FreqtradeBot:
|
|||||||
current_rate = self.get_buy_rate(trade.pair, False)
|
current_rate = self.get_buy_rate(trade.pair, False)
|
||||||
|
|
||||||
msg = {
|
msg = {
|
||||||
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
|
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
@ -660,7 +662,7 @@ class FreqtradeBot:
|
|||||||
trades_closed += 1
|
trades_closed += 1
|
||||||
|
|
||||||
except DependencyException as exception:
|
except DependencyException as exception:
|
||||||
logger.warning('Unable to sell trade: %s', exception)
|
logger.warning('Unable to sell trade %s: %s', trade.pair, exception)
|
||||||
|
|
||||||
# Updating wallets if any trade occured
|
# Updating wallets if any trade occured
|
||||||
if trades_closed:
|
if trades_closed:
|
||||||
@ -827,10 +829,8 @@ class FreqtradeBot:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||||
if (not stoploss_order):
|
if not stoploss_order:
|
||||||
|
|
||||||
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
||||||
|
|
||||||
stop_price = trade.open_rate * (1 + stoploss)
|
stop_price = trade.open_rate * (1 + stoploss)
|
||||||
|
|
||||||
if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price):
|
if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price):
|
||||||
@ -978,6 +978,12 @@ class FreqtradeBot:
|
|||||||
reason = constants.CANCEL_REASON['TIMEOUT']
|
reason = constants.CANCEL_REASON['TIMEOUT']
|
||||||
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
|
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
|
||||||
trade.amount)
|
trade.amount)
|
||||||
|
# Avoid race condition where the order could not be cancelled coz its already filled.
|
||||||
|
# Simply bailing here is the only safe way - as this order will then be
|
||||||
|
# handled in the next iteration.
|
||||||
|
if corder.get('status') not in ('canceled', 'closed'):
|
||||||
|
logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.")
|
||||||
|
return False
|
||||||
else:
|
else:
|
||||||
# Order was cancelled already, so we can reuse the existing dict
|
# Order was cancelled already, so we can reuse the existing dict
|
||||||
corder = order
|
corder = order
|
||||||
@ -1153,6 +1159,7 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'trade_id': trade.id,
|
||||||
'exchange': trade.exchange.capitalize(),
|
'exchange': trade.exchange.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'gain': gain,
|
'gain': gain,
|
||||||
@ -1195,6 +1202,7 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
msg = {
|
msg = {
|
||||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
||||||
|
'trade_id': trade.id,
|
||||||
'exchange': trade.exchange.capitalize(),
|
'exchange': trade.exchange.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
'gain': gain,
|
'gain': gain,
|
||||||
|
@ -101,7 +101,7 @@ class Backtesting:
|
|||||||
if len(self.pairlists.whitelist) == 0:
|
if len(self.pairlists.whitelist) == 0:
|
||||||
raise OperationalException("No pair in whitelist.")
|
raise OperationalException("No pair in whitelist.")
|
||||||
|
|
||||||
if config.get('fee'):
|
if config.get('fee', None) is not None:
|
||||||
self.fee = config['fee']
|
self.fee = config['fee']
|
||||||
else:
|
else:
|
||||||
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0])
|
||||||
|
@ -5,6 +5,7 @@ import logging
|
|||||||
import arrow
|
import arrow
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import plural
|
from freqtrade.misc import plural
|
||||||
from freqtrade.pairlist.IPairList import IPairList
|
from freqtrade.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
@ -23,6 +24,13 @@ class AgeFilter(IPairList):
|
|||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
self._min_days_listed = pairlistconfig.get('min_days_listed', 10)
|
||||||
|
|
||||||
|
if self._min_days_listed < 1:
|
||||||
|
raise OperationalException("AgeFilter requires min_days_listed must be >= 1")
|
||||||
|
if self._min_days_listed > exchange.ohlcv_candle_limit:
|
||||||
|
raise OperationalException("AgeFilter requires min_days_listed must not exceed "
|
||||||
|
"exchange max request size "
|
||||||
|
f"({exchange.ohlcv_candle_limit})")
|
||||||
self._enabled = self._min_days_listed >= 1
|
self._enabled = self._min_days_listed >= 1
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -18,7 +18,11 @@ class PriceFilter(IPairList):
|
|||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0)
|
self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0)
|
||||||
self._enabled = self._low_price_ratio != 0
|
self._min_price = pairlistconfig.get('min_price', 0)
|
||||||
|
self._max_price = pairlistconfig.get('max_price', 0)
|
||||||
|
self._enabled = ((self._low_price_ratio != 0) or
|
||||||
|
(self._min_price != 0) or
|
||||||
|
(self._max_price != 0))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
@ -33,7 +37,18 @@ class PriceFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
Short whitelist method description - used for startup-messages
|
Short whitelist method description - used for startup-messages
|
||||||
"""
|
"""
|
||||||
return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%."
|
active_price_filters = []
|
||||||
|
if self._low_price_ratio != 0:
|
||||||
|
active_price_filters.append(f"below {self._low_price_ratio * 100}%")
|
||||||
|
if self._min_price != 0:
|
||||||
|
active_price_filters.append(f"below {self._min_price:.8f}")
|
||||||
|
if self._max_price != 0:
|
||||||
|
active_price_filters.append(f"above {self._max_price:.8f}")
|
||||||
|
|
||||||
|
if len(active_price_filters):
|
||||||
|
return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}."
|
||||||
|
|
||||||
|
return f"{self.name} - No price filters configured."
|
||||||
|
|
||||||
def _validate_pair(self, ticker) -> bool:
|
def _validate_pair(self, ticker) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -41,15 +56,33 @@ class PriceFilter(IPairList):
|
|||||||
:param ticker: ticker dict as returned from ccxt.load_markets()
|
:param ticker: ticker dict as returned from ccxt.load_markets()
|
||||||
:return: True if the pair can stay, false if it should be removed
|
:return: True if the pair can stay, false if it should be removed
|
||||||
"""
|
"""
|
||||||
if ticker['last'] is None:
|
if ticker['last'] is None or ticker['last'] == 0:
|
||||||
self.log_on_refresh(logger.info,
|
self.log_on_refresh(logger.info,
|
||||||
f"Removed {ticker['symbol']} from whitelist, because "
|
f"Removed {ticker['symbol']} from whitelist, because "
|
||||||
"ticker['last'] is empty (Usually no trade in the last 24h).")
|
"ticker['last'] is empty (Usually no trade in the last 24h).")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Perform low_price_ratio check.
|
||||||
|
if self._low_price_ratio != 0:
|
||||||
compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last'])
|
compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last'])
|
||||||
changeperc = compare / ticker['last']
|
changeperc = compare / ticker['last']
|
||||||
if changeperc > self._low_price_ratio:
|
if changeperc > self._low_price_ratio:
|
||||||
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
|
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
|
||||||
f"because 1 unit is {changeperc * 100:.3f}%")
|
f"because 1 unit is {changeperc * 100:.3f}%")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Perform min_price check.
|
||||||
|
if self._min_price != 0:
|
||||||
|
if ticker['last'] < self._min_price:
|
||||||
|
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
|
||||||
|
f"because last price < {self._min_price:.8f}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Perform max_price check.
|
||||||
|
if self._max_price != 0:
|
||||||
|
if ticker['last'] > self._max_price:
|
||||||
|
self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, "
|
||||||
|
f"because last price > {self._max_price:.8f}")
|
||||||
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@ -10,11 +10,13 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown,
|
|||||||
create_cum_profit,
|
create_cum_profit,
|
||||||
extract_trades_of_period, load_trades)
|
extract_trades_of_period, load_trades)
|
||||||
from freqtrade.data.converter import trim_dataframe
|
from freqtrade.data.converter import trim_dataframe
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.data.history import load_data
|
from freqtrade.data.history import load_data
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_prev_date
|
from freqtrade.exchange import timeframe_to_prev_date
|
||||||
from freqtrade.misc import pair_to_filename
|
from freqtrade.misc import pair_to_filename
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
|
from freqtrade.strategy import IStrategy
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -467,6 +469,8 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
|||||||
"""
|
"""
|
||||||
strategy = StrategyResolver.load_strategy(config)
|
strategy = StrategyResolver.load_strategy(config)
|
||||||
|
|
||||||
|
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config)
|
||||||
|
IStrategy.dp = DataProvider(config, exchange)
|
||||||
plot_elements = init_plotscript(config)
|
plot_elements = init_plotscript(config)
|
||||||
trades = plot_elements['trades']
|
trades = plot_elements['trades']
|
||||||
pair_counter = 0
|
pair_counter = 0
|
||||||
|
@ -42,13 +42,13 @@ class HyperOptResolver(IResolver):
|
|||||||
extra_dir=config.get('hyperopt_path'))
|
extra_dir=config.get('hyperopt_path'))
|
||||||
|
|
||||||
if not hasattr(hyperopt, 'populate_indicators'):
|
if not hasattr(hyperopt, 'populate_indicators'):
|
||||||
logger.warning("Hyperopt class does not provide populate_indicators() method. "
|
logger.info("Hyperopt class does not provide populate_indicators() method. "
|
||||||
"Using populate_indicators from the strategy.")
|
"Using populate_indicators from the strategy.")
|
||||||
if not hasattr(hyperopt, 'populate_buy_trend'):
|
if not hasattr(hyperopt, 'populate_buy_trend'):
|
||||||
logger.warning("Hyperopt class does not provide populate_buy_trend() method. "
|
logger.info("Hyperopt class does not provide populate_buy_trend() method. "
|
||||||
"Using populate_buy_trend from the strategy.")
|
"Using populate_buy_trend from the strategy.")
|
||||||
if not hasattr(hyperopt, 'populate_sell_trend'):
|
if not hasattr(hyperopt, 'populate_sell_trend'):
|
||||||
logger.warning("Hyperopt class does not provide populate_sell_trend() method. "
|
logger.info("Hyperopt class does not provide populate_sell_trend() method. "
|
||||||
"Using populate_sell_trend from the strategy.")
|
"Using populate_sell_trend from the strategy.")
|
||||||
return hyperopt
|
return hyperopt
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ def require_login(func: Callable[[Any, Any], Any]):
|
|||||||
|
|
||||||
|
|
||||||
# Type should really be Callable[[ApiServer], Any], but that will create a circular dependency
|
# Type should really be Callable[[ApiServer], Any], but that will create a circular dependency
|
||||||
def rpc_catch_errors(func: Callable[[Any], Any]):
|
def rpc_catch_errors(func: Callable[..., Any]):
|
||||||
|
|
||||||
def func_wrapper(obj, *args, **kwargs):
|
def func_wrapper(obj, *args, **kwargs):
|
||||||
|
|
||||||
@ -200,6 +200,8 @@ class ApiServer(RPC):
|
|||||||
view_func=self._ping, methods=['GET'])
|
view_func=self._ping, methods=['GET'])
|
||||||
self.app.add_url_rule(f'{BASE_URI}/trades', 'trades',
|
self.app.add_url_rule(f'{BASE_URI}/trades', 'trades',
|
||||||
view_func=self._trades, methods=['GET'])
|
view_func=self._trades, methods=['GET'])
|
||||||
|
self.app.add_url_rule(f'{BASE_URI}/trades/<int:tradeid>', 'trades_delete',
|
||||||
|
view_func=self._trades_delete, methods=['DELETE'])
|
||||||
# Combined actions and infos
|
# Combined actions and infos
|
||||||
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
|
self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist,
|
||||||
methods=['GET', 'POST'])
|
methods=['GET', 'POST'])
|
||||||
@ -424,6 +426,19 @@ class ApiServer(RPC):
|
|||||||
results = self._rpc_trade_history(limit)
|
results = self._rpc_trade_history(limit)
|
||||||
return self.rest_dump(results)
|
return self.rest_dump(results)
|
||||||
|
|
||||||
|
@require_login
|
||||||
|
@rpc_catch_errors
|
||||||
|
def _trades_delete(self, tradeid):
|
||||||
|
"""
|
||||||
|
Handler for DELETE /trades/<tradeid> endpoint.
|
||||||
|
Removes the trade from the database (tries to cancel open orders first!)
|
||||||
|
get:
|
||||||
|
param:
|
||||||
|
tradeid: Numeric trade-id assigned to the trade.
|
||||||
|
"""
|
||||||
|
result = self._rpc_delete(tradeid)
|
||||||
|
return self.rest_dump(result)
|
||||||
|
|
||||||
@require_login
|
@require_login
|
||||||
@rpc_catch_errors
|
@rpc_catch_errors
|
||||||
def _whitelist(self):
|
def _whitelist(self):
|
||||||
|
@ -6,14 +6,14 @@ from abc import abstractmethod
|
|||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from math import isnan
|
from math import isnan
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from numpy import NAN, mean
|
from numpy import NAN, mean
|
||||||
|
|
||||||
from freqtrade.exceptions import ExchangeError, PricingError
|
from freqtrade.exceptions import (ExchangeError, InvalidOrderException,
|
||||||
|
PricingError)
|
||||||
from freqtrade.exchange import timeframe_to_msecs, timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||||
from freqtrade.misc import shorten_date
|
from freqtrade.misc import shorten_date
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
@ -252,9 +252,10 @@ class RPC:
|
|||||||
def _rpc_trade_history(self, limit: int) -> Dict:
|
def _rpc_trade_history(self, limit: int) -> Dict:
|
||||||
""" Returns the X last trades """
|
""" Returns the X last trades """
|
||||||
if limit > 0:
|
if limit > 0:
|
||||||
trades = Trade.get_trades().order_by(Trade.id.desc()).limit(limit)
|
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
|
||||||
|
Trade.id.desc()).limit(limit)
|
||||||
else:
|
else:
|
||||||
trades = Trade.get_trades().order_by(Trade.id.desc()).all()
|
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all()
|
||||||
|
|
||||||
output = [trade.to_json() for trade in trades]
|
output = [trade.to_json() for trade in trades]
|
||||||
|
|
||||||
@ -523,7 +524,7 @@ class RPC:
|
|||||||
# check if valid pair
|
# check if valid pair
|
||||||
|
|
||||||
# check if pair already has an open pair
|
# check if pair already has an open pair
|
||||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first()
|
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||||
if trade:
|
if trade:
|
||||||
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
||||||
|
|
||||||
@ -532,11 +533,51 @@ class RPC:
|
|||||||
|
|
||||||
# execute buy
|
# execute buy
|
||||||
if self._freqtrade.execute_buy(pair, stakeamount, price):
|
if self._freqtrade.execute_buy(pair, stakeamount, price):
|
||||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first()
|
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||||
return trade
|
return trade
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _rpc_delete(self, trade_id: str) -> Dict[str, Union[str, int]]:
|
||||||
|
"""
|
||||||
|
Handler for delete <id>.
|
||||||
|
Delete the given trade and close eventually existing open orders.
|
||||||
|
"""
|
||||||
|
with self._freqtrade._sell_lock:
|
||||||
|
c_count = 0
|
||||||
|
trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
|
||||||
|
if not trade:
|
||||||
|
logger.warning('delete trade: Invalid argument received')
|
||||||
|
raise RPCException('invalid argument')
|
||||||
|
|
||||||
|
# Try cancelling regular order if that exists
|
||||||
|
if trade.open_order_id:
|
||||||
|
try:
|
||||||
|
self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||||
|
c_count += 1
|
||||||
|
except (ExchangeError, InvalidOrderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# cancel stoploss on exchange ...
|
||||||
|
if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange')
|
||||||
|
and trade.stoploss_order_id):
|
||||||
|
try:
|
||||||
|
self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id,
|
||||||
|
trade.pair)
|
||||||
|
c_count += 1
|
||||||
|
except (ExchangeError, InvalidOrderException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
Trade.session.delete(trade)
|
||||||
|
Trade.session.flush()
|
||||||
|
self._freqtrade.wallets.update()
|
||||||
|
return {
|
||||||
|
'result': 'success',
|
||||||
|
'trade_id': trade_id,
|
||||||
|
'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.',
|
||||||
|
'cancel_order_count': c_count,
|
||||||
|
}
|
||||||
|
|
||||||
def _rpc_performance(self) -> List[Dict[str, Any]]:
|
def _rpc_performance(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Handler for performance.
|
Handler for performance.
|
||||||
|
@ -5,6 +5,7 @@ This module manage Telegram communication
|
|||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import arrow
|
||||||
from typing import Any, Callable, Dict
|
from typing import Any, Callable, Dict
|
||||||
|
|
||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
@ -92,6 +93,8 @@ class Telegram(RPC):
|
|||||||
CommandHandler('stop', self._stop),
|
CommandHandler('stop', self._stop),
|
||||||
CommandHandler('forcesell', self._forcesell),
|
CommandHandler('forcesell', self._forcesell),
|
||||||
CommandHandler('forcebuy', self._forcebuy),
|
CommandHandler('forcebuy', self._forcebuy),
|
||||||
|
CommandHandler('trades', self._trades),
|
||||||
|
CommandHandler('delete', self._delete_trade),
|
||||||
CommandHandler('performance', self._performance),
|
CommandHandler('performance', self._performance),
|
||||||
CommandHandler('daily', self._daily),
|
CommandHandler('daily', self._daily),
|
||||||
CommandHandler('count', self._count),
|
CommandHandler('count', self._count),
|
||||||
@ -496,6 +499,62 @@ class Telegram(RPC):
|
|||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(e))
|
self._send_msg(str(e))
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _trades(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /trades <n>
|
||||||
|
Returns last n recent trades.
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
stake_cur = self._config['stake_currency']
|
||||||
|
try:
|
||||||
|
nrecent = int(context.args[0])
|
||||||
|
except (TypeError, ValueError, IndexError):
|
||||||
|
nrecent = 10
|
||||||
|
try:
|
||||||
|
trades = self._rpc_trade_history(
|
||||||
|
nrecent
|
||||||
|
)
|
||||||
|
trades_tab = tabulate(
|
||||||
|
[[arrow.get(trade['open_date']).humanize(),
|
||||||
|
trade['pair'],
|
||||||
|
f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
|
||||||
|
for trade in trades['trades']],
|
||||||
|
headers=[
|
||||||
|
'Open Date',
|
||||||
|
'Pair',
|
||||||
|
f'Profit ({stake_cur})',
|
||||||
|
],
|
||||||
|
tablefmt='simple')
|
||||||
|
message = (f"<b>{min(trades['trades_count'], nrecent)} recent trades</b>:\n"
|
||||||
|
+ (f"<pre>{trades_tab}</pre>" if trades['trades_count'] > 0 else ''))
|
||||||
|
self._send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e))
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _delete_trade(self, update: Update, context: CallbackContext) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /delete <id>.
|
||||||
|
Delete the given trade
|
||||||
|
:param bot: telegram bot
|
||||||
|
:param update: message update
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
trade_id = context.args[0] if len(context.args) > 0 else None
|
||||||
|
try:
|
||||||
|
msg = self._rpc_delete(trade_id)
|
||||||
|
self._send_msg((
|
||||||
|
'`{result_msg}`\n'
|
||||||
|
'Please make sure to take care of this asset on the exchange manually.'
|
||||||
|
).format(**msg))
|
||||||
|
|
||||||
|
except RPCException as e:
|
||||||
|
self._send_msg(str(e))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _performance(self, update: Update, context: CallbackContext) -> None:
|
def _performance(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
@ -609,10 +668,12 @@ class Telegram(RPC):
|
|||||||
" *table :* `will display trades in a table`\n"
|
" *table :* `will display trades in a table`\n"
|
||||||
" `pending buy orders are marked with an asterisk (*)`\n"
|
" `pending buy orders are marked with an asterisk (*)`\n"
|
||||||
" `pending sell orders are marked with a double asterisk (**)`\n"
|
" `pending sell orders are marked with a double asterisk (**)`\n"
|
||||||
|
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||||
"*/profit:* `Lists cumulative profit from all finished trades`\n"
|
"*/profit:* `Lists cumulative profit from all finished trades`\n"
|
||||||
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
|
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
|
||||||
"regardless of profit`\n"
|
"regardless of profit`\n"
|
||||||
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
|
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
|
||||||
|
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
|
||||||
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
|
"*/performance:* `Show performance of each finished trade grouped by pair`\n"
|
||||||
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
|
"*/daily <n>:* `Shows profit or loss per day, over the last n days`\n"
|
||||||
"*/count:* `Show number of trades running compared to allowed number of trades`"
|
"*/count:* `Show number of trades running compared to allowed number of trades`"
|
||||||
|
@ -34,7 +34,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
|
|||||||
"""
|
"""
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
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:
|
rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
|
||||||
"""
|
"""
|
||||||
Called right before placing a regular sell order.
|
Called right before placing a regular sell order.
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
# requirements without requirements installable via conda
|
# requirements without requirements installable via conda
|
||||||
# mainly used for Raspberry pi installs
|
# mainly used for Raspberry pi installs
|
||||||
ccxt==1.30.93
|
ccxt==1.32.88
|
||||||
SQLAlchemy==1.3.18
|
SQLAlchemy==1.3.18
|
||||||
python-telegram-bot==12.8
|
python-telegram-bot==12.8
|
||||||
arrow==0.15.7
|
arrow==0.15.8
|
||||||
cachetools==4.1.1
|
cachetools==4.1.1
|
||||||
requests==2.24.0
|
requests==2.24.0
|
||||||
urllib3==1.25.9
|
urllib3==1.25.10
|
||||||
wrapt==1.12.1
|
wrapt==1.12.1
|
||||||
jsonschema==3.2.0
|
jsonschema==3.2.0
|
||||||
TA-Lib==0.4.18
|
TA-Lib==0.4.18
|
||||||
|
@ -8,7 +8,7 @@ flake8==3.8.3
|
|||||||
flake8-type-annotations==0.1.0
|
flake8-type-annotations==0.1.0
|
||||||
flake8-tidy-imports==4.1.0
|
flake8-tidy-imports==4.1.0
|
||||||
mypy==0.782
|
mypy==0.782
|
||||||
pytest==5.4.3
|
pytest==6.0.1
|
||||||
pytest-asyncio==0.14.0
|
pytest-asyncio==0.14.0
|
||||||
pytest-cov==2.10.0
|
pytest-cov==2.10.0
|
||||||
pytest-mock==3.2.0
|
pytest-mock==3.2.0
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.5.1
|
scipy==1.5.2
|
||||||
scikit-learn==0.23.1
|
scikit-learn==0.23.1
|
||||||
scikit-optimize==0.7.4
|
scikit-optimize==0.7.4
|
||||||
filelock==3.0.12
|
filelock==3.0.12
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==4.8.2
|
plotly==4.9.0
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Load common requirements
|
# Load common requirements
|
||||||
-r requirements-common.txt
|
-r requirements-common.txt
|
||||||
|
|
||||||
numpy==1.19.0
|
numpy==1.19.1
|
||||||
pandas==1.0.5
|
pandas==1.1.0
|
||||||
|
@ -62,6 +62,9 @@ class FtRestClient():
|
|||||||
def _get(self, apipath, params: dict = None):
|
def _get(self, apipath, params: dict = None):
|
||||||
return self._call("GET", apipath, params=params)
|
return self._call("GET", apipath, params=params)
|
||||||
|
|
||||||
|
def _delete(self, apipath, params: dict = None):
|
||||||
|
return self._call("DELETE", apipath, params=params)
|
||||||
|
|
||||||
def _post(self, apipath, params: dict = None, data: dict = None):
|
def _post(self, apipath, params: dict = None, data: dict = None):
|
||||||
return self._call("POST", apipath, params=params, data=data)
|
return self._call("POST", apipath, params=params, data=data)
|
||||||
|
|
||||||
@ -164,6 +167,15 @@ class FtRestClient():
|
|||||||
"""
|
"""
|
||||||
return self._get("trades", params={"limit": limit} if limit else 0)
|
return self._get("trades", params={"limit": limit} if limit else 0)
|
||||||
|
|
||||||
|
def delete_trade(self, trade_id):
|
||||||
|
"""Delete trade from the database.
|
||||||
|
Tries to close open orders. Requires manual handling of this asset on the exchange.
|
||||||
|
|
||||||
|
:param trade_id: Deletes the trade with this ID from the database.
|
||||||
|
:return: json object
|
||||||
|
"""
|
||||||
|
return self._delete("trades/{}".format(trade_id))
|
||||||
|
|
||||||
def whitelist(self):
|
def whitelist(self):
|
||||||
"""Show the current whitelist.
|
"""Show the current whitelist.
|
||||||
|
|
||||||
|
@ -736,7 +736,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
|
|
||||||
args = [
|
args = [
|
||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--no-details"
|
"--no-details",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -749,7 +749,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
args = [
|
args = [
|
||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--best",
|
"--best",
|
||||||
"--no-details"
|
"--no-details",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -763,7 +763,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
args = [
|
args = [
|
||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--profitable",
|
"--profitable",
|
||||||
"--no-details"
|
"--no-details",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -776,7 +776,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
" 11/12", " 12/12"])
|
" 11/12", " 12/12"])
|
||||||
args = [
|
args = [
|
||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--profitable"
|
"--profitable",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -792,7 +792,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--no-details",
|
"--no-details",
|
||||||
"--no-color",
|
"--no-color",
|
||||||
"--min-trades", "20"
|
"--min-trades", "20",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -806,7 +806,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--profitable",
|
"--profitable",
|
||||||
"--no-details",
|
"--no-details",
|
||||||
"--max-trades", "20"
|
"--max-trades", "20",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -821,7 +821,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--profitable",
|
"--profitable",
|
||||||
"--no-details",
|
"--no-details",
|
||||||
"--min-avg-profit", "0.11"
|
"--min-avg-profit", "0.11",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -835,7 +835,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
args = [
|
args = [
|
||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--no-details",
|
"--no-details",
|
||||||
"--max-avg-profit", "0.10"
|
"--max-avg-profit", "0.10",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -849,7 +849,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
args = [
|
args = [
|
||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--no-details",
|
"--no-details",
|
||||||
"--min-total-profit", "0.4"
|
"--min-total-profit", "0.4",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -863,7 +863,35 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
args = [
|
args = [
|
||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--no-details",
|
"--no-details",
|
||||||
"--max-total-profit", "0.4"
|
"--max-total-profit", "0.4",
|
||||||
|
]
|
||||||
|
pargs = get_args(args)
|
||||||
|
pargs['config'] = None
|
||||||
|
start_hyperopt_list(pargs)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert all(x in captured.out
|
||||||
|
for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12",
|
||||||
|
" 9/12", " 11/12"])
|
||||||
|
assert all(x not in captured.out
|
||||||
|
for x in [" 4/12", " 10/12", " 12/12"])
|
||||||
|
args = [
|
||||||
|
"hyperopt-list",
|
||||||
|
"--no-details",
|
||||||
|
"--min-objective", "0.1",
|
||||||
|
]
|
||||||
|
pargs = get_args(args)
|
||||||
|
pargs['config'] = None
|
||||||
|
start_hyperopt_list(pargs)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert all(x in captured.out
|
||||||
|
for x in [" 10/12"])
|
||||||
|
assert all(x not in captured.out
|
||||||
|
for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12",
|
||||||
|
" 9/12", " 11/12", " 12/12"])
|
||||||
|
args = [
|
||||||
|
"hyperopt-list",
|
||||||
|
"--no-details",
|
||||||
|
"--max-objective", "0.1",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -878,7 +906,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--profitable",
|
"--profitable",
|
||||||
"--no-details",
|
"--no-details",
|
||||||
"--min-avg-time", "2000"
|
"--min-avg-time", "2000",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -892,7 +920,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
args = [
|
args = [
|
||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--no-details",
|
"--no-details",
|
||||||
"--max-avg-time", "1500"
|
"--max-avg-time", "1500",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -906,7 +934,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
|
|||||||
args = [
|
args = [
|
||||||
"hyperopt-list",
|
"hyperopt-list",
|
||||||
"--no-details",
|
"--no-details",
|
||||||
"--export-csv", "test_file.csv"
|
"--export-csv", "test_file.csv",
|
||||||
]
|
]
|
||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
@ -1089,7 +1117,7 @@ def test_show_trades(mocker, fee, capsys, caplog):
|
|||||||
pargs = get_args(args)
|
pargs = get_args(args)
|
||||||
pargs['config'] = None
|
pargs['config'] = None
|
||||||
start_show_trades(pargs)
|
start_show_trades(pargs)
|
||||||
assert log_has("Printing 3 Trades: ", caplog)
|
assert log_has("Printing 4 Trades: ", caplog)
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
assert "Trade(id=1" in captured.out
|
assert "Trade(id=1" in captured.out
|
||||||
assert "Trade(id=2" in captured.out
|
assert "Trade(id=2" in captured.out
|
||||||
|
@ -201,6 +201,20 @@ def create_mock_trades(fee):
|
|||||||
)
|
)
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
|
|
||||||
|
trade = Trade(
|
||||||
|
pair='XRP/BTC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
amount=123.0,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
open_rate=0.05,
|
||||||
|
close_rate=0.06,
|
||||||
|
close_profit=0.01,
|
||||||
|
exchange='bittrex',
|
||||||
|
is_open=False,
|
||||||
|
)
|
||||||
|
Trade.session.add(trade)
|
||||||
|
|
||||||
# Simulate prod entry
|
# Simulate prod entry
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='ETC/BTC',
|
pair='ETC/BTC',
|
||||||
@ -664,7 +678,8 @@ def shitcoinmarkets(markets):
|
|||||||
Fixture with shitcoin markets - used to test filters in pairlists
|
Fixture with shitcoin markets - used to test filters in pairlists
|
||||||
"""
|
"""
|
||||||
shitmarkets = deepcopy(markets)
|
shitmarkets = deepcopy(markets)
|
||||||
shitmarkets.update({'HOT/BTC': {
|
shitmarkets.update({
|
||||||
|
'HOT/BTC': {
|
||||||
'id': 'HOTBTC',
|
'id': 'HOTBTC',
|
||||||
'symbol': 'HOT/BTC',
|
'symbol': 'HOT/BTC',
|
||||||
'base': 'HOT',
|
'base': 'HOT',
|
||||||
@ -770,6 +785,31 @@ def shitcoinmarkets(markets):
|
|||||||
"future": False,
|
"future": False,
|
||||||
"active": True
|
"active": True
|
||||||
},
|
},
|
||||||
|
'ADADOUBLE/USDT': {
|
||||||
|
"percentage": True,
|
||||||
|
"tierBased": False,
|
||||||
|
"taker": 0.001,
|
||||||
|
"maker": 0.001,
|
||||||
|
"precision": {
|
||||||
|
"base": 8,
|
||||||
|
"quote": 8,
|
||||||
|
"amount": 2,
|
||||||
|
"price": 4
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
},
|
||||||
|
"id": "ADADOUBLEUSDT",
|
||||||
|
"symbol": "ADADOUBLE/USDT",
|
||||||
|
"base": "ADADOUBLE",
|
||||||
|
"quote": "USDT",
|
||||||
|
"baseId": "ADADOUBLE",
|
||||||
|
"quoteId": "USDT",
|
||||||
|
"info": {},
|
||||||
|
"type": "spot",
|
||||||
|
"spot": True,
|
||||||
|
"future": False,
|
||||||
|
"active": True
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return shitmarkets
|
return shitmarkets
|
||||||
|
|
||||||
@ -1391,6 +1431,28 @@ def tickers():
|
|||||||
"quoteVolume": 0.0,
|
"quoteVolume": 0.0,
|
||||||
"info": {}
|
"info": {}
|
||||||
},
|
},
|
||||||
|
"ADADOUBLE/USDT": {
|
||||||
|
"symbol": "ADADOUBLE/USDT",
|
||||||
|
"timestamp": 1580469388244,
|
||||||
|
"datetime": "2020-01-31T11:16:28.244Z",
|
||||||
|
"high": None,
|
||||||
|
"low": None,
|
||||||
|
"bid": 0.7305,
|
||||||
|
"bidVolume": None,
|
||||||
|
"ask": 0.7342,
|
||||||
|
"askVolume": None,
|
||||||
|
"vwap": None,
|
||||||
|
"open": None,
|
||||||
|
"close": None,
|
||||||
|
"last": 0,
|
||||||
|
"previousClose": None,
|
||||||
|
"change": None,
|
||||||
|
"percentage": 2.628,
|
||||||
|
"average": None,
|
||||||
|
"baseVolume": 0.0,
|
||||||
|
"quoteVolume": 0.0,
|
||||||
|
"info": {}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
|
|||||||
|
|
||||||
trades = load_trades_from_db(db_url=default_conf['db_url'])
|
trades = load_trades_from_db(db_url=default_conf['db_url'])
|
||||||
assert init_mock.call_count == 1
|
assert init_mock.call_count == 1
|
||||||
assert len(trades) == 3
|
assert len(trades) == 4
|
||||||
assert isinstance(trades, DataFrame)
|
assert isinstance(trades, DataFrame)
|
||||||
assert "pair" in trades.columns
|
assert "pair" in trades.columns
|
||||||
assert "open_time" in trades.columns
|
assert "open_time" in trades.columns
|
||||||
|
@ -409,3 +409,98 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc
|
|||||||
final = edge._process_expectancy(trades_df)
|
final = edge._process_expectancy(trades_df)
|
||||||
assert len(final) == 0
|
assert len(final) == 0
|
||||||
assert isinstance(final, dict)
|
assert isinstance(final, dict)
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,):
|
||||||
|
edge_conf['edge']['min_trade_number'] = 2
|
||||||
|
edge_conf['edge']['remove_pumps'] = True
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
|
||||||
|
|
||||||
|
freqtrade.exchange.get_fee = fee
|
||||||
|
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
|
||||||
|
|
||||||
|
trades = [
|
||||||
|
{'pair': 'TEST/BTC',
|
||||||
|
'stoploss': -0.9,
|
||||||
|
'profit_percent': '',
|
||||||
|
'profit_abs': '',
|
||||||
|
'open_time': np.datetime64('2018-10-03T00:05:00.000000000'),
|
||||||
|
'close_time': np.datetime64('2018-10-03T00:10:00.000000000'),
|
||||||
|
'open_index': 1,
|
||||||
|
'close_index': 1,
|
||||||
|
'trade_duration': '',
|
||||||
|
'open_rate': 17,
|
||||||
|
'close_rate': 15,
|
||||||
|
'exit_type': 'sell_signal'},
|
||||||
|
|
||||||
|
{'pair': 'TEST/BTC',
|
||||||
|
'stoploss': -0.9,
|
||||||
|
'profit_percent': '',
|
||||||
|
'profit_abs': '',
|
||||||
|
'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
|
||||||
|
'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
|
||||||
|
'open_index': 4,
|
||||||
|
'close_index': 4,
|
||||||
|
'trade_duration': '',
|
||||||
|
'open_rate': 20,
|
||||||
|
'close_rate': 10,
|
||||||
|
'exit_type': 'sell_signal'},
|
||||||
|
{'pair': 'TEST/BTC',
|
||||||
|
'stoploss': -0.9,
|
||||||
|
'profit_percent': '',
|
||||||
|
'profit_abs': '',
|
||||||
|
'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
|
||||||
|
'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
|
||||||
|
'open_index': 4,
|
||||||
|
'close_index': 4,
|
||||||
|
'trade_duration': '',
|
||||||
|
'open_rate': 20,
|
||||||
|
'close_rate': 10,
|
||||||
|
'exit_type': 'sell_signal'},
|
||||||
|
{'pair': 'TEST/BTC',
|
||||||
|
'stoploss': -0.9,
|
||||||
|
'profit_percent': '',
|
||||||
|
'profit_abs': '',
|
||||||
|
'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
|
||||||
|
'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
|
||||||
|
'open_index': 4,
|
||||||
|
'close_index': 4,
|
||||||
|
'trade_duration': '',
|
||||||
|
'open_rate': 20,
|
||||||
|
'close_rate': 10,
|
||||||
|
'exit_type': 'sell_signal'},
|
||||||
|
{'pair': 'TEST/BTC',
|
||||||
|
'stoploss': -0.9,
|
||||||
|
'profit_percent': '',
|
||||||
|
'profit_abs': '',
|
||||||
|
'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
|
||||||
|
'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
|
||||||
|
'open_index': 4,
|
||||||
|
'close_index': 4,
|
||||||
|
'trade_duration': '',
|
||||||
|
'open_rate': 20,
|
||||||
|
'close_rate': 10,
|
||||||
|
'exit_type': 'sell_signal'},
|
||||||
|
|
||||||
|
{'pair': 'TEST/BTC',
|
||||||
|
'stoploss': -0.9,
|
||||||
|
'profit_percent': '',
|
||||||
|
'profit_abs': '',
|
||||||
|
'open_time': np.datetime64('2018-10-03T00:30:00.000000000'),
|
||||||
|
'close_time': np.datetime64('2018-10-03T00:40:00.000000000'),
|
||||||
|
'open_index': 6,
|
||||||
|
'close_index': 7,
|
||||||
|
'trade_duration': '',
|
||||||
|
'open_rate': 26,
|
||||||
|
'close_rate': 134,
|
||||||
|
'exit_type': 'sell_signal'}
|
||||||
|
]
|
||||||
|
|
||||||
|
trades_df = DataFrame(trades)
|
||||||
|
trades_df = edge._fill_calculable_fields(trades_df)
|
||||||
|
final = edge._process_expectancy(trades_df)
|
||||||
|
|
||||||
|
assert 'TEST/BTC' in final
|
||||||
|
assert final['TEST/BTC'].stoploss == -0.9
|
||||||
|
assert final['TEST/BTC'].nb_trades == len(trades_df) - 1
|
||||||
|
assert round(final['TEST/BTC'].winrate, 10) == 0.0
|
||||||
|
@ -714,13 +714,13 @@ def test_validate_order_types(default_conf, mocker):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
|
mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex')
|
||||||
|
|
||||||
default_conf['order_types'] = {
|
default_conf['order_types'] = {
|
||||||
'buy': 'limit',
|
'buy': 'limit',
|
||||||
'sell': 'limit',
|
'sell': 'limit',
|
||||||
'stoploss': 'market',
|
'stoploss': 'market',
|
||||||
'stoploss_on_exchange': False
|
'stoploss_on_exchange': False
|
||||||
}
|
}
|
||||||
|
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False})
|
type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False})
|
||||||
@ -730,9 +730,8 @@ def test_validate_order_types(default_conf, mocker):
|
|||||||
'buy': 'limit',
|
'buy': 'limit',
|
||||||
'sell': 'limit',
|
'sell': 'limit',
|
||||||
'stoploss': 'market',
|
'stoploss': 'market',
|
||||||
'stoploss_on_exchange': 'false'
|
'stoploss_on_exchange': False
|
||||||
}
|
}
|
||||||
|
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r'Exchange .* does not support market orders.'):
|
match=r'Exchange .* does not support market orders.'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
@ -743,7 +742,6 @@ def test_validate_order_types(default_conf, mocker):
|
|||||||
'stoploss': 'limit',
|
'stoploss': 'limit',
|
||||||
'stoploss_on_exchange': True
|
'stoploss_on_exchange': True
|
||||||
}
|
}
|
||||||
|
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match=r'On exchange stoploss is not supported for .*'):
|
match=r'On exchange stoploss is not supported for .*'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
@ -1820,7 +1818,7 @@ def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, cap
|
|||||||
|
|
||||||
res = exchange.cancel_order_with_result('1234', 'ETH/BTC', 1541)
|
res = exchange.cancel_order_with_result('1234', 'ETH/BTC', 1541)
|
||||||
assert isinstance(res, dict)
|
assert isinstance(res, dict)
|
||||||
assert log_has("Could not cancel order 1234.", caplog)
|
assert log_has("Could not cancel order 1234 for ETH/BTC.", caplog)
|
||||||
assert log_has("Could not fetch cancelled order 1234.", caplog)
|
assert log_has("Could not fetch cancelled order 1234.", caplog)
|
||||||
assert res['amount'] == 1541
|
assert res['amount'] == 1541
|
||||||
|
|
||||||
@ -1898,10 +1896,10 @@ def test_fetch_order(default_conf, mocker, exchange_name):
|
|||||||
assert tm.call_args_list[1][0][0] == 2
|
assert tm.call_args_list[1][0][0] == 2
|
||||||
assert tm.call_args_list[2][0][0] == 5
|
assert tm.call_args_list[2][0][0] == 5
|
||||||
assert tm.call_args_list[3][0][0] == 10
|
assert tm.call_args_list[3][0][0] == 10
|
||||||
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
|
assert api_mock.fetch_order.call_count == 6
|
||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||||
'fetch_order', 'fetch_order',
|
'fetch_order', 'fetch_order', retries=6,
|
||||||
order_id='_', pair='TKN/BTC')
|
order_id='_', pair='TKN/BTC')
|
||||||
|
|
||||||
|
|
||||||
@ -1934,6 +1932,7 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name):
|
|||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||||
'fetch_stoploss_order', 'fetch_order',
|
'fetch_stoploss_order', 'fetch_order',
|
||||||
|
retries=6,
|
||||||
order_id='_', pair='TKN/BTC')
|
order_id='_', pair='TKN/BTC')
|
||||||
|
|
||||||
|
|
||||||
@ -2317,6 +2316,18 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
|
|||||||
(3, 3, 1),
|
(3, 3, 1),
|
||||||
(0, 1, 2),
|
(0, 1, 2),
|
||||||
(1, 1, 1),
|
(1, 1, 1),
|
||||||
|
(0, 4, 17),
|
||||||
|
(1, 4, 10),
|
||||||
|
(2, 4, 5),
|
||||||
|
(3, 4, 2),
|
||||||
|
(4, 4, 1),
|
||||||
|
(0, 5, 26),
|
||||||
|
(1, 5, 17),
|
||||||
|
(2, 5, 10),
|
||||||
|
(3, 5, 5),
|
||||||
|
(4, 5, 2),
|
||||||
|
(5, 5, 1),
|
||||||
|
|
||||||
])
|
])
|
||||||
def test_calculate_backoff(retrycount, max_retries, expected):
|
def test_calculate_backoff(retrycount, max_retries, expected):
|
||||||
assert calculate_backoff(retrycount, max_retries) == expected
|
assert calculate_backoff(retrycount, max_retries) == expected
|
||||||
|
@ -154,4 +154,5 @@ def test_fetch_stoploss_order(default_conf, mocker):
|
|||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx',
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx',
|
||||||
'fetch_stoploss_order', 'fetch_orders',
|
'fetch_stoploss_order', 'fetch_orders',
|
||||||
|
retries=6,
|
||||||
order_id='_', pair='TKN/BTC')
|
order_id='_', pair='TKN/BTC')
|
||||||
|
@ -308,6 +308,11 @@ def test_data_with_fee(default_conf, mocker, testdatadir) -> None:
|
|||||||
assert backtesting.fee == 0.1234
|
assert backtesting.fee == 0.1234
|
||||||
assert fee_mock.call_count == 0
|
assert fee_mock.call_count == 0
|
||||||
|
|
||||||
|
default_conf['fee'] = 0.0
|
||||||
|
backtesting = Backtesting(default_conf)
|
||||||
|
assert backtesting.fee == 0.0
|
||||||
|
assert fee_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
|
def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None:
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
@ -235,7 +235,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
|
||||||
"BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
|
"BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
||||||
"USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']),
|
"USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']),
|
||||||
# No pair for ETH, VolumePairList
|
# No pair for ETH, VolumePairList
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
||||||
"ETH", []),
|
"ETH", []),
|
||||||
@ -275,11 +275,16 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
{"method": "PriceFilter", "low_price_ratio": 0.03}],
|
{"method": "PriceFilter", "low_price_ratio": 0.03}],
|
||||||
"USDT", ['ETH/USDT', 'NANO/USDT']),
|
"USDT", ['ETH/USDT', 'NANO/USDT']),
|
||||||
# Hot is removed by precision_filter, Fuel by low_price_filter.
|
# Hot is removed by precision_filter, Fuel by low_price_ratio, Ripple by min_price.
|
||||||
([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"},
|
([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"},
|
||||||
{"method": "PrecisionFilter"},
|
{"method": "PrecisionFilter"},
|
||||||
{"method": "PriceFilter", "low_price_ratio": 0.02}],
|
{"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.01}],
|
||||||
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
|
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
|
||||||
|
# Hot is removed by precision_filter, Fuel by low_price_ratio, Ethereum by max_price.
|
||||||
|
([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"},
|
||||||
|
{"method": "PrecisionFilter"},
|
||||||
|
{"method": "PriceFilter", "low_price_ratio": 0.02, "max_price": 0.05}],
|
||||||
|
"BTC", ['TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
|
||||||
# HOT and XRP are removed because below 1250 quoteVolume
|
# HOT and XRP are removed because below 1250 quoteVolume
|
||||||
([{"method": "VolumePairList", "number_assets": 5,
|
([{"method": "VolumePairList", "number_assets": 5,
|
||||||
"sort_key": "quoteVolume", "min_value": 1250}],
|
"sort_key": "quoteVolume", "min_value": 1250}],
|
||||||
@ -298,11 +303,11 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
# ShuffleFilter
|
# ShuffleFilter
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
{"method": "ShuffleFilter", "seed": 77}],
|
{"method": "ShuffleFilter", "seed": 77}],
|
||||||
"USDT", ['ETH/USDT', 'ADAHALF/USDT', 'NANO/USDT']),
|
"USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']),
|
||||||
# ShuffleFilter, other seed
|
# ShuffleFilter, other seed
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
{"method": "ShuffleFilter", "seed": 42}],
|
{"method": "ShuffleFilter", "seed": 42}],
|
||||||
"USDT", ['NANO/USDT', 'ETH/USDT', 'ADAHALF/USDT']),
|
"USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']),
|
||||||
# ShuffleFilter, no seed
|
# ShuffleFilter, no seed
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
{"method": "ShuffleFilter"}],
|
{"method": "ShuffleFilter"}],
|
||||||
@ -319,7 +324,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
"BTC", 'filter_at_the_beginning'), # OperationalException expected
|
"BTC", 'filter_at_the_beginning'), # OperationalException expected
|
||||||
# PriceFilter after StaticPairList
|
# PriceFilter after StaticPairList
|
||||||
([{"method": "StaticPairList"},
|
([{"method": "StaticPairList"},
|
||||||
{"method": "PriceFilter", "low_price_ratio": 0.02}],
|
{"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.000001, "max_price": 0.1}],
|
||||||
"BTC", ['ETH/BTC', 'TKN/BTC']),
|
"BTC", ['ETH/BTC', 'TKN/BTC']),
|
||||||
# PriceFilter only
|
# PriceFilter only
|
||||||
([{"method": "PriceFilter", "low_price_ratio": 0.02}],
|
([{"method": "PriceFilter", "low_price_ratio": 0.02}],
|
||||||
@ -342,6 +347,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
|
||||||
{"method": "StaticPairList"}],
|
{"method": "StaticPairList"}],
|
||||||
"BTC", 'static_in_the_middle'),
|
"BTC", 'static_in_the_middle'),
|
||||||
|
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
|
||||||
|
{"method": "PriceFilter", "low_price_ratio": 0.02}],
|
||||||
|
"USDT", ['ETH/USDT', 'NANO/USDT']),
|
||||||
])
|
])
|
||||||
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
||||||
ohlcv_history_list, pairlists, base_currency,
|
ohlcv_history_list, pairlists, base_currency,
|
||||||
@ -396,6 +404,10 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
|
|||||||
r'would be <= stop limit.*', caplog)
|
r'would be <= stop limit.*', caplog)
|
||||||
if pairlist['method'] == 'PriceFilter' and whitelist_result:
|
if pairlist['method'] == 'PriceFilter' and whitelist_result:
|
||||||
assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or
|
assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or
|
||||||
|
log_has_re(r'^Removed .* from whitelist, '
|
||||||
|
r'because last price < .*%$', caplog) or
|
||||||
|
log_has_re(r'^Removed .* from whitelist, '
|
||||||
|
r'because last price > .*%$', caplog) or
|
||||||
log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] "
|
log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] "
|
||||||
r"is empty.*", caplog))
|
r"is empty.*", caplog))
|
||||||
if pairlist['method'] == 'VolumePairList':
|
if pairlist['method'] == 'VolumePairList':
|
||||||
@ -524,6 +536,37 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers):
|
|||||||
assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf
|
assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf
|
||||||
|
|
||||||
|
|
||||||
|
def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog):
|
||||||
|
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
|
||||||
|
{'method': 'AgeFilter', 'min_days_listed': -1}]
|
||||||
|
|
||||||
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
exchange_has=MagicMock(return_value=True),
|
||||||
|
get_tickers=tickers
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'AgeFilter requires min_days_listed must be >= 1'):
|
||||||
|
get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog):
|
||||||
|
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10},
|
||||||
|
{'method': 'AgeFilter', 'min_days_listed': 99999}]
|
||||||
|
|
||||||
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
exchange_has=MagicMock(return_value=True),
|
||||||
|
get_tickers=tickers
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException,
|
||||||
|
match=r'AgeFilter requires min_days_listed must not exceed '
|
||||||
|
r'exchange max request size \([0-9]+\)'):
|
||||||
|
get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list):
|
def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list):
|
||||||
|
|
||||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
@ -547,6 +590,36 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his
|
|||||||
assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count
|
assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("pairlistconfig,expected", [
|
||||||
|
({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010,
|
||||||
|
"max_price": 1.0}, "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below "
|
||||||
|
"0.1% or below 0.00000010 or above 1.00000000.'}]"
|
||||||
|
),
|
||||||
|
({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010},
|
||||||
|
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or below 0.00000010.'}]"
|
||||||
|
),
|
||||||
|
({"method": "PriceFilter", "low_price_ratio": 0.001, "max_price": 1.00010000},
|
||||||
|
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or above 1.00010000.'}]"
|
||||||
|
),
|
||||||
|
({"method": "PriceFilter", "min_price": 0.00002000},
|
||||||
|
"[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]"
|
||||||
|
),
|
||||||
|
({"method": "PriceFilter"},
|
||||||
|
"[{'PriceFilter': 'PriceFilter - No price filters configured.'}]"
|
||||||
|
),
|
||||||
|
])
|
||||||
|
def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, expected):
|
||||||
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
exchange_has=MagicMock(return_value=True)
|
||||||
|
)
|
||||||
|
whitelist_conf['pairlists'] = [pairlistconfig]
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
|
short_desc = str(freqtrade.pairlists.short_desc())
|
||||||
|
assert short_desc == expected
|
||||||
|
|
||||||
|
|
||||||
def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
|
def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog):
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ import pytest
|
|||||||
from numpy import isnan
|
from numpy import isnan
|
||||||
|
|
||||||
from freqtrade.edge import PairInfo
|
from freqtrade.edge import PairInfo
|
||||||
from freqtrade.exceptions import ExchangeError, TemporaryError
|
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPC, RPCException
|
from freqtrade.rpc import RPC, RPCException
|
||||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||||
@ -286,12 +286,66 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
|
|||||||
assert isinstance(trades['trades'][1], dict)
|
assert isinstance(trades['trades'][1], dict)
|
||||||
|
|
||||||
trades = rpc._rpc_trade_history(0)
|
trades = rpc._rpc_trade_history(0)
|
||||||
assert len(trades['trades']) == 3
|
assert len(trades['trades']) == 2
|
||||||
assert trades['trades_count'] == 3
|
assert trades['trades_count'] == 2
|
||||||
# The first trade is for ETH ... sorting is descending
|
# The first closed trade is for ETC ... sorting is descending
|
||||||
assert trades['trades'][-1]['pair'] == 'ETH/BTC'
|
assert trades['trades'][-1]['pair'] == 'ETC/BTC'
|
||||||
assert trades['trades'][0]['pair'] == 'ETC/BTC'
|
assert trades['trades'][0]['pair'] == 'XRP/BTC'
|
||||||
assert trades['trades'][1]['pair'] == 'ETC/BTC'
|
|
||||||
|
|
||||||
|
def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog):
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
stoploss_mock = MagicMock()
|
||||||
|
cancel_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
cancel_order=cancel_mock,
|
||||||
|
cancel_stoploss_order=stoploss_mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
freqtradebot.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
create_mock_trades(fee)
|
||||||
|
rpc = RPC(freqtradebot)
|
||||||
|
with pytest.raises(RPCException, match='invalid argument'):
|
||||||
|
rpc._rpc_delete('200')
|
||||||
|
|
||||||
|
create_mock_trades(fee)
|
||||||
|
trades = Trade.query.all()
|
||||||
|
trades[1].stoploss_order_id = '1234'
|
||||||
|
trades[2].stoploss_order_id = '1234'
|
||||||
|
assert len(trades) > 2
|
||||||
|
|
||||||
|
res = rpc._rpc_delete('1')
|
||||||
|
assert isinstance(res, dict)
|
||||||
|
assert res['result'] == 'success'
|
||||||
|
assert res['trade_id'] == '1'
|
||||||
|
assert res['cancel_order_count'] == 1
|
||||||
|
assert cancel_mock.call_count == 1
|
||||||
|
assert stoploss_mock.call_count == 0
|
||||||
|
cancel_mock.reset_mock()
|
||||||
|
stoploss_mock.reset_mock()
|
||||||
|
|
||||||
|
res = rpc._rpc_delete('2')
|
||||||
|
assert isinstance(res, dict)
|
||||||
|
assert cancel_mock.call_count == 1
|
||||||
|
assert stoploss_mock.call_count == 1
|
||||||
|
assert res['cancel_order_count'] == 2
|
||||||
|
|
||||||
|
stoploss_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
|
||||||
|
side_effect=InvalidOrderException)
|
||||||
|
|
||||||
|
res = rpc._rpc_delete('3')
|
||||||
|
assert stoploss_mock.call_count == 1
|
||||||
|
stoploss_mock.reset_mock()
|
||||||
|
|
||||||
|
cancel_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_order',
|
||||||
|
side_effect=InvalidOrderException)
|
||||||
|
|
||||||
|
res = rpc._rpc_delete('4')
|
||||||
|
assert cancel_mock.call_count == 1
|
||||||
|
assert stoploss_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
|
@ -50,6 +50,12 @@ def client_get(client, url):
|
|||||||
'Origin': 'http://example.com'})
|
'Origin': 'http://example.com'})
|
||||||
|
|
||||||
|
|
||||||
|
def client_delete(client, url):
|
||||||
|
# Add fake Origin to ensure CORS kicks in
|
||||||
|
return client.delete(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS),
|
||||||
|
'Origin': 'http://example.com'})
|
||||||
|
|
||||||
|
|
||||||
def assert_response(response, expected_code=200, needs_cors=True):
|
def assert_response(response, expected_code=200, needs_cors=True):
|
||||||
assert response.status_code == expected_code
|
assert response.status_code == expected_code
|
||||||
assert response.content_type == "application/json"
|
assert response.content_type == "application/json"
|
||||||
@ -352,7 +358,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
|
|||||||
assert rc.json['data'][0]['date'] == str(datetime.utcnow().date())
|
assert rc.json['data'][0]['date'] == str(datetime.utcnow().date())
|
||||||
|
|
||||||
|
|
||||||
def test_api_trades(botclient, mocker, ticker, fee, markets):
|
def test_api_trades(botclient, mocker, fee, markets):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
patch_get_signal(ftbot, (True, False))
|
patch_get_signal(ftbot, (True, False))
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -368,12 +374,53 @@ def test_api_trades(botclient, mocker, ticker, fee, markets):
|
|||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/trades")
|
rc = client_get(client, f"{BASE_URI}/trades")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert len(rc.json['trades']) == 3
|
|
||||||
assert rc.json['trades_count'] == 3
|
|
||||||
rc = client_get(client, f"{BASE_URI}/trades?limit=2")
|
|
||||||
assert_response(rc)
|
|
||||||
assert len(rc.json['trades']) == 2
|
assert len(rc.json['trades']) == 2
|
||||||
assert rc.json['trades_count'] == 2
|
assert rc.json['trades_count'] == 2
|
||||||
|
rc = client_get(client, f"{BASE_URI}/trades?limit=1")
|
||||||
|
assert_response(rc)
|
||||||
|
assert len(rc.json['trades']) == 1
|
||||||
|
assert rc.json['trades_count'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_delete_trade(botclient, mocker, fee, markets):
|
||||||
|
ftbot, client = botclient
|
||||||
|
patch_get_signal(ftbot, (True, False))
|
||||||
|
stoploss_mock = MagicMock()
|
||||||
|
cancel_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
markets=PropertyMock(return_value=markets),
|
||||||
|
cancel_order=cancel_mock,
|
||||||
|
cancel_stoploss_order=stoploss_mock,
|
||||||
|
)
|
||||||
|
rc = client_delete(client, f"{BASE_URI}/trades/1")
|
||||||
|
# Error - trade won't exist yet.
|
||||||
|
assert_response(rc, 502)
|
||||||
|
|
||||||
|
create_mock_trades(fee)
|
||||||
|
ftbot.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
trades = Trade.query.all()
|
||||||
|
trades[1].stoploss_order_id = '1234'
|
||||||
|
assert len(trades) > 2
|
||||||
|
|
||||||
|
rc = client_delete(client, f"{BASE_URI}/trades/1")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json['result_msg'] == 'Deleted trade 1. Closed 1 open orders.'
|
||||||
|
assert len(trades) - 1 == len(Trade.query.all())
|
||||||
|
assert cancel_mock.call_count == 1
|
||||||
|
|
||||||
|
cancel_mock.reset_mock()
|
||||||
|
rc = client_delete(client, f"{BASE_URI}/trades/1")
|
||||||
|
# Trade is gone now.
|
||||||
|
assert_response(rc, 502)
|
||||||
|
assert cancel_mock.call_count == 0
|
||||||
|
|
||||||
|
assert len(trades) - 1 == len(Trade.query.all())
|
||||||
|
rc = client_delete(client, f"{BASE_URI}/trades/2")
|
||||||
|
assert_response(rc)
|
||||||
|
assert rc.json['result_msg'] == 'Deleted trade 2. Closed 2 open orders.'
|
||||||
|
assert len(trades) - 2 == len(Trade.query.all())
|
||||||
|
assert stoploss_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
||||||
|
@ -21,8 +21,9 @@ from freqtrade.rpc import RPCMessageType
|
|||||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange,
|
from tests.conftest import (create_mock_trades, get_patched_freqtradebot,
|
||||||
patch_get_signal, patch_whitelist)
|
log_has, patch_exchange, patch_get_signal,
|
||||||
|
patch_whitelist)
|
||||||
|
|
||||||
|
|
||||||
class DummyCls(Telegram):
|
class DummyCls(Telegram):
|
||||||
@ -60,7 +61,7 @@ def test__init__(default_conf, mocker) -> None:
|
|||||||
assert telegram._config == default_conf
|
assert telegram._config == default_conf
|
||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker, caplog) -> None:
|
def test_telegram_init(default_conf, mocker, caplog) -> None:
|
||||||
start_polling = MagicMock()
|
start_polling = MagicMock()
|
||||||
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
|
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
|
||||||
|
|
||||||
@ -72,10 +73,10 @@ def test_init(default_conf, mocker, caplog) -> None:
|
|||||||
assert start_polling.start_polling.call_count == 1
|
assert start_polling.start_polling.call_count == 1
|
||||||
|
|
||||||
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], "
|
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
|
||||||
"['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], "
|
"['delete'], ['performance'], ['daily'], ['count'], ['reload_config', "
|
||||||
"['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], "
|
"'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], "
|
||||||
"['edge'], ['help'], ['version']]")
|
"['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]")
|
||||||
|
|
||||||
assert log_has(message_str, caplog)
|
assert log_has(message_str, caplog)
|
||||||
|
|
||||||
@ -725,6 +726,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'trade_id': 1,
|
||||||
'exchange': 'Bittrex',
|
'exchange': 'Bittrex',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'gain': 'profit',
|
'gain': 'profit',
|
||||||
@ -784,6 +786,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
|
|||||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'trade_id': 1,
|
||||||
'exchange': 'Bittrex',
|
'exchange': 'Bittrex',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
@ -832,6 +835,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
|||||||
msg = rpc_mock.call_args_list[0][0][0]
|
msg = rpc_mock.call_args_list[0][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'trade_id': 1,
|
||||||
'exchange': 'Bittrex',
|
'exchange': 'Bittrex',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
@ -1143,6 +1147,63 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
|
|||||||
assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0]
|
assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_telegram_trades(mocker, update, default_conf, fee):
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
_init=MagicMock(),
|
||||||
|
_send_msg=msg_mock
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
telegram = Telegram(freqtradebot)
|
||||||
|
context = MagicMock()
|
||||||
|
context.args = []
|
||||||
|
|
||||||
|
telegram._trades(update=update, context=context)
|
||||||
|
assert "<b>0 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert "<pre>" not in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
create_mock_trades(fee)
|
||||||
|
|
||||||
|
context = MagicMock()
|
||||||
|
context.args = [5]
|
||||||
|
telegram._trades(update=update, context=context)
|
||||||
|
msg_mock.call_count == 1
|
||||||
|
assert "2 recent trades</b>:" in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert "Profit (" in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert "Open Date" in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert "<pre>" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_telegram_delete_trade(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._delete_trade(update=update, context=context)
|
||||||
|
assert "invalid argument" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
create_mock_trades(fee)
|
||||||
|
|
||||||
|
context = MagicMock()
|
||||||
|
context.args = [1]
|
||||||
|
telegram._delete_trade(update=update, context=context)
|
||||||
|
msg_mock.call_count == 1
|
||||||
|
assert "Deleted trade 1." in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_help_handle(default_conf, update, mocker) -> None:
|
def test_help_handle(default_conf, update, mocker) -> None:
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
|
@ -871,6 +871,14 @@ def test_load_config_default_exchange_name(all_conf) -> None:
|
|||||||
validate_config_schema(all_conf)
|
validate_config_schema(all_conf)
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_config_stoploss_exchange_limit_ratio(all_conf) -> None:
|
||||||
|
all_conf['order_types']['stoploss_on_exchange_limit_ratio'] = 1.15
|
||||||
|
|
||||||
|
with pytest.raises(ValidationError,
|
||||||
|
match=r"1.15 is greater than the maximum"):
|
||||||
|
validate_config_schema(all_conf)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("keys", [("exchange", "sandbox", False),
|
@pytest.mark.parametrize("keys", [("exchange", "sandbox", False),
|
||||||
("exchange", "key", ""),
|
("exchange", "key", ""),
|
||||||
("exchange", "secret", ""),
|
("exchange", "secret", ""),
|
||||||
|
@ -1660,6 +1660,7 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog)
|
|||||||
trade = MagicMock()
|
trade = MagicMock()
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.open_fee = 0.001
|
trade.open_fee = 0.001
|
||||||
|
trade.pair = 'ETH/BTC'
|
||||||
trades = [trade]
|
trades = [trade]
|
||||||
|
|
||||||
# Test raise of DependencyException exception
|
# Test raise of DependencyException exception
|
||||||
@ -1669,7 +1670,7 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog)
|
|||||||
)
|
)
|
||||||
n = freqtrade.exit_positions(trades)
|
n = freqtrade.exit_positions(trades)
|
||||||
assert n == 0
|
assert n == 0
|
||||||
assert log_has('Unable to sell trade: ', caplog)
|
assert log_has('Unable to sell trade ETH/BTC: ', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None:
|
def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None:
|
||||||
@ -1726,6 +1727,7 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_
|
|||||||
amount=amount,
|
amount=amount,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
|
open_date=arrow.utcnow().datetime,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
open_order_id="123456",
|
open_order_id="123456",
|
||||||
@ -1816,6 +1818,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde
|
|||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
fee_open=0.0025,
|
fee_open=0.0025,
|
||||||
fee_close=0.0025,
|
fee_close=0.0025,
|
||||||
|
open_date=arrow.utcnow().datetime,
|
||||||
open_order_id="123456",
|
open_order_id="123456",
|
||||||
is_open=True,
|
is_open=True,
|
||||||
)
|
)
|
||||||
@ -2023,11 +2026,16 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or
|
|||||||
|
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
|
cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
|
||||||
|
cancel_buy_order = deepcopy(limit_buy_order_old)
|
||||||
|
cancel_buy_order['status'] = 'canceled'
|
||||||
|
cancel_order_wr_mock = MagicMock(return_value=cancel_buy_order)
|
||||||
|
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
fetch_order=MagicMock(return_value=limit_buy_order_old),
|
fetch_order=MagicMock(return_value=limit_buy_order_old),
|
||||||
|
cancel_order_with_result=cancel_order_wr_mock,
|
||||||
cancel_order=cancel_order_mock,
|
cancel_order=cancel_order_mock,
|
||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
@ -2060,7 +2068,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or
|
|||||||
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True)
|
freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True)
|
||||||
# Trade should be closed since the function returns true
|
# Trade should be closed since the function returns true
|
||||||
freqtrade.check_handle_timedout()
|
freqtrade.check_handle_timedout()
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_wr_mock.call_count == 1
|
||||||
assert rpc_mock.call_count == 1
|
assert rpc_mock.call_count == 1
|
||||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||||
nb_trades = len(trades)
|
nb_trades = len(trades)
|
||||||
@ -2071,7 +2079,9 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or
|
|||||||
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade,
|
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade,
|
||||||
fee, mocker) -> None:
|
fee, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
cancel_order_mock = MagicMock(return_value=limit_buy_order_old)
|
limit_buy_cancel = deepcopy(limit_buy_order_old)
|
||||||
|
limit_buy_cancel['status'] = 'canceled'
|
||||||
|
cancel_order_mock = MagicMock(return_value=limit_buy_cancel)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -2259,7 +2269,10 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old,
|
|||||||
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
|
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
|
||||||
open_trade, mocker) -> None:
|
open_trade, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial)
|
limit_buy_canceled = deepcopy(limit_buy_order_old_partial)
|
||||||
|
limit_buy_canceled['status'] = 'canceled'
|
||||||
|
|
||||||
|
cancel_order_mock = MagicMock(return_value=limit_buy_canceled)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -2392,7 +2405,11 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke
|
|||||||
def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> None:
|
def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
cancel_order_mock = MagicMock(return_value=limit_buy_order)
|
cancel_buy_order = deepcopy(limit_buy_order)
|
||||||
|
cancel_buy_order['status'] = 'canceled'
|
||||||
|
del cancel_buy_order['filled']
|
||||||
|
|
||||||
|
cancel_order_mock = MagicMock(return_value=cancel_buy_order)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -2412,9 +2429,12 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non
|
|||||||
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
limit_buy_order['filled'] = 2
|
# Order remained open for some reason (cancel failed)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException)
|
cancel_buy_order['status'] = 'open'
|
||||||
|
cancel_order_mock = MagicMock(return_value=cancel_buy_order)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
|
||||||
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason)
|
||||||
|
assert log_has_re(r"Order .* for .* not cancelled.", caplog)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
|
@pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'],
|
||||||
@ -2572,6 +2592,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
|
|||||||
assert rpc_mock.call_count == 1
|
assert rpc_mock.call_count == 1
|
||||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
assert {
|
assert {
|
||||||
|
'trade_id': 1,
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
'exchange': 'Bittrex',
|
'exchange': 'Bittrex',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
@ -2622,6 +2643,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
|
|||||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'trade_id': 1,
|
||||||
'exchange': 'Bittrex',
|
'exchange': 'Bittrex',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
@ -2678,6 +2700,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
|
|||||||
|
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'trade_id': 1,
|
||||||
'exchange': 'Bittrex',
|
'exchange': 'Bittrex',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
@ -2883,6 +2906,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
|
|||||||
last_msg = rpc_mock.call_args_list[-1][0][0]
|
last_msg = rpc_mock.call_args_list[-1][0][0]
|
||||||
assert {
|
assert {
|
||||||
'type': RPCMessageType.SELL_NOTIFICATION,
|
'type': RPCMessageType.SELL_NOTIFICATION,
|
||||||
|
'trade_id': 1,
|
||||||
'exchange': 'Bittrex',
|
'exchange': 'Bittrex',
|
||||||
'pair': 'ETH/BTC',
|
'pair': 'ETH/BTC',
|
||||||
'gain': 'profit',
|
'gain': 'profit',
|
||||||
@ -4090,7 +4114,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi
|
|||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
trades = Trade.query.all()
|
trades = Trade.query.all()
|
||||||
assert len(trades) == 3
|
assert len(trades) == 4
|
||||||
freqtrade.cancel_all_open_orders()
|
freqtrade.cancel_all_open_orders()
|
||||||
assert buy_mock.call_count == 1
|
assert buy_mock.call_count == 1
|
||||||
assert sell_mock.call_count == 1
|
assert sell_mock.call_count == 1
|
||||||
|
@ -995,7 +995,7 @@ def test_get_overall_performance(fee):
|
|||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
res = Trade.get_overall_performance()
|
res = Trade.get_overall_performance()
|
||||||
|
|
||||||
assert len(res) == 1
|
assert len(res) == 2
|
||||||
assert 'pair' in res[0]
|
assert 'pair' in res[0]
|
||||||
assert 'profit' in res[0]
|
assert 'profit' in res[0]
|
||||||
assert 'count' in res[0]
|
assert 'count' in res[0]
|
||||||
@ -1010,5 +1010,5 @@ def test_get_best_pair(fee):
|
|||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
res = Trade.get_best_pair()
|
res = Trade.get_best_pair()
|
||||||
assert len(res) == 2
|
assert len(res) == 2
|
||||||
assert res[0] == 'ETC/BTC'
|
assert res[0] == 'XRP/BTC'
|
||||||
assert res[1] == 0.005
|
assert res[1] == 0.01
|
||||||
|
@ -21,7 +21,7 @@ from freqtrade.plot.plotting import (add_indicators, add_profit,
|
|||||||
load_and_plot_trades, plot_profit,
|
load_and_plot_trades, plot_profit,
|
||||||
plot_trades, store_plot_file)
|
plot_trades, store_plot_file)
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
from tests.conftest import get_args, log_has, log_has_re
|
from tests.conftest import get_args, log_has, log_has_re, patch_exchange
|
||||||
|
|
||||||
|
|
||||||
def fig_generating_mock(fig, *args, **kwargs):
|
def fig_generating_mock(fig, *args, **kwargs):
|
||||||
@ -316,6 +316,8 @@ def test_start_plot_dataframe(mocker):
|
|||||||
|
|
||||||
|
|
||||||
def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
|
def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir):
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
default_conf['trade_source'] = 'file'
|
default_conf['trade_source'] = 'file'
|
||||||
default_conf["datadir"] = testdatadir
|
default_conf["datadir"] = testdatadir
|
||||||
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
|
default_conf['exportfilename'] = testdatadir / "backtest-result_test.json"
|
||||||
|
Loading…
Reference in New Issue
Block a user