diff --git a/.travis.yml b/.travis.yml index 57265fd40..3dfcf6111 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ addons: install: - cd build_helpers && ./install_ta-lib.sh; cd .. - export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH -- pip install --upgrade flake8 coveralls pytest-random-order pytest-asyncio mypy +- pip install --upgrade pytest-random-order - pip install -r requirements-dev.txt - pip install -e . jobs: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9a967834..3c511f44d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,6 +14,10 @@ Few pointers for contributions: If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. +## Getting started + +Best start by reading the [documentation](https://www.freqtrade.io/) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://www.freqtrade.io/en/latest/developer/) (WIP) which should help you getting started. + ## Before sending the PR: ### 1. Run unit tests @@ -41,12 +45,6 @@ pytest freqtrade/tests/test_.py::test_ ### 2. Test if your code is PEP8 compliant -#### Install packages - -```bash -pip3.6 install flake8 coveralls -``` - #### Run Flake8 ```bash @@ -60,22 +58,12 @@ Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using ### 3. Test if all type-hints are correct -#### Install packages - -``` bash -pip3.6 install mypy -``` - #### Run mypy ``` bash mypy freqtrade ``` -## Getting started - -Best start by reading the [documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/developer.md) (WIP) which should help you getting started. - ## (Core)-Committer Guide ### Process: Pull Requests diff --git a/config_full.json.example b/config_full.json.example index 0427f8700..234722f82 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -37,7 +37,8 @@ "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", diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index 1cfae1bc4..1c622737f 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -216,12 +216,40 @@ This is the set of candles the bot should download and use for the analysis. Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work. Please note that the same buy/sell signals may work with one interval, but not the other. +This setting is accessible within the strategy by using `self.ticker_interval`. ### Metadata dict The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information. Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`. +The Metadata-dict should not be modified and does not persist information across multiple calls. +Instead, have a look at the section [Storing information](#Storing-information) + +### Storing information + +Storing information can be accomplished by crating a new dictionary within the strategy class. + +The name of the variable can be choosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. + +```python +class Awesomestrategy(IStrategy): + # Create custom dictionary + cust_info = {} + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Check if the entry already exists + if "crosstime" in self.cust_info[metadata["pair"]: + self.cust_info[metadata["pair"]["crosstime"] += 1 + else: + self.cust_info[metadata["pair"]["crosstime"] = 1 +``` + +!!! Warning: + The data is not persisted after a bot-restart (or config-reload). Also, the amount of data should be kept smallish (no DataFrames and such), otherwise the bot will start to consume a lot of memory and eventually run out of memory and crash. + +!!! Note: + If the data is pair-specific, make sure to use pair as one of the keys in the dictionary. + ### Where is the default strategy? The default buy strategy is located in the file diff --git a/docs/configuration.md b/docs/configuration.md index a7deaa60c..95bf3b1ff 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -13,7 +13,7 @@ The table below will list all configuration parameters. |----------|---------|-----------|-------------| | `max_open_trades` | 3 | Yes | Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) | `stake_currency` | BTC | Yes | Crypto-currency used for trading. -| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to 'unlimited' to allow the bot to use all avaliable balance. +| `stake_amount` | 0.05 | Yes | Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all avaliable balance. | `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes | `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below. | `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode. @@ -144,10 +144,10 @@ end up paying more then would probably have been necessary. ### Understand order_types -`order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled. +`order_types` contains a dict mapping order-types to market-types as well as stoploss on or off exchange type and stoploss on exchange update interval in seconds. This allows to buy using limit orders, sell using limit-orders, and create stoploss orders using market. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled. In case stoploss on exchange and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check it periodically and update it if necessary (e.x. in case of trailing stoploss). This can be set in the configuration or in the strategy. Configuration overwrites strategy configurations. -If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start. +If this is configured, all 4 values (`"buy"`, `"sell"`, `"stoploss"` and `"stoploss_on_exchange"`) need to be present, otherwise the bot warn about it and will fail to start. The below is the default which is used if this is not configured in either Strategy or configuration. ```python @@ -155,7 +155,8 @@ The below is the default which is used if this is not configured in either Strat "buy": "limit", "sell": "limit", "stoploss": "market", - "stoploss_on_exchange": False + "stoploss_on_exchange": False, + "stoploss_on_exchange_interval": 60 }, ``` @@ -163,6 +164,9 @@ The below is the default which is used if this is not configured in either Strat Not all exchanges support "market" orders. The following message will be shown if your exchange does not support market orders: `"Exchange does not support market orders."` +!!! Note + stoploss on exchange interval is not mandatory. Do not change it's value if you are unsure of what you are doing. For more information about how stoploss works please read [the stoploss documentation](stoploss.md). + ### Understand order_time_in_force `order_time_in_force` defines the policy by which the order is executed on the exchange. Three commonly used time in force are:
**GTC (Goog Till Canceled):** diff --git a/docs/developer.md b/docs/developer.md index 64f3b9a52..6fbcdc812 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -4,8 +4,20 @@ This page is intended for developers of FreqTrade, people who want to contribute All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE) where you can ask questions. +## Documentation -## Module +Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR. + +Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/extensions/admonition/). + +## Developer setup + +To configure a development environment, use best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". +Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -r requirements-dev.txt`. + +This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. + +## Modules ### Dynamic Pairlist diff --git a/docs/edge.md b/docs/edge.md index 61abf354b..b208cb318 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -24,7 +24,7 @@ The answer comes to two factors: Means over X trades what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only If you won or not). -`W = (Number of winning trades) / (Number of losing trades)` +`W = (Number of winning trades) / (Total number of trades)` ### Risk Reward Ratio Risk Reward Ratio is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose: @@ -209,4 +209,4 @@ The full timerange specification: * Use tickframes till 2018/01/31: --timerange=-20180131 * Use tickframes since 2018/01/31: --timerange=20180131- * Use tickframes since 2018/01/31 till 2018/03/01 : --timerange=20180131-20180301 -* Use tickframes between POSIX timestamps 1527595200 1527618600: --timerange=1527595200-1527618600 \ No newline at end of file +* Use tickframes between POSIX timestamps 1527595200 1527618600: --timerange=1527595200-1527618600 diff --git a/docs/stoploss.md b/docs/stoploss.md index 0278e7bbb..0726aebbc 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -2,12 +2,20 @@ At this stage the bot contains the following stoploss support modes: -1. static stop loss, defined in either the strategy or configuration -2. trailing stop loss, defined in the configuration -3. trailing stop loss, custom positive loss, defined in configuration +1. static stop loss, defined in either the strategy or configuration. +2. trailing stop loss, defined in the configuration. +3. trailing stop loss, custom positive loss, defined in configuration. !!! Note - All stoploss properties can be configured in eihter Strategy or configuration. Configuration values override strategy values. + All stoploss properties can be configured in either Strategy or configuration. Configuration values override strategy values. + +Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfuly. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. + +In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. As an example in case of trailing stoploss if the order is on the exchange and the market is going up then the bot automatically cancels the previous stoploss order and put a new one with a stop value higher than previous one. It is clear that the bot cannot do it every 5 seconds otherwise it gets banned. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). + +!!! Note + Stoploss on exchange is only supported for Binance as of now. + ## Static Stop Loss diff --git a/freqtrade/constants.py b/freqtrade/constants.py index b2393c2b7..8fbcdfed7 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -112,7 +112,8 @@ CONF_SCHEMA = { 'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, - 'stoploss_on_exchange': {'type': 'boolean'} + 'stoploss_on_exchange': {'type': 'boolean'}, + 'stoploss_on_exchange_interval': {'type': 'number'} }, 'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] }, @@ -137,7 +138,7 @@ CONF_SCHEMA = { 'pairlist': { 'type': 'object', 'properties': { - 'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS}, + 'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS}, 'config': {'type': 'object'} }, 'required': ['method'] diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index c74b32ad2..e0e4d7723 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -402,8 +402,11 @@ class Exchange(object): return self._dry_run_open_orders[order_id] try: - return self._api.create_order(pair, 'stop_loss_limit', 'sell', - amount, rate, {'stopPrice': stop_price}) + order = self._api.create_order(pair, 'stop_loss_limit', 'sell', + amount, rate, {'stopPrice': stop_price}) + logger.info('stoploss limit order added for %s. ' + 'stop price: %s. limit: %s' % (pair, stop_price, rate)) + return order except ccxt.InsufficientFunds as e: raise DependencyException( @@ -553,12 +556,17 @@ class Exchange(object): tickers = await asyncio.gather(*input_coroutines, return_exceptions=True) # handle caching - for pair, ticks in tickers: + for res in tickers: + if isinstance(res, Exception): + logger.warning("Async code raised an exception: %s", res.__class__.__name__) + continue + pair = res[0] + ticks = res[1] # keeping last candle time as last refreshed time of the pair if ticks: self._pairs_last_refresh_time[pair] = ticks[-1][0] // 1000 - # keeping parsed dataframe in cache - self._klines[pair] = parse_ticker_dataframe(ticks, tick_interval, fill_missing=True) + # keeping parsed dataframe in cache + self._klines[pair] = parse_ticker_dataframe(ticks, tick_interval, fill_missing=True) return tickers @retrier_async @@ -575,9 +583,12 @@ class Exchange(object): # Ex: Bittrex returns a list of tickers ASC (oldest first, newest last) # when GDAX returns a list of tickers DESC (newest first, oldest last) # Only sort if necessary to save computing time - if data and data[0][0] > data[-1][0]: - data = sorted(data, key=lambda x: x[0]) - + try: + if data and data[0][0] > data[-1][0]: + data = sorted(data, key=lambda x: x[0]) + except IndexError: + logger.exception("Error loading %s. Result was %s.", pair, data) + return pair, [] logger.debug("done fetching %s ...", pair) return pair, data diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4febe9dd0..7d14c734c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -34,7 +34,7 @@ class FreqtradeBot(object): This is from here the bot start its logic. """ - def __init__(self, config: Dict[str, Any])-> None: + def __init__(self, config: Dict[str, Any]) -> None: """ Init all variables and object the bot need to work :param config: configuration dict, you can use the Configuration.get_config() @@ -601,7 +601,7 @@ class FreqtradeBot(object): if self.check_sell(trade, sell_rate, buy, sell): return True - break + else: logger.debug('checking sell') if self.check_sell(trade, sell_rate, buy, sell): @@ -613,7 +613,7 @@ class FreqtradeBot(object): def handle_stoploss_on_exchange(self, trade: Trade) -> bool: """ Check if trade is fulfilled in which case the stoploss - on exchange should be added immediately if stoploss on exchnage + on exchange should be added immediately if stoploss on exchange is enabled. """ @@ -630,13 +630,14 @@ class FreqtradeBot(object): stop_price = trade.open_rate * (1 + stoploss) # limit price should be less than stop price. - # 0.98 is arbitrary here. - limit_price = stop_price * 0.98 + # 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 ... @@ -647,10 +648,38 @@ class FreqtradeBot(object): trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value trade.update(order) result = True - else: - result = False + 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) + return result + def handle_trailing_stoploss_on_exchange(self, trade: Trade, order): + """ + Check to see if stoploss on exchange should be updated + in case of trailing stoploss on exchange + :param Trade: Corresponding Trade + :param order: Current on exchange stoploss order + :return: None + """ + + if trade.stop_loss > float(order['info']['stopPrice']): + # we check if the update is neccesary + update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) + if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() > update_beat: + # cancelling the current stoploss on exchange first + logger.info('Trailing stoploss: cancelling current stoploss on exchange ' + 'in order to add another one ...') + if self.exchange.cancel_order(order['id'], trade.pair): + # creating the new one + stoploss_order_id = self.exchange.stoploss_limit( + pair=trade.pair, amount=trade.amount, + stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99 + )['id'] + trade.stoploss_order_id = str(stoploss_order_id) + def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool: if self.edge: stoploss = self.edge.stoploss(trade.pair) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e96a91856..37ba2ad83 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -13,7 +13,7 @@ from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame from tabulate import tabulate -import freqtrade.optimize as optimize +from freqtrade import optimize from freqtrade import DependencyException, constants from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration @@ -134,7 +134,9 @@ class Backtesting(object): len(results[results.profit_abs > 0]), len(results[results.profit_abs < 0]) ]) - return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") + # Ignore type as floatfmt does allow tuples but mypy does not know that + return tabulate(tabular_data, headers=headers, # type: ignore + floatfmt=floatfmt, tablefmt="pipe") def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str: """ @@ -168,7 +170,9 @@ class Backtesting(object): len(results[results.profit_abs > 0]), len(results[results.profit_abs < 0]) ]) - return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") + # Ignore type as floatfmt does allow tuples but mypy does not know that + return tabulate(tabular_data, headers=headers, # type: ignore + floatfmt=floatfmt, tablefmt="pipe") def _store_backtest_result(self, recordfilename: str, results: DataFrame, strategyname: Optional[str] = None) -> None: @@ -221,7 +225,7 @@ class Backtesting(object): elif sell.sell_type == (SellType.ROI): # get next entry in min_roi > to trade duration # Interface.py skips on trade_duration <= duration - roi_entry = max(list(filter(lambda x: trade_dur > x, + roi_entry = max(list(filter(lambda x: trade_dur >= x, self.strategy.minimal_roi.keys()))) roi = self.strategy.minimal_roi[roi_entry] diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index a98f0c877..fdae47b99 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -67,7 +67,9 @@ class EdgeCli(object): round(result[1].avg_trade_duration) ]) - return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") + # Ignore type as floatfmt does allow tuples but mypy does not know that + return tabulate(tabular_data, headers=headers, # type: ignore + floatfmt=floatfmt, tablefmt="pipe") def start(self) -> None: self.edge.calculate() diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index a14d22b98..f9b34fc64 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_order_id'): + if not has_column(cols, 'stoploss_last_update'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') @@ -93,6 +93,7 @@ def check_migrate(engine) -> None: stop_loss = get_column_def(cols, 'stop_loss', '0.0') initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') 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') sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') @@ -111,7 +112,8 @@ 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, max_rate, sell_reason, strategy, + stop_loss, initial_stop_loss, stoploss_order_id, stoploss_last_update, + max_rate, sell_reason, strategy, ticker_interval ) select id, lower(exchange), @@ -127,9 +129,9 @@ def check_migrate(engine) -> None: {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, - {stoploss_order_id} stoploss_order_id, {max_rate} max_rate, - {sell_reason} sell_reason, {strategy} strategy, - {ticker_interval} ticker_interval + {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, + {max_rate} max_rate, {sell_reason} sell_reason, + {strategy} strategy, {ticker_interval} ticker_interval from {table_back_name} """) @@ -185,6 +187,8 @@ class Trade(_DECL_BASE): initial_stop_loss = Column(Float, nullable=True, default=0.0) # 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) sell_reason = Column(String, nullable=True) @@ -218,11 +222,13 @@ class Trade(_DECL_BASE): logger.debug("assigning new stop loss") self.stop_loss = new_loss self.initial_stop_loss = new_loss + 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.stoploss_last_update = datetime.utcnow() logger.debug("adjusted stop loss") else: logger.debug("keeping current stop loss") diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index be2498d78..3ce7c9167 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -246,14 +246,14 @@ class Telegram(RPC): stake_cur, fiat_disp_cur ) - stats = tabulate(stats, - headers=[ - 'Day', - f'Profit {stake_cur}', - f'Profit {fiat_disp_cur}' - ], - tablefmt='simple') - message = f'Daily Profit over the last {timescale} days:\n
{stats}
' + stats_tab = tabulate(stats, + headers=[ + 'Day', + f'Profit {stake_cur}', + f'Profit {fiat_disp_cur}' + ], + tablefmt='simple') + message = f'Daily Profit over the last {timescale} days:\n
{stats_tab}
' self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e), bot=bot) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a6569ec19..08a5cf1cd 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -80,7 +80,8 @@ class IStrategy(ABC): 'buy': 'limit', 'sell': 'limit', 'stoploss': 'limit', - 'stoploss_on_exchange': False + 'stoploss_on_exchange': False, + 'stoploss_on_exchange_interval': 60, } # Optional time in force @@ -233,12 +234,9 @@ class IStrategy(ABC): current_rate = low or rate current_profit = trade.calc_profit_percent(current_rate) - if self.order_types.get('stoploss_on_exchange'): - stoplossflag = SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - else: - stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, - current_time=date, current_profit=current_profit, - force_stoploss=force_stoploss) + stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, + current_time=date, current_profit=current_profit, + force_stoploss=force_stoploss) if stoplossflag.sell_flag: return stoplossflag @@ -276,12 +274,13 @@ 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) - # evaluate if the stoploss was hit - if self.stoploss is not None and trade.stop_loss >= current_rate: + # 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'))): 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: @@ -301,7 +300,8 @@ class IStrategy(ABC): # check if we have a special stop loss for positive condition # and if profit is positive - stop_loss_value = self.stoploss + stop_loss_value = force_stoploss if force_stoploss else self.stoploss + sl_offset = self.config.get('trailing_stop_positive_offset', 0.0) if 'trailing_stop_positive' in self.config and current_profit > sl_offset: @@ -319,17 +319,18 @@ class IStrategy(ABC): def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: """ Based an earlier trade and current price and ROI configuration, decides whether bot should - sell + sell. Requires current_profit to be in percent!! :return True if bot should sell at current rate """ # Check if time matches and current rate is above threshold - time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60 - for duration, threshold in self.minimal_roi.items(): - if time_diff <= duration: - continue - if current_profit > threshold: - return True + trade_dur = (current_time.timestamp() - trade.open_date.timestamp()) / 60 + + # Get highest entry in ROI dict where key >= trade-duration + roi_entry = max(list(filter(lambda x: trade_dur >= x, self.minimal_roi.keys()))) + threshold = self.minimal_roi[roi_entry] + if current_profit > threshold: + return True return False diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 29154bc39..26808e78a 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -923,6 +923,30 @@ async def test_async_get_candles_history(default_conf, mocker): assert exchange._async_get_candle_history.call_count == 2 +@pytest.mark.asyncio +async def test_async_get_candles_history_inv_result(default_conf, mocker, caplog): + + async def mock_get_candle_hist(pair, *args, **kwargs): + if pair == 'ETH/BTC': + return [[]] + else: + raise TypeError() + + exchange = get_patched_exchange(mocker, default_conf) + + # Monkey-patch async function with empty result + exchange._api_async.fetch_ohlcv = MagicMock(side_effect=mock_get_candle_hist) + + pairs = ['ETH/BTC', 'XRP/BTC'] + res = await exchange.async_get_candles_history(pairs, "5m") + assert type(res) is list + assert len(res) == 2 + assert type(res[0]) is tuple + assert type(res[1]) is TypeError + assert log_has("Error loading ETH/BTC. Result was [[]].", caplog.record_tuples) + assert log_has("Async code raised an exception: TypeError", caplog.record_tuples) + + def test_get_order_book(default_conf, mocker, order_book_l2): default_conf['exchange']['name'] = 'binance' api_mock = MagicMock() diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 6e0ab24a4..5ab44baad 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -530,10 +530,10 @@ def test_backtest(default_conf, fee, mocker) -> None: 'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime, Arrow(2018, 1, 30, 3, 30, 0).datetime], 'close_time': [Arrow(2018, 1, 29, 22, 35, 0).datetime, - Arrow(2018, 1, 30, 4, 15, 0).datetime], + Arrow(2018, 1, 30, 4, 10, 0).datetime], 'open_index': [78, 184], - 'close_index': [125, 193], - 'trade_duration': [235, 45], + 'close_index': [125, 192], + 'trade_duration': [235, 40], 'open_at_end': [False, False], 'open_rate': [0.104445, 0.10302485], 'close_rate': [0.104969, 0.103541], diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index 08634073d..f2e8c577f 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -118,6 +118,7 @@ def test_tickerdata_to_dataframe(default_conf) -> None: def test_min_roi_reached(default_conf, fee) -> None: + # Use list to confirm sequence does not matter min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1}, {0: 0.1, 20: 0.05, 55: 0.01}] for roi in min_roi_list: @@ -143,6 +144,47 @@ def test_min_roi_reached(default_conf, fee) -> None: assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime) +def test_min_roi_reached2(default_conf, fee) -> None: + + # test with ROI raising after last interval + min_roi_list = [{20: 0.07, + 30: 0.05, + 55: 0.30, + 0: 0.1 + }, + {0: 0.1, + 20: 0.07, + 30: 0.05, + 55: 0.30 + }, + ] + for roi in min_roi_list: + strategy = DefaultStrategy(default_conf) + strategy.minimal_roi = roi + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + open_date=arrow.utcnow().shift(hours=-1).datetime, + fee_open=fee.return_value, + fee_close=fee.return_value, + exchange='bittrex', + open_rate=1, + ) + + assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime) + assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-56).datetime) + + assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime) + assert strategy.min_roi_reached(trade, 0.071, arrow.utcnow().shift(minutes=-39).datetime) + + assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-26).datetime) + assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-26).datetime) + + # Should not trigger with 20% profit since after 55 minutes only 30% is active. + assert not strategy.min_roi_reached(trade, 0.20, arrow.utcnow().shift(minutes=-2).datetime) + assert strategy.min_roi_reached(trade, 0.31, arrow.utcnow().shift(minutes=-2).datetime) + + def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 2b14c586b..1149a69e9 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1014,6 +1014,211 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, assert trade.is_open is False +def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, + markets, limit_buy_order, limit_sell_order) -> None: + # When trailing stoploss is set + stoploss_limit = MagicMock(return_value={'id': 13434334}) + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + sell=MagicMock(return_value={'id': limit_sell_order['id']}), + get_fee=fee, + get_markets=markets, + stoploss_limit=stoploss_limit + ) + + # enabling TSL + default_conf['trailing_stop'] = True + + # disabling ROI + default_conf['minimal_roi']['0'] = 999999999 + + freqtrade = FreqtradeBot(default_conf) + + # enabling stoploss on exchange + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + # setting stoploss + freqtrade.strategy.stoploss = -0.05 + + # setting stoploss_on_exchange_interval to 60 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 + + patch_get_signal(freqtrade) + + freqtrade.create_trade() + trade = Trade.query.first() + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = 100 + + stoploss_order_hanging = MagicMock(return_value={ + 'id': 100, + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'info': { + 'stopPrice': '0.000011134' + } + }) + + mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) + + # stoploss initially at 5% + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + # price jumped 2x + mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={ + 'bid': 0.00002344, + 'ask': 0.00002346, + 'last': 0.00002344 + })) + + cancel_order_mock = MagicMock() + stoploss_order_mock = MagicMock() + mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock) + + # stoploss should not be updated as the interval is 60 seconds + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + cancel_order_mock.assert_not_called() + stoploss_order_mock.assert_not_called() + + assert freqtrade.handle_trade(trade) is False + assert trade.stop_loss == 0.00002344 * 0.95 + + # setting stoploss_on_exchange_interval to 0 seconds + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 + + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') + stoploss_order_mock.assert_called_once_with(amount=85.25149190110828, + pair='ETH/BTC', + rate=0.00002344 * 0.95 * 0.99, + stop_price=0.00002344 * 0.95) + + +def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, + markets, limit_buy_order, limit_sell_order) -> None: + + # When trailing stoploss is set + stoploss_limit = MagicMock(return_value={'id': 13434334}) + patch_RPCManager(mocker) + patch_exchange(mocker) + patch_edge(mocker) + + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + sell=MagicMock(return_value={'id': limit_sell_order['id']}), + get_fee=fee, + get_markets=markets, + stoploss_limit=stoploss_limit + ) + + # enabling TSL + edge_conf['trailing_stop'] = True + edge_conf['trailing_stop_positive'] = 0.01 + edge_conf['trailing_stop_positive_offset'] = 0.011 + + # disabling ROI + edge_conf['minimal_roi']['0'] = 999999999 + + freqtrade = FreqtradeBot(edge_conf) + + # enabling stoploss on exchange + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + # setting stoploss + freqtrade.strategy.stoploss = -0.02 + + # setting stoploss_on_exchange_interval to 0 second + freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 + + patch_get_signal(freqtrade) + + freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist) + + freqtrade.create_trade() + trade = Trade.query.first() + trade.is_open = True + trade.open_order_id = None + trade.stoploss_order_id = 100 + + stoploss_order_hanging = MagicMock(return_value={ + 'id': 100, + 'status': 'open', + 'type': 'stop_loss_limit', + 'price': 3, + 'average': 2, + 'info': { + 'stopPrice': '0.000009384' + } + }) + + mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) + + # stoploss initially at 20% as edge dictated it. + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + assert trade.stop_loss == 0.000009384 + + cancel_order_mock = MagicMock() + stoploss_order_mock = MagicMock() + mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock) + + # price goes down 5% + mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={ + 'bid': 0.00001172 * 0.95, + 'ask': 0.00001173 * 0.95, + 'last': 0.00001172 * 0.95 + })) + + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + # stoploss should remain the same + assert trade.stop_loss == 0.000009384 + + # stoploss on exchange should not be canceled + cancel_order_mock.assert_not_called() + + # price jumped 2x + mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={ + 'bid': 0.00002344, + 'ask': 0.00002346, + 'last': 0.00002344 + })) + + assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_stoploss_on_exchange(trade) is False + + # stoploss should be set to 1% as trailing is on + assert trade.stop_loss == 0.00002344 * 0.99 + cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') + stoploss_order_mock.assert_called_once_with(amount=2131074.168797954, + pair='NEO/BTC', + rate=0.00002344 * 0.99 * 0.99, + stop_price=0.00002344 * 0.99) + + def test_process_maybe_execute_buy(mocker, default_conf) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index e64a08262..be6efc2ff 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -516,6 +516,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.strategy is None assert trade.ticker_interval is None assert trade.stoploss_order_id is None + assert trade.stoploss_last_update is None assert log_has("trying trades_bak1", caplog.record_tuples) assert log_has("trying trades_bak2", caplog.record_tuples) assert log_has("Running database migration - backup available as trades_bak2", diff --git a/requirements-dev.txt b/requirements-dev.txt index 2eeae1fbc..4f8504c8a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,7 +2,11 @@ -r requirements.txt flake8==3.6.0 -pytest==4.1.0 +flake8-type-annotations==0.1.0 +flake8-tidy-imports==1.1.0 +pytest==4.1.1 pytest-mock==1.10.0 pytest-asyncio==0.10.0 pytest-cov==2.6.1 +coveralls==1.5.1 +mypy==0.660 diff --git a/requirements.txt b/requirements.txt index 25c441d73..25e337306 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,17 +1,17 @@ -ccxt==1.18.117 -SQLAlchemy==1.2.15 +ccxt==1.18.144 +SQLAlchemy==1.2.16 python-telegram-bot==11.1.0 arrow==0.13.0 cachetools==3.0.0 requests==2.21.0 urllib3==1.24.1 -wrapt==1.10.11 +wrapt==1.11.1 pandas==0.23.4 scikit-learn==0.20.2 -joblib==0.13.0 +joblib==0.13.1 scipy==1.2.0 jsonschema==2.6.0 -numpy==1.15.4 +numpy==1.16.0 TA-Lib==0.4.17 tabulate==0.8.2 coinmarketcap==5.0.3