Merge pull request #6492 from samgermain/feat/short

Merge develop into feat/short
This commit is contained in:
Matthias 2022-03-05 14:47:34 +01:00 committed by GitHub
commit 0bac7f824e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 664 additions and 236 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -30,12 +30,13 @@ hesitate to read the source code and understand the mechanism of this bot.
Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange. Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange.
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist)) - [X] [Binance](https://www.binance.com/)
- [X] [Bittrex](https://bittrex.com/) - [X] [Bittrex](https://bittrex.com/)
- [X] [FTX](https://ftx.com) - [X] [FTX](https://ftx.com/#a=2258149)
- [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [Huobi](http://huobi.com/)
- [X] [Kraken](https://kraken.com/) - [X] [Kraken](https://kraken.com/)
- [X] [OKX](https://www.okx.com/) - [X] [OKX](https://okx.com/) (Former OKEX)
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ - [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Community tested ### Community tested
@ -68,15 +69,9 @@ Please find the complete documentation on the [freqtrade website](https://www.fr
## Quick start ## Quick start
Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. Please refer to the [Docker Quickstart documentation](https://www.freqtrade.io/en/stable/docker_quickstart/) on how to get started quickly.
```bash For further (native) installation methods, please refer to the [Installation documentation page](https://www.freqtrade.io/en/stable/installation/).
git clone -b develop https://github.com/freqtrade/freqtrade.git
cd freqtrade
./setup.sh --install
```
For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/stable/installation/).
## Basic Usage ## Basic Usage

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

View File

@ -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:

View File

@ -42,12 +42,13 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is
Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange. Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange.
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#binance-blacklist)) - [X] [Binance](https://www.binance.com/)
- [X] [Bittrex](https://bittrex.com/) - [X] [Bittrex](https://bittrex.com/)
- [X] [FTX](https://ftx.com) - [X] [FTX](https://ftx.com/#a=2258149)
- [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Gate.io](https://www.gate.io/ref/6266643)
- [X] [Huobi](http://huobi.com/)
- [X] [Kraken](https://kraken.com/) - [X] [Kraken](https://kraken.com/)
- [X] [OKX](https://www.okx.com/) - [X] [OKX](https://okx.com/) (Former OKEX)
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ - [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Community tested ### Community tested

View File

@ -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

View File

@ -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.

View File

@ -54,6 +54,8 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++
Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker compose](docker_quickstart.md) first. You can download the Visual C++ build tools from [here](https://visualstudio.microsoft.com/visual-cpp-build-tools/) and install "Desktop development with C++" in it's default configuration. Unfortunately, this is a heavy download / dependency so you might want to consider WSL2 or [docker compose](docker_quickstart.md) first.
![Windows installation](assets/windows_install.png)
--- ---

View File

@ -110,6 +110,7 @@ def ask_user_config() -> Dict[str, Any]:
"bittrex", "bittrex",
"ftx", "ftx",
"gateio", "gateio",
"huobi",
"kraken", "kraken",
"kucoin", "kucoin",
"okx", "okx",

View File

@ -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

View File

@ -447,7 +447,6 @@ SCHEMA_TRADE_REQUIRED = [
'dry_run_wallet', 'dry_run_wallet',
'ask_strategy', 'ask_strategy',
'bid_strategy', 'bid_strategy',
'unfilledtimeout',
'stoploss', 'stoploss',
'minimal_roi', 'minimal_roi',
'internals', 'internals',
@ -463,7 +462,6 @@ SCHEMA_BACKTEST_REQUIRED = [
'dry_run_wallet', 'dry_run_wallet',
'dataformat_ohlcv', 'dataformat_ohlcv',
'dataformat_trades', 'dataformat_trades',
'unfilledtimeout',
] ]
SCHEMA_MINIMAL_REQUIRED = [ SCHEMA_MINIMAL_REQUIRED = [

View File

@ -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

View File

@ -9,8 +9,7 @@ import arrow
import ccxt import ccxt
from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
@ -22,6 +21,8 @@ class Binance(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "stop_loss_limit"},
"stoploss_order_types_futures": {"limit": "stop"},
"order_time_in_force": ['gtc', 'fok', 'ioc'], "order_time_in_force": ['gtc', 'fok', 'ioc'],
"time_in_force_parameter": "timeInForce", "time_in_force_parameter": "timeInForce",
"ohlcv_candle_limit": 1000, "ohlcv_candle_limit": 1000,
@ -52,82 +53,6 @@ class Binance(Exchange):
(side == "buy" and stop_loss < float(order['info']['stopPrice'])) (side == "buy" and stop_loss < float(order['info']['stopPrice']))
) )
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: str, leverage: float) -> Dict:
"""
creates a stoploss limit order.
this stoploss-limit is binance-specific.
It may work with a limited number of other exchanges, but this has not been tested yet.
:param side: "buy" or "sell"
"""
# Limit price threshold: As limit price should always be below stop-price
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
if side == "sell":
# TODO: Name limit_rate in other exchange subclasses
rate = stop_price * limit_price_pct
else:
rate = stop_price * (2 - limit_price_pct)
ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit'
stop_price = self.price_to_precision(pair, stop_price)
bad_stop_price = (stop_price <= rate) if side == "sell" else (stop_price >= rate)
# Ensure rate is less than stop price
if bad_stop_price:
raise OperationalException(
'In stoploss limit order, stop price should be better than limit price')
if self._config['dry_run']:
dry_order = self.create_dry_run_order(
pair, ordertype, side, amount, stop_price, leverage)
return dry_order
try:
params = self._params.copy()
params.update({'stopPrice': stop_price})
if self.trading_mode == TradingMode.FUTURES:
params.update({'reduceOnly': True})
amount = self.amount_to_precision(pair, amount)
rate = self.price_to_precision(pair, rate)
self._lev_prep(pair, leverage, side)
order = self._api.create_order(
symbol=pair,
type=ordertype,
side=side,
amount=amount,
price=rate,
params=params
)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
self._log_exchange_response('create_stoploss_order', order)
return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
# Errors:
# `binance Order would trigger immediately.`
raise InvalidOrderException(
f'Could not create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier @retrier
def _set_leverage( def _set_leverage(
self, self,

View File

@ -769,7 +769,8 @@ class Exchange:
# Dry-run methods # Dry-run methods
def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]: rate: float, leverage: float, params: Dict = {},
stop_loss: bool = False) -> Dict[str, Any]:
order_id = f'dry_run_{side}_{datetime.now().timestamp()}' order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
_amount = self.amount_to_precision(pair, amount) _amount = self.amount_to_precision(pair, amount)
dry_order: Dict[str, Any] = { dry_order: Dict[str, Any] = {
@ -785,15 +786,18 @@ class Exchange:
'remaining': _amount, 'remaining': _amount,
'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'), 'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'),
'timestamp': arrow.utcnow().int_timestamp * 1000, 'timestamp': arrow.utcnow().int_timestamp * 1000,
'status': "closed" if ordertype == "market" else "open", 'status': "closed" if ordertype == "market" and not stop_loss else "open",
'fee': None, 'fee': None,
'info': {}, 'info': {},
'leverage': leverage 'leverage': leverage
} }
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: if stop_loss:
dry_order["info"] = {"stopPrice": dry_order["price"]} dry_order["info"] = {"stopPrice": dry_order["price"]}
dry_order["stopPrice"] = dry_order["price"]
# Workaround to avoid filling stoploss orders immediately
dry_order["ft_order_type"] = "stoploss"
if dry_order["type"] == "market": if dry_order["type"] == "market" and not dry_order.get("ft_order_type"):
# Update market order pricing # Update market order pricing
average = self.get_dry_market_fill_price(pair, side, amount, rate) average = self.get_dry_market_fill_price(pair, side, amount, rate)
dry_order.update({ dry_order.update({
@ -884,7 +888,9 @@ class Exchange:
""" """
Check dry-run limit order fill and update fee (if it filled). Check dry-run limit order fill and update fee (if it filled).
""" """
if order['status'] != "closed" and order['type'] in ["limit"]: if (order['status'] != "closed"
and order['type'] in ["limit"]
and not order.get('ft_order_type')):
pair = order['symbol'] pair = order['symbol']
if self._is_dry_limit_order_filled(pair, order['side'], order['price']): if self._is_dry_limit_order_filled(pair, order['side'], order['price']):
order.update({ order.update({
@ -1002,19 +1008,120 @@ class Exchange:
""" """
raise OperationalException(f"stoploss is not implemented for {self.name}.") raise OperationalException(f"stoploss is not implemented for {self.name}.")
def stoploss(self, pair: str, amount: float, stop_price: float, def _get_stop_order_type(self, user_order_type) -> Tuple[str, str]:
order_types: Dict, side: str, leverage: float) -> Dict:
available_order_Types: Dict[str, str] = self._ft_has["stoploss_order_types"]
if self.trading_mode == TradingMode.FUTURES:
# Optionally use different order type for stop order
available_order_Types = self._ft_has.get('stoploss_order_types_futures',
self._ft_has["stoploss_order_types"])
if user_order_type in available_order_Types.keys():
ordertype = available_order_Types[user_order_type]
else:
# Otherwise pick only one available
ordertype = list(available_order_Types.values())[0]
user_order_type = list(available_order_Types.keys())[0]
return ordertype, user_order_type
def _get_stop_limit_rate(self, stop_price: float, order_types: Dict, side: str) -> float:
# Limit price threshold: As limit price should always be below stop-price
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
if side == "sell":
# TODO: Name limit_rate in other exchange subclasses
rate = stop_price * limit_price_pct
else:
rate = stop_price * (2 - limit_price_pct)
bad_stop_price = (stop_price <= rate) if side == "sell" else (stop_price >= rate)
# Ensure rate is less than stop price
if bad_stop_price:
raise OperationalException(
'In stoploss limit order, stop price should be more than limit price')
return rate
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
# Verify if stopPrice works for your exchange!
params.update({'stopPrice': stop_price})
return params
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict,
side: str, leverage: float) -> Dict:
""" """
creates a stoploss order. creates a stoploss order.
requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market
to the corresponding exchange type.
The precise ordertype is determined by the order_types dict or exchange default. The precise ordertype is determined by the order_types dict or exchange default.
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
exchange's subclass.
The exception below should never raise, since we disallow The exception below should never raise, since we disallow
starting the bot in validate_ordertypes() starting the bot in validate_ordertypes()
Note: Changes to this interface need to be applied to all sub-classes too.
"""
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')
ordertype, user_order_type = self._get_stop_order_type(user_order_type)
stop_price_norm = self.price_to_precision(pair, stop_price)
rate = None
if user_order_type == 'limit':
rate = self._get_stop_limit_rate(stop_price, order_types, side)
rate = self.price_to_precision(pair, rate)
if self._config['dry_run']:
dry_order = self.create_dry_run_order(
pair,
ordertype,
side,
amount,
stop_price_norm,
stop_loss=True,
leverage=leverage,
)
return dry_order
try:
params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm)
if self.trading_mode == TradingMode.FUTURES:
params['reduceOnly'] = True
amount = self.amount_to_precision(pair, amount)
self._lev_prep(pair, leverage, side)
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
amount=amount, price=rate, params=params)
logger.info(f"stoploss {user_order_type} order added for {pair}. "
f"stop price: {stop_price}. limit: {rate}")
self._log_exchange_response('create_stoploss_order', order)
return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
f'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
# Errors:
# `Order would trigger immediately.`
raise InvalidOrderException(
f'Could not create {ordertype} sell order on market {pair}. '
f'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f"Could not place stoploss order due to {e.__class__.__name__}. "
f"Message: {e}") from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
@retrier(retries=API_FETCH_ORDER_RETRY_COUNT) @retrier(retries=API_FETCH_ORDER_RETRY_COUNT)
def fetch_order(self, order_id: str, pair: str) -> Dict: def fetch_order(self, order_id: str, pair: str) -> Dict:
@ -2384,7 +2491,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
def is_exchange_officially_supported(exchange_name: str) -> bool: def is_exchange_officially_supported(exchange_name: str) -> bool:
return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx'] return exchange_name in ['binance', 'bittrex', 'ftx', 'gateio', 'huobi', 'kraken', 'okx']
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:

View File

@ -62,7 +62,7 @@ class Ftx(Exchange):
if self._config['dry_run']: if self._config['dry_run']:
dry_order = self.create_dry_run_order( dry_order = self.create_dry_run_order(
pair, ordertype, side, amount, stop_price, leverage) pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
return dry_order return dry_order
try: try:

View File

@ -0,0 +1,39 @@
""" Huobi exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Huobi(Exchange):
"""
Huobi exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
"""
_ft_has: Dict = {
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "stop-limit"},
"ohlcv_candle_limit": 1000,
"l2_limit_range": [5, 10, 20],
"l2_limit_range_required": False,
}
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return order['type'] == 'stop' and stop_loss > float(order['stopPrice'])
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
params.update({
"stopPrice": stop_price,
"operator": "lte",
})
return params

View File

@ -99,6 +99,8 @@ class Kraken(Exchange):
""" """
Creates a stoploss market order. Creates a stoploss market order.
Stoploss market orders is the only stoploss type supported by kraken. Stoploss market orders is the only stoploss type supported by kraken.
TODO: investigate if this can be combined with generic implementation
(careful, prices are reversed)
""" """
params = self._params.copy() params = self._params.copy()
if self.trading_mode == TradingMode.FUTURES: if self.trading_mode == TradingMode.FUTURES:
@ -119,7 +121,7 @@ class Kraken(Exchange):
if self._config['dry_run']: if self._config['dry_run']:
dry_order = self.create_dry_run_order( dry_order = self.create_dry_run_order(
pair, ordertype, side, amount, stop_price, leverage) pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
return dry_order return dry_order
try: try:

View File

@ -19,8 +19,26 @@ class Kucoin(Exchange):
""" """
_ft_has: Dict = { _ft_has: Dict = {
"stoploss_on_exchange": True,
"stoploss_order_types": {"limit": "limit", "market": "market"},
"l2_limit_range": [20, 100], "l2_limit_range": [20, 100],
"l2_limit_range_required": False, "l2_limit_range_required": False,
"order_time_in_force": ['gtc', 'fok', 'ioc'], "order_time_in_force": ['gtc', 'fok', 'ioc'],
"time_in_force_parameter": "timeInForce", "time_in_force_parameter": "timeInForce",
} }
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice'])
def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict:
params = self._params.copy()
params.update({
'stopPrice': stop_price,
'stop': 'loss'
})
return params

View File

@ -639,7 +639,6 @@ class FreqtradeBot(LoggingMixin):
entry_tag=enter_tag, side=trade_side): entry_tag=enter_tag, side=trade_side):
logger.info(f"User requested abortion of buying {pair}") logger.info(f"User requested abortion of buying {pair}")
return False return False
amount = self.exchange.amount_to_precision(pair, amount)
order = self.exchange.create_order( order = self.exchange.create_order(
pair=pair, pair=pair,
ordertype=order_type, ordertype=order_type,
@ -1070,7 +1069,7 @@ class FreqtradeBot(LoggingMixin):
return False return False
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None: def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: Dict) -> None:
""" """
Check to see if stoploss on exchange should be updated Check to see if stoploss on exchange should be updated
in case of trailing stoploss on exchange in case of trailing stoploss on exchange
@ -1142,15 +1141,19 @@ class FreqtradeBot(LoggingMixin):
max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0)
order_obj = trade.select_order_by_order_id(trade.open_order_id) order_obj = trade.select_order_by_order_id(trade.open_order_id)
if not_closed and (fully_cancelled or (order_obj and self.strategy.ft_check_timed_out( if not_closed and (fully_cancelled or (order_obj and self.strategy.ft_check_timed_out(
time_method, trade, order_obj, datetime.now(timezone.utc))) time_method, trade, order_obj, datetime.now(timezone.utc)))
): ):
if is_entering: if is_entering:
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
else: else:
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) canceled = self.handle_cancel_exit(
trade, order, constants.CANCEL_REASON['TIMEOUT'])
canceled_count = trade.get_exit_order_count() canceled_count = trade.get_exit_order_count()
if max_timeouts > 0 and canceled_count >= max_timeouts: max_timeouts = self.config.get(
'unfilledtimeout', {}).get('exit_timeout_count', 0)
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
logger.warning(f'Emergencyselling trade {trade}, as the sell order ' logger.warning(f'Emergencyselling trade {trade}, as the sell order '
f'timed out {max_timeouts} times.') f'timed out {max_timeouts} times.')
try: try:
@ -1252,11 +1255,12 @@ class FreqtradeBot(LoggingMixin):
reason=reason) reason=reason)
return was_trade_fully_canceled return was_trade_fully_canceled
def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool:
""" """
exit order cancel - cancel order and update trade exit order cancel - cancel order and update trade
:return: Reason for cancel :return: True if exit order was cancelled, false otherwise
""" """
cancelled = False
# if trade is not partially completed, just cancel the order # if trade is not partially completed, just cancel the order
if order['remaining'] == order['amount'] or order.get('filled') == 0.0: if order['remaining'] == order['amount'] or order.get('filled') == 0.0:
if not self.exchange.check_order_canceled_empty(order): if not self.exchange.check_order_canceled_empty(order):
@ -1268,7 +1272,7 @@ class FreqtradeBot(LoggingMixin):
except InvalidOrderException: except InvalidOrderException:
logger.exception( logger.exception(
f"Could not cancel {trade.exit_side} order {trade.open_order_id}") f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
return 'error cancelling order' return False
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade) logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
else: else:
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
@ -1282,9 +1286,12 @@ class FreqtradeBot(LoggingMixin):
trade.close_date = None trade.close_date = None
trade.is_open = True trade.is_open = True
trade.open_order_id = None trade.open_order_id = None
trade.sell_reason = None
cancelled = True
else: else:
# TODO: figure out how to handle partially complete sell orders # TODO: figure out how to handle partially complete sell orders
reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
cancelled = False
self.wallets.update() self.wallets.update()
self._notify_exit_cancel( self._notify_exit_cancel(
@ -1292,7 +1299,7 @@ class FreqtradeBot(LoggingMixin):
order_type=self.strategy.order_types[trade.exit_side], order_type=self.strategy.order_types[trade.exit_side],
reason=reason reason=reason
) )
return reason return cancelled
def _safe_exit_amount(self, pair: str, amount: float) -> float: def _safe_exit_amount(self, pair: str, amount: float) -> float:
""" """
@ -1351,8 +1358,8 @@ class FreqtradeBot(LoggingMixin):
# if stoploss is on exchange and we are on dry_run mode, # if stoploss is on exchange and we are on dry_run mode,
# we consider the sell price stop price # we consider the sell price stop price
if self.config['dry_run'] and exit_type == 'stoploss' \ if (self.config['dry_run'] and exit_type == 'stoploss'
and self.strategy.order_types['stoploss_on_exchange']: and self.strategy.order_types['stoploss_on_exchange']):
limit = trade.stop_loss limit = trade.stop_loss
# set custom_exit_price if available # set custom_exit_price if available

View File

@ -121,7 +121,7 @@ class Order(_DECL_BASE):
ft_pair: str = Column(String(25), nullable=False) ft_pair: str = Column(String(25), nullable=False)
ft_is_open = Column(Boolean, nullable=False, default=True, index=True) ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
order_id = Column(String(255), nullable=False, index=True) order_id: str = Column(String(255), nullable=False, index=True)
status = Column(String(255), nullable=True) status = Column(String(255), nullable=True)
symbol = Column(String(25), nullable=True) symbol = Column(String(25), nullable=True)
order_type: str = Column(String(50), nullable=True) order_type: str = Column(String(50), nullable=True)
@ -197,10 +197,14 @@ class Order(_DECL_BASE):
self.order_filled_date = datetime.now(timezone.utc) self.order_filled_date = datetime.now(timezone.utc)
self.order_update_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc)
def to_json(self) -> Dict[str, Any]: def to_json(self, entry_side: str) -> Dict[str, Any]:
return { return {
'pair': self.ft_pair,
'order_id': self.order_id,
'status': self.status,
'amount': self.amount, 'amount': self.amount,
'average': round(self.average, 8) if self.average else 0, 'average': round(self.average, 8) if self.average else 0,
'safe_price': self.safe_price,
'cost': self.cost if self.cost else 0, 'cost': self.cost if self.cost else 0,
'filled': self.filled, 'filled': self.filled,
'ft_order_side': self.ft_order_side, 'ft_order_side': self.ft_order_side,
@ -214,10 +218,9 @@ class Order(_DECL_BASE):
'order_filled_timestamp': int(self.order_filled_date.replace( 'order_filled_timestamp': int(self.order_filled_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None, tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
'order_type': self.order_type, 'order_type': self.order_type,
'pair': self.ft_pair,
'price': self.price, 'price': self.price,
'ft_is_entry': self.ft_order_side == entry_side,
'remaining': self.remaining, 'remaining': self.remaining,
'status': self.status,
} }
def close_bt_order(self, close_date: datetime): def close_bt_order(self, close_date: datetime):
@ -456,14 +459,7 @@ class LocalTrade():
def to_json(self) -> Dict[str, Any]: def to_json(self) -> Dict[str, Any]:
filled_orders = self.select_filled_orders() filled_orders = self.select_filled_orders()
filled_entries = [] orders = [order.to_json(self.enter_side) for order in filled_orders]
filled_exits = []
if len(filled_orders) > 0:
for order in filled_orders:
if order.ft_order_side == 'buy':
filled_entries.append(order.to_json())
if order.ft_order_side == 'sell':
filled_exits.append(order.to_json())
return { return {
'trade_id': self.id, 'trade_id': self.id,
@ -535,8 +531,7 @@ class LocalTrade():
'trading_mode': self.trading_mode, 'trading_mode': self.trading_mode,
'funding_fees': self.funding_fees, 'funding_fees': self.funding_fees,
'open_order_id': self.open_order_id, 'open_order_id': self.open_order_id,
'filled_entry_orders': filled_entries, 'orders': orders,
'filled_exit_orders': filled_exits,
} }
@staticmethod @staticmethod

View File

@ -8,7 +8,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc
from freqtrade.enums import BacktestState from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException from freqtrade.exceptions import DependencyException
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
from freqtrade.rpc.api_server.deps import get_config from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
from freqtrade.rpc.api_server.webserver import ApiServer from freqtrade.rpc.api_server.webserver import ApiServer
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
@ -22,7 +22,7 @@ router = APIRouter()
@router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
# flake8: noqa: C901 # flake8: noqa: C901
async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks, async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks,
config=Depends(get_config)): config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
"""Start backtesting if not done so already""" """Start backtesting if not done so already"""
if ApiServer._bgtask_running: if ApiServer._bgtask_running:
raise RPCException('Bot Background task already running') raise RPCException('Bot Background task already running')
@ -121,7 +121,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac
@router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_get_backtest(): def api_get_backtest(ws_mode=Depends(is_webserver_mode)):
""" """
Get backtesting result. Get backtesting result.
Returns Result after backtesting has been ran. Returns Result after backtesting has been ran.
@ -157,7 +157,7 @@ def api_get_backtest():
@router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_delete_backtest(): def api_delete_backtest(ws_mode=Depends(is_webserver_mode)):
"""Reset backtesting""" """Reset backtesting"""
if ApiServer._bgtask_running: if ApiServer._bgtask_running:
return { return {
@ -183,7 +183,7 @@ def api_delete_backtest():
@router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest']) @router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_backtest_abort(): def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
if not ApiServer._bgtask_running: if not ApiServer._bgtask_running:
return { return {
"status": "not_running", "status": "not_running",

View File

@ -184,6 +184,22 @@ class ShowConfig(BaseModel):
max_entry_position_adjustment: int max_entry_position_adjustment: int
class OrderSchema(BaseModel):
pair: str
order_id: str
status: str
remaining: float
amount: float
safe_price: float
cost: float
filled: float
ft_order_side: str
order_type: str
is_open: bool
order_timestamp: Optional[int]
order_filled_timestamp: Optional[int]
class TradeSchema(BaseModel): class TradeSchema(BaseModel):
trade_id: int trade_id: int
pair: str pair: str
@ -233,6 +249,7 @@ class TradeSchema(BaseModel):
min_rate: Optional[float] min_rate: Optional[float]
max_rate: Optional[float] max_rate: Optional[float]
open_order_id: Optional[str] open_order_id: Optional[str]
orders: List[OrderSchema]
leverage: Optional[float] leverage: Optional[float]
interest_rate: Optional[float] interest_rate: Optional[float]

View File

@ -34,8 +34,8 @@ logger = logging.getLogger(__name__)
# 1.12: add blacklist delete endpoint # 1.12: add blacklist delete endpoint
# 1.13: forcebuy supports stake_amount # 1.13: forcebuy supports stake_amount
# versions 2.xx -> futures/short branch # versions 2.xx -> futures/short branch
# 2.13: addition of Forceenter # 2.14: Add entry/exit orders to trade response
API_VERSION = 2.13 API_VERSION = 2.14
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() router_public = APIRouter()

View File

@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, Optional
from fastapi import Depends from fastapi import Depends
from freqtrade.enums import RunMode
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
@ -38,3 +39,9 @@ def get_exchange(config=Depends(get_config)):
ApiServer._exchange = ExchangeResolver.load_exchange( ApiServer._exchange = ExchangeResolver.load_exchange(
config['exchange']['name'], config) config['exchange']['name'], config)
return ApiServer._exchange return ApiServer._exchange
def is_webserver_mode(config=Depends(get_config)):
if config['runmode'] != RunMode.WEBSERVER:
raise RPCException('Bot is not in the correct state')
return None

View File

@ -171,7 +171,7 @@ class RPC:
# calculate profit and send message to user # calculate profit and send message to user
if trade.is_open: if trade.is_open:
try: try:
closing_side = "buy" if trade.is_short else "sell" closing_side = trade.exit_side
current_rate = self._freqtrade.exchange.get_rate( current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side=closing_side) trade.pair, refresh=False, side=closing_side)
except (ExchangeError, PricingError): except (ExchangeError, PricingError):

View File

@ -389,46 +389,52 @@ class Telegram(RPCHandler):
else: else:
return "\N{CROSS MARK}" return "\N{CROSS MARK}"
def _prepare_entry_details(self, filled_orders, base_currency, is_open): def _prepare_entry_details(self, filled_orders: List, base_currency: str, is_open: bool):
""" """
Prepare details of trade with entry adjustment enabled Prepare details of trade with entry adjustment enabled
""" """
lines = [] lines: List[str] = []
if len(filled_orders) > 0:
first_avg = filled_orders[0]["safe_price"]
for x, order in enumerate(filled_orders): for x, order in enumerate(filled_orders):
if not order['ft_is_entry']:
continue
cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_datetime = arrow.get(order["order_filled_date"])
cur_entry_amount = order["amount"] cur_entry_amount = order["amount"]
cur_entry_average = order["average"] cur_entry_average = order["safe_price"]
lines.append(" ") lines.append(" ")
if x == 0: if x == 0:
lines.append("*Entry #{}:*".format(x+1)) lines.append(f"*Entry #{x+1}:*")
lines.append("*Entry Amount:* {} ({:.8f} {})" lines.append(
.format(cur_entry_amount, order["cost"], base_currency)) f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
lines.append("*Average Entry Price:* {}".format(cur_entry_average)) lines.append(f"*Average Entry Price:* {cur_entry_average}")
else: else:
sumA = 0 sumA = 0
sumB = 0 sumB = 0
for y in range(x): for y in range(x):
sumA += (filled_orders[y]["amount"] * filled_orders[y]["average"]) sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"])
sumB += filled_orders[y]["amount"] sumB += filled_orders[y]["amount"]
prev_avg_price = sumA/sumB prev_avg_price = sumA / sumB
price_to_1st_entry = ((cur_entry_average - filled_orders[0]["average"]) price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg)
/ filled_orders[0]["average"]) minus_on_entry = 0
minus_on_entry = (cur_entry_average - prev_avg_price)/prev_avg_price if prev_avg_price:
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"]) dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"])
days = dur_entry.days days = dur_entry.days
hours, remainder = divmod(dur_entry.seconds, 3600) hours, remainder = divmod(dur_entry.seconds, 3600)
minutes, seconds = divmod(remainder, 60) minutes, seconds = divmod(remainder, 60)
lines.append("*Entry #{}:* at {:.2%} avg profit".format(x+1, minus_on_entry)) lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit")
if is_open: if is_open:
lines.append("({})".format(cur_entry_datetime lines.append("({})".format(cur_entry_datetime
.humanize(granularity=["day", "hour", "minute"]))) .humanize(granularity=["day", "hour", "minute"])))
lines.append("*Entry Amount:* {} ({:.8f} {})" lines.append(
.format(cur_entry_amount, order["cost"], base_currency)) f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
lines.append("*Average Entry Price:* {} ({:.2%} from 1st entry rate)" lines.append(f"*Average Entry Price:* {cur_entry_average} "
.format(cur_entry_average, price_to_1st_entry)) f"({price_to_1st_entry:.2%} from 1st entry rate)")
lines.append("*Order filled at:* {}".format(order["order_filled_date"])) lines.append(f"*Order filled at:* {order['order_filled_date']}")
lines.append("({}d {}h {}m {}s from previous entry)" lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)")
.format(days, hours, minutes, seconds))
return lines return lines
@authorized_only @authorized_only
@ -459,7 +465,7 @@ class Telegram(RPCHandler):
messages = [] messages = []
for r in results: for r in results:
r['open_date_hum'] = arrow.get(r['open_date']).humanize() r['open_date_hum'] = arrow.get(r['open_date']).humanize()
r['num_entries'] = len(r['filled_entry_orders']) r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']])
r['sell_reason'] = r.get('sell_reason', "") r['sell_reason'] = r.get('sell_reason', "")
lines = [ lines = [
"*Trade ID:* `{trade_id}`" + "*Trade ID:* `{trade_id}`" +
@ -505,8 +511,8 @@ class Telegram(RPCHandler):
lines.append("*Open Order:* `{open_order}`") lines.append("*Open Order:* `{open_order}`")
lines_detail = self._prepare_entry_details( lines_detail = self._prepare_entry_details(
r['filled_entry_orders'], r['base_currency'], r['is_open']) r['orders'], r['base_currency'], r['is_open'])
lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else "")) lines.extend(lines_detail if lines_detail else "")
# Filter empty lines using list-comprehension # Filter empty lines using list-comprehension
messages.append("\n".join([line for line in lines if line]).format(**r)) messages.append("\n".join([line for line in lines if line]).format(**r))

View 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/.*"
]
}

View File

@ -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

View File

@ -2,7 +2,7 @@ numpy==1.22.2
pandas==1.4.1 pandas==1.4.1
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==1.74.22 ccxt==1.74.63
# Pin cryptography for now due to rust build errors with piwheels # Pin cryptography for now due to rust build errors with piwheels
cryptography==36.0.1 cryptography==36.0.1
aiohttp==3.8.1 aiohttp==3.8.1
@ -31,7 +31,7 @@ python-rapidjson==1.6
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.74.0 fastapi==0.74.1
uvicorn==0.17.5 uvicorn==0.17.5
pyjwt==2.3.0 pyjwt==2.3.0
aiofiles==0.8.0 aiofiles==0.8.0

View File

@ -42,7 +42,7 @@ setup(
], ],
install_requires=[ install_requires=[
# from requirements.txt # from requirements.txt
'ccxt>=1.73.1', 'ccxt>=1.74.17',
'SQLAlchemy', 'SQLAlchemy',
'python-telegram-bot>=13.4', 'python-telegram-bot>=13.4',
'arrow>=0.17.0', 'arrow>=0.17.0',

View File

@ -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')

View File

@ -11,6 +11,7 @@ from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re
from tests.exchange.test_exchange import ccxt_exceptionhandlers from tests.exchange.test_exchange import ccxt_exceptionhandlers
@pytest.mark.parametrize('trademode', [TradingMode.FUTURES, TradingMode.SPOT])
@pytest.mark.parametrize('limitratio,expected,side', [ @pytest.mark.parametrize('limitratio,expected,side', [
(None, 220 * 0.99, "sell"), (None, 220 * 0.99, "sell"),
(0.99, 220 * 0.99, "sell"), (0.99, 220 * 0.99, "sell"),
@ -19,16 +20,10 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers
(0.99, 220 * 1.01, "buy"), (0.99, 220 * 1.01, "buy"),
(0.98, 220 * 1.02, "buy"), (0.98, 220 * 1.02, "buy"),
]) ])
def test_stoploss_order_binance( def test_stoploss_order_binance(default_conf, mocker, limitratio, expected, side, trademode):
default_conf,
mocker,
limitratio,
expected,
side
):
api_mock = MagicMock() api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop_loss_limit' order_type = 'stop_loss_limit' if trademode == TradingMode.SPOT else 'stop'
api_mock.create_order = MagicMock(return_value={ api_mock.create_order = MagicMock(return_value={
'id': order_id, 'id': order_id,
@ -37,6 +32,8 @@ def test_stoploss_order_binance(
} }
}) })
default_conf['dry_run'] = False default_conf['dry_run'] = False
default_conf['margin_mode'] = MarginMode.ISOLATED
default_conf['trading_mode'] = trademode
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
@ -72,7 +69,11 @@ def test_stoploss_order_binance(
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
# Price should be 1% below stopprice # Price should be 1% below stopprice
assert api_mock.create_order.call_args_list[0][1]['price'] == expected assert api_mock.create_order.call_args_list[0][1]['price'] == expected
assert api_mock.create_order.call_args_list[0][1]['params'] == {'stopPrice': 220} if trademode == TradingMode.SPOT:
params_dict = {'stopPrice': 220}
else:
params_dict = {'stopPrice': 220, 'reduceOnly': True}
assert api_mock.create_order.call_args_list[0][1]['params'] == params_dict
# test exception handling # test exception handling
with pytest.raises(DependencyException): with pytest.raises(DependencyException):

View File

@ -82,6 +82,13 @@ EXCHANGES = {
'leverage_tiers_public': True, 'leverage_tiers_public': True,
'leverage_in_spot_market': True, 'leverage_in_spot_market': True,
}, },
'huobi': {
'pair': 'BTC/USDT',
'stake_currency': 'USDT',
'hasQuoteVolume': True,
'timeframe': '5m',
'futures': False,
},
'bitvavo': { 'bitvavo': {
'pair': 'BTC/EUR', 'pair': 'BTC/EUR',
'stake_currency': 'EUR', 'stake_currency': 'EUR',
@ -198,7 +205,10 @@ class TestCCXTExchange():
else: else:
next_limit = exchange.get_next_limit_in_list( next_limit = exchange.get_next_limit_in_list(
val, l2_limit_range, l2_limit_range_required) val, l2_limit_range, l2_limit_range_required)
if next_limit is None or next_limit > 200: if next_limit is None:
assert len(l2['asks']) > 100
assert len(l2['asks']) > 100
elif next_limit > 200:
# Large orderbook sizes can be a problem for some exchanges (bitrex ...) # Large orderbook sizes can be a problem for some exchanges (bitrex ...)
assert len(l2['asks']) > 200 assert len(l2['asks']) > 200
assert len(l2['asks']) > 200 assert len(l2['asks']) > 200
@ -313,7 +323,7 @@ class TestCCXTExchange():
def test_ccxt_get_max_leverage_spot(self, exchange): def test_ccxt_get_max_leverage_spot(self, exchange):
spot, spot_name = exchange spot, spot_name = exchange
if spot: if spot:
leverage_in_market_spot = EXCHANGES[spot_name]['leverage_in_spot_market'] leverage_in_market_spot = EXCHANGES[spot_name].get('leverage_in_spot_market')
if leverage_in_market_spot: if leverage_in_market_spot:
spot_pair = EXCHANGES[spot_name].get('pair', EXCHANGES[spot_name]['pair']) spot_pair = EXCHANGES[spot_name].get('pair', EXCHANGES[spot_name]['pair'])
spot_leverage = spot.get_max_leverage(spot_pair, 20) spot_leverage = spot.get_max_leverage(spot_pair, 20)
@ -323,7 +333,7 @@ class TestCCXTExchange():
def test_ccxt_get_max_leverage_futures(self, exchange_futures): def test_ccxt_get_max_leverage_futures(self, exchange_futures):
futures, futures_name = exchange_futures futures, futures_name = exchange_futures
if futures: if futures:
leverage_tiers_public = EXCHANGES[futures_name]['leverage_tiers_public'] leverage_tiers_public = EXCHANGES[futures_name].get('leverage_tiers_public')
if leverage_tiers_public: if leverage_tiers_public:
futures_pair = EXCHANGES[futures_name].get( futures_pair = EXCHANGES[futures_name].get(
'futures_pair', 'futures_pair',
@ -346,7 +356,7 @@ class TestCCXTExchange():
def test_ccxt_load_leverage_tiers(self, exchange_futures): def test_ccxt_load_leverage_tiers(self, exchange_futures):
futures, futures_name = exchange_futures futures, futures_name = exchange_futures
if futures and EXCHANGES[futures_name]['leverage_tiers_public']: if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
leverage_tiers = futures.load_leverage_tiers() leverage_tiers = futures.load_leverage_tiers()
futures_pair = EXCHANGES[futures_name].get( futures_pair = EXCHANGES[futures_name].get(
'futures_pair', 'futures_pair',
@ -379,7 +389,7 @@ class TestCCXTExchange():
def test_ccxt_dry_run_liquidation_price(self, exchange_futures): def test_ccxt_dry_run_liquidation_price(self, exchange_futures):
futures, futures_name = exchange_futures futures, futures_name = exchange_futures
if futures and EXCHANGES[futures_name]['leverage_tiers_public']: if futures and EXCHANGES[futures_name].get('leverage_tiers_public'):
futures_pair = EXCHANGES[futures_name].get( futures_pair = EXCHANGES[futures_name].get(
'futures_pair', 'futures_pair',

View File

@ -168,7 +168,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
exchange = ExchangeResolver.load_exchange('huobi', default_conf) exchange = ExchangeResolver.load_exchange('zaif', default_conf)
assert isinstance(exchange, Exchange) assert isinstance(exchange, Exchange)
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog)
caplog.clear() caplog.clear()

View File

@ -0,0 +1,117 @@
from random import randint
from unittest.mock import MagicMock
import ccxt
import pytest
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@pytest.mark.parametrize('limitratio,expected,side', [
(None, 220 * 0.99, "sell"),
(0.99, 220 * 0.99, "sell"),
(0.98, 220 * 0.98, "sell"),
])
def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected, side):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'stop-limit'
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
side=side,
leverage=1.0)
api_mock.create_order.reset_mock()
order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio}
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types,
side=side, leverage=1.0)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
# Price should be 1% below stopprice
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
assert api_mock.create_order.call_args_list[0][1]['params'] == {"stopPrice": 220,
"operator": "lte",
}
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side=side, leverage=1.0)
with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side=side, leverage=1.0)
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi",
"stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
side=side, leverage=1.0)
def test_stoploss_order_dry_run_huobi(default_conf, mocker):
api_mock = MagicMock()
order_type = 'stop-limit'
default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'huobi')
with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
side='sell', leverage=1.0)
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side='sell', leverage=1.0)
assert 'id' in order
assert 'info' in order
assert 'type' in order
assert order['type'] == order_type
assert order['price'] == 220
assert order['amount'] == 1
def test_stoploss_adjust_huobi(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf, id='huobi')
order = {
'type': 'stop',
'price': 1500,
'stopPrice': '1500',
}
assert exchange.stoploss_adjust(1501, order, 'sell')
assert not exchange.stoploss_adjust(1499, order, 'sell')
# Test with invalid order case
order['type'] = 'stop_loss'
assert not exchange.stoploss_adjust(1501, order, 'sell')

View File

@ -0,0 +1,127 @@
from random import randint
from unittest.mock import MagicMock
import ccxt
import pytest
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
from tests.conftest import get_patched_exchange
from tests.exchange.test_exchange import ccxt_exceptionhandlers
@pytest.mark.parametrize('order_type', ['market', 'limit'])
@pytest.mark.parametrize('limitratio,expected,side', [
(None, 220 * 0.99, "sell"),
(0.99, 220 * 0.99, "sell"),
(0.98, 220 * 0.98, "sell"),
])
def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, side, order_type):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
if order_type == 'limit':
with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={
'stoploss': order_type,
'stoploss_on_exchange_limit_ratio': 1.05},
side=side, leverage=1.0)
api_mock.create_order.reset_mock()
order_types = {'stoploss': order_type}
if limitratio is not None:
order_types.update({'stoploss_on_exchange_limit_ratio': limitratio})
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types=order_types, side=side, leverage=1.0)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
# Price should be 1% below stopprice
if order_type == 'limit':
assert api_mock.create_order.call_args_list[0][1]['price'] == expected
else:
assert api_mock.create_order.call_args_list[0][1]['price'] is None
assert api_mock.create_order.call_args_list[0][1]['params'] == {
'stopPrice': 220,
'stop': 'loss'
}
# test exception handling
with pytest.raises(DependencyException):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side=side, leverage=1.0)
with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(
side_effect=ccxt.InvalidOrder("kucoin Order would trigger immediately."))
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side=side, leverage=1.0)
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin",
"stoploss", "create_order", retries=1,
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
side=side, leverage=1.0)
def test_stoploss_order_dry_run_kucoin(default_conf, mocker):
api_mock = MagicMock()
order_type = 'market'
default_conf['dry_run'] = True
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin')
with pytest.raises(OperationalException):
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
order_types={'stoploss': 'limit',
'stoploss_on_exchange_limit_ratio': 1.05},
side='sell', leverage=1.0)
api_mock.create_order.reset_mock()
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
order_types={}, side='sell', leverage=1.0)
assert 'id' in order
assert 'info' in order
assert 'type' in order
assert order['type'] == order_type
assert order['price'] == 220
assert order['amount'] == 1
def test_stoploss_adjust_kucoin(mocker, default_conf):
exchange = get_patched_exchange(mocker, default_conf, id='kucoin')
order = {
'type': 'limit',
'price': 1500,
'stopPrice': 1500,
'info': {'stopPrice': 1500, 'stop': "limit"},
}
assert exchange.stoploss_adjust(1501, order, 'sell')
assert not exchange.stoploss_adjust(1499, order, 'sell')
# Test with invalid order case
order['info']['stop'] = None
assert not exchange.stoploss_adjust(1501, order, 'sell')

View File

@ -80,7 +80,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'close_rate': None, 'close_rate': None,
'current_rate': 1.099e-05, 'current_rate': 1.099e-05,
'amount': 91.07468123, 'amount': 91.07468123,
'amount_requested': 91.07468123, 'amount_requested': 91.07468124,
'stake_amount': 0.001, 'stake_amount': 0.001,
'trade_duration': None, 'trade_duration': None,
'trade_duration_s': None, 'trade_duration_s': None,
@ -116,14 +116,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'is_short': False, 'is_short': False,
'funding_fees': 0.0, 'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT, 'trading_mode': TradingMode.SPOT,
'filled_entry_orders': [{ 'orders': [{
'amount': 91.07468123, 'average': 1.098e-05, 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
'is_open': False, 'pair': 'ETH/BTC', 'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
'remaining': ANY, 'status': ANY}], 'remaining': ANY, 'status': ANY, 'ft_is_entry': True,
'filled_exit_orders': [], }],
} }
mocker.patch('freqtrade.exchange.Exchange.get_rate', mocker.patch('freqtrade.exchange.Exchange.get_rate',
@ -162,7 +162,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'close_rate': None, 'close_rate': None,
'current_rate': ANY, 'current_rate': ANY,
'amount': 91.07468123, 'amount': 91.07468123,
'amount_requested': 91.07468123, 'amount_requested': 91.07468124,
'trade_duration': ANY, 'trade_duration': ANY,
'trade_duration_s': ANY, 'trade_duration_s': ANY,
'stake_amount': 0.001, 'stake_amount': 0.001,
@ -198,14 +198,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'is_short': False, 'is_short': False,
'funding_fees': 0.0, 'funding_fees': 0.0,
'trading_mode': TradingMode.SPOT, 'trading_mode': TradingMode.SPOT,
'filled_entry_orders': [{ 'orders': [{
'amount': 91.07468123, 'average': 1.098e-05, 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
'is_open': False, 'pair': 'ETH/BTC', 'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY,
'remaining': ANY, 'status': ANY}], 'remaining': ANY, 'status': ANY, 'ft_is_entry': True,
'filled_exit_orders': [], }],
} }

View File

@ -971,6 +971,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short,
'interest_rate': 0.0, 'interest_rate': 0.0,
'funding_fees': None, 'funding_fees': None,
'trading_mode': ANY, 'trading_mode': ANY,
'orders': [ANY],
} }
mocker.patch('freqtrade.exchange.Exchange.get_rate', mocker.patch('freqtrade.exchange.Exchange.get_rate',
@ -1044,8 +1045,8 @@ def test_api_blacklist(botclient, mocker):
"NOTHING/BTC": { "NOTHING/BTC": {
"error_msg": "Pair NOTHING/BTC is not in the current blacklist." "error_msg": "Pair NOTHING/BTC is not in the current blacklist."
} }
}, },
} }
rc = client_delete( rc = client_delete(
client, client,
f"{BASE_URI}/blacklist?pairs_to_delete=HOT/BTC&pairs_to_delete=ETH/BTC") f"{BASE_URI}/blacklist?pairs_to_delete=HOT/BTC&pairs_to_delete=ETH/BTC")
@ -1170,6 +1171,7 @@ def test_api_forceentry(botclient, mocker, fee, endpoint):
'interest_rate': None, 'interest_rate': None,
'funding_fees': None, 'funding_fees': None,
'trading_mode': 'spot', 'trading_mode': 'spot',
'orders': [],
} }
@ -1452,6 +1454,11 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir):
ftbot, client = botclient ftbot, client = botclient
mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee)
rc = client_get(client, f"{BASE_URI}/backtest")
# Backtest prevented in default mode
assert_response(rc, 502)
ftbot.config['runmode'] = RunMode.WEBSERVER
# Backtesting not started yet # Backtesting not started yet
rc = client_get(client, f"{BASE_URI}/backtest") rc = client_get(client, f"{BASE_URI}/backtest")
assert_response(rc) assert_response(rc)

View File

@ -208,6 +208,7 @@ def test_telegram_status(default_conf, update, mocker) -> None:
'is_open': True, 'is_open': True,
'is_short': False, 'is_short': False,
'filled_entry_orders': [], 'filled_entry_orders': [],
'orders': []
}]), }]),
) )
@ -240,6 +241,8 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
create_mock_trades(fee) create_mock_trades(fee)
trades = Trade.get_open_trades() trades = Trade.get_open_trades()
trade = trades[0] trade = trades[0]
# Average may be empty on some exchanges
trade.orders[0].average = 0
trade.orders.append(Order( trade.orders.append(Order(
order_id='5412vbb', order_id='5412vbb',
ft_order_side='buy', ft_order_side='buy',
@ -250,7 +253,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
order_type="market", order_type="market",
side="buy", side="buy",
price=trade.open_rate * 0.95, price=trade.open_rate * 0.95,
average=trade.open_rate * 0.95, average=0,
filled=trade.amount, filled=trade.amount,
remaining=0, remaining=0,
cost=trade.amount, cost=trade.amount,
@ -626,7 +629,7 @@ def test_weekly_wrong_input(default_conf, update, ticker, mocker) -> None:
context.args = ["this week"] context.args = ["this week"]
telegram._weekly(update=update, context=context) telegram._weekly(update=update, context=context)
assert str('Weekly Profit over the last 8 weeks (starting from Monday)</b>:') \ assert str('Weekly Profit over the last 8 weeks (starting from Monday)</b>:') \
in msg_mock.call_args_list[0][0][0] in msg_mock.call_args_list[0][0][0]
def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee, def test_monthly_handle(default_conf, update, ticker, limit_buy_order, fee,
@ -1872,7 +1875,7 @@ def test_send_msg_protection_notification(default_conf, mocker, time_machine) ->
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_01', 1.0), (RPCMessageType.BUY_FILL, 'Longed', 'long_signal_01', 1.0),
(RPCMessageType.BUY_FILL, 'Longed', 'long_signal_02', 2.0), (RPCMessageType.BUY_FILL, 'Longed', 'long_signal_02', 2.0),
(RPCMessageType.SHORT_FILL, 'Shorted', 'short_signal_01', 2.0), (RPCMessageType.SHORT_FILL, 'Shorted', 'short_signal_01', 2.0),
]) ])
def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, entered, def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, entered,
enter_signal, leverage) -> None: enter_signal, leverage) -> None:
@ -1902,7 +1905,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker, message_type, ente
f"{leverage_text}" f"{leverage_text}"
'*Open Rate:* `0.00001099`\n' '*Open Rate:* `0.00001099`\n'
'*Total:* `(0.01465333 BTC, 180.895 USD)`' '*Total:* `(0.01465333 BTC, 180.895 USD)`'
) )
def test_send_msg_sell_notification(default_conf, mocker) -> None: def test_send_msg_sell_notification(default_conf, mocker) -> None:
@ -1944,7 +1947,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n' '*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`' '*Close Rate:* `0.00003201`'
) )
msg_mock.reset_mock() msg_mock.reset_mock()
telegram.send_msg({ telegram.send_msg({
@ -1978,7 +1981,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n' '*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`' '*Close Rate:* `0.00003201`'
) )
# Reset singleton function to avoid random breaks # Reset singleton function to avoid random breaks
telegram._rpc._fiat_converter.convert_amount = old_convamount telegram._rpc._fiat_converter.convert_amount = old_convamount
@ -2142,7 +2145,7 @@ def test_send_msg_buy_notification_no_fiat(
('Long', 'long_signal_01', 1.0), ('Long', 'long_signal_01', 1.0),
('Long', 'long_signal_01', 5.0), ('Long', 'long_signal_01', 5.0),
('Short', 'short_signal_01', 2.0), ('Short', 'short_signal_01', 2.0),
]) ])
def test_send_msg_sell_notification_no_fiat( def test_send_msg_sell_notification_no_fiat(
default_conf, mocker, direction, enter_signal, leverage) -> None: default_conf, mocker, direction, enter_signal, leverage) -> None:
del default_conf['fiat_display_currency'] del default_conf['fiat_display_currency']
@ -2184,7 +2187,7 @@ def test_send_msg_sell_notification_no_fiat(
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n' '*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`' '*Close Rate:* `0.00003201`'
) )
@pytest.mark.parametrize('msg,expected', [ @pytest.mark.parametrize('msg,expected', [

View File

@ -6,7 +6,7 @@ import time
from copy import deepcopy from copy import deepcopy
from math import isclose from math import isclose
from typing import List from typing import List
from unittest.mock import ANY, MagicMock, PropertyMock from unittest.mock import ANY, MagicMock, PropertyMock, patch
import arrow import arrow
import pytest import pytest
@ -1407,7 +1407,7 @@ def test_handle_stoploss_on_exchange_trailing(
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT') cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
stoploss_order_mock.assert_called_once_with( stoploss_order_mock.assert_called_once_with(
amount=amt, amount=pytest.approx(amt),
pair='ETH/USDT', pair='ETH/USDT',
order_types=freqtrade.strategy.order_types, order_types=freqtrade.strategy.order_types,
stop_price=stop_price[1], stop_price=stop_price[1],
@ -1611,7 +1611,7 @@ def test_handle_stoploss_on_exchange_custom_stop(
cancel_order_mock.assert_called_once_with(100, 'ETH/USDT') cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
# Long uses modified ask - offset, short modified bid + offset # Long uses modified ask - offset, short modified bid + offset
stoploss_order_mock.assert_called_once_with( stoploss_order_mock.assert_called_once_with(
amount=trade.amount, amount=pytest.approx(trade.amount),
pair='ETH/USDT', pair='ETH/USDT',
order_types=freqtrade.strategy.order_types, order_types=freqtrade.strategy.order_types,
stop_price=4.4 * 0.96 if not is_short else 0.95 * 1.04, stop_price=4.4 * 0.96 if not is_short else 0.95 * 1.04,
@ -1741,7 +1741,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
assert trade.stop_loss == 4.4 * 0.99 assert trade.stop_loss == 4.4 * 0.99
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
stoploss_order_mock.assert_called_once_with( stoploss_order_mock.assert_called_once_with(
amount=11.41438356, amount=pytest.approx(11.41438356),
pair='NEO/BTC', pair='NEO/BTC',
order_types=freqtrade.strategy.order_types, order_types=freqtrade.strategy.order_types,
stop_price=4.4 * 0.99, stop_price=4.4 * 0.99,
@ -2534,9 +2534,14 @@ def test_check_handle_timedout_sell_usercustom(
et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
caplog.clear() caplog.clear()
# 2nd canceled trade ... # 2nd canceled trade ...
open_trade_usdt.open_order_id = limit_sell_order_old['id'] open_trade_usdt.open_order_id = limit_sell_order_old['id']
# If cancelling fails - no emergency sell!
with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False):
freqtrade.check_handle_timedout()
assert et_mock.call_count == 0
freqtrade.check_handle_timedout() freqtrade.check_handle_timedout()
assert log_has_re('Emergencyselling trade.*', caplog) assert log_has_re('Emergencyselling trade.*', caplog)
assert et_mock.call_count == 1 assert et_mock.call_count == 1
@ -2896,9 +2901,12 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
exchange='binance', exchange='binance',
open_rate=0.245441, open_rate=0.245441,
open_order_id="123456", open_order_id="123456",
open_date=arrow.utcnow().datetime, open_date=arrow.utcnow().shift(days=-2).datetime,
fee_open=fee.return_value, fee_open=fee.return_value,
fee_close=fee.return_value, fee_close=fee.return_value,
close_rate=0.555,
close_date=arrow.utcnow().datetime,
sell_reason="sell_reason_whatever",
) )
order = {'remaining': 1, order = {'remaining': 1,
'amount': 1, 'amount': 1,
@ -2907,17 +2915,23 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None:
assert freqtrade.handle_cancel_exit(trade, order, reason) assert freqtrade.handle_cancel_exit(trade, order, reason)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert send_msg_mock.call_count == 1 assert send_msg_mock.call_count == 1
assert trade.close_rate is None
assert trade.sell_reason is None
send_msg_mock.reset_mock() send_msg_mock.reset_mock()
order['amount'] = 2 order['amount'] = 2
assert freqtrade.handle_cancel_exit(trade, order, reason assert not freqtrade.handle_cancel_exit(trade, order, reason)
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
# Assert cancel_order was not called (callcount remains unchanged) # Assert cancel_order was not called (callcount remains unchanged)
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert send_msg_mock.call_count == 1 assert send_msg_mock.call_count == 1
assert freqtrade.handle_cancel_exit(trade, order, reason assert (send_msg_mock.call_args_list[0][0][0]['reason']
) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'])
assert not freqtrade.handle_cancel_exit(trade, order, reason)
send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
# Message should not be iterated again # Message should not be iterated again
assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']
assert send_msg_mock.call_count == 1 assert send_msg_mock.call_count == 1
@ -2936,7 +2950,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
order = {'remaining': 1, order = {'remaining': 1,
'amount': 1, 'amount': 1,
'status': "open"} 'status': "open"}
assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order' assert not freqtrade.handle_cancel_exit(trade, order, reason)
@pytest.mark.parametrize("is_short, open_rate, amt", [ @pytest.mark.parametrize("is_short, open_rate, amt", [
@ -3001,7 +3015,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
'pair': 'ETH/USDT', 'pair': 'ETH/USDT',
'gain': 'profit', 'gain': 'profit',
'limit': 2.0 if is_short else 2.2, 'limit': 2.0 if is_short else 2.2,
'amount': amt, 'amount': pytest.approx(amt),
'order_type': 'limit', 'order_type': 'limit',
'buy_tag': None, 'buy_tag': None,
'direction': 'Short' if trade.is_short else 'Long', 'direction': 'Short' if trade.is_short else 'Long',
@ -3062,7 +3076,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
'leverage': 1.0, 'leverage': 1.0,
'gain': 'loss', 'gain': 'loss',
'limit': 2.2 if is_short else 2.01, 'limit': 2.2 if is_short else 2.01,
'amount': 29.70297029 if is_short else 30.0, 'amount': pytest.approx(29.70297029) if is_short else 30.0,
'order_type': 'limit', 'order_type': 'limit',
'buy_tag': None, 'buy_tag': None,
'enter_tag': None, 'enter_tag': None,
@ -3142,13 +3156,13 @@ def test_execute_trade_exit_custom_exit_price(
'leverage': 1.0, 'leverage': 1.0,
'gain': profit_or_loss, 'gain': profit_or_loss,
'limit': limit, 'limit': limit,
'amount': amount, 'amount': pytest.approx(amount),
'order_type': 'limit', 'order_type': 'limit',
'buy_tag': None, 'buy_tag': None,
'enter_tag': None, 'enter_tag': None,
'open_rate': open_rate, 'open_rate': open_rate,
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit_amount, 'profit_amount': pytest.approx(profit_amount),
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
@ -3209,7 +3223,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
'leverage': 1.0, 'leverage': 1.0,
'gain': 'loss', 'gain': 'loss',
'limit': 2.02 if is_short else 1.98, 'limit': 2.02 if is_short else 1.98,
'amount': 29.70297029 if is_short else 30.0, 'amount': pytest.approx(29.70297029 if is_short else 30.0),
'order_type': 'limit', 'order_type': 'limit',
'buy_tag': None, 'buy_tag': None,
'enter_tag': None, 'enter_tag': None,
@ -3472,13 +3486,13 @@ def test_execute_trade_exit_market_order(
'leverage': 1.0, 'leverage': 1.0,
'gain': profit_or_loss, 'gain': profit_or_loss,
'limit': limit, 'limit': limit,
'amount': round(amount, 9), 'amount': pytest.approx(amount),
'order_type': 'market', 'order_type': 'market',
'buy_tag': None, 'buy_tag': None,
'enter_tag': None, 'enter_tag': None,
'open_rate': open_rate, 'open_rate': open_rate,
'current_rate': current_rate, 'current_rate': current_rate,
'profit_amount': profit_amount, 'profit_amount': pytest.approx(profit_amount),
'profit_ratio': profit_ratio, 'profit_ratio': profit_ratio,
'stake_currency': 'USDT', 'stake_currency': 'USDT',
'fiat_currency': 'USD', 'fiat_currency': 'USD',

View File

@ -1608,8 +1608,7 @@ def test_to_json(default_conf, fee):
'is_short': None, 'is_short': None,
'trading_mode': None, 'trading_mode': None,
'funding_fees': None, 'funding_fees': None,
'filled_entry_orders': [], 'orders': [],
'filled_exit_orders': [],
} }
# Simulate dry_run entries # Simulate dry_run entries
@ -1684,8 +1683,7 @@ def test_to_json(default_conf, fee):
'is_short': None, 'is_short': None,
'trading_mode': None, 'trading_mode': None,
'funding_fees': None, 'funding_fees': None,
'filled_entry_orders': [], 'orders': [],
'filled_exit_orders': [],
} }