Merge branch 'develop' into pr/stash86/6458
This commit is contained in:
commit
c2b90afa61
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,8 @@
|
|||||||
# Freqtrade rules
|
# Freqtrade rules
|
||||||
config*.json
|
config*.json
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
logfile.txt
|
logfile.txt
|
||||||
user_data/*
|
user_data/*
|
||||||
!user_data/strategy/sample_strategy.py
|
!user_data/strategy/sample_strategy.py
|
||||||
|
15
README.md
15
README.md
@ -30,12 +30,13 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
|
|
||||||
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [Huobi](http://huobi.com/)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
- [X] [OKX](https://www.okx.com/)
|
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
@ -68,15 +69,9 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
|||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
Please refer to the [Docker Quickstart documentation](https://www.freqtrade.io/en/stable/docker_quickstart/) on how to get started quickly.
|
||||||
|
|
||||||
```bash
|
For further (native) installation methods, please refer to the [Installation documentation page](https://www.freqtrade.io/en/stable/installation/).
|
||||||
git clone -b develop https://github.com/freqtrade/freqtrade.git
|
|
||||||
cd freqtrade
|
|
||||||
./setup.sh --install
|
|
||||||
```
|
|
||||||
|
|
||||||
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/stable/installation/).
|
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
|
BIN
docs/assets/windows_install.png
Normal file
BIN
docs/assets/windows_install.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 92 KiB |
@ -57,7 +57,7 @@ This configuration enables kraken, as well as rate-limiting to avoid bans from t
|
|||||||
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
Binance supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange..
|
||||||
|
|
||||||
### Binance Blacklist
|
### Binance Blacklist
|
||||||
|
|
||||||
@ -177,12 +177,21 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
|
|||||||
|
|
||||||
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
|
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
|
||||||
|
|
||||||
### Kucoin Blacklists
|
### Kucoin Blacklists
|
||||||
|
|
||||||
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
||||||
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
||||||
|
|
||||||
## OKX
|
## Huobi
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Huobi supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||||
|
|
||||||
|
## OKX (former OKEX)
|
||||||
|
|
||||||
OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
|
|
||||||
|
@ -42,12 +42,13 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
|||||||
|
|
||||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#binance-blacklist))
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [Huobi](http://huobi.com/)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
- [X] [OKX](https://www.okx.com/)
|
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mkdocs==1.2.3
|
mkdocs==1.2.3
|
||||||
mkdocs-material==8.2.1
|
mkdocs-material==8.2.3
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.2
|
pymdown-extensions==9.2
|
||||||
|
@ -24,7 +24,7 @@ These modes can be configured with these values:
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now.
|
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) and kucoin (stop-limit and stop-market) as of now.
|
||||||
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
||||||
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
||||||
|
|
||||||
|
@ -54,6 +54,8 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++
|
|||||||
|
|
||||||
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||||
|
|
||||||
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker compose](docker_quickstart.md) first.
|
You can download the Visual C++ build tools from [here](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and install "Desktop development with C++" in it's default configuration. Unfortunately, this is a heavy download / dependency so you might want to consider WSL2 or [docker compose](docker_quickstart.md) first.
|
||||||
|
|
||||||
|
![Windows installation](assets/windows_install.png)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -108,10 +108,11 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"binance",
|
"binance",
|
||||||
"binanceus",
|
"binanceus",
|
||||||
"bittrex",
|
"bittrex",
|
||||||
"kraken",
|
|
||||||
"ftx",
|
"ftx",
|
||||||
"kucoin",
|
|
||||||
"gateio",
|
"gateio",
|
||||||
|
"huobi",
|
||||||
|
"kraken",
|
||||||
|
"kucoin",
|
||||||
"okx",
|
"okx",
|
||||||
Separator(),
|
Separator(),
|
||||||
"other",
|
"other",
|
||||||
|
@ -18,6 +18,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
|
|||||||
from freqtrade.exchange.ftx import Ftx
|
from freqtrade.exchange.ftx import Ftx
|
||||||
from freqtrade.exchange.gateio import Gateio
|
from freqtrade.exchange.gateio import Gateio
|
||||||
from freqtrade.exchange.hitbtc import Hitbtc
|
from freqtrade.exchange.hitbtc import Hitbtc
|
||||||
|
from freqtrade.exchange.huobi import Huobi
|
||||||
from freqtrade.exchange.kraken import Kraken
|
from freqtrade.exchange.kraken import Kraken
|
||||||
from freqtrade.exchange.kucoin import Kucoin
|
from freqtrade.exchange.kucoin import Kucoin
|
||||||
from freqtrade.exchange.okx import Okx
|
from freqtrade.exchange.okx import Okx
|
||||||
|
@ -3,12 +3,8 @@ import logging
|
|||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
|
||||||
|
|
||||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
|
||||||
OperationalException, TemporaryError)
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -18,6 +14,7 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||||
"time_in_force_parameter": "timeInForce",
|
"time_in_force_parameter": "timeInForce",
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
@ -33,65 +30,6 @@ class Binance(Exchange):
|
|||||||
"""
|
"""
|
||||||
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
||||||
|
|
||||||
@retrier(retries=0)
|
|
||||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
|
||||||
"""
|
|
||||||
creates a stoploss limit order.
|
|
||||||
this stoploss-limit is binance-specific.
|
|
||||||
It may work with a limited number of other exchanges, but this has not been tested yet.
|
|
||||||
"""
|
|
||||||
# Limit price threshold: As limit price should always be below stop-price
|
|
||||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
|
||||||
rate = stop_price * limit_price_pct
|
|
||||||
|
|
||||||
ordertype = "stop_loss_limit"
|
|
||||||
|
|
||||||
stop_price = self.price_to_precision(pair, stop_price)
|
|
||||||
|
|
||||||
# Ensure rate is less than stop price
|
|
||||||
if stop_price <= rate:
|
|
||||||
raise OperationalException(
|
|
||||||
'In stoploss limit order, stop price should be more than limit price')
|
|
||||||
|
|
||||||
if self._config['dry_run']:
|
|
||||||
dry_order = self.create_dry_run_order(
|
|
||||||
pair, ordertype, "sell", amount, stop_price)
|
|
||||||
return dry_order
|
|
||||||
|
|
||||||
try:
|
|
||||||
params = self._params.copy()
|
|
||||||
params.update({'stopPrice': stop_price})
|
|
||||||
|
|
||||||
amount = self.amount_to_precision(pair, amount)
|
|
||||||
|
|
||||||
rate = self.price_to_precision(pair, rate)
|
|
||||||
|
|
||||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
|
||||||
amount=amount, price=rate, params=params)
|
|
||||||
logger.info('stoploss limit order added for %s. '
|
|
||||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
|
||||||
self._log_exchange_response('create_stoploss_order', order)
|
|
||||||
return order
|
|
||||||
except ccxt.InsufficientFunds as e:
|
|
||||||
raise InsufficientFundsError(
|
|
||||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
|
||||||
f'Tried to sell amount {amount} at rate {rate}. '
|
|
||||||
f'Message: {e}') from e
|
|
||||||
except ccxt.InvalidOrder as e:
|
|
||||||
# Errors:
|
|
||||||
# `binance Order would trigger immediately.`
|
|
||||||
raise InvalidOrderException(
|
|
||||||
f'Could not create {ordertype} sell order on market {pair}. '
|
|
||||||
f'Tried to sell amount {amount} at rate {rate}. '
|
|
||||||
f'Message: {e}') from e
|
|
||||||
except ccxt.DDoSProtection as e:
|
|
||||||
raise DDosProtection(e) from e
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
|
||||||
raise TemporaryError(
|
|
||||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
|
||||||
except ccxt.BaseError as e:
|
|
||||||
raise OperationalException(e) from e
|
|
||||||
|
|
||||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int, is_new_pair: bool = False,
|
since_ms: int, is_new_pair: bool = False,
|
||||||
raise_: bool = False
|
raise_: bool = False
|
||||||
|
@ -600,7 +600,8 @@ class Exchange:
|
|||||||
# Dry-run methods
|
# Dry-run methods
|
||||||
|
|
||||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
rate: float, params: Dict = {},
|
||||||
|
stop_loss: bool = False) -> Dict[str, Any]:
|
||||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||||
_amount = self.amount_to_precision(pair, amount)
|
_amount = self.amount_to_precision(pair, amount)
|
||||||
dry_order: Dict[str, Any] = {
|
dry_order: Dict[str, Any] = {
|
||||||
@ -616,14 +617,17 @@ class Exchange:
|
|||||||
'remaining': _amount,
|
'remaining': _amount,
|
||||||
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'status': "closed" if ordertype == "market" else "open",
|
'status': "closed" if ordertype == "market" and not stop_loss else "open",
|
||||||
'fee': None,
|
'fee': None,
|
||||||
'info': {}
|
'info': {}
|
||||||
}
|
}
|
||||||
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
if stop_loss:
|
||||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||||
|
dry_order["stopPrice"] = dry_order["price"]
|
||||||
|
# Workaround to avoid filling stoploss orders immediately
|
||||||
|
dry_order["ft_order_type"] = "stoploss"
|
||||||
|
|
||||||
if dry_order["type"] == "market":
|
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
||||||
# Update market order pricing
|
# Update market order pricing
|
||||||
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
||||||
dry_order.update({
|
dry_order.update({
|
||||||
@ -714,7 +718,9 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Check dry-run limit order fill and update fee (if it filled).
|
Check dry-run limit order fill and update fee (if it filled).
|
||||||
"""
|
"""
|
||||||
if order['status'] != "closed" and order['type'] in ["limit"]:
|
if (order['status'] != "closed"
|
||||||
|
and order['type'] in ["limit"]
|
||||||
|
and not order.get('ft_order_type')):
|
||||||
pair = order['symbol']
|
pair = order['symbol']
|
||||||
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
||||||
order.update({
|
order.update({
|
||||||
@ -791,18 +797,89 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
|
||||||
|
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
params = self._params.copy()
|
||||||
|
# Verify if stopPrice works for your exchange!
|
||||||
|
params.update({'stopPrice': stop_price})
|
||||||
|
return params
|
||||||
|
|
||||||
|
@retrier(retries=0)
|
||||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
creates a stoploss order.
|
creates a stoploss order.
|
||||||
|
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
|
||||||
|
to the corresponding exchange type.
|
||||||
|
|
||||||
The precise ordertype is determined by the order_types dict or exchange default.
|
The precise ordertype is determined by the order_types dict or exchange default.
|
||||||
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
|
|
||||||
exchange's subclass.
|
|
||||||
The exception below should never raise, since we disallow
|
The exception below should never raise, since we disallow
|
||||||
starting the bot in validate_ordertypes()
|
starting the bot in validate_ordertypes()
|
||||||
Note: Changes to this interface need to be applied to all sub-classes too.
|
|
||||||
"""
|
|
||||||
|
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
This may work with a limited number of other exchanges, but correct working
|
||||||
|
needs to be tested individually.
|
||||||
|
WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange.
|
||||||
|
`stoploss_adjust` must still be implemented for this to work.
|
||||||
|
"""
|
||||||
|
if not self._ft_has['stoploss_on_exchange']:
|
||||||
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
|
||||||
|
user_order_type = order_types.get('stoploss', 'market')
|
||||||
|
if user_order_type in self._ft_has["stoploss_order_types"].keys():
|
||||||
|
ordertype = self._ft_has["stoploss_order_types"][user_order_type]
|
||||||
|
else:
|
||||||
|
# Otherwise pick only one available
|
||||||
|
ordertype = list(self._ft_has["stoploss_order_types"].values())[0]
|
||||||
|
user_order_type = list(self._ft_has["stoploss_order_types"].keys())[0]
|
||||||
|
|
||||||
|
stop_price_norm = self.price_to_precision(pair, stop_price)
|
||||||
|
rate = None
|
||||||
|
if user_order_type == 'limit':
|
||||||
|
# Limit price threshold: As limit price should always be below stop-price
|
||||||
|
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
|
rate = stop_price * limit_price_pct
|
||||||
|
|
||||||
|
# Ensure rate is less than stop price
|
||||||
|
if stop_price_norm <= rate:
|
||||||
|
raise OperationalException(
|
||||||
|
'In stoploss limit order, stop price should be more than limit price')
|
||||||
|
rate = self.price_to_precision(pair, rate)
|
||||||
|
|
||||||
|
if self._config['dry_run']:
|
||||||
|
dry_order = self.create_dry_run_order(
|
||||||
|
pair, ordertype, "sell", amount, stop_price_norm, stop_loss=True)
|
||||||
|
return dry_order
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm)
|
||||||
|
|
||||||
|
amount = self.amount_to_precision(pair, amount)
|
||||||
|
|
||||||
|
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||||
|
amount=amount, price=rate, params=params)
|
||||||
|
logger.info(f"stoploss {user_order_type} order added for {pair}. "
|
||||||
|
f"stop price: {stop_price}. limit: {rate}")
|
||||||
|
self._log_exchange_response('create_stoploss_order', order)
|
||||||
|
return order
|
||||||
|
except ccxt.InsufficientFunds as e:
|
||||||
|
raise InsufficientFundsError(
|
||||||
|
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||||
|
f'Tried to sell amount {amount} at rate {rate}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
# Errors:
|
||||||
|
# `Order would trigger immediately.`
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Could not create {ordertype} sell order on market {pair}. '
|
||||||
|
f'Tried to sell amount {amount} at rate {rate}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Could not place stoploss order due to {e.__class__.__name__}. "
|
||||||
|
f"Message: {e}") from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
||||||
@ -1587,7 +1664,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
|||||||
|
|
||||||
|
|
||||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||||
return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx']
|
return exchange_name in ['binance', 'bittrex', 'ftx', 'gateio', 'huobi', 'kraken', 'okx']
|
||||||
|
|
||||||
|
|
||||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||||
|
@ -56,7 +56,7 @@ class Ftx(Exchange):
|
|||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
pair, ordertype, "sell", amount, stop_price)
|
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
39
freqtrade/exchange/huobi.py
Normal file
39
freqtrade/exchange/huobi.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
""" Huobi exchange subclass """
|
||||||
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Huobi(Exchange):
|
||||||
|
"""
|
||||||
|
Huobi exchange class. Contains adjustments needed for Freqtrade to work
|
||||||
|
with this exchange.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ft_has: Dict = {
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "stop-limit"},
|
||||||
|
"ohlcv_candle_limit": 1000,
|
||||||
|
"l2_limit_range": [5, 10, 20],
|
||||||
|
"l2_limit_range_required": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
|
Returns True if adjustment is necessary.
|
||||||
|
"""
|
||||||
|
return order['type'] == 'stop' and stop_loss > float(order['stopPrice'])
|
||||||
|
|
||||||
|
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
|
||||||
|
params = self._params.copy()
|
||||||
|
params.update({
|
||||||
|
"stopPrice": stop_price,
|
||||||
|
"operator": "lte",
|
||||||
|
})
|
||||||
|
return params
|
@ -86,6 +86,8 @@ class Kraken(Exchange):
|
|||||||
"""
|
"""
|
||||||
Creates a stoploss market order.
|
Creates a stoploss market order.
|
||||||
Stoploss market orders is the only stoploss type supported by kraken.
|
Stoploss market orders is the only stoploss type supported by kraken.
|
||||||
|
TODO: investigate if this can be combined with generic implementation
|
||||||
|
(careful, prices are reversed)
|
||||||
"""
|
"""
|
||||||
params = self._params.copy()
|
params = self._params.copy()
|
||||||
|
|
||||||
@ -101,7 +103,7 @@ class Kraken(Exchange):
|
|||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
pair, ordertype, "sell", amount, stop_price)
|
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -19,8 +19,26 @@ class Kucoin(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||||
"l2_limit_range": [20, 100],
|
"l2_limit_range": [20, 100],
|
||||||
"l2_limit_range_required": False,
|
"l2_limit_range_required": False,
|
||||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||||
"time_in_force_parameter": "timeInForce",
|
"time_in_force_parameter": "timeInForce",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
|
Returns True if adjustment is necessary.
|
||||||
|
"""
|
||||||
|
return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice'])
|
||||||
|
|
||||||
|
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
|
||||||
|
params = self._params.copy()
|
||||||
|
params.update({
|
||||||
|
'stopPrice': stop_price,
|
||||||
|
'stop': 'loss'
|
||||||
|
})
|
||||||
|
return params
|
||||||
|
@ -542,7 +542,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
entry_tag=buy_tag):
|
entry_tag=buy_tag):
|
||||||
logger.info(f"User requested abortion of buying {pair}")
|
logger.info(f"User requested abortion of buying {pair}")
|
||||||
return False
|
return False
|
||||||
amount = self.exchange.amount_to_precision(pair, amount)
|
|
||||||
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
||||||
amount=amount, rate=enter_limit_requested,
|
amount=amount, rate=enter_limit_requested,
|
||||||
time_in_force=time_in_force)
|
time_in_force=time_in_force)
|
||||||
@ -900,7 +899,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
|
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
Check to see if stoploss on exchange should be updated
|
Check to see if stoploss on exchange should be updated
|
||||||
in case of trailing stoploss on exchange
|
in case of trailing stoploss on exchange
|
||||||
@ -979,10 +978,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
or (order_obj and self.strategy.ft_check_timed_out(
|
or (order_obj and self.strategy.ft_check_timed_out(
|
||||||
'sell', trade, order_obj, datetime.now(timezone.utc))
|
'sell', trade, order_obj, datetime.now(timezone.utc))
|
||||||
))):
|
))):
|
||||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
canceled = self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
canceled_count = trade.get_exit_order_count()
|
canceled_count = trade.get_exit_order_count()
|
||||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||||
if max_timeouts > 0 and canceled_count >= max_timeouts:
|
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||||
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
|
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
|
||||||
f'timed out {max_timeouts} times.')
|
f'timed out {max_timeouts} times.')
|
||||||
try:
|
try:
|
||||||
@ -1079,11 +1078,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
reason=reason)
|
reason=reason)
|
||||||
return was_trade_fully_canceled
|
return was_trade_fully_canceled
|
||||||
|
|
||||||
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str:
|
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Sell cancel - cancel order and update trade
|
Sell cancel - cancel order and update trade
|
||||||
:return: Reason for cancel
|
:return: True if exit order was cancelled, false otherwise
|
||||||
"""
|
"""
|
||||||
|
cancelled = False
|
||||||
# if trade is not partially completed, just cancel the order
|
# if trade is not partially completed, just cancel the order
|
||||||
if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
|
if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
|
||||||
if not self.exchange.check_order_canceled_empty(order):
|
if not self.exchange.check_order_canceled_empty(order):
|
||||||
@ -1094,7 +1094,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.update_order(co)
|
trade.update_order(co)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel sell order {trade.open_order_id}")
|
logger.exception(f"Could not cancel sell order {trade.open_order_id}")
|
||||||
return 'error cancelling order'
|
return False
|
||||||
logger.info('Sell order %s for %s.', reason, trade)
|
logger.info('Sell order %s for %s.', reason, trade)
|
||||||
else:
|
else:
|
||||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||||
@ -1109,9 +1109,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.sell_reason = None
|
trade.sell_reason = None
|
||||||
|
cancelled = True
|
||||||
else:
|
else:
|
||||||
# TODO: figure out how to handle partially complete sell orders
|
# TODO: figure out how to handle partially complete sell orders
|
||||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
|
cancelled = False
|
||||||
|
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
self._notify_exit_cancel(
|
self._notify_exit_cancel(
|
||||||
@ -1119,7 +1121,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order_type=self.strategy.order_types['sell'],
|
order_type=self.strategy.order_types['sell'],
|
||||||
reason=reason
|
reason=reason
|
||||||
)
|
)
|
||||||
return reason
|
return cancelled
|
||||||
|
|
||||||
def _safe_exit_amount(self, pair: str, amount: float) -> float:
|
def _safe_exit_amount(self, pair: str, amount: float) -> float:
|
||||||
"""
|
"""
|
||||||
@ -1168,8 +1170,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# if stoploss is on exchange and we are on dry_run mode,
|
# if stoploss is on exchange and we are on dry_run mode,
|
||||||
# we consider the sell price stop price
|
# we consider the sell price stop price
|
||||||
if self.config['dry_run'] and sell_type == 'stoploss' \
|
if (self.config['dry_run'] and sell_type == 'stoploss'
|
||||||
and self.strategy.order_types['stoploss_on_exchange']:
|
and self.strategy.order_types['stoploss_on_exchange']):
|
||||||
limit = trade.stop_loss
|
limit = trade.stop_loss
|
||||||
|
|
||||||
# set custom_exit_price if available
|
# set custom_exit_price if available
|
||||||
|
@ -195,6 +195,7 @@ class Order(_DECL_BASE):
|
|||||||
return {
|
return {
|
||||||
'amount': self.amount,
|
'amount': self.amount,
|
||||||
'average': round(self.average, 8) if self.average else 0,
|
'average': round(self.average, 8) if self.average else 0,
|
||||||
|
'safe_price': self.safe_price,
|
||||||
'cost': self.cost if self.cost else 0,
|
'cost': self.cost if self.cost else 0,
|
||||||
'filled': self.filled,
|
'filled': self.filled,
|
||||||
'ft_order_side': self.ft_order_side,
|
'ft_order_side': self.ft_order_side,
|
||||||
|
@ -8,7 +8,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc
|
|||||||
from freqtrade.enums import BacktestState
|
from freqtrade.enums import BacktestState
|
||||||
from freqtrade.exceptions import DependencyException
|
from freqtrade.exceptions import DependencyException
|
||||||
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
|
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
|
||||||
from freqtrade.rpc.api_server.deps import get_config
|
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
|
||||||
from freqtrade.rpc.api_server.webserver import ApiServer
|
from freqtrade.rpc.api_server.webserver import ApiServer
|
||||||
from freqtrade.rpc.rpc import RPCException
|
from freqtrade.rpc.rpc import RPCException
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ router = APIRouter()
|
|||||||
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||||
# flake8: noqa: C901
|
# flake8: noqa: C901
|
||||||
async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
|
async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
|
||||||
config=Depends(get_config)):
|
config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
|
||||||
"""Start backtesting if not done so already"""
|
"""Start backtesting if not done so already"""
|
||||||
if ApiServer._bgtask_running:
|
if ApiServer._bgtask_running:
|
||||||
raise RPCException('Bot Background task already running')
|
raise RPCException('Bot Background task already running')
|
||||||
@ -121,7 +121,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||||
def api_get_backtest():
|
def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
|
||||||
"""
|
"""
|
||||||
Get backtesting result.
|
Get backtesting result.
|
||||||
Returns Result after backtesting has been ran.
|
Returns Result after backtesting has been ran.
|
||||||
@ -157,7 +157,7 @@ def api_get_backtest():
|
|||||||
|
|
||||||
|
|
||||||
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||||
def api_delete_backtest():
|
def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
|
||||||
"""Reset backtesting"""
|
"""Reset backtesting"""
|
||||||
if ApiServer._bgtask_running:
|
if ApiServer._bgtask_running:
|
||||||
return {
|
return {
|
||||||
@ -183,7 +183,7 @@ def api_delete_backtest():
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||||
def api_backtest_abort():
|
def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
|
||||||
if not ApiServer._bgtask_running:
|
if not ApiServer._bgtask_running:
|
||||||
return {
|
return {
|
||||||
"status": "not_running",
|
"status": "not_running",
|
||||||
|
@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, Optional
|
|||||||
|
|
||||||
from fastapi import Depends
|
from fastapi import Depends
|
||||||
|
|
||||||
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc.rpc import RPC, RPCException
|
from freqtrade.rpc.rpc import RPC, RPCException
|
||||||
|
|
||||||
@ -38,3 +39,9 @@ def get_exchange(config=Depends(get_config)):
|
|||||||
ApiServer._exchange = ExchangeResolver.load_exchange(
|
ApiServer._exchange = ExchangeResolver.load_exchange(
|
||||||
config['exchange']['name'], config)
|
config['exchange']['name'], config)
|
||||||
return ApiServer._exchange
|
return ApiServer._exchange
|
||||||
|
|
||||||
|
|
||||||
|
def is_webserver_mode(config=Depends(get_config)):
|
||||||
|
if config['runmode'] != RunMode.WEBSERVER:
|
||||||
|
raise RPCException('Bot is not in the correct state')
|
||||||
|
return None
|
||||||
|
@ -370,46 +370,50 @@ class Telegram(RPCHandler):
|
|||||||
else:
|
else:
|
||||||
return "\N{CROSS MARK}"
|
return "\N{CROSS MARK}"
|
||||||
|
|
||||||
def _prepare_entry_details(self, filled_orders, base_currency, is_open):
|
def _prepare_entry_details(self, filled_orders: List, base_currency: str, is_open: bool):
|
||||||
"""
|
"""
|
||||||
Prepare details of trade with entry adjustment enabled
|
Prepare details of trade with entry adjustment enabled
|
||||||
"""
|
"""
|
||||||
lines = []
|
lines: List[str] = []
|
||||||
|
if len(filled_orders) > 0:
|
||||||
|
first_avg = filled_orders[0]["safe_price"]
|
||||||
|
|
||||||
for x, order in enumerate(filled_orders):
|
for x, order in enumerate(filled_orders):
|
||||||
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
||||||
cur_entry_amount = order["amount"]
|
cur_entry_amount = order["amount"]
|
||||||
cur_entry_average = order["average"]
|
cur_entry_average = order["safe_price"]
|
||||||
lines.append(" ")
|
lines.append(" ")
|
||||||
if x == 0:
|
if x == 0:
|
||||||
lines.append("*Entry #{}:*".format(x+1))
|
lines.append(f"*Entry #{x+1}:*")
|
||||||
lines.append("*Entry Amount:* {} ({:.8f} {})"
|
lines.append(
|
||||||
.format(cur_entry_amount, order["cost"], base_currency))
|
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
|
||||||
lines.append("*Average Entry Price:* {}".format(cur_entry_average))
|
lines.append(f"*Average Entry Price:* {cur_entry_average}")
|
||||||
else:
|
else:
|
||||||
sumA = 0
|
sumA = 0
|
||||||
sumB = 0
|
sumB = 0
|
||||||
for y in range(x):
|
for y in range(x):
|
||||||
sumA += (filled_orders[y]["amount"] * filled_orders[y]["average"])
|
sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"])
|
||||||
sumB += filled_orders[y]["amount"]
|
sumB += filled_orders[y]["amount"]
|
||||||
prev_avg_price = sumA/sumB
|
prev_avg_price = sumA / sumB
|
||||||
price_to_1st_entry = ((cur_entry_average - filled_orders[0]["average"])
|
price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
|
||||||
/ filled_orders[0]["average"])
|
minus_on_entry = 0
|
||||||
minus_on_entry = (cur_entry_average - prev_avg_price)/prev_avg_price
|
if prev_avg_price:
|
||||||
|
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
|
||||||
|
|
||||||
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"])
|
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"])
|
||||||
days = dur_entry.days
|
days = dur_entry.days
|
||||||
hours, remainder = divmod(dur_entry.seconds, 3600)
|
hours, remainder = divmod(dur_entry.seconds, 3600)
|
||||||
minutes, seconds = divmod(remainder, 60)
|
minutes, seconds = divmod(remainder, 60)
|
||||||
lines.append("*Entry #{}:* at {:.2%} avg profit".format(x+1, minus_on_entry))
|
lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit")
|
||||||
if is_open:
|
if is_open:
|
||||||
lines.append("({})".format(cur_entry_datetime
|
lines.append("({})".format(cur_entry_datetime
|
||||||
.humanize(granularity=["day", "hour", "minute"])))
|
.humanize(granularity=["day", "hour", "minute"])))
|
||||||
lines.append("*Entry Amount:* {} ({:.8f} {})"
|
lines.append(
|
||||||
.format(cur_entry_amount, order["cost"], base_currency))
|
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
|
||||||
lines.append("*Average Entry Price:* {} ({:.2%} from 1st entry rate)"
|
lines.append(f"*Average Entry Price:* {cur_entry_average} "
|
||||||
.format(cur_entry_average, price_to_1st_entry))
|
f"({price_to_1st_entry:.2%} from 1st entry rate)")
|
||||||
lines.append("*Order filled at:* {}".format(order["order_filled_date"]))
|
lines.append(f"*Order filled at:* {order['order_filled_date']}")
|
||||||
lines.append("({}d {}h {}m {}s from previous entry)"
|
lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)")
|
||||||
.format(days, hours, minutes, seconds))
|
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
|
12
freqtrade/templates/subtemplates/exchange_huobi.j2
Normal file
12
freqtrade/templates/subtemplates/exchange_huobi.j2
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"exchange": {
|
||||||
|
"name": "{{ exchange_name | lower }}",
|
||||||
|
"key": "{{ exchange_key }}",
|
||||||
|
"secret": "{{ exchange_secret }}",
|
||||||
|
"ccxt_config": {},
|
||||||
|
"ccxt_async_config": {},
|
||||||
|
"pair_whitelist": [
|
||||||
|
],
|
||||||
|
"pair_blacklist": [
|
||||||
|
"HT/.*"
|
||||||
|
]
|
||||||
|
}
|
@ -22,7 +22,7 @@ nbconvert==6.4.2
|
|||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==4.2.9
|
types-cachetools==4.2.9
|
||||||
types-filelock==3.2.5
|
types-filelock==3.2.5
|
||||||
types-requests==2.27.10
|
types-requests==2.27.11
|
||||||
types-tabulate==0.8.5
|
types-tabulate==0.8.5
|
||||||
|
|
||||||
# Extensions to datetime library
|
# Extensions to datetime library
|
||||||
|
@ -2,7 +2,7 @@ numpy==1.22.2
|
|||||||
pandas==1.4.1
|
pandas==1.4.1
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.73.70
|
ccxt==1.74.43
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==36.0.1
|
cryptography==36.0.1
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
@ -31,7 +31,7 @@ python-rapidjson==1.6
|
|||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.74.0
|
fastapi==0.74.1
|
||||||
uvicorn==0.17.5
|
uvicorn==0.17.5
|
||||||
pyjwt==2.3.0
|
pyjwt==2.3.0
|
||||||
aiofiles==0.8.0
|
aiofiles==0.8.0
|
||||||
|
2
setup.py
2
setup.py
@ -42,7 +42,7 @@ setup(
|
|||||||
],
|
],
|
||||||
install_requires=[
|
install_requires=[
|
||||||
# from requirements.txt
|
# from requirements.txt
|
||||||
'ccxt>=1.66.32',
|
'ccxt>=1.74.17',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
'python-telegram-bot>=13.4',
|
'python-telegram-bot>=13.4',
|
||||||
'arrow>=0.17.0',
|
'arrow>=0.17.0',
|
||||||
|
@ -59,6 +59,12 @@ EXCHANGES = {
|
|||||||
'hasQuoteVolume': True,
|
'hasQuoteVolume': True,
|
||||||
'timeframe': '5m',
|
'timeframe': '5m',
|
||||||
},
|
},
|
||||||
|
'huobi': {
|
||||||
|
'pair': 'BTC/USDT',
|
||||||
|
'stake_currency': 'USDT',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '5m',
|
||||||
|
},
|
||||||
'bitvavo': {
|
'bitvavo': {
|
||||||
'pair': 'BTC/EUR',
|
'pair': 'BTC/EUR',
|
||||||
'stake_currency': 'EUR',
|
'stake_currency': 'EUR',
|
||||||
@ -140,7 +146,10 @@ class TestCCXTExchange():
|
|||||||
else:
|
else:
|
||||||
next_limit = exchange.get_next_limit_in_list(
|
next_limit = exchange.get_next_limit_in_list(
|
||||||
val, l2_limit_range, l2_limit_range_required)
|
val, l2_limit_range, l2_limit_range_required)
|
||||||
if next_limit is None or next_limit > 200:
|
if next_limit is None:
|
||||||
|
assert len(l2['asks']) > 100
|
||||||
|
assert len(l2['asks']) > 100
|
||||||
|
elif next_limit > 200:
|
||||||
# Large orderbook sizes can be a problem for some exchanges (bitrex ...)
|
# Large orderbook sizes can be a problem for some exchanges (bitrex ...)
|
||||||
assert len(l2['asks']) > 200
|
assert len(l2['asks']) > 200
|
||||||
assert len(l2['asks']) > 200
|
assert len(l2['asks']) > 200
|
||||||
|
@ -166,7 +166,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
|||||||
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')
|
||||||
|
|
||||||
exchange = ExchangeResolver.load_exchange('huobi', default_conf)
|
exchange = ExchangeResolver.load_exchange('zaif', default_conf)
|
||||||
assert isinstance(exchange, Exchange)
|
assert isinstance(exchange, Exchange)
|
||||||
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
109
tests/exchange/test_huobi.py
Normal file
109
tests/exchange/test_huobi.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
from random import randint
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('limitratio,expected', [
|
||||||
|
(None, 220 * 0.99),
|
||||||
|
(0.99, 220 * 0.99),
|
||||||
|
(0.98, 220 * 0.98),
|
||||||
|
])
|
||||||
|
def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
order_type = 'stop-limit'
|
||||||
|
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio}
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
# Price should be 1% below stopprice
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['params'] == {"stopPrice": 220,
|
||||||
|
"operator": "lte",
|
||||||
|
}
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.create_order = MagicMock(
|
||||||
|
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi",
|
||||||
|
"stoploss", "create_order", retries=1,
|
||||||
|
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_dry_run_huobi(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_type = 'stop-limit'
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert 'type' in order
|
||||||
|
|
||||||
|
assert order['type'] == order_type
|
||||||
|
assert order['price'] == 220
|
||||||
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_adjust_huobi(mocker, default_conf):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='huobi')
|
||||||
|
order = {
|
||||||
|
'type': 'stop',
|
||||||
|
'price': 1500,
|
||||||
|
'stopPrice': '1500',
|
||||||
|
}
|
||||||
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
assert not exchange.stoploss_adjust(1499, order)
|
||||||
|
# Test with invalid order case
|
||||||
|
order['type'] = 'stop_loss'
|
||||||
|
assert not exchange.stoploss_adjust(1501, order)
|
120
tests/exchange/test_kucoin.py
Normal file
120
tests/exchange/test_kucoin.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
from random import randint
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('order_type', ['market', 'limit'])
|
||||||
|
@pytest.mark.parametrize('limitratio,expected', [
|
||||||
|
(None, 220 * 0.99),
|
||||||
|
(0.99, 220 * 0.99),
|
||||||
|
(0.98, 220 * 0.98),
|
||||||
|
])
|
||||||
|
def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, order_type):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
if order_type == 'limit':
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={
|
||||||
|
'stoploss': order_type,
|
||||||
|
'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
order_types = {'stoploss': order_type}
|
||||||
|
if limitratio is not None:
|
||||||
|
order_types.update({'stoploss_on_exchange_limit_ratio': limitratio})
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
# Price should be 1% below stopprice
|
||||||
|
if order_type == 'limit':
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
|
||||||
|
else:
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] is None
|
||||||
|
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['params'] == {
|
||||||
|
'stopPrice': 220,
|
||||||
|
'stop': 'loss'
|
||||||
|
}
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.create_order = MagicMock(
|
||||||
|
side_effect=ccxt.InvalidOrder("kucoin Order would trigger immediately."))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin",
|
||||||
|
"stoploss", "create_order", retries=1,
|
||||||
|
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_type = 'market'
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert 'type' in order
|
||||||
|
|
||||||
|
assert order['type'] == order_type
|
||||||
|
assert order['price'] == 220
|
||||||
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_adjust_kucoin(mocker, default_conf):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='kucoin')
|
||||||
|
order = {
|
||||||
|
'type': 'limit',
|
||||||
|
'price': 1500,
|
||||||
|
'stopPrice': 1500,
|
||||||
|
'info': {'stopPrice': 1500, 'stop': "limit"},
|
||||||
|
}
|
||||||
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
assert not exchange.stoploss_adjust(1499, order)
|
||||||
|
# Test with invalid order case
|
||||||
|
order['info']['stop'] = None
|
||||||
|
assert not exchange.stoploss_adjust(1501, order)
|
@ -79,7 +79,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'close_rate': None,
|
'close_rate': None,
|
||||||
'current_rate': 1.099e-05,
|
'current_rate': 1.099e-05,
|
||||||
'amount': 91.07468123,
|
'amount': 91.07468123,
|
||||||
'amount_requested': 91.07468123,
|
'amount_requested': 91.07468124,
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'trade_duration': None,
|
'trade_duration': None,
|
||||||
'trade_duration_s': None,
|
'trade_duration_s': None,
|
||||||
@ -110,7 +110,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'open_order': None,
|
'open_order': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'filled_entry_orders': [{
|
'filled_entry_orders': [{
|
||||||
'amount': 91.07468123, 'average': 1.098e-05,
|
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
||||||
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
||||||
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
||||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
||||||
@ -154,7 +154,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'close_rate': None,
|
'close_rate': None,
|
||||||
'current_rate': ANY,
|
'current_rate': ANY,
|
||||||
'amount': 91.07468123,
|
'amount': 91.07468123,
|
||||||
'amount_requested': 91.07468123,
|
'amount_requested': 91.07468124,
|
||||||
'trade_duration': ANY,
|
'trade_duration': ANY,
|
||||||
'trade_duration_s': ANY,
|
'trade_duration_s': ANY,
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
@ -185,7 +185,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'open_order': None,
|
'open_order': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'filled_entry_orders': [{
|
'filled_entry_orders': [{
|
||||||
'amount': 91.07468123, 'average': 1.098e-05,
|
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
||||||
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
||||||
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
||||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
||||||
|
@ -1350,6 +1350,11 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
|
|||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
|
||||||
|
|
||||||
|
rc = client_get(client, f"{BASE_URI}/backtest")
|
||||||
|
# Backtest prevented in default mode
|
||||||
|
assert_response(rc, 502)
|
||||||
|
|
||||||
|
ftbot.config['runmode'] = RunMode.WEBSERVER
|
||||||
# Backtesting not started yet
|
# Backtesting not started yet
|
||||||
rc = client_get(client, f"{BASE_URI}/backtest")
|
rc = client_get(client, f"{BASE_URI}/backtest")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
|
@ -236,6 +236,8 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
|
|||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
trade = trades[0]
|
trade = trades[0]
|
||||||
|
# Average may be empty on some exchanges
|
||||||
|
trade.orders[0].average = 0
|
||||||
trade.orders.append(Order(
|
trade.orders.append(Order(
|
||||||
order_id='5412vbb',
|
order_id='5412vbb',
|
||||||
ft_order_side='buy',
|
ft_order_side='buy',
|
||||||
@ -246,7 +248,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
|
|||||||
order_type="market",
|
order_type="market",
|
||||||
side="buy",
|
side="buy",
|
||||||
price=trade.open_rate * 0.95,
|
price=trade.open_rate * 0.95,
|
||||||
average=trade.open_rate * 0.95,
|
average=0,
|
||||||
filled=trade.amount,
|
filled=trade.amount,
|
||||||
remaining=0,
|
remaining=0,
|
||||||
cost=trade.amount,
|
cost=trade.amount,
|
||||||
|
@ -6,7 +6,7 @@ import time
|
|||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from math import isclose
|
from math import isclose
|
||||||
from typing import List
|
from typing import List
|
||||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
from unittest.mock import ANY, MagicMock, PropertyMock, patch
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
@ -727,7 +727,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt,
|
|||||||
call_args = buy_mm.call_args_list[0][1]
|
call_args = buy_mm.call_args_list[0][1]
|
||||||
assert call_args['pair'] == pair
|
assert call_args['pair'] == pair
|
||||||
assert call_args['rate'] == bid
|
assert call_args['rate'] == bid
|
||||||
assert call_args['amount'] == round(stake_amount / bid, 8)
|
assert call_args['amount'] == stake_amount / bid
|
||||||
buy_rate_mock.reset_mock()
|
buy_rate_mock.reset_mock()
|
||||||
|
|
||||||
# Should create an open trade with an open order id
|
# Should create an open trade with an open order id
|
||||||
@ -748,7 +748,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt,
|
|||||||
call_args = buy_mm.call_args_list[1][1]
|
call_args = buy_mm.call_args_list[1][1]
|
||||||
assert call_args['pair'] == pair
|
assert call_args['pair'] == pair
|
||||||
assert call_args['rate'] == fix_price
|
assert call_args['rate'] == fix_price
|
||||||
assert call_args['amount'] == round(stake_amount / fix_price, 8)
|
assert call_args['amount'] == stake_amount / fix_price
|
||||||
|
|
||||||
# In case of closed order
|
# In case of closed order
|
||||||
limit_buy_order_usdt['status'] = 'closed'
|
limit_buy_order_usdt['status'] = 'closed'
|
||||||
@ -1266,7 +1266,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee,
|
|||||||
|
|
||||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=27.39726027,
|
amount=pytest.approx(27.39726027),
|
||||||
pair='ETH/USDT',
|
pair='ETH/USDT',
|
||||||
order_types=freqtrade.strategy.order_types,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=4.4 * 0.95
|
stop_price=4.4 * 0.95
|
||||||
@ -1458,7 +1458,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
|||||||
|
|
||||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=31.57894736,
|
amount=pytest.approx(31.57894736),
|
||||||
pair='ETH/USDT',
|
pair='ETH/USDT',
|
||||||
order_types=freqtrade.strategy.order_types,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=4.4 * 0.96
|
stop_price=4.4 * 0.96
|
||||||
@ -1583,7 +1583,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee,
|
|||||||
assert trade.stop_loss == 4.4 * 0.99
|
assert trade.stop_loss == 4.4 * 0.99
|
||||||
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=11.41438356,
|
amount=pytest.approx(11.41438356),
|
||||||
pair='NEO/BTC',
|
pair='NEO/BTC',
|
||||||
order_types=freqtrade.strategy.order_types,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=4.4 * 0.99
|
stop_price=4.4 * 0.99
|
||||||
@ -2220,9 +2220,14 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l
|
|||||||
|
|
||||||
et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
|
et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
|
||||||
# 2nd canceled trade ...
|
# 2nd canceled trade ...
|
||||||
open_trade.open_order_id = limit_sell_order_old['id']
|
open_trade.open_order_id = limit_sell_order_old['id']
|
||||||
|
|
||||||
|
# If cancelling fails - no emergency sell!
|
||||||
|
with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False):
|
||||||
|
freqtrade.check_handle_timedout()
|
||||||
|
assert et_mock.call_count == 0
|
||||||
|
|
||||||
freqtrade.check_handle_timedout()
|
freqtrade.check_handle_timedout()
|
||||||
assert log_has_re('Emergencyselling trade.*', caplog)
|
assert log_has_re('Emergencyselling trade.*', caplog)
|
||||||
assert et_mock.call_count == 1
|
assert et_mock.call_count == 1
|
||||||
@ -2563,19 +2568,23 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
|||||||
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert send_msg_mock.call_count == 1
|
assert send_msg_mock.call_count == 1
|
||||||
assert trade.close_rate == None
|
assert trade.close_rate is None
|
||||||
assert trade.sell_reason is None
|
assert trade.sell_reason is None
|
||||||
|
|
||||||
send_msg_mock.reset_mock()
|
send_msg_mock.reset_mock()
|
||||||
|
|
||||||
order['amount'] = 2
|
order['amount'] = 2
|
||||||
assert freqtrade.handle_cancel_exit(trade, order, reason
|
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
|
||||||
# Assert cancel_order was not called (callcount remains unchanged)
|
# Assert cancel_order was not called (callcount remains unchanged)
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert send_msg_mock.call_count == 1
|
assert send_msg_mock.call_count == 1
|
||||||
assert freqtrade.handle_cancel_exit(trade, order, reason
|
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||||
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
== CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'])
|
||||||
|
|
||||||
|
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
|
|
||||||
|
send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
|
|
||||||
# Message should not be iterated again
|
# Message should not be iterated again
|
||||||
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||||
assert send_msg_mock.call_count == 1
|
assert send_msg_mock.call_count == 1
|
||||||
@ -2594,7 +2603,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
|||||||
order = {'remaining': 1,
|
order = {'remaining': 1,
|
||||||
'amount': 1,
|
'amount': 1,
|
||||||
'status': "open"}
|
'status': "open"}
|
||||||
assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order'
|
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||||
|
|
||||||
|
|
||||||
def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker
|
def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker
|
||||||
|
Loading…
Reference in New Issue
Block a user