Merged feat/short into lev-strat
This commit is contained in:
parent
a89c67787b
commit
778f0d9d0a
@ -165,6 +165,7 @@ Example to remove the first 10 pairs from the pairlist:
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
"pairlists": [
|
"pairlists": [
|
||||||
|
// ...
|
||||||
{
|
{
|
||||||
"method": "OffsetFilter",
|
"method": "OffsetFilter",
|
||||||
"offset": 10
|
"offset": 10
|
||||||
@ -190,6 +191,19 @@ Sorts pairs by past trade performance, as follows:
|
|||||||
|
|
||||||
Trade count is used as a tie breaker.
|
Trade count is used as a tie breaker.
|
||||||
|
|
||||||
|
You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window).
|
||||||
|
Not defining this parameter (or setting it to 0) will use all-time performance.
|
||||||
|
|
||||||
|
```json
|
||||||
|
"pairlists": [
|
||||||
|
// ...
|
||||||
|
{
|
||||||
|
"method": "PerformanceFilter",
|
||||||
|
"minutes": 1440 // rolling 24h
|
||||||
|
}
|
||||||
|
],
|
||||||
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
`PerformanceFilter` does not support backtesting mode.
|
`PerformanceFilter` does not support backtesting mode.
|
||||||
|
|
||||||
|
@ -15,3 +15,7 @@ For longs, the currency which pays the interest fee for the `borrowed` will alre
|
|||||||
Rollover fee = P (borrowed money) * R (quat_hourly_interest) * ceiling(T/4) (in hours)
|
Rollover fee = P (borrowed money) * R (quat_hourly_interest) * ceiling(T/4) (in hours)
|
||||||
I (interest) = Opening fee + Rollover fee
|
I (interest) = Opening fee + Rollover fee
|
||||||
[source](https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-)
|
[source](https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-)
|
||||||
|
|
||||||
|
# TODO-lev: Mention that says you can't run 2 bots on the same account with leverage,
|
||||||
|
|
||||||
|
#TODO-lev: Create a huge risk disclaimer
|
@ -288,6 +288,12 @@ Stoploss values returned from `custom_stoploss()` always specify a percentage re
|
|||||||
|
|
||||||
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||||
|
|
||||||
|
### Calculating stoploss percentage from absolute price
|
||||||
|
|
||||||
|
Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price.
|
||||||
|
|
||||||
|
The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`.
|
||||||
|
|
||||||
#### Stepped stoploss
|
#### Stepped stoploss
|
||||||
|
|
||||||
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit.
|
||||||
|
@ -639,6 +639,167 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
|
|||||||
|
|
||||||
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
|
Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation.
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings.
|
||||||
|
This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade
|
||||||
|
is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `sell_reason` in
|
||||||
|
`confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when
|
||||||
|
`current_profit < open_relative_stop`.
|
||||||
|
|
||||||
|
### *stoploss_from_absolute()*
|
||||||
|
|
||||||
|
In some situations it may be confusing to deal with stops relative to current rate. Instead, you may define a stoploss level using an absolute price.
|
||||||
|
|
||||||
|
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
|
||||||
|
|
||||||
|
If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)`.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy, stoploss_from_open
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
use_custom_stoploss = True
|
||||||
|
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
|
||||||
|
current_rate: float, current_profit: float, **kwargs) -> float:
|
||||||
|
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||||||
|
candle = dataframe.iloc[-1].squeeze()
|
||||||
|
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### *@informative()*
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def informative(timeframe: str, asset: str = '',
|
||||||
|
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
|
||||||
|
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||||
|
"""
|
||||||
|
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||||
|
define informative indicators.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||||
|
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||||
|
current pair.
|
||||||
|
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||||
|
specified, defaults to:
|
||||||
|
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||||
|
* {column}_{timeframe} if asset is not specified.
|
||||||
|
Format string supports these format variables:
|
||||||
|
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||||
|
* {base} - base currency in lower case, for example 'eth'.
|
||||||
|
* {BASE} - same as {base}, except in upper case.
|
||||||
|
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||||
|
* {QUOTE} - same as {quote}, except in upper case.
|
||||||
|
* {column} - name of dataframe column.
|
||||||
|
* {timeframe} - timeframe of informative dataframe.
|
||||||
|
:param ffill: ffill dataframe after merging informative pair.
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation,
|
||||||
|
not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method.
|
||||||
|
When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter)
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
??? Example "Fast and easy way to define informative pairs"
|
||||||
|
|
||||||
|
Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy import IStrategy, informative
|
||||||
|
|
||||||
|
class AwesomeStrategy(IStrategy):
|
||||||
|
|
||||||
|
# This method is not required.
|
||||||
|
# def informative_pairs(self): ...
|
||||||
|
|
||||||
|
# Define informative upper timeframe for each pair. Decorators can be stacked on same
|
||||||
|
# method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'.
|
||||||
|
@informative('30m')
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
|
||||||
|
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
|
||||||
|
# instead of hardcoding actual stake currency. Available in populate_indicators and other
|
||||||
|
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
|
||||||
|
@informative('1h', 'BTC/{stake}')
|
||||||
|
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/ETH informative pair. You must specify quote currency if it is different from
|
||||||
|
# stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'.
|
||||||
|
@informative('1h', 'ETH/BTC')
|
||||||
|
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Define BTC/STAKE informative pair. A custom formatter may be specified for formatting
|
||||||
|
# column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom
|
||||||
|
# formatting. Available in populate_indicators and other methods as 'rsi_upper'.
|
||||||
|
@informative('1h', 'BTC/{stake}', '{column}')
|
||||||
|
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# Strategy timeframe indicators for current pair.
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
# Informative pairs are available in this method.
|
||||||
|
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs
|
||||||
|
manually as described [in the DataProvider section](#complete-data-provider-sample).
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
stake = self.config['stake_currency']
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
(dataframe[f'btc_{stake}_rsi_1h'] < 35)
|
||||||
|
&
|
||||||
|
(dataframe['volume'] > 0)
|
||||||
|
),
|
||||||
|
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
|
||||||
|
|
||||||
|
return dataframe
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`.
|
||||||
|
|
||||||
|
!!! Warning "Duplicate method names"
|
||||||
|
Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method)
|
||||||
|
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
|
||||||
|
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
|
||||||
|
|
||||||
## Additional data (Wallets)
|
## Additional data (Wallets)
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
if epochs and export_csv:
|
if epochs and export_csv:
|
||||||
HyperoptTools.export_csv_file(
|
HyperoptTools.export_csv_file(
|
||||||
config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv
|
config, epochs, export_csv
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ class Edge:
|
|||||||
)
|
)
|
||||||
# Download informative pairs too
|
# Download informative pairs too
|
||||||
res = defaultdict(list)
|
res = defaultdict(list)
|
||||||
for p, t in self.strategy.informative_pairs():
|
for p, t in self.strategy.gather_informative_pairs():
|
||||||
res[t].append(p)
|
res[t].append(p)
|
||||||
for timeframe, inf_pairs in res.items():
|
for timeframe, inf_pairs in res.items():
|
||||||
timerange_startup = deepcopy(self._timerange)
|
timerange_startup = deepcopy(self._timerange)
|
||||||
|
@ -20,4 +20,7 @@ class Bibox(Exchange):
|
|||||||
|
|
||||||
# fetchCurrencies API point requires authentication for Bibox,
|
# fetchCurrencies API point requires authentication for Bibox,
|
||||||
# so switch it off for Freqtrade load_markets()
|
# so switch it off for Freqtrade load_markets()
|
||||||
_ccxt_config: Dict = {"has": {"fetchCurrencies": False}}
|
@property
|
||||||
|
def _ccxt_config(self) -> Dict:
|
||||||
|
# Parameters to add directly to ccxt sync/async initialization.
|
||||||
|
return {"has": {"fetchCurrencies": False}}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
""" Binance exchange subclass """
|
""" Binance exchange subclass """
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
|
from freqtrade.enums import Collateral, TradingMode
|
||||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||||
OperationalException, TemporaryError)
|
OperationalException, TemporaryError)
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
@ -26,36 +29,74 @@ class Binance(Exchange):
|
|||||||
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
|
||||||
}
|
}
|
||||||
|
|
||||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
||||||
|
# TradingMode.SPOT always supported and not required in this list
|
||||||
|
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
|
||||||
|
# (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported
|
||||||
|
# (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _ccxt_config(self) -> Dict:
|
||||||
|
# Parameters to add directly to ccxt sync/async initialization.
|
||||||
|
if self.trading_mode == TradingMode.MARGIN:
|
||||||
|
return {
|
||||||
|
"options": {
|
||||||
|
"defaultType": "margin"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
elif self.trading_mode == TradingMode.FUTURES:
|
||||||
|
return {
|
||||||
|
"options": {
|
||||||
|
"defaultType": "future"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Verify stop_loss against stoploss-order value (limit or price)
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
Returns True if adjustment is necessary.
|
Returns True if adjustment is necessary.
|
||||||
|
:param side: "buy" or "sell"
|
||||||
"""
|
"""
|
||||||
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
|
||||||
|
return order['type'] == 'stop_loss_limit' and (
|
||||||
|
(side == "sell" and stop_loss > float(order['info']['stopPrice'])) or
|
||||||
|
(side == "buy" and stop_loss < float(order['info']['stopPrice']))
|
||||||
|
)
|
||||||
|
|
||||||
@retrier(retries=0)
|
@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, side: str, leverage: float) -> Dict:
|
||||||
"""
|
"""
|
||||||
creates a stoploss limit order.
|
creates a stoploss limit order.
|
||||||
this stoploss-limit is binance-specific.
|
this stoploss-limit is binance-specific.
|
||||||
It may work with a limited number of other exchanges, but this has not been tested yet.
|
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 threshold: As limit price should always be below stop-price
|
||||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
rate = stop_price * limit_price_pct
|
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_loss_limit"
|
ordertype = "stop_loss_limit"
|
||||||
|
|
||||||
stop_price = self.price_to_precision(pair, stop_price)
|
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
|
# Ensure rate is less than stop price
|
||||||
if stop_price <= rate:
|
if bad_stop_price:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'In stoploss limit order, stop price should be more than limit price')
|
'In stoploss limit order, stop price should be better than limit price')
|
||||||
|
|
||||||
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, side, amount, stop_price, leverage)
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -66,7 +107,8 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
rate = self.price_to_precision(pair, rate)
|
rate = self.price_to_precision(pair, rate)
|
||||||
|
|
||||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
self._lev_prep(pair, leverage)
|
||||||
|
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||||
amount=amount, price=rate, params=params)
|
amount=amount, price=rate, params=params)
|
||||||
logger.info('stoploss limit order added for %s. '
|
logger.info('stoploss limit order added for %s. '
|
||||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||||
@ -74,21 +116,96 @@ class Binance(Exchange):
|
|||||||
return order
|
return order
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise InsufficientFundsError(
|
raise InsufficientFundsError(
|
||||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to sell amount {amount} at rate {rate}. '
|
f'Tried to {side} amount {amount} at rate {rate}. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
# Errors:
|
# Errors:
|
||||||
# `binance Order would trigger immediately.`
|
# `binance Order would trigger immediately.`
|
||||||
raise InvalidOrderException(
|
raise InvalidOrderException(
|
||||||
f'Could not create {ordertype} sell order on market {pair}. '
|
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to sell amount {amount} at rate {rate}. '
|
f'Tried to {side} amount {amount} at rate {rate}. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def fill_leverage_brackets(self):
|
||||||
|
"""
|
||||||
|
Assigns property _leverage_brackets to a dictionary of information about the leverage
|
||||||
|
allowed on each pair
|
||||||
|
"""
|
||||||
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
|
try:
|
||||||
|
if self._config['dry_run']:
|
||||||
|
leverage_brackets_path = (
|
||||||
|
Path(__file__).parent / 'binance_leverage_brackets.json'
|
||||||
|
)
|
||||||
|
with open(leverage_brackets_path) as json_file:
|
||||||
|
leverage_brackets = json.load(json_file)
|
||||||
|
else:
|
||||||
|
leverage_brackets = self._api.load_leverage_brackets()
|
||||||
|
|
||||||
|
for pair, brackets in leverage_brackets.items():
|
||||||
|
self._leverage_brackets[pair] = [
|
||||||
|
[
|
||||||
|
min_amount,
|
||||||
|
float(margin_req)
|
||||||
|
] for [
|
||||||
|
min_amount,
|
||||||
|
margin_req
|
||||||
|
] in brackets
|
||||||
|
]
|
||||||
|
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(f'Could not fetch leverage amounts due to'
|
||||||
|
f'{e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
|
||||||
|
"""
|
||||||
|
Returns the maximum leverage that a pair can be traded at
|
||||||
|
:param pair: The base/quote currency pair being traded
|
||||||
|
:nominal_value: The total value of the trade in quote currency (collateral + debt)
|
||||||
|
"""
|
||||||
|
pair_brackets = self._leverage_brackets[pair]
|
||||||
|
max_lev = 1.0
|
||||||
|
for [min_amount, margin_req] in pair_brackets:
|
||||||
|
if nominal_value >= min_amount:
|
||||||
|
max_lev = 1/margin_req
|
||||||
|
return max_lev
|
||||||
|
|
||||||
|
@ retrier
|
||||||
|
def _set_leverage(
|
||||||
|
self,
|
||||||
|
leverage: float,
|
||||||
|
pair: Optional[str] = None,
|
||||||
|
trading_mode: Optional[TradingMode] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Set's the leverage before making a trade, in order to not
|
||||||
|
have the same leverage on every trade
|
||||||
|
"""
|
||||||
|
trading_mode = trading_mode or self.trading_mode
|
||||||
|
|
||||||
|
if self._config['dry_run'] or trading_mode != TradingMode.FUTURES:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._api.set_leverage(symbol=pair, leverage=leverage)
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
1214
freqtrade/exchange/binance_leverage_brackets.json
Normal file
1214
freqtrade/exchange/binance_leverage_brackets.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -22,6 +22,7 @@ from pandas import DataFrame
|
|||||||
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES,
|
from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES,
|
||||||
ListPairsWithTimeframes)
|
ListPairsWithTimeframes)
|
||||||
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
|
||||||
|
from freqtrade.enums import Collateral, TradingMode
|
||||||
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, OperationalException, PricingError,
|
InvalidOrderException, OperationalException, PricingError,
|
||||||
RetryableOrderError, TemporaryError)
|
RetryableOrderError, TemporaryError)
|
||||||
@ -48,9 +49,6 @@ class Exchange:
|
|||||||
|
|
||||||
_config: Dict = {}
|
_config: Dict = {}
|
||||||
|
|
||||||
# Parameters to add directly to ccxt sync/async initialization.
|
|
||||||
_ccxt_config: Dict = {}
|
|
||||||
|
|
||||||
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
|
||||||
_params: Dict = {}
|
_params: Dict = {}
|
||||||
|
|
||||||
@ -74,6 +72,10 @@ class Exchange:
|
|||||||
}
|
}
|
||||||
_ft_has: Dict = {}
|
_ft_has: Dict = {}
|
||||||
|
|
||||||
|
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
||||||
|
# TradingMode.SPOT always supported and not required in this list
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
|
def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes this module with the given config,
|
Initializes this module with the given config,
|
||||||
@ -83,6 +85,7 @@ class Exchange:
|
|||||||
self._api: ccxt.Exchange = None
|
self._api: ccxt.Exchange = None
|
||||||
self._api_async: ccxt_async.Exchange = None
|
self._api_async: ccxt_async.Exchange = None
|
||||||
self._markets: Dict = {}
|
self._markets: Dict = {}
|
||||||
|
self._leverage_brackets: Dict = {}
|
||||||
|
|
||||||
self._config.update(config)
|
self._config.update(config)
|
||||||
|
|
||||||
@ -125,14 +128,25 @@ class Exchange:
|
|||||||
self._trades_pagination = self._ft_has['trades_pagination']
|
self._trades_pagination = self._ft_has['trades_pagination']
|
||||||
self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
|
self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
|
||||||
|
|
||||||
|
self.trading_mode: TradingMode = (
|
||||||
|
TradingMode(config.get('trading_mode'))
|
||||||
|
if config.get('trading_mode')
|
||||||
|
else TradingMode.SPOT
|
||||||
|
)
|
||||||
|
self.collateral: Optional[Collateral] = (
|
||||||
|
Collateral(config.get('collateral'))
|
||||||
|
if config.get('collateral')
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
# Initialize ccxt objects
|
# Initialize ccxt objects
|
||||||
ccxt_config = self._ccxt_config.copy()
|
ccxt_config = self._ccxt_config
|
||||||
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config)
|
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config)
|
||||||
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config)
|
ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config)
|
||||||
|
|
||||||
self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config)
|
self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config)
|
||||||
|
|
||||||
ccxt_async_config = self._ccxt_config.copy()
|
ccxt_async_config = self._ccxt_config
|
||||||
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}),
|
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}),
|
||||||
ccxt_async_config)
|
ccxt_async_config)
|
||||||
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}),
|
ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}),
|
||||||
@ -140,6 +154,9 @@ class Exchange:
|
|||||||
self._api_async = self._init_ccxt(
|
self._api_async = self._init_ccxt(
|
||||||
exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config)
|
exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config)
|
||||||
|
|
||||||
|
if self.trading_mode != TradingMode.SPOT:
|
||||||
|
self.fill_leverage_brackets()
|
||||||
|
|
||||||
logger.info('Using Exchange "%s"', self.name)
|
logger.info('Using Exchange "%s"', self.name)
|
||||||
|
|
||||||
if validate:
|
if validate:
|
||||||
@ -157,7 +174,7 @@ class Exchange:
|
|||||||
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
|
||||||
self.validate_required_startup_candles(config.get('startup_candle_count', 0),
|
self.validate_required_startup_candles(config.get('startup_candle_count', 0),
|
||||||
config.get('timeframe', ''))
|
config.get('timeframe', ''))
|
||||||
|
self.validate_trading_mode_and_collateral(self.trading_mode, self.collateral)
|
||||||
# Converts the interval provided in minutes in config to seconds
|
# Converts the interval provided in minutes in config to seconds
|
||||||
self.markets_refresh_interval: int = exchange_config.get(
|
self.markets_refresh_interval: int = exchange_config.get(
|
||||||
"markets_refresh_interval", 60) * 60
|
"markets_refresh_interval", 60) * 60
|
||||||
@ -190,6 +207,7 @@ class Exchange:
|
|||||||
'secret': exchange_config.get('secret'),
|
'secret': exchange_config.get('secret'),
|
||||||
'password': exchange_config.get('password'),
|
'password': exchange_config.get('password'),
|
||||||
'uid': exchange_config.get('uid', ''),
|
'uid': exchange_config.get('uid', ''),
|
||||||
|
# 'options': exchange_config.get('options', {})
|
||||||
}
|
}
|
||||||
if ccxt_kwargs:
|
if ccxt_kwargs:
|
||||||
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
|
logger.info('Applying additional ccxt config: %s', ccxt_kwargs)
|
||||||
@ -210,6 +228,11 @@ class Exchange:
|
|||||||
|
|
||||||
return api
|
return api
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _ccxt_config(self) -> Dict:
|
||||||
|
# Parameters to add directly to ccxt sync/async initialization.
|
||||||
|
return {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""exchange Name (from ccxt)"""
|
"""exchange Name (from ccxt)"""
|
||||||
@ -355,6 +378,7 @@ class Exchange:
|
|||||||
# Also reload async markets to avoid issues with newly listed pairs
|
# Also reload async markets to avoid issues with newly listed pairs
|
||||||
self._load_async_markets(reload=True)
|
self._load_async_markets(reload=True)
|
||||||
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
self._last_markets_refresh = arrow.utcnow().int_timestamp
|
||||||
|
self.fill_leverage_brackets()
|
||||||
except ccxt.BaseError:
|
except ccxt.BaseError:
|
||||||
logger.exception("Could not reload markets.")
|
logger.exception("Could not reload markets.")
|
||||||
|
|
||||||
@ -370,7 +394,7 @@ class Exchange:
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
'Could not load markets, therefore cannot start. '
|
'Could not load markets, therefore cannot start. '
|
||||||
'Please investigate the above error for more details.'
|
'Please investigate the above error for more details.'
|
||||||
)
|
)
|
||||||
quote_currencies = self.get_quote_currencies()
|
quote_currencies = self.get_quote_currencies()
|
||||||
if stake_currency not in quote_currencies:
|
if stake_currency not in quote_currencies:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
@ -482,6 +506,25 @@ class Exchange:
|
|||||||
f"This strategy requires {startup_candles} candles to start. "
|
f"This strategy requires {startup_candles} candles to start. "
|
||||||
f"{self.name} only provides {candle_limit} for {timeframe}.")
|
f"{self.name} only provides {candle_limit} for {timeframe}.")
|
||||||
|
|
||||||
|
def validate_trading_mode_and_collateral(
|
||||||
|
self,
|
||||||
|
trading_mode: TradingMode,
|
||||||
|
collateral: Optional[Collateral] # Only None when trading_mode = TradingMode.SPOT
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Checks if freqtrade can perform trades using the configured
|
||||||
|
trading mode(Margin, Futures) and Collateral(Cross, Isolated)
|
||||||
|
Throws OperationalException:
|
||||||
|
If the trading_mode/collateral type are not supported by freqtrade on this exchange
|
||||||
|
"""
|
||||||
|
if trading_mode != TradingMode.SPOT and (
|
||||||
|
(trading_mode, collateral) not in self._supported_trading_mode_collateral_pairs
|
||||||
|
):
|
||||||
|
collateral_value = collateral and collateral.value
|
||||||
|
raise OperationalException(
|
||||||
|
f"Freqtrade does not support {collateral_value} {trading_mode.value} on {self.name}"
|
||||||
|
)
|
||||||
|
|
||||||
def exchange_has(self, endpoint: str) -> bool:
|
def exchange_has(self, endpoint: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks if exchange implements a specific API endpoint.
|
Checks if exchange implements a specific API endpoint.
|
||||||
@ -541,8 +584,8 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
return 1 / pow(10, precision)
|
return 1 / pow(10, precision)
|
||||||
|
|
||||||
def get_min_pair_stake_amount(self, pair: str, price: float,
|
def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float,
|
||||||
stoploss: float) -> Optional[float]:
|
leverage: Optional[float] = 1.0) -> Optional[float]:
|
||||||
try:
|
try:
|
||||||
market = self.markets[pair]
|
market = self.markets[pair]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@ -576,12 +619,24 @@ class Exchange:
|
|||||||
# The value returned should satisfy both limits: for amount (base currency) and
|
# The value returned should satisfy both limits: for amount (base currency) and
|
||||||
# for cost (quote, stake currency), so max() is used here.
|
# for cost (quote, stake currency), so max() is used here.
|
||||||
# See also #2575 at github.
|
# See also #2575 at github.
|
||||||
return max(min_stake_amounts) * amount_reserve_percent
|
return self._get_stake_amount_considering_leverage(
|
||||||
|
max(min_stake_amounts) * amount_reserve_percent,
|
||||||
|
leverage or 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float):
|
||||||
|
"""
|
||||||
|
Takes the minimum stake amount for a pair with no leverage and returns the minimum
|
||||||
|
stake amount when leverage is considered
|
||||||
|
:param stake_amount: The stake amount for a pair before leverage is considered
|
||||||
|
:param leverage: The amount of leverage being used on the current trade
|
||||||
|
"""
|
||||||
|
return stake_amount / leverage
|
||||||
|
|
||||||
# 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, leverage: float, params: Dict = {}) -> 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] = {
|
||||||
@ -598,7 +653,8 @@ class Exchange:
|
|||||||
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
'timestamp': arrow.utcnow().int_timestamp * 1000,
|
||||||
'status': "closed" if ordertype == "market" else "open",
|
'status': "closed" if ordertype == "market" else "open",
|
||||||
'fee': None,
|
'fee': None,
|
||||||
'info': {}
|
'info': {},
|
||||||
|
'leverage': leverage
|
||||||
}
|
}
|
||||||
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]:
|
||||||
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
dry_order["info"] = {"stopPrice": dry_order["price"]}
|
||||||
@ -608,7 +664,7 @@ class Exchange:
|
|||||||
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({
|
||||||
'average': average,
|
'average': average,
|
||||||
'cost': dry_order['amount'] * average,
|
'cost': (dry_order['amount'] * average) / leverage
|
||||||
})
|
})
|
||||||
dry_order = self.add_dry_order_fee(pair, dry_order)
|
dry_order = self.add_dry_order_fee(pair, dry_order)
|
||||||
|
|
||||||
@ -716,17 +772,26 @@ class Exchange:
|
|||||||
|
|
||||||
# Order handling
|
# Order handling
|
||||||
|
|
||||||
def create_order(self, pair: str, ordertype: str, side: str, amount: float,
|
def _lev_prep(self, pair: str, leverage: float):
|
||||||
rate: float, time_in_force: str = 'gtc') -> Dict:
|
if self.trading_mode != TradingMode.SPOT:
|
||||||
|
self.set_margin_mode(pair, self.collateral)
|
||||||
if self._config['dry_run']:
|
self._set_leverage(leverage, pair)
|
||||||
dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate)
|
|
||||||
return dry_order
|
|
||||||
|
|
||||||
|
def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict:
|
||||||
params = self._params.copy()
|
params = self._params.copy()
|
||||||
if time_in_force != 'gtc' and ordertype != 'market':
|
if time_in_force != 'gtc' and ordertype != 'market':
|
||||||
param = self._ft_has.get('time_in_force_parameter', '')
|
param = self._ft_has.get('time_in_force_parameter', '')
|
||||||
params.update({param: time_in_force})
|
params.update({param: time_in_force})
|
||||||
|
return params
|
||||||
|
|
||||||
|
def create_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||||
|
rate: float, leverage: float = 1.0, time_in_force: str = 'gtc') -> Dict:
|
||||||
|
# TODO-lev: remove default for leverage
|
||||||
|
if self._config['dry_run']:
|
||||||
|
dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage)
|
||||||
|
return dry_order
|
||||||
|
|
||||||
|
params = self._get_params(ordertype, leverage, time_in_force)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||||
@ -735,6 +800,7 @@ class Exchange:
|
|||||||
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
or self._api.options.get("createMarketBuyOrderRequiresPrice", False))
|
||||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||||
|
|
||||||
|
self._lev_prep(pair, leverage)
|
||||||
order = self._api.create_order(pair, ordertype, side,
|
order = self._api.create_order(pair, ordertype, side,
|
||||||
amount, rate_for_order, params)
|
amount, rate_for_order, params)
|
||||||
self._log_exchange_response('create_order', order)
|
self._log_exchange_response('create_order', order)
|
||||||
@ -758,14 +824,15 @@ class Exchange:
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Verify stop_loss against stoploss-order value (limit or price)
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
Returns True if adjustment is necessary.
|
Returns True if adjustment is necessary.
|
||||||
"""
|
"""
|
||||||
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, order_types: Dict) -> Dict:
|
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.
|
||||||
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.
|
||||||
@ -1528,6 +1595,69 @@ class Exchange:
|
|||||||
self._async_get_trade_history(pair=pair, since=since,
|
self._async_get_trade_history(pair=pair, since=since,
|
||||||
until=until, from_id=from_id))
|
until=until, from_id=from_id))
|
||||||
|
|
||||||
|
def fill_leverage_brackets(self):
|
||||||
|
"""
|
||||||
|
# TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken
|
||||||
|
Assigns property _leverage_brackets to a dictionary of information about the leverage
|
||||||
|
allowed on each pair
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
|
||||||
|
"""
|
||||||
|
Returns the maximum leverage that a pair can be traded at
|
||||||
|
:param pair: The base/quote currency pair being traded
|
||||||
|
:nominal_value: The total value of the trade in quote currency (collateral + debt)
|
||||||
|
"""
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def _set_leverage(
|
||||||
|
self,
|
||||||
|
leverage: float,
|
||||||
|
pair: Optional[str] = None,
|
||||||
|
trading_mode: Optional[TradingMode] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Set's the leverage before making a trade, in order to not
|
||||||
|
have the same leverage on every trade
|
||||||
|
"""
|
||||||
|
# TODO-lev: Make a documentation page that says you can't run 2 bots
|
||||||
|
# TODO-lev: on the same account with leverage
|
||||||
|
if self._config['dry_run'] or not self.exchange_has("setLeverage"):
|
||||||
|
# Some exchanges only support one collateral type
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._api.set_leverage(symbol=pair, leverage=leverage)
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}):
|
||||||
|
'''
|
||||||
|
Set's the margin mode on the exchange to cross or isolated for a specific pair
|
||||||
|
:param symbol: base/quote currency pair (e.g. "ADA/USDT")
|
||||||
|
'''
|
||||||
|
if self._config['dry_run'] or not self.exchange_has("setMarginMode"):
|
||||||
|
# Some exchanges only support one collateral type
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._api.set_margin_mode(pair, collateral.value, params)
|
||||||
|
except ccxt.DDoSProtection as e:
|
||||||
|
raise DDosProtection(e) from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
|
||||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
||||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
""" FTX exchange subclass """
|
""" FTX exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
|
from freqtrade.enums import Collateral, TradingMode
|
||||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||||
OperationalException, TemporaryError)
|
OperationalException, TemporaryError)
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
@ -21,6 +22,12 @@ class Ftx(Exchange):
|
|||||||
"ohlcv_candle_limit": 1500,
|
"ohlcv_candle_limit": 1500,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
||||||
|
# TradingMode.SPOT always supported and not required in this list
|
||||||
|
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
|
||||||
|
# (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: Uncomment once supported
|
||||||
|
]
|
||||||
|
|
||||||
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
|
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the market symbol is tradable by Freqtrade.
|
Check if the market symbol is tradable by Freqtrade.
|
||||||
@ -31,15 +38,19 @@ class Ftx(Exchange):
|
|||||||
return (parent_check and
|
return (parent_check and
|
||||||
market.get('spot', False) is True)
|
market.get('spot', False) is True)
|
||||||
|
|
||||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Verify stop_loss against stoploss-order value (limit or price)
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
Returns True if adjustment is necessary.
|
Returns True if adjustment is necessary.
|
||||||
"""
|
"""
|
||||||
return order['type'] == 'stop' and stop_loss > float(order['price'])
|
return order['type'] == 'stop' and (
|
||||||
|
side == "sell" and stop_loss > float(order['price']) or
|
||||||
|
side == "buy" and stop_loss < float(order['price'])
|
||||||
|
)
|
||||||
|
|
||||||
@retrier(retries=0)
|
@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, side: str, leverage: float) -> Dict:
|
||||||
"""
|
"""
|
||||||
Creates a stoploss order.
|
Creates a stoploss order.
|
||||||
depending on order_types.stoploss configuration, uses 'market' or limit order.
|
depending on order_types.stoploss configuration, uses 'market' or limit order.
|
||||||
@ -47,7 +58,10 @@ class Ftx(Exchange):
|
|||||||
Limit orders are defined by having orderPrice set, otherwise a market order is used.
|
Limit orders are defined by having orderPrice set, otherwise a market order is used.
|
||||||
"""
|
"""
|
||||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
limit_rate = stop_price * limit_price_pct
|
if side == "sell":
|
||||||
|
limit_rate = stop_price * limit_price_pct
|
||||||
|
else:
|
||||||
|
limit_rate = stop_price * (2 - limit_price_pct)
|
||||||
|
|
||||||
ordertype = "stop"
|
ordertype = "stop"
|
||||||
|
|
||||||
@ -55,7 +69,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, side, amount, stop_price, leverage)
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -67,7 +81,8 @@ class Ftx(Exchange):
|
|||||||
params['stopPrice'] = stop_price
|
params['stopPrice'] = stop_price
|
||||||
amount = self.amount_to_precision(pair, amount)
|
amount = self.amount_to_precision(pair, amount)
|
||||||
|
|
||||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
self._lev_prep(pair, leverage)
|
||||||
|
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||||
amount=amount, params=params)
|
amount=amount, params=params)
|
||||||
self._log_exchange_response('create_stoploss_order', order)
|
self._log_exchange_response('create_stoploss_order', order)
|
||||||
logger.info('stoploss order added for %s. '
|
logger.info('stoploss order added for %s. '
|
||||||
@ -75,19 +90,19 @@ class Ftx(Exchange):
|
|||||||
return order
|
return order
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise InsufficientFundsError(
|
raise InsufficientFundsError(
|
||||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise InvalidOrderException(
|
raise InvalidOrderException(
|
||||||
f'Could not create {ordertype} sell order on market {pair}. '
|
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@ -152,3 +167,18 @@ class Ftx(Exchange):
|
|||||||
if order['type'] == 'stop':
|
if order['type'] == 'stop':
|
||||||
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
return safe_value_fallback2(order, order, 'id_stop', 'id')
|
||||||
return order['id']
|
return order['id']
|
||||||
|
|
||||||
|
def fill_leverage_brackets(self):
|
||||||
|
"""
|
||||||
|
FTX leverage is static across the account, and doesn't change from pair to pair,
|
||||||
|
so _leverage_brackets doesn't need to be set
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
|
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
|
||||||
|
"""
|
||||||
|
Returns the maximum leverage that a pair can be traded at, which is always 20 on ftx
|
||||||
|
:param pair: Here for super method, not used on FTX
|
||||||
|
:nominal_value: Here for super method, not used on FTX
|
||||||
|
"""
|
||||||
|
return 20.0
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
""" Kraken exchange subclass """
|
""" Kraken exchange subclass """
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
|
from freqtrade.enums import Collateral, TradingMode
|
||||||
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
|
||||||
OperationalException, TemporaryError)
|
OperationalException, TemporaryError)
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
@ -23,6 +24,12 @@ class Kraken(Exchange):
|
|||||||
"trades_pagination_arg": "since",
|
"trades_pagination_arg": "since",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [
|
||||||
|
# TradingMode.SPOT always supported and not required in this list
|
||||||
|
# (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported
|
||||||
|
# (TradingMode.FUTURES, Collateral.CROSS) # TODO-lev: No CCXT support
|
||||||
|
]
|
||||||
|
|
||||||
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
|
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the market symbol is tradable by Freqtrade.
|
Check if the market symbol is tradable by Freqtrade.
|
||||||
@ -67,16 +74,19 @@ class Kraken(Exchange):
|
|||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Verify stop_loss against stoploss-order value (limit or price)
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
Returns True if adjustment is necessary.
|
Returns True if adjustment is necessary.
|
||||||
"""
|
"""
|
||||||
return (order['type'] in ('stop-loss', 'stop-loss-limit')
|
return (order['type'] in ('stop-loss', 'stop-loss-limit') and (
|
||||||
and stop_loss > float(order['price']))
|
(side == "sell" and stop_loss > float(order['price'])) or
|
||||||
|
(side == "buy" and stop_loss < float(order['price']))
|
||||||
|
))
|
||||||
|
|
||||||
@retrier(retries=0)
|
@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, side: str, leverage: float) -> Dict:
|
||||||
"""
|
"""
|
||||||
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.
|
||||||
@ -86,7 +96,10 @@ class Kraken(Exchange):
|
|||||||
if order_types.get('stoploss', 'market') == 'limit':
|
if order_types.get('stoploss', 'market') == 'limit':
|
||||||
ordertype = "stop-loss-limit"
|
ordertype = "stop-loss-limit"
|
||||||
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
limit_rate = stop_price * limit_price_pct
|
if side == "sell":
|
||||||
|
limit_rate = stop_price * limit_price_pct
|
||||||
|
else:
|
||||||
|
limit_rate = stop_price * (2 - limit_price_pct)
|
||||||
params['price2'] = self.price_to_precision(pair, limit_rate)
|
params['price2'] = self.price_to_precision(pair, limit_rate)
|
||||||
else:
|
else:
|
||||||
ordertype = "stop-loss"
|
ordertype = "stop-loss"
|
||||||
@ -95,13 +108,13 @@ 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, side, amount, stop_price, leverage)
|
||||||
return dry_order
|
return dry_order
|
||||||
|
|
||||||
try:
|
try:
|
||||||
amount = self.amount_to_precision(pair, amount)
|
amount = self.amount_to_precision(pair, amount)
|
||||||
|
|
||||||
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
|
||||||
amount=amount, price=stop_price, params=params)
|
amount=amount, price=stop_price, params=params)
|
||||||
self._log_exchange_response('create_stoploss_order', order)
|
self._log_exchange_response('create_stoploss_order', order)
|
||||||
logger.info('stoploss order added for %s. '
|
logger.info('stoploss order added for %s. '
|
||||||
@ -109,18 +122,70 @@ class Kraken(Exchange):
|
|||||||
return order
|
return order
|
||||||
except ccxt.InsufficientFunds as e:
|
except ccxt.InsufficientFunds as e:
|
||||||
raise InsufficientFundsError(
|
raise InsufficientFundsError(
|
||||||
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
|
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.InvalidOrder as e:
|
except ccxt.InvalidOrder as e:
|
||||||
raise InvalidOrderException(
|
raise InvalidOrderException(
|
||||||
f'Could not create {ordertype} sell order on market {pair}. '
|
f'Could not create {ordertype} {side} order on market {pair}. '
|
||||||
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
raise DDosProtection(e) from e
|
raise DDosProtection(e) from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
def fill_leverage_brackets(self):
|
||||||
|
"""
|
||||||
|
Assigns property _leverage_brackets to a dictionary of information about the leverage
|
||||||
|
allowed on each pair
|
||||||
|
"""
|
||||||
|
leverages = {}
|
||||||
|
|
||||||
|
for pair, market in self.markets.items():
|
||||||
|
leverages[pair] = [1]
|
||||||
|
info = market['info']
|
||||||
|
leverage_buy = info.get('leverage_buy', [])
|
||||||
|
leverage_sell = info.get('leverage_sell', [])
|
||||||
|
if len(leverage_buy) > 0 or len(leverage_sell) > 0:
|
||||||
|
if leverage_buy != leverage_sell:
|
||||||
|
logger.warning(
|
||||||
|
f"The buy({leverage_buy}) and sell({leverage_sell}) leverage are not equal"
|
||||||
|
"for {pair}. Please notify freqtrade because this has never happened before"
|
||||||
|
)
|
||||||
|
if max(leverage_buy) <= max(leverage_sell):
|
||||||
|
leverages[pair] += [int(lev) for lev in leverage_buy]
|
||||||
|
else:
|
||||||
|
leverages[pair] += [int(lev) for lev in leverage_sell]
|
||||||
|
else:
|
||||||
|
leverages[pair] += [int(lev) for lev in leverage_buy]
|
||||||
|
self._leverage_brackets = leverages
|
||||||
|
|
||||||
|
def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float:
|
||||||
|
"""
|
||||||
|
Returns the maximum leverage that a pair can be traded at
|
||||||
|
:param pair: The base/quote currency pair being traded
|
||||||
|
:nominal_value: Here for super class, not needed on Kraken
|
||||||
|
"""
|
||||||
|
return float(max(self._leverage_brackets[pair]))
|
||||||
|
|
||||||
|
def _set_leverage(
|
||||||
|
self,
|
||||||
|
leverage: float,
|
||||||
|
pair: Optional[str] = None,
|
||||||
|
trading_mode: Optional[TradingMode] = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Kraken set's the leverage as an option in the order object, so we need to
|
||||||
|
add it to params
|
||||||
|
"""
|
||||||
|
return
|
||||||
|
|
||||||
|
def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict:
|
||||||
|
params = super()._get_params(ordertype, leverage, time_in_force)
|
||||||
|
if leverage > 1.0:
|
||||||
|
params['leverage'] = leverage
|
||||||
|
return params
|
||||||
|
@ -85,10 +85,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists)
|
||||||
|
|
||||||
# Attach Dataprovider to Strategy baseclass
|
# Attach Dataprovider to strategy instance
|
||||||
IStrategy.dp = self.dataprovider
|
self.strategy.dp = self.dataprovider
|
||||||
# Attach Wallets to Strategy baseclass
|
# Attach Wallets to strategy instance
|
||||||
IStrategy.wallets = self.wallets
|
self.strategy.wallets = self.wallets
|
||||||
|
|
||||||
# Initializing Edge only if enabled
|
# Initializing Edge only if enabled
|
||||||
self.edge = Edge(self.config, self.exchange, self.strategy) if \
|
self.edge = Edge(self.config, self.exchange, self.strategy) if \
|
||||||
@ -162,7 +162,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# Refreshing candles
|
# Refreshing candles
|
||||||
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist),
|
||||||
self.strategy.informative_pairs())
|
self.strategy.gather_informative_pairs())
|
||||||
|
|
||||||
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)()
|
||||||
|
|
||||||
@ -735,9 +735,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:return: True if the order succeeded, and False in case of problems.
|
:return: True if the order succeeded, and False in case of problems.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount,
|
stoploss_order = self.exchange.stoploss(
|
||||||
stop_price=stop_price,
|
pair=trade.pair,
|
||||||
order_types=self.strategy.order_types)
|
amount=trade.amount,
|
||||||
|
stop_price=stop_price,
|
||||||
|
order_types=self.strategy.order_types,
|
||||||
|
side=trade.exit_side,
|
||||||
|
leverage=trade.leverage
|
||||||
|
)
|
||||||
|
|
||||||
order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss')
|
order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss')
|
||||||
trade.orders.append(order_obj)
|
trade.orders.append(order_obj)
|
||||||
@ -829,11 +834,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# if trailing stoploss is enabled we check if stoploss value has changed
|
# if trailing stoploss is enabled we check if stoploss value has changed
|
||||||
# in which case we cancel stoploss order and put another one with new
|
# in which case we cancel stoploss order and put another one with new
|
||||||
# value immediately
|
# value immediately
|
||||||
self.handle_trailing_stoploss_on_exchange(trade, stoploss_order)
|
self.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side)
|
||||||
|
|
||||||
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, side: str) -> 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
|
||||||
@ -841,7 +846,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:param order: Current on exchange stoploss order
|
:param order: Current on exchange stoploss order
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if self.exchange.stoploss_adjust(trade.stop_loss, order):
|
if self.exchange.stoploss_adjust(trade.stop_loss, order, side):
|
||||||
# we check if the update is necessary
|
# we check if the update is necessary
|
||||||
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
||||||
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
||||||
|
@ -20,7 +20,7 @@ def interest(
|
|||||||
|
|
||||||
:param exchange_name: The exchanged being trading on
|
:param exchange_name: The exchanged being trading on
|
||||||
:param borrowed: The amount of currency being borrowed
|
:param borrowed: The amount of currency being borrowed
|
||||||
:param rate: The rate of interest
|
:param rate: The rate of interest (i.e daily interest rate)
|
||||||
:param hours: The time in hours that the currency has been borrowed for
|
:param hours: The time in hours that the currency has been borrowed for
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
@ -36,7 +36,8 @@ def interest(
|
|||||||
# Rounded based on https://kraken-fees-calculator.github.io/
|
# Rounded based on https://kraken-fees-calculator.github.io/
|
||||||
return borrowed * rate * (one+ceil(hours/four))
|
return borrowed * rate * (one+ceil(hours/four))
|
||||||
elif exchange_name == "ftx":
|
elif exchange_name == "ftx":
|
||||||
# TODO-lev: Add FTX interest formula
|
# As Explained under #Interest rates section in
|
||||||
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")
|
# https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer
|
||||||
|
return borrowed * rate * ceil(hours)/twenty_four
|
||||||
else:
|
else:
|
||||||
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")
|
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")
|
||||||
|
@ -157,7 +157,7 @@ class Backtesting:
|
|||||||
self.strategy: IStrategy = strategy
|
self.strategy: IStrategy = strategy
|
||||||
strategy.dp = self.dataprovider
|
strategy.dp = self.dataprovider
|
||||||
# Attach Wallets to Strategy baseclass
|
# Attach Wallets to Strategy baseclass
|
||||||
IStrategy.wallets = self.wallets
|
strategy.wallets = self.wallets
|
||||||
# Set stoploss_on_exchange to false for backtesting,
|
# Set stoploss_on_exchange to false for backtesting,
|
||||||
# since a "perfect" stoploss-sell is assumed anyway
|
# since a "perfect" stoploss-sell is assumed anyway
|
||||||
# And the regular "stoploss" function would not apply to that case
|
# And the regular "stoploss" function would not apply to that case
|
||||||
|
@ -8,6 +8,7 @@ from typing import Any, Dict
|
|||||||
|
|
||||||
from freqtrade import constants
|
from freqtrade import constants
|
||||||
from freqtrade.configuration import TimeRange, validate_config_consistency
|
from freqtrade.configuration import TimeRange, validate_config_consistency
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.optimize.optimize_reports import generate_edge_table
|
from freqtrade.optimize.optimize_reports import generate_edge_table
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
@ -33,6 +34,7 @@ class EdgeCli:
|
|||||||
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
|
||||||
self.strategy = StrategyResolver.load_strategy(self.config)
|
self.strategy = StrategyResolver.load_strategy(self.config)
|
||||||
|
self.strategy.dp = DataProvider(config, None)
|
||||||
|
|
||||||
validate_config_consistency(self.config)
|
validate_config_consistency(self.config)
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
from typing import Any, Dict, Iterator, List, Optional, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
import rapidjson
|
import rapidjson
|
||||||
import tabulate
|
import tabulate
|
||||||
from colorama import Fore, Style
|
from colorama import Fore, Style
|
||||||
@ -298,8 +299,8 @@ class HyperoptTools():
|
|||||||
f"Objective: {results['loss']:.5f}")
|
f"Objective: {results['loss']:.5f}")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str:
|
def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool,
|
||||||
|
has_drawdown: bool) -> pd.DataFrame:
|
||||||
trials['Best'] = ''
|
trials['Best'] = ''
|
||||||
|
|
||||||
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
if 'results_metrics.winsdrawslosses' not in trials.columns:
|
||||||
@ -435,8 +436,7 @@ class HyperoptTools():
|
|||||||
return table
|
return table
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool,
|
def export_csv_file(config: dict, results: list, csv_file: str) -> None:
|
||||||
csv_file: str) -> None:
|
|
||||||
"""
|
"""
|
||||||
Log result to csv-file
|
Log result to csv-file
|
||||||
"""
|
"""
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
This module contains the class to persist trades into SQLite
|
This module contains the class to persist trades into SQLite
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
@ -1026,17 +1026,21 @@ class Trade(_DECL_BASE, LocalTrade):
|
|||||||
return total_open_stake_amount or 0
|
return total_open_stake_amount or 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_overall_performance() -> List[Dict[str, Any]]:
|
def get_overall_performance(minutes=None) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Returns List of dicts containing all Trades, including profit and trade count
|
Returns List of dicts containing all Trades, including profit and trade count
|
||||||
NOTE: Not supported in Backtesting.
|
NOTE: Not supported in Backtesting.
|
||||||
"""
|
"""
|
||||||
|
filters = [Trade.is_open.is_(False)]
|
||||||
|
if minutes:
|
||||||
|
start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes)
|
||||||
|
filters.append(Trade.close_date >= start_date)
|
||||||
pair_rates = Trade.query.with_entities(
|
pair_rates = Trade.query.with_entities(
|
||||||
Trade.pair,
|
Trade.pair,
|
||||||
func.sum(Trade.close_profit).label('profit_sum'),
|
func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
|
||||||
func.count(Trade.pair).label('count')
|
func.count(Trade.pair).label('count')
|
||||||
).filter(Trade.is_open.is_(False))\
|
).filter(*filters)\
|
||||||
.group_by(Trade.pair) \
|
.group_by(Trade.pair) \
|
||||||
.order_by(desc('profit_sum_abs')) \
|
.order_by(desc('profit_sum_abs')) \
|
||||||
.all()
|
.all()
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Performance pair list filter
|
Performance pair list filter
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
@ -15,6 +15,13 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class PerformanceFilter(IPairList):
|
class PerformanceFilter(IPairList):
|
||||||
|
|
||||||
|
def __init__(self, exchange, pairlistmanager,
|
||||||
|
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||||
|
pairlist_pos: int) -> None:
|
||||||
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
|
self._minutes = pairlistconfig.get('minutes', 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
"""
|
"""
|
||||||
@ -40,7 +47,7 @@ class PerformanceFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
# Get the trading performance for pairs from database
|
# Get the trading performance for pairs from database
|
||||||
try:
|
try:
|
||||||
performance = pd.DataFrame(Trade.get_overall_performance())
|
performance = pd.DataFrame(Trade.get_overall_performance(self._minutes))
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# Performancefilter does not work in backtesting.
|
# Performancefilter does not work in backtesting.
|
||||||
self.log_once("PerformanceFilter is not available in this mode.", logger.warning)
|
self.log_once("PerformanceFilter is not available in this mode.", logger.warning)
|
||||||
|
@ -46,6 +46,12 @@ class Balances(BaseModel):
|
|||||||
value: float
|
value: float
|
||||||
stake: str
|
stake: str
|
||||||
note: str
|
note: str
|
||||||
|
starting_capital: float
|
||||||
|
starting_capital_ratio: float
|
||||||
|
starting_capital_pct: float
|
||||||
|
starting_capital_fiat: float
|
||||||
|
starting_capital_fiat_ratio: float
|
||||||
|
starting_capital_fiat_pct: float
|
||||||
|
|
||||||
|
|
||||||
class Count(BaseModel):
|
class Count(BaseModel):
|
||||||
|
@ -459,6 +459,9 @@ class RPC:
|
|||||||
raise RPCException('Error getting current tickers.')
|
raise RPCException('Error getting current tickers.')
|
||||||
|
|
||||||
self._freqtrade.wallets.update(require_update=False)
|
self._freqtrade.wallets.update(require_update=False)
|
||||||
|
starting_capital = self._freqtrade.wallets.get_starting_balance()
|
||||||
|
starting_cap_fiat = self._fiat_converter.convert_amount(
|
||||||
|
starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||||
|
|
||||||
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
||||||
if not balance.total:
|
if not balance.total:
|
||||||
@ -494,15 +497,25 @@ class RPC:
|
|||||||
else:
|
else:
|
||||||
raise RPCException('All balances are zero.')
|
raise RPCException('All balances are zero.')
|
||||||
|
|
||||||
symbol = fiat_display_currency
|
value = self._fiat_converter.convert_amount(
|
||||||
value = self._fiat_converter.convert_amount(total, stake_currency,
|
total, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||||
symbol) if self._fiat_converter else 0
|
|
||||||
|
starting_capital_ratio = 0.0
|
||||||
|
starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0
|
||||||
|
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'currencies': output,
|
'currencies': output,
|
||||||
'total': total,
|
'total': total,
|
||||||
'symbol': symbol,
|
'symbol': fiat_display_currency,
|
||||||
'value': value,
|
'value': value,
|
||||||
'stake': stake_currency,
|
'stake': stake_currency,
|
||||||
|
'starting_capital': starting_capital,
|
||||||
|
'starting_capital_ratio': starting_capital_ratio,
|
||||||
|
'starting_capital_pct': round(starting_capital_ratio * 100, 2),
|
||||||
|
'starting_capital_fiat': starting_cap_fiat,
|
||||||
|
'starting_capital_fiat_ratio': starting_cap_fiat_ratio,
|
||||||
|
'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2),
|
||||||
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
|
'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -603,12 +603,15 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
output = ''
|
output = ''
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
output += (
|
output += "*Warning:* Simulated balances in Dry Mode.\n"
|
||||||
f"*Warning:* Simulated balances in Dry Mode.\n"
|
|
||||||
"This mode is still experimental!\n"
|
output += ("Starting capital: "
|
||||||
"Starting capital: "
|
f"`{result['starting_capital']}` {self._config['stake_currency']}"
|
||||||
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
|
)
|
||||||
)
|
output += (f" `{result['starting_capital_fiat']}` "
|
||||||
|
f"{self._config['fiat_display_currency']}.\n"
|
||||||
|
) if result['starting_capital_fiat'] > 0 else '.\n'
|
||||||
|
|
||||||
total_dust_balance = 0
|
total_dust_balance = 0
|
||||||
total_dust_currencies = 0
|
total_dust_currencies = 0
|
||||||
for curr in result['currencies']:
|
for curr in result['currencies']:
|
||||||
@ -641,9 +644,12 @@ class Telegram(RPCHandler):
|
|||||||
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
|
f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n")
|
||||||
|
|
||||||
output += ("\n*Estimated Value*:\n"
|
output += ("\n*Estimated Value*:\n"
|
||||||
f"\t`{result['stake']}: {result['total']: .8f}`\n"
|
f"\t`{result['stake']}: "
|
||||||
|
f"{round_coin_value(result['total'], result['stake'], False)}`"
|
||||||
|
f" `({result['starting_capital_pct']}%)`\n"
|
||||||
f"\t`{result['symbol']}: "
|
f"\t`{result['symbol']}: "
|
||||||
f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
|
f"{round_coin_value(result['value'], result['symbol'], False)}`"
|
||||||
|
f" `({result['starting_capital_fiat_pct']}%)`\n")
|
||||||
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
self._send_msg(output, reload_able=True, callback_path="update_balance",
|
||||||
query=update.callback_query)
|
query=update.callback_query)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
|
@ -3,5 +3,7 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr
|
|||||||
timeframe_to_prev_date, timeframe_to_seconds)
|
timeframe_to_prev_date, timeframe_to_seconds)
|
||||||
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter,
|
||||||
IntParameter, RealParameter)
|
IntParameter, RealParameter)
|
||||||
|
from freqtrade.strategy.informative_decorator import informative
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open
|
from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute,
|
||||||
|
stoploss_from_open)
|
||||||
|
128
freqtrade/strategy/informative_decorator.py
Normal file
128
freqtrade/strategy/informative_decorator.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
from typing import Any, Callable, NamedTuple, Optional, Union
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.exceptions import OperationalException
|
||||||
|
from freqtrade.strategy.strategy_helper import merge_informative_pair
|
||||||
|
|
||||||
|
|
||||||
|
PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame]
|
||||||
|
|
||||||
|
|
||||||
|
class InformativeData(NamedTuple):
|
||||||
|
asset: Optional[str]
|
||||||
|
timeframe: str
|
||||||
|
fmt: Union[str, Callable[[Any], str], None]
|
||||||
|
ffill: bool
|
||||||
|
|
||||||
|
|
||||||
|
def informative(timeframe: str, asset: str = '',
|
||||||
|
fmt: Optional[Union[str, Callable[[Any], str]]] = None,
|
||||||
|
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
|
||||||
|
"""
|
||||||
|
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
|
||||||
|
define informative indicators.
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
:param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe.
|
||||||
|
:param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use
|
||||||
|
current pair.
|
||||||
|
:param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not
|
||||||
|
specified, defaults to:
|
||||||
|
* {base}_{quote}_{column}_{timeframe} if asset is specified.
|
||||||
|
* {column}_{timeframe} if asset is not specified.
|
||||||
|
Format string supports these format variables:
|
||||||
|
* {asset} - full name of the asset, for example 'BTC/USDT'.
|
||||||
|
* {base} - base currency in lower case, for example 'eth'.
|
||||||
|
* {BASE} - same as {base}, except in upper case.
|
||||||
|
* {quote} - quote currency in lower case, for example 'usdt'.
|
||||||
|
* {QUOTE} - same as {quote}, except in upper case.
|
||||||
|
* {column} - name of dataframe column.
|
||||||
|
* {timeframe} - timeframe of informative dataframe.
|
||||||
|
:param ffill: ffill dataframe after merging informative pair.
|
||||||
|
"""
|
||||||
|
_asset = asset
|
||||||
|
_timeframe = timeframe
|
||||||
|
_fmt = fmt
|
||||||
|
_ffill = ffill
|
||||||
|
|
||||||
|
def decorator(fn: PopulateIndicators):
|
||||||
|
informative_pairs = getattr(fn, '_ft_informative', [])
|
||||||
|
informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill))
|
||||||
|
setattr(fn, '_ft_informative', informative_pairs)
|
||||||
|
return fn
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def _format_pair_name(config, pair: str) -> str:
|
||||||
|
return pair.format(stake_currency=config['stake_currency'],
|
||||||
|
stake=config['stake_currency']).upper()
|
||||||
|
|
||||||
|
|
||||||
|
def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict,
|
||||||
|
inf_data: InformativeData,
|
||||||
|
populate_indicators: PopulateIndicators):
|
||||||
|
asset = inf_data.asset or ''
|
||||||
|
timeframe = inf_data.timeframe
|
||||||
|
fmt = inf_data.fmt
|
||||||
|
config = strategy.config
|
||||||
|
|
||||||
|
if asset:
|
||||||
|
# Insert stake currency if needed.
|
||||||
|
asset = _format_pair_name(config, asset)
|
||||||
|
else:
|
||||||
|
# Not specifying an asset will define informative dataframe for current pair.
|
||||||
|
asset = metadata['pair']
|
||||||
|
|
||||||
|
if '/' in asset:
|
||||||
|
base, quote = asset.split('/')
|
||||||
|
else:
|
||||||
|
# When futures are supported this may need reevaluation.
|
||||||
|
# base, quote = asset, ''
|
||||||
|
raise OperationalException('Not implemented.')
|
||||||
|
|
||||||
|
# Default format. This optimizes for the common case: informative pairs using same stake
|
||||||
|
# currency. When quote currency matches stake currency, column name will omit base currency.
|
||||||
|
# This allows easily reconfiguring strategy to use different base currency. In a rare case
|
||||||
|
# where it is desired to keep quote currency in column name at all times user should specify
|
||||||
|
# fmt='{base}_{quote}_{column}_{timeframe}' format or similar.
|
||||||
|
if not fmt:
|
||||||
|
fmt = '{column}_{timeframe}' # Informatives of current pair
|
||||||
|
if inf_data.asset:
|
||||||
|
fmt = '{base}_{quote}_' + fmt # Informatives of other pairs
|
||||||
|
|
||||||
|
inf_metadata = {'pair': asset, 'timeframe': timeframe}
|
||||||
|
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe)
|
||||||
|
inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata)
|
||||||
|
|
||||||
|
formatter: Any = None
|
||||||
|
if callable(fmt):
|
||||||
|
formatter = fmt # A custom user-specified formatter function.
|
||||||
|
else:
|
||||||
|
formatter = fmt.format # A default string formatter.
|
||||||
|
|
||||||
|
fmt_args = {
|
||||||
|
'BASE': base.upper(),
|
||||||
|
'QUOTE': quote.upper(),
|
||||||
|
'base': base.lower(),
|
||||||
|
'quote': quote.lower(),
|
||||||
|
'asset': asset,
|
||||||
|
'timeframe': timeframe,
|
||||||
|
}
|
||||||
|
inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args),
|
||||||
|
inplace=True)
|
||||||
|
|
||||||
|
date_column = formatter(column='date', **fmt_args)
|
||||||
|
if date_column in dataframe.columns:
|
||||||
|
raise OperationalException(f'Duplicate column name {date_column} exists in '
|
||||||
|
f'dataframe! Ensure column names are unique!')
|
||||||
|
dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe,
|
||||||
|
ffill=inf_data.ffill, append_timeframe=False,
|
||||||
|
date_column=date_column)
|
||||||
|
return dataframe
|
@ -19,6 +19,9 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
|||||||
from freqtrade.exchange.exchange import timeframe_to_next_date
|
from freqtrade.exchange.exchange import timeframe_to_next_date
|
||||||
from freqtrade.persistence import PairLocks, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.strategy.hyper import HyperStrategyMixin
|
from freqtrade.strategy.hyper import HyperStrategyMixin
|
||||||
|
from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators,
|
||||||
|
_create_and_merge_informative_pair,
|
||||||
|
_format_pair_name)
|
||||||
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
|
||||||
from freqtrade.wallets import Wallets
|
from freqtrade.wallets import Wallets
|
||||||
|
|
||||||
@ -118,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
# Class level variables (intentional) containing
|
# Class level variables (intentional) containing
|
||||||
# the dataprovider (dp) (access to other candles, historic data, ...)
|
# the dataprovider (dp) (access to other candles, historic data, ...)
|
||||||
# and wallets - access to the current balance.
|
# and wallets - access to the current balance.
|
||||||
dp: Optional[DataProvider] = None
|
dp: Optional[DataProvider]
|
||||||
wallets: Optional[Wallets] = None
|
wallets: Optional[Wallets] = None
|
||||||
# Filled from configuration
|
# Filled from configuration
|
||||||
stake_currency: str
|
stake_currency: str
|
||||||
@ -134,6 +137,24 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
self._last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||||
super().__init__(config)
|
super().__init__(config)
|
||||||
|
|
||||||
|
# Gather informative pairs from @informative-decorated methods.
|
||||||
|
self._ft_informative: List[Tuple[InformativeData, PopulateIndicators]] = []
|
||||||
|
for attr_name in dir(self.__class__):
|
||||||
|
cls_method = getattr(self.__class__, attr_name)
|
||||||
|
if not callable(cls_method):
|
||||||
|
continue
|
||||||
|
informative_data_list = getattr(cls_method, '_ft_informative', None)
|
||||||
|
if not isinstance(informative_data_list, list):
|
||||||
|
# Type check is required because mocker would return a mock object that evaluates to
|
||||||
|
# True, confusing this code.
|
||||||
|
continue
|
||||||
|
strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe)
|
||||||
|
for informative_data in informative_data_list:
|
||||||
|
if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes:
|
||||||
|
raise OperationalException('Informative timeframe must be equal or higher than '
|
||||||
|
'strategy timeframe!')
|
||||||
|
self._ft_informative.append((informative_data, cls_method))
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
@ -379,6 +400,23 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
# END - Intended to be overridden by strategy
|
# END - Intended to be overridden by strategy
|
||||||
###
|
###
|
||||||
|
|
||||||
|
def gather_informative_pairs(self) -> ListPairsWithTimeframes:
|
||||||
|
"""
|
||||||
|
Internal method which gathers all informative pairs (user or automatically defined).
|
||||||
|
"""
|
||||||
|
informative_pairs = self.informative_pairs()
|
||||||
|
for inf_data, _ in self._ft_informative:
|
||||||
|
if inf_data.asset:
|
||||||
|
pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe)
|
||||||
|
informative_pairs.append(pair_tf)
|
||||||
|
else:
|
||||||
|
if not self.dp:
|
||||||
|
raise OperationalException('@informative decorator with unspecified asset '
|
||||||
|
'requires DataProvider instance.')
|
||||||
|
for pair in self.dp.current_whitelist():
|
||||||
|
informative_pairs.append((pair, inf_data.timeframe))
|
||||||
|
return list(set(informative_pairs))
|
||||||
|
|
||||||
def get_strategy_name(self) -> str:
|
def get_strategy_name(self) -> str:
|
||||||
"""
|
"""
|
||||||
Returns strategy class name
|
Returns strategy class name
|
||||||
@ -878,6 +916,12 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
:return: a Dataframe with all mandatory indicators for the strategies
|
:return: a Dataframe with all mandatory indicators for the strategies
|
||||||
"""
|
"""
|
||||||
logger.debug(f"Populating indicators for pair {metadata.get('pair')}.")
|
logger.debug(f"Populating indicators for pair {metadata.get('pair')}.")
|
||||||
|
|
||||||
|
# call populate_indicators_Nm() which were tagged with @informative decorator.
|
||||||
|
for inf_data, populate_fn in self._ft_informative:
|
||||||
|
dataframe = _create_and_merge_informative_pair(
|
||||||
|
self, dataframe, metadata, inf_data, populate_fn)
|
||||||
|
|
||||||
if self._populate_fun_len == 2:
|
if self._populate_fun_len == 2:
|
||||||
warnings.warn("deprecated - check out the Sample strategy to see "
|
warnings.warn("deprecated - check out the Sample strategy to see "
|
||||||
"the current function headers!", DeprecationWarning)
|
"the current function headers!", DeprecationWarning)
|
||||||
|
@ -5,7 +5,9 @@ from freqtrade.exchange import timeframe_to_minutes
|
|||||||
|
|
||||||
|
|
||||||
def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
||||||
timeframe: str, timeframe_inf: str, ffill: bool = True) -> pd.DataFrame:
|
timeframe: str, timeframe_inf: str, ffill: bool = True,
|
||||||
|
append_timeframe: bool = True,
|
||||||
|
date_column: str = 'date') -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Correctly merge informative samples to the original dataframe, avoiding lookahead bias.
|
Correctly merge informative samples to the original dataframe, avoiding lookahead bias.
|
||||||
|
|
||||||
@ -25,6 +27,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
|||||||
:param timeframe: Timeframe of the original pair sample.
|
:param timeframe: Timeframe of the original pair sample.
|
||||||
:param timeframe_inf: Timeframe of the informative pair sample.
|
:param timeframe_inf: Timeframe of the informative pair sample.
|
||||||
:param ffill: Forwardfill missing values - optional but usually required
|
:param ffill: Forwardfill missing values - optional but usually required
|
||||||
|
:param append_timeframe: Rename columns by appending timeframe.
|
||||||
|
:param date_column: A custom date column name.
|
||||||
:return: Merged dataframe
|
:return: Merged dataframe
|
||||||
:raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe
|
:raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe
|
||||||
"""
|
"""
|
||||||
@ -33,25 +37,29 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
|
|||||||
minutes = timeframe_to_minutes(timeframe)
|
minutes = timeframe_to_minutes(timeframe)
|
||||||
if minutes == minutes_inf:
|
if minutes == minutes_inf:
|
||||||
# No need to forwardshift if the timeframes are identical
|
# No need to forwardshift if the timeframes are identical
|
||||||
informative['date_merge'] = informative["date"]
|
informative['date_merge'] = informative[date_column]
|
||||||
elif minutes < minutes_inf:
|
elif minutes < minutes_inf:
|
||||||
# Subtract "small" timeframe so merging is not delayed by 1 small candle
|
# Subtract "small" timeframe so merging is not delayed by 1 small candle
|
||||||
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
|
# Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073
|
||||||
informative['date_merge'] = (
|
informative['date_merge'] = (
|
||||||
informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm')
|
informative[date_column] + pd.to_timedelta(minutes_inf, 'm') -
|
||||||
|
pd.to_timedelta(minutes, 'm')
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError("Tried to merge a faster timeframe to a slower timeframe."
|
raise ValueError("Tried to merge a faster timeframe to a slower timeframe."
|
||||||
"This would create new rows, and can throw off your regular indicators.")
|
"This would create new rows, and can throw off your regular indicators.")
|
||||||
|
|
||||||
# Rename columns to be unique
|
# Rename columns to be unique
|
||||||
informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns]
|
date_merge = 'date_merge'
|
||||||
|
if append_timeframe:
|
||||||
|
date_merge = f'date_merge_{timeframe_inf}'
|
||||||
|
informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns]
|
||||||
|
|
||||||
# Combine the 2 dataframes
|
# Combine the 2 dataframes
|
||||||
# all indicators on the informative sample MUST be calculated before this point
|
# all indicators on the informative sample MUST be calculated before this point
|
||||||
dataframe = pd.merge(dataframe, informative, left_on='date',
|
dataframe = pd.merge(dataframe, informative, left_on='date',
|
||||||
right_on=f'date_merge_{timeframe_inf}', how='left')
|
right_on=date_merge, how='left')
|
||||||
dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1)
|
dataframe = dataframe.drop(date_merge, axis=1)
|
||||||
|
|
||||||
if ffill:
|
if ffill:
|
||||||
dataframe = dataframe.ffill()
|
dataframe = dataframe.ffill()
|
||||||
@ -97,3 +105,28 @@ def stoploss_from_open(
|
|||||||
return min(stoploss, 0.0)
|
return min(stoploss, 0.0)
|
||||||
else:
|
else:
|
||||||
return max(stoploss, 0.0)
|
return max(stoploss, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float:
|
||||||
|
"""
|
||||||
|
Given current price and desired stop price, return a stop loss value that is relative to current
|
||||||
|
price.
|
||||||
|
|
||||||
|
The requested stop can be positive for a stop above the open price, or negative for
|
||||||
|
a stop below the open price. The return value is always >= 0.
|
||||||
|
|
||||||
|
Returns 0 if the resulting stop price would be above the current price.
|
||||||
|
|
||||||
|
:param stop_rate: Stop loss price.
|
||||||
|
:param current_rate: Current asset price.
|
||||||
|
:return: Positive stop loss value relative to current price
|
||||||
|
"""
|
||||||
|
|
||||||
|
# formula is undefined for current_rate 0, return maximum value
|
||||||
|
if current_rate == 0:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
stoploss = 1 - (stop_rate / current_rate)
|
||||||
|
|
||||||
|
# negative stoploss values indicate the requested stop price is higher than the current price
|
||||||
|
return max(stoploss, 0.0)
|
||||||
|
2
setup.sh
2
setup.sh
@ -62,7 +62,7 @@ function updateenv() {
|
|||||||
then
|
then
|
||||||
REQUIREMENTS_PLOT="-r requirements-plot.txt"
|
REQUIREMENTS_PLOT="-r requirements-plot.txt"
|
||||||
fi
|
fi
|
||||||
if [ "${SYS_ARCH}" == "armv7l" ]; then
|
if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then
|
||||||
echo "Detected Raspberry, installing cython, skipping hyperopt installation."
|
echo "Detected Raspberry, installing cython, skipping hyperopt installation."
|
||||||
${PYTHON} -m pip install --upgrade cython
|
${PYTHON} -m pip install --upgrade cython
|
||||||
else
|
else
|
||||||
|
@ -18,7 +18,7 @@ from freqtrade import constants
|
|||||||
from freqtrade.commands import Arguments
|
from freqtrade.commands import Arguments
|
||||||
from freqtrade.data.converter import ohlcv_to_dataframe
|
from freqtrade.data.converter import ohlcv_to_dataframe
|
||||||
from freqtrade.edge import Edge, PairInfo
|
from freqtrade.edge import Edge, PairInfo
|
||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import Collateral, RunMode, TradingMode
|
||||||
from freqtrade.enums.signaltype import SignalDirection
|
from freqtrade.enums.signaltype import SignalDirection
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
@ -82,7 +82,13 @@ def patched_configuration_load_config_file(mocker, config) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None:
|
def patch_exchange(
|
||||||
|
mocker,
|
||||||
|
api_mock=None,
|
||||||
|
id='binance',
|
||||||
|
mock_markets=True,
|
||||||
|
mock_supported_modes=True
|
||||||
|
) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
@ -91,10 +97,22 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
|
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
|
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title()))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
|
mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2))
|
||||||
|
|
||||||
if mock_markets:
|
if mock_markets:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.markets',
|
mocker.patch('freqtrade.exchange.Exchange.markets',
|
||||||
PropertyMock(return_value=get_markets()))
|
PropertyMock(return_value=get_markets()))
|
||||||
|
|
||||||
|
if mock_supported_modes:
|
||||||
|
mocker.patch(
|
||||||
|
f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_collateral_pairs',
|
||||||
|
PropertyMock(return_value=[
|
||||||
|
(TradingMode.MARGIN, Collateral.CROSS),
|
||||||
|
(TradingMode.MARGIN, Collateral.ISOLATED),
|
||||||
|
(TradingMode.FUTURES, Collateral.CROSS),
|
||||||
|
(TradingMode.FUTURES, Collateral.ISOLATED)
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
if api_mock:
|
if api_mock:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
else:
|
else:
|
||||||
@ -102,8 +120,8 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No
|
|||||||
|
|
||||||
|
|
||||||
def get_patched_exchange(mocker, config, api_mock=None, id='binance',
|
def get_patched_exchange(mocker, config, api_mock=None, id='binance',
|
||||||
mock_markets=True) -> Exchange:
|
mock_markets=True, mock_supported_modes=True) -> Exchange:
|
||||||
patch_exchange(mocker, api_mock, id, mock_markets)
|
patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes)
|
||||||
config['exchange']['name'] = id
|
config['exchange']['name'] = id
|
||||||
try:
|
try:
|
||||||
exchange = ExchangeResolver.load_exchange(id, config)
|
exchange = ExchangeResolver.load_exchange(id, config)
|
||||||
@ -465,7 +483,10 @@ def get_markets():
|
|||||||
'max': 500000,
|
'max': 500000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'info': {},
|
'info': {
|
||||||
|
'leverage_buy': ['2'],
|
||||||
|
'leverage_sell': ['2'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'TKN/BTC': {
|
'TKN/BTC': {
|
||||||
'id': 'tknbtc',
|
'id': 'tknbtc',
|
||||||
@ -491,7 +512,10 @@ def get_markets():
|
|||||||
'max': 500000,
|
'max': 500000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'info': {},
|
'info': {
|
||||||
|
'leverage_buy': ['2', '3', '4', '5'],
|
||||||
|
'leverage_sell': ['2', '3', '4', '5'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'BLK/BTC': {
|
'BLK/BTC': {
|
||||||
'id': 'blkbtc',
|
'id': 'blkbtc',
|
||||||
@ -516,7 +540,10 @@ def get_markets():
|
|||||||
'max': 500000,
|
'max': 500000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'info': {},
|
'info': {
|
||||||
|
'leverage_buy': ['2', '3'],
|
||||||
|
'leverage_sell': ['2', '3'],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'LTC/BTC': {
|
'LTC/BTC': {
|
||||||
'id': 'ltcbtc',
|
'id': 'ltcbtc',
|
||||||
@ -541,7 +568,10 @@ def get_markets():
|
|||||||
'max': 500000,
|
'max': 500000,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'info': {},
|
'info': {
|
||||||
|
'leverage_buy': [],
|
||||||
|
'leverage_sell': [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'XRP/BTC': {
|
'XRP/BTC': {
|
||||||
'id': 'xrpbtc',
|
'id': 'xrpbtc',
|
||||||
@ -619,7 +649,10 @@ def get_markets():
|
|||||||
'max': None
|
'max': None
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'info': {},
|
'info': {
|
||||||
|
'leverage_buy': [],
|
||||||
|
'leverage_sell': [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
'ETH/USDT': {
|
'ETH/USDT': {
|
||||||
'id': 'USDT-ETH',
|
'id': 'USDT-ETH',
|
||||||
@ -735,6 +768,8 @@ def get_markets():
|
|||||||
'max': None
|
'max': None
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
'info': {
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,21 +1,31 @@
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.enums import Collateral, TradingMode
|
||||||
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException
|
||||||
from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re
|
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('limitratio,expected', [
|
@pytest.mark.parametrize('limitratio,expected,side', [
|
||||||
(None, 220 * 0.99),
|
(None, 220 * 0.99, "sell"),
|
||||||
(0.99, 220 * 0.99),
|
(0.99, 220 * 0.99, "sell"),
|
||||||
(0.98, 220 * 0.98),
|
(0.98, 220 * 0.98, "sell"),
|
||||||
|
(None, 220 * 1.01, "buy"),
|
||||||
|
(0.99, 220 * 1.01, "buy"),
|
||||||
|
(0.98, 220 * 1.02, "buy"),
|
||||||
])
|
])
|
||||||
def test_stoploss_order_binance(default_conf, mocker, limitratio, expected):
|
def test_stoploss_order_binance(
|
||||||
|
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'
|
||||||
@ -33,19 +43,32 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
with pytest.raises(OperationalException):
|
||||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
order = exchange.stoploss(
|
||||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=190,
|
||||||
|
side=side,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio}
|
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)
|
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 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert order['id'] == order_id
|
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]['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]['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]['side'] == side
|
||||||
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
|
||||||
@ -55,17 +78,31 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected):
|
|||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
exchange.stoploss(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={},
|
||||||
|
side=side,
|
||||||
|
leverage=1.0)
|
||||||
|
|
||||||
with pytest.raises(InvalidOrderException):
|
with pytest.raises(InvalidOrderException):
|
||||||
api_mock.create_order = MagicMock(
|
api_mock.create_order = MagicMock(
|
||||||
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
exchange.stoploss(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={},
|
||||||
|
side=side,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance",
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance",
|
||||||
"stoploss", "create_order", retries=1,
|
"stoploss", "create_order", retries=1,
|
||||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
|
||||||
|
side=side, leverage=1.0)
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_order_dry_run_binance(default_conf, mocker):
|
def test_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||||
@ -78,12 +115,25 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
with pytest.raises(OperationalException):
|
||||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
order = exchange.stoploss(
|
||||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=190,
|
||||||
|
side="sell",
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05},
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
order = exchange.stoploss(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={},
|
||||||
|
side="sell",
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
@ -94,18 +144,202 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker):
|
|||||||
assert order['amount'] == 1
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_adjust_binance(mocker, default_conf):
|
@pytest.mark.parametrize('sl1,sl2,sl3,side', [
|
||||||
|
(1501, 1499, 1501, "sell"),
|
||||||
|
(1499, 1501, 1499, "buy")
|
||||||
|
])
|
||||||
|
def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id='binance')
|
exchange = get_patched_exchange(mocker, default_conf, id='binance')
|
||||||
order = {
|
order = {
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': 1500,
|
'price': 1500,
|
||||||
'info': {'stopPrice': 1500},
|
'info': {'stopPrice': 1500},
|
||||||
}
|
}
|
||||||
assert exchange.stoploss_adjust(1501, order)
|
assert exchange.stoploss_adjust(sl1, order, side=side)
|
||||||
assert not exchange.stoploss_adjust(1499, order)
|
assert not exchange.stoploss_adjust(sl2, order, side=side)
|
||||||
# Test with invalid order case
|
# Test with invalid order case
|
||||||
order['type'] = 'stop_loss'
|
order['type'] = 'stop_loss'
|
||||||
assert not exchange.stoploss_adjust(1501, order)
|
assert not exchange.stoploss_adjust(sl3, order, side=side)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('pair,nominal_value,max_lev', [
|
||||||
|
("BNB/BUSD", 0.0, 40.0),
|
||||||
|
("BNB/USDT", 100.0, 153.84615384615384),
|
||||||
|
("BTC/USDT", 170.30, 250.0),
|
||||||
|
("BNB/BUSD", 999999.9, 10.0),
|
||||||
|
("BNB/USDT", 5000000.0, 6.666666666666667),
|
||||||
|
("BTC/USDT", 300000000.1, 2.0),
|
||||||
|
])
|
||||||
|
def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max_lev):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
|
exchange._leverage_brackets = {
|
||||||
|
'BNB/BUSD': [[0.0, 0.025],
|
||||||
|
[100000.0, 0.05],
|
||||||
|
[500000.0, 0.1],
|
||||||
|
[1000000.0, 0.15],
|
||||||
|
[2000000.0, 0.25],
|
||||||
|
[5000000.0, 0.5]],
|
||||||
|
'BNB/USDT': [[0.0, 0.0065],
|
||||||
|
[10000.0, 0.01],
|
||||||
|
[50000.0, 0.02],
|
||||||
|
[250000.0, 0.05],
|
||||||
|
[1000000.0, 0.1],
|
||||||
|
[2000000.0, 0.125],
|
||||||
|
[5000000.0, 0.15],
|
||||||
|
[10000000.0, 0.25]],
|
||||||
|
'BTC/USDT': [[0.0, 0.004],
|
||||||
|
[50000.0, 0.005],
|
||||||
|
[250000.0, 0.01],
|
||||||
|
[1000000.0, 0.025],
|
||||||
|
[5000000.0, 0.05],
|
||||||
|
[20000000.0, 0.1],
|
||||||
|
[50000000.0, 0.125],
|
||||||
|
[100000000.0, 0.15],
|
||||||
|
[200000000.0, 0.25],
|
||||||
|
[300000000.0, 0.5]],
|
||||||
|
}
|
||||||
|
assert exchange.get_max_leverage(pair, nominal_value) == max_lev
|
||||||
|
|
||||||
|
|
||||||
|
def test_fill_leverage_brackets_binance(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.load_leverage_brackets = MagicMock(return_value={
|
||||||
|
'ADA/BUSD': [[0.0, 0.025],
|
||||||
|
[100000.0, 0.05],
|
||||||
|
[500000.0, 0.1],
|
||||||
|
[1000000.0, 0.15],
|
||||||
|
[2000000.0, 0.25],
|
||||||
|
[5000000.0, 0.5]],
|
||||||
|
'BTC/USDT': [[0.0, 0.004],
|
||||||
|
[50000.0, 0.005],
|
||||||
|
[250000.0, 0.01],
|
||||||
|
[1000000.0, 0.025],
|
||||||
|
[5000000.0, 0.05],
|
||||||
|
[20000000.0, 0.1],
|
||||||
|
[50000000.0, 0.125],
|
||||||
|
[100000000.0, 0.15],
|
||||||
|
[200000000.0, 0.25],
|
||||||
|
[300000000.0, 0.5]],
|
||||||
|
"ZEC/USDT": [[0.0, 0.01],
|
||||||
|
[5000.0, 0.025],
|
||||||
|
[25000.0, 0.05],
|
||||||
|
[100000.0, 0.1],
|
||||||
|
[250000.0, 0.125],
|
||||||
|
[1000000.0, 0.5]],
|
||||||
|
|
||||||
|
})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
default_conf['trading_mode'] = TradingMode.FUTURES
|
||||||
|
default_conf['collateral'] = Collateral.ISOLATED
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
|
||||||
|
exchange.fill_leverage_brackets()
|
||||||
|
|
||||||
|
assert exchange._leverage_brackets == {
|
||||||
|
'ADA/BUSD': [[0.0, 0.025],
|
||||||
|
[100000.0, 0.05],
|
||||||
|
[500000.0, 0.1],
|
||||||
|
[1000000.0, 0.15],
|
||||||
|
[2000000.0, 0.25],
|
||||||
|
[5000000.0, 0.5]],
|
||||||
|
'BTC/USDT': [[0.0, 0.004],
|
||||||
|
[50000.0, 0.005],
|
||||||
|
[250000.0, 0.01],
|
||||||
|
[1000000.0, 0.025],
|
||||||
|
[5000000.0, 0.05],
|
||||||
|
[20000000.0, 0.1],
|
||||||
|
[50000000.0, 0.125],
|
||||||
|
[100000000.0, 0.15],
|
||||||
|
[200000000.0, 0.25],
|
||||||
|
[300000000.0, 0.5]],
|
||||||
|
"ZEC/USDT": [[0.0, 0.01],
|
||||||
|
[5000.0, 0.025],
|
||||||
|
[25000.0, 0.05],
|
||||||
|
[100000.0, 0.1],
|
||||||
|
[250000.0, 0.125],
|
||||||
|
[1000000.0, 0.5]],
|
||||||
|
}
|
||||||
|
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.load_leverage_brackets = MagicMock()
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'loadLeverageBrackets': True})
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
api_mock,
|
||||||
|
"binance",
|
||||||
|
"fill_leverage_brackets",
|
||||||
|
"load_leverage_brackets"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
default_conf['trading_mode'] = TradingMode.FUTURES
|
||||||
|
default_conf['collateral'] = Collateral.ISOLATED
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance")
|
||||||
|
exchange.fill_leverage_brackets()
|
||||||
|
|
||||||
|
leverage_brackets = {
|
||||||
|
"1000SHIB/USDT": [
|
||||||
|
[0.0, 0.01],
|
||||||
|
[5000.0, 0.025],
|
||||||
|
[25000.0, 0.05],
|
||||||
|
[100000.0, 0.1],
|
||||||
|
[250000.0, 0.125],
|
||||||
|
[1000000.0, 0.5]
|
||||||
|
],
|
||||||
|
"1INCH/USDT": [
|
||||||
|
[0.0, 0.012],
|
||||||
|
[5000.0, 0.025],
|
||||||
|
[25000.0, 0.05],
|
||||||
|
[100000.0, 0.1],
|
||||||
|
[250000.0, 0.125],
|
||||||
|
[1000000.0, 0.5]
|
||||||
|
],
|
||||||
|
"AAVE/USDT": [
|
||||||
|
[0.0, 0.01],
|
||||||
|
[50000.0, 0.02],
|
||||||
|
[250000.0, 0.05],
|
||||||
|
[1000000.0, 0.1],
|
||||||
|
[2000000.0, 0.125],
|
||||||
|
[5000000.0, 0.1665],
|
||||||
|
[10000000.0, 0.25]
|
||||||
|
],
|
||||||
|
"ADA/BUSD": [
|
||||||
|
[0.0, 0.025],
|
||||||
|
[100000.0, 0.05],
|
||||||
|
[500000.0, 0.1],
|
||||||
|
[1000000.0, 0.15],
|
||||||
|
[2000000.0, 0.25],
|
||||||
|
[5000000.0, 0.5]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in leverage_brackets.items():
|
||||||
|
assert exchange._leverage_brackets[key] == value
|
||||||
|
|
||||||
|
|
||||||
|
def test__set_leverage_binance(mocker, default_conf):
|
||||||
|
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.set_leverage = MagicMock()
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'setLeverage': True})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
|
exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN)
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
api_mock,
|
||||||
|
"binance",
|
||||||
|
"_set_leverage",
|
||||||
|
"set_leverage",
|
||||||
|
pair="XRP/USDT",
|
||||||
|
leverage=5.0,
|
||||||
|
trading_mode=TradingMode.FUTURES
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@ -138,3 +372,15 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog):
|
|||||||
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
assert exchange._api_async.fetch_ohlcv.call_count == 2
|
||||||
assert res == ohlcv
|
assert res == ohlcv
|
||||||
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog)
|
assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("trading_mode,collateral,config", [
|
||||||
|
("", "", {}),
|
||||||
|
("margin", "cross", {"options": {"defaultType": "margin"}}),
|
||||||
|
("futures", "isolated", {"options": {"defaultType": "future"}}),
|
||||||
|
])
|
||||||
|
def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config):
|
||||||
|
default_conf['trading_mode'] = trading_mode
|
||||||
|
default_conf['collateral'] = collateral
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="binance")
|
||||||
|
assert exchange._ccxt_config == config
|
||||||
|
@ -11,6 +11,7 @@ import ccxt
|
|||||||
import pytest
|
import pytest
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.enums import Collateral, TradingMode
|
||||||
from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException,
|
from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException,
|
||||||
OperationalException, PricingError, TemporaryError)
|
OperationalException, PricingError, TemporaryError)
|
||||||
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
|
from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken
|
||||||
@ -131,6 +132,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
|||||||
|
|
||||||
assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog)
|
assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog)
|
||||||
assert ex._api.headers == {'hello': 'world'}
|
assert ex._api.headers == {'hello': 'world'}
|
||||||
|
assert ex._ccxt_config == {}
|
||||||
Exchange._headers = {}
|
Exchange._headers = {}
|
||||||
|
|
||||||
|
|
||||||
@ -395,7 +397,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
|
|||||||
PropertyMock(return_value=markets)
|
PropertyMock(return_value=markets)
|
||||||
)
|
)
|
||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
|
||||||
assert isclose(result, 2 * (1+0.05) / (1-abs(stoploss)))
|
expected_result = 2 * (1+0.05) / (1-abs(stoploss))
|
||||||
|
assert isclose(result, expected_result)
|
||||||
|
# With Leverage
|
||||||
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0)
|
||||||
|
assert isclose(result, expected_result/3)
|
||||||
|
|
||||||
# min amount is set
|
# min amount is set
|
||||||
markets["ETH/BTC"]["limits"] = {
|
markets["ETH/BTC"]["limits"] = {
|
||||||
@ -407,7 +413,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
|
|||||||
PropertyMock(return_value=markets)
|
PropertyMock(return_value=markets)
|
||||||
)
|
)
|
||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
||||||
assert isclose(result, 2 * 2 * (1+0.05) / (1-abs(stoploss)))
|
expected_result = 2 * 2 * (1+0.05) / (1-abs(stoploss))
|
||||||
|
assert isclose(result, expected_result)
|
||||||
|
# With Leverage
|
||||||
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0)
|
||||||
|
assert isclose(result, expected_result/5)
|
||||||
|
|
||||||
# min amount and cost are set (cost is minimal)
|
# min amount and cost are set (cost is minimal)
|
||||||
markets["ETH/BTC"]["limits"] = {
|
markets["ETH/BTC"]["limits"] = {
|
||||||
@ -419,7 +429,11 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
|
|||||||
PropertyMock(return_value=markets)
|
PropertyMock(return_value=markets)
|
||||||
)
|
)
|
||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
||||||
assert isclose(result, max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)))
|
expected_result = max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss))
|
||||||
|
assert isclose(result, expected_result)
|
||||||
|
# With Leverage
|
||||||
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10)
|
||||||
|
assert isclose(result, expected_result/10)
|
||||||
|
|
||||||
# min amount and cost are set (amount is minial)
|
# min amount and cost are set (amount is minial)
|
||||||
markets["ETH/BTC"]["limits"] = {
|
markets["ETH/BTC"]["limits"] = {
|
||||||
@ -431,14 +445,26 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
|
|||||||
PropertyMock(return_value=markets)
|
PropertyMock(return_value=markets)
|
||||||
)
|
)
|
||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
|
||||||
assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)))
|
expected_result = max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))
|
||||||
|
assert isclose(result, expected_result)
|
||||||
|
# With Leverage
|
||||||
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0)
|
||||||
|
assert isclose(result, expected_result/7.0)
|
||||||
|
|
||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4)
|
||||||
assert isclose(result, max(8, 2 * 2) * 1.5)
|
expected_result = max(8, 2 * 2) * 1.5
|
||||||
|
assert isclose(result, expected_result)
|
||||||
|
# With Leverage
|
||||||
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0)
|
||||||
|
assert isclose(result, expected_result/8.0)
|
||||||
|
|
||||||
# Really big stoploss
|
# Really big stoploss
|
||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1)
|
||||||
assert isclose(result, max(8, 2 * 2) * 1.5)
|
expected_result = max(8, 2 * 2) * 1.5
|
||||||
|
assert isclose(result, expected_result)
|
||||||
|
# With Leverage
|
||||||
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0)
|
||||||
|
assert isclose(result, expected_result/12)
|
||||||
|
|
||||||
|
|
||||||
def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
|
def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
|
||||||
@ -456,10 +482,10 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
|
|||||||
PropertyMock(return_value=markets)
|
PropertyMock(return_value=markets)
|
||||||
)
|
)
|
||||||
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss)
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss)
|
||||||
assert round(result, 8) == round(
|
expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss))
|
||||||
max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)),
|
assert round(result, 8) == round(expected_result, 8)
|
||||||
8
|
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0)
|
||||||
)
|
assert round(result, 8) == round(expected_result/3, 8)
|
||||||
|
|
||||||
|
|
||||||
def test_set_sandbox(default_conf, mocker):
|
def test_set_sandbox(default_conf, mocker):
|
||||||
@ -970,7 +996,13 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
|
|
||||||
order = exchange.create_dry_run_order(
|
order = exchange.create_dry_run_order(
|
||||||
pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200)
|
pair='ETH/BTC',
|
||||||
|
ordertype='limit',
|
||||||
|
side=side,
|
||||||
|
amount=1,
|
||||||
|
rate=200,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert f'dry_run_{side}_' in order["id"]
|
assert f'dry_run_{side}_' in order["id"]
|
||||||
assert order["side"] == side
|
assert order["side"] == side
|
||||||
@ -993,7 +1025,13 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice,
|
|||||||
)
|
)
|
||||||
|
|
||||||
order = exchange.create_dry_run_order(
|
order = exchange.create_dry_run_order(
|
||||||
pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice)
|
pair='LTC/USDT',
|
||||||
|
ordertype='limit',
|
||||||
|
side=side,
|
||||||
|
amount=1,
|
||||||
|
rate=startprice,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
assert order_book_l2_usd.call_count == 1
|
assert order_book_l2_usd.call_count == 1
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert f'dry_run_{side}_' in order["id"]
|
assert f'dry_run_{side}_' in order["id"]
|
||||||
@ -1039,7 +1077,13 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou
|
|||||||
)
|
)
|
||||||
|
|
||||||
order = exchange.create_dry_run_order(
|
order = exchange.create_dry_run_order(
|
||||||
pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate)
|
pair='LTC/USDT',
|
||||||
|
ordertype='market',
|
||||||
|
side=side,
|
||||||
|
amount=amount,
|
||||||
|
rate=rate,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert f'dry_run_{side}_' in order["id"]
|
assert f'dry_run_{side}_' in order["id"]
|
||||||
assert order["side"] == side
|
assert order["side"] == side
|
||||||
@ -1049,10 +1093,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou
|
|||||||
assert round(order["average"], 4) == round(endprice, 4)
|
assert round(order["average"], 4) == round(endprice, 4)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("side", [
|
@pytest.mark.parametrize("side", ["buy", "sell"])
|
||||||
("buy"),
|
|
||||||
("sell")
|
|
||||||
])
|
|
||||||
@pytest.mark.parametrize("ordertype,rate,marketprice", [
|
@pytest.mark.parametrize("ordertype,rate,marketprice", [
|
||||||
("market", None, None),
|
("market", None, None),
|
||||||
("market", 200, True),
|
("market", 200, True),
|
||||||
@ -1074,9 +1115,17 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
|
|||||||
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)
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||||
|
exchange._set_leverage = MagicMock()
|
||||||
|
exchange.set_margin_mode = MagicMock()
|
||||||
|
|
||||||
order = exchange.create_order(
|
order = exchange.create_order(
|
||||||
pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200)
|
pair='ETH/BTC',
|
||||||
|
ordertype=ordertype,
|
||||||
|
side=side,
|
||||||
|
amount=1,
|
||||||
|
rate=200,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
@ -1086,6 +1135,21 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice,
|
|||||||
assert api_mock.create_order.call_args[0][2] == side
|
assert api_mock.create_order.call_args[0][2] == side
|
||||||
assert api_mock.create_order.call_args[0][3] == 1
|
assert api_mock.create_order.call_args[0][3] == 1
|
||||||
assert api_mock.create_order.call_args[0][4] is rate
|
assert api_mock.create_order.call_args[0][4] is rate
|
||||||
|
assert exchange._set_leverage.call_count == 0
|
||||||
|
assert exchange.set_margin_mode.call_count == 0
|
||||||
|
|
||||||
|
exchange.trading_mode = TradingMode.FUTURES
|
||||||
|
order = exchange.create_order(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
ordertype=ordertype,
|
||||||
|
side=side,
|
||||||
|
amount=1,
|
||||||
|
rate=200,
|
||||||
|
leverage=3.0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert exchange._set_leverage.call_count == 1
|
||||||
|
assert exchange.set_margin_mode.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_buy_dry_run(default_conf, mocker):
|
def test_buy_dry_run(default_conf, mocker):
|
||||||
@ -2624,10 +2688,17 @@ def test_get_fee(default_conf, mocker, exchange_name):
|
|||||||
def test_stoploss_order_unsupported_exchange(default_conf, mocker):
|
def test_stoploss_order_unsupported_exchange(default_conf, mocker):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id='bittrex')
|
exchange = get_patched_exchange(mocker, default_conf, id='bittrex')
|
||||||
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
||||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
exchange.stoploss(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={},
|
||||||
|
side="sell",
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
||||||
exchange.stoploss_adjust(1, {})
|
exchange.stoploss_adjust(1, {}, side="sell")
|
||||||
|
|
||||||
|
|
||||||
def test_merge_ft_has_dict(default_conf, mocker):
|
def test_merge_ft_has_dict(default_conf, mocker):
|
||||||
@ -2972,7 +3043,123 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None:
|
|||||||
(3, 5, 5),
|
(3, 5, 5),
|
||||||
(4, 5, 2),
|
(4, 5, 2),
|
||||||
(5, 5, 1),
|
(5, 5, 1),
|
||||||
|
|
||||||
])
|
])
|
||||||
def test_calculate_backoff(retrycount, max_retries, expected):
|
def test_calculate_backoff(retrycount, max_retries, expected):
|
||||||
assert calculate_backoff(retrycount, max_retries) == expected
|
assert calculate_backoff(retrycount, max_retries) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('exchange', ['binance', 'kraken', 'ftx'])
|
||||||
|
@pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [
|
||||||
|
(9.0, 3.0, 3.0),
|
||||||
|
(20.0, 5.0, 4.0),
|
||||||
|
(100.0, 100.0, 1.0)
|
||||||
|
])
|
||||||
|
def test_get_stake_amount_considering_leverage(
|
||||||
|
exchange,
|
||||||
|
stake_amount,
|
||||||
|
leverage,
|
||||||
|
min_stake_with_lev,
|
||||||
|
mocker,
|
||||||
|
default_conf
|
||||||
|
):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange)
|
||||||
|
assert exchange._get_stake_amount_considering_leverage(
|
||||||
|
stake_amount, leverage) == min_stake_with_lev
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("exchange_name,trading_mode", [
|
||||||
|
("binance", TradingMode.FUTURES),
|
||||||
|
("ftx", TradingMode.MARGIN),
|
||||||
|
("ftx", TradingMode.FUTURES)
|
||||||
|
])
|
||||||
|
def test__set_leverage(mocker, default_conf, exchange_name, trading_mode):
|
||||||
|
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.set_leverage = MagicMock()
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'setLeverage': True})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
api_mock,
|
||||||
|
exchange_name,
|
||||||
|
"_set_leverage",
|
||||||
|
"set_leverage",
|
||||||
|
pair="XRP/USDT",
|
||||||
|
leverage=5.0,
|
||||||
|
trading_mode=trading_mode
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("collateral", [
|
||||||
|
(Collateral.CROSS),
|
||||||
|
(Collateral.ISOLATED)
|
||||||
|
])
|
||||||
|
def test_set_margin_mode(mocker, default_conf, collateral):
|
||||||
|
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.set_margin_mode = MagicMock()
|
||||||
|
type(api_mock).has = PropertyMock(return_value={'setMarginMode': True})
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
|
||||||
|
ccxt_exceptionhandlers(
|
||||||
|
mocker,
|
||||||
|
default_conf,
|
||||||
|
api_mock,
|
||||||
|
"binance",
|
||||||
|
"set_margin_mode",
|
||||||
|
"set_margin_mode",
|
||||||
|
pair="XRP/USDT",
|
||||||
|
collateral=collateral
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("exchange_name, trading_mode, collateral, exception_thrown", [
|
||||||
|
("binance", TradingMode.SPOT, None, False),
|
||||||
|
("binance", TradingMode.MARGIN, Collateral.ISOLATED, True),
|
||||||
|
("kraken", TradingMode.SPOT, None, False),
|
||||||
|
("kraken", TradingMode.MARGIN, Collateral.ISOLATED, True),
|
||||||
|
("kraken", TradingMode.FUTURES, Collateral.ISOLATED, True),
|
||||||
|
("ftx", TradingMode.SPOT, None, False),
|
||||||
|
("ftx", TradingMode.MARGIN, Collateral.ISOLATED, True),
|
||||||
|
("ftx", TradingMode.FUTURES, Collateral.ISOLATED, True),
|
||||||
|
("bittrex", TradingMode.SPOT, None, False),
|
||||||
|
("bittrex", TradingMode.MARGIN, Collateral.CROSS, True),
|
||||||
|
("bittrex", TradingMode.MARGIN, Collateral.ISOLATED, True),
|
||||||
|
("bittrex", TradingMode.FUTURES, Collateral.CROSS, True),
|
||||||
|
("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True),
|
||||||
|
|
||||||
|
# TODO-lev: Remove once implemented
|
||||||
|
("binance", TradingMode.MARGIN, Collateral.CROSS, True),
|
||||||
|
("binance", TradingMode.FUTURES, Collateral.CROSS, True),
|
||||||
|
("binance", TradingMode.FUTURES, Collateral.ISOLATED, True),
|
||||||
|
("kraken", TradingMode.MARGIN, Collateral.CROSS, True),
|
||||||
|
("kraken", TradingMode.FUTURES, Collateral.CROSS, True),
|
||||||
|
("ftx", TradingMode.MARGIN, Collateral.CROSS, True),
|
||||||
|
("ftx", TradingMode.FUTURES, Collateral.CROSS, True),
|
||||||
|
|
||||||
|
# TODO-lev: Uncomment once implemented
|
||||||
|
# ("binance", TradingMode.MARGIN, Collateral.CROSS, False),
|
||||||
|
# ("binance", TradingMode.FUTURES, Collateral.CROSS, False),
|
||||||
|
# ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False),
|
||||||
|
# ("kraken", TradingMode.MARGIN, Collateral.CROSS, False),
|
||||||
|
# ("kraken", TradingMode.FUTURES, Collateral.CROSS, False),
|
||||||
|
# ("ftx", TradingMode.MARGIN, Collateral.CROSS, False),
|
||||||
|
# ("ftx", TradingMode.FUTURES, Collateral.CROSS, False)
|
||||||
|
])
|
||||||
|
def test_validate_trading_mode_and_collateral(
|
||||||
|
default_conf,
|
||||||
|
mocker,
|
||||||
|
exchange_name,
|
||||||
|
trading_mode,
|
||||||
|
collateral,
|
||||||
|
exception_thrown
|
||||||
|
):
|
||||||
|
exchange = get_patched_exchange(
|
||||||
|
mocker, default_conf, id=exchange_name, mock_supported_modes=False)
|
||||||
|
if (exception_thrown):
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
exchange.validate_trading_mode_and_collateral(trading_mode, collateral)
|
||||||
|
else:
|
||||||
|
exchange.validate_trading_mode_and_collateral(trading_mode, collateral)
|
||||||
|
@ -14,7 +14,11 @@ from .test_exchange import ccxt_exceptionhandlers
|
|||||||
STOPLOSS_ORDERTYPE = 'stop'
|
STOPLOSS_ORDERTYPE = 'stop'
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_order_ftx(default_conf, mocker):
|
@pytest.mark.parametrize('order_price,exchangelimitratio,side', [
|
||||||
|
(217.8, 1.05, "sell"),
|
||||||
|
(222.2, 0.95, "buy"),
|
||||||
|
])
|
||||||
|
def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitratio, 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))
|
||||||
|
|
||||||
@ -32,12 +36,18 @@ def test_stoploss_order_ftx(default_conf, mocker):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
|
|
||||||
# stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders
|
# stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders
|
||||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
order = exchange.stoploss(
|
||||||
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=190,
|
||||||
|
side=side,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio},
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
||||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
assert api_mock.create_order.call_args_list[0][1]['side'] == side
|
||||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
|
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
|
||||||
assert 'stopPrice' in api_mock.create_order.call_args_list[0][1]['params']
|
assert 'stopPrice' in api_mock.create_order.call_args_list[0][1]['params']
|
||||||
@ -47,51 +57,79 @@ def test_stoploss_order_ftx(default_conf, mocker):
|
|||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
order = exchange.stoploss(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={},
|
||||||
|
side=side,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert order['id'] == order_id
|
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]['symbol'] == 'ETH/BTC'
|
||||||
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
||||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
assert api_mock.create_order.call_args_list[0][1]['side'] == side
|
||||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
|
assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params']
|
||||||
assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220
|
assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220
|
||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
order = exchange.stoploss(
|
||||||
order_types={'stoploss': 'limit'})
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={'stoploss': 'limit'}, side=side,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert order['id'] == order_id
|
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]['symbol'] == 'ETH/BTC'
|
||||||
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
||||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
assert api_mock.create_order.call_args_list[0][1]['side'] == side
|
||||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params']
|
assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params']
|
||||||
assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8
|
assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == order_price
|
||||||
assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220
|
assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220
|
||||||
|
|
||||||
# test exception handling
|
# test exception handling
|
||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
exchange.stoploss(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={},
|
||||||
|
side=side,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(InvalidOrderException):
|
with pytest.raises(InvalidOrderException):
|
||||||
api_mock.create_order = MagicMock(
|
api_mock.create_order = MagicMock(
|
||||||
side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately."))
|
side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately."))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx')
|
||||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
exchange.stoploss(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={},
|
||||||
|
side=side,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx",
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx",
|
||||||
"stoploss", "create_order", retries=1,
|
"stoploss", "create_order", retries=1,
|
||||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
|
||||||
|
side=side, leverage=1.0)
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_order_dry_run_ftx(default_conf, mocker):
|
@pytest.mark.parametrize('side', [("sell"), ("buy")])
|
||||||
|
def test_stoploss_order_dry_run_ftx(default_conf, mocker, side):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
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)
|
||||||
@ -101,7 +139,14 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker):
|
|||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
order = exchange.stoploss(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={},
|
||||||
|
side=side,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
@ -112,20 +157,24 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker):
|
|||||||
assert order['amount'] == 1
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_adjust_ftx(mocker, default_conf):
|
@pytest.mark.parametrize('sl1,sl2,sl3,side', [
|
||||||
|
(1501, 1499, 1501, "sell"),
|
||||||
|
(1499, 1501, 1499, "buy")
|
||||||
|
])
|
||||||
|
def test_stoploss_adjust_ftx(mocker, default_conf, sl1, sl2, sl3, side):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
|
exchange = get_patched_exchange(mocker, default_conf, id='ftx')
|
||||||
order = {
|
order = {
|
||||||
'type': STOPLOSS_ORDERTYPE,
|
'type': STOPLOSS_ORDERTYPE,
|
||||||
'price': 1500,
|
'price': 1500,
|
||||||
}
|
}
|
||||||
assert exchange.stoploss_adjust(1501, order)
|
assert exchange.stoploss_adjust(sl1, order, side=side)
|
||||||
assert not exchange.stoploss_adjust(1499, order)
|
assert not exchange.stoploss_adjust(sl2, order, side=side)
|
||||||
# Test with invalid order case ...
|
# Test with invalid order case ...
|
||||||
order['type'] = 'stop_loss_limit'
|
order['type'] = 'stop_loss_limit'
|
||||||
assert not exchange.stoploss_adjust(1501, order)
|
assert not exchange.stoploss_adjust(sl3, order, side=side)
|
||||||
|
|
||||||
|
|
||||||
def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order):
|
def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order, limit_buy_order):
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
order = MagicMock()
|
order = MagicMock()
|
||||||
order.myid = 123
|
order.myid = 123
|
||||||
@ -158,6 +207,16 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order):
|
|||||||
assert resp['type'] == 'stop'
|
assert resp['type'] == 'stop'
|
||||||
assert resp['status_stop'] == 'triggered'
|
assert resp['status_stop'] == 'triggered'
|
||||||
|
|
||||||
|
api_mock.fetch_order = MagicMock(return_value=limit_buy_order)
|
||||||
|
|
||||||
|
resp = exchange.fetch_stoploss_order('X', 'TKN/BTC')
|
||||||
|
assert resp
|
||||||
|
assert api_mock.fetch_order.call_count == 1
|
||||||
|
assert resp['id_stop'] == 'mocked_limit_buy'
|
||||||
|
assert resp['id'] == 'X'
|
||||||
|
assert resp['type'] == 'stop'
|
||||||
|
assert resp['status_stop'] == 'triggered'
|
||||||
|
|
||||||
with pytest.raises(InvalidOrderException):
|
with pytest.raises(InvalidOrderException):
|
||||||
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx')
|
||||||
@ -191,3 +250,20 @@ def test_get_order_id(mocker, default_conf):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
assert exchange.get_order_id_conditional(order) == '1111'
|
assert exchange.get_order_id_conditional(order) == '1111'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('pair,nominal_value,max_lev', [
|
||||||
|
("ADA/BTC", 0.0, 20.0),
|
||||||
|
("BTC/EUR", 100.0, 20.0),
|
||||||
|
("ZEC/USD", 173.31, 20.0),
|
||||||
|
])
|
||||||
|
def test_get_max_leverage_ftx(default_conf, mocker, pair, nominal_value, max_lev):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="ftx")
|
||||||
|
assert exchange.get_max_leverage(pair, nominal_value) == max_lev
|
||||||
|
|
||||||
|
|
||||||
|
def test_fill_leverage_brackets_ftx(default_conf, mocker):
|
||||||
|
# FTX only has one account wide leverage, so there's no leverage brackets
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="ftx")
|
||||||
|
exchange.fill_leverage_brackets()
|
||||||
|
assert exchange._leverage_brackets == {}
|
||||||
|
@ -166,7 +166,11 @@ def test_get_balances_prod(default_conf, mocker):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('ordertype', ['market', 'limit'])
|
@pytest.mark.parametrize('ordertype', ['market', 'limit'])
|
||||||
def test_stoploss_order_kraken(default_conf, mocker, ordertype):
|
@pytest.mark.parametrize('side,adjustedprice', [
|
||||||
|
("sell", 217.8),
|
||||||
|
("buy", 222.2),
|
||||||
|
])
|
||||||
|
def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice):
|
||||||
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))
|
||||||
|
|
||||||
@ -183,10 +187,17 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype):
|
|||||||
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||||
|
|
||||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220,
|
order = exchange.stoploss(
|
||||||
order_types={'stoploss': ordertype,
|
pair='ETH/BTC',
|
||||||
'stoploss_on_exchange_limit_ratio': 0.99
|
amount=1,
|
||||||
})
|
stop_price=220,
|
||||||
|
side=side,
|
||||||
|
order_types={
|
||||||
|
'stoploss': ordertype,
|
||||||
|
'stoploss_on_exchange_limit_ratio': 0.99
|
||||||
|
},
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
@ -195,12 +206,14 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype):
|
|||||||
if ordertype == 'limit':
|
if ordertype == 'limit':
|
||||||
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_LIMIT_ORDERTYPE
|
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_LIMIT_ORDERTYPE
|
||||||
assert api_mock.create_order.call_args_list[0][1]['params'] == {
|
assert api_mock.create_order.call_args_list[0][1]['params'] == {
|
||||||
'trading_agreement': 'agree', 'price2': 217.8}
|
'trading_agreement': 'agree',
|
||||||
|
'price2': adjustedprice
|
||||||
|
}
|
||||||
else:
|
else:
|
||||||
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE
|
||||||
assert api_mock.create_order.call_args_list[0][1]['params'] == {
|
assert api_mock.create_order.call_args_list[0][1]['params'] == {
|
||||||
'trading_agreement': 'agree'}
|
'trading_agreement': 'agree'}
|
||||||
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
assert api_mock.create_order.call_args_list[0][1]['side'] == side
|
||||||
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||||
|
|
||||||
@ -208,20 +221,36 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype):
|
|||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
exchange.stoploss(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={},
|
||||||
|
side=side,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
with pytest.raises(InvalidOrderException):
|
with pytest.raises(InvalidOrderException):
|
||||||
api_mock.create_order = MagicMock(
|
api_mock.create_order = MagicMock(
|
||||||
side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately."))
|
side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately."))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||||
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
exchange.stoploss(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={},
|
||||||
|
side=side,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
|
||||||
"stoploss", "create_order", retries=1,
|
"stoploss", "create_order", retries=1,
|
||||||
pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
pair='ETH/BTC', amount=1, stop_price=220, order_types={},
|
||||||
|
side=side, leverage=1.0)
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
@pytest.mark.parametrize('side', ['buy', 'sell'])
|
||||||
|
def test_stoploss_order_dry_run_kraken(default_conf, mocker, side):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
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)
|
||||||
@ -231,7 +260,14 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
|||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
order = exchange.stoploss(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
amount=1,
|
||||||
|
stop_price=220,
|
||||||
|
order_types={},
|
||||||
|
side=side,
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
@ -242,14 +278,54 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
|||||||
assert order['amount'] == 1
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_adjust_kraken(mocker, default_conf):
|
@pytest.mark.parametrize('sl1,sl2,sl3,side', [
|
||||||
|
(1501, 1499, 1501, "sell"),
|
||||||
|
(1499, 1501, 1499, "buy")
|
||||||
|
])
|
||||||
|
def test_stoploss_adjust_kraken(mocker, default_conf, sl1, sl2, sl3, side):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id='kraken')
|
exchange = get_patched_exchange(mocker, default_conf, id='kraken')
|
||||||
order = {
|
order = {
|
||||||
'type': STOPLOSS_ORDERTYPE,
|
'type': STOPLOSS_ORDERTYPE,
|
||||||
'price': 1500,
|
'price': 1500,
|
||||||
}
|
}
|
||||||
assert exchange.stoploss_adjust(1501, order)
|
assert exchange.stoploss_adjust(sl1, order, side=side)
|
||||||
assert not exchange.stoploss_adjust(1499, order)
|
assert not exchange.stoploss_adjust(sl2, order, side=side)
|
||||||
# Test with invalid order case ...
|
# Test with invalid order case ...
|
||||||
order['type'] = 'stop_loss_limit'
|
order['type'] = 'stop_loss_limit'
|
||||||
assert not exchange.stoploss_adjust(1501, order)
|
assert not exchange.stoploss_adjust(sl3, order, side=side)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('pair,nominal_value,max_lev', [
|
||||||
|
("ADA/BTC", 0.0, 3.0),
|
||||||
|
("BTC/EUR", 100.0, 5.0),
|
||||||
|
("ZEC/USD", 173.31, 2.0),
|
||||||
|
])
|
||||||
|
def test_get_max_leverage_kraken(default_conf, mocker, pair, nominal_value, max_lev):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id="kraken")
|
||||||
|
exchange._leverage_brackets = {
|
||||||
|
'ADA/BTC': ['2', '3'],
|
||||||
|
'BTC/EUR': ['2', '3', '4', '5'],
|
||||||
|
'ZEC/USD': ['2']
|
||||||
|
}
|
||||||
|
assert exchange.get_max_leverage(pair, nominal_value) == max_lev
|
||||||
|
|
||||||
|
|
||||||
|
def test_fill_leverage_brackets_kraken(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
|
||||||
|
exchange.fill_leverage_brackets()
|
||||||
|
|
||||||
|
assert exchange._leverage_brackets == {
|
||||||
|
'BLK/BTC': [1, 2, 3],
|
||||||
|
'TKN/BTC': [1, 2, 3, 4, 5],
|
||||||
|
'ETH/BTC': [1, 2],
|
||||||
|
'LTC/BTC': [1],
|
||||||
|
'XRP/BTC': [1],
|
||||||
|
'NEO/BTC': [1],
|
||||||
|
'BTT/BTC': [1],
|
||||||
|
'ETH/USDT': [1],
|
||||||
|
'LTC/USDT': [1],
|
||||||
|
'LTC/USD': [1],
|
||||||
|
'XLTCUSDT': [1],
|
||||||
|
'LTC/ETH': [1]
|
||||||
|
}
|
||||||
|
@ -22,9 +22,10 @@ twentyfive_hours = Decimal(25.0)
|
|||||||
('kraken', 0.00025, five_hours, 0.045),
|
('kraken', 0.00025, five_hours, 0.045),
|
||||||
('kraken', 0.00025, twentyfive_hours, 0.12),
|
('kraken', 0.00025, twentyfive_hours, 0.12),
|
||||||
# FTX
|
# FTX
|
||||||
# TODO-lev: - implement FTX tests
|
('ftx', 0.0005, ten_mins, 0.00125),
|
||||||
# ('ftx', Decimal(0.0005), ten_mins, 0.06),
|
('ftx', 0.00025, ten_mins, 0.000625),
|
||||||
# ('ftx', Decimal(0.0005), five_hours, 0.045),
|
('ftx', 0.00025, five_hours, 0.003125),
|
||||||
|
('ftx', 0.00025, twentyfive_hours, 0.015625),
|
||||||
])
|
])
|
||||||
def test_interest(exchange, interest_rate, hours, expected):
|
def test_interest(exchange, interest_rate, hours, expected):
|
||||||
borrowed = Decimal(60.0)
|
borrowed = Decimal(60.0)
|
@ -12,7 +12,8 @@ from freqtrade.persistence import Trade
|
|||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.plugins.pairlistmanager import PairListManager
|
from freqtrade.plugins.pairlistmanager import PairListManager
|
||||||
from freqtrade.resolvers import PairListResolver
|
from freqtrade.resolvers import PairListResolver
|
||||||
from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re
|
from tests.conftest import (create_mock_trades, get_patched_exchange, get_patched_freqtradebot,
|
||||||
|
log_has, log_has_re)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="function")
|
@pytest.fixture(scope="function")
|
||||||
@ -663,6 +664,31 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
|
|||||||
assert log_has("PerformanceFilter is not available in this mode.", caplog)
|
assert log_has("PerformanceFilter is not available in this mode.", caplog)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None:
|
||||||
|
whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
|
||||||
|
whitelist_conf['pairlists'] = [
|
||||||
|
{"method": "StaticPairList"},
|
||||||
|
{"method": "PerformanceFilter", "minutes": 60}
|
||||||
|
]
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
exchange = get_patched_exchange(mocker, whitelist_conf)
|
||||||
|
pm = PairListManager(exchange, whitelist_conf)
|
||||||
|
pm.refresh_pairlist()
|
||||||
|
|
||||||
|
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC']
|
||||||
|
|
||||||
|
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
|
||||||
|
create_mock_trades(fee)
|
||||||
|
pm.refresh_pairlist()
|
||||||
|
assert pm.whitelist == ['XRP/BTC', 'ETH/BTC', 'TKN/BTC']
|
||||||
|
|
||||||
|
# Move to "outside" of lookback window, so original sorting is restored.
|
||||||
|
t.move_to("2021-09-01 07:00:00 +00:00")
|
||||||
|
pm.refresh_pairlist()
|
||||||
|
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC']
|
||||||
|
|
||||||
|
|
||||||
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
|
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
|
||||||
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}]
|
default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}]
|
||||||
|
|
||||||
|
@ -422,20 +422,22 @@ def test_api_stopbuy(botclient):
|
|||||||
assert ftbot.config['max_open_trades'] == 0
|
assert ftbot.config['max_open_trades'] == 0
|
||||||
|
|
||||||
|
|
||||||
def test_api_balance(botclient, mocker, rpc_balance):
|
def test_api_balance(botclient, mocker, rpc_balance, tickers):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
|
|
||||||
ftbot.config['dry_run'] = False
|
ftbot.config['dry_run'] = False
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
|
mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
|
mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination',
|
||||||
side_effect=lambda a, b: f"{a}/{b}")
|
side_effect=lambda a, b: f"{a}/{b}")
|
||||||
ftbot.wallets.update()
|
ftbot.wallets.update()
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/balance")
|
rc = client_get(client, f"{BASE_URI}/balance")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert "currencies" in rc.json()
|
response = rc.json()
|
||||||
assert len(rc.json()["currencies"]) == 5
|
assert "currencies" in response
|
||||||
assert rc.json()['currencies'][0] == {
|
assert len(response["currencies"]) == 5
|
||||||
|
assert response['currencies'][0] == {
|
||||||
'currency': 'BTC',
|
'currency': 'BTC',
|
||||||
'free': 12.0,
|
'free': 12.0,
|
||||||
'balance': 12.0,
|
'balance': 12.0,
|
||||||
@ -443,6 +445,10 @@ def test_api_balance(botclient, mocker, rpc_balance):
|
|||||||
'est_stake': 12.0,
|
'est_stake': 12.0,
|
||||||
'stake': 'BTC',
|
'stake': 'BTC',
|
||||||
}
|
}
|
||||||
|
assert 'starting_capital' in response
|
||||||
|
assert 'starting_capital_fiat' in response
|
||||||
|
assert 'starting_capital_pct' in response
|
||||||
|
assert 'starting_capital_ratio' in response
|
||||||
|
|
||||||
|
|
||||||
def test_api_count(botclient, mocker, ticker, fee, markets):
|
def test_api_count(botclient, mocker, ticker, fee, markets):
|
||||||
@ -1218,6 +1224,7 @@ def test_api_strategies(botclient):
|
|||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json() == {'strategies': [
|
assert rc.json() == {'strategies': [
|
||||||
'HyperoptableStrategy',
|
'HyperoptableStrategy',
|
||||||
|
'InformativeDecoratorTest',
|
||||||
'StrategyTestV2',
|
'StrategyTestV2',
|
||||||
'TestStrategyLegacyV1'
|
'TestStrategyLegacyV1'
|
||||||
]}
|
]}
|
||||||
|
@ -576,6 +576,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None
|
|||||||
'total': 100.0,
|
'total': 100.0,
|
||||||
'symbol': 100.0,
|
'symbol': 100.0,
|
||||||
'value': 1000.0,
|
'value': 1000.0,
|
||||||
|
'starting_capital': 1000,
|
||||||
|
'starting_capital_fiat': 1000,
|
||||||
})
|
})
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
75
tests/strategy/strats/informative_decorator_strategy.py
Normal file
75
tests/strategy/strats/informative_decorator_strategy.py
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||||||
|
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.strategy import informative, merge_informative_pair
|
||||||
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class InformativeDecoratorTest(IStrategy):
|
||||||
|
"""
|
||||||
|
Strategy used by tests freqtrade bot.
|
||||||
|
Please do not modify this strategy, it's intended for internal use only.
|
||||||
|
Please look at the SampleStrategy in the user_data/strategy directory
|
||||||
|
or strategy repository https://github.com/freqtrade/freqtrade-strategies
|
||||||
|
for samples and inspiration.
|
||||||
|
"""
|
||||||
|
INTERFACE_VERSION = 2
|
||||||
|
stoploss = -0.10
|
||||||
|
timeframe = '5m'
|
||||||
|
startup_candle_count: int = 20
|
||||||
|
|
||||||
|
def informative_pairs(self):
|
||||||
|
return [('BTC/USDT', '5m')]
|
||||||
|
|
||||||
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['buy'] = 0
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['sell'] = 0
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Decorator stacking test.
|
||||||
|
@informative('30m')
|
||||||
|
@informative('1h')
|
||||||
|
def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Simple informative test.
|
||||||
|
@informative('1h', 'BTC/{stake}')
|
||||||
|
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Quote currency different from stake currency test.
|
||||||
|
@informative('1h', 'ETH/BTC')
|
||||||
|
def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Formatting test.
|
||||||
|
@informative('30m', 'BTC/{stake}', '{column}_{BASE}_{QUOTE}_{base}_{quote}_{asset}_{timeframe}')
|
||||||
|
def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
# Custom formatter test
|
||||||
|
@informative('30m', 'ETH/{stake}', fmt=lambda column, **kwargs: column + '_from_callable')
|
||||||
|
def populate_indicators_eth_30m(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
return dataframe
|
||||||
|
|
||||||
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
# Strategy timeframe indicators for current pair.
|
||||||
|
dataframe['rsi'] = 14
|
||||||
|
# Informative pairs are available in this method.
|
||||||
|
dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h']
|
||||||
|
|
||||||
|
# Mixing manual informative pairs with decorators.
|
||||||
|
informative = self.dp.get_pair_dataframe('BTC/USDT', '5m')
|
||||||
|
informative['rsi'] = 14
|
||||||
|
dataframe = merge_informative_pair(dataframe, informative, self.timeframe, '5m', ffill=True)
|
||||||
|
|
||||||
|
return dataframe
|
@ -648,7 +648,7 @@ def test_is_informative_pairs_callback(default_conf):
|
|||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
# Should return empty
|
# Should return empty
|
||||||
# Uses fallback to base implementation
|
# Uses fallback to base implementation
|
||||||
assert [] == strategy.informative_pairs()
|
assert [] == strategy.gather_informative_pairs()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('error', [
|
@pytest.mark.parametrize('error', [
|
||||||
|
@ -4,7 +4,9 @@ import numpy as np
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
|
from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open,
|
||||||
|
timeframe_to_minutes)
|
||||||
|
|
||||||
|
|
||||||
def generate_test_data(timeframe: str, size: int):
|
def generate_test_data(timeframe: str, size: int):
|
||||||
@ -132,3 +134,65 @@ def test_stoploss_from_open():
|
|||||||
assert stoploss == 0
|
assert stoploss == 0
|
||||||
else:
|
else:
|
||||||
assert isclose(stop_price, expected_stop_price, rel_tol=0.00001)
|
assert isclose(stop_price, expected_stop_price, rel_tol=0.00001)
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_from_absolute():
|
||||||
|
assert stoploss_from_absolute(90, 100) == 1 - (90 / 100)
|
||||||
|
assert stoploss_from_absolute(100, 100) == 0
|
||||||
|
assert stoploss_from_absolute(110, 100) == 0
|
||||||
|
assert stoploss_from_absolute(100, 0) == 1
|
||||||
|
assert stoploss_from_absolute(0, 100) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_informative_decorator(mocker, default_conf):
|
||||||
|
test_data_5m = generate_test_data('5m', 40)
|
||||||
|
test_data_30m = generate_test_data('30m', 40)
|
||||||
|
test_data_1h = generate_test_data('1h', 40)
|
||||||
|
data = {
|
||||||
|
('XRP/USDT', '5m'): test_data_5m,
|
||||||
|
('XRP/USDT', '30m'): test_data_30m,
|
||||||
|
('XRP/USDT', '1h'): test_data_1h,
|
||||||
|
('LTC/USDT', '5m'): test_data_5m,
|
||||||
|
('LTC/USDT', '30m'): test_data_30m,
|
||||||
|
('LTC/USDT', '1h'): test_data_1h,
|
||||||
|
('BTC/USDT', '30m'): test_data_30m,
|
||||||
|
('BTC/USDT', '5m'): test_data_5m,
|
||||||
|
('BTC/USDT', '1h'): test_data_1h,
|
||||||
|
('ETH/USDT', '1h'): test_data_1h,
|
||||||
|
('ETH/USDT', '30m'): test_data_30m,
|
||||||
|
('ETH/BTC', '1h'): test_data_1h,
|
||||||
|
}
|
||||||
|
from .strats.informative_decorator_strategy import InformativeDecoratorTest
|
||||||
|
default_conf['stake_currency'] = 'USDT'
|
||||||
|
strategy = InformativeDecoratorTest(config=default_conf)
|
||||||
|
strategy.dp = DataProvider({}, None, None)
|
||||||
|
mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[
|
||||||
|
'XRP/USDT', 'LTC/USDT', 'BTC/USDT'
|
||||||
|
])
|
||||||
|
|
||||||
|
assert len(strategy._ft_informative) == 6 # Equal to number of decorators used
|
||||||
|
informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'),
|
||||||
|
('LTC/USDT', '30m'), ('BTC/USDT', '1h'), ('BTC/USDT', '30m'),
|
||||||
|
('BTC/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')]
|
||||||
|
for inf_pair in informative_pairs:
|
||||||
|
assert inf_pair in strategy.gather_informative_pairs()
|
||||||
|
|
||||||
|
def test_historic_ohlcv(pair, timeframe):
|
||||||
|
return data[(pair, timeframe or strategy.timeframe)].copy()
|
||||||
|
mocker.patch('freqtrade.data.dataprovider.DataProvider.historic_ohlcv',
|
||||||
|
side_effect=test_historic_ohlcv)
|
||||||
|
|
||||||
|
analyzed = strategy.advise_all_indicators(
|
||||||
|
{p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')})
|
||||||
|
expected_columns = [
|
||||||
|
'rsi_1h', 'rsi_30m', # Stacked informative decorators
|
||||||
|
'btc_usdt_rsi_1h', # BTC 1h informative
|
||||||
|
'rsi_BTC_USDT_btc_usdt_BTC/USDT_30m', # Column formatting
|
||||||
|
'rsi_from_callable', # Custom column formatter
|
||||||
|
'eth_btc_rsi_1h', # Quote currency not matching stake currency
|
||||||
|
'rsi', 'rsi_less', # Non-informative columns
|
||||||
|
'rsi_5m', # Manual informative dataframe
|
||||||
|
]
|
||||||
|
for _, dataframe in analyzed.items():
|
||||||
|
for col in expected_columns:
|
||||||
|
assert col in dataframe.columns
|
||||||
|
@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed():
|
|||||||
directory = Path(__file__).parent / "strats"
|
directory = Path(__file__).parent / "strats"
|
||||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
|
strategies = StrategyResolver.search_all_objects(directory, enum_failed=False)
|
||||||
assert isinstance(strategies, list)
|
assert isinstance(strategies, list)
|
||||||
assert len(strategies) == 3
|
assert len(strategies) == 4
|
||||||
assert isinstance(strategies[0], dict)
|
assert isinstance(strategies[0], dict)
|
||||||
|
|
||||||
|
|
||||||
@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed():
|
|||||||
directory = Path(__file__).parent / "strats"
|
directory = Path(__file__).parent / "strats"
|
||||||
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
|
strategies = StrategyResolver.search_all_objects(directory, enum_failed=True)
|
||||||
assert isinstance(strategies, list)
|
assert isinstance(strategies, list)
|
||||||
assert len(strategies) == 4
|
assert len(strategies) == 5
|
||||||
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
# with enum_failed=True search_all_objects() shall find 2 good strategies
|
||||||
# and 1 which fails to load
|
# and 1 which fails to load
|
||||||
assert len([x for x in strategies if x['class'] is not None]) == 3
|
assert len([x for x in strategies if x['class'] is not None]) == 4
|
||||||
assert len([x for x in strategies if x['class'] is None]) == 1
|
assert len([x for x in strategies if x['class'] is None]) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,11 +78,15 @@ def test_bot_cleanup(mocker, default_conf, caplog) -> None:
|
|||||||
assert coo_mock.call_count == 1
|
assert coo_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
|
@pytest.mark.parametrize('runmode', [
|
||||||
|
RunMode.DRY_RUN,
|
||||||
|
RunMode.LIVE
|
||||||
|
])
|
||||||
|
def test_order_dict(default_conf, mocker, runmode, caplog) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
conf = default_conf.copy()
|
conf = default_conf.copy()
|
||||||
conf['runmode'] = RunMode.DRY_RUN
|
conf['runmode'] = runmode
|
||||||
conf['order_types'] = {
|
conf['order_types'] = {
|
||||||
'buy': 'market',
|
'buy': 'market',
|
||||||
'sell': 'limit',
|
'sell': 'limit',
|
||||||
@ -92,45 +96,14 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
|
|||||||
conf['bid_strategy']['price_side'] = 'ask'
|
conf['bid_strategy']['price_side'] = 'ask'
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(conf)
|
freqtrade = FreqtradeBot(conf)
|
||||||
|
if runmode == RunMode.LIVE:
|
||||||
|
assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
|
||||||
assert freqtrade.strategy.order_types['stoploss_on_exchange']
|
assert freqtrade.strategy.order_types['stoploss_on_exchange']
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
# is left untouched
|
# is left untouched
|
||||||
conf = default_conf.copy()
|
conf = default_conf.copy()
|
||||||
conf['runmode'] = RunMode.DRY_RUN
|
conf['runmode'] = runmode
|
||||||
conf['order_types'] = {
|
|
||||||
'buy': 'market',
|
|
||||||
'sell': 'limit',
|
|
||||||
'stoploss': 'limit',
|
|
||||||
'stoploss_on_exchange': False,
|
|
||||||
}
|
|
||||||
freqtrade = FreqtradeBot(conf)
|
|
||||||
assert not freqtrade.strategy.order_types['stoploss_on_exchange']
|
|
||||||
assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_order_dict_live(default_conf, mocker, caplog) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
|
|
||||||
conf = default_conf.copy()
|
|
||||||
conf['runmode'] = RunMode.LIVE
|
|
||||||
conf['order_types'] = {
|
|
||||||
'buy': 'market',
|
|
||||||
'sell': 'limit',
|
|
||||||
'stoploss': 'limit',
|
|
||||||
'stoploss_on_exchange': True,
|
|
||||||
}
|
|
||||||
conf['bid_strategy']['price_side'] = 'ask'
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(conf)
|
|
||||||
assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog)
|
|
||||||
assert freqtrade.strategy.order_types['stoploss_on_exchange']
|
|
||||||
|
|
||||||
caplog.clear()
|
|
||||||
# is left untouched
|
|
||||||
conf = default_conf.copy()
|
|
||||||
conf['runmode'] = RunMode.LIVE
|
|
||||||
conf['order_types'] = {
|
conf['order_types'] = {
|
||||||
'buy': 'market',
|
'buy': 'market',
|
||||||
'sell': 'limit',
|
'sell': 'limit',
|
||||||
@ -219,8 +192,14 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
|
|||||||
'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21
|
'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21
|
||||||
|
|
||||||
|
|
||||||
def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None:
|
@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [
|
||||||
|
# Override stoploss
|
||||||
|
(0.79, False),
|
||||||
|
# Override strategy stoploss
|
||||||
|
(0.85, True)
|
||||||
|
])
|
||||||
|
def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker,
|
||||||
|
buy_price_mult, ignore_strat_sl, edge_conf) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
patch_edge(mocker)
|
patch_edge(mocker)
|
||||||
@ -234,9 +213,9 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=MagicMock(return_value={
|
fetch_ticker=MagicMock(return_value={
|
||||||
'bid': buy_price * 0.79,
|
'bid': buy_price * buy_price_mult,
|
||||||
'ask': buy_price * 0.79,
|
'ask': buy_price * buy_price_mult,
|
||||||
'last': buy_price * 0.79
|
'last': buy_price * buy_price_mult,
|
||||||
}),
|
}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
@ -253,46 +232,10 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
|
|||||||
#############################################
|
#############################################
|
||||||
|
|
||||||
# stoploss shoud be hit
|
# stoploss shoud be hit
|
||||||
assert freqtrade.handle_trade(trade) is True
|
assert freqtrade.handle_trade(trade) is not ignore_strat_sl
|
||||||
assert log_has('Exit for NEO/BTC detected. Reason: stop_loss', caplog)
|
if not ignore_strat_sl:
|
||||||
assert trade.sell_reason == SellType.STOP_LOSS.value
|
assert log_has('Exit for NEO/BTC detected. Reason: stop_loss', caplog)
|
||||||
|
assert trade.sell_reason == SellType.STOP_LOSS.value
|
||||||
|
|
||||||
def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee,
|
|
||||||
mocker, edge_conf) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
patch_edge(mocker)
|
|
||||||
edge_conf['max_open_trades'] = float('inf')
|
|
||||||
|
|
||||||
# Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2
|
|
||||||
# Thus, if price falls 15%, stoploss should not be triggered
|
|
||||||
#
|
|
||||||
# mocking the ticker: price is falling ...
|
|
||||||
buy_price = limit_buy_order['price']
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=MagicMock(return_value={
|
|
||||||
'bid': buy_price * 0.85,
|
|
||||||
'ask': buy_price * 0.85,
|
|
||||||
'last': buy_price * 0.85
|
|
||||||
}),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
#############################################
|
|
||||||
|
|
||||||
# Create a trade with "limit_buy_order" price
|
|
||||||
freqtrade = FreqtradeBot(edge_conf)
|
|
||||||
freqtrade.active_pair_whitelist = ['NEO/BTC']
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
|
||||||
freqtrade.enter_positions()
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
#############################################
|
|
||||||
|
|
||||||
# stoploss shoud not be hit
|
|
||||||
assert freqtrade.handle_trade(trade) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None:
|
def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None:
|
||||||
@ -376,8 +319,16 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order,
|
|||||||
freqtrade.create_trade('ETH/BTC')
|
freqtrade.create_trade('ETH/BTC')
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open,
|
@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [
|
||||||
fee, mocker) -> None:
|
(0.0005, True, True, 99),
|
||||||
|
(0.000000005, True, False, 99),
|
||||||
|
(0, False, True, 99),
|
||||||
|
(UNLIMITED_STAKE_AMOUNT, False, True, 0),
|
||||||
|
])
|
||||||
|
def test_create_trade_minimal_amount(
|
||||||
|
default_conf, ticker, limit_buy_order_open, fee, mocker,
|
||||||
|
stake_amount, create, amount_enough, max_open_trades, caplog
|
||||||
|
) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
buy_mock = MagicMock(return_value=limit_buy_order_open)
|
buy_mock = MagicMock(return_value=limit_buy_order_open)
|
||||||
@ -387,78 +338,33 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open,
|
|||||||
create_order=buy_mock,
|
create_order=buy_mock,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
default_conf['stake_amount'] = 0.0005
|
default_conf['max_open_trades'] = max_open_trades
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
freqtrade.config['stake_amount'] = stake_amount
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
freqtrade.create_trade('ETH/BTC')
|
if create:
|
||||||
rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount']
|
assert freqtrade.create_trade('ETH/BTC')
|
||||||
assert rate * amount <= default_conf['stake_amount']
|
if amount_enough:
|
||||||
|
rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount']
|
||||||
|
assert rate * amount <= default_conf['stake_amount']
|
||||||
def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open,
|
else:
|
||||||
fee, mocker, caplog) -> None:
|
assert log_has_re(
|
||||||
patch_RPCManager(mocker)
|
r"Stake amount for pair .* is too small.*",
|
||||||
patch_exchange(mocker)
|
caplog
|
||||||
buy_mock = MagicMock(return_value=limit_buy_order_open)
|
)
|
||||||
mocker.patch.multiple(
|
else:
|
||||||
'freqtrade.exchange.Exchange',
|
assert not freqtrade.create_trade('ETH/BTC')
|
||||||
fetch_ticker=ticker,
|
if not max_open_trades:
|
||||||
create_order=buy_mock,
|
assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
freqtrade.config['stake_amount'] = 0.000000005
|
|
||||||
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
|
|
||||||
assert freqtrade.create_trade('ETH/BTC')
|
|
||||||
assert log_has_re(r"Stake amount for pair .* is too small.*", caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_open,
|
|
||||||
fee, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
buy_mock = MagicMock(return_value=limit_buy_order_open)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker,
|
|
||||||
create_order=buy_mock,
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
freqtrade.config['stake_amount'] = 0
|
|
||||||
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
|
|
||||||
assert not freqtrade.create_trade('ETH/BTC')
|
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open,
|
|
||||||
fee, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker,
|
|
||||||
create_order=MagicMock(return_value=limit_buy_order_open),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
default_conf['max_open_trades'] = 0
|
|
||||||
default_conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
|
|
||||||
assert not freqtrade.create_trade('ETH/BTC')
|
|
||||||
assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0
|
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('whitelist,positions', [
|
||||||
|
(["ETH/BTC"], 1), # No pairs left
|
||||||
|
([], 0), # No pairs in whitelist
|
||||||
|
])
|
||||||
def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee,
|
def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee,
|
||||||
mocker, caplog) -> None:
|
whitelist, positions, mocker, caplog) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -467,36 +373,20 @@ def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_ope
|
|||||||
create_order=MagicMock(return_value=limit_buy_order_open),
|
create_order=MagicMock(return_value=limit_buy_order_open),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
default_conf['exchange']['pair_whitelist'] = whitelist
|
||||||
default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
n = freqtrade.enter_positions()
|
n = freqtrade.enter_positions()
|
||||||
assert n == 1
|
assert n == positions
|
||||||
assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog)
|
if positions:
|
||||||
n = freqtrade.enter_positions()
|
assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog)
|
||||||
assert n == 0
|
n = freqtrade.enter_positions()
|
||||||
assert log_has_re(r"No currency pair in active pair whitelist.*", caplog)
|
assert n == 0
|
||||||
|
assert log_has_re(r"No currency pair in active pair whitelist.*", caplog)
|
||||||
|
else:
|
||||||
def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee,
|
assert n == 0
|
||||||
mocker, caplog) -> None:
|
assert log_has("Active pair whitelist is empty.", caplog)
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=ticker,
|
|
||||||
create_order=MagicMock(return_value={'id': limit_buy_order['id']}),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
default_conf['exchange']['pair_whitelist'] = []
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
|
|
||||||
n = freqtrade.enter_positions()
|
|
||||||
assert n == 0
|
|
||||||
assert log_has("Active pair whitelist is empty.", caplog)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@ -1253,6 +1143,7 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog,
|
|||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
|
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
|
||||||
limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
|
# TODO-lev: test for short
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
stoploss = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': 13434334})
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
@ -1344,10 +1235,14 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
|
|||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
|
||||||
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
|
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
|
||||||
stoploss_order_mock.assert_called_once_with(amount=85.32423208,
|
stoploss_order_mock.assert_called_once_with(
|
||||||
pair='ETH/BTC',
|
amount=85.32423208,
|
||||||
order_types=freqtrade.strategy.order_types,
|
pair='ETH/BTC',
|
||||||
stop_price=0.00002346 * 0.95)
|
order_types=freqtrade.strategy.order_types,
|
||||||
|
stop_price=0.00002346 * 0.95,
|
||||||
|
side="sell",
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
# price fell below stoploss, so dry-run sells trade.
|
# price fell below stoploss, so dry-run sells trade.
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
|
||||||
@ -1360,6 +1255,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
|
|||||||
|
|
||||||
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
||||||
limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
|
# TODO-lev: test for short
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
stoploss = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': 13434334})
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -1418,7 +1314,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
|||||||
side_effect=InvalidOrderException())
|
side_effect=InvalidOrderException())
|
||||||
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
|
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
|
||||||
return_value=stoploss_order_hanging)
|
return_value=stoploss_order_hanging)
|
||||||
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell")
|
||||||
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
|
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
|
||||||
|
|
||||||
# Still try to create order
|
# Still try to create order
|
||||||
@ -1428,7 +1324,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
|||||||
caplog.clear()
|
caplog.clear()
|
||||||
cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock())
|
cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock())
|
||||||
mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError())
|
mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError())
|
||||||
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell")
|
||||||
assert cancel_mock.call_count == 1
|
assert cancel_mock.call_count == 1
|
||||||
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog)
|
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog)
|
||||||
|
|
||||||
@ -1437,6 +1333,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
|||||||
def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee,
|
def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee,
|
||||||
limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
|
# TODO-lev: test for short
|
||||||
stoploss = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': 13434334})
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -1527,10 +1424,14 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee,
|
|||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
|
||||||
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
|
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
|
||||||
stoploss_order_mock.assert_called_once_with(amount=85.32423208,
|
stoploss_order_mock.assert_called_once_with(
|
||||||
pair='ETH/BTC',
|
amount=85.32423208,
|
||||||
order_types=freqtrade.strategy.order_types,
|
pair='ETH/BTC',
|
||||||
stop_price=0.00002346 * 0.96)
|
order_types=freqtrade.strategy.order_types,
|
||||||
|
stop_price=0.00002346 * 0.96,
|
||||||
|
side="sell",
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
# price fell below stoploss, so dry-run sells trade.
|
# price fell below stoploss, so dry-run sells trade.
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
|
||||||
@ -1543,7 +1444,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee,
|
|||||||
|
|
||||||
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||||
limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
|
# TODO-lev: test for short
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
stoploss = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': 13434334})
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
@ -1648,36 +1549,37 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
|||||||
# stoploss should be set to 1% as trailing is on
|
# stoploss should be set to 1% as trailing is on
|
||||||
assert trade.stop_loss == 0.00002346 * 0.99
|
assert trade.stop_loss == 0.00002346 * 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(amount=2132892.49146757,
|
stoploss_order_mock.assert_called_once_with(
|
||||||
pair='NEO/BTC',
|
amount=2132892.49146757,
|
||||||
order_types=freqtrade.strategy.order_types,
|
pair='NEO/BTC',
|
||||||
stop_price=0.00002346 * 0.99)
|
order_types=freqtrade.strategy.order_types,
|
||||||
|
stop_price=0.00002346 * 0.99,
|
||||||
|
side="sell",
|
||||||
|
leverage=1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_enter_positions(mocker, default_conf, caplog) -> None:
|
@pytest.mark.parametrize('return_value,side_effect,log_message', [
|
||||||
|
(False, None, 'Found no enter signals for whitelisted currencies. Trying again...'),
|
||||||
|
(None, DependencyException, 'Unable to create trade for ETH/BTC: ')
|
||||||
|
])
|
||||||
|
def test_enter_positions(mocker, default_conf, return_value, side_effect,
|
||||||
|
log_message, caplog) -> None:
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
mock_ct = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade',
|
|
||||||
MagicMock(return_value=False))
|
|
||||||
n = freqtrade.enter_positions()
|
|
||||||
assert n == 0
|
|
||||||
assert log_has('Found no enter signals for whitelisted currencies. Trying again...', caplog)
|
|
||||||
# create_trade should be called once for every pair in the whitelist.
|
|
||||||
assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist'])
|
|
||||||
|
|
||||||
|
|
||||||
def test_enter_positions_exception(mocker, default_conf, caplog) -> None:
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
|
|
||||||
mock_ct = mocker.patch(
|
mock_ct = mocker.patch(
|
||||||
'freqtrade.freqtradebot.FreqtradeBot.create_trade',
|
'freqtrade.freqtradebot.FreqtradeBot.create_trade',
|
||||||
MagicMock(side_effect=DependencyException)
|
MagicMock(
|
||||||
|
return_value=return_value,
|
||||||
|
side_effect=side_effect
|
||||||
|
)
|
||||||
)
|
)
|
||||||
n = freqtrade.enter_positions()
|
n = freqtrade.enter_positions()
|
||||||
assert n == 0
|
assert n == 0
|
||||||
|
assert log_has(log_message, caplog)
|
||||||
|
# create_trade should be called once for every pair in the whitelist.
|
||||||
assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist'])
|
assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist'])
|
||||||
assert log_has('Unable to create trade for ETH/BTC: ', caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None:
|
def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None:
|
||||||
@ -1771,8 +1673,13 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No
|
|||||||
assert log_has_re('Found open order for.*', caplog)
|
assert log_has_re('Found open order for.*', caplog)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('initial_amount,has_rounding_fee', [
|
||||||
|
(90.99181073 + 1e-14, True),
|
||||||
|
(8.0, False)
|
||||||
|
])
|
||||||
def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee,
|
def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee,
|
||||||
mocker):
|
mocker, initial_amount, has_rounding_fee, caplog):
|
||||||
|
trades_for_order[0]['amount'] = initial_amount
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||||
# fetch_order should not be called!!
|
# fetch_order should not be called!!
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
|
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
|
||||||
@ -1793,32 +1700,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_
|
|||||||
freqtrade.update_trade_state(trade, '123456', limit_buy_order)
|
freqtrade.update_trade_state(trade, '123456', limit_buy_order)
|
||||||
assert trade.amount != amount
|
assert trade.amount != amount
|
||||||
assert trade.amount == limit_buy_order['amount']
|
assert trade.amount == limit_buy_order['amount']
|
||||||
|
if has_rounding_fee:
|
||||||
|
assert log_has_re(r'Applying fee on amount for .*', caplog)
|
||||||
def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, fee,
|
|
||||||
limit_buy_order, mocker, caplog):
|
|
||||||
trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
|
||||||
# fetch_order should not be called!!
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
|
|
||||||
patch_exchange(mocker)
|
|
||||||
amount = sum(x['amount'] for x in trades_for_order)
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
|
||||||
trade = Trade(
|
|
||||||
pair='LTC/ETH',
|
|
||||||
amount=amount,
|
|
||||||
exchange='binance',
|
|
||||||
open_rate=0.245441,
|
|
||||||
fee_open=fee.return_value,
|
|
||||||
fee_close=fee.return_value,
|
|
||||||
open_order_id='123456',
|
|
||||||
is_open=True,
|
|
||||||
open_date=arrow.utcnow().datetime,
|
|
||||||
)
|
|
||||||
freqtrade.update_trade_state(trade, '123456', limit_buy_order)
|
|
||||||
assert trade.amount != amount
|
|
||||||
assert trade.amount == limit_buy_order['amount']
|
|
||||||
assert log_has_re(r'Applying fee on amount for .*', caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_update_trade_state_exception(mocker, default_conf,
|
def test_update_trade_state_exception(mocker, default_conf,
|
||||||
@ -3130,16 +3013,28 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee,
|
|||||||
assert mock_insuf.call_count == 1
|
assert mock_insuf.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open,
|
@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [
|
||||||
fee, mocker) -> None:
|
# Enable profit
|
||||||
|
(True, 0.00001172, 0.00001173, False, True, SellType.SELL_SIGNAL.value),
|
||||||
|
# Disable profit
|
||||||
|
(False, 0.00002172, 0.00002173, True, False, SellType.SELL_SIGNAL.value),
|
||||||
|
# Enable loss
|
||||||
|
# * Shouldn't this be SellType.STOP_LOSS.value
|
||||||
|
(True, 0.00000172, 0.00000173, False, False, None),
|
||||||
|
# Disable loss
|
||||||
|
(False, 0.00000172, 0.00000173, True, False, SellType.SELL_SIGNAL.value),
|
||||||
|
])
|
||||||
|
def test_sell_profit_only(
|
||||||
|
default_conf, limit_buy_order, limit_buy_order_open,
|
||||||
|
fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=MagicMock(return_value={
|
fetch_ticker=MagicMock(return_value={
|
||||||
'bid': 0.00001172,
|
'bid': bid,
|
||||||
'ask': 0.00001173,
|
'ask': ask,
|
||||||
'last': 0.00001172
|
'last': bid
|
||||||
}),
|
}),
|
||||||
create_order=MagicMock(side_effect=[
|
create_order=MagicMock(side_effect=[
|
||||||
limit_buy_order_open,
|
limit_buy_order_open,
|
||||||
@ -3149,128 +3044,29 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy
|
|||||||
)
|
)
|
||||||
default_conf.update({
|
default_conf.update({
|
||||||
'use_sell_signal': True,
|
'use_sell_signal': True,
|
||||||
'sell_profit_only': True,
|
'sell_profit_only': profit_only,
|
||||||
'sell_profit_offset': 0.1,
|
'sell_profit_offset': 0.1,
|
||||||
})
|
})
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
if sell_type == SellType.SELL_SIGNAL.value:
|
||||||
|
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
||||||
|
else:
|
||||||
|
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple(
|
||||||
|
sell_type=SellType.NONE))
|
||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
|
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
trade.update(limit_buy_order)
|
trade.update(limit_buy_order)
|
||||||
freqtrade.wallets.update()
|
freqtrade.wallets.update()
|
||||||
patch_get_signal(freqtrade, enter_long=False, exit_long=True)
|
patch_get_signal(freqtrade, enter_long=False, exit_long=True)
|
||||||
assert freqtrade.handle_trade(trade) is False
|
assert freqtrade.handle_trade(trade) is handle_first
|
||||||
|
|
||||||
freqtrade.strategy.sell_profit_offset = 0.0
|
if handle_second:
|
||||||
assert freqtrade.handle_trade(trade) is True
|
freqtrade.strategy.sell_profit_offset = 0.0
|
||||||
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
|
||||||
assert trade.sell_reason == SellType.SELL_SIGNAL.value
|
assert trade.sell_reason == sell_type
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open,
|
|
||||||
fee, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00002172,
|
|
||||||
'ask': 0.00002173,
|
|
||||||
'last': 0.00002172
|
|
||||||
}),
|
|
||||||
create_order=MagicMock(side_effect=[
|
|
||||||
limit_buy_order_open,
|
|
||||||
{'id': 1234553382},
|
|
||||||
]),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
default_conf.update({
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': False,
|
|
||||||
})
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
|
||||||
freqtrade.enter_positions()
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
freqtrade.wallets.update()
|
|
||||||
patch_get_signal(freqtrade, enter_long=False, exit_long=True)
|
|
||||||
assert freqtrade.handle_trade(trade) is True
|
|
||||||
assert trade.sell_reason == SellType.SELL_SIGNAL.value
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open,
|
|
||||||
fee, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.00000172,
|
|
||||||
'ask': 0.00000173,
|
|
||||||
'last': 0.00000172
|
|
||||||
}),
|
|
||||||
create_order=MagicMock(side_effect=[
|
|
||||||
limit_buy_order_open,
|
|
||||||
{'id': 1234553382},
|
|
||||||
]),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
default_conf.update({
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': True,
|
|
||||||
})
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple(
|
|
||||||
sell_type=SellType.NONE))
|
|
||||||
freqtrade.enter_positions()
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
patch_get_signal(freqtrade, enter_long=False, exit_long=True)
|
|
||||||
assert freqtrade.handle_trade(trade) is False
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open,
|
|
||||||
fee, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_ticker=MagicMock(return_value={
|
|
||||||
'bid': 0.0000172,
|
|
||||||
'ask': 0.0000173,
|
|
||||||
'last': 0.0000172
|
|
||||||
}),
|
|
||||||
create_order=MagicMock(side_effect=[
|
|
||||||
limit_buy_order_open,
|
|
||||||
{'id': 1234553382},
|
|
||||||
]),
|
|
||||||
get_fee=fee,
|
|
||||||
)
|
|
||||||
default_conf.update({
|
|
||||||
'use_sell_signal': True,
|
|
||||||
'sell_profit_only': False,
|
|
||||||
})
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
|
|
||||||
|
|
||||||
freqtrade.enter_positions()
|
|
||||||
|
|
||||||
trade = Trade.query.first()
|
|
||||||
trade.update(limit_buy_order)
|
|
||||||
freqtrade.wallets.update()
|
|
||||||
patch_get_signal(freqtrade, enter_long=False, exit_long=True)
|
|
||||||
assert freqtrade.handle_trade(trade) is True
|
|
||||||
assert trade.sell_reason == SellType.SELL_SIGNAL.value
|
|
||||||
|
|
||||||
|
|
||||||
def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open,
|
def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open,
|
||||||
@ -3308,11 +3104,15 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_
|
|||||||
assert trade.amount != amnt
|
assert trade.amount != amnt
|
||||||
|
|
||||||
|
|
||||||
def test__safe_exit_amount(default_conf, fee, caplog, mocker):
|
@pytest.mark.parametrize('amount_wallet,has_err', [
|
||||||
|
(95.29, False),
|
||||||
|
(91.29, True)
|
||||||
|
])
|
||||||
|
def test__safe_exit_amount(default_conf, fee, caplog, mocker, amount_wallet, has_err):
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
amount = 95.33
|
amount = 95.33
|
||||||
amount_wallet = 95.29
|
amount_wallet = amount_wallet
|
||||||
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet))
|
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet))
|
||||||
wallet_update = mocker.patch('freqtrade.wallets.Wallets.update')
|
wallet_update = mocker.patch('freqtrade.wallets.Wallets.update')
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
@ -3326,37 +3126,19 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker):
|
|||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
if has_err:
|
||||||
wallet_update.reset_mock()
|
with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."):
|
||||||
assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet
|
assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
|
||||||
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
else:
|
||||||
assert wallet_update.call_count == 1
|
wallet_update.reset_mock()
|
||||||
caplog.clear()
|
assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet
|
||||||
wallet_update.reset_mock()
|
assert log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||||
assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet
|
assert wallet_update.call_count == 1
|
||||||
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
caplog.clear()
|
||||||
assert wallet_update.call_count == 1
|
wallet_update.reset_mock()
|
||||||
|
assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet
|
||||||
|
assert not log_has_re(r'.*Falling back to wallet-amount.', caplog)
|
||||||
def test__safe_exit_amount_error(default_conf, fee, caplog, mocker):
|
assert wallet_update.call_count == 1
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
amount = 95.33
|
|
||||||
amount_wallet = 91.29
|
|
||||||
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet))
|
|
||||||
trade = Trade(
|
|
||||||
pair='LTC/ETH',
|
|
||||||
amount=amount,
|
|
||||||
exchange='binance',
|
|
||||||
open_rate=0.245441,
|
|
||||||
open_order_id="123456",
|
|
||||||
fee_open=fee.return_value,
|
|
||||||
fee_close=fee.return_value,
|
|
||||||
)
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
with pytest.raises(DependencyException, match=r"Not enough amount to exit."):
|
|
||||||
assert freqtrade._safe_exit_amount(trade.pair, trade.amount)
|
|
||||||
|
|
||||||
|
|
||||||
def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None:
|
def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None:
|
||||||
@ -4144,50 +3926,37 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o
|
|||||||
assert trade is None
|
assert trade is None
|
||||||
|
|
||||||
|
|
||||||
def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None:
|
@pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [
|
||||||
|
(False, 0.045, 0.046, 2, None),
|
||||||
|
(True, 0.042, 0.046, 1, {'bids': [[]], 'asks': [[]]})
|
||||||
|
])
|
||||||
|
def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, exception_thrown,
|
||||||
|
ask, last, order_book_top, order_book, caplog) -> None:
|
||||||
"""
|
"""
|
||||||
test if function get_rate will return the order book price
|
test if function get_rate will return the order book price instead of the ask rate
|
||||||
instead of the ask rate
|
|
||||||
"""
|
"""
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046})
|
ticker_mock = MagicMock(return_value={'ask': ask, 'last': last})
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_l2_order_book=order_book_l2,
|
fetch_l2_order_book=MagicMock(return_value=order_book) if order_book else order_book_l2,
|
||||||
fetch_ticker=ticker_mock,
|
fetch_ticker=ticker_mock,
|
||||||
|
|
||||||
)
|
)
|
||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
default_conf['bid_strategy']['use_order_book'] = True
|
default_conf['bid_strategy']['use_order_book'] = True
|
||||||
default_conf['bid_strategy']['order_book_top'] = 2
|
default_conf['bid_strategy']['order_book_top'] = order_book_top
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 0
|
default_conf['bid_strategy']['ask_last_balance'] = 0
|
||||||
default_conf['telegram']['enabled'] = False
|
default_conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935
|
if exception_thrown:
|
||||||
assert ticker_mock.call_count == 0
|
with pytest.raises(PricingError):
|
||||||
|
freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy")
|
||||||
|
assert log_has_re(
|
||||||
def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None:
|
r'Buy Price at location 1 from orderbook could not be determined.', caplog)
|
||||||
patch_exchange(mocker)
|
else:
|
||||||
ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046})
|
assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935
|
||||||
mocker.patch.multiple(
|
assert ticker_mock.call_count == 0
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
fetch_l2_order_book=MagicMock(return_value={'bids': [[]], 'asks': [[]]}),
|
|
||||||
fetch_ticker=ticker_mock,
|
|
||||||
|
|
||||||
)
|
|
||||||
default_conf['exchange']['name'] = 'binance'
|
|
||||||
default_conf['bid_strategy']['use_order_book'] = True
|
|
||||||
default_conf['bid_strategy']['order_book_top'] = 1
|
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 0
|
|
||||||
default_conf['telegram']['enabled'] = False
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
# orderbook shall be used even if tickers would be lower.
|
|
||||||
with pytest.raises(PricingError):
|
|
||||||
freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy")
|
|
||||||
assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog)
|
|
||||||
|
|
||||||
|
|
||||||
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None:
|
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None:
|
||||||
|
Loading…
Reference in New Issue
Block a user