Merge pull request #6492 from samgermain/feat/short
Merge develop into feat/short
This commit is contained in:
commit
0bac7f824e
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
|
||||||
|
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.
|
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/#a=2258149)
|
||||||
- [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/#a=2258149)
|
||||||
- [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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
@ -110,6 +110,7 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"bittrex",
|
"bittrex",
|
||||||
"ftx",
|
"ftx",
|
||||||
"gateio",
|
"gateio",
|
||||||
|
"huobi",
|
||||||
"kraken",
|
"kraken",
|
||||||
"kucoin",
|
"kucoin",
|
||||||
"okx",
|
"okx",
|
||||||
|
@ -25,12 +25,16 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
|||||||
RunMode.HYPEROPT: 'hyperoptimization',
|
RunMode.HYPEROPT: 'hyperoptimization',
|
||||||
}
|
}
|
||||||
if method in no_unlimited_runmodes.keys():
|
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
|
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
||||||
and config['stake_amount'] > config['dry_run_wallet']):
|
and config['stake_amount'] > wallet_size):
|
||||||
wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency'])
|
wallet = round_coin_value(wallet_size, config['stake_currency'])
|
||||||
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
||||||
raise OperationalException(f"Starting balance ({wallet}) "
|
raise OperationalException(
|
||||||
f"is smaller than stake_amount {stake}.")
|
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
|
||||||
|
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
|
||||||
|
)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
@ -447,7 +447,6 @@ SCHEMA_TRADE_REQUIRED = [
|
|||||||
'dry_run_wallet',
|
'dry_run_wallet',
|
||||||
'ask_strategy',
|
'ask_strategy',
|
||||||
'bid_strategy',
|
'bid_strategy',
|
||||||
'unfilledtimeout',
|
|
||||||
'stoploss',
|
'stoploss',
|
||||||
'minimal_roi',
|
'minimal_roi',
|
||||||
'internals',
|
'internals',
|
||||||
@ -463,7 +462,6 @@ SCHEMA_BACKTEST_REQUIRED = [
|
|||||||
'dry_run_wallet',
|
'dry_run_wallet',
|
||||||
'dataformat_ohlcv',
|
'dataformat_ohlcv',
|
||||||
'dataformat_trades',
|
'dataformat_trades',
|
||||||
'unfilledtimeout',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
SCHEMA_MINIMAL_REQUIRED = [
|
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.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
|
||||||
|
@ -9,8 +9,7 @@ import arrow
|
|||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
from freqtrade.enums import CandleType, MarginMode, TradingMode
|
||||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
|
||||||
OperationalException, TemporaryError)
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
from freqtrade.exchange.common import retrier
|
||||||
|
|
||||||
@ -22,6 +21,8 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||||
|
"stoploss_order_types_futures": {"limit": "stop"},
|
||||||
"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,
|
||||||
@ -52,82 +53,6 @@ class Binance(Exchange):
|
|||||||
(side == "buy" and stop_loss < float(order['info']['stopPrice']))
|
(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
|
@retrier
|
||||||
def _set_leverage(
|
def _set_leverage(
|
||||||
self,
|
self,
|
||||||
|
@ -769,7 +769,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, 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()}'
|
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] = {
|
||||||
@ -785,15 +786,18 @@ 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': {},
|
||||||
'leverage': leverage
|
'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["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({
|
||||||
@ -884,7 +888,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({
|
||||||
@ -1002,20 +1008,121 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
|
||||||
def stoploss(self, pair: str, amount: float, stop_price: float,
|
def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
|
||||||
order_types: Dict, side: str, leverage: float) -> Dict:
|
|
||||||
|
available_order_Types: Dict[str, str] = self._ft_has["stoploss_order_types"]
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
# Optionally use different order type for stop order
|
||||||
|
available_order_Types = self._ft_has.get('stoploss_order_types_futures',
|
||||||
|
self._ft_has["stoploss_order_types"])
|
||||||
|
|
||||||
|
if user_order_type in available_order_Types.keys():
|
||||||
|
ordertype = available_order_Types[user_order_type]
|
||||||
|
else:
|
||||||
|
# Otherwise pick only one available
|
||||||
|
ordertype = list(available_order_Types.values())[0]
|
||||||
|
user_order_type = list(available_order_Types.keys())[0]
|
||||||
|
return ordertype, user_order_type
|
||||||
|
|
||||||
|
def _get_stop_limit_rate(self, stop_price: float, order_types: Dict, side: str) -> float:
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
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 more than limit price')
|
||||||
|
return rate
|
||||||
|
|
||||||
|
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.
|
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.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
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}.")
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
|
||||||
|
user_order_type = order_types.get('stoploss', 'market')
|
||||||
|
ordertype, user_order_type = self._get_stop_order_type(user_order_type)
|
||||||
|
|
||||||
|
stop_price_norm = self.price_to_precision(pair, stop_price)
|
||||||
|
rate = None
|
||||||
|
if user_order_type == 'limit':
|
||||||
|
rate = self._get_stop_limit_rate(stop_price, order_types, side)
|
||||||
|
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)
|
@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:
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
@ -2384,7 +2491,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]:
|
||||||
|
@ -62,7 +62,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, side, amount, stop_price, leverage)
|
pair, ordertype, side, amount, stop_price, leverage, 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, side: str) -> 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.
|
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()
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
@ -119,7 +121,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, side, amount, stop_price, leverage)
|
pair, ordertype, side, amount, stop_price, leverage, 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, side: str) -> 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
|
||||||
|
@ -639,7 +639,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
entry_tag=enter_tag, side=trade_side):
|
entry_tag=enter_tag, side=trade_side):
|
||||||
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(
|
order = self.exchange.create_order(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
ordertype=order_type,
|
ordertype=order_type,
|
||||||
@ -1070,7 +1069,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
|
||||||
@ -1142,15 +1141,19 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
|
||||||
|
|
||||||
order_obj = trade.select_order_by_order_id(trade.open_order_id)
|
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(
|
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)))
|
time_method, trade, order_obj, datetime.now(timezone.utc)))
|
||||||
):
|
):
|
||||||
if is_entering:
|
if is_entering:
|
||||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
else:
|
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()
|
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 '
|
logger.warning(f'Emergencyselling trade {trade}, as the sell order '
|
||||||
f'timed out {max_timeouts} times.')
|
f'timed out {max_timeouts} times.')
|
||||||
try:
|
try:
|
||||||
@ -1252,11 +1255,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:
|
||||||
"""
|
"""
|
||||||
exit order cancel - cancel order and update trade
|
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 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):
|
||||||
@ -1268,7 +1272,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
|
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)
|
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
|
||||||
else:
|
else:
|
||||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||||
@ -1282,9 +1286,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.close_date = None
|
trade.close_date = None
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = None
|
trade.open_order_id = 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(
|
||||||
@ -1292,7 +1299,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order_type=self.strategy.order_types[trade.exit_side],
|
order_type=self.strategy.order_types[trade.exit_side],
|
||||||
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:
|
||||||
"""
|
"""
|
||||||
@ -1351,8 +1358,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 exit_type == 'stoploss' \
|
if (self.config['dry_run'] and exit_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
|
||||||
|
@ -121,7 +121,7 @@ class Order(_DECL_BASE):
|
|||||||
ft_pair: str = Column(String(25), nullable=False)
|
ft_pair: str = Column(String(25), nullable=False)
|
||||||
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
|
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)
|
status = Column(String(255), nullable=True)
|
||||||
symbol = Column(String(25), nullable=True)
|
symbol = Column(String(25), nullable=True)
|
||||||
order_type: str = Column(String(50), nullable=True)
|
order_type: str = Column(String(50), nullable=True)
|
||||||
@ -197,10 +197,14 @@ class Order(_DECL_BASE):
|
|||||||
self.order_filled_date = datetime.now(timezone.utc)
|
self.order_filled_date = datetime.now(timezone.utc)
|
||||||
self.order_update_date = datetime.now(timezone.utc)
|
self.order_update_date = datetime.now(timezone.utc)
|
||||||
|
|
||||||
def to_json(self) -> Dict[str, Any]:
|
def to_json(self, entry_side: str) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
|
'pair': self.ft_pair,
|
||||||
|
'order_id': self.order_id,
|
||||||
|
'status': self.status,
|
||||||
'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,
|
||||||
@ -214,10 +218,9 @@ class Order(_DECL_BASE):
|
|||||||
'order_filled_timestamp': int(self.order_filled_date.replace(
|
'order_filled_timestamp': int(self.order_filled_date.replace(
|
||||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
||||||
'order_type': self.order_type,
|
'order_type': self.order_type,
|
||||||
'pair': self.ft_pair,
|
|
||||||
'price': self.price,
|
'price': self.price,
|
||||||
|
'ft_is_entry': self.ft_order_side == entry_side,
|
||||||
'remaining': self.remaining,
|
'remaining': self.remaining,
|
||||||
'status': self.status,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def close_bt_order(self, close_date: datetime):
|
def close_bt_order(self, close_date: datetime):
|
||||||
@ -456,14 +459,7 @@ class LocalTrade():
|
|||||||
|
|
||||||
def to_json(self) -> Dict[str, Any]:
|
def to_json(self) -> Dict[str, Any]:
|
||||||
filled_orders = self.select_filled_orders()
|
filled_orders = self.select_filled_orders()
|
||||||
filled_entries = []
|
orders = [order.to_json(self.enter_side) for order in filled_orders]
|
||||||
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())
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'trade_id': self.id,
|
'trade_id': self.id,
|
||||||
@ -535,8 +531,7 @@ class LocalTrade():
|
|||||||
'trading_mode': self.trading_mode,
|
'trading_mode': self.trading_mode,
|
||||||
'funding_fees': self.funding_fees,
|
'funding_fees': self.funding_fees,
|
||||||
'open_order_id': self.open_order_id,
|
'open_order_id': self.open_order_id,
|
||||||
'filled_entry_orders': filled_entries,
|
'orders': orders,
|
||||||
'filled_exit_orders': filled_exits,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -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",
|
||||||
|
@ -184,6 +184,22 @@ class ShowConfig(BaseModel):
|
|||||||
max_entry_position_adjustment: int
|
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):
|
class TradeSchema(BaseModel):
|
||||||
trade_id: int
|
trade_id: int
|
||||||
pair: str
|
pair: str
|
||||||
@ -233,6 +249,7 @@ class TradeSchema(BaseModel):
|
|||||||
min_rate: Optional[float]
|
min_rate: Optional[float]
|
||||||
max_rate: Optional[float]
|
max_rate: Optional[float]
|
||||||
open_order_id: Optional[str]
|
open_order_id: Optional[str]
|
||||||
|
orders: List[OrderSchema]
|
||||||
|
|
||||||
leverage: Optional[float]
|
leverage: Optional[float]
|
||||||
interest_rate: Optional[float]
|
interest_rate: Optional[float]
|
||||||
|
@ -34,8 +34,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# 1.12: add blacklist delete endpoint
|
# 1.12: add blacklist delete endpoint
|
||||||
# 1.13: forcebuy supports stake_amount
|
# 1.13: forcebuy supports stake_amount
|
||||||
# versions 2.xx -> futures/short branch
|
# versions 2.xx -> futures/short branch
|
||||||
# 2.13: addition of Forceenter
|
# 2.14: Add entry/exit orders to trade response
|
||||||
API_VERSION = 2.13
|
API_VERSION = 2.14
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
|
@ -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
|
||||||
|
@ -171,7 +171,7 @@ class RPC:
|
|||||||
# calculate profit and send message to user
|
# calculate profit and send message to user
|
||||||
if trade.is_open:
|
if trade.is_open:
|
||||||
try:
|
try:
|
||||||
closing_side = "buy" if trade.is_short else "sell"
|
closing_side = trade.exit_side
|
||||||
current_rate = self._freqtrade.exchange.get_rate(
|
current_rate = self._freqtrade.exchange.get_rate(
|
||||||
trade.pair, refresh=False, side=closing_side)
|
trade.pair, refresh=False, side=closing_side)
|
||||||
except (ExchangeError, PricingError):
|
except (ExchangeError, PricingError):
|
||||||
|
@ -389,46 +389,52 @@ 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):
|
||||||
|
if not order['ft_is_entry']:
|
||||||
|
continue
|
||||||
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
|
||||||
@ -459,7 +465,7 @@ class Telegram(RPCHandler):
|
|||||||
messages = []
|
messages = []
|
||||||
for r in results:
|
for r in results:
|
||||||
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
|
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_is_entry']])
|
||||||
r['sell_reason'] = r.get('sell_reason', "")
|
r['sell_reason'] = r.get('sell_reason', "")
|
||||||
lines = [
|
lines = [
|
||||||
"*Trade ID:* `{trade_id}`" +
|
"*Trade ID:* `{trade_id}`" +
|
||||||
@ -505,8 +511,8 @@ class Telegram(RPCHandler):
|
|||||||
lines.append("*Open Order:* `{open_order}`")
|
lines.append("*Open Order:* `{open_order}`")
|
||||||
|
|
||||||
lines_detail = self._prepare_entry_details(
|
lines_detail = self._prepare_entry_details(
|
||||||
r['filled_entry_orders'], r['base_currency'], r['is_open'])
|
r['orders'], r['base_currency'], r['is_open'])
|
||||||
lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else ""))
|
lines.extend(lines_detail if lines_detail else "")
|
||||||
|
|
||||||
# Filter empty lines using list-comprehension
|
# Filter empty lines using list-comprehension
|
||||||
messages.append("\n".join([line for line in lines if line]).format(**r))
|
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
|
# 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.74.22
|
ccxt==1.74.63
|
||||||
# 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.73.1',
|
'ccxt>=1.74.17',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
'python-telegram-bot>=13.4',
|
'python-telegram-bot>=13.4',
|
||||||
'arrow>=0.17.0',
|
'arrow>=0.17.0',
|
||||||
|
3
setup.sh
3
setup.sh
@ -132,6 +132,9 @@ function install_macos() {
|
|||||||
echo_block "Installing Brew"
|
echo_block "Installing Brew"
|
||||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
brew install gettext
|
||||||
|
|
||||||
#Gets number after decimal in python version
|
#Gets number after decimal in python version
|
||||||
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re
|
|||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('trademode', [TradingMode.FUTURES, TradingMode.SPOT])
|
||||||
@pytest.mark.parametrize('limitratio,expected,side', [
|
@pytest.mark.parametrize('limitratio,expected,side', [
|
||||||
(None, 220 * 0.99, "sell"),
|
(None, 220 * 0.99, "sell"),
|
||||||
(0.99, 220 * 0.99, "sell"),
|
(0.99, 220 * 0.99, "sell"),
|
||||||
@ -19,16 +20,10 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
|||||||
(0.99, 220 * 1.01, "buy"),
|
(0.99, 220 * 1.01, "buy"),
|
||||||
(0.98, 220 * 1.02, "buy"),
|
(0.98, 220 * 1.02, "buy"),
|
||||||
])
|
])
|
||||||
def test_stoploss_order_binance(
|
def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
|
||||||
default_conf,
|
|
||||||
mocker,
|
|
||||||
limitratio,
|
|
||||||
expected,
|
|
||||||
side
|
|
||||||
):
|
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
order_type = 'stop_loss_limit'
|
order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'stop'
|
||||||
|
|
||||||
api_mock.create_order = MagicMock(return_value={
|
api_mock.create_order = MagicMock(return_value={
|
||||||
'id': order_id,
|
'id': order_id,
|
||||||
@ -37,6 +32,8 @@ def test_stoploss_order_binance(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
default_conf['dry_run'] = False
|
default_conf['dry_run'] = False
|
||||||
|
default_conf['margin_mode'] = MarginMode.ISOLATED
|
||||||
|
default_conf['trading_mode'] = trademode
|
||||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
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)
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
@ -72,7 +69,11 @@ def test_stoploss_order_binance(
|
|||||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
# Price should be 1% below stopprice
|
# 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]['price'] == expected
|
||||||
assert api_mock.create_order.call_args_list[0][1]['params'] == {'stopPrice': 220}
|
if trademode == TradingMode.SPOT:
|
||||||
|
params_dict = {'stopPrice': 220}
|
||||||
|
else:
|
||||||
|
params_dict = {'stopPrice': 220, 'reduceOnly': True}
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['params'] == params_dict
|
||||||
|
|
||||||
# test exception handling
|
# test exception handling
|
||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
|
@ -82,6 +82,13 @@ EXCHANGES = {
|
|||||||
'leverage_tiers_public': True,
|
'leverage_tiers_public': True,
|
||||||
'leverage_in_spot_market': True,
|
'leverage_in_spot_market': True,
|
||||||
},
|
},
|
||||||
|
'huobi': {
|
||||||
|
'pair': 'BTC/USDT',
|
||||||
|
'stake_currency': 'USDT',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '5m',
|
||||||
|
'futures': False,
|
||||||
|
},
|
||||||
'bitvavo': {
|
'bitvavo': {
|
||||||
'pair': 'BTC/EUR',
|
'pair': 'BTC/EUR',
|
||||||
'stake_currency': 'EUR',
|
'stake_currency': 'EUR',
|
||||||
@ -198,7 +205,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
|
||||||
@ -313,7 +323,7 @@ class TestCCXTExchange():
|
|||||||
def test_ccxt_get_max_leverage_spot(self, exchange):
|
def test_ccxt_get_max_leverage_spot(self, exchange):
|
||||||
spot, spot_name = exchange
|
spot, spot_name = exchange
|
||||||
if spot:
|
if spot:
|
||||||
leverage_in_market_spot = EXCHANGES[spot_name]['leverage_in_spot_market']
|
leverage_in_market_spot = EXCHANGES[spot_name].get('leverage_in_spot_market')
|
||||||
if leverage_in_market_spot:
|
if leverage_in_market_spot:
|
||||||
spot_pair = EXCHANGES[spot_name].get('pair', EXCHANGES[spot_name]['pair'])
|
spot_pair = EXCHANGES[spot_name].get('pair', EXCHANGES[spot_name]['pair'])
|
||||||
spot_leverage = spot.get_max_leverage(spot_pair, 20)
|
spot_leverage = spot.get_max_leverage(spot_pair, 20)
|
||||||
@ -323,7 +333,7 @@ class TestCCXTExchange():
|
|||||||
def test_ccxt_get_max_leverage_futures(self, exchange_futures):
|
def test_ccxt_get_max_leverage_futures(self, exchange_futures):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures:
|
if futures:
|
||||||
leverage_tiers_public = EXCHANGES[futures_name]['leverage_tiers_public']
|
leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public')
|
||||||
if leverage_tiers_public:
|
if leverage_tiers_public:
|
||||||
futures_pair = EXCHANGES[futures_name].get(
|
futures_pair = EXCHANGES[futures_name].get(
|
||||||
'futures_pair',
|
'futures_pair',
|
||||||
@ -346,7 +356,7 @@ class TestCCXTExchange():
|
|||||||
|
|
||||||
def test_ccxt_load_leverage_tiers(self, exchange_futures):
|
def test_ccxt_load_leverage_tiers(self, exchange_futures):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures and EXCHANGES[futures_name]['leverage_tiers_public']:
|
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||||
leverage_tiers = futures.load_leverage_tiers()
|
leverage_tiers = futures.load_leverage_tiers()
|
||||||
futures_pair = EXCHANGES[futures_name].get(
|
futures_pair = EXCHANGES[futures_name].get(
|
||||||
'futures_pair',
|
'futures_pair',
|
||||||
@ -379,7 +389,7 @@ class TestCCXTExchange():
|
|||||||
|
|
||||||
def test_ccxt_dry_run_liquidation_price(self, exchange_futures):
|
def test_ccxt_dry_run_liquidation_price(self, exchange_futures):
|
||||||
futures, futures_name = exchange_futures
|
futures, futures_name = exchange_futures
|
||||||
if futures and EXCHANGES[futures_name]['leverage_tiers_public']:
|
if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
|
||||||
|
|
||||||
futures_pair = EXCHANGES[futures_name].get(
|
futures_pair = EXCHANGES[futures_name].get(
|
||||||
'futures_pair',
|
'futures_pair',
|
||||||
|
@ -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_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()
|
||||||
|
117
tests/exchange/test_huobi.py
Normal file
117
tests/exchange/test_huobi.py
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
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,side', [
|
||||||
|
(None, 220 * 0.99, "sell"),
|
||||||
|
(0.99, 220 * 0.99, "sell"),
|
||||||
|
(0.98, 220 * 0.98, "sell"),
|
||||||
|
])
|
||||||
|
def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
|
||||||
|
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},
|
||||||
|
side=side,
|
||||||
|
leverage=1.0)
|
||||||
|
|
||||||
|
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,
|
||||||
|
side=side, leverage=1.0)
|
||||||
|
|
||||||
|
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={}, side=side, leverage=1.0)
|
||||||
|
|
||||||
|
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={}, side=side, leverage=1.0)
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi",
|
||||||
|
"stoploss", "create_order", retries=1,
|
||||||
|
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
|
||||||
|
side=side, leverage=1.0)
|
||||||
|
|
||||||
|
|
||||||
|
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},
|
||||||
|
side='sell', leverage=1.0)
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||||
|
order_types={}, side='sell', leverage=1.0)
|
||||||
|
|
||||||
|
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, 'sell')
|
||||||
|
assert not exchange.stoploss_adjust(1499, order, 'sell')
|
||||||
|
# Test with invalid order case
|
||||||
|
order['type'] = 'stop_loss'
|
||||||
|
assert not exchange.stoploss_adjust(1501, order, 'sell')
|
127
tests/exchange/test_kucoin.py
Normal file
127
tests/exchange/test_kucoin.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
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,side', [
|
||||||
|
(None, 220 * 0.99, "sell"),
|
||||||
|
(0.99, 220 * 0.99, "sell"),
|
||||||
|
(0.98, 220 * 0.98, "sell"),
|
||||||
|
])
|
||||||
|
def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, 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},
|
||||||
|
side=side, leverage=1.0)
|
||||||
|
|
||||||
|
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, side=side, leverage=1.0)
|
||||||
|
|
||||||
|
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={}, side=side, leverage=1.0)
|
||||||
|
|
||||||
|
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={}, side=side, leverage=1.0)
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin",
|
||||||
|
"stoploss", "create_order", retries=1,
|
||||||
|
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
|
||||||
|
side=side, leverage=1.0)
|
||||||
|
|
||||||
|
|
||||||
|
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},
|
||||||
|
side='sell', leverage=1.0)
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
||||||
|
order_types={}, side='sell', leverage=1.0)
|
||||||
|
|
||||||
|
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, 'sell')
|
||||||
|
assert not exchange.stoploss_adjust(1499, order, 'sell')
|
||||||
|
# Test with invalid order case
|
||||||
|
order['info']['stop'] = None
|
||||||
|
assert not exchange.stoploss_adjust(1501, order, 'sell')
|
@ -80,7 +80,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,
|
||||||
@ -116,14 +116,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'is_short': False,
|
'is_short': False,
|
||||||
'funding_fees': 0.0,
|
'funding_fees': 0.0,
|
||||||
'trading_mode': TradingMode.SPOT,
|
'trading_mode': TradingMode.SPOT,
|
||||||
'filled_entry_orders': [{
|
'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,
|
||||||
'is_open': False, 'pair': 'ETH/BTC',
|
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
||||||
'remaining': ANY, 'status': ANY}],
|
'remaining': ANY, 'status': ANY, 'ft_is_entry': True,
|
||||||
'filled_exit_orders': [],
|
}],
|
||||||
}
|
}
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||||
@ -162,7 +162,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,
|
||||||
@ -198,14 +198,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'is_short': False,
|
'is_short': False,
|
||||||
'funding_fees': 0.0,
|
'funding_fees': 0.0,
|
||||||
'trading_mode': TradingMode.SPOT,
|
'trading_mode': TradingMode.SPOT,
|
||||||
'filled_entry_orders': [{
|
'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,
|
||||||
'is_open': False, 'pair': 'ETH/BTC',
|
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
||||||
'remaining': ANY, 'status': ANY}],
|
'remaining': ANY, 'status': ANY, 'ft_is_entry': True,
|
||||||
'filled_exit_orders': [],
|
}],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -971,6 +971,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
|
|||||||
'interest_rate': 0.0,
|
'interest_rate': 0.0,
|
||||||
'funding_fees': None,
|
'funding_fees': None,
|
||||||
'trading_mode': ANY,
|
'trading_mode': ANY,
|
||||||
|
'orders': [ANY],
|
||||||
}
|
}
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||||
@ -1170,6 +1171,7 @@ def test_api_forceentry(botclient, mocker, fee, endpoint):
|
|||||||
'interest_rate': None,
|
'interest_rate': None,
|
||||||
'funding_fees': None,
|
'funding_fees': None,
|
||||||
'trading_mode': 'spot',
|
'trading_mode': 'spot',
|
||||||
|
'orders': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -1452,6 +1454,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)
|
||||||
|
@ -208,6 +208,7 @@ def test_telegram_status(default_conf, update, mocker) -> None:
|
|||||||
'is_open': True,
|
'is_open': True,
|
||||||
'is_short': False,
|
'is_short': False,
|
||||||
'filled_entry_orders': [],
|
'filled_entry_orders': [],
|
||||||
|
'orders': []
|
||||||
}]),
|
}]),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -240,6 +241,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',
|
||||||
@ -250,7 +253,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,
|
||||||
@ -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_01', 1.0),
|
||||||
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_02', 2.0),
|
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_02', 2.0),
|
||||||
(RPCMessageType.SHORT_FILL, 'Shorted', 'short_signal_01', 2.0),
|
(RPCMessageType.SHORT_FILL, 'Shorted', 'short_signal_01', 2.0),
|
||||||
])
|
])
|
||||||
def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, entered,
|
def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, entered,
|
||||||
enter_signal, leverage) -> None:
|
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', 1.0),
|
||||||
('Long', 'long_signal_01', 5.0),
|
('Long', 'long_signal_01', 5.0),
|
||||||
('Short', 'short_signal_01', 2.0),
|
('Short', 'short_signal_01', 2.0),
|
||||||
])
|
])
|
||||||
def test_send_msg_sell_notification_no_fiat(
|
def test_send_msg_sell_notification_no_fiat(
|
||||||
default_conf, mocker, direction, enter_signal, leverage) -> None:
|
default_conf, mocker, direction, enter_signal, leverage) -> None:
|
||||||
del default_conf['fiat_display_currency']
|
del default_conf['fiat_display_currency']
|
||||||
|
@ -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
|
||||||
@ -1407,7 +1407,7 @@ def test_handle_stoploss_on_exchange_trailing(
|
|||||||
|
|
||||||
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=amt,
|
amount=pytest.approx(amt),
|
||||||
pair='ETH/USDT',
|
pair='ETH/USDT',
|
||||||
order_types=freqtrade.strategy.order_types,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=stop_price[1],
|
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')
|
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
||||||
# Long uses modified ask - offset, short modified bid + offset
|
# Long uses modified ask - offset, short modified bid + offset
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=trade.amount,
|
amount=pytest.approx(trade.amount),
|
||||||
pair='ETH/USDT',
|
pair='ETH/USDT',
|
||||||
order_types=freqtrade.strategy.order_types,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=4.4 * 0.96 if not is_short else 0.95 * 1.04,
|
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
|
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,
|
||||||
@ -2534,9 +2534,14 @@ def test_check_handle_timedout_sell_usercustom(
|
|||||||
|
|
||||||
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_usdt.open_order_id = limit_sell_order_old['id']
|
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()
|
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
|
||||||
@ -2896,9 +2901,12 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
|||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_rate=0.245441,
|
open_rate=0.245441,
|
||||||
open_order_id="123456",
|
open_order_id="123456",
|
||||||
open_date=arrow.utcnow().datetime,
|
open_date=arrow.utcnow().shift(days=-2).datetime,
|
||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=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,
|
order = {'remaining': 1,
|
||||||
'amount': 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 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 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
|
||||||
@ -2936,7 +2950,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)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("is_short, open_rate, amt", [
|
@pytest.mark.parametrize("is_short, open_rate, amt", [
|
||||||
@ -3001,7 +3015,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
|
|||||||
'pair': 'ETH/USDT',
|
'pair': 'ETH/USDT',
|
||||||
'gain': 'profit',
|
'gain': 'profit',
|
||||||
'limit': 2.0 if is_short else 2.2,
|
'limit': 2.0 if is_short else 2.2,
|
||||||
'amount': amt,
|
'amount': pytest.approx(amt),
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
'direction': 'Short' if trade.is_short else 'Long',
|
'direction': 'Short' if trade.is_short else 'Long',
|
||||||
@ -3062,7 +3076,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
|
|||||||
'leverage': 1.0,
|
'leverage': 1.0,
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'limit': 2.2 if is_short else 2.01,
|
'limit': 2.2 if is_short else 2.01,
|
||||||
'amount': 29.70297029 if is_short else 30.0,
|
'amount': pytest.approx(29.70297029) if is_short else 30.0,
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
'enter_tag': None,
|
'enter_tag': None,
|
||||||
@ -3142,13 +3156,13 @@ def test_execute_trade_exit_custom_exit_price(
|
|||||||
'leverage': 1.0,
|
'leverage': 1.0,
|
||||||
'gain': profit_or_loss,
|
'gain': profit_or_loss,
|
||||||
'limit': limit,
|
'limit': limit,
|
||||||
'amount': amount,
|
'amount': pytest.approx(amount),
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
'enter_tag': None,
|
'enter_tag': None,
|
||||||
'open_rate': open_rate,
|
'open_rate': open_rate,
|
||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit_amount,
|
'profit_amount': pytest.approx(profit_amount),
|
||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
@ -3209,7 +3223,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
|
|||||||
'leverage': 1.0,
|
'leverage': 1.0,
|
||||||
'gain': 'loss',
|
'gain': 'loss',
|
||||||
'limit': 2.02 if is_short else 1.98,
|
'limit': 2.02 if is_short else 1.98,
|
||||||
'amount': 29.70297029 if is_short else 30.0,
|
'amount': pytest.approx(29.70297029 if is_short else 30.0),
|
||||||
'order_type': 'limit',
|
'order_type': 'limit',
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
'enter_tag': None,
|
'enter_tag': None,
|
||||||
@ -3472,13 +3486,13 @@ def test_execute_trade_exit_market_order(
|
|||||||
'leverage': 1.0,
|
'leverage': 1.0,
|
||||||
'gain': profit_or_loss,
|
'gain': profit_or_loss,
|
||||||
'limit': limit,
|
'limit': limit,
|
||||||
'amount': round(amount, 9),
|
'amount': pytest.approx(amount),
|
||||||
'order_type': 'market',
|
'order_type': 'market',
|
||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
'enter_tag': None,
|
'enter_tag': None,
|
||||||
'open_rate': open_rate,
|
'open_rate': open_rate,
|
||||||
'current_rate': current_rate,
|
'current_rate': current_rate,
|
||||||
'profit_amount': profit_amount,
|
'profit_amount': pytest.approx(profit_amount),
|
||||||
'profit_ratio': profit_ratio,
|
'profit_ratio': profit_ratio,
|
||||||
'stake_currency': 'USDT',
|
'stake_currency': 'USDT',
|
||||||
'fiat_currency': 'USD',
|
'fiat_currency': 'USD',
|
||||||
|
@ -1608,8 +1608,7 @@ def test_to_json(default_conf, fee):
|
|||||||
'is_short': None,
|
'is_short': None,
|
||||||
'trading_mode': None,
|
'trading_mode': None,
|
||||||
'funding_fees': None,
|
'funding_fees': None,
|
||||||
'filled_entry_orders': [],
|
'orders': [],
|
||||||
'filled_exit_orders': [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
@ -1684,8 +1683,7 @@ def test_to_json(default_conf, fee):
|
|||||||
'is_short': None,
|
'is_short': None,
|
||||||
'trading_mode': None,
|
'trading_mode': None,
|
||||||
'funding_fees': None,
|
'funding_fees': None,
|
||||||
'filled_entry_orders': [],
|
'orders': [],
|
||||||
'filled_exit_orders': [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user