From bd51cd332bc89801ea3e4af4ee4e217833e7bd79 Mon Sep 17 00:00:00 2001 From: jpribyl Date: Fri, 24 Apr 2020 16:16:52 -0600 Subject: [PATCH 1/8] Cancel all open orders after receiving /stop or ctrl+c --- config.json.example | 1 + config_binance.json.example | 1 + config_full.json.example | 1 + config_kraken.json.example | 1 + docs/configuration.md | 1 + freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 5 ++ freqtrade/configuration/configuration.py | 5 ++ freqtrade/constants.py | 8 ++ freqtrade/freqtradebot.py | 108 +++++++++++++++-------- freqtrade/templates/base_config.json.j2 | 1 + freqtrade/worker.py | 3 +- tests/conftest.py | 1 + tests/test_arguments.py | 8 ++ tests/test_configuration.py | 2 + tests/test_freqtradebot.py | 28 +++--- 16 files changed, 124 insertions(+), 52 deletions(-) diff --git a/config.json.example b/config.json.example index 8ebb092e1..d37a6b336 100644 --- a/config.json.example +++ b/config.json.example @@ -6,6 +6,7 @@ "fiat_display_currency": "USD", "ticker_interval": "5m", "dry_run": false, + "cancel_open_orders_on_exit": false, "trailing_stop": false, "unfilledtimeout": { "buy": 10, diff --git a/config_binance.json.example b/config_binance.json.example index d324ce883..5d7b6b656 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -6,6 +6,7 @@ "fiat_display_currency": "USD", "ticker_interval": "5m", "dry_run": true, + "cancel_open_orders_on_exit": false, "trailing_stop": false, "unfilledtimeout": { "buy": 10, diff --git a/config_full.json.example b/config_full.json.example index 181740b9a..79154b0d4 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -8,6 +8,7 @@ "amend_last_stake_amount": false, "last_stake_amount_min_ratio": 0.5, "dry_run": false, + "cancel_open_orders_on_exit": false, "ticker_interval": "5m", "trailing_stop": false, "trailing_stop_positive": 0.005, diff --git a/config_kraken.json.example b/config_kraken.json.example index dcf4c552a..54fbf4a00 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -6,6 +6,7 @@ "fiat_display_currency": "EUR", "ticker_interval": "5m", "dry_run": true, + "cancel_open_orders_on_exit": false, "trailing_stop": false, "unfilledtimeout": { "buy": 10, diff --git a/docs/configuration.md b/docs/configuration.md index 67e8578dd..28df5bd95 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -51,6 +51,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
**Datatype:** String | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
**Datatype:** Boolean | `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.
*Defaults to `1000`.*
**Datatype:** Float +| `cancel_open_orders_on_exit` | Cancel orders when `/stop` is issued or `ctrl+c` is pressed. Including this will allow you to use `/stop` to cancel unfilled orders in the event of a market crash. This will not impact open positions.
*Defaults to `False`.*
**Datatype:** Boolean | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Float (as ratio) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 8c64c5857..be03bb906 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -13,7 +13,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_TRADE = ["db_url", "sd_notify", "dry_run"] +ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "cancel_open_orders_on_exit"] ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", "max_open_trades", "stake_amount", "fee"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 498ea9359..adb28399a 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -109,6 +109,11 @@ AVAILABLE_CLI_OPTIONS = { help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).', action='store_true', ), + "cancel_open_orders_on_exit": Arg( + '--cancel-open-orders-on-exit', + help='Close unfilled open orders when the bot stops / exits', + action='store_true', + ), # Optimize common "ticker_interval": Arg( '-i', '--ticker-interval', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index e5515670d..cfbba9987 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -134,6 +134,11 @@ class Configuration: if config['runmode'] not in TRADING_MODES: return + if self.args.get('cancel_open_orders_on_exit', False): + config.update({ + 'cancel_open_orders_on_exit': self.args.get('cancel_open_orders_on_exit') + }) + if config.get('dry_run', False): logger.info('Dry run is enabled') if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]: diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 54f620631..c238f227b 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -85,6 +85,7 @@ CONF_SCHEMA = { 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, 'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET}, + 'cancel_open_orders_on_exit': {'type': 'boolean', 'default': False}, 'process_only_new_candles': {'type': 'boolean'}, 'minimal_roi': { 'type': 'object', @@ -318,3 +319,10 @@ SCHEMA_MINIMAL_REQUIRED = [ 'dataformat_ohlcv', 'dataformat_trades', ] + +CANCEL_REASON = { + "TIMEOUT": "cancelled due to timeout", + "PARTIALLY_FILLED": "partially filled - keeping order open", + "ALL_CANCELLED": "cancelled (all unfilled orders cancelled)", + "CANCELLED_ON_EXCHANGE": "cancelled on exchange", +} diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7ae87e807..5b8d59ed2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -113,6 +113,9 @@ class FreqtradeBot: """ logger.info('Cleaning up modules ...') + if self.config['cancel_open_orders_on_exit']: + self.cancel_all_open_orders() + self.rpc.cleanup() persistence.cleanup() @@ -162,6 +165,13 @@ class FreqtradeBot: Trade.session.flush() + def process_stopped(self) -> None: + """ + Close all trades that were left open + """ + if self.config['cancel_open_orders_on_exit']: + self.cancel_all_open_orders() + def _refresh_whitelist(self, trades: List[Trade] = []) -> List[str]: """ Refresh whitelist from pairlist or edge and extend it with trades. @@ -875,11 +885,7 @@ class FreqtradeBot: default_retval=False)(pair=trade.pair, trade=trade, order=order))): - - self.handle_timedout_limit_buy(trade, order) - self.wallets.update() - order_type = self.strategy.order_types['buy'] - self._notify_buy_cancel(trade, order_type) + self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT']) elif (order['side'] == 'sell' and ( trade_state_update @@ -888,24 +894,42 @@ class FreqtradeBot: default_retval=False)(pair=trade.pair, trade=trade, order=order))): - reason = self.handle_timedout_limit_sell(trade, order) - self.wallets.update() - order_type = self.strategy.order_types['sell'] - self._notify_sell_cancel(trade, order_type, reason) + self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT']) - def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool: + def cancel_all_open_orders(self) -> None: """ - Buy timeout - cancel order + Cancel all orders that are currently open + :return: None + """ + + for trade in Trade.get_open_order_trades(): + try: + order = self.exchange.get_order(trade.open_order_id, trade.pair) + except (DependencyException, InvalidOrderException): + logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) + continue + + if order['side'] == 'buy': + self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) + + elif order['side'] == 'sell': + self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) + + def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool: + """ + Buy cancel - cancel order :return: True if order was fully cancelled """ + was_trade_fully_canceled = False + if order['status'] != 'canceled': - reason = "cancelled due to timeout" + reason = constants.CANCEL_REASON['TIMEOUT'] corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) else: # Order was cancelled already, so we can reuse the existing dict corder = order - reason = "cancelled on exchange" + reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] logger.info('Buy order %s for %s.', reason, trade) @@ -914,40 +938,42 @@ class FreqtradeBot: # if trade is not partially completed, just delete the trade Trade.session.delete(trade) Trade.session.flush() - return True + was_trade_fully_canceled = True + else: + # if trade is partially complete, edit the stake details for the trade + # and close the order + # cancel_order may not contain the full order dict, so we need to fallback + # to the order dict aquired before cancelling. + # we need to fall back to the values from order if corder does not contain these keys. + trade.amount = order['amount'] - safe_value_fallback(corder, order, + 'remaining', 'remaining') + trade.stake_amount = trade.amount * trade.open_rate + self.update_trade_state(trade, corder, trade.amount) - # if trade is partially complete, edit the stake details for the trade - # and close the order - # cancel_order may not contain the full order dict, so we need to fallback - # to the order dict aquired before cancelling. - # we need to fall back to the values from order if corder does not contain these keys. - trade.amount = order['amount'] - safe_value_fallback(corder, order, - 'remaining', 'remaining') - trade.stake_amount = trade.amount * trade.open_rate - self.update_trade_state(trade, corder, trade.amount) + trade.open_order_id = None + logger.info('Partial buy order timeout for %s.', trade) + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'Remaining buy order for {trade.pair} cancelled due to timeout' + }) - trade.open_order_id = None - logger.info('Partial buy order timeout for %s.', trade) - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Remaining buy order for {trade.pair} cancelled due to timeout' - }) - return False + self.wallets.update() + self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy']) + return was_trade_fully_canceled - def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> str: + def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str: """ - Sell timeout - cancel order and update trade + Sell cancel - cancel order and update trade :return: Reason for cancel """ # if trade is not partially completed, just cancel the trade if order['remaining'] == order['amount'] or order.get('filled') == 0.0: if not self.exchange.check_order_canceled_empty(order): - reason = "cancelled due to timeout" # if trade is not partially completed, just delete the trade self.exchange.cancel_order(trade.open_order_id, trade.pair) logger.info('Sell order %s for %s.', reason, trade) else: - reason = "cancelled on exchange" + reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] logger.info('Sell order %s for %s.', reason, trade) trade.close_rate = None @@ -957,11 +983,17 @@ class FreqtradeBot: trade.close_date = None trade.is_open = True trade.open_order_id = None + else: + # TODO: figure out how to handle partially complete sell orders + reason = constants.CANCEL_REASON['PARTIALLY_FILLED'] - return reason - - # TODO: figure out how to handle partially complete sell orders - return 'partially filled - keeping order open' + self.wallets.update() + self._notify_sell_cancel( + trade, + order_type=self.strategy.order_types['sell'], + reason=reason + ) + return reason def _safe_sell_amount(self, pair: str, amount: float) -> float: """ diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 134719273..6d3174347 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -6,6 +6,7 @@ "fiat_display_currency": "{{ fiat_display_currency }}", "ticker_interval": "{{ ticker_interval }}", "dry_run": {{ dry_run | lower }}, + "cancel_open_orders_on_exit": false, "unfilledtimeout": { "buy": 10, "sell": 30 diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 4c28ecaeb..55cf30d16 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -131,8 +131,7 @@ class Worker: return result def _process_stopped(self) -> None: - # Maybe do here something in the future... - pass + self.freqtrade.process_stopped() def _process_running(self) -> None: try: diff --git a/tests/conftest.py b/tests/conftest.py index d95475b8c..2f5f468f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -249,6 +249,7 @@ def default_conf(testdatadir): "fiat_display_currency": "USD", "ticker_interval": '5m', "dry_run": True, + "cancel_open_orders_on_exit": False, "minimal_roi": { "40": 0.0, "30": 0.01, diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 0052a61d0..91501384b 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -64,6 +64,14 @@ def test_parse_args_db_url() -> None: assert args["db_url"] == 'sqlite:///test.sqlite' +def test_parse_args_cancel_open_orders_on_exit() -> None: + args = Arguments(['trade']).get_parsed_arg() + assert args["cancel_open_orders_on_exit"] is False + + args = Arguments(['trade', '--cancel-open-orders-on-exit']).get_parsed_arg() + assert args["cancel_open_orders_on_exit"] is True + + def test_parse_args_verbose() -> None: args = Arguments(['trade', '-v']).get_parsed_arg() assert args["verbosity"] == 1 diff --git a/tests/test_configuration.py b/tests/test_configuration.py index c89f1381e..0315ffe1b 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -250,6 +250,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: '--strategy', 'TestStrategy', '--strategy-path', '/some/path', '--db-url', 'sqlite:///someurl', + '--cancel-open-orders-on-exit', ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) @@ -258,6 +259,7 @@ def test_load_config_with_params(default_conf, mocker) -> None: assert validated_conf.get('strategy') == 'TestStrategy' assert validated_conf.get('strategy_path') == '/some/path' assert validated_conf.get('db_url') == 'sqlite:///someurl' + assert validated_conf.get('cancel_open_orders_on_exit') is True # Test conf provided db_url prod conf = default_conf.copy() diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6f2ce9f3c..8adc834cf 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -11,7 +11,7 @@ import arrow import pytest import requests -from freqtrade.constants import MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT +from freqtrade.constants import MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT, CANCEL_REASON from freqtrade.exceptions import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot @@ -2281,8 +2281,8 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', - handle_timedout_limit_buy=MagicMock(), - handle_timedout_limit_sell=MagicMock(), + handle_cancel_buy=MagicMock(), + handle_cancel_sell=MagicMock(), ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -2309,21 +2309,23 @@ def test_handle_timedout_limit_buy(mocker, caplog, default_conf, limit_buy_order mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) freqtrade = FreqtradeBot(default_conf) + freqtrade._notify_buy_cancel = MagicMock() Trade.session = MagicMock() trade = MagicMock() trade.pair = 'LTC/ETH' limit_buy_order['remaining'] = limit_buy_order['amount'] - assert freqtrade.handle_timedout_limit_buy(trade, limit_buy_order) + reason = CANCEL_REASON['TIMEOUT'] + assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 cancel_order_mock.reset_mock() limit_buy_order['amount'] = 2 - assert not freqtrade.handle_timedout_limit_buy(trade, limit_buy_order) + assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException) - assert not freqtrade.handle_timedout_limit_buy(trade, limit_buy_order) + assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) @pytest.mark.parametrize('cancelorder', [ @@ -2343,17 +2345,19 @@ def test_handle_timedout_limit_buy_corder_empty(mocker, default_conf, limit_buy_ ) freqtrade = FreqtradeBot(default_conf) + freqtrade._notify_buy_cancel = MagicMock() Trade.session = MagicMock() trade = MagicMock() trade.pair = 'LTC/ETH' limit_buy_order['remaining'] = limit_buy_order['amount'] - assert freqtrade.handle_timedout_limit_buy(trade, limit_buy_order) + reason = CANCEL_REASON['TIMEOUT'] + assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 cancel_order_mock.reset_mock() limit_buy_order['amount'] = 2 - assert not freqtrade.handle_timedout_limit_buy(trade, limit_buy_order) + assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 @@ -2367,16 +2371,18 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None: ) freqtrade = FreqtradeBot(default_conf) + freqtrade._notify_sell_cancel = MagicMock() trade = MagicMock() order = {'remaining': 1, 'amount': 1, 'status': "open"} - assert freqtrade.handle_timedout_limit_sell(trade, order) + reason = CANCEL_REASON['TIMEOUT'] + assert freqtrade.handle_cancel_sell(trade, order, reason) assert cancel_order_mock.call_count == 1 order['amount'] = 2 - assert (freqtrade.handle_timedout_limit_sell(trade, order) - == 'partially filled - keeping order open') + assert (freqtrade.handle_cancel_sell(trade, order, reason) + == CANCEL_REASON['PARTIALLY_FILLED']) # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 From 2307495f2837971f6325efa97afea7d874ae8c1d Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Fri, 8 May 2020 20:10:16 +0300 Subject: [PATCH 2/8] Update docs/configuration.md Co-authored-by: Matthias --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 28df5bd95..59d8ba1eb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -51,7 +51,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
**Datatype:** String | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
**Datatype:** Boolean | `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.
*Defaults to `1000`.*
**Datatype:** Float -| `cancel_open_orders_on_exit` | Cancel orders when `/stop` is issued or `ctrl+c` is pressed. Including this will allow you to use `/stop` to cancel unfilled orders in the event of a market crash. This will not impact open positions.
*Defaults to `False`.*
**Datatype:** Boolean +| `cancel_open_orders_on_exit` | Cancel orders when `/stop` is issued or `ctrl+c` is pressed. This will allow you to use `/stop` to cancel unfilled orders in the event of a market crash. This will not impact open positions.
*Defaults to `False`.*
**Datatype:** Boolean | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Float (as ratio) From 61f6acb5c99734542971e49b170663eda05a93f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 May 2020 07:07:24 +0200 Subject: [PATCH 3/8] Add cors support - needed for UI --- freqtrade/rpc/api_server.py | 2 ++ requirements-common.txt | 1 + setup.py | 2 +- tests/rpc/test_rpc_apiserver.py | 1 + 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 68f4b1ca9..61eacf639 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -7,6 +7,7 @@ from typing import Any, Callable, Dict from arrow import Arrow from flask import Flask, jsonify, request from flask.json import JSONEncoder +from flask_cors import CORS from flask_jwt_extended import (JWTManager, create_access_token, create_refresh_token, get_jwt_identity, jwt_refresh_token_required, @@ -88,6 +89,7 @@ class ApiServer(RPC): self._config = freqtrade.config self.app = Flask(__name__) + self._cors = CORS(self.app, resources={r"/api/*": {"origins": "*"}}) # Setup the Flask-JWT-Extended extension self.app.config['JWT_SECRET_KEY'] = self._config['api_server'].get( diff --git a/requirements-common.txt b/requirements-common.txt index 02c4ebc20..017974c9e 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -26,6 +26,7 @@ sdnotify==0.3.2 # Api server flask==1.1.2 flask-jwt-extended==3.24.1 +flask-cors==3.0.8 # Support for colorized terminal output colorama==0.4.3 diff --git a/setup.py b/setup.py index 8c0de095e..20963a15f 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ if readme_file.is_file(): readme_long = (Path(__file__).parent / "README.md").read_text() # Requirements used for submodules -api = ['flask', 'flask-jwt-extended'] +api = ['flask', 'flask-jwt-extended', 'flask-cors'] plot = ['plotly>=4.0'] hyperopt = [ 'scipy', diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index ffdd5be15..b953097d5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -49,6 +49,7 @@ def client_get(client, url): def assert_response(response, expected_code=200): assert response.status_code == expected_code assert response.content_type == "application/json" + assert ('Access-Control-Allow-Origin', '*') in response.headers._list def test_api_not_found(botclient): From c3f3242f28f01b282c6970b45a3f3bbfeea7955c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 May 2020 11:05:34 +0200 Subject: [PATCH 4/8] Add tests for cancel_open_orders_on_exit --- tests/test_freqtradebot.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8adc834cf..8e3bc8928 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -48,13 +48,31 @@ def test_freqtradebot_state(mocker, default_conf, markets) -> None: assert freqtrade.state is State.STOPPED -def test_cleanup(mocker, default_conf, caplog) -> None: - mock_cleanup = MagicMock() - mocker.patch('freqtrade.persistence.cleanup', mock_cleanup) +def test_process_stopped(mocker, default_conf) -> None: + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders') + freqtrade.process_stopped() + assert coo_mock.call_count == 0 + + default_conf['cancel_open_orders_on_exit'] = True + freqtrade = get_patched_freqtradebot(mocker, default_conf) + freqtrade.process_stopped() + assert coo_mock.call_count == 1 + + +def test_bot_cleanup(mocker, default_conf, caplog) -> None: + mock_cleanup = mocker.patch('freqtrade.persistence.cleanup') + coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders') freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.cleanup() assert log_has('Cleaning up modules ...', caplog) assert mock_cleanup.call_count == 1 + assert coo_mock.call_count == 0 + + freqtrade.config['cancel_open_orders_on_exit'] = True + freqtrade.cleanup() + assert coo_mock.call_count == 1 def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: From 22a08768f73112f8f1e1cddbb63774cb988c0eba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 May 2020 12:40:25 +0200 Subject: [PATCH 5/8] Add test for cancel_open_order --- tests/test_freqtradebot.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8e3bc8928..940ae8e77 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -22,7 +22,7 @@ from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.worker import Worker from tests.conftest import (get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, - patch_get_signal, patch_wallet, patch_whitelist) + patch_get_signal, patch_wallet, patch_whitelist, create_mock_trades) def patch_RPCManager(mocker) -> MagicMock: @@ -3882,3 +3882,21 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, assert log_has_re(r"Unable to create trade for XRP/BTC: " r"Available balance \(0.0 BTC\) is lower than stake amount \(0.001 BTC\)", caplog) + + +@pytest.mark.usefixtures("init_persistence") +def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order): + default_conf['cancel_open_orders_on_exit'] = True + mocker.patch('freqtrade.exchange.Exchange.get_order', + side_effect=[DependencyException(), limit_sell_order, limit_buy_order]) + buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') + sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + create_mock_trades(fee) + trades = Trade.query.all() + assert len(trades) == 3 + freqtrade.cancel_all_open_orders() + assert buy_mock.call_count == 1 + assert sell_mock.call_count == 1 + From 7a11219b613b2ce782afa113456e2571a54817cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 May 2020 13:17:48 +0200 Subject: [PATCH 6/8] Reword some documentation strings Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/configuration.md | 2 +- freqtrade/commands/cli_options.py | 2 +- freqtrade/freqtradebot.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index b0b840fcc..7405fc92e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -51,7 +51,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
**Datatype:** String | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
**Datatype:** Boolean | `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.
*Defaults to `1000`.*
**Datatype:** Float -| `cancel_open_orders_on_exit` | Cancel orders when `/stop` is issued or `ctrl+c` is pressed. This will allow you to use `/stop` to cancel unfilled orders in the event of a market crash. This will not impact open positions.
*Defaults to `False`.*
**Datatype:** Boolean +| `cancel_open_orders_on_exit` | Cancel open orders when the `/stop` RPC command is issued, `Ctrl+C` is pressed or the bot dies unexpectedly. When set to `true`, this allows you to use `/stop` to cancel unfilled and partially filled orders in the event of a market crash. It does not impact open positions.
*Defaults to `false`.*
**Datatype:** Boolean | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
**Datatype:** Boolean | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Dict | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** Float (as ratio) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index cca9d7048..7a2110a91 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -111,7 +111,7 @@ AVAILABLE_CLI_OPTIONS = { ), "cancel_open_orders_on_exit": Arg( '--cancel-open-orders-on-exit', - help='Close unfilled open orders when the bot stops / exits', + help='Close unfilled and partially filled open orders when the bot stops / exits.', action='store_true', ), # Optimize common diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cad8b4aea..3b3758f36 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -170,7 +170,7 @@ class FreqtradeBot: def process_stopped(self) -> None: """ - Close all trades that were left open + Close all orders that were left open """ if self.config['cancel_open_orders_on_exit']: self.cancel_all_open_orders() From fed75d8718303b837f5192711f9a68189a9c7efa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 16 May 2020 13:23:40 +0200 Subject: [PATCH 7/8] remove --cancel_open_orders cli switch --- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 5 ----- freqtrade/configuration/configuration.py | 5 ----- tests/test_arguments.py | 8 -------- tests/test_configuration.py | 2 -- 5 files changed, 1 insertion(+), 21 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 234b719f7..a03da00ab 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -13,7 +13,7 @@ ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_dat ARGS_STRATEGY = ["strategy", "strategy_path"] -ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "cancel_open_orders_on_exit"] +ARGS_TRADE = ["db_url", "sd_notify", "dry_run"] ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", "max_open_trades", "stake_amount", "fee"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 7a2110a91..a8f2ffdba 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -109,11 +109,6 @@ AVAILABLE_CLI_OPTIONS = { help='Enforce dry-run for trading (removes Exchange secrets and simulates trades).', action='store_true', ), - "cancel_open_orders_on_exit": Arg( - '--cancel-open-orders-on-exit', - help='Close unfilled and partially filled open orders when the bot stops / exits.', - action='store_true', - ), # Optimize common "ticker_interval": Arg( '-i', '--ticker-interval', diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 188d79f90..7edd9bca1 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -134,11 +134,6 @@ class Configuration: if config['runmode'] not in TRADING_MODES: return - if self.args.get('cancel_open_orders_on_exit', False): - config.update({ - 'cancel_open_orders_on_exit': self.args.get('cancel_open_orders_on_exit') - }) - if config.get('dry_run', False): logger.info('Dry run is enabled') if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]: diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 91501384b..0052a61d0 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -64,14 +64,6 @@ def test_parse_args_db_url() -> None: assert args["db_url"] == 'sqlite:///test.sqlite' -def test_parse_args_cancel_open_orders_on_exit() -> None: - args = Arguments(['trade']).get_parsed_arg() - assert args["cancel_open_orders_on_exit"] is False - - args = Arguments(['trade', '--cancel-open-orders-on-exit']).get_parsed_arg() - assert args["cancel_open_orders_on_exit"] is True - - def test_parse_args_verbose() -> None: args = Arguments(['trade', '-v']).get_parsed_arg() assert args["verbosity"] == 1 diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 0315ffe1b..c89f1381e 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -250,7 +250,6 @@ def test_load_config_with_params(default_conf, mocker) -> None: '--strategy', 'TestStrategy', '--strategy-path', '/some/path', '--db-url', 'sqlite:///someurl', - '--cancel-open-orders-on-exit', ] args = Arguments(arglist).get_parsed_arg() configuration = Configuration(args) @@ -259,7 +258,6 @@ def test_load_config_with_params(default_conf, mocker) -> None: assert validated_conf.get('strategy') == 'TestStrategy' assert validated_conf.get('strategy_path') == '/some/path' assert validated_conf.get('db_url') == 'sqlite:///someurl' - assert validated_conf.get('cancel_open_orders_on_exit') is True # Test conf provided db_url prod conf = default_conf.copy() From baf5f4f29cd0caa5d0c76471e989a193daec0bcd Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sat, 16 May 2020 21:28:54 +0300 Subject: [PATCH 8/8] Update freqtrade/constants.py --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 0c8dde8c4..e56586bbc 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -326,6 +326,6 @@ SCHEMA_MINIMAL_REQUIRED = [ CANCEL_REASON = { "TIMEOUT": "cancelled due to timeout", "PARTIALLY_FILLED": "partially filled - keeping order open", - "ALL_CANCELLED": "cancelled (all unfilled orders cancelled)", + "ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)", "CANCELLED_ON_EXCHANGE": "cancelled on exchange", }