Merge branch 'develop' into feat/short
This commit is contained in:
commit
488da9b875
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,8 @@
|
||||
# Freqtrade rules
|
||||
config*.json
|
||||
*.sqlite
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
logfile.txt
|
||||
user_data/*
|
||||
!user_data/strategy/sample_strategy.py
|
||||
|
17
README.md
17
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.
|
||||
|
||||
- [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] [FTX](https://ftx.com)
|
||||
- [X] [FTX](https://ftx.com/#a=2258149)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [Huobi](http://huobi.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)_
|
||||
|
||||
### Community tested
|
||||
@ -68,15 +69,9 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
||||
|
||||
## 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
|
||||
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/).
|
||||
For further (native) installation methods, please refer to the [Installation documentation page](https://www.freqtrade.io/en/stable/installation/).
|
||||
|
||||
## 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).
|
||||
|
||||
!!! 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
|
||||
|
||||
@ -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).
|
||||
|
||||
!!! 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
|
||||
|
||||
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.
|
||||
|
||||
## 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:
|
||||
|
||||
|
@ -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.
|
||||
|
||||
- [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] [FTX](https://ftx.com)
|
||||
- [X] [FTX](https://ftx.com/#a=2258149)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [Huobi](http://huobi.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)_
|
||||
|
||||
### Community tested
|
||||
|
@ -1,4 +1,4 @@
|
||||
mkdocs==1.2.3
|
||||
mkdocs-material==8.2.1
|
||||
mkdocs-material==8.2.3
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==9.2
|
||||
|
@ -24,7 +24,7 @@ These modes can be configured with these values:
|
||||
```
|
||||
|
||||
!!! 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>
|
||||
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.
|
||||
|
||||
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)
|
||||
|
||||
---
|
||||
|
@ -110,6 +110,7 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"bittrex",
|
||||
"ftx",
|
||||
"gateio",
|
||||
"huobi",
|
||||
"kraken",
|
||||
"kucoin",
|
||||
"okx",
|
||||
|
@ -25,12 +25,16 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
||||
RunMode.HYPEROPT: 'hyperoptimization',
|
||||
}
|
||||
if method in no_unlimited_runmodes.keys():
|
||||
wallet_size = config['dry_run_wallet'] * config['tradable_balance_ratio']
|
||||
# tradable_balance_ratio
|
||||
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
||||
and config['stake_amount'] > config['dry_run_wallet']):
|
||||
wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency'])
|
||||
and config['stake_amount'] > wallet_size):
|
||||
wallet = round_coin_value(wallet_size, config['stake_currency'])
|
||||
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
||||
raise OperationalException(f"Starting balance ({wallet}) "
|
||||
f"is smaller than stake_amount {stake}.")
|
||||
raise OperationalException(
|
||||
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
|
||||
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
@ -447,7 +447,6 @@ SCHEMA_TRADE_REQUIRED = [
|
||||
'dry_run_wallet',
|
||||
'ask_strategy',
|
||||
'bid_strategy',
|
||||
'unfilledtimeout',
|
||||
'stoploss',
|
||||
'minimal_roi',
|
||||
'internals',
|
||||
@ -463,7 +462,6 @@ SCHEMA_BACKTEST_REQUIRED = [
|
||||
'dry_run_wallet',
|
||||
'dataformat_ohlcv',
|
||||
'dataformat_trades',
|
||||
'unfilledtimeout',
|
||||
]
|
||||
|
||||
SCHEMA_MINIMAL_REQUIRED = [
|
||||
|
@ -18,6 +18,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
|
||||
from freqtrade.exchange.ftx import Ftx
|
||||
from freqtrade.exchange.gateio import Gateio
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.huobi import Huobi
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.kucoin import Kucoin
|
||||
from freqtrade.exchange.okx import Okx
|
||||
|
@ -6,13 +6,10 @@ from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
|
||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exceptions import (DDosProtection, OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -22,6 +19,7 @@ class Binance(Exchange):
|
||||
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
"ohlcv_candle_limit": 1000,
|
||||
@ -52,82 +50,6 @@ class Binance(Exchange):
|
||||
(side == "buy" and stop_loss < float(order['info']['stopPrice']))
|
||||
)
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float,
|
||||
order_types: Dict, side: str, leverage: float) -> 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.
|
||||
:param side: "buy" or "sell"
|
||||
"""
|
||||
# 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)
|
||||
if side == "sell":
|
||||
# TODO: Name limit_rate in other exchange subclasses
|
||||
rate = stop_price * limit_price_pct
|
||||
else:
|
||||
rate = stop_price * (2 - limit_price_pct)
|
||||
|
||||
ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit'
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
|
||||
bad_stop_price = (stop_price <= rate) if side == "sell" else (stop_price >= rate)
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
if bad_stop_price:
|
||||
raise OperationalException(
|
||||
'In stoploss limit order, stop price should be better than limit price')
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, side, amount, stop_price, leverage)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._params.copy()
|
||||
params.update({'stopPrice': stop_price})
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
params.update({'reduceOnly': True})
|
||||
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
rate = self.price_to_precision(pair, rate)
|
||||
|
||||
self._lev_prep(pair, leverage, side)
|
||||
order = self._api.create_order(
|
||||
symbol=pair,
|
||||
type=ordertype,
|
||||
side=side,
|
||||
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} {side} order on market {pair}. '
|
||||
f'Tried to {side} 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} {side} order on market {pair}. '
|
||||
f'Tried to {side} amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier
|
||||
def _set_leverage(
|
||||
self,
|
||||
|
@ -769,7 +769,8 @@ class Exchange:
|
||||
# Dry-run methods
|
||||
|
||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]:
|
||||
rate: float, leverage: float, params: Dict = {},
|
||||
stop_loss: bool = False) -> Dict[str, Any]:
|
||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||
_amount = self.amount_to_precision(pair, amount)
|
||||
dry_order: Dict[str, Any] = {
|
||||
@ -785,15 +786,18 @@ class Exchange:
|
||||
'remaining': _amount,
|
||||
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||
'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,
|
||||
'info': {},
|
||||
'leverage': leverage
|
||||
}
|
||||
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
||||
if stop_loss:
|
||||
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
|
||||
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
||||
dry_order.update({
|
||||
@ -884,7 +888,9 @@ class Exchange:
|
||||
"""
|
||||
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']
|
||||
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
||||
order.update({
|
||||
@ -1002,20 +1008,100 @@ class Exchange:
|
||||
"""
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float,
|
||||
order_types: Dict, side: str, leverage: float) -> Dict:
|
||||
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, side: str, leverage: float) -> Dict:
|
||||
"""
|
||||
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.
|
||||
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
|
||||
starting the bot in validate_ordertypes()
|
||||
Note: Changes to this interface need to be applied to all sub-classes too.
|
||||
"""
|
||||
|
||||
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,
|
||||
side,
|
||||
amount,
|
||||
stop_price_norm,
|
||||
stop_loss=True,
|
||||
leverage=leverage,
|
||||
)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm)
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
params['reduceOnly'] = True
|
||||
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
self._lev_prep(pair, leverage, side)
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||
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)
|
||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._config['dry_run']:
|
||||
@ -2384,7 +2470,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
||||
|
||||
|
||||
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]:
|
||||
|
@ -62,7 +62,7 @@ class Ftx(Exchange):
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, side, amount, stop_price, leverage)
|
||||
pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
|
||||
return dry_order
|
||||
|
||||
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
|
@ -99,6 +99,8 @@ class Kraken(Exchange):
|
||||
"""
|
||||
Creates a stoploss market order.
|
||||
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()
|
||||
if self.trading_mode == TradingMode.FUTURES:
|
||||
@ -119,7 +121,7 @@ class Kraken(Exchange):
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, side, amount, stop_price, leverage)
|
||||
pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
|
@ -19,8 +19,26 @@ class Kucoin(Exchange):
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
"l2_limit_range": [20, 100],
|
||||
"l2_limit_range_required": False,
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"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
|
||||
|
@ -1070,7 +1070,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
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
|
||||
in case of trailing stoploss on exchange
|
||||
@ -1142,15 +1142,19 @@ class FreqtradeBot(LoggingMixin):
|
||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||
|
||||
order_obj = trade.select_order_by_order_id(trade.open_order_id)
|
||||
|
||||
if not_closed and (fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
|
||||
time_method, trade, order_obj, datetime.now(timezone.utc)))
|
||||
):
|
||||
if is_entering:
|
||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||
else:
|
||||
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()
|
||||
if max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||
max_timeouts = self.config.get(
|
||||
'unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
|
||||
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
|
||||
f'timed out {max_timeouts} times.')
|
||||
try:
|
||||
@ -1252,11 +1256,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
reason=reason)
|
||||
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:
|
||||
"""
|
||||
exit order 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 order['remaining'] == order['amount'] or order.get('filled') == 0.0:
|
||||
if not self.exchange.check_order_canceled_empty(order):
|
||||
@ -1268,7 +1273,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
except InvalidOrderException:
|
||||
logger.exception(
|
||||
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
|
||||
return 'error cancelling order'
|
||||
return False
|
||||
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
|
||||
else:
|
||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||
@ -1282,9 +1287,12 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.close_date = None
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.sell_reason = None
|
||||
cancelled = True
|
||||
else:
|
||||
# TODO: figure out how to handle partially complete sell orders
|
||||
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
cancelled = False
|
||||
|
||||
self.wallets.update()
|
||||
self._notify_exit_cancel(
|
||||
@ -1292,7 +1300,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
order_type=self.strategy.order_types[trade.exit_side],
|
||||
reason=reason
|
||||
)
|
||||
return reason
|
||||
return cancelled
|
||||
|
||||
def _safe_exit_amount(self, pair: str, amount: float) -> float:
|
||||
"""
|
||||
@ -1351,8 +1359,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# if stoploss is on exchange and we are on dry_run mode,
|
||||
# we consider the sell price stop price
|
||||
if self.config['dry_run'] and exit_type == 'stoploss' \
|
||||
and self.strategy.order_types['stoploss_on_exchange']:
|
||||
if (self.config['dry_run'] and exit_type == 'stoploss'
|
||||
and self.strategy.order_types['stoploss_on_exchange']):
|
||||
limit = trade.stop_loss
|
||||
|
||||
# set custom_exit_price if available
|
||||
|
@ -121,7 +121,7 @@ class Order(_DECL_BASE):
|
||||
ft_pair: str = Column(String(25), nullable=False)
|
||||
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
|
||||
|
||||
order_id = Column(String(255), nullable=False, index=True)
|
||||
order_id: str = Column(String(255), nullable=False, index=True)
|
||||
status = Column(String(255), nullable=True)
|
||||
symbol = Column(String(25), nullable=True)
|
||||
order_type: str = Column(String(50), nullable=True)
|
||||
@ -199,8 +199,12 @@ class Order(_DECL_BASE):
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'pair': self.ft_pair,
|
||||
'order_id': self.order_id,
|
||||
'status': self.status,
|
||||
'amount': self.amount,
|
||||
'average': round(self.average, 8) if self.average else 0,
|
||||
'safe_price': self.safe_price,
|
||||
'cost': self.cost if self.cost else 0,
|
||||
'filled': self.filled,
|
||||
'ft_order_side': self.ft_order_side,
|
||||
@ -214,10 +218,8 @@ class Order(_DECL_BASE):
|
||||
'order_filled_timestamp': int(self.order_filled_date.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
||||
'order_type': self.order_type,
|
||||
'pair': self.ft_pair,
|
||||
'price': self.price,
|
||||
'remaining': self.remaining,
|
||||
'status': self.status,
|
||||
}
|
||||
|
||||
def close_bt_order(self, close_date: datetime):
|
||||
@ -456,14 +458,7 @@ class LocalTrade():
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
filled_orders = self.select_filled_orders()
|
||||
filled_entries = []
|
||||
filled_exits = []
|
||||
if len(filled_orders) > 0:
|
||||
for order in filled_orders:
|
||||
if order.ft_order_side == 'buy':
|
||||
filled_entries.append(order.to_json())
|
||||
if order.ft_order_side == 'sell':
|
||||
filled_exits.append(order.to_json())
|
||||
orders = [order.to_json() for order in filled_orders]
|
||||
|
||||
return {
|
||||
'trade_id': self.id,
|
||||
@ -535,8 +530,7 @@ class LocalTrade():
|
||||
'trading_mode': self.trading_mode,
|
||||
'funding_fees': self.funding_fees,
|
||||
'open_order_id': self.open_order_id,
|
||||
'filled_entry_orders': filled_entries,
|
||||
'filled_exit_orders': filled_exits,
|
||||
'orders': orders,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
@ -8,7 +8,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc
|
||||
from freqtrade.enums import BacktestState
|
||||
from freqtrade.exceptions import DependencyException
|
||||
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.rpc import RPCException
|
||||
|
||||
@ -22,7 +22,7 @@ router = APIRouter()
|
||||
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
|
||||
# flake8: noqa: C901
|
||||
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"""
|
||||
if ApiServer._bgtask_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'])
|
||||
def api_get_backtest():
|
||||
def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
|
||||
"""
|
||||
Get backtesting result.
|
||||
Returns Result after backtesting has been ran.
|
||||
@ -157,7 +157,7 @@ def api_get_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"""
|
||||
if ApiServer._bgtask_running:
|
||||
return {
|
||||
@ -183,7 +183,7 @@ def api_delete_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:
|
||||
return {
|
||||
"status": "not_running",
|
||||
|
@ -184,6 +184,22 @@ class ShowConfig(BaseModel):
|
||||
max_entry_position_adjustment: int
|
||||
|
||||
|
||||
class OrderSchema(BaseModel):
|
||||
pair: str
|
||||
order_id: str
|
||||
status: str
|
||||
remaining: float
|
||||
amount: float
|
||||
safe_price: float
|
||||
cost: float
|
||||
filled: float
|
||||
ft_order_side: str
|
||||
order_type: str
|
||||
is_open: bool
|
||||
order_timestamp: Optional[int]
|
||||
order_filled_timestamp: Optional[int]
|
||||
|
||||
|
||||
class TradeSchema(BaseModel):
|
||||
trade_id: int
|
||||
pair: str
|
||||
@ -233,6 +249,7 @@ class TradeSchema(BaseModel):
|
||||
min_rate: Optional[float]
|
||||
max_rate: Optional[float]
|
||||
open_order_id: Optional[str]
|
||||
orders: List[OrderSchema]
|
||||
|
||||
leverage: Optional[float]
|
||||
interest_rate: Optional[float]
|
||||
|
@ -34,8 +34,8 @@ logger = logging.getLogger(__name__)
|
||||
# 1.12: add blacklist delete endpoint
|
||||
# 1.13: forcebuy supports stake_amount
|
||||
# versions 2.xx -> futures/short branch
|
||||
# 2.13: addition of Forceenter
|
||||
API_VERSION = 2.13
|
||||
# 2.14: Add entry/exit orders to trade response
|
||||
API_VERSION = 2.14
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
|
@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, Optional
|
||||
|
||||
from fastapi import Depends
|
||||
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.persistence import Trade
|
||||
from freqtrade.rpc.rpc import RPC, RPCException
|
||||
|
||||
@ -38,3 +39,9 @@ def get_exchange(config=Depends(get_config)):
|
||||
ApiServer._exchange = ExchangeResolver.load_exchange(
|
||||
config['exchange']['name'], config)
|
||||
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
|
||||
|
@ -389,46 +389,52 @@ class Telegram(RPCHandler):
|
||||
else:
|
||||
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
|
||||
"""
|
||||
lines = []
|
||||
lines: List[str] = []
|
||||
if len(filled_orders) > 0:
|
||||
first_avg = filled_orders[0]["safe_price"]
|
||||
|
||||
for x, order in enumerate(filled_orders):
|
||||
if order['ft_order_side'] != 'buy':
|
||||
continue
|
||||
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
||||
cur_entry_amount = order["amount"]
|
||||
cur_entry_average = order["average"]
|
||||
cur_entry_average = order["safe_price"]
|
||||
lines.append(" ")
|
||||
if x == 0:
|
||||
lines.append("*Entry #{}:*".format(x+1))
|
||||
lines.append("*Entry Amount:* {} ({:.8f} {})"
|
||||
.format(cur_entry_amount, order["cost"], base_currency))
|
||||
lines.append("*Average Entry Price:* {}".format(cur_entry_average))
|
||||
lines.append(f"*Entry #{x+1}:*")
|
||||
lines.append(
|
||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
|
||||
lines.append(f"*Average Entry Price:* {cur_entry_average}")
|
||||
else:
|
||||
sumA = 0
|
||||
sumB = 0
|
||||
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"]
|
||||
prev_avg_price = sumA/sumB
|
||||
price_to_1st_entry = ((cur_entry_average - filled_orders[0]["average"])
|
||||
/ filled_orders[0]["average"])
|
||||
minus_on_entry = (cur_entry_average - prev_avg_price)/prev_avg_price
|
||||
prev_avg_price = sumA / sumB
|
||||
price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
|
||||
minus_on_entry = 0
|
||||
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"])
|
||||
days = dur_entry.days
|
||||
hours, remainder = divmod(dur_entry.seconds, 3600)
|
||||
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:
|
||||
lines.append("({})".format(cur_entry_datetime
|
||||
.humanize(granularity=["day", "hour", "minute"])))
|
||||
lines.append("*Entry Amount:* {} ({:.8f} {})"
|
||||
.format(cur_entry_amount, order["cost"], base_currency))
|
||||
lines.append("*Average Entry Price:* {} ({:.2%} from 1st entry rate)"
|
||||
.format(cur_entry_average, price_to_1st_entry))
|
||||
lines.append("*Order filled at:* {}".format(order["order_filled_date"]))
|
||||
lines.append("({}d {}h {}m {}s from previous entry)"
|
||||
.format(days, hours, minutes, seconds))
|
||||
lines.append(
|
||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
|
||||
lines.append(f"*Average Entry Price:* {cur_entry_average} "
|
||||
f"({price_to_1st_entry:.2%} from 1st entry rate)")
|
||||
lines.append(f"*Order filled at:* {order['order_filled_date']}")
|
||||
lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)")
|
||||
return lines
|
||||
|
||||
@authorized_only
|
||||
@ -459,7 +465,7 @@ class Telegram(RPCHandler):
|
||||
messages = []
|
||||
for r in results:
|
||||
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
|
||||
r['num_entries'] = len(r['filled_entry_orders'])
|
||||
r['num_entries'] = len([o for o in r['orders'] if o['ft_order_side'] == 'buy'])
|
||||
r['sell_reason'] = r.get('sell_reason', "")
|
||||
lines = [
|
||||
"*Trade ID:* `{trade_id}`" +
|
||||
@ -505,8 +511,8 @@ class Telegram(RPCHandler):
|
||||
lines.append("*Open Order:* `{open_order}`")
|
||||
|
||||
lines_detail = self._prepare_entry_details(
|
||||
r['filled_entry_orders'], r['base_currency'], r['is_open'])
|
||||
lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else ""))
|
||||
r['orders'], r['base_currency'], r['is_open'])
|
||||
lines.extend(lines_detail if lines_detail else "")
|
||||
|
||||
# Filter empty lines using list-comprehension
|
||||
messages.append("\n".join([line for line in lines if line]).format(**r))
|
||||
|
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
|
||||
types-cachetools==4.2.9
|
||||
types-filelock==3.2.5
|
||||
types-requests==2.27.10
|
||||
types-requests==2.27.11
|
||||
types-tabulate==0.8.5
|
||||
|
||||
# Extensions to datetime library
|
||||
|
@ -2,7 +2,7 @@ numpy==1.22.2
|
||||
pandas==1.4.1
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==1.74.22
|
||||
ccxt==1.74.63
|
||||
# Pin cryptography for now due to rust build errors with piwheels
|
||||
cryptography==36.0.1
|
||||
aiohttp==3.8.1
|
||||
@ -31,7 +31,7 @@ python-rapidjson==1.6
|
||||
sdnotify==0.3.2
|
||||
|
||||
# API Server
|
||||
fastapi==0.74.0
|
||||
fastapi==0.74.1
|
||||
uvicorn==0.17.5
|
||||
pyjwt==2.3.0
|
||||
aiofiles==0.8.0
|
||||
|
2
setup.py
2
setup.py
@ -42,7 +42,7 @@ setup(
|
||||
],
|
||||
install_requires=[
|
||||
# from requirements.txt
|
||||
'ccxt>=1.73.1',
|
||||
'ccxt>=1.74.17',
|
||||
'SQLAlchemy',
|
||||
'python-telegram-bot>=13.4',
|
||||
'arrow>=0.17.0',
|
||||
|
3
setup.sh
3
setup.sh
@ -132,6 +132,9 @@ function install_macos() {
|
||||
echo_block "Installing Brew"
|
||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
fi
|
||||
|
||||
brew install gettext
|
||||
|
||||
#Gets number after decimal in python version
|
||||
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
||||
|
||||
|
@ -82,6 +82,12 @@ EXCHANGES = {
|
||||
'leverage_tiers_public': True,
|
||||
'leverage_in_spot_market': True,
|
||||
},
|
||||
'huobi': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'hasQuoteVolume': True,
|
||||
'timeframe': '5m',
|
||||
},
|
||||
'bitvavo': {
|
||||
'pair': 'BTC/EUR',
|
||||
'stake_currency': 'EUR',
|
||||
@ -198,7 +204,10 @@ class TestCCXTExchange():
|
||||
else:
|
||||
next_limit = exchange.get_next_limit_in_list(
|
||||
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 ...)
|
||||
assert len(l2['asks']) > 200
|
||||
assert len(l2['asks']) > 200
|
||||
|
@ -168,7 +168,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||
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 log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
||||
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)
|
@ -80,7 +80,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'close_rate': None,
|
||||
'current_rate': 1.099e-05,
|
||||
'amount': 91.07468123,
|
||||
'amount_requested': 91.07468123,
|
||||
'amount_requested': 91.07468124,
|
||||
'stake_amount': 0.001,
|
||||
'trade_duration': None,
|
||||
'trade_duration_s': None,
|
||||
@ -116,14 +116,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'is_short': False,
|
||||
'funding_fees': 0.0,
|
||||
'trading_mode': TradingMode.SPOT,
|
||||
'filled_entry_orders': [{
|
||||
'amount': 91.07468123, 'average': 1.098e-05,
|
||||
# 'filled_entery_orders': [{
|
||||
'orders': [{
|
||||
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
||||
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
||||
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
||||
'is_open': False, 'pair': 'ETH/BTC',
|
||||
'remaining': ANY, 'status': ANY}],
|
||||
'filled_exit_orders': [],
|
||||
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
||||
'remaining': ANY, 'status': ANY
|
||||
}],
|
||||
# 'filled_exit_orders': [],
|
||||
}
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||
@ -162,7 +164,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'close_rate': None,
|
||||
'current_rate': ANY,
|
||||
'amount': 91.07468123,
|
||||
'amount_requested': 91.07468123,
|
||||
'amount_requested': 91.07468124,
|
||||
'trade_duration': ANY,
|
||||
'trade_duration_s': ANY,
|
||||
'stake_amount': 0.001,
|
||||
@ -198,14 +200,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'is_short': False,
|
||||
'funding_fees': 0.0,
|
||||
'trading_mode': TradingMode.SPOT,
|
||||
'filled_entry_orders': [{
|
||||
'amount': 91.07468123, 'average': 1.098e-05,
|
||||
# 'filled_entry_orders': [{
|
||||
'orders': [{
|
||||
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
||||
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
||||
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
||||
'is_open': False, 'pair': 'ETH/BTC',
|
||||
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
||||
'remaining': ANY, 'status': ANY}],
|
||||
'filled_exit_orders': [],
|
||||
# 'filled_exit_orders': [],
|
||||
}
|
||||
|
||||
|
||||
|
@ -971,6 +971,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
||||
'interest_rate': 0.0,
|
||||
'funding_fees': None,
|
||||
'trading_mode': ANY,
|
||||
'orders': [ANY],
|
||||
}
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||
@ -1170,6 +1171,7 @@ def test_api_forceentry(botclient, mocker, fee, endpoint):
|
||||
'interest_rate': None,
|
||||
'funding_fees': None,
|
||||
'trading_mode': 'spot',
|
||||
'orders': [],
|
||||
}
|
||||
|
||||
|
||||
@ -1452,6 +1454,11 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
|
||||
ftbot, client = botclient
|
||||
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
|
||||
rc = client_get(client, f"{BASE_URI}/backtest")
|
||||
assert_response(rc)
|
||||
|
@ -208,6 +208,7 @@ def test_telegram_status(default_conf, update, mocker) -> None:
|
||||
'is_open': True,
|
||||
'is_short': False,
|
||||
'filled_entry_orders': [],
|
||||
'orders': []
|
||||
}]),
|
||||
)
|
||||
|
||||
@ -240,6 +241,8 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
|
||||
create_mock_trades(fee)
|
||||
trades = Trade.get_open_trades()
|
||||
trade = trades[0]
|
||||
# Average may be empty on some exchanges
|
||||
trade.orders[0].average = 0
|
||||
trade.orders.append(Order(
|
||||
order_id='5412vbb',
|
||||
ft_order_side='buy',
|
||||
@ -250,7 +253,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
|
||||
order_type="market",
|
||||
side="buy",
|
||||
price=trade.open_rate * 0.95,
|
||||
average=trade.open_rate * 0.95,
|
||||
average=0,
|
||||
filled=trade.amount,
|
||||
remaining=0,
|
||||
cost=trade.amount,
|
||||
@ -1872,7 +1875,7 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) ->
|
||||
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_01', 1.0),
|
||||
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_02', 2.0),
|
||||
(RPCMessageType.SHORT_FILL, 'Shorted', 'short_signal_01', 2.0),
|
||||
])
|
||||
])
|
||||
def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, entered,
|
||||
enter_signal, leverage) -> None:
|
||||
|
||||
@ -2142,7 +2145,7 @@ def test_send_msg_buy_notification_no_fiat(
|
||||
('Long', 'long_signal_01', 1.0),
|
||||
('Long', 'long_signal_01', 5.0),
|
||||
('Short', 'short_signal_01', 2.0),
|
||||
])
|
||||
])
|
||||
def test_send_msg_sell_notification_no_fiat(
|
||||
default_conf, mocker, direction, enter_signal, leverage) -> None:
|
||||
del default_conf['fiat_display_currency']
|
||||
|
@ -6,7 +6,7 @@ import time
|
||||
from copy import deepcopy
|
||||
from math import isclose
|
||||
from typing import List
|
||||
from unittest.mock import ANY, MagicMock, PropertyMock
|
||||
from unittest.mock import ANY, MagicMock, PropertyMock, patch
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
@ -1407,7 +1407,7 @@ def test_handle_stoploss_on_exchange_trailing(
|
||||
|
||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
||||
stoploss_order_mock.assert_called_once_with(
|
||||
amount=amt,
|
||||
amount=pytest.approx(amt),
|
||||
pair='ETH/USDT',
|
||||
order_types=freqtrade.strategy.order_types,
|
||||
stop_price=stop_price[1],
|
||||
@ -1611,7 +1611,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
||||
# Long uses modified ask - offset, short modified bid + offset
|
||||
stoploss_order_mock.assert_called_once_with(
|
||||
amount=trade.amount,
|
||||
amount=pytest.approx(trade.amount),
|
||||
pair='ETH/USDT',
|
||||
order_types=freqtrade.strategy.order_types,
|
||||
stop_price=4.4 * 0.96 if not is_short else 0.95 * 1.04,
|
||||
@ -1741,7 +1741,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||
assert trade.stop_loss == 4.4 * 0.99
|
||||
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
||||
stoploss_order_mock.assert_called_once_with(
|
||||
amount=11.41438356,
|
||||
amount=pytest.approx(11.41438356),
|
||||
pair='NEO/BTC',
|
||||
order_types=freqtrade.strategy.order_types,
|
||||
stop_price=4.4 * 0.99,
|
||||
@ -2534,9 +2534,14 @@ def test_check_handle_timedout_sell_usercustom(
|
||||
|
||||
et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
|
||||
caplog.clear()
|
||||
|
||||
# 2nd canceled trade ...
|
||||
open_trade_usdt.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()
|
||||
assert log_has_re('Emergencyselling trade.*', caplog)
|
||||
assert et_mock.call_count == 1
|
||||
@ -2896,9 +2901,12 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
||||
exchange='binance',
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456",
|
||||
open_date=arrow.utcnow().datetime,
|
||||
open_date=arrow.utcnow().shift(days=-2).datetime,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
close_rate=0.555,
|
||||
close_date=arrow.utcnow().datetime,
|
||||
sell_reason="sell_reason_whatever",
|
||||
)
|
||||
order = {'remaining': 1,
|
||||
'amount': 1,
|
||||
@ -2907,17 +2915,23 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
||||
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert send_msg_mock.call_count == 1
|
||||
assert trade.close_rate is None
|
||||
assert trade.sell_reason is None
|
||||
|
||||
send_msg_mock.reset_mock()
|
||||
|
||||
order['amount'] = 2
|
||||
assert freqtrade.handle_cancel_exit(trade, order, reason
|
||||
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
# Assert cancel_order was not called (callcount remains unchanged)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert send_msg_mock.call_count == 1
|
||||
assert freqtrade.handle_cancel_exit(trade, order, reason
|
||||
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
assert (send_msg_mock.call_args_list[0][0][0]['reason']
|
||||
== 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
|
||||
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
|
||||
assert send_msg_mock.call_count == 1
|
||||
@ -2936,7 +2950,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
|
||||
order = {'remaining': 1,
|
||||
'amount': 1,
|
||||
'status': "open"}
|
||||
assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order'
|
||||
assert not freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_short, open_rate, amt", [
|
||||
|
@ -1610,6 +1610,7 @@ def test_to_json(default_conf, fee):
|
||||
'funding_fees': None,
|
||||
'filled_entry_orders': [],
|
||||
'filled_exit_orders': [],
|
||||
'orders': [],
|
||||
}
|
||||
|
||||
# Simulate dry_run entries
|
||||
@ -1686,6 +1687,7 @@ def test_to_json(default_conf, fee):
|
||||
'funding_fees': None,
|
||||
'filled_entry_orders': [],
|
||||
'filled_exit_orders': [],
|
||||
'orders': [],
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user