Merge branch 'freqtrade:develop' into plot_hyperopt_stats
This commit is contained in:
commit
0804ef411b
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,8 @@
|
||||
# Freqtrade rules
|
||||
config*.json
|
||||
*.sqlite
|
||||
*.sqlite-shm
|
||||
*.sqlite-wal
|
||||
logfile.txt
|
||||
user_data/*
|
||||
!user_data/strategy/sample_strategy.py
|
||||
|
15
README.md
15
README.md
@ -30,12 +30,13 @@ hesitate to read the source code and understand the mechanism of this bot.
|
||||
|
||||
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||
|
||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Bittrex](https://bittrex.com/)
|
||||
- [X] [FTX](https://ftx.com)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [Huobi](http://huobi.com/)
|
||||
- [X] [Kraken](https://kraken.com/)
|
||||
- [X] [OKX](https://www.okx.com/)
|
||||
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||
|
||||
### Community tested
|
||||
@ -68,15 +69,9 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
||||
|
||||
## Quick start
|
||||
|
||||
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
||||
Please refer to the [Docker Quickstart documentation](https://www.freqtrade.io/en/stable/docker_quickstart/) on how to get started quickly.
|
||||
|
||||
```bash
|
||||
git clone -b develop https://github.com/freqtrade/freqtrade.git
|
||||
cd freqtrade
|
||||
./setup.sh --install
|
||||
```
|
||||
|
||||
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/stable/installation/).
|
||||
For further (native) installation methods, please refer to the [Installation documentation page](https://www.freqtrade.io/en/stable/installation/).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
|
@ -57,7 +57,7 @@ This configuration enables kraken, as well as rate-limiting to avoid bans from t
|
||||
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
Binance supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange..
|
||||
|
||||
### Binance Blacklist
|
||||
|
||||
@ -177,12 +177,21 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
|
||||
|
||||
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
|
||||
|
||||
### Kucoin Blacklists
|
||||
|
||||
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
||||
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
||||
|
||||
## OKX
|
||||
## Huobi
|
||||
|
||||
!!! Tip "Stoploss on Exchange"
|
||||
Huobi supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||
|
||||
## OKX (former OKEX)
|
||||
|
||||
OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||
|
||||
|
@ -42,12 +42,13 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
||||
|
||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||
|
||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#binance-blacklist))
|
||||
- [X] [Binance](https://www.binance.com/)
|
||||
- [X] [Bittrex](https://bittrex.com/)
|
||||
- [X] [FTX](https://ftx.com)
|
||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||
- [X] [Huobi](http://huobi.com/)
|
||||
- [X] [Kraken](https://kraken.com/)
|
||||
- [X] [OKX](https://www.okx.com/)
|
||||
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||
|
||||
### Community tested
|
||||
|
@ -1,4 +1,4 @@
|
||||
mkdocs==1.2.3
|
||||
mkdocs-material==8.2.1
|
||||
mkdocs-material==8.2.3
|
||||
mdx_truly_sane_lists==1.2
|
||||
pymdown-extensions==9.2
|
||||
|
@ -24,7 +24,7 @@ These modes can be configured with these values:
|
||||
```
|
||||
|
||||
!!! Note
|
||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now.
|
||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) and kucoin (stop-limit and stop-market) as of now.
|
||||
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
||||
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
""" Freqtrade bot """
|
||||
__version__ = '2022.1'
|
||||
__version__ = 'develop'
|
||||
|
||||
if __version__ == 'develop':
|
||||
|
||||
|
@ -108,10 +108,11 @@ def ask_user_config() -> Dict[str, Any]:
|
||||
"binance",
|
||||
"binanceus",
|
||||
"bittrex",
|
||||
"kraken",
|
||||
"ftx",
|
||||
"kucoin",
|
||||
"gateio",
|
||||
"huobi",
|
||||
"kraken",
|
||||
"kucoin",
|
||||
"okx",
|
||||
Separator(),
|
||||
"other",
|
||||
|
@ -25,12 +25,16 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[
|
||||
RunMode.HYPEROPT: 'hyperoptimization',
|
||||
}
|
||||
if method in no_unlimited_runmodes.keys():
|
||||
wallet_size = config['dry_run_wallet'] * config['tradable_balance_ratio']
|
||||
# tradable_balance_ratio
|
||||
if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT
|
||||
and config['stake_amount'] > config['dry_run_wallet']):
|
||||
wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency'])
|
||||
and config['stake_amount'] > wallet_size):
|
||||
wallet = round_coin_value(wallet_size, config['stake_currency'])
|
||||
stake = round_coin_value(config['stake_amount'], config['stake_currency'])
|
||||
raise OperationalException(f"Starting balance ({wallet}) "
|
||||
f"is smaller than stake_amount {stake}.")
|
||||
raise OperationalException(
|
||||
f"Starting balance ({wallet}) is smaller than stake_amount {stake}. "
|
||||
f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`."
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
@ -440,7 +440,6 @@ SCHEMA_TRADE_REQUIRED = [
|
||||
'dry_run_wallet',
|
||||
'ask_strategy',
|
||||
'bid_strategy',
|
||||
'unfilledtimeout',
|
||||
'stoploss',
|
||||
'minimal_roi',
|
||||
'internals',
|
||||
@ -456,7 +455,6 @@ SCHEMA_BACKTEST_REQUIRED = [
|
||||
'dry_run_wallet',
|
||||
'dataformat_ohlcv',
|
||||
'dataformat_trades',
|
||||
'unfilledtimeout',
|
||||
]
|
||||
|
||||
SCHEMA_MINIMAL_REQUIRED = [
|
||||
|
@ -18,6 +18,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
|
||||
from freqtrade.exchange.ftx import Ftx
|
||||
from freqtrade.exchange.gateio import Gateio
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.huobi import Huobi
|
||||
from freqtrade.exchange.kraken import Kraken
|
||||
from freqtrade.exchange.kucoin import Kucoin
|
||||
from freqtrade.exchange.okx import Okx
|
||||
|
@ -3,12 +3,8 @@ import logging
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
import arrow
|
||||
import ccxt
|
||||
|
||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||
OperationalException, TemporaryError)
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.exchange.common import retrier
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -18,6 +14,7 @@ class Binance(Exchange):
|
||||
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
"ohlcv_candle_limit": 1000,
|
||||
@ -33,65 +30,6 @@ class Binance(Exchange):
|
||||
"""
|
||||
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
this stoploss-limit is binance-specific.
|
||||
It may work with a limited number of other exchanges, but this has not been tested yet.
|
||||
"""
|
||||
# Limit price threshold: As limit price should always be below stop-price
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
rate = stop_price * limit_price_pct
|
||||
|
||||
ordertype = "stop_loss_limit"
|
||||
|
||||
stop_price = self.price_to_precision(pair, stop_price)
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
if stop_price <= rate:
|
||||
raise OperationalException(
|
||||
'In stoploss limit order, stop price should be more than limit price')
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._params.copy()
|
||||
params.update({'stopPrice': stop_price})
|
||||
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
rate = self.price_to_precision(pair, rate)
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
amount=amount, price=rate, params=params)
|
||||
logger.info('stoploss limit order added for %s. '
|
||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
# Errors:
|
||||
# `binance Order would trigger immediately.`
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||
since_ms: int, is_new_pair: bool = False,
|
||||
raise_: bool = False
|
||||
|
@ -600,7 +600,8 @@ class Exchange:
|
||||
# Dry-run methods
|
||||
|
||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
||||
rate: float, params: Dict = {},
|
||||
stop_loss: bool = False) -> Dict[str, Any]:
|
||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||
_amount = self.amount_to_precision(pair, amount)
|
||||
dry_order: Dict[str, Any] = {
|
||||
@ -616,14 +617,17 @@ class Exchange:
|
||||
'remaining': _amount,
|
||||
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||
'status': "closed" if ordertype == "market" else "open",
|
||||
'status': "closed" if ordertype == "market" and not stop_loss else "open",
|
||||
'fee': None,
|
||||
'info': {}
|
||||
}
|
||||
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
||||
if stop_loss:
|
||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||
dry_order["stopPrice"] = dry_order["price"]
|
||||
# Workaround to avoid filling stoploss orders immediately
|
||||
dry_order["ft_order_type"] = "stoploss"
|
||||
|
||||
if dry_order["type"] == "market":
|
||||
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
||||
# Update market order pricing
|
||||
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
||||
dry_order.update({
|
||||
@ -714,7 +718,9 @@ class Exchange:
|
||||
"""
|
||||
Check dry-run limit order fill and update fee (if it filled).
|
||||
"""
|
||||
if order['status'] != "closed" and order['type'] in ["limit"]:
|
||||
if (order['status'] != "closed"
|
||||
and order['type'] in ["limit"]
|
||||
and not order.get('ft_order_type')):
|
||||
pair = order['symbol']
|
||||
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
||||
order.update({
|
||||
@ -791,18 +797,89 @@ class Exchange:
|
||||
"""
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||
params = self._params.copy()
|
||||
# Verify if stopPrice works for your exchange!
|
||||
params.update({'stopPrice': stop_price})
|
||||
return params
|
||||
|
||||
@retrier(retries=0)
|
||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||
"""
|
||||
creates a stoploss order.
|
||||
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
|
||||
to the corresponding exchange type.
|
||||
|
||||
The precise ordertype is determined by the order_types dict or exchange default.
|
||||
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
|
||||
exchange's subclass.
|
||||
|
||||
The exception below should never raise, since we disallow
|
||||
starting the bot in validate_ordertypes()
|
||||
Note: Changes to this interface need to be applied to all sub-classes too.
|
||||
"""
|
||||
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
This may work with a limited number of other exchanges, but correct working
|
||||
needs to be tested individually.
|
||||
WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange.
|
||||
`stoploss_adjust` must still be implemented for this to work.
|
||||
"""
|
||||
if not self._ft_has['stoploss_on_exchange']:
|
||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||
|
||||
user_order_type = order_types.get('stoploss', 'market')
|
||||
if user_order_type in self._ft_has["stoploss_order_types"].keys():
|
||||
ordertype = self._ft_has["stoploss_order_types"][user_order_type]
|
||||
else:
|
||||
# Otherwise pick only one available
|
||||
ordertype = list(self._ft_has["stoploss_order_types"].values())[0]
|
||||
user_order_type = list(self._ft_has["stoploss_order_types"].keys())[0]
|
||||
|
||||
stop_price_norm = self.price_to_precision(pair, stop_price)
|
||||
rate = None
|
||||
if user_order_type == 'limit':
|
||||
# Limit price threshold: As limit price should always be below stop-price
|
||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||
rate = stop_price * limit_price_pct
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
if stop_price_norm <= rate:
|
||||
raise OperationalException(
|
||||
'In stoploss limit order, stop price should be more than limit price')
|
||||
rate = self.price_to_precision(pair, rate)
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price_norm, stop_loss=True)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm)
|
||||
|
||||
amount = self.amount_to_precision(pair, amount)
|
||||
|
||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||
amount=amount, price=rate, params=params)
|
||||
logger.info(f"stoploss {user_order_type} order added for {pair}. "
|
||||
f"stop price: {stop_price}. limit: {rate}")
|
||||
self._log_exchange_response('create_stoploss_order', order)
|
||||
return order
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise InsufficientFundsError(
|
||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.InvalidOrder as e:
|
||||
# Errors:
|
||||
# `Order would trigger immediately.`
|
||||
raise InvalidOrderException(
|
||||
f'Could not create {ordertype} sell order on market {pair}. '
|
||||
f'Tried to sell amount {amount} at rate {rate}. '
|
||||
f'Message: {e}') from e
|
||||
except ccxt.DDoSProtection as e:
|
||||
raise DDosProtection(e) from e
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f"Could not place stoploss order due to {e.__class__.__name__}. "
|
||||
f"Message: {e}") from e
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e) from e
|
||||
|
||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
||||
@ -1587,7 +1664,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
||||
|
||||
|
||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||
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]:
|
||||
|
@ -56,7 +56,7 @@ class Ftx(Exchange):
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
|
39
freqtrade/exchange/huobi.py
Normal file
39
freqtrade/exchange/huobi.py
Normal file
@ -0,0 +1,39 @@
|
||||
""" Huobi exchange subclass """
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Huobi(Exchange):
|
||||
"""
|
||||
Huobi exchange class. Contains adjustments needed for Freqtrade to work
|
||||
with this exchange.
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "stop-limit"},
|
||||
"ohlcv_candle_limit": 1000,
|
||||
"l2_limit_range": [5, 10, 20],
|
||||
"l2_limit_range_required": False,
|
||||
}
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['type'] == 'stop' and stop_loss > float(order['stopPrice'])
|
||||
|
||||
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||
|
||||
params = self._params.copy()
|
||||
params.update({
|
||||
"stopPrice": stop_price,
|
||||
"operator": "lte",
|
||||
})
|
||||
return params
|
@ -86,6 +86,8 @@ class Kraken(Exchange):
|
||||
"""
|
||||
Creates a stoploss market order.
|
||||
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()
|
||||
|
||||
@ -101,7 +103,7 @@ class Kraken(Exchange):
|
||||
|
||||
if self._config['dry_run']:
|
||||
dry_order = self.create_dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
|
@ -19,8 +19,26 @@ class Kucoin(Exchange):
|
||||
"""
|
||||
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||
"l2_limit_range": [20, 100],
|
||||
"l2_limit_range_required": False,
|
||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||
"time_in_force_parameter": "timeInForce",
|
||||
}
|
||||
|
||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||
"""
|
||||
Verify stop_loss against stoploss-order value (limit or price)
|
||||
Returns True if adjustment is necessary.
|
||||
"""
|
||||
return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice'])
|
||||
|
||||
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||
|
||||
params = self._params.copy()
|
||||
params.update({
|
||||
'stopPrice': stop_price,
|
||||
'stop': 'loss'
|
||||
})
|
||||
return params
|
||||
|
@ -542,7 +542,6 @@ class FreqtradeBot(LoggingMixin):
|
||||
entry_tag=buy_tag):
|
||||
logger.info(f"User requested abortion of buying {pair}")
|
||||
return False
|
||||
amount = self.exchange.amount_to_precision(pair, amount)
|
||||
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
||||
amount=amount, rate=enter_limit_requested,
|
||||
time_in_force=time_in_force)
|
||||
@ -900,7 +899,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
return False
|
||||
|
||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
|
||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: Dict) -> None:
|
||||
"""
|
||||
Check to see if stoploss on exchange should be updated
|
||||
in case of trailing stoploss on exchange
|
||||
@ -1109,6 +1108,7 @@ class FreqtradeBot(LoggingMixin):
|
||||
trade.close_date = None
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
trade.sell_reason = None
|
||||
cancelled = True
|
||||
else:
|
||||
# TODO: figure out how to handle partially complete sell orders
|
||||
@ -1170,8 +1170,8 @@ class FreqtradeBot(LoggingMixin):
|
||||
|
||||
# if stoploss is on exchange and we are on dry_run mode,
|
||||
# we consider the sell price stop price
|
||||
if self.config['dry_run'] and sell_type == 'stoploss' \
|
||||
and self.strategy.order_types['stoploss_on_exchange']:
|
||||
if (self.config['dry_run'] and sell_type == 'stoploss'
|
||||
and self.strategy.order_types['stoploss_on_exchange']):
|
||||
limit = trade.stop_loss
|
||||
|
||||
# set custom_exit_price if available
|
||||
|
@ -120,7 +120,7 @@ class Order(_DECL_BASE):
|
||||
ft_pair: str = Column(String(25), nullable=False)
|
||||
ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
|
||||
|
||||
order_id = Column(String(255), nullable=False, index=True)
|
||||
order_id: str = Column(String(255), nullable=False, index=True)
|
||||
status = Column(String(255), nullable=True)
|
||||
symbol = Column(String(25), nullable=True)
|
||||
order_type: str = Column(String(50), nullable=True)
|
||||
@ -193,8 +193,12 @@ class Order(_DECL_BASE):
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'pair': self.ft_pair,
|
||||
'order_id': self.order_id,
|
||||
'status': self.status,
|
||||
'amount': self.amount,
|
||||
'average': round(self.average, 8) if self.average else 0,
|
||||
'safe_price': self.safe_price,
|
||||
'cost': self.cost if self.cost else 0,
|
||||
'filled': self.filled,
|
||||
'ft_order_side': self.ft_order_side,
|
||||
@ -208,10 +212,8 @@ class Order(_DECL_BASE):
|
||||
'order_filled_timestamp': int(self.order_filled_date.replace(
|
||||
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
|
||||
'order_type': self.order_type,
|
||||
'pair': self.ft_pair,
|
||||
'price': self.price,
|
||||
'remaining': self.remaining,
|
||||
'status': self.status,
|
||||
}
|
||||
|
||||
def close_bt_order(self, close_date: datetime):
|
||||
@ -338,14 +340,7 @@ class LocalTrade():
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
filled_orders = self.select_filled_orders()
|
||||
filled_entries = []
|
||||
filled_exits = []
|
||||
if len(filled_orders) > 0:
|
||||
for order in filled_orders:
|
||||
if order.ft_order_side == 'buy':
|
||||
filled_entries.append(order.to_json())
|
||||
if order.ft_order_side == 'sell':
|
||||
filled_exits.append(order.to_json())
|
||||
orders = [order.to_json() for order in filled_orders]
|
||||
|
||||
return {
|
||||
'trade_id': self.id,
|
||||
@ -410,8 +405,7 @@ class LocalTrade():
|
||||
'max_rate': self.max_rate,
|
||||
|
||||
'open_order_id': self.open_order_id,
|
||||
'filled_entry_orders': filled_entries,
|
||||
'filled_exit_orders': filled_exits,
|
||||
'orders': orders,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
@ -177,6 +177,22 @@ class ShowConfig(BaseModel):
|
||||
max_entry_position_adjustment: int
|
||||
|
||||
|
||||
class OrderSchema(BaseModel):
|
||||
pair: str
|
||||
order_id: str
|
||||
status: str
|
||||
remaining: float
|
||||
amount: float
|
||||
safe_price: float
|
||||
cost: float
|
||||
filled: float
|
||||
ft_order_side: str
|
||||
order_type: str
|
||||
is_open: bool
|
||||
order_timestamp: Optional[int]
|
||||
order_filled_timestamp: Optional[int]
|
||||
|
||||
|
||||
class TradeSchema(BaseModel):
|
||||
trade_id: int
|
||||
pair: str
|
||||
@ -224,6 +240,7 @@ class TradeSchema(BaseModel):
|
||||
min_rate: Optional[float]
|
||||
max_rate: Optional[float]
|
||||
open_order_id: Optional[str]
|
||||
orders: List[OrderSchema]
|
||||
|
||||
|
||||
class OpenTradeSchema(TradeSchema):
|
||||
|
@ -32,7 +32,8 @@ logger = logging.getLogger(__name__)
|
||||
# 1.11: forcebuy and forcesell accept ordertype
|
||||
# 1.12: add blacklist delete endpoint
|
||||
# 1.13: forcebuy supports stake_amount
|
||||
API_VERSION = 1.13
|
||||
# 1.14: Add entry/exit orders to trade response
|
||||
API_VERSION = 1.14
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
|
@ -370,46 +370,52 @@ class Telegram(RPCHandler):
|
||||
else:
|
||||
return "\N{CROSS MARK}"
|
||||
|
||||
def _prepare_entry_details(self, filled_orders, base_currency, is_open):
|
||||
def _prepare_entry_details(self, filled_orders: List, base_currency: str, is_open: bool):
|
||||
"""
|
||||
Prepare details of trade with entry adjustment enabled
|
||||
"""
|
||||
lines = []
|
||||
lines: List[str] = []
|
||||
if len(filled_orders) > 0:
|
||||
first_avg = filled_orders[0]["safe_price"]
|
||||
|
||||
for x, order in enumerate(filled_orders):
|
||||
if order['ft_order_side'] != 'buy':
|
||||
continue
|
||||
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
||||
cur_entry_amount = order["amount"]
|
||||
cur_entry_average = order["average"]
|
||||
cur_entry_average = order["safe_price"]
|
||||
lines.append(" ")
|
||||
if x == 0:
|
||||
lines.append("*Entry #{}:*".format(x+1))
|
||||
lines.append("*Entry Amount:* {} ({:.8f} {})"
|
||||
.format(cur_entry_amount, order["cost"], base_currency))
|
||||
lines.append("*Average Entry Price:* {}".format(cur_entry_average))
|
||||
lines.append(f"*Entry #{x+1}:*")
|
||||
lines.append(
|
||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
|
||||
lines.append(f"*Average Entry Price:* {cur_entry_average}")
|
||||
else:
|
||||
sumA = 0
|
||||
sumB = 0
|
||||
for y in range(x):
|
||||
sumA += (filled_orders[y]["amount"] * filled_orders[y]["average"])
|
||||
sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"])
|
||||
sumB += filled_orders[y]["amount"]
|
||||
prev_avg_price = sumA/sumB
|
||||
price_to_1st_entry = ((cur_entry_average - filled_orders[0]["average"])
|
||||
/ filled_orders[0]["average"])
|
||||
minus_on_entry = (cur_entry_average - prev_avg_price)/prev_avg_price
|
||||
prev_avg_price = sumA / sumB
|
||||
price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
|
||||
minus_on_entry = 0
|
||||
if prev_avg_price:
|
||||
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
|
||||
|
||||
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"])
|
||||
days = dur_entry.days
|
||||
hours, remainder = divmod(dur_entry.seconds, 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
lines.append("*Entry #{}:* at {:.2%} avg profit".format(x+1, minus_on_entry))
|
||||
lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit")
|
||||
if is_open:
|
||||
lines.append("({})".format(cur_entry_datetime
|
||||
.humanize(granularity=["day", "hour", "minute"])))
|
||||
lines.append("*Entry Amount:* {} ({:.8f} {})"
|
||||
.format(cur_entry_amount, order["cost"], base_currency))
|
||||
lines.append("*Average Entry Price:* {} ({:.2%} from 1st entry rate)"
|
||||
.format(cur_entry_average, price_to_1st_entry))
|
||||
lines.append("*Order filled at:* {}".format(order["order_filled_date"]))
|
||||
lines.append("({}d {}h {}m {}s from previous entry)"
|
||||
.format(days, hours, minutes, seconds))
|
||||
lines.append(
|
||||
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
|
||||
lines.append(f"*Average Entry Price:* {cur_entry_average} "
|
||||
f"({price_to_1st_entry:.2%} from 1st entry rate)")
|
||||
lines.append(f"*Order filled at:* {order['order_filled_date']}")
|
||||
lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)")
|
||||
return lines
|
||||
|
||||
@authorized_only
|
||||
@ -440,7 +446,7 @@ class Telegram(RPCHandler):
|
||||
messages = []
|
||||
for r in results:
|
||||
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
|
||||
r['num_entries'] = len(r['filled_entry_orders'])
|
||||
r['num_entries'] = len([o for o in r['orders'] if o['ft_order_side'] == 'buy'])
|
||||
r['sell_reason'] = r.get('sell_reason', "")
|
||||
lines = [
|
||||
"*Trade ID:* `{trade_id}`" +
|
||||
@ -484,8 +490,8 @@ class Telegram(RPCHandler):
|
||||
lines.append("*Open Order:* `{open_order}`")
|
||||
|
||||
lines_detail = self._prepare_entry_details(
|
||||
r['filled_entry_orders'], r['base_currency'], r['is_open'])
|
||||
lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else ""))
|
||||
r['orders'], r['base_currency'], r['is_open'])
|
||||
lines.extend(lines_detail if lines_detail else "")
|
||||
|
||||
# Filter empty lines using list-comprehension
|
||||
messages.append("\n".join([line for line in lines if line]).format(**r))
|
||||
|
12
freqtrade/templates/subtemplates/exchange_huobi.j2
Normal file
12
freqtrade/templates/subtemplates/exchange_huobi.j2
Normal file
@ -0,0 +1,12 @@
|
||||
"exchange": {
|
||||
"name": "{{ exchange_name | lower }}",
|
||||
"key": "{{ exchange_key }}",
|
||||
"secret": "{{ exchange_secret }}",
|
||||
"ccxt_config": {},
|
||||
"ccxt_async_config": {},
|
||||
"pair_whitelist": [
|
||||
],
|
||||
"pair_blacklist": [
|
||||
"HT/.*"
|
||||
]
|
||||
}
|
@ -22,7 +22,7 @@ nbconvert==6.4.2
|
||||
# mypy types
|
||||
types-cachetools==4.2.9
|
||||
types-filelock==3.2.5
|
||||
types-requests==2.27.10
|
||||
types-requests==2.27.11
|
||||
types-tabulate==0.8.5
|
||||
|
||||
# Extensions to datetime library
|
||||
|
@ -2,7 +2,7 @@ numpy==1.22.2
|
||||
pandas==1.4.1
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==1.73.70
|
||||
ccxt==1.74.63
|
||||
# Pin cryptography for now due to rust build errors with piwheels
|
||||
cryptography==36.0.1
|
||||
aiohttp==3.8.1
|
||||
@ -31,7 +31,7 @@ python-rapidjson==1.6
|
||||
sdnotify==0.3.2
|
||||
|
||||
# API Server
|
||||
fastapi==0.74.0
|
||||
fastapi==0.74.1
|
||||
uvicorn==0.17.5
|
||||
pyjwt==2.3.0
|
||||
aiofiles==0.8.0
|
||||
|
2
setup.py
2
setup.py
@ -42,7 +42,7 @@ setup(
|
||||
],
|
||||
install_requires=[
|
||||
# from requirements.txt
|
||||
'ccxt>=1.66.32',
|
||||
'ccxt>=1.74.17',
|
||||
'SQLAlchemy',
|
||||
'python-telegram-bot>=13.4',
|
||||
'arrow>=0.17.0',
|
||||
|
3
setup.sh
3
setup.sh
@ -132,6 +132,9 @@ function install_macos() {
|
||||
echo_block "Installing Brew"
|
||||
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
|
||||
fi
|
||||
|
||||
brew install gettext
|
||||
|
||||
#Gets number after decimal in python version
|
||||
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
||||
|
||||
|
@ -59,6 +59,12 @@ EXCHANGES = {
|
||||
'hasQuoteVolume': True,
|
||||
'timeframe': '5m',
|
||||
},
|
||||
'huobi': {
|
||||
'pair': 'BTC/USDT',
|
||||
'stake_currency': 'USDT',
|
||||
'hasQuoteVolume': True,
|
||||
'timeframe': '5m',
|
||||
},
|
||||
'bitvavo': {
|
||||
'pair': 'BTC/EUR',
|
||||
'stake_currency': 'EUR',
|
||||
@ -140,7 +146,10 @@ class TestCCXTExchange():
|
||||
else:
|
||||
next_limit = exchange.get_next_limit_in_list(
|
||||
val, l2_limit_range, l2_limit_range_required)
|
||||
if next_limit is None or next_limit > 200:
|
||||
if next_limit is None:
|
||||
assert len(l2['asks']) > 100
|
||||
assert len(l2['asks']) > 100
|
||||
elif next_limit > 200:
|
||||
# Large orderbook sizes can be a problem for some exchanges (bitrex ...)
|
||||
assert len(l2['asks']) > 200
|
||||
assert len(l2['asks']) > 200
|
||||
|
@ -166,7 +166,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||
|
||||
exchange = ExchangeResolver.load_exchange('huobi', default_conf)
|
||||
exchange = ExchangeResolver.load_exchange('zaif', default_conf)
|
||||
assert isinstance(exchange, Exchange)
|
||||
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
||||
caplog.clear()
|
||||
|
109
tests/exchange/test_huobi.py
Normal file
109
tests/exchange/test_huobi.py
Normal file
@ -0,0 +1,109 @@
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
|
||||
@pytest.mark.parametrize('limitratio,expected', [
|
||||
(None, 220 * 0.99),
|
||||
(0.99, 220 * 0.99),
|
||||
(0.98, 220 * 0.98),
|
||||
])
|
||||
def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'stop-limit'
|
||||
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio}
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||
# Price should be 1% below stopprice
|
||||
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
|
||||
assert api_mock.create_order.call_args_list[0][1]['params'] == {"stopPrice": 220,
|
||||
"operator": "lte",
|
||||
}
|
||||
|
||||
# test exception handling
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
api_mock.create_order = MagicMock(
|
||||
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi",
|
||||
"stoploss", "create_order", retries=1,
|
||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
|
||||
def test_stoploss_order_dry_run_huobi(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'stop-limit'
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert 'type' in order
|
||||
|
||||
assert order['type'] == order_type
|
||||
assert order['price'] == 220
|
||||
assert order['amount'] == 1
|
||||
|
||||
|
||||
def test_stoploss_adjust_huobi(mocker, default_conf):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='huobi')
|
||||
order = {
|
||||
'type': 'stop',
|
||||
'price': 1500,
|
||||
'stopPrice': '1500',
|
||||
}
|
||||
assert exchange.stoploss_adjust(1501, order)
|
||||
assert not exchange.stoploss_adjust(1499, order)
|
||||
# Test with invalid order case
|
||||
order['type'] = 'stop_loss'
|
||||
assert not exchange.stoploss_adjust(1501, order)
|
120
tests/exchange/test_kucoin.py
Normal file
120
tests/exchange/test_kucoin.py
Normal file
@ -0,0 +1,120 @@
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import ccxt
|
||||
import pytest
|
||||
|
||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||
from tests.conftest import get_patched_exchange
|
||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||
|
||||
|
||||
@pytest.mark.parametrize('order_type', ['market', 'limit'])
|
||||
@pytest.mark.parametrize('limitratio,expected', [
|
||||
(None, 220 * 0.99),
|
||||
(0.99, 220 * 0.99),
|
||||
(0.98, 220 * 0.98),
|
||||
])
|
||||
def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, order_type):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||
if order_type == 'limit':
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={
|
||||
'stoploss': order_type,
|
||||
'stoploss_on_exchange_limit_ratio': 1.05})
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
order_types = {'stoploss': order_type}
|
||||
if limitratio is not None:
|
||||
order_types.update({'stoploss_on_exchange_limit_ratio': limitratio})
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||
# Price should be 1% below stopprice
|
||||
if order_type == 'limit':
|
||||
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
|
||||
else:
|
||||
assert api_mock.create_order.call_args_list[0][1]['price'] is None
|
||||
|
||||
assert api_mock.create_order.call_args_list[0][1]['params'] == {
|
||||
'stopPrice': 220,
|
||||
'stop': 'loss'
|
||||
}
|
||||
|
||||
# test exception handling
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
with pytest.raises(InvalidOrderException):
|
||||
api_mock.create_order = MagicMock(
|
||||
side_effect=ccxt.InvalidOrder("kucoin Order would trigger immediately."))
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin",
|
||||
"stoploss", "create_order", retries=1,
|
||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
|
||||
def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
order_type = 'market'
|
||||
default_conf['dry_run'] = True
|
||||
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||
order_types={'stoploss': 'limit',
|
||||
'stoploss_on_exchange_limit_ratio': 1.05})
|
||||
|
||||
api_mock.create_order.reset_mock()
|
||||
|
||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert 'type' in order
|
||||
|
||||
assert order['type'] == order_type
|
||||
assert order['price'] == 220
|
||||
assert order['amount'] == 1
|
||||
|
||||
|
||||
def test_stoploss_adjust_kucoin(mocker, default_conf):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id='kucoin')
|
||||
order = {
|
||||
'type': 'limit',
|
||||
'price': 1500,
|
||||
'stopPrice': 1500,
|
||||
'info': {'stopPrice': 1500, 'stop': "limit"},
|
||||
}
|
||||
assert exchange.stoploss_adjust(1501, order)
|
||||
assert not exchange.stoploss_adjust(1499, order)
|
||||
# Test with invalid order case
|
||||
order['info']['stop'] = None
|
||||
assert not exchange.stoploss_adjust(1501, order)
|
@ -79,7 +79,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'close_rate': None,
|
||||
'current_rate': 1.099e-05,
|
||||
'amount': 91.07468123,
|
||||
'amount_requested': 91.07468123,
|
||||
'amount_requested': 91.07468124,
|
||||
'stake_amount': 0.001,
|
||||
'trade_duration': None,
|
||||
'trade_duration_s': None,
|
||||
@ -109,14 +109,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'stoploss_entry_dist_ratio': -0.10448878,
|
||||
'open_order': None,
|
||||
'exchange': 'binance',
|
||||
'filled_entry_orders': [{
|
||||
'amount': 91.07468123, 'average': 1.098e-05,
|
||||
'orders': [{
|
||||
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
||||
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
||||
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
||||
'is_open': False, 'pair': 'ETH/BTC',
|
||||
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
||||
'remaining': ANY, 'status': ANY}],
|
||||
'filled_exit_orders': []
|
||||
}
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||
@ -154,7 +153,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'close_rate': None,
|
||||
'current_rate': ANY,
|
||||
'amount': 91.07468123,
|
||||
'amount_requested': 91.07468123,
|
||||
'amount_requested': 91.07468124,
|
||||
'trade_duration': ANY,
|
||||
'trade_duration_s': ANY,
|
||||
'stake_amount': 0.001,
|
||||
@ -184,14 +183,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
||||
'stoploss_entry_dist_ratio': -0.10448878,
|
||||
'open_order': None,
|
||||
'exchange': 'binance',
|
||||
'filled_entry_orders': [{
|
||||
'amount': 91.07468123, 'average': 1.098e-05,
|
||||
'orders': [{
|
||||
'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
|
||||
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
|
||||
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
|
||||
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
|
||||
'is_open': False, 'pair': 'ETH/BTC',
|
||||
'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
|
||||
'remaining': ANY, 'status': ANY}],
|
||||
'filled_exit_orders': []
|
||||
}
|
||||
|
||||
|
||||
|
@ -902,6 +902,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||
'buy_tag': None,
|
||||
'timeframe': 5,
|
||||
'exchange': 'binance',
|
||||
'orders': [ANY],
|
||||
|
||||
}
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||
@ -1089,6 +1091,7 @@ def test_api_forcebuy(botclient, mocker, fee):
|
||||
'buy_tag': None,
|
||||
'timeframe': 5,
|
||||
'exchange': 'binance',
|
||||
'orders': [],
|
||||
}
|
||||
|
||||
|
||||
|
@ -203,7 +203,7 @@ def test_telegram_status(default_conf, update, mocker) -> None:
|
||||
'stop_loss_ratio': -0.0001,
|
||||
'open_order': '(limit buy rem=0.00000000)',
|
||||
'is_open': True,
|
||||
'filled_entry_orders': []
|
||||
'orders': []
|
||||
}]),
|
||||
)
|
||||
|
||||
@ -236,6 +236,8 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
|
||||
create_mock_trades(fee)
|
||||
trades = Trade.get_open_trades()
|
||||
trade = trades[0]
|
||||
# Average may be empty on some exchanges
|
||||
trade.orders[0].average = 0
|
||||
trade.orders.append(Order(
|
||||
order_id='5412vbb',
|
||||
ft_order_side='buy',
|
||||
@ -246,7 +248,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
|
||||
order_type="market",
|
||||
side="buy",
|
||||
price=trade.open_rate * 0.95,
|
||||
average=trade.open_rate * 0.95,
|
||||
average=0,
|
||||
filled=trade.amount,
|
||||
remaining=0,
|
||||
cost=trade.amount,
|
||||
|
@ -727,7 +727,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt,
|
||||
call_args = buy_mm.call_args_list[0][1]
|
||||
assert call_args['pair'] == pair
|
||||
assert call_args['rate'] == bid
|
||||
assert call_args['amount'] == round(stake_amount / bid, 8)
|
||||
assert call_args['amount'] == stake_amount / bid
|
||||
buy_rate_mock.reset_mock()
|
||||
|
||||
# Should create an open trade with an open order id
|
||||
@ -748,7 +748,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt,
|
||||
call_args = buy_mm.call_args_list[1][1]
|
||||
assert call_args['pair'] == pair
|
||||
assert call_args['rate'] == fix_price
|
||||
assert call_args['amount'] == round(stake_amount / fix_price, 8)
|
||||
assert call_args['amount'] == stake_amount / fix_price
|
||||
|
||||
# In case of closed order
|
||||
limit_buy_order_usdt['status'] = 'closed'
|
||||
@ -1266,7 +1266,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee,
|
||||
|
||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
||||
stoploss_order_mock.assert_called_once_with(
|
||||
amount=27.39726027,
|
||||
amount=pytest.approx(27.39726027),
|
||||
pair='ETH/USDT',
|
||||
order_types=freqtrade.strategy.order_types,
|
||||
stop_price=4.4 * 0.95
|
||||
@ -1458,7 +1458,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
||||
|
||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
||||
stoploss_order_mock.assert_called_once_with(
|
||||
amount=31.57894736,
|
||||
amount=pytest.approx(31.57894736),
|
||||
pair='ETH/USDT',
|
||||
order_types=freqtrade.strategy.order_types,
|
||||
stop_price=4.4 * 0.96
|
||||
@ -1583,7 +1583,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee,
|
||||
assert trade.stop_loss == 4.4 * 0.99
|
||||
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
||||
stoploss_order_mock.assert_called_once_with(
|
||||
amount=11.41438356,
|
||||
amount=pytest.approx(11.41438356),
|
||||
pair='NEO/BTC',
|
||||
order_types=freqtrade.strategy.order_types,
|
||||
stop_price=4.4 * 0.99
|
||||
@ -2554,9 +2554,12 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
||||
exchange='binance',
|
||||
open_rate=0.245441,
|
||||
open_order_id="123456",
|
||||
open_date=arrow.utcnow().datetime,
|
||||
open_date=arrow.utcnow().shift(days=-2).datetime,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
close_rate=0.555,
|
||||
close_date=arrow.utcnow().datetime,
|
||||
sell_reason="sell_reason_whatever",
|
||||
)
|
||||
order = {'remaining': 1,
|
||||
'amount': 1,
|
||||
@ -2565,6 +2568,8 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
|
||||
assert freqtrade.handle_cancel_exit(trade, order, reason)
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert send_msg_mock.call_count == 1
|
||||
assert trade.close_rate is None
|
||||
assert trade.sell_reason is None
|
||||
|
||||
send_msg_mock.reset_mock()
|
||||
|
||||
|
@ -901,8 +901,7 @@ def test_to_json(default_conf, fee):
|
||||
'buy_tag': None,
|
||||
'timeframe': None,
|
||||
'exchange': 'binance',
|
||||
'filled_entry_orders': [],
|
||||
'filled_exit_orders': []
|
||||
'orders': [],
|
||||
}
|
||||
|
||||
# Simulate dry_run entries
|
||||
@ -970,8 +969,7 @@ def test_to_json(default_conf, fee):
|
||||
'buy_tag': 'buys_signal_001',
|
||||
'timeframe': None,
|
||||
'exchange': 'binance',
|
||||
'filled_entry_orders': [],
|
||||
'filled_exit_orders': []
|
||||
'orders': [],
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user