Merge branch 'develop' into pr/mkavinkumar1/6545
This commit is contained in:
commit
2cacb3767f
@ -15,9 +15,9 @@ repos:
|
|||||||
additional_dependencies:
|
additional_dependencies:
|
||||||
- types-cachetools==5.2.1
|
- types-cachetools==5.2.1
|
||||||
- types-filelock==3.2.7
|
- types-filelock==3.2.7
|
||||||
- types-requests==2.28.1
|
- types-requests==2.28.3
|
||||||
- types-tabulate==0.8.11
|
- types-tabulate==0.8.11
|
||||||
- types-python-dateutil==2.8.18
|
- types-python-dateutil==2.8.19
|
||||||
# stages: [push]
|
# stages: [push]
|
||||||
|
|
||||||
- repo: https://github.com/pycqa/isort
|
- repo: https://github.com/pycqa/isort
|
||||||
|
@ -193,7 +193,7 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/
|
|||||||
|
|
||||||
The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges.
|
The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges.
|
||||||
|
|
||||||
### Min hardware required
|
### Minimum hardware required
|
||||||
|
|
||||||
To run this bot we recommend you a cloud instance with a minimum of:
|
To run this bot we recommend you a cloud instance with a minimum of:
|
||||||
|
|
||||||
|
@ -50,6 +50,8 @@ This applies across all pairs, unless `only_per_pair` is set to true, which will
|
|||||||
|
|
||||||
Similarly, this protection will by default look at all trades (long and short). For futures bots, setting `only_per_side` will make the bot only consider one side, and will then only lock this one side, allowing for example shorts to continue after a series of long stoplosses.
|
Similarly, this protection will by default look at all trades (long and short). For futures bots, setting `only_per_side` will make the bot only consider one side, and will then only lock this one side, allowing for example shorts to continue after a series of long stoplosses.
|
||||||
|
|
||||||
|
`required_profit` will determine the required relative profit (or loss) for stoplosses to consider. This should normally not be set and defaults to 0.0 - which means all losing stoplosses will be triggering a block.
|
||||||
|
|
||||||
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.
|
The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles.
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
@ -61,6 +63,7 @@ def protections(self):
|
|||||||
"lookback_period_candles": 24,
|
"lookback_period_candles": 24,
|
||||||
"trade_limit": 4,
|
"trade_limit": 4,
|
||||||
"stop_duration_candles": 4,
|
"stop_duration_candles": 4,
|
||||||
|
"required_profit": 0.0,
|
||||||
"only_per_pair": False,
|
"only_per_pair": False,
|
||||||
"only_per_side": False
|
"only_per_side": False
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
markdown==3.4.1
|
markdown==3.3.7
|
||||||
mkdocs==1.3.0
|
mkdocs==1.3.1
|
||||||
mkdocs-material==8.3.9
|
mkdocs-material==8.3.9
|
||||||
mdx_truly_sane_lists==1.3
|
mdx_truly_sane_lists==1.3
|
||||||
pymdown-extensions==9.5
|
pymdown-extensions==9.5
|
||||||
|
@ -623,6 +623,7 @@ class AwesomeStrategy(IStrategy):
|
|||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
`confirm_trade_exit()` can prevent stoploss exits, causing significant losses as this would ignore stoploss exits.
|
`confirm_trade_exit()` can prevent stoploss exits, causing significant losses as this would ignore stoploss exits.
|
||||||
|
`confirm_trade_exit()` will not be called for Liquidations - as liquidations are forced by the exchange, and therefore cannot be rejected.
|
||||||
|
|
||||||
## Adjust trade position
|
## Adjust trade position
|
||||||
|
|
||||||
|
@ -646,6 +646,9 @@ This is where calling `self.dp.current_whitelist()` comes in handy.
|
|||||||
return informative_pairs
|
return informative_pairs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
??? Note "Plotting with current_whitelist"
|
||||||
|
Current whitelist is not supported for `plot-dataframe`, as this command is usually used by providing an explicit pairlist - and would therefore make the return values of this method misleading.
|
||||||
|
|
||||||
### *get_pair_dataframe(pair, timeframe)*
|
### *get_pair_dataframe(pair, timeframe)*
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
@ -731,6 +734,23 @@ if self.dp:
|
|||||||
!!! Warning "Warning about backtesting"
|
!!! Warning "Warning about backtesting"
|
||||||
This method will always return up-to-date values - so usage during backtesting / hyperopt will lead to wrong results.
|
This method will always return up-to-date values - so usage during backtesting / hyperopt will lead to wrong results.
|
||||||
|
|
||||||
|
### Send Notification
|
||||||
|
|
||||||
|
The dataprovider `.send_msg()` function allows you to send custom notifications from your strategy.
|
||||||
|
Identical notifications will only be sent once per candle, unless the 2nd argument (`always_send`) is set to True.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
self.dp.send_msg(f"{metadata['pair']} just got hot!")
|
||||||
|
|
||||||
|
# Force send this notification, avoid caching (Please read warning below!)
|
||||||
|
self.dp.send_msg(f"{metadata['pair']} just got hot!", always_send=True)
|
||||||
|
```
|
||||||
|
|
||||||
|
Notifications will only be sent in trading modes (Live/Dry-run) - so this method can be called without conditions for backtesting.
|
||||||
|
|
||||||
|
!!! Warning "Spamming"
|
||||||
|
You can spam yourself pretty good by setting `always_send=True` in this method. Use this with great care and only in conditions you know will not happen throughout a candle to avoid a message every 5 seconds.
|
||||||
|
|
||||||
### Complete Data-provider sample
|
### Complete Data-provider sample
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
@ -98,6 +98,7 @@ Example configuration showing the different settings:
|
|||||||
"exit_fill": "off",
|
"exit_fill": "off",
|
||||||
"protection_trigger": "off",
|
"protection_trigger": "off",
|
||||||
"protection_trigger_global": "on",
|
"protection_trigger_global": "on",
|
||||||
|
"strategy_msg": "off",
|
||||||
"show_candle": "off"
|
"show_candle": "off"
|
||||||
},
|
},
|
||||||
"reload": true,
|
"reload": true,
|
||||||
@ -109,7 +110,8 @@ Example configuration showing the different settings:
|
|||||||
`exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
|
`exit` notifications are sent when the order is placed, while `exit_fill` notifications are sent when the order is filled on the exchange.
|
||||||
`*_fill` notifications are off by default and must be explicitly enabled.
|
`*_fill` notifications are off by default and must be explicitly enabled.
|
||||||
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
|
`protection_trigger` notifications are sent when a protection triggers and `protection_trigger_global` notifications trigger when global protections are triggered.
|
||||||
`show_candle` - show candle values as part of entry/exit messages. Only possible value is "ohlc".
|
`strategy_msg` - Receive notifications from the strategy, sent via `self.dp.send_msg()` from the strategy [more details](strategy-customization.md#send-notification).
|
||||||
|
`show_candle` - show candle values as part of entry/exit messages. Only possible values are `"ohlc"` or `"off"`.
|
||||||
|
|
||||||
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
|
||||||
`reload` allows you to disable reload-buttons on selected messages.
|
`reload` allows you to disable reload-buttons on selected messages.
|
||||||
|
@ -317,6 +317,10 @@ CONF_SCHEMA = {
|
|||||||
'type': 'string',
|
'type': 'string',
|
||||||
'enum': ['off', 'ohlc'],
|
'enum': ['off', 'ohlc'],
|
||||||
},
|
},
|
||||||
|
'strategy_msg': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': TELEGRAM_SETTING_OPTIONS,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'reload': {'type': 'boolean'},
|
'reload': {'type': 'boolean'},
|
||||||
|
@ -5,12 +5,14 @@ including ticker and orderbook data, live and historical candle (OHLCV) data
|
|||||||
Common Interface for bot and strategy to access data.
|
Common Interface for bot and strategy to access data.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from collections import deque
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.configuration import TimeRange
|
from freqtrade.configuration import TimeRange
|
||||||
|
from freqtrade.configuration.PeriodicCache import PeriodicCache
|
||||||
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
|
||||||
from freqtrade.data.history import load_pair_history
|
from freqtrade.data.history import load_pair_history
|
||||||
from freqtrade.enums import CandleType, RunMode
|
from freqtrade.enums import CandleType, RunMode
|
||||||
@ -33,6 +35,10 @@ class DataProvider:
|
|||||||
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {}
|
||||||
self.__slice_index: Optional[int] = None
|
self.__slice_index: Optional[int] = None
|
||||||
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {}
|
||||||
|
self._msg_queue: deque = deque()
|
||||||
|
|
||||||
|
self.__msg_cache = PeriodicCache(
|
||||||
|
maxsize=1000, ttl=timeframe_to_seconds(self._config.get('timeframe', '1h')))
|
||||||
|
|
||||||
def _set_dataframe_max_index(self, limit_index: int):
|
def _set_dataframe_max_index(self, limit_index: int):
|
||||||
"""
|
"""
|
||||||
@ -265,3 +271,20 @@ class DataProvider:
|
|||||||
if self._exchange is None:
|
if self._exchange is None:
|
||||||
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
raise OperationalException(NO_EXCHANGE_EXCEPTION)
|
||||||
return self._exchange.fetch_l2_order_book(pair, maximum)
|
return self._exchange.fetch_l2_order_book(pair, maximum)
|
||||||
|
|
||||||
|
def send_msg(self, message: str, *, always_send: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
Send custom RPC Notifications from your bot.
|
||||||
|
Will not send any bot in modes other than Dry-run or Live.
|
||||||
|
:param message: Message to be sent. Must be below 4096.
|
||||||
|
:param always_send: If False, will send the message only once per candle, and surpress
|
||||||
|
identical messages.
|
||||||
|
Careful as this can end up spaming your chat.
|
||||||
|
Defaults to False
|
||||||
|
"""
|
||||||
|
if self.runmode not in (RunMode.DRY_RUN, RunMode.LIVE):
|
||||||
|
return
|
||||||
|
|
||||||
|
if always_send or message not in self.__msg_cache:
|
||||||
|
self._msg_queue.append(message)
|
||||||
|
self.__msg_cache[message] = True
|
||||||
|
@ -9,6 +9,7 @@ class ExitType(Enum):
|
|||||||
STOP_LOSS = "stop_loss"
|
STOP_LOSS = "stop_loss"
|
||||||
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
STOPLOSS_ON_EXCHANGE = "stoploss_on_exchange"
|
||||||
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
TRAILING_STOP_LOSS = "trailing_stop_loss"
|
||||||
|
LIQUIDATION = "liquidation"
|
||||||
EXIT_SIGNAL = "exit_signal"
|
EXIT_SIGNAL = "exit_signal"
|
||||||
FORCE_EXIT = "force_exit"
|
FORCE_EXIT = "force_exit"
|
||||||
EMERGENCY_EXIT = "emergency_exit"
|
EMERGENCY_EXIT = "emergency_exit"
|
||||||
|
@ -17,6 +17,8 @@ class RPCMessageType(Enum):
|
|||||||
PROTECTION_TRIGGER = 'protection_trigger'
|
PROTECTION_TRIGGER = 'protection_trigger'
|
||||||
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
||||||
|
|
||||||
|
STRATEGY_MSG = 'strategy_msg'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
@ -1264,7 +1264,7 @@ class Exchange:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
required = ('fee', 'status', 'amount')
|
required = ('fee', 'status', 'amount')
|
||||||
return all(k in corder for k in required)
|
return all(corder.get(k, None) is not None for k in required)
|
||||||
|
|
||||||
def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
|
def cancel_order_with_result(self, order_id: str, pair: str, amount: float) -> Dict:
|
||||||
"""
|
"""
|
||||||
@ -1332,11 +1332,19 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def fetch_positions(self) -> List[Dict]:
|
def fetch_positions(self, pair: str = None) -> List[Dict]:
|
||||||
|
"""
|
||||||
|
Fetch positions from the exchange.
|
||||||
|
If no pair is given, all positions are returned.
|
||||||
|
:param pair: Pair for the query
|
||||||
|
"""
|
||||||
if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES:
|
if self._config['dry_run'] or self.trading_mode != TradingMode.FUTURES:
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
positions: List[Dict] = self._api.fetch_positions()
|
symbols = []
|
||||||
|
if pair:
|
||||||
|
symbols.append(pair)
|
||||||
|
positions: List[Dict] = self._api.fetch_positions(symbols)
|
||||||
self._log_exchange_response('fetch_positions', positions)
|
self._log_exchange_response('fetch_positions', positions)
|
||||||
return positions
|
return positions
|
||||||
except ccxt.DDoSProtection as e:
|
except ccxt.DDoSProtection as e:
|
||||||
@ -2569,7 +2577,6 @@ class Exchange:
|
|||||||
else:
|
else:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
@retrier
|
|
||||||
def get_or_calculate_liquidation_price(
|
def get_or_calculate_liquidation_price(
|
||||||
self,
|
self,
|
||||||
pair: str,
|
pair: str,
|
||||||
@ -2603,20 +2610,12 @@ class Exchange:
|
|||||||
upnl_ex_1=upnl_ex_1
|
upnl_ex_1=upnl_ex_1
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
try:
|
positions = self.fetch_positions(pair)
|
||||||
positions = self._api.fetch_positions([pair])
|
if len(positions) > 0:
|
||||||
if len(positions) > 0:
|
pos = positions[0]
|
||||||
pos = positions[0]
|
isolated_liq = pos['liquidationPrice']
|
||||||
isolated_liq = pos['liquidationPrice']
|
else:
|
||||||
else:
|
return None
|
||||||
return None
|
|
||||||
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
|
|
||||||
|
|
||||||
if isolated_liq:
|
if isolated_liq:
|
||||||
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
|
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer
|
||||||
|
@ -215,6 +215,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
self._schedule.run_pending()
|
self._schedule.run_pending()
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
self.rpc.process_msg_queue(self.dataprovider._msg_queue)
|
||||||
self.last_process = datetime.now(timezone.utc)
|
self.last_process = datetime.now(timezone.utc)
|
||||||
|
|
||||||
def process_stopped(self) -> None:
|
def process_stopped(self) -> None:
|
||||||
@ -1042,7 +1043,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.stoploss_order_id = None
|
trade.stoploss_order_id = None
|
||||||
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
logger.error(f'Unable to place a stoploss order on exchange. {e}')
|
||||||
logger.warning('Exiting the trade forcefully')
|
logger.warning('Exiting the trade forcefully')
|
||||||
self.execute_trade_exit(trade, trade.stop_loss, exit_check=ExitCheckTuple(
|
self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple(
|
||||||
exit_type=ExitType.EMERGENCY_EXIT))
|
exit_type=ExitType.EMERGENCY_EXIT))
|
||||||
|
|
||||||
except ExchangeError:
|
except ExchangeError:
|
||||||
@ -1112,7 +1113,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if (trade.is_open
|
if (trade.is_open
|
||||||
and stoploss_order
|
and stoploss_order
|
||||||
and stoploss_order['status'] in ('canceled', 'cancelled')):
|
and stoploss_order['status'] in ('canceled', 'cancelled')):
|
||||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
|
if self.create_stoploss_order(trade=trade, stop_price=trade.stoploss_or_liquidation):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
trade.stoploss_order_id = None
|
trade.stoploss_order_id = None
|
||||||
@ -1141,7 +1142,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:param order: Current on exchange stoploss order
|
:param order: Current on exchange stoploss order
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stop_loss)
|
stoploss_norm = self.exchange.price_to_precision(trade.pair, trade.stoploss_or_liquidation)
|
||||||
|
|
||||||
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
|
if self.exchange.stoploss_adjust(stoploss_norm, order, side=trade.exit_side):
|
||||||
# we check if the update is necessary
|
# we check if the update is necessary
|
||||||
@ -1159,7 +1160,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
f"for pair {trade.pair}")
|
f"for pair {trade.pair}")
|
||||||
|
|
||||||
# Create new stoploss order
|
# Create new stoploss order
|
||||||
if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
|
if not self.create_stoploss_order(trade=trade, stop_price=stoploss_norm):
|
||||||
logger.warning(f"Could not create trailing stoploss order "
|
logger.warning(f"Could not create trailing stoploss order "
|
||||||
f"for pair {trade.pair}.")
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
@ -1465,14 +1466,15 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
)
|
)
|
||||||
exit_type = 'exit'
|
exit_type = 'exit'
|
||||||
exit_reason = exit_tag or exit_check.exit_reason
|
exit_reason = exit_tag or exit_check.exit_reason
|
||||||
if exit_check.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
|
if exit_check.exit_type in (
|
||||||
|
ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION):
|
||||||
exit_type = 'stoploss'
|
exit_type = 'stoploss'
|
||||||
|
|
||||||
# if stoploss is on exchange and we are on dry_run mode,
|
# if stoploss is on exchange and we are on dry_run mode,
|
||||||
# we consider the sell price stop price
|
# we consider the sell price stop price
|
||||||
if (self.config['dry_run'] and exit_type == 'stoploss'
|
if (self.config['dry_run'] and exit_type == 'stoploss'
|
||||||
and self.strategy.order_types['stoploss_on_exchange']):
|
and self.strategy.order_types['stoploss_on_exchange']):
|
||||||
limit = trade.stop_loss
|
limit = trade.stoploss_or_liquidation
|
||||||
|
|
||||||
# set custom_exit_price if available
|
# set custom_exit_price if available
|
||||||
proposed_limit_rate = limit
|
proposed_limit_rate = limit
|
||||||
@ -1497,12 +1499,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount = self._safe_exit_amount(trade.pair, sub_trade_amt or trade.amount)
|
amount = self._safe_exit_amount(trade.pair, sub_trade_amt or trade.amount)
|
||||||
time_in_force = self.strategy.order_time_in_force['exit']
|
time_in_force = self.strategy.order_time_in_force['exit']
|
||||||
|
|
||||||
if not sub_trade_amt and not strategy_safe_wrapper(
|
if (exit_check.exit_type != ExitType.LIQUIDATION
|
||||||
self.strategy.confirm_trade_exit, default_retval=True)(
|
and not sub_trade_amt
|
||||||
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
|
and not strategy_safe_wrapper(
|
||||||
time_in_force=time_in_force, exit_reason=exit_reason,
|
self.strategy.confirm_trade_exit, default_retval=True)(
|
||||||
sell_reason=exit_reason, # sellreason -> compatibility
|
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
|
||||||
current_time=datetime.now(timezone.utc)):
|
time_in_force=time_in_force, exit_reason=exit_reason,
|
||||||
|
sell_reason=exit_reason, # sellreason -> compatibility
|
||||||
|
current_time=datetime.now(timezone.utc))):
|
||||||
logger.info(f"User denied exit for {trade.pair}.")
|
logger.info(f"User denied exit for {trade.pair}.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1711,7 +1715,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# Must also run for partial exits
|
# Must also run for partial exits
|
||||||
# TODO: Margin will need to use interest_rate as well.
|
# TODO: Margin will need to use interest_rate as well.
|
||||||
# interest_rate = self.exchange.get_interest_rate()
|
# interest_rate = self.exchange.get_interest_rate()
|
||||||
trade.set_isolated_liq(self.exchange.get_liquidation_price(
|
trade.set_liquidation_price(self.exchange.get_liquidation_price(
|
||||||
leverage=trade.leverage,
|
leverage=trade.leverage,
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
amount=trade.amount,
|
amount=trade.amount,
|
||||||
|
@ -381,7 +381,8 @@ class Backtesting:
|
|||||||
Get close rate for backtesting result
|
Get close rate for backtesting result
|
||||||
"""
|
"""
|
||||||
# Special handling if high or low hit STOP_LOSS or ROI
|
# Special handling if high or low hit STOP_LOSS or ROI
|
||||||
if exit.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
|
if exit.exit_type in (
|
||||||
|
ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS, ExitType.LIQUIDATION):
|
||||||
return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur)
|
return self._get_close_rate_for_stoploss(row, trade, exit, trade_dur)
|
||||||
elif exit.exit_type == (ExitType.ROI):
|
elif exit.exit_type == (ExitType.ROI):
|
||||||
return self._get_close_rate_for_roi(row, trade, exit, trade_dur)
|
return self._get_close_rate_for_roi(row, trade, exit, trade_dur)
|
||||||
@ -396,11 +397,16 @@ class Backtesting:
|
|||||||
is_short = trade.is_short or False
|
is_short = trade.is_short or False
|
||||||
leverage = trade.leverage or 1.0
|
leverage = trade.leverage or 1.0
|
||||||
side_1 = -1 if is_short else 1
|
side_1 = -1 if is_short else 1
|
||||||
|
if exit.exit_type == ExitType.LIQUIDATION and trade.liquidation_price:
|
||||||
|
stoploss_value = trade.liquidation_price
|
||||||
|
else:
|
||||||
|
stoploss_value = trade.stop_loss
|
||||||
|
|
||||||
if is_short:
|
if is_short:
|
||||||
if trade.stop_loss < row[LOW_IDX]:
|
if stoploss_value < row[LOW_IDX]:
|
||||||
return row[OPEN_IDX]
|
return row[OPEN_IDX]
|
||||||
else:
|
else:
|
||||||
if trade.stop_loss > row[HIGH_IDX]:
|
if stoploss_value > row[HIGH_IDX]:
|
||||||
return row[OPEN_IDX]
|
return row[OPEN_IDX]
|
||||||
|
|
||||||
# Special case: trailing triggers within same candle as trade opened. Assume most
|
# Special case: trailing triggers within same candle as trade opened. Assume most
|
||||||
@ -433,7 +439,7 @@ class Backtesting:
|
|||||||
return max(row[LOW_IDX], stop_rate)
|
return max(row[LOW_IDX], stop_rate)
|
||||||
|
|
||||||
# Set close_rate to stoploss
|
# Set close_rate to stoploss
|
||||||
return trade.stop_loss
|
return stoploss_value
|
||||||
|
|
||||||
def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple,
|
def _get_close_rate_for_roi(self, row: Tuple, trade: LocalTrade, exit: ExitCheckTuple,
|
||||||
trade_dur: int) -> float:
|
trade_dur: int) -> float:
|
||||||
@ -614,7 +620,8 @@ class Backtesting:
|
|||||||
# Confirm trade exit:
|
# Confirm trade exit:
|
||||||
time_in_force = self.strategy.order_time_in_force['exit']
|
time_in_force = self.strategy.order_time_in_force['exit']
|
||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
if (exit_.exit_type != ExitType.LIQUIDATION and not strategy_safe_wrapper(
|
||||||
|
self.strategy.confirm_trade_exit, default_retval=True)(
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
trade=trade, # type: ignore[arg-type]
|
trade=trade, # type: ignore[arg-type]
|
||||||
order_type=order_type,
|
order_type=order_type,
|
||||||
@ -623,7 +630,7 @@ class Backtesting:
|
|||||||
time_in_force=time_in_force,
|
time_in_force=time_in_force,
|
||||||
sell_reason=exit_reason, # deprecated
|
sell_reason=exit_reason, # deprecated
|
||||||
exit_reason=exit_reason,
|
exit_reason=exit_reason,
|
||||||
current_time=exit_candle_time):
|
current_time=exit_candle_time)):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
trade.exit_reason = exit_reason
|
trade.exit_reason = exit_reason
|
||||||
@ -835,7 +842,7 @@ class Backtesting:
|
|||||||
|
|
||||||
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
|
||||||
|
|
||||||
trade.set_isolated_liq(self.exchange.get_liquidation_price(
|
trade.set_liquidation_price(self.exchange.get_liquidation_price(
|
||||||
pair=pair,
|
pair=pair,
|
||||||
open_rate=propose_rate,
|
open_rate=propose_rate,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
|
@ -304,6 +304,16 @@ class LocalTrade():
|
|||||||
# Futures properties
|
# Futures properties
|
||||||
funding_fees: Optional[float] = None
|
funding_fees: Optional[float] = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stoploss_or_liquidation(self) -> float:
|
||||||
|
if self.liquidation_price:
|
||||||
|
if self.is_short:
|
||||||
|
return min(self.stop_loss, self.liquidation_price)
|
||||||
|
else:
|
||||||
|
return max(self.stop_loss, self.liquidation_price)
|
||||||
|
|
||||||
|
return self.stop_loss
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def buy_tag(self) -> Optional[str]:
|
def buy_tag(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
@ -500,7 +510,7 @@ class LocalTrade():
|
|||||||
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
||||||
self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
|
self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
|
||||||
|
|
||||||
def set_isolated_liq(self, liquidation_price: Optional[float]):
|
def set_liquidation_price(self, liquidation_price: Optional[float]):
|
||||||
"""
|
"""
|
||||||
Method you should use to set self.liquidation price.
|
Method you should use to set self.liquidation price.
|
||||||
Assures stop_loss is not passed the liquidation price
|
Assures stop_loss is not passed the liquidation price
|
||||||
@ -509,22 +519,13 @@ class LocalTrade():
|
|||||||
return
|
return
|
||||||
self.liquidation_price = liquidation_price
|
self.liquidation_price = liquidation_price
|
||||||
|
|
||||||
def _set_stop_loss(self, stop_loss: float, percent: float):
|
def __set_stop_loss(self, stop_loss: float, percent: float):
|
||||||
"""
|
"""
|
||||||
Method you should use to set self.stop_loss.
|
Method used internally to set self.stop_loss.
|
||||||
Assures stop_loss is not passed the liquidation price
|
|
||||||
"""
|
"""
|
||||||
if self.liquidation_price is not None:
|
|
||||||
if self.is_short:
|
|
||||||
sl = min(stop_loss, self.liquidation_price)
|
|
||||||
else:
|
|
||||||
sl = max(stop_loss, self.liquidation_price)
|
|
||||||
else:
|
|
||||||
sl = stop_loss
|
|
||||||
|
|
||||||
if not self.stop_loss:
|
if not self.stop_loss:
|
||||||
self.initial_stop_loss = sl
|
self.initial_stop_loss = stop_loss
|
||||||
self.stop_loss = sl
|
self.stop_loss = stop_loss
|
||||||
|
|
||||||
self.stop_loss_pct = -1 * abs(percent)
|
self.stop_loss_pct = -1 * abs(percent)
|
||||||
self.stoploss_last_update = datetime.utcnow()
|
self.stoploss_last_update = datetime.utcnow()
|
||||||
@ -546,18 +547,12 @@ class LocalTrade():
|
|||||||
leverage = self.leverage or 1.0
|
leverage = self.leverage or 1.0
|
||||||
if self.is_short:
|
if self.is_short:
|
||||||
new_loss = float(current_price * (1 + abs(stoploss / leverage)))
|
new_loss = float(current_price * (1 + abs(stoploss / leverage)))
|
||||||
# If trading with leverage, don't set the stoploss below the liquidation price
|
|
||||||
if self.liquidation_price:
|
|
||||||
new_loss = min(self.liquidation_price, new_loss)
|
|
||||||
else:
|
else:
|
||||||
new_loss = float(current_price * (1 - abs(stoploss / leverage)))
|
new_loss = float(current_price * (1 - abs(stoploss / leverage)))
|
||||||
# If trading with leverage, don't set the stoploss below the liquidation price
|
|
||||||
if self.liquidation_price:
|
|
||||||
new_loss = max(self.liquidation_price, new_loss)
|
|
||||||
|
|
||||||
# no stop loss assigned yet
|
# no stop loss assigned yet
|
||||||
if self.initial_stop_loss_pct is None or refresh:
|
if self.initial_stop_loss_pct is None or refresh:
|
||||||
self._set_stop_loss(new_loss, stoploss)
|
self.__set_stop_loss(new_loss, stoploss)
|
||||||
self.initial_stop_loss = new_loss
|
self.initial_stop_loss = new_loss
|
||||||
self.initial_stop_loss_pct = -1 * abs(stoploss)
|
self.initial_stop_loss_pct = -1 * abs(stoploss)
|
||||||
|
|
||||||
@ -572,7 +567,7 @@ class LocalTrade():
|
|||||||
# ? decreasing the minimum stoploss
|
# ? decreasing the minimum stoploss
|
||||||
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
|
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
|
||||||
logger.debug(f"{self.pair} - Adjusting stoploss...")
|
logger.debug(f"{self.pair} - Adjusting stoploss...")
|
||||||
self._set_stop_loss(new_loss, stoploss)
|
self.__set_stop_loss(new_loss, stoploss)
|
||||||
else:
|
else:
|
||||||
logger.debug(f"{self.pair} - Keeping current stoploss...")
|
logger.debug(f"{self.pair} - Keeping current stoploss...")
|
||||||
|
|
||||||
|
@ -23,13 +23,14 @@ class StoplossGuard(IProtection):
|
|||||||
self._trade_limit = protection_config.get('trade_limit', 10)
|
self._trade_limit = protection_config.get('trade_limit', 10)
|
||||||
self._disable_global_stop = protection_config.get('only_per_pair', False)
|
self._disable_global_stop = protection_config.get('only_per_pair', False)
|
||||||
self._only_per_side = protection_config.get('only_per_side', False)
|
self._only_per_side = protection_config.get('only_per_side', False)
|
||||||
|
self._profit_limit = protection_config.get('required_profit', 0.0)
|
||||||
|
|
||||||
def short_desc(self) -> str:
|
def short_desc(self) -> str:
|
||||||
"""
|
"""
|
||||||
Short method description - used for startup-messages
|
Short method description - used for startup-messages
|
||||||
"""
|
"""
|
||||||
return (f"{self.name} - Frequent Stoploss Guard, {self._trade_limit} stoplosses "
|
return (f"{self.name} - Frequent Stoploss Guard, {self._trade_limit} stoplosses "
|
||||||
f"within {self.lookback_period_str}.")
|
f"with profit < {self._profit_limit:.2%} within {self.lookback_period_str}.")
|
||||||
|
|
||||||
def _reason(self) -> str:
|
def _reason(self) -> str:
|
||||||
"""
|
"""
|
||||||
@ -48,8 +49,8 @@ class StoplossGuard(IProtection):
|
|||||||
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
|
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
|
||||||
trades = [trade for trade in trades1 if (str(trade.exit_reason) in (
|
trades = [trade for trade in trades1 if (str(trade.exit_reason) in (
|
||||||
ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value,
|
ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value,
|
||||||
ExitType.STOPLOSS_ON_EXCHANGE.value)
|
ExitType.STOPLOSS_ON_EXCHANGE.value, ExitType.LIQUIDATION.value)
|
||||||
and trade.close_profit and trade.close_profit < 0)]
|
and trade.close_profit and trade.close_profit < self._profit_limit)]
|
||||||
|
|
||||||
if self._only_per_side:
|
if self._only_per_side:
|
||||||
# Long or short trades only
|
# Long or short trades only
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
This module contains class to manage RPC communications (Telegram, API, ...)
|
This module contains class to manage RPC communications (Telegram, API, ...)
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from collections import deque
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from freqtrade.enums import RPCMessageType
|
from freqtrade.enums import RPCMessageType
|
||||||
@ -77,6 +78,17 @@ class RPCManager:
|
|||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.")
|
logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.")
|
||||||
|
|
||||||
|
def process_msg_queue(self, queue: deque) -> None:
|
||||||
|
"""
|
||||||
|
Process all messages in the queue.
|
||||||
|
"""
|
||||||
|
while queue:
|
||||||
|
msg = queue.popleft()
|
||||||
|
self.send_msg({
|
||||||
|
'type': RPCMessageType.STRATEGY_MSG,
|
||||||
|
'msg': msg,
|
||||||
|
})
|
||||||
|
|
||||||
def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None:
|
def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None:
|
||||||
if config['dry_run']:
|
if config['dry_run']:
|
||||||
self.send_msg({
|
self.send_msg({
|
||||||
|
@ -407,7 +407,8 @@ class Telegram(RPCHandler):
|
|||||||
|
|
||||||
elif msg_type == RPCMessageType.STARTUP:
|
elif msg_type == RPCMessageType.STARTUP:
|
||||||
message = f"{msg['status']}"
|
message = f"{msg['status']}"
|
||||||
|
elif msg_type == RPCMessageType.STRATEGY_MSG:
|
||||||
|
message = f"{msg['msg']}"
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f"Unknown message type: {msg_type}")
|
raise NotImplementedError(f"Unknown message type: {msg_type}")
|
||||||
return message
|
return message
|
||||||
|
@ -972,7 +972,7 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
# ROI
|
# ROI
|
||||||
# Trailing stoploss
|
# Trailing stoploss
|
||||||
|
|
||||||
if stoplossflag.exit_type == ExitType.STOP_LOSS:
|
if stoplossflag.exit_type in (ExitType.STOP_LOSS, ExitType.LIQUIDATION):
|
||||||
|
|
||||||
logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}")
|
logger.debug(f"{trade.pair} - Stoploss hit. exit_type={stoplossflag.exit_type}")
|
||||||
exits.append(stoplossflag)
|
exits.append(stoplossflag)
|
||||||
@ -1044,6 +1044,17 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
|
|
||||||
sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short)
|
sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short)
|
||||||
sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short)
|
sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short)
|
||||||
|
liq_higher_long = (trade.liquidation_price
|
||||||
|
and trade.liquidation_price >= (low or current_rate)
|
||||||
|
and not trade.is_short)
|
||||||
|
liq_lower_short = (trade.liquidation_price
|
||||||
|
and trade.liquidation_price <= (high or current_rate)
|
||||||
|
and trade.is_short)
|
||||||
|
|
||||||
|
if (liq_higher_long or liq_lower_short):
|
||||||
|
logger.debug(f"{trade.pair} - Liquidation price hit. exit_type=ExitType.LIQUIDATION")
|
||||||
|
return ExitCheckTuple(exit_type=ExitType.LIQUIDATION)
|
||||||
|
|
||||||
# evaluate if the stoploss was hit if stoploss is not on exchange
|
# evaluate if the stoploss was hit if stoploss is not on exchange
|
||||||
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
||||||
# regular stoploss handling.
|
# regular stoploss handling.
|
||||||
@ -1061,13 +1072,6 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
f"stoploss is {trade.stop_loss:.6f}, "
|
f"stoploss is {trade.stop_loss:.6f}, "
|
||||||
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
|
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
|
||||||
f"trade opened at {trade.open_rate:.6f}")
|
f"trade opened at {trade.open_rate:.6f}")
|
||||||
new_stoploss = (
|
|
||||||
trade.stop_loss + trade.initial_stop_loss
|
|
||||||
if trade.is_short else
|
|
||||||
trade.stop_loss - trade.initial_stop_loss
|
|
||||||
)
|
|
||||||
logger.debug(f"{trade.pair} - Trailing stop saved "
|
|
||||||
f"{new_stoploss:.6f}")
|
|
||||||
|
|
||||||
return ExitCheckTuple(exit_type=exit_type)
|
return ExitCheckTuple(exit_type=exit_type)
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
coveralls==3.3.1
|
coveralls==3.3.1
|
||||||
flake8==4.0.1
|
flake8==4.0.1
|
||||||
flake8-tidy-imports==4.8.0
|
flake8-tidy-imports==4.8.0
|
||||||
mypy==0.961
|
mypy==0.971
|
||||||
pre-commit==2.20.0
|
pre-commit==2.20.0
|
||||||
pytest==7.1.2
|
pytest==7.1.2
|
||||||
pytest-asyncio==0.19.0
|
pytest-asyncio==0.19.0
|
||||||
@ -24,6 +24,6 @@ nbconvert==6.5.0
|
|||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==5.2.1
|
types-cachetools==5.2.1
|
||||||
types-filelock==3.2.7
|
types-filelock==3.2.7
|
||||||
types-requests==2.28.1
|
types-requests==2.28.3
|
||||||
types-tabulate==0.8.11
|
types-tabulate==0.8.11
|
||||||
types-python-dateutil==2.8.18
|
types-python-dateutil==2.8.19
|
||||||
|
@ -2,7 +2,7 @@ numpy==1.23.1
|
|||||||
pandas==1.4.3
|
pandas==1.4.3
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.90.89
|
ccxt==1.91.29
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==37.0.4
|
cryptography==37.0.4
|
||||||
aiohttp==3.8.1
|
aiohttp==3.8.1
|
||||||
@ -28,7 +28,7 @@ py_find_1st==1.1.5
|
|||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==1.8
|
python-rapidjson==1.8
|
||||||
# Properly format api responses
|
# Properly format api responses
|
||||||
orjson==3.7.7
|
orjson==3.7.8
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
@ -311,3 +311,27 @@ def test_no_exchange_mode(default_conf):
|
|||||||
|
|
||||||
with pytest.raises(OperationalException, match=message):
|
with pytest.raises(OperationalException, match=message):
|
||||||
dp.available_pairs()
|
dp.available_pairs()
|
||||||
|
|
||||||
|
|
||||||
|
def test_dp_send_msg(default_conf):
|
||||||
|
|
||||||
|
default_conf["runmode"] = RunMode.DRY_RUN
|
||||||
|
|
||||||
|
default_conf["timeframe"] = '1h'
|
||||||
|
dp = DataProvider(default_conf, None)
|
||||||
|
msg = 'Test message'
|
||||||
|
dp.send_msg(msg)
|
||||||
|
|
||||||
|
assert msg in dp._msg_queue
|
||||||
|
dp._msg_queue.pop()
|
||||||
|
assert msg not in dp._msg_queue
|
||||||
|
# Message is not resent due to caching
|
||||||
|
dp.send_msg(msg)
|
||||||
|
assert msg not in dp._msg_queue
|
||||||
|
dp.send_msg(msg, always_send=True)
|
||||||
|
assert msg in dp._msg_queue
|
||||||
|
|
||||||
|
default_conf["runmode"] = RunMode.BACKTEST
|
||||||
|
dp = DataProvider(default_conf, None)
|
||||||
|
dp.send_msg(msg, always_send=True)
|
||||||
|
assert msg not in dp._msg_queue
|
||||||
|
@ -2993,6 +2993,9 @@ def test_check_order_canceled_empty(mocker, default_conf, exchange_name, order,
|
|||||||
({'amount': 10.0, 'fee': {}}, False),
|
({'amount': 10.0, 'fee': {}}, False),
|
||||||
({'result': 'testest123'}, False),
|
({'result': 'testest123'}, False),
|
||||||
('hello_world', False),
|
('hello_world', False),
|
||||||
|
({'status': 'canceled', 'amount': None, 'fee': None}, False),
|
||||||
|
({'status': 'canceled', 'filled': None, 'amount': None, 'fee': None}, False),
|
||||||
|
|
||||||
])
|
])
|
||||||
def test_is_cancel_order_result_suitable(mocker, default_conf, exchange_name, order, result):
|
def test_is_cancel_order_result_suitable(mocker, default_conf, exchange_name, order, result):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
@ -4179,20 +4182,6 @@ def test_get_or_calculate_liquidation_price(mocker, default_conf):
|
|||||||
)
|
)
|
||||||
assert liq_price == 17.540699999999998
|
assert liq_price == 17.540699999999998
|
||||||
|
|
||||||
ccxt_exceptionhandlers(
|
|
||||||
mocker,
|
|
||||||
default_conf,
|
|
||||||
api_mock,
|
|
||||||
"binance",
|
|
||||||
"get_or_calculate_liquidation_price",
|
|
||||||
"fetch_positions",
|
|
||||||
pair="XRP/USDT",
|
|
||||||
open_rate=0.0,
|
|
||||||
is_short=False,
|
|
||||||
position=0.0,
|
|
||||||
wallet_balance=0.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [
|
@pytest.mark.parametrize('exchange,rate_start,rate_end,d1,d2,amount,expected_fees', [
|
||||||
('binance', 0, 2, "2021-09-01 01:00:00", "2021-09-01 04:00:00", 30.0, 0.0),
|
('binance', 0, 2, "2021-09-01 01:00:00", "2021-09-01 04:00:00", 30.0, 0.0),
|
||||||
|
@ -424,7 +424,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
|
|||||||
@pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [
|
@pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [
|
||||||
({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60},
|
({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60},
|
||||||
"[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, "
|
"[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, "
|
||||||
"2 stoplosses within 60 minutes.'}]",
|
"2 stoplosses with profit < 0.00% within 60 minutes.'}]",
|
||||||
None
|
None
|
||||||
),
|
),
|
||||||
({"method": "CooldownPeriod", "stop_duration": 60},
|
({"method": "CooldownPeriod", "stop_duration": 60},
|
||||||
@ -442,9 +442,9 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog):
|
|||||||
None
|
None
|
||||||
),
|
),
|
||||||
({"method": "StoplossGuard", "lookback_period_candles": 12, "trade_limit": 2,
|
({"method": "StoplossGuard", "lookback_period_candles": 12, "trade_limit": 2,
|
||||||
"stop_duration": 60},
|
"required_profit": -0.05, "stop_duration": 60},
|
||||||
"[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, "
|
"[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, "
|
||||||
"2 stoplosses within 12 candles.'}]",
|
"2 stoplosses with profit < -5.00% within 12 candles.'}]",
|
||||||
None
|
None
|
||||||
),
|
),
|
||||||
({"method": "CooldownPeriod", "stop_duration_candles": 5},
|
({"method": "CooldownPeriod", "stop_duration_candles": 5},
|
||||||
|
@ -1402,7 +1402,6 @@ def test_api_strategies(botclient):
|
|||||||
'InformativeDecoratorTest',
|
'InformativeDecoratorTest',
|
||||||
'StrategyTestV2',
|
'StrategyTestV2',
|
||||||
'StrategyTestV3',
|
'StrategyTestV3',
|
||||||
'StrategyTestV3Analysis',
|
|
||||||
'StrategyTestV3Futures'
|
'StrategyTestV3Futures'
|
||||||
]}
|
]}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from collections import deque
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
from freqtrade.enums import RPCMessageType
|
from freqtrade.enums import RPCMessageType
|
||||||
@ -81,9 +82,25 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
|
|||||||
assert telegram_mock.call_count == 0
|
assert telegram_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_msg_queue(mocker, default_conf, caplog) -> None:
|
||||||
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg')
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
|
||||||
|
|
||||||
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
queue = deque()
|
||||||
|
queue.append('Test message')
|
||||||
|
queue.append('Test message 2')
|
||||||
|
rpc_manager.process_msg_queue(queue)
|
||||||
|
|
||||||
|
assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message'}", caplog)
|
||||||
|
assert log_has("Sending rpc message: {'type': strategy_msg, 'msg': 'Test message 2'}", caplog)
|
||||||
|
assert telegram_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
|
||||||
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg')
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init')
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
rpc_manager = RPCManager(freqtradebot)
|
rpc_manager = RPCManager(freqtradebot)
|
||||||
|
@ -2067,6 +2067,16 @@ def test_startup_notification(default_conf, mocker) -> None:
|
|||||||
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'
|
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_msg_strategy_msg_notification(default_conf, mocker) -> None:
|
||||||
|
|
||||||
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
|
telegram.send_msg({
|
||||||
|
'type': RPCMessageType.STRATEGY_MSG,
|
||||||
|
'msg': 'hello world, Test msg'
|
||||||
|
})
|
||||||
|
assert msg_mock.call_args[0][0] == 'hello world, Test msg'
|
||||||
|
|
||||||
|
|
||||||
def test_send_msg_unknown_type(default_conf, mocker) -> None:
|
def test_send_msg_unknown_type(default_conf, mocker) -> None:
|
||||||
telegram, _, _ = get_telegram_testobject(mocker, default_conf)
|
telegram, _, _ = get_telegram_testobject(mocker, default_conf)
|
||||||
with pytest.raises(NotImplementedError, match=r'Unknown message type: None'):
|
with pytest.raises(NotImplementedError, match=r'Unknown message type: None'):
|
||||||
|
@ -1,175 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
|
||||||
|
|
||||||
import talib.abstract as ta
|
|
||||||
from pandas import DataFrame
|
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
|
||||||
from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy,
|
|
||||||
RealParameter)
|
|
||||||
|
|
||||||
|
|
||||||
class StrategyTestV3Analysis(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 = 3
|
|
||||||
|
|
||||||
# Minimal ROI designed for the strategy
|
|
||||||
minimal_roi = {
|
|
||||||
"40": 0.0,
|
|
||||||
"30": 0.01,
|
|
||||||
"20": 0.02,
|
|
||||||
"0": 0.04
|
|
||||||
}
|
|
||||||
|
|
||||||
# Optimal stoploss designed for the strategy
|
|
||||||
stoploss = -0.10
|
|
||||||
|
|
||||||
# Optimal timeframe for the strategy
|
|
||||||
timeframe = '5m'
|
|
||||||
|
|
||||||
# Optional order type mapping
|
|
||||||
order_types = {
|
|
||||||
'entry': 'limit',
|
|
||||||
'exit': 'limit',
|
|
||||||
'stoploss': 'limit',
|
|
||||||
'stoploss_on_exchange': False
|
|
||||||
}
|
|
||||||
|
|
||||||
# Number of candles the strategy requires before producing valid signals
|
|
||||||
startup_candle_count: int = 20
|
|
||||||
|
|
||||||
# Optional time in force for orders
|
|
||||||
order_time_in_force = {
|
|
||||||
'entry': 'gtc',
|
|
||||||
'exit': 'gtc',
|
|
||||||
}
|
|
||||||
|
|
||||||
buy_params = {
|
|
||||||
'buy_rsi': 35,
|
|
||||||
# Intentionally not specified, so "default" is tested
|
|
||||||
# 'buy_plusdi': 0.4
|
|
||||||
}
|
|
||||||
|
|
||||||
sell_params = {
|
|
||||||
'sell_rsi': 74,
|
|
||||||
'sell_minusdi': 0.4
|
|
||||||
}
|
|
||||||
|
|
||||||
buy_rsi = IntParameter([0, 50], default=30, space='buy')
|
|
||||||
buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy')
|
|
||||||
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell')
|
|
||||||
sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell',
|
|
||||||
load=False)
|
|
||||||
protection_enabled = BooleanParameter(default=True)
|
|
||||||
protection_cooldown_lookback = IntParameter([0, 50], default=30)
|
|
||||||
|
|
||||||
# TODO: Can this work with protection tests? (replace HyperoptableStrategy implicitly ... )
|
|
||||||
# @property
|
|
||||||
# def protections(self):
|
|
||||||
# prot = []
|
|
||||||
# if self.protection_enabled.value:
|
|
||||||
# prot.append({
|
|
||||||
# "method": "CooldownPeriod",
|
|
||||||
# "stop_duration_candles": self.protection_cooldown_lookback.value
|
|
||||||
# })
|
|
||||||
# return prot
|
|
||||||
|
|
||||||
bot_started = False
|
|
||||||
|
|
||||||
def bot_start(self):
|
|
||||||
self.bot_started = True
|
|
||||||
|
|
||||||
def informative_pairs(self):
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
|
|
||||||
# Momentum Indicator
|
|
||||||
# ------------------------------------
|
|
||||||
|
|
||||||
# ADX
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
|
||||||
|
|
||||||
# MACD
|
|
||||||
macd = ta.MACD(dataframe)
|
|
||||||
dataframe['macd'] = macd['macd']
|
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
|
||||||
dataframe['macdhist'] = macd['macdhist']
|
|
||||||
|
|
||||||
# Minus Directional Indicator / Movement
|
|
||||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
|
||||||
|
|
||||||
# Plus Directional Indicator / Movement
|
|
||||||
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
|
|
||||||
|
|
||||||
# RSI
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
|
||||||
|
|
||||||
# Stoch fast
|
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
|
||||||
dataframe['fastk'] = stoch_fast['fastk']
|
|
||||||
|
|
||||||
# Bollinger bands
|
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
|
||||||
dataframe['bb_middleband'] = bollinger['mid']
|
|
||||||
dataframe['bb_upperband'] = bollinger['upper']
|
|
||||||
|
|
||||||
# EMA - Exponential Moving Average
|
|
||||||
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(dataframe['rsi'] < self.buy_rsi.value) &
|
|
||||||
(dataframe['fastd'] < 35) &
|
|
||||||
(dataframe['adx'] > 30) &
|
|
||||||
(dataframe['plus_di'] > self.buy_plusdi.value)
|
|
||||||
) |
|
|
||||||
(
|
|
||||||
(dataframe['adx'] > 65) &
|
|
||||||
(dataframe['plus_di'] > self.buy_plusdi.value)
|
|
||||||
),
|
|
||||||
['enter_long', 'enter_tag']] = 1, 'enter_tag_long'
|
|
||||||
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
qtpylib.crossed_below(dataframe['rsi'], self.sell_rsi.value)
|
|
||||||
),
|
|
||||||
['enter_short', 'enter_tag']] = 1, 'enter_tag_short'
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
(
|
|
||||||
(qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) |
|
|
||||||
(qtpylib.crossed_above(dataframe['fastd'], 70))
|
|
||||||
) &
|
|
||||||
(dataframe['adx'] > 10) &
|
|
||||||
(dataframe['minus_di'] > 0)
|
|
||||||
) |
|
|
||||||
(
|
|
||||||
(dataframe['adx'] > 70) &
|
|
||||||
(dataframe['minus_di'] > self.sell_minusdi.value)
|
|
||||||
),
|
|
||||||
['exit_long', 'exit_tag']] = 1, 'exit_tag_long'
|
|
||||||
|
|
||||||
dataframe.loc[
|
|
||||||
(
|
|
||||||
qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value)
|
|
||||||
),
|
|
||||||
['exit_long', 'exit_tag']] = 1, 'exit_tag_short'
|
|
||||||
|
|
||||||
return dataframe
|
|
@ -408,28 +408,31 @@ def test_min_roi_reached3(default_conf, fee) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'profit,adjusted,expected,trailing,custom,profit2,adjusted2,expected2,custom_stop', [
|
'profit,adjusted,expected,liq,trailing,custom,profit2,adjusted2,expected2,custom_stop', [
|
||||||
# Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing,
|
# Profit, adjusted stoploss(absolute), profit for 2nd call, enable trailing,
|
||||||
# enable custom stoploss, expected after 1st call, expected after 2nd call
|
# enable custom stoploss, expected after 1st call, expected after 2nd call
|
||||||
(0.2, 0.9, ExitType.NONE, False, False, 0.3, 0.9, ExitType.NONE, None),
|
(0.2, 0.9, ExitType.NONE, None, False, False, 0.3, 0.9, ExitType.NONE, None),
|
||||||
(0.2, 0.9, ExitType.NONE, False, False, -0.2, 0.9, ExitType.STOP_LOSS, None),
|
(0.2, 0.9, ExitType.NONE, None, False, False, -0.2, 0.9, ExitType.STOP_LOSS, None),
|
||||||
(0.2, 1.14, ExitType.NONE, True, False, 0.05, 1.14, ExitType.TRAILING_STOP_LOSS, None),
|
(0.2, 0.9, ExitType.NONE, 0.8, False, False, -0.2, 0.9, ExitType.LIQUIDATION, None),
|
||||||
(0.01, 0.96, ExitType.NONE, True, False, 0.05, 1, ExitType.NONE, None),
|
(0.2, 1.14, ExitType.NONE, None, True, False, 0.05, 1.14, ExitType.TRAILING_STOP_LOSS,
|
||||||
(0.05, 1, ExitType.NONE, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None),
|
None),
|
||||||
|
(0.01, 0.96, ExitType.NONE, None, True, False, 0.05, 1, ExitType.NONE, None),
|
||||||
|
(0.05, 1, ExitType.NONE, None, True, False, -0.01, 1, ExitType.TRAILING_STOP_LOSS, None),
|
||||||
# Default custom case - trails with 10%
|
# Default custom case - trails with 10%
|
||||||
(0.05, 0.95, ExitType.NONE, False, True, -0.02, 0.95, ExitType.NONE, None),
|
(0.05, 0.95, ExitType.NONE, None, False, True, -0.02, 0.95, ExitType.NONE, None),
|
||||||
(0.05, 0.95, ExitType.NONE, False, True, -0.06, 0.95, ExitType.TRAILING_STOP_LOSS, None),
|
(0.05, 0.95, ExitType.NONE, None, False, True, -0.06, 0.95, ExitType.TRAILING_STOP_LOSS,
|
||||||
(0.05, 1, ExitType.NONE, False, True, -0.06, 1, ExitType.TRAILING_STOP_LOSS,
|
None),
|
||||||
|
(0.05, 1, ExitType.NONE, None, False, True, -0.06, 1, ExitType.TRAILING_STOP_LOSS,
|
||||||
lambda **kwargs: -0.05),
|
lambda **kwargs: -0.05),
|
||||||
(0.05, 1, ExitType.NONE, False, True, 0.09, 1.04, ExitType.NONE,
|
(0.05, 1, ExitType.NONE, None, False, True, 0.09, 1.04, ExitType.NONE,
|
||||||
lambda **kwargs: -0.05),
|
lambda **kwargs: -0.05),
|
||||||
(0.05, 0.95, ExitType.NONE, False, True, 0.09, 0.98, ExitType.NONE,
|
(0.05, 0.95, ExitType.NONE, None, False, True, 0.09, 0.98, ExitType.NONE,
|
||||||
lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)),
|
lambda current_profit, **kwargs: -0.1 if current_profit < 0.6 else -(current_profit * 2)),
|
||||||
# Error case - static stoploss in place
|
# Error case - static stoploss in place
|
||||||
(0.05, 0.9, ExitType.NONE, False, True, 0.09, 0.9, ExitType.NONE,
|
(0.05, 0.9, ExitType.NONE, None, False, True, 0.09, 0.9, ExitType.NONE,
|
||||||
lambda **kwargs: None),
|
lambda **kwargs: None),
|
||||||
])
|
])
|
||||||
def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom,
|
def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, liq, trailing, custom,
|
||||||
profit2, adjusted2, expected2, custom_stop) -> None:
|
profit2, adjusted2, expected2, custom_stop) -> None:
|
||||||
|
|
||||||
strategy = StrategyResolver.load_strategy(default_conf)
|
strategy = StrategyResolver.load_strategy(default_conf)
|
||||||
@ -442,6 +445,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
|
|||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_rate=1,
|
open_rate=1,
|
||||||
|
liquidation_price=liq,
|
||||||
)
|
)
|
||||||
trade.adjust_min_max_rates(trade.open_rate, trade.open_rate)
|
trade.adjust_min_max_rates(trade.open_rate, trade.open_rate)
|
||||||
strategy.trailing_stop = trailing
|
strategy.trailing_stop = trailing
|
||||||
|
@ -34,7 +34,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) == 7
|
assert len(strategies) == 6
|
||||||
assert isinstance(strategies[0], dict)
|
assert isinstance(strategies[0], dict)
|
||||||
|
|
||||||
|
|
||||||
@ -42,10 +42,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) == 8
|
assert len(strategies) == 7
|
||||||
# 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]) == 7
|
assert len([x for x in strategies if x['class'] is not None]) == 6
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -68,6 +68,12 @@ def test_process_stopped(mocker, default_conf_usdt) -> None:
|
|||||||
assert coo_mock.call_count == 1
|
assert coo_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_process_calls_sendmsg(mocker, default_conf_usdt) -> None:
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||||
|
freqtrade.process()
|
||||||
|
assert freqtrade.rpc.process_msg_queue.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_bot_cleanup(mocker, default_conf_usdt, caplog) -> None:
|
def test_bot_cleanup(mocker, default_conf_usdt, caplog) -> None:
|
||||||
mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db')
|
mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db')
|
||||||
coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders')
|
coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders')
|
||||||
|
@ -99,7 +99,7 @@ def test_enter_exit_side(fee, is_short):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_set_stop_loss_isolated_liq(fee):
|
def test_set_stop_loss_liquidation(fee):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
id=2,
|
id=2,
|
||||||
pair='ADA/USDT',
|
pair='ADA/USDT',
|
||||||
@ -115,73 +115,94 @@ def test_set_stop_loss_isolated_liq(fee):
|
|||||||
leverage=2.0,
|
leverage=2.0,
|
||||||
trading_mode=margin
|
trading_mode=margin
|
||||||
)
|
)
|
||||||
trade.set_isolated_liq(0.09)
|
trade.set_liquidation_price(0.09)
|
||||||
assert trade.liquidation_price == 0.09
|
assert trade.liquidation_price == 0.09
|
||||||
assert trade.stop_loss is None
|
assert trade.stop_loss is None
|
||||||
assert trade.initial_stop_loss is None
|
assert trade.initial_stop_loss is None
|
||||||
|
|
||||||
trade._set_stop_loss(0.1, (1.0 / 9.0))
|
trade.adjust_stop_loss(2.0, 0.2, True)
|
||||||
assert trade.liquidation_price == 0.09
|
assert trade.liquidation_price == 0.09
|
||||||
assert trade.stop_loss == 0.1
|
assert trade.stop_loss == 1.8
|
||||||
assert trade.initial_stop_loss == 0.1
|
assert trade.initial_stop_loss == 1.8
|
||||||
|
|
||||||
trade.set_isolated_liq(0.08)
|
trade.set_liquidation_price(0.08)
|
||||||
assert trade.liquidation_price == 0.08
|
assert trade.liquidation_price == 0.08
|
||||||
assert trade.stop_loss == 0.1
|
assert trade.stop_loss == 1.8
|
||||||
assert trade.initial_stop_loss == 0.1
|
assert trade.initial_stop_loss == 1.8
|
||||||
|
|
||||||
trade.set_isolated_liq(0.11)
|
trade.set_liquidation_price(0.11)
|
||||||
trade._set_stop_loss(0.1, 0)
|
trade.adjust_stop_loss(2.0, 0.2)
|
||||||
assert trade.liquidation_price == 0.11
|
assert trade.liquidation_price == 0.11
|
||||||
assert trade.stop_loss == 0.11
|
# Stoploss does not change from liquidation price
|
||||||
assert trade.initial_stop_loss == 0.1
|
assert trade.stop_loss == 1.8
|
||||||
|
assert trade.initial_stop_loss == 1.8
|
||||||
|
|
||||||
# lower stop doesn't move stoploss
|
# lower stop doesn't move stoploss
|
||||||
trade._set_stop_loss(0.1, 0)
|
trade.adjust_stop_loss(1.8, 0.2)
|
||||||
assert trade.liquidation_price == 0.11
|
assert trade.liquidation_price == 0.11
|
||||||
assert trade.stop_loss == 0.11
|
assert trade.stop_loss == 1.8
|
||||||
assert trade.initial_stop_loss == 0.1
|
assert trade.initial_stop_loss == 1.8
|
||||||
|
|
||||||
|
# higher stop does move stoploss
|
||||||
|
trade.adjust_stop_loss(2.1, 0.1)
|
||||||
|
assert trade.liquidation_price == 0.11
|
||||||
|
assert pytest.approx(trade.stop_loss) == 1.994999
|
||||||
|
assert trade.initial_stop_loss == 1.8
|
||||||
|
assert trade.stoploss_or_liquidation == trade.stop_loss
|
||||||
|
|
||||||
trade.stop_loss = None
|
trade.stop_loss = None
|
||||||
trade.liquidation_price = None
|
trade.liquidation_price = None
|
||||||
trade.initial_stop_loss = None
|
trade.initial_stop_loss = None
|
||||||
|
trade.initial_stop_loss_pct = None
|
||||||
|
|
||||||
trade._set_stop_loss(0.07, 0)
|
trade.adjust_stop_loss(2.0, 0.1, True)
|
||||||
assert trade.liquidation_price is None
|
assert trade.liquidation_price is None
|
||||||
assert trade.stop_loss == 0.07
|
assert trade.stop_loss == 1.9
|
||||||
assert trade.initial_stop_loss == 0.07
|
assert trade.initial_stop_loss == 1.9
|
||||||
|
assert trade.stoploss_or_liquidation == 1.9
|
||||||
|
|
||||||
trade.is_short = True
|
trade.is_short = True
|
||||||
trade.recalc_open_trade_value()
|
trade.recalc_open_trade_value()
|
||||||
trade.stop_loss = None
|
trade.stop_loss = None
|
||||||
trade.initial_stop_loss = None
|
trade.initial_stop_loss = None
|
||||||
|
trade.initial_stop_loss_pct = None
|
||||||
|
|
||||||
trade.set_isolated_liq(0.09)
|
trade.set_liquidation_price(3.09)
|
||||||
assert trade.liquidation_price == 0.09
|
assert trade.liquidation_price == 3.09
|
||||||
assert trade.stop_loss is None
|
assert trade.stop_loss is None
|
||||||
assert trade.initial_stop_loss is None
|
assert trade.initial_stop_loss is None
|
||||||
|
|
||||||
trade._set_stop_loss(0.08, (1.0 / 9.0))
|
trade.adjust_stop_loss(2.0, 0.2)
|
||||||
assert trade.liquidation_price == 0.09
|
assert trade.liquidation_price == 3.09
|
||||||
assert trade.stop_loss == 0.08
|
assert trade.stop_loss == 2.2
|
||||||
assert trade.initial_stop_loss == 0.08
|
assert trade.initial_stop_loss == 2.2
|
||||||
|
assert trade.stoploss_or_liquidation == 2.2
|
||||||
|
|
||||||
trade.set_isolated_liq(0.1)
|
trade.set_liquidation_price(3.1)
|
||||||
assert trade.liquidation_price == 0.1
|
assert trade.liquidation_price == 3.1
|
||||||
assert trade.stop_loss == 0.08
|
assert trade.stop_loss == 2.2
|
||||||
assert trade.initial_stop_loss == 0.08
|
assert trade.initial_stop_loss == 2.2
|
||||||
|
assert trade.stoploss_or_liquidation == 2.2
|
||||||
|
|
||||||
trade.set_isolated_liq(0.07)
|
trade.set_liquidation_price(3.8)
|
||||||
trade._set_stop_loss(0.1, (1.0 / 8.0))
|
assert trade.liquidation_price == 3.8
|
||||||
assert trade.liquidation_price == 0.07
|
# Stoploss does not change from liquidation price
|
||||||
assert trade.stop_loss == 0.07
|
assert trade.stop_loss == 2.2
|
||||||
assert trade.initial_stop_loss == 0.08
|
assert trade.initial_stop_loss == 2.2
|
||||||
|
|
||||||
# Stop doesn't move stop higher
|
# Stop doesn't move stop higher
|
||||||
trade._set_stop_loss(0.1, (1.0 / 9.0))
|
trade.adjust_stop_loss(2.0, 0.3)
|
||||||
assert trade.liquidation_price == 0.07
|
assert trade.liquidation_price == 3.8
|
||||||
assert trade.stop_loss == 0.07
|
assert trade.stop_loss == 2.2
|
||||||
assert trade.initial_stop_loss == 0.08
|
assert trade.initial_stop_loss == 2.2
|
||||||
|
|
||||||
|
# Stoploss does move lower
|
||||||
|
trade.set_liquidation_price(1.5)
|
||||||
|
trade.adjust_stop_loss(1.8, 0.1)
|
||||||
|
assert trade.liquidation_price == 1.5
|
||||||
|
assert pytest.approx(trade.stop_loss) == 1.89
|
||||||
|
assert trade.initial_stop_loss == 2.2
|
||||||
|
assert trade.stoploss_or_liquidation == 1.5
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [
|
@pytest.mark.parametrize('exchange,is_short,lev,minutes,rate,interest,trading_mode', [
|
||||||
@ -1542,26 +1563,26 @@ def test_adjust_stop_loss(fee):
|
|||||||
|
|
||||||
# Get percent of profit with a custom rate (Higher than open rate)
|
# Get percent of profit with a custom rate (Higher than open rate)
|
||||||
trade.adjust_stop_loss(1.3, -0.1)
|
trade.adjust_stop_loss(1.3, -0.1)
|
||||||
assert round(trade.stop_loss, 8) == 1.17
|
assert pytest.approx(trade.stop_loss) == 1.17
|
||||||
assert trade.stop_loss_pct == -0.1
|
assert trade.stop_loss_pct == -0.1
|
||||||
assert trade.initial_stop_loss == 0.95
|
assert trade.initial_stop_loss == 0.95
|
||||||
assert trade.initial_stop_loss_pct == -0.05
|
assert trade.initial_stop_loss_pct == -0.05
|
||||||
|
|
||||||
# current rate lower again ... should not change
|
# current rate lower again ... should not change
|
||||||
trade.adjust_stop_loss(1.2, 0.1)
|
trade.adjust_stop_loss(1.2, 0.1)
|
||||||
assert round(trade.stop_loss, 8) == 1.17
|
assert pytest.approx(trade.stop_loss) == 1.17
|
||||||
assert trade.initial_stop_loss == 0.95
|
assert trade.initial_stop_loss == 0.95
|
||||||
assert trade.initial_stop_loss_pct == -0.05
|
assert trade.initial_stop_loss_pct == -0.05
|
||||||
|
|
||||||
# current rate higher... should raise stoploss
|
# current rate higher... should raise stoploss
|
||||||
trade.adjust_stop_loss(1.4, 0.1)
|
trade.adjust_stop_loss(1.4, 0.1)
|
||||||
assert round(trade.stop_loss, 8) == 1.26
|
assert pytest.approx(trade.stop_loss) == 1.26
|
||||||
assert trade.initial_stop_loss == 0.95
|
assert trade.initial_stop_loss == 0.95
|
||||||
assert trade.initial_stop_loss_pct == -0.05
|
assert trade.initial_stop_loss_pct == -0.05
|
||||||
|
|
||||||
# Initial is true but stop_loss set - so doesn't do anything
|
# Initial is true but stop_loss set - so doesn't do anything
|
||||||
trade.adjust_stop_loss(1.7, 0.1, True)
|
trade.adjust_stop_loss(1.7, 0.1, True)
|
||||||
assert round(trade.stop_loss, 8) == 1.26
|
assert pytest.approx(trade.stop_loss) == 1.26
|
||||||
assert trade.initial_stop_loss == 0.95
|
assert trade.initial_stop_loss == 0.95
|
||||||
assert trade.initial_stop_loss_pct == -0.05
|
assert trade.initial_stop_loss_pct == -0.05
|
||||||
assert trade.stop_loss_pct == -0.1
|
assert trade.stop_loss_pct == -0.1
|
||||||
@ -1614,9 +1635,10 @@ def test_adjust_stop_loss_short(fee):
|
|||||||
assert trade.initial_stop_loss == 1.05
|
assert trade.initial_stop_loss == 1.05
|
||||||
assert trade.initial_stop_loss_pct == -0.05
|
assert trade.initial_stop_loss_pct == -0.05
|
||||||
assert trade.stop_loss_pct == -0.1
|
assert trade.stop_loss_pct == -0.1
|
||||||
trade.set_isolated_liq(0.63)
|
# Liquidation price is lower than stoploss - so liquidation would trigger first.
|
||||||
|
trade.set_liquidation_price(0.63)
|
||||||
trade.adjust_stop_loss(0.59, -0.1)
|
trade.adjust_stop_loss(0.59, -0.1)
|
||||||
assert trade.stop_loss == 0.63
|
assert trade.stop_loss == 0.649
|
||||||
assert trade.liquidation_price == 0.63
|
assert trade.liquidation_price == 0.63
|
||||||
|
|
||||||
|
|
||||||
@ -2016,10 +2038,10 @@ def test_stoploss_reinitialization_short(default_conf, fee):
|
|||||||
assert trade_adj.initial_stop_loss == 1.01
|
assert trade_adj.initial_stop_loss == 1.01
|
||||||
assert trade_adj.initial_stop_loss_pct == -0.05
|
assert trade_adj.initial_stop_loss_pct == -0.05
|
||||||
# Stoploss can't go above liquidation price
|
# Stoploss can't go above liquidation price
|
||||||
trade_adj.set_isolated_liq(0.985)
|
trade_adj.set_liquidation_price(0.985)
|
||||||
trade.adjust_stop_loss(0.9799, -0.05)
|
trade.adjust_stop_loss(0.9799, -0.05)
|
||||||
assert trade_adj.stop_loss == 0.985
|
assert trade_adj.stop_loss == 0.989699
|
||||||
assert trade_adj.stop_loss == 0.985
|
assert trade_adj.liquidation_price == 0.985
|
||||||
|
|
||||||
|
|
||||||
def test_update_fee(fee):
|
def test_update_fee(fee):
|
||||||
|
Loading…
Reference in New Issue
Block a user