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
|
# Freqtrade rules
|
||||||
config*.json
|
config*.json
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
*.sqlite-shm
|
||||||
|
*.sqlite-wal
|
||||||
logfile.txt
|
logfile.txt
|
||||||
user_data/*
|
user_data/*
|
||||||
!user_data/strategy/sample_strategy.py
|
!user_data/strategy/sample_strategy.py
|
||||||
|
15
README.md
15
README.md
@ -30,12 +30,13 @@ hesitate to read the source code and understand the mechanism of this bot.
|
|||||||
|
|
||||||
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist))
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [Huobi](http://huobi.com/)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
- [X] [OKX](https://www.okx.com/)
|
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||||
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
@ -68,15 +69,9 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
|
|||||||
|
|
||||||
## Quick start
|
## Quick start
|
||||||
|
|
||||||
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot.
|
Please refer to the [Docker Quickstart documentation](https://www.freqtrade.io/en/stable/docker_quickstart/) on how to get started quickly.
|
||||||
|
|
||||||
```bash
|
For further (native) installation methods, please refer to the [Installation documentation page](https://www.freqtrade.io/en/stable/installation/).
|
||||||
git clone -b develop https://github.com/freqtrade/freqtrade.git
|
|
||||||
cd freqtrade
|
|
||||||
./setup.sh --install
|
|
||||||
```
|
|
||||||
|
|
||||||
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/stable/installation/).
|
|
||||||
|
|
||||||
## Basic Usage
|
## Basic Usage
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ This configuration enables kraken, as well as rate-limiting to avoid bans from t
|
|||||||
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
Binance supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
Binance supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange..
|
||||||
|
|
||||||
### Binance Blacklist
|
### Binance Blacklist
|
||||||
|
|
||||||
@ -177,12 +177,21 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th
|
|||||||
|
|
||||||
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force).
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
|
You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used.
|
||||||
|
|
||||||
### Kucoin Blacklists
|
### Kucoin Blacklists
|
||||||
|
|
||||||
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
|
||||||
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
|
||||||
|
|
||||||
## OKX
|
## Huobi
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Huobi supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.
|
||||||
|
|
||||||
|
## OKX (former OKEX)
|
||||||
|
|
||||||
OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
|
|
||||||
|
@ -42,12 +42,13 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
|
|||||||
|
|
||||||
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
|
||||||
|
|
||||||
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#binance-blacklist))
|
- [X] [Binance](https://www.binance.com/)
|
||||||
- [X] [Bittrex](https://bittrex.com/)
|
- [X] [Bittrex](https://bittrex.com/)
|
||||||
- [X] [FTX](https://ftx.com)
|
- [X] [FTX](https://ftx.com)
|
||||||
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
- [X] [Gate.io](https://www.gate.io/ref/6266643)
|
||||||
|
- [X] [Huobi](http://huobi.com/)
|
||||||
- [X] [Kraken](https://kraken.com/)
|
- [X] [Kraken](https://kraken.com/)
|
||||||
- [X] [OKX](https://www.okx.com/)
|
- [X] [OKX](https://okx.com/) (Former OKEX)
|
||||||
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
|
||||||
|
|
||||||
### Community tested
|
### Community tested
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mkdocs==1.2.3
|
mkdocs==1.2.3
|
||||||
mkdocs-material==8.2.1
|
mkdocs-material==8.2.3
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.2
|
pymdown-extensions==9.2
|
||||||
|
@ -24,7 +24,7 @@ These modes can be configured with these values:
|
|||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now.
|
Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) and kucoin (stop-limit and stop-market) as of now.
|
||||||
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
<ins>Do not set too low/tight stoploss value if using stop loss on exchange!</ins>
|
||||||
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work.
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
""" Freqtrade bot """
|
""" Freqtrade bot """
|
||||||
__version__ = '2022.1'
|
__version__ = 'develop'
|
||||||
|
|
||||||
if __version__ == 'develop':
|
if __version__ == 'develop':
|
||||||
|
|
||||||
|
@ -108,10 +108,11 @@ def ask_user_config() -> Dict[str, Any]:
|
|||||||
"binance",
|
"binance",
|
||||||
"binanceus",
|
"binanceus",
|
||||||
"bittrex",
|
"bittrex",
|
||||||
"kraken",
|
|
||||||
"ftx",
|
"ftx",
|
||||||
"kucoin",
|
|
||||||
"gateio",
|
"gateio",
|
||||||
|
"huobi",
|
||||||
|
"kraken",
|
||||||
|
"kucoin",
|
||||||
"okx",
|
"okx",
|
||||||
Separator(),
|
Separator(),
|
||||||
"other",
|
"other",
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -440,7 +440,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',
|
||||||
@ -456,7 +455,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
|
||||||
|
@ -3,12 +3,8 @@ import logging
|
|||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
|
||||||
|
|
||||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
|
||||||
OperationalException, TemporaryError)
|
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.common import retrier
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -18,6 +14,7 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
"stoploss_on_exchange": True,
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "stop_loss_limit"},
|
||||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||||
"time_in_force_parameter": "timeInForce",
|
"time_in_force_parameter": "timeInForce",
|
||||||
"ohlcv_candle_limit": 1000,
|
"ohlcv_candle_limit": 1000,
|
||||||
@ -33,65 +30,6 @@ class Binance(Exchange):
|
|||||||
"""
|
"""
|
||||||
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
||||||
|
|
||||||
@retrier(retries=0)
|
|
||||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
|
||||||
"""
|
|
||||||
creates a stoploss limit order.
|
|
||||||
this stoploss-limit is binance-specific.
|
|
||||||
It may work with a limited number of other exchanges, but this has not been tested yet.
|
|
||||||
"""
|
|
||||||
# Limit price threshold: As limit price should always be below stop-price
|
|
||||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
|
||||||
rate = stop_price * limit_price_pct
|
|
||||||
|
|
||||||
ordertype = "stop_loss_limit"
|
|
||||||
|
|
||||||
stop_price = self.price_to_precision(pair, stop_price)
|
|
||||||
|
|
||||||
# Ensure rate is less than stop price
|
|
||||||
if stop_price <= rate:
|
|
||||||
raise OperationalException(
|
|
||||||
'In stoploss limit order, stop price should be more than limit price')
|
|
||||||
|
|
||||||
if self._config['dry_run']:
|
|
||||||
dry_order = self.create_dry_run_order(
|
|
||||||
pair, ordertype, "sell", amount, stop_price)
|
|
||||||
return dry_order
|
|
||||||
|
|
||||||
try:
|
|
||||||
params = self._params.copy()
|
|
||||||
params.update({'stopPrice': stop_price})
|
|
||||||
|
|
||||||
amount = self.amount_to_precision(pair, amount)
|
|
||||||
|
|
||||||
rate = self.price_to_precision(pair, rate)
|
|
||||||
|
|
||||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
|
||||||
amount=amount, price=rate, params=params)
|
|
||||||
logger.info('stoploss limit order added for %s. '
|
|
||||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
|
||||||
self._log_exchange_response('create_stoploss_order', order)
|
|
||||||
return order
|
|
||||||
except ccxt.InsufficientFunds as e:
|
|
||||||
raise InsufficientFundsError(
|
|
||||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
|
||||||
f'Tried to sell amount {amount} at rate {rate}. '
|
|
||||||
f'Message: {e}') from e
|
|
||||||
except ccxt.InvalidOrder as e:
|
|
||||||
# Errors:
|
|
||||||
# `binance Order would trigger immediately.`
|
|
||||||
raise InvalidOrderException(
|
|
||||||
f'Could not create {ordertype} sell order on market {pair}. '
|
|
||||||
f'Tried to sell amount {amount} at rate {rate}. '
|
|
||||||
f'Message: {e}') from e
|
|
||||||
except ccxt.DDoSProtection as e:
|
|
||||||
raise DDosProtection(e) from e
|
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
|
||||||
raise TemporaryError(
|
|
||||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
|
||||||
except ccxt.BaseError as e:
|
|
||||||
raise OperationalException(e) from e
|
|
||||||
|
|
||||||
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
|
||||||
since_ms: int, is_new_pair: bool = False,
|
since_ms: int, is_new_pair: bool = False,
|
||||||
raise_: bool = False
|
raise_: bool = False
|
||||||
|
@ -600,7 +600,8 @@ class Exchange:
|
|||||||
# Dry-run methods
|
# Dry-run methods
|
||||||
|
|
||||||
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
rate: float, params: Dict = {},
|
||||||
|
stop_loss: bool = False) -> Dict[str, Any]:
|
||||||
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
|
||||||
_amount = self.amount_to_precision(pair, amount)
|
_amount = self.amount_to_precision(pair, amount)
|
||||||
dry_order: Dict[str, Any] = {
|
dry_order: Dict[str, Any] = {
|
||||||
@ -616,14 +617,17 @@ class Exchange:
|
|||||||
'remaining': _amount,
|
'remaining': _amount,
|
||||||
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
|
||||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'status': "closed" if ordertype == "market" else "open",
|
'status': "closed" if ordertype == "market" and not stop_loss else "open",
|
||||||
'fee': None,
|
'fee': None,
|
||||||
'info': {}
|
'info': {}
|
||||||
}
|
}
|
||||||
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
if stop_loss:
|
||||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||||
|
dry_order["stopPrice"] = dry_order["price"]
|
||||||
|
# Workaround to avoid filling stoploss orders immediately
|
||||||
|
dry_order["ft_order_type"] = "stoploss"
|
||||||
|
|
||||||
if dry_order["type"] == "market":
|
if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
|
||||||
# Update market order pricing
|
# Update market order pricing
|
||||||
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
average = self.get_dry_market_fill_price(pair, side, amount, rate)
|
||||||
dry_order.update({
|
dry_order.update({
|
||||||
@ -714,7 +718,9 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
Check dry-run limit order fill and update fee (if it filled).
|
Check dry-run limit order fill and update fee (if it filled).
|
||||||
"""
|
"""
|
||||||
if order['status'] != "closed" and order['type'] in ["limit"]:
|
if (order['status'] != "closed"
|
||||||
|
and order['type'] in ["limit"]
|
||||||
|
and not order.get('ft_order_type')):
|
||||||
pair = order['symbol']
|
pair = order['symbol']
|
||||||
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
|
||||||
order.update({
|
order.update({
|
||||||
@ -791,18 +797,89 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
|
||||||
|
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
params = self._params.copy()
|
||||||
|
# Verify if stopPrice works for your exchange!
|
||||||
|
params.update({'stopPrice': stop_price})
|
||||||
|
return params
|
||||||
|
|
||||||
|
@retrier(retries=0)
|
||||||
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
creates a stoploss order.
|
creates a stoploss order.
|
||||||
|
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
|
||||||
|
to the corresponding exchange type.
|
||||||
|
|
||||||
The precise ordertype is determined by the order_types dict or exchange default.
|
The precise ordertype is determined by the order_types dict or exchange default.
|
||||||
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
|
|
||||||
exchange's subclass.
|
|
||||||
The exception below should never raise, since we disallow
|
The exception below should never raise, since we disallow
|
||||||
starting the bot in validate_ordertypes()
|
starting the bot in validate_ordertypes()
|
||||||
Note: Changes to this interface need to be applied to all sub-classes too.
|
|
||||||
"""
|
|
||||||
|
|
||||||
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
This may work with a limited number of other exchanges, but correct working
|
||||||
|
needs to be tested individually.
|
||||||
|
WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange.
|
||||||
|
`stoploss_adjust` must still be implemented for this to work.
|
||||||
|
"""
|
||||||
|
if not self._ft_has['stoploss_on_exchange']:
|
||||||
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
|
||||||
|
user_order_type = order_types.get('stoploss', 'market')
|
||||||
|
if user_order_type in self._ft_has["stoploss_order_types"].keys():
|
||||||
|
ordertype = self._ft_has["stoploss_order_types"][user_order_type]
|
||||||
|
else:
|
||||||
|
# Otherwise pick only one available
|
||||||
|
ordertype = list(self._ft_has["stoploss_order_types"].values())[0]
|
||||||
|
user_order_type = list(self._ft_has["stoploss_order_types"].keys())[0]
|
||||||
|
|
||||||
|
stop_price_norm = self.price_to_precision(pair, stop_price)
|
||||||
|
rate = None
|
||||||
|
if user_order_type == 'limit':
|
||||||
|
# Limit price threshold: As limit price should always be below stop-price
|
||||||
|
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
|
rate = stop_price * limit_price_pct
|
||||||
|
|
||||||
|
# Ensure rate is less than stop price
|
||||||
|
if stop_price_norm <= rate:
|
||||||
|
raise OperationalException(
|
||||||
|
'In stoploss limit order, stop price should be more than limit price')
|
||||||
|
rate = self.price_to_precision(pair, rate)
|
||||||
|
|
||||||
|
if self._config['dry_run']:
|
||||||
|
dry_order = self.create_dry_run_order(
|
||||||
|
pair, ordertype, "sell", amount, stop_price_norm, stop_loss=True)
|
||||||
|
return dry_order
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm)
|
||||||
|
|
||||||
|
amount = self.amount_to_precision(pair, amount)
|
||||||
|
|
||||||
|
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||||
|
amount=amount, price=rate, params=params)
|
||||||
|
logger.info(f"stoploss {user_order_type} order added for {pair}. "
|
||||||
|
f"stop price: {stop_price}. limit: {rate}")
|
||||||
|
self._log_exchange_response('create_stoploss_order', order)
|
||||||
|
return order
|
||||||
|
except ccxt.InsufficientFunds as e:
|
||||||
|
raise InsufficientFundsError(
|
||||||
|
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
||||||
|
f'Tried to sell amount {amount} at rate {rate}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
# Errors:
|
||||||
|
# `Order would trigger immediately.`
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Could not create {ordertype} sell order on market {pair}. '
|
||||||
|
f'Tried to sell amount {amount} at rate {rate}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f"Could not place stoploss order due to {e.__class__.__name__}. "
|
||||||
|
f"Message: {e}") from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
|
||||||
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
def fetch_order(self, order_id: str, pair: str) -> Dict:
|
||||||
@ -1587,7 +1664,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
|
|||||||
|
|
||||||
|
|
||||||
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
def is_exchange_officially_supported(exchange_name: str) -> bool:
|
||||||
return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx']
|
return exchange_name in ['binance', 'bittrex', 'ftx', 'gateio', 'huobi', 'kraken', 'okx']
|
||||||
|
|
||||||
|
|
||||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||||
|
@ -56,7 +56,7 @@ class Ftx(Exchange):
|
|||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
pair, ordertype, "sell", amount, stop_price)
|
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
39
freqtrade/exchange/huobi.py
Normal file
39
freqtrade/exchange/huobi.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
""" Huobi exchange subclass """
|
||||||
|
import logging
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from freqtrade.exchange import Exchange
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Huobi(Exchange):
|
||||||
|
"""
|
||||||
|
Huobi exchange class. Contains adjustments needed for Freqtrade to work
|
||||||
|
with this exchange.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_ft_has: Dict = {
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "stop-limit"},
|
||||||
|
"ohlcv_candle_limit": 1000,
|
||||||
|
"l2_limit_range": [5, 10, 20],
|
||||||
|
"l2_limit_range_required": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
|
Returns True if adjustment is necessary.
|
||||||
|
"""
|
||||||
|
return order['type'] == 'stop' and stop_loss > float(order['stopPrice'])
|
||||||
|
|
||||||
|
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
|
||||||
|
params = self._params.copy()
|
||||||
|
params.update({
|
||||||
|
"stopPrice": stop_price,
|
||||||
|
"operator": "lte",
|
||||||
|
})
|
||||||
|
return params
|
@ -86,6 +86,8 @@ class Kraken(Exchange):
|
|||||||
"""
|
"""
|
||||||
Creates a stoploss market order.
|
Creates a stoploss market order.
|
||||||
Stoploss market orders is the only stoploss type supported by kraken.
|
Stoploss market orders is the only stoploss type supported by kraken.
|
||||||
|
TODO: investigate if this can be combined with generic implementation
|
||||||
|
(careful, prices are reversed)
|
||||||
"""
|
"""
|
||||||
params = self._params.copy()
|
params = self._params.copy()
|
||||||
|
|
||||||
@ -101,7 +103,7 @@ class Kraken(Exchange):
|
|||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.create_dry_run_order(
|
dry_order = self.create_dry_run_order(
|
||||||
pair, ordertype, "sell", amount, stop_price)
|
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -19,8 +19,26 @@ class Kucoin(Exchange):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
|
"stoploss_order_types": {"limit": "limit", "market": "market"},
|
||||||
"l2_limit_range": [20, 100],
|
"l2_limit_range": [20, 100],
|
||||||
"l2_limit_range_required": False,
|
"l2_limit_range_required": False,
|
||||||
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
"order_time_in_force": ['gtc', 'fok', 'ioc'],
|
||||||
"time_in_force_parameter": "timeInForce",
|
"time_in_force_parameter": "timeInForce",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
|
Returns True if adjustment is necessary.
|
||||||
|
"""
|
||||||
|
return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice'])
|
||||||
|
|
||||||
|
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
|
||||||
|
|
||||||
|
params = self._params.copy()
|
||||||
|
params.update({
|
||||||
|
'stopPrice': stop_price,
|
||||||
|
'stop': 'loss'
|
||||||
|
})
|
||||||
|
return params
|
||||||
|
@ -542,7 +542,6 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
entry_tag=buy_tag):
|
entry_tag=buy_tag):
|
||||||
logger.info(f"User requested abortion of buying {pair}")
|
logger.info(f"User requested abortion of buying {pair}")
|
||||||
return False
|
return False
|
||||||
amount = self.exchange.amount_to_precision(pair, amount)
|
|
||||||
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
||||||
amount=amount, rate=enter_limit_requested,
|
amount=amount, rate=enter_limit_requested,
|
||||||
time_in_force=time_in_force)
|
time_in_force=time_in_force)
|
||||||
@ -900,7 +899,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
|
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
Check to see if stoploss on exchange should be updated
|
Check to see if stoploss on exchange should be updated
|
||||||
in case of trailing stoploss on exchange
|
in case of trailing stoploss on exchange
|
||||||
@ -1109,6 +1108,7 @@ 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
|
cancelled = True
|
||||||
else:
|
else:
|
||||||
# TODO: figure out how to handle partially complete sell orders
|
# 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,
|
# if stoploss is on exchange and we are on dry_run mode,
|
||||||
# we consider the sell price stop price
|
# we consider the sell price stop price
|
||||||
if self.config['dry_run'] and sell_type == 'stoploss' \
|
if (self.config['dry_run'] and sell_type == 'stoploss'
|
||||||
and self.strategy.order_types['stoploss_on_exchange']:
|
and self.strategy.order_types['stoploss_on_exchange']):
|
||||||
limit = trade.stop_loss
|
limit = trade.stop_loss
|
||||||
|
|
||||||
# set custom_exit_price if available
|
# set custom_exit_price if available
|
||||||
|
@ -120,7 +120,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)
|
||||||
@ -193,8 +193,12 @@ class Order(_DECL_BASE):
|
|||||||
|
|
||||||
def to_json(self) -> Dict[str, Any]:
|
def to_json(self) -> 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,
|
||||||
@ -208,10 +212,8 @@ 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,
|
||||||
'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):
|
||||||
@ -338,14 +340,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() 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,
|
||||||
@ -410,8 +405,7 @@ class LocalTrade():
|
|||||||
'max_rate': self.max_rate,
|
'max_rate': self.max_rate,
|
||||||
|
|
||||||
'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
|
||||||
|
@ -177,6 +177,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
|
||||||
@ -224,6 +240,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]
|
||||||
|
|
||||||
|
|
||||||
class OpenTradeSchema(TradeSchema):
|
class OpenTradeSchema(TradeSchema):
|
||||||
|
@ -32,7 +32,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# 1.11: forcebuy and forcesell accept ordertype
|
# 1.11: forcebuy and forcesell accept ordertype
|
||||||
# 1.12: add blacklist delete endpoint
|
# 1.12: add blacklist delete endpoint
|
||||||
# 1.13: forcebuy supports stake_amount
|
# 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.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
|
@ -370,46 +370,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 order['ft_order_side'] != 'buy':
|
||||||
|
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
|
||||||
@ -440,7 +446,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_order_side'] == 'buy'])
|
||||||
r['sell_reason'] = r.get('sell_reason', "")
|
r['sell_reason'] = r.get('sell_reason', "")
|
||||||
lines = [
|
lines = [
|
||||||
"*Trade ID:* `{trade_id}`" +
|
"*Trade ID:* `{trade_id}`" +
|
||||||
@ -484,8 +490,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.73.70
|
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.66.32',
|
'ccxt>=1.74.17',
|
||||||
'SQLAlchemy',
|
'SQLAlchemy',
|
||||||
'python-telegram-bot>=13.4',
|
'python-telegram-bot>=13.4',
|
||||||
'arrow>=0.17.0',
|
'arrow>=0.17.0',
|
||||||
|
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')
|
||||||
|
|
||||||
|
@ -59,6 +59,12 @@ EXCHANGES = {
|
|||||||
'hasQuoteVolume': True,
|
'hasQuoteVolume': True,
|
||||||
'timeframe': '5m',
|
'timeframe': '5m',
|
||||||
},
|
},
|
||||||
|
'huobi': {
|
||||||
|
'pair': 'BTC/USDT',
|
||||||
|
'stake_currency': 'USDT',
|
||||||
|
'hasQuoteVolume': True,
|
||||||
|
'timeframe': '5m',
|
||||||
|
},
|
||||||
'bitvavo': {
|
'bitvavo': {
|
||||||
'pair': 'BTC/EUR',
|
'pair': 'BTC/EUR',
|
||||||
'stake_currency': 'EUR',
|
'stake_currency': 'EUR',
|
||||||
@ -140,7 +146,10 @@ class TestCCXTExchange():
|
|||||||
else:
|
else:
|
||||||
next_limit = exchange.get_next_limit_in_list(
|
next_limit = exchange.get_next_limit_in_list(
|
||||||
val, l2_limit_range, l2_limit_range_required)
|
val, l2_limit_range, l2_limit_range_required)
|
||||||
if next_limit is None or next_limit > 200:
|
if next_limit is None:
|
||||||
|
assert len(l2['asks']) > 100
|
||||||
|
assert len(l2['asks']) > 100
|
||||||
|
elif next_limit > 200:
|
||||||
# Large orderbook sizes can be a problem for some exchanges (bitrex ...)
|
# Large orderbook sizes can be a problem for some exchanges (bitrex ...)
|
||||||
assert len(l2['asks']) > 200
|
assert len(l2['asks']) > 200
|
||||||
assert len(l2['asks']) > 200
|
assert len(l2['asks']) > 200
|
||||||
|
@ -166,7 +166,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
|
|
||||||
exchange = ExchangeResolver.load_exchange('huobi', default_conf)
|
exchange = ExchangeResolver.load_exchange('zaif', default_conf)
|
||||||
assert isinstance(exchange, Exchange)
|
assert isinstance(exchange, Exchange)
|
||||||
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
|
109
tests/exchange/test_huobi.py
Normal file
109
tests/exchange/test_huobi.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
from random import randint
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('limitratio,expected', [
|
||||||
|
(None, 220 * 0.99),
|
||||||
|
(0.99, 220 * 0.99),
|
||||||
|
(0.98, 220 * 0.98),
|
||||||
|
])
|
||||||
|
def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
order_type = 'stop-limit'
|
||||||
|
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio}
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
# Price should be 1% below stopprice
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['params'] == {"stopPrice": 220,
|
||||||
|
"operator": "lte",
|
||||||
|
}
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.create_order = MagicMock(
|
||||||
|
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi",
|
||||||
|
"stoploss", "create_order", retries=1,
|
||||||
|
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_dry_run_huobi(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_type = 'stop-limit'
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert 'type' in order
|
||||||
|
|
||||||
|
assert order['type'] == order_type
|
||||||
|
assert order['price'] == 220
|
||||||
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_adjust_huobi(mocker, default_conf):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='huobi')
|
||||||
|
order = {
|
||||||
|
'type': 'stop',
|
||||||
|
'price': 1500,
|
||||||
|
'stopPrice': '1500',
|
||||||
|
}
|
||||||
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
assert not exchange.stoploss_adjust(1499, order)
|
||||||
|
# Test with invalid order case
|
||||||
|
order['type'] = 'stop_loss'
|
||||||
|
assert not exchange.stoploss_adjust(1501, order)
|
120
tests/exchange/test_kucoin.py
Normal file
120
tests/exchange/test_kucoin.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
from random import randint
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||||
|
from tests.conftest import get_patched_exchange
|
||||||
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('order_type', ['market', 'limit'])
|
||||||
|
@pytest.mark.parametrize('limitratio,expected', [
|
||||||
|
(None, 220 * 0.99),
|
||||||
|
(0.99, 220 * 0.99),
|
||||||
|
(0.98, 220 * 0.98),
|
||||||
|
])
|
||||||
|
def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, order_type):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
if order_type == 'limit':
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={
|
||||||
|
'stoploss': order_type,
|
||||||
|
'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
order_types = {'stoploss': order_type}
|
||||||
|
if limitratio is not None:
|
||||||
|
order_types.update({'stoploss_on_exchange_limit_ratio': limitratio})
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types)
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
# Price should be 1% below stopprice
|
||||||
|
if order_type == 'limit':
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
|
||||||
|
else:
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] is None
|
||||||
|
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['params'] == {
|
||||||
|
'stopPrice': 220,
|
||||||
|
'stop': 'loss'
|
||||||
|
}
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.create_order = MagicMock(
|
||||||
|
side_effect=ccxt.InvalidOrder("kucoin Order would trigger immediately."))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin",
|
||||||
|
"stoploss", "create_order", retries=1,
|
||||||
|
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_type = 'market'
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss': 'limit',
|
||||||
|
'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert 'type' in order
|
||||||
|
|
||||||
|
assert order['type'] == order_type
|
||||||
|
assert order['price'] == 220
|
||||||
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_adjust_kucoin(mocker, default_conf):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='kucoin')
|
||||||
|
order = {
|
||||||
|
'type': 'limit',
|
||||||
|
'price': 1500,
|
||||||
|
'stopPrice': 1500,
|
||||||
|
'info': {'stopPrice': 1500, 'stop': "limit"},
|
||||||
|
}
|
||||||
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
assert not exchange.stoploss_adjust(1499, order)
|
||||||
|
# Test with invalid order case
|
||||||
|
order['info']['stop'] = None
|
||||||
|
assert not exchange.stoploss_adjust(1501, order)
|
@ -79,7 +79,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'close_rate': None,
|
'close_rate': None,
|
||||||
'current_rate': 1.099e-05,
|
'current_rate': 1.099e-05,
|
||||||
'amount': 91.07468123,
|
'amount': 91.07468123,
|
||||||
'amount_requested': 91.07468123,
|
'amount_requested': 91.07468124,
|
||||||
'stake_amount': 0.001,
|
'stake_amount': 0.001,
|
||||||
'trade_duration': None,
|
'trade_duration': None,
|
||||||
'trade_duration_s': None,
|
'trade_duration_s': None,
|
||||||
@ -109,14 +109,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'stoploss_entry_dist_ratio': -0.10448878,
|
'stoploss_entry_dist_ratio': -0.10448878,
|
||||||
'open_order': None,
|
'open_order': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'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}],
|
||||||
'filled_exit_orders': []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
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,
|
'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,
|
||||||
@ -184,14 +183,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'stoploss_entry_dist_ratio': -0.10448878,
|
'stoploss_entry_dist_ratio': -0.10448878,
|
||||||
'open_order': None,
|
'open_order': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'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}],
|
||||||
'filled_exit_orders': []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -902,6 +902,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
'timeframe': 5,
|
'timeframe': 5,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
|
'orders': [ANY],
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
mocker.patch('freqtrade.exchange.Exchange.get_rate',
|
||||||
@ -1089,6 +1091,7 @@ def test_api_forcebuy(botclient, mocker, fee):
|
|||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
'timeframe': 5,
|
'timeframe': 5,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
|
'orders': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -203,7 +203,7 @@ def test_telegram_status(default_conf, update, mocker) -> None:
|
|||||||
'stop_loss_ratio': -0.0001,
|
'stop_loss_ratio': -0.0001,
|
||||||
'open_order': '(limit buy rem=0.00000000)',
|
'open_order': '(limit buy rem=0.00000000)',
|
||||||
'is_open': True,
|
'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)
|
create_mock_trades(fee)
|
||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
trade = trades[0]
|
trade = trades[0]
|
||||||
|
# Average may be empty on some exchanges
|
||||||
|
trade.orders[0].average = 0
|
||||||
trade.orders.append(Order(
|
trade.orders.append(Order(
|
||||||
order_id='5412vbb',
|
order_id='5412vbb',
|
||||||
ft_order_side='buy',
|
ft_order_side='buy',
|
||||||
@ -246,7 +248,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
|
|||||||
order_type="market",
|
order_type="market",
|
||||||
side="buy",
|
side="buy",
|
||||||
price=trade.open_rate * 0.95,
|
price=trade.open_rate * 0.95,
|
||||||
average=trade.open_rate * 0.95,
|
average=0,
|
||||||
filled=trade.amount,
|
filled=trade.amount,
|
||||||
remaining=0,
|
remaining=0,
|
||||||
cost=trade.amount,
|
cost=trade.amount,
|
||||||
|
@ -727,7 +727,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt,
|
|||||||
call_args = buy_mm.call_args_list[0][1]
|
call_args = buy_mm.call_args_list[0][1]
|
||||||
assert call_args['pair'] == pair
|
assert call_args['pair'] == pair
|
||||||
assert call_args['rate'] == bid
|
assert call_args['rate'] == bid
|
||||||
assert call_args['amount'] == round(stake_amount / bid, 8)
|
assert call_args['amount'] == stake_amount / bid
|
||||||
buy_rate_mock.reset_mock()
|
buy_rate_mock.reset_mock()
|
||||||
|
|
||||||
# Should create an open trade with an open order id
|
# Should create an open trade with an open order id
|
||||||
@ -748,7 +748,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt,
|
|||||||
call_args = buy_mm.call_args_list[1][1]
|
call_args = buy_mm.call_args_list[1][1]
|
||||||
assert call_args['pair'] == pair
|
assert call_args['pair'] == pair
|
||||||
assert call_args['rate'] == fix_price
|
assert call_args['rate'] == fix_price
|
||||||
assert call_args['amount'] == round(stake_amount / fix_price, 8)
|
assert call_args['amount'] == stake_amount / fix_price
|
||||||
|
|
||||||
# In case of closed order
|
# In case of closed order
|
||||||
limit_buy_order_usdt['status'] = 'closed'
|
limit_buy_order_usdt['status'] = 'closed'
|
||||||
@ -1266,7 +1266,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee,
|
|||||||
|
|
||||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=27.39726027,
|
amount=pytest.approx(27.39726027),
|
||||||
pair='ETH/USDT',
|
pair='ETH/USDT',
|
||||||
order_types=freqtrade.strategy.order_types,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=4.4 * 0.95
|
stop_price=4.4 * 0.95
|
||||||
@ -1458,7 +1458,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
|
|||||||
|
|
||||||
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=31.57894736,
|
amount=pytest.approx(31.57894736),
|
||||||
pair='ETH/USDT',
|
pair='ETH/USDT',
|
||||||
order_types=freqtrade.strategy.order_types,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=4.4 * 0.96
|
stop_price=4.4 * 0.96
|
||||||
@ -1583,7 +1583,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee,
|
|||||||
assert trade.stop_loss == 4.4 * 0.99
|
assert trade.stop_loss == 4.4 * 0.99
|
||||||
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
||||||
stoploss_order_mock.assert_called_once_with(
|
stoploss_order_mock.assert_called_once_with(
|
||||||
amount=11.41438356,
|
amount=pytest.approx(11.41438356),
|
||||||
pair='NEO/BTC',
|
pair='NEO/BTC',
|
||||||
order_types=freqtrade.strategy.order_types,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=4.4 * 0.99
|
stop_price=4.4 * 0.99
|
||||||
@ -2554,9 +2554,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,
|
||||||
@ -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 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()
|
||||||
|
|
||||||
|
@ -901,8 +901,7 @@ def test_to_json(default_conf, fee):
|
|||||||
'buy_tag': None,
|
'buy_tag': None,
|
||||||
'timeframe': None,
|
'timeframe': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'filled_entry_orders': [],
|
'orders': [],
|
||||||
'filled_exit_orders': []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
@ -970,8 +969,7 @@ def test_to_json(default_conf, fee):
|
|||||||
'buy_tag': 'buys_signal_001',
|
'buy_tag': 'buys_signal_001',
|
||||||
'timeframe': None,
|
'timeframe': None,
|
||||||
'exchange': 'binance',
|
'exchange': 'binance',
|
||||||
'filled_entry_orders': [],
|
'orders': [],
|
||||||
'filled_exit_orders': []
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user