diff --git a/README.md b/README.md index ade62ce94..8f7578561 100644 --- a/README.md +++ b/README.md @@ -68,9 +68,9 @@ For any other type of installation please refer to [Installation doc](https://ww ### Bot commands ``` -usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] - [--strategy-path PATH] [--dynamic-whitelist [INT]] - [--db-url PATH] +usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH] + [-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]] + [--db-url PATH] [--sd-notify] {backtesting,edge,hyperopt} ... Free, open source crypto trading bot @@ -84,6 +84,7 @@ positional arguments: optional arguments: -h, --help show this help message and exit -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified --version show program's version number and exit -c PATH, --config PATH Specify configuration file (default: None). Multiple @@ -100,6 +101,7 @@ optional arguments: --db-url PATH Override trades database URL, this is useful if dry_run is enabled or in custom deployments (default: None). + --sd-notify Notify systemd service manager. ``` ### Telegram RPC commands diff --git a/config_full.json.example b/config_full.json.example index 357a8f525..58d3d3ac6 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -39,12 +39,12 @@ "buy": "limit", "sell": "limit", "stoploss": "market", - "stoploss_on_exchange": "false", + "stoploss_on_exchange": false, "stoploss_on_exchange_interval": 60 }, "order_time_in_force": { "buy": "gtc", - "sell": "gtc", + "sell": "gtc" }, "pairlist": { "method": "VolumePairList", diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 35e4a776d..55988985a 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -6,9 +6,9 @@ This page explains the different parameters of the bot and how to run it. ## Bot commands ``` -usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] - [--strategy-path PATH] [--dynamic-whitelist [INT]] - [--db-url PATH] +usage: freqtrade [-h] [-v] [--logfile FILE] [--version] [-c PATH] [-d PATH] + [-s NAME] [--strategy-path PATH] [--dynamic-whitelist [INT]] + [--db-url PATH] [--sd-notify] {backtesting,edge,hyperopt} ... Free, open source crypto trading bot @@ -22,6 +22,7 @@ positional arguments: optional arguments: -h, --help show this help message and exit -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified --version show program's version number and exit -c PATH, --config PATH Specify configuration file (default: None). Multiple @@ -78,7 +79,7 @@ prevent unintended disclosure of sensitive private data when you publish example of your configuration in the project issues or in the Internet. See more details on this technique with examples in the documentation page on -[configuration](bot-configuration.md). +[configuration](configuration.md). ### How to use **--strategy**? diff --git a/docs/configuration.md b/docs/configuration.md index 11b941220..75843ef4a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -70,6 +70,7 @@ Mandatory Parameters are marked as **Required**. | `strategy` | DefaultStrategy | Defines Strategy class to use. | `strategy_path` | null | Adds an additional strategy lookup path (must be a folder). | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. +| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. ### Parameters in the strategy diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 1ca61e54a..4cc8eaa5c 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -65,16 +65,14 @@ Once all positions are sold, run `/stop` to completely stop the bot. For each open trade, the bot will send you the following message. -> **Trade ID:** `123` -> **Current Pair:** CVC/BTC -> **Open Since:** `1 days ago` -> **Amount:** `26.64180098` -> **Open Rate:** `0.00007489` -> **Close Rate:** `None` -> **Current Rate:** `0.00007489` -> **Close Profit:** `None` -> **Current Profit:** `12.95%` -> **Open Order:** `None` +> **Trade ID:** `123` `(since 1 days ago)` +> **Current Pair:** CVC/BTC +> **Open Since:** `1 days ago` +> **Amount:** `26.64180098` +> **Open Rate:** `0.00007489` +> **Current Rate:** `0.00007489` +> **Current Profit:** `12.95%` +> **Stoploss:** `0.00007389 (-0.02%)` ### /status table @@ -99,18 +97,18 @@ current max Return a summary of your profit/loss and performance. -> **ROI:** Close trades -> ∙ `0.00485701 BTC (258.45%)` -> ∙ `62.968 USD` -> **ROI:** All trades -> ∙ `0.00255280 BTC (143.43%)` -> ∙ `33.095 EUR` -> -> **Total Trade Count:** `138` -> **First Trade opened:** `3 days ago` -> **Latest Trade opened:** `2 minutes ago` -> **Avg. Duration:** `2:33:45` -> **Best Performing:** `PAY/BTC: 50.23%` +> **ROI:** Close trades +> ∙ `0.00485701 BTC (258.45%)` +> ∙ `62.968 USD` +> **ROI:** All trades +> ∙ `0.00255280 BTC (143.43%)` +> ∙ `33.095 EUR` +> +> **Total Trade Count:** `138` +> **First Trade opened:** `3 days ago` +> **Latest Trade opened:** `2 minutes ago` +> **Avg. Duration:** `2:33:45` +> **Best Performing:** `PAY/BTC: 50.23%` ### /forcesell @@ -128,26 +126,26 @@ Note that for this to work, `forcebuy_enable` needs to be set to true. Return the performance of each crypto-currency the bot has sold. > Performance: -> 1. `RCN/BTC 57.77%` -> 2. `PAY/BTC 56.91%` -> 3. `VIB/BTC 47.07%` -> 4. `SALT/BTC 30.24%` -> 5. `STORJ/BTC 27.24%` -> ... +> 1. `RCN/BTC 57.77%` +> 2. `PAY/BTC 56.91%` +> 3. `VIB/BTC 47.07%` +> 4. `SALT/BTC 30.24%` +> 5. `STORJ/BTC 27.24%` +> ... ### /balance Return the balance of all crypto-currency your have on the exchange. -> **Currency:** BTC -> **Available:** 3.05890234 -> **Balance:** 3.05890234 -> **Pending:** 0.0 +> **Currency:** BTC +> **Available:** 3.05890234 +> **Balance:** 3.05890234 +> **Pending:** 0.0 -> **Currency:** CVC -> **Available:** 86.64180098 -> **Balance:** 86.64180098 -> **Pending:** 0.0 +> **Currency:** CVC +> **Available:** 86.64180098 +> **Balance:** 86.64180098 +> **Pending:** 0.0 ### /daily diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 604386426..8d7dac4bc 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -71,6 +71,13 @@ class Arguments(object): dest='loglevel', default=0, ) + self.parser.add_argument( + '--logfile', + help='Log to the file specified', + dest='logfile', + type=str, + metavar='FILE' + ) self.parser.add_argument( '--version', action='version', diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index ba7a0e200..fdd71f2f5 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -4,16 +4,18 @@ This module contains the configuration class import json import logging import os +import sys from argparse import Namespace -from typing import Any, Dict, Optional +from logging.handlers import RotatingFileHandler +from typing import Any, Dict, List, Optional import ccxt from jsonschema import Draft4Validator, validate from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants -from freqtrade.state import RunMode from freqtrade.misc import deep_merge_dicts +from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -116,9 +118,22 @@ class Configuration(object): config.update({'verbosity': self.args.loglevel}) else: config.update({'verbosity': 0}) + + # Log to stdout, not stderr + log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)] + if 'logfile' in self.args and self.args.logfile: + config.update({'logfile': self.args.logfile}) + + # Allow setting this as either configuration or argument + if 'logfile' in config: + log_handlers.append(RotatingFileHandler(config['logfile'], + maxBytes=1024 * 1024, # 1Mb + backupCount=10)) + logging.basicConfig( level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=log_handlers ) set_loggers(config['verbosity']) logger.info('Verbosity set to %s', config['verbosity']) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6345dae56..4fe86bb8e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -367,7 +367,6 @@ class FreqtradeBot(object): stake_amount = order['cost'] amount = order['amount'] buy_limit_filled_price = order['price'] - order_id = None self.rpc.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, @@ -396,6 +395,10 @@ class FreqtradeBot(object): ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] ) + # Update fees if order is closed + if order_status == 'closed': + self.update_trade_state(trade, order) + Trade.session.add(trade) Trade.session.flush() @@ -426,24 +429,7 @@ class FreqtradeBot(object): :return: True if executed """ try: - # Get order details for actual price per unit - if trade.open_order_id: - # Update trade with order values - logger.info('Found open order for %s', trade) - order = self.exchange.get_order(trade.open_order_id, trade.pair) - # Try update amount (binance-fix) - try: - new_amount = self.get_real_amount(trade, order) - if order['amount'] != new_amount: - order['amount'] = new_amount - # Fee was applied, so set to 0 - trade.fee_open = 0 - - except OperationalException as exception: - logger.warning("Could not update trade amount: %s", exception) - - # This handles both buy and sell orders! - trade.update(order) + self.update_trade_state(trade) if self.strategy.order_types.get('stoploss_on_exchange') and trade.is_open: result = self.handle_stoploss_on_exchange(trade) @@ -508,6 +494,28 @@ class FreqtradeBot(object): f"(from {order_amount} to {real_amount}) from Trades") return real_amount + def update_trade_state(self, trade, action_order: dict = None): + """ + Checks trades with open orders and updates the amount if necessary + """ + # Get order details for actual price per unit + if trade.open_order_id: + # Update trade with order values + logger.info('Found open order for %s', trade) + order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) + # Try update amount (binance-fix) + try: + new_amount = self.get_real_amount(trade, order) + if order['amount'] != new_amount: + order['amount'] = new_amount + # Fee was applied, so set to 0 + trade.fee_open = 0 + + except OperationalException as exception: + logger.warning("Could not update trade amount: %s", exception) + + trade.update(order) + def get_sell_rate(self, pair: str, refresh: bool) -> float: """ Get sell rate - either using get-ticker bid or first bid based on orderbook @@ -578,43 +586,44 @@ class FreqtradeBot(object): """ result = False + try: + # If trade is open and the buy order is fulfilled but there is no stoploss, + # then we add a stoploss on exchange + if not trade.open_order_id and not trade.stoploss_order_id: + if self.edge: + stoploss = self.edge.stoploss(pair=trade.pair) + else: + stoploss = self.strategy.stoploss - # If trade is open and the buy order is fulfilled but there is no stoploss, - # then we add a stoploss on exchange - if not trade.open_order_id and not trade.stoploss_order_id: - if self.edge: - stoploss = self.edge.stoploss(pair=trade.pair) - else: - stoploss = self.strategy.stoploss + stop_price = trade.open_rate * (1 + stoploss) - stop_price = trade.open_rate * (1 + stoploss) + # limit price should be less than stop price. + # 0.99 is arbitrary here. + limit_price = stop_price * 0.99 - # limit price should be less than stop price. - # 0.99 is arbitrary here. - limit_price = stop_price * 0.99 - - stoploss_order_id = self.exchange.stoploss_limit( - pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price - )['id'] - trade.stoploss_order_id = str(stoploss_order_id) - trade.stoploss_last_update = datetime.now() - - # Or the trade open and there is already a stoploss on exchange. - # so we check if it is hit ... - elif trade.stoploss_order_id: - logger.debug('Handling stoploss on exchange %s ...', trade) - order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) - if order['status'] == 'closed': - trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value - trade.update(order) - self.notify_sell(trade) - result = True - elif self.config.get('trailing_stop', False): - # 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 - # value immediately - self.handle_trailing_stoploss_on_exchange(trade, order) + stoploss_order_id = self.exchange.stoploss_limit( + pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price + )['id'] + trade.stoploss_order_id = str(stoploss_order_id) + trade.stoploss_last_update = datetime.now() + # Or the trade open and there is already a stoploss on exchange. + # so we check if it is hit ... + elif trade.stoploss_order_id: + logger.debug('Handling stoploss on exchange %s ...', trade) + order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) + if order['status'] == 'closed': + trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value + trade.update(order) + self.notify_sell(trade) + result = True + elif self.config.get('trailing_stop', False): + # 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 + # value immediately + self.handle_trailing_stoploss_on_exchange(trade, order) + except DependencyException as exception: + logger.warning('Unable to create stoploss order: %s', exception) return result def handle_trailing_stoploss_on_exchange(self, trade: Trade, order): diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 10aff72ec..5a18a922a 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -83,7 +83,7 @@ def check_migrate(engine) -> None: logger.debug(f'trying {table_back_name}') # Check for latest column - if not has_column(cols, 'stoploss_last_update'): + if not has_column(cols, 'stop_loss_pct'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') @@ -91,10 +91,13 @@ def check_migrate(engine) -> None: open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null') close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') stop_loss = get_column_def(cols, 'stop_loss', '0.0') + stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null') initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') + initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null') stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null') stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null') max_rate = get_column_def(cols, 'max_rate', '0.0') + min_rate = get_column_def(cols, 'min_rate', 'null') sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') ticker_interval = get_column_def(cols, 'ticker_interval', 'null') @@ -112,8 +115,9 @@ def check_migrate(engine) -> None: (id, exchange, pair, is_open, fee_open, fee_close, open_rate, open_rate_requested, close_rate, close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, - stop_loss, initial_stop_loss, stoploss_order_id, stoploss_last_update, - max_rate, sell_reason, strategy, + stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, + stoploss_order_id, stoploss_last_update, + max_rate, min_rate, sell_reason, strategy, ticker_interval ) select id, lower(exchange), @@ -128,9 +132,11 @@ def check_migrate(engine) -> None: open_rate, {open_rate_requested} open_rate_requested, close_rate, {close_rate_requested} close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, - {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss, + {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct, + {initial_stop_loss} initial_stop_loss, + {initial_stop_loss_pct} initial_stop_loss_pct, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, - {max_rate} max_rate, {sell_reason} sell_reason, + {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {strategy} strategy, {ticker_interval} ticker_interval from {table_back_name} """) @@ -183,14 +189,20 @@ class Trade(_DECL_BASE): open_order_id = Column(String) # absolute value of the stop loss stop_loss = Column(Float, nullable=True, default=0.0) + # percentage value of the stop loss + stop_loss_pct = Column(Float, nullable=True) # absolute value of the initial stop loss initial_stop_loss = Column(Float, nullable=True, default=0.0) + # percentage value of the initial stop loss + initial_stop_loss_pct = Column(Float, nullable=True) # stoploss order id which is on exchange stoploss_order_id = Column(String, nullable=True, index=True) # last update time of the stoploss order on exchange stoploss_last_update = Column(DateTime, nullable=True) # absolute value of the highest reached price max_rate = Column(Float, nullable=True, default=0.0) + # Lowest price reached + min_rate = Column(Float, nullable=True) sell_reason = Column(String, nullable=True) strategy = Column(String, nullable=True) ticker_interval = Column(Integer, nullable=True) @@ -201,8 +213,22 @@ class Trade(_DECL_BASE): return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' f'open_rate={self.open_rate:.8f}, open_since={open_since})') + def adjust_min_max_rates(self, current_price: float): + """ + Adjust the max_rate and min_rate. + """ + logger.debug("Adjusting min/max rates") + self.max_rate = max(current_price, self.max_rate or self.open_rate) + self.min_rate = min(current_price, self.min_rate or self.open_rate) + def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False): - """this adjusts the stop loss to it's most recently observed setting""" + """ + This adjusts the stop loss to it's most recently observed setting + :param current_price: Current rate the asset is traded + :param stoploss: Stoploss as factor (sample -0.05 -> -5% below current price). + :param initial: Called to initiate stop_loss. + Skips everything if self.stop_loss is already set. + """ if initial and not (self.stop_loss is None or self.stop_loss == 0): # Don't modify if called with initial and nothing to do @@ -210,24 +236,20 @@ class Trade(_DECL_BASE): new_loss = float(current_price * (1 - abs(stoploss))) - # keeping track of the highest observed rate for this trade - if self.max_rate is None: - self.max_rate = current_price - else: - if current_price > self.max_rate: - self.max_rate = current_price - # no stop loss assigned yet if not self.stop_loss: logger.debug("assigning new stop loss") self.stop_loss = new_loss + self.stop_loss_pct = -1 * abs(stoploss) self.initial_stop_loss = new_loss + self.initial_stop_loss_pct = -1 * abs(stoploss) self.stoploss_last_update = datetime.utcnow() # evaluate if the stop loss needs to be updated else: if new_loss > self.stop_loss: # stop losses only walk up, never down! self.stop_loss = new_loss + self.stop_loss_pct = -1 * abs(stoploss) self.stoploss_last_update = datetime.utcnow() logger.debug("adjusted stop loss") else: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 1203ad7a6..5308c9d51 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -110,6 +110,10 @@ class RPC(object): amount=round(trade.amount, 8), close_profit=fmt_close_profit, current_profit=round(current_profit * 100, 2), + stop_loss=trade.stop_loss, + stop_loss_pct=trade.stop_loss_pct, + initial_stop_loss=trade.initial_stop_loss, + initial_stop_loss_pct=trade.initial_stop_loss_pct, open_order='({} {} rem={:.8f})'.format( order['type'], order['side'], order['remaining'] ) if order else None, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 06bf5efe9..2d822820f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -194,21 +194,34 @@ class Telegram(RPC): for result in results: result['date'] = result['date'].humanize() - messages = [ - "*Trade ID:* `{trade_id}`\n" - "*Current Pair:* {pair}\n" - "*Open Since:* `{date}`\n" - "*Amount:* `{amount}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Close Rate:* `{close_rate}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Profit:* `{close_profit}`\n" - "*Current Profit:* `{current_profit:.2f}%`\n" - "*Open Order:* `{open_order}`".format(**result) - for result in results - ] + messages = [] + for r in results: + lines = [ + "*Trade ID:* `{trade_id}` `(since {date})`", + "*Current Pair:* {pair}", + "*Amount:* `{amount}`", + "*Open Rate:* `{open_rate:.8f}`", + "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", + "*Current Rate:* `{current_rate:.8f}`", + "*Close Profit:* `{close_profit}`" if r['close_profit'] else "", + "*Current Profit:* `{current_profit:.2f}%`", + + # Adding initial stoploss only if it is different from stoploss + "*Initial Stoploss:* `{initial_stop_loss:.8f}` " + + ("`({initial_stop_loss_pct:.2f}%)`" if r['initial_stop_loss_pct'] else "") + if r['stop_loss'] != r['initial_stop_loss'] else "", + + # Adding stoploss and stoploss percentage only if it is not None + "*Stoploss:* `{stop_loss:.8f}` " + + ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""), + + "*Open Order:* `{open_order}`" if r['open_order'] else "" + ] + messages.append("\n".join(filter(None, lines)).format(**r)) + for msg in messages: self._send_msg(msg, bot=bot) + except RPCException as e: self._send_msg(str(e), bot=bot) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 41dcb8c57..fcb27d7bd 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -247,6 +247,9 @@ class IStrategy(ABC): """ This function evaluate if on the condition required to trigger a sell has been reached if the threshold is reached and updates the trade record. + :param low: Only used during backtesting to simulate stoploss + :param high: Only used during backtesting, to simulate ROI + :param force_stoploss: Externally provided stoploss :return: True if trade should be sold, False otherwise """ @@ -254,14 +257,16 @@ class IStrategy(ABC): current_rate = low or rate current_profit = trade.calc_profit_percent(current_rate) + trade.adjust_min_max_rates(high or current_rate) + stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, - force_stoploss=force_stoploss) + force_stoploss=force_stoploss, high=high) if stoplossflag.sell_flag: return stoplossflag - # Set current rate to low for backtesting sell + # Set current rate to high for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_percent(current_rate) experimental = self.config.get('experimental', {}) @@ -285,8 +290,9 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, - current_profit: float, force_stoploss: float) -> SellCheckTuple: + def stop_loss_reached(self, current_rate: float, trade: Trade, + current_time: datetime, current_profit: float, + force_stoploss: float, high: float = None) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not @@ -294,13 +300,33 @@ class IStrategy(ABC): """ trailing_stop = self.config.get('trailing_stop', False) - trade.adjust_stop_loss(trade.open_rate, force_stoploss if force_stoploss - else self.stoploss, initial=True) + stop_loss_value = force_stoploss if force_stoploss else self.stoploss + + # Initiate stoploss with open_rate. Does nothing if stoploss is already set. + trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) + + if trailing_stop: + # trailing stoploss handling + + sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 + tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) + + # Don't update stoploss if trailing_only_offset_is_reached is true. + if not (tsl_only_offset and current_profit < sl_offset): + # Specific handling for trailing_stop_positive + if 'trailing_stop_positive' in self.config and current_profit > sl_offset: + # Ignore mypy error check in configuration that this is a float + stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore + logger.debug(f"using positive stop loss: {stop_loss_value} " + f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%") + + trade.adjust_stop_loss(high or current_rate, stop_loss_value) # evaluate if the stoploss was hit if stoploss is not on exchange if ((self.stoploss is not None) and - (trade.stop_loss >= current_rate) and - (not self.order_types.get('stoploss_on_exchange'))): + (trade.stop_loss >= current_rate) and + (not self.order_types.get('stoploss_on_exchange'))): + selltype = SellType.STOP_LOSS # If Trailing stop (and max-rate did move above open rate) if trailing_stop and trade.open_rate != trade.max_rate: @@ -315,29 +341,6 @@ class IStrategy(ABC): logger.debug('Stop loss hit.') return SellCheckTuple(sell_flag=True, sell_type=selltype) - # update the stop loss afterwards, after all by definition it's supposed to be hanging - if trailing_stop: - - # check if we have a special stop loss for positive condition - # and if profit is positive - stop_loss_value = force_stoploss if force_stoploss else self.stoploss - - sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 - - if 'trailing_stop_positive' in self.config and current_profit > sl_offset: - - # Ignore mypy error check in configuration that this is a float - stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore - logger.debug(f"using positive stop loss mode: {stop_loss_value} " - f"with offset {sl_offset:.4g} " - f"since we have profit {current_profit:.4f}%") - - # if trailing_only_offset_is_reached is true, - # we update trailing stoploss only if offset is reached. - tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False) - if not (tsl_only_offset and current_profit < sl_offset): - trade.adjust_stop_loss(current_rate, stop_loss_value) - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 63309506d..0bff1d5e9 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -4,7 +4,6 @@ import logging import re from datetime import datetime from functools import reduce -from typing import Dict, Optional from unittest.mock import MagicMock, PropertyMock import arrow @@ -13,11 +12,11 @@ from telegram import Chat, Message, Update from freqtrade import constants from freqtrade.data.converter import parse_ticker_dataframe -from freqtrade.exchange import Exchange from freqtrade.edge import Edge, PairInfo -from freqtrade.worker import Worker +from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.resolvers import ExchangeResolver +from freqtrade.worker import Worker logging.getLogger('').setLevel(logging.INFO) @@ -96,7 +95,6 @@ def patch_freqtradebot(mocker, config) -> None: :param config: Config to pass to the bot :return: None """ - patch_coinmarketcap(mocker, {'price_usd': 12345.0}) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) patch_exchange(mocker, None) @@ -114,7 +112,8 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) -def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None: +@pytest.fixture(autouse=True) +def patch_coinmarketcap(mocker) -> None: """ Mocker to coinmarketcap to speed up tests :param mocker: mocker to patch coinmarketcap class diff --git a/freqtrade/tests/optimize/__init__.py b/freqtrade/tests/optimize/__init__.py index 129a09f40..075938a61 100644 --- a/freqtrade/tests/optimize/__init__.py +++ b/freqtrade/tests/optimize/__init__.py @@ -28,6 +28,7 @@ class BTContainer(NamedTuple): roi: float trades: List[BTrade] profit_perc: float + trailing_stop: bool = False def _get_frame_time_from_offset(offset): diff --git a/freqtrade/tests/optimize/test_backtest_detail.py b/freqtrade/tests/optimize/test_backtest_detail.py index e8514e76f..b98369533 100644 --- a/freqtrade/tests/optimize/test_backtest_detail.py +++ b/freqtrade/tests/optimize/test_backtest_detail.py @@ -14,10 +14,10 @@ from freqtrade.tests.optimize import (BTrade, BTContainer, _build_backtest_dataf from freqtrade.tests.conftest import patch_exchange -# Test 0 Minus 8% Close +# Test 1 Minus 8% Close # Test with Stop-loss at 1% # TC1: Stop-Loss Triggered 1% loss -tc0 = BTContainer(data=[ +tc1 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -30,10 +30,10 @@ tc0 = BTContainer(data=[ ) -# Test 1 Minus 4% Low, minus 1% close +# Test 2 Minus 4% Low, minus 1% close # Test with Stop-Loss at 3% # TC2: Stop-Loss Triggered 3% Loss -tc1 = BTContainer(data=[ +tc2 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -49,11 +49,10 @@ tc1 = BTContainer(data=[ # Test 3 Candle drops 4%, Recovers 1%. # Entry Criteria Met # Candle drops 20% -# Candle Data for test 3 # Test with Stop-Loss at 2% # TC3: Trade-A: Stop-Loss Triggered 2% Loss # Trade-B: Stop-Loss Triggered 2% Loss -tc2 = BTContainer(data=[ +tc3 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -71,7 +70,7 @@ tc2 = BTContainer(data=[ # Candle Data for test 3 – Candle drops 3% Closed 15% up # Test with Stop-loss at 2% ROI 6% # TC4: Stop-Loss Triggered 2% Loss -tc3 = BTContainer(data=[ +tc4 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -83,10 +82,10 @@ tc3 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=2)] ) -# Test 4 / Drops 0.5% Closes +20% +# Test 5 / Drops 0.5% Closes +20% # Set stop-loss at 1% ROI 3% # TC5: ROI triggers 3% Gain -tc4 = BTContainer(data=[ +tc5 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4980, 4987, 6172, 1, 0], [1, 5000, 5025, 4980, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -99,10 +98,9 @@ tc4 = BTContainer(data=[ ) # Test 6 / Drops 3% / Recovers 6% Positive / Closes 1% positve -# Candle Data for test 6 # Set stop-loss at 2% ROI at 5% # TC6: Stop-Loss triggers 2% Loss -tc5 = BTContainer(data=[ +tc6 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], # enter trade (signal on last candle) @@ -115,10 +113,9 @@ tc5 = BTContainer(data=[ ) # Test 7 - 6% Positive / 1% Negative / Close 1% Positve -# Candle Data for test 7 # Set stop-loss at 2% ROI at 3% # TC7: ROI Triggers 3% Gain -tc6 = BTContainer(data=[ +tc7 = BTContainer(data=[ # D O H L C V B S [0, 5000, 5025, 4975, 4987, 6172, 1, 0], [1, 5000, 5025, 4975, 4987, 6172, 0, 0], @@ -130,14 +127,47 @@ tc6 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] ) + +# Test 8 - trailing_stop should raise so candle 3 causes a stoploss. +# Set stop-loss at 10%, ROI at 10% (should not apply) +# TC8: Trailing stoploss - stoploss should be adjusted candle 2 +tc8 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5000, 6172, 0, 0], + [2, 5000, 5250, 4750, 4850, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.10, profit_perc=-0.055, trailing_stop=True, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] +) + + +# Test 9 - trailing_stop should raise - high and low in same candle. +# Candle Data for test 9 +# Set stop-loss at 10%, ROI at 10% (should not apply) +# TC9: Trailing stoploss - stoploss should be adjusted candle 2 +tc9 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5000, 6172, 0, 0], + [2, 5000, 5050, 4950, 5000, 6172, 0, 0], + [3, 5000, 5200, 4550, 4850, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi=0.10, profit_perc=-0.064, trailing_stop=True, + trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=3)] +) + TESTS = [ - tc0, tc1, tc2, tc3, tc4, tc5, tc6, + tc7, + tc8, + tc9, ] @@ -148,8 +178,9 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: """ default_conf["stoploss"] = data.stop_loss default_conf["minimal_roi"] = {"0": data.roi} - default_conf['ticker_interval'] = tests_ticker_interval - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.0)) + default_conf["ticker_interval"] = tests_ticker_interval + default_conf["trailing_stop"] = data.trailing_stop + mocker.patch("freqtrade.exchange.Exchange.get_fee", MagicMock(return_value=0.0)) patch_exchange(mocker) frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) @@ -157,7 +188,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: backtesting.advise_sell = lambda a, m: frame caplog.set_level(logging.DEBUG) - pair = 'UNITTEST/BTC' + pair = "UNITTEST/BTC" # Dummy data as we mock the analyze functions data_processed = {pair: DataFrame()} min_date, max_date = get_timeframe({pair: frame}) diff --git a/freqtrade/tests/rpc/test_fiat_convert.py b/freqtrade/tests/rpc/test_fiat_convert.py index fbc942432..66870efcc 100644 --- a/freqtrade/tests/rpc/test_fiat_convert.py +++ b/freqtrade/tests/rpc/test_fiat_convert.py @@ -8,7 +8,7 @@ import pytest from requests.exceptions import RequestException from freqtrade.rpc.fiat_convert import CryptoFiat, CryptoToFiatConverter -from freqtrade.tests.conftest import log_has, patch_coinmarketcap +from freqtrade.tests.conftest import log_has def test_pair_convertion_object(): @@ -40,7 +40,6 @@ def test_pair_convertion_object(): def test_fiat_convert_is_supported(mocker): - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert fiat_convert._is_supported_fiat(fiat='USD') is True assert fiat_convert._is_supported_fiat(fiat='usd') is True @@ -49,7 +48,6 @@ def test_fiat_convert_is_supported(mocker): def test_fiat_convert_add_pair(mocker): - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() @@ -72,8 +70,6 @@ def test_fiat_convert_add_pair(mocker): def test_fiat_convert_find_price(mocker): - patch_coinmarketcap(mocker) - fiat_convert = CryptoToFiatConverter() with pytest.raises(ValueError, match=r'The fiat ABC is not supported.'): @@ -93,15 +89,12 @@ def test_fiat_convert_find_price(mocker): def test_fiat_convert_unsupported_crypto(mocker, caplog): mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[]) - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0 assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples) def test_fiat_convert_get_price(mocker): - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=28000.0) @@ -134,21 +127,18 @@ def test_fiat_convert_get_price(mocker): def test_fiat_convert_same_currencies(mocker): - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='USD') == 1.0 def test_fiat_convert_two_FIAT(mocker): - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='EUR') == 0.0 def test_loadcryptomap(mocker): - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() assert len(fiat_convert._cryptomap) == 2 @@ -174,7 +164,6 @@ def test_fiat_init_network_exception(mocker): def test_fiat_convert_without_network(mocker): # Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap - patch_coinmarketcap(mocker) fiat_convert = CryptoToFiatConverter() @@ -205,7 +194,6 @@ def test_fiat_invalid_response(mocker, caplog): def test_convert_amount(mocker): - patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0) fiat_convert = CryptoToFiatConverter() diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index b7b041695..4a3cafbd2 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -14,7 +14,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from freqtrade.tests.conftest import patch_coinmarketcap, patch_exchange +from freqtrade.tests.conftest import patch_exchange from freqtrade.tests.test_freqtradebot import patch_get_signal @@ -28,7 +28,6 @@ def prec_satoshi(a, b) -> float: # Unit tests def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -60,6 +59,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: 'amount': 90.99181074, 'close_profit': None, 'current_profit': -0.59, + 'stop_loss': 0.0, + 'initial_stop_loss': 0.0, + 'initial_stop_loss_pct': None, + 'stop_loss_pct': None, 'open_order': '(limit buy rem=0.00000000)' } == results[0] @@ -80,12 +83,15 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: 'amount': 90.99181074, 'close_profit': None, 'current_profit': ANY, + 'stop_loss': 0.0, + 'initial_stop_loss': 0.0, + 'initial_stop_loss_pct': None, + 'stop_loss_pct': None, 'open_order': '(limit buy rem=0.00000000)' } == results[0] def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -122,7 +128,6 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: def test_rpc_daily_profit(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -176,7 +181,6 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -336,7 +340,6 @@ def test_rpc_balance_handle(default_conf, mocker): 'freqtrade.rpc.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -367,7 +370,6 @@ def test_rpc_balance_handle(default_conf, mocker): def test_rpc_start(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -391,7 +393,6 @@ def test_rpc_start(mocker, default_conf) -> None: def test_rpc_stop(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -416,7 +417,6 @@ def test_rpc_stop(mocker, default_conf) -> None: def test_rpc_stopbuy(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -437,7 +437,6 @@ def test_rpc_stopbuy(mocker, default_conf) -> None: def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -539,7 +538,6 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: def test_performance_handle(default_conf, ticker, limit_buy_order, fee, limit_sell_order, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -576,7 +574,6 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -605,7 +602,6 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order) -> None: default_conf['forcebuy_enable'] = True - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -657,7 +653,6 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, markets, limit_buy_order def test_rpcforcebuy_stopped(mocker, default_conf) -> None: default_conf['forcebuy_enable'] = True default_conf['initial_state'] = 'stopped' - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -671,7 +666,6 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None: def test_rpcforcebuy_disabled(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -685,7 +679,6 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None: def test_rpc_whitelist(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -698,7 +691,6 @@ def test_rpc_whitelist(mocker, default_conf) -> None: def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) default_conf['pairlist'] = {'method': 'VolumePairList', 'config': {'number_assets': 4} @@ -716,7 +708,6 @@ def test_rpc_whitelist_dynamic(mocker, default_conf) -> None: def test_rpc_blacklist(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -736,7 +727,6 @@ def test_rpc_blacklist(mocker, default_conf) -> None: def test_rpc_edge_disabled(mocker, default_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) freqtradebot = FreqtradeBot(default_conf) @@ -746,7 +736,6 @@ def test_rpc_edge_disabled(mocker, default_conf) -> None: def test_rpc_edge_enabled(mocker, edge_conf) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index c0b77076f..c3ab5064c 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -20,8 +20,7 @@ from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType -from freqtrade.tests.conftest import (get_patched_freqtradebot, - log_has, patch_coinmarketcap, patch_exchange) +from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange) from freqtrade.tests.test_freqtradebot import patch_get_signal @@ -90,7 +89,6 @@ def test_cleanup(default_conf, mocker) -> None: def test_authorized_only(default_conf, mocker, caplog) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker, None) chat = Chat(0, 0) @@ -120,7 +118,6 @@ def test_authorized_only(default_conf, mocker, caplog) -> None: def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker, None) chat = Chat(0xdeadbeef, 0) update = Update(randint(1, 100)) @@ -149,7 +146,6 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: def test_authorized_only_exception(default_conf, mocker, caplog) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) update = Update(randint(1, 100)) @@ -183,7 +179,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: default_conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = 123 - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -206,6 +201,10 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: 'amount': 90.99181074, 'close_profit': None, 'current_profit': -0.59, + 'initial_stop_loss': 1.098e-05, + 'stop_loss': 1.099e-05, + 'initial_stop_loss_pct': -0.05, + 'stop_loss_pct': -0.01, 'open_order': '(limit buy rem=0.00000000)' }]), _status_table=status_table, @@ -232,7 +231,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -274,12 +272,18 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No # Trigger status while we have a fulfilled order for the open trade telegram._status(bot=MagicMock(), update=update) + # close_rate should not be included in the message as the trade is not closed + # and no line should be empty + lines = msg_mock.call_args_list[0][0][0].split('\n') + assert '' not in lines + assert 'Close Rate' not in ''.join(lines) + assert 'Close Profit' not in ''.join(lines) + assert msg_mock.call_count == 1 assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -333,7 +337,6 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, limit_sell_order, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_exchange(mocker) mocker.patch( 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', @@ -405,7 +408,6 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -442,7 +444,6 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_exchange(mocker) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( @@ -548,7 +549,6 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: 'last': 0.1, } - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) @@ -635,7 +635,6 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None: def test_stop_handle(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -655,7 +654,6 @@ def test_stop_handle(default_conf, update, mocker) -> None: def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -675,7 +673,6 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: def test_stopbuy_handle(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -695,7 +692,6 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None: def test_reload_conf_handle(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -716,7 +712,6 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) @@ -767,7 +762,6 @@ def test_forcesell_handle(default_conf, update, ticker, fee, def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_down, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -822,7 +816,6 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_exchange(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) @@ -869,7 +862,6 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = MagicMock() @@ -910,7 +902,6 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: def test_forcebuy_handle(default_conf, update, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) @@ -948,7 +939,6 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None: def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> None: - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) @@ -973,7 +963,6 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non def test_performance_handle(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -1015,7 +1004,6 @@ def test_performance_handle(default_conf, update, ticker, fee, def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - patch_coinmarketcap(mocker) patch_exchange(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -1058,7 +1046,6 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non def test_whitelist_static(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1076,7 +1063,6 @@ def test_whitelist_static(default_conf, update, mocker) -> None: def test_whitelist_dynamic(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1098,7 +1084,6 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: def test_blacklist_static(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1123,7 +1108,6 @@ def test_blacklist_static(default_conf, update, mocker) -> None: def test_edge_disabled(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1141,7 +1125,6 @@ def test_edge_disabled(default_conf, update, mocker) -> None: def test_edge_enabled(edge_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ @@ -1165,7 +1148,6 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: def test_help_handle(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1182,7 +1164,6 @@ def test_help_handle(default_conf, update, mocker) -> None: def test_version_handle(default_conf, update, mocker) -> None: - patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1409,7 +1390,6 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: def test__send_msg(default_conf, mocker) -> None: - patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -1421,7 +1401,6 @@ def test__send_msg(default_conf, mocker) -> None: def test__send_msg_network_error(default_conf, mocker, caplog) -> None: - patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index 21547d205..45e539c2f 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -5,6 +5,7 @@ import logging from argparse import Namespace from copy import deepcopy from unittest.mock import MagicMock +from pathlib import Path import pytest from jsonschema import Draft4Validator, ValidationError, validate @@ -547,6 +548,23 @@ def test_set_loggers() -> None: assert logging.getLogger('telegram').level is logging.INFO +def test_set_logfile(default_conf, mocker): + mocker.patch('freqtrade.configuration.open', + mocker.mock_open(read_data=json.dumps(default_conf))) + + arglist = [ + '--logfile', 'test_file.log', + ] + args = Arguments(arglist, '').get_parsed_arg() + configuration = Configuration(args) + validated_conf = configuration.load_config() + + assert validated_conf['logfile'] == "test_file.log" + f = Path("test_file.log") + assert f.is_file() + f.unlink() + + def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: default_conf['forcebuy_enable'] = True mocker.patch('freqtrade.configuration.open', mocker.mock_open( diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 250e43c3b..6790f513c 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1062,6 +1062,13 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, assert trade.stoploss_order_id is None assert trade.is_open is False + mocker.patch( + 'freqtrade.exchange.Exchange.stoploss_limit', + side_effect=DependencyException() + ) + freqtrade.handle_stoploss_on_exchange(trade) + assert log_has('Unable to create stoploss order: ', caplog.record_tuples) + def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, markets, limit_buy_order, limit_sell_order) -> None: @@ -1312,17 +1319,84 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo # test amount modified by fee-logic assert not freqtrade.process_maybe_execute_sell(trade) + +def test_process_maybe_execute_sell_exception(mocker, default_conf, + limit_buy_order, caplog) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + + trade = MagicMock() + trade.open_order_id = '123' + trade.open_fee = 0.001 + + # Test raise of DependencyException exception + mocker.patch( + 'freqtrade.freqtradebot.FreqtradeBot.update_trade_state', + side_effect=DependencyException() + ) + freqtrade.process_maybe_execute_sell(trade) + assert log_has('Unable to sell trade: ', caplog.record_tuples) + + +def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', + return_value=limit_buy_order['amount']) + + trade = Trade() + # Mock session away + Trade.session = MagicMock() + trade.open_order_id = '123' + trade.open_fee = 0.001 + freqtrade.update_trade_state(trade) + # Test amount not modified by fee-logic + assert not log_has_re(r'Applying fee to .*', caplog.record_tuples) + assert trade.open_order_id is None + assert trade.amount == limit_buy_order['amount'] + + trade.open_order_id = '123' + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) + assert trade.amount != 90.81 + # test amount modified by fee-logic + freqtrade.update_trade_state(trade) + assert trade.amount == 90.81 + assert trade.open_order_id is None + trade.is_open = True trade.open_order_id = None # Assert we call handle_trade() if trade is feasible for execution - assert freqtrade.process_maybe_execute_sell(trade) + freqtrade.update_trade_state(trade) regexp = re.compile('Found open order for.*') assert filter(regexp.match, caplog.record_tuples) -def test_process_maybe_execute_sell_exception(mocker, default_conf, - limit_buy_order, caplog) -> None: +def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker): + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) + # get_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + patch_exchange(mocker) + Trade.session = MagicMock() + 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, + open_order_id="123456" + ) + freqtrade.update_trade_state(trade, limit_buy_order) + assert trade.amount != amount + assert trade.amount == limit_buy_order['amount'] + + +def test_update_trade_state_exception(mocker, default_conf, + limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) @@ -1335,17 +1409,9 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf, 'freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=OperationalException() ) - freqtrade.process_maybe_execute_sell(trade) + freqtrade.update_trade_state(trade) assert log_has('Could not update trade amount: ', caplog.record_tuples) - # Test raise of DependencyException exception - mocker.patch( - 'freqtrade.freqtradebot.FreqtradeBot.get_real_amount', - side_effect=DependencyException() - ) - freqtrade.process_maybe_execute_sell(trade) - assert log_has('Unable to sell trade: ', caplog.record_tuples) - def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, markets, mocker) -> None: @@ -2296,9 +2362,8 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market } freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.stop_loss_reached = \ - lambda current_rate, trade, current_time, force_stoploss, current_profit: SellCheckTuple( - sell_flag=False, sell_type=SellType.NONE) + freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( + sell_flag=False, sell_type=SellType.NONE)) freqtrade.create_trade() trade = Trade.query.first() @@ -2444,8 +2509,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets })) # stop-loss not reached, adjusted stoploss assert freqtrade.handle_trade(trade) is False - assert log_has(f'using positive stop loss mode: 0.01 with offset 0 ' - f'since we have profit 0.2666%', + assert log_has(f'using positive stop loss: 0.01 offset: 0 profit: 0.2666%', caplog.record_tuples) assert log_has(f'adjusted stop loss', caplog.record_tuples) assert trade.stop_loss == 0.0000138501 @@ -2504,8 +2568,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, })) # stop-loss not reached, adjusted stoploss assert freqtrade.handle_trade(trade) is False - assert log_has(f'using positive stop loss mode: 0.01 with offset 0.011 ' - f'since we have profit 0.2666%', + assert log_has(f'using positive stop loss: 0.01 offset: 0.011 profit: 0.2666%', caplog.record_tuples) assert log_has(f'adjusted stop loss', caplog.record_tuples) assert trade.stop_loss == 0.0000138501 @@ -2584,8 +2647,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, })) assert freqtrade.handle_trade(trade) is False - assert log_has(f'using positive stop loss mode: 0.05 with offset 0.055 ' - f'since we have profit 0.1218%', + assert log_has(f'using positive stop loss: 0.05 offset: 0.055 profit: 0.1218%', caplog.record_tuples) assert log_has(f'adjusted stop loss', caplog.record_tuples) assert trade.stop_loss == 0.0000117705 diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index a9519e693..f57a466e3 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -510,6 +510,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.pair == "ETC/BTC" assert trade.exchange == "binance" assert trade.max_rate == 0.0 + assert trade.min_rate is None assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 assert trade.sell_reason is None @@ -585,7 +586,58 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): caplog.record_tuples) -def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): +def test_adjust_stop_loss(fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='bittrex', + open_rate=1, + max_rate=1, + ) + + trade.adjust_stop_loss(trade.open_rate, 0.05, True) + assert trade.stop_loss == 0.95 + assert trade.stop_loss_pct == -0.05 + assert trade.initial_stop_loss == 0.95 + assert trade.initial_stop_loss_pct == -0.05 + + # Get percent of profit with a lower rate + trade.adjust_stop_loss(0.96, 0.05) + assert trade.stop_loss == 0.95 + assert trade.stop_loss_pct == -0.05 + assert trade.initial_stop_loss == 0.95 + assert trade.initial_stop_loss_pct == -0.05 + + # Get percent of profit with a custom rate (Higher than open rate) + trade.adjust_stop_loss(1.3, -0.1) + assert round(trade.stop_loss, 8) == 1.17 + assert trade.stop_loss_pct == -0.1 + assert trade.initial_stop_loss == 0.95 + assert trade.initial_stop_loss_pct == -0.05 + + # current rate lower again ... should not change + trade.adjust_stop_loss(1.2, 0.1) + assert round(trade.stop_loss, 8) == 1.17 + assert trade.initial_stop_loss == 0.95 + assert trade.initial_stop_loss_pct == -0.05 + + # current rate higher... should raise stoploss + trade.adjust_stop_loss(1.4, 0.1) + assert round(trade.stop_loss, 8) == 1.26 + assert trade.initial_stop_loss == 0.95 + assert trade.initial_stop_loss_pct == -0.05 + + # Initial is true but stop_loss set - so doesn't do anything + trade.adjust_stop_loss(1.7, 0.1, True) + assert round(trade.stop_loss, 8) == 1.26 + assert trade.initial_stop_loss == 0.95 + assert trade.initial_stop_loss_pct == -0.05 + assert trade.stop_loss_pct == -0.1 + + +def test_adjust_min_max_rates(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -595,40 +647,24 @@ def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): open_rate=1, ) - trade.adjust_stop_loss(trade.open_rate, 0.05, True) - assert trade.stop_loss == 0.95 + trade.adjust_min_max_rates(trade.open_rate) assert trade.max_rate == 1 - assert trade.initial_stop_loss == 0.95 + assert trade.min_rate == 1 - # Get percent of profit with a lowre rate - trade.adjust_stop_loss(0.96, 0.05) - assert trade.stop_loss == 0.95 + # check min adjusted, max remained + trade.adjust_min_max_rates(0.96) assert trade.max_rate == 1 - assert trade.initial_stop_loss == 0.95 + assert trade.min_rate == 0.96 - # Get percent of profit with a custom rate (Higher than open rate) - trade.adjust_stop_loss(1.3, -0.1) - assert round(trade.stop_loss, 8) == 1.17 - assert trade.max_rate == 1.3 - assert trade.initial_stop_loss == 0.95 + # check max adjusted, min remains + trade.adjust_min_max_rates(1.05) + assert trade.max_rate == 1.05 + assert trade.min_rate == 0.96 - # current rate lower again ... should not change - trade.adjust_stop_loss(1.2, 0.1) - assert round(trade.stop_loss, 8) == 1.17 - assert trade.max_rate == 1.3 - assert trade.initial_stop_loss == 0.95 - - # current rate higher... should raise stoploss - trade.adjust_stop_loss(1.4, 0.1) - assert round(trade.stop_loss, 8) == 1.26 - assert trade.max_rate == 1.4 - assert trade.initial_stop_loss == 0.95 - - # Initial is true but stop_loss set - so doesn't do anything - trade.adjust_stop_loss(1.7, 0.1, True) - assert round(trade.stop_loss, 8) == 1.26 - assert trade.max_rate == 1.4 - assert trade.initial_stop_loss == 0.95 + # current rate "in the middle" - no adjustment + trade.adjust_min_max_rates(1.03) + assert trade.max_rate == 1.05 + assert trade.min_rate == 0.96 def test_get_open(default_conf, fee): diff --git a/requirements-dev.txt b/requirements-dev.txt index 56d7964c3..69082587a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,8 +4,8 @@ flake8==3.7.7 flake8-type-annotations==0.1.0 flake8-tidy-imports==2.0.0 -pytest==4.3.1 -pytest-mock==1.10.2 +pytest==4.4.0 +pytest-mock==1.10.3 pytest-asyncio==0.10.0 pytest-cov==2.6.1 coveralls==1.7.0 diff --git a/requirements.txt b/requirements.txt index 2bf329bd8..83d77b693 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -ccxt==1.18.406 +ccxt==1.18.425 SQLAlchemy==1.3.1 python-telegram-bot==11.1.0 arrow==0.13.1