From a9714727b11c8de835aa1012de0e593fbcaebff1 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 11:54:44 -0400 Subject: [PATCH 01/45] feat: add paused state to initial state in schema --- build_helpers/schema.json | 1 + 1 file changed, 1 insertion(+) diff --git a/build_helpers/schema.json b/build_helpers/schema.json index c5bbeb4ef..bc4f2e6f4 100644 --- a/build_helpers/schema.json +++ b/build_helpers/schema.json @@ -1032,6 +1032,7 @@ "type": "string", "enum": [ "running", + "paused", "stopped" ] }, From bbf0ac83a172bc3a8ea71c5c1dd0a0386c2c2f85 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 12:01:28 -0400 Subject: [PATCH 02/45] feat: add paused state to State class --- freqtrade/enums/state.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/enums/state.py b/freqtrade/enums/state.py index 1ce486920..20b3766b9 100644 --- a/freqtrade/enums/state.py +++ b/freqtrade/enums/state.py @@ -7,8 +7,9 @@ class State(Enum): """ RUNNING = 1 - STOPPED = 2 - RELOAD_CONFIG = 3 + PAUSED = 2 + STOPPED = 3 + RELOAD_CONFIG = 4 def __str__(self): return f"{self.name.lower()}" From db57f8392220f96f301687ad548198a9f886e891 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 16:49:05 -0400 Subject: [PATCH 03/45] feat: add paused state handler to rpc --- freqtrade/rpc/rpc.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 075ddd374..77356e26f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -836,6 +836,21 @@ class RPC: self._freqtrade.state = State.RUNNING return {"status": "starting trader ..."} + def _rpc_pause(self) -> dict[str, str]: + """Handler for pause""" + if self._freqtrade.state == State.PAUSED: + return {"status": "already paused"} + + if self._freqtrade.state == State.RUNNING: + self._freqtrade.state = State.PAUSED + return {"status": "pausing trader ..."} + + if self._freqtrade.state == State.STOPPED: + self._freqtrade.state = State.PAUSED + return {"status": "starting bot with trader in paused state..."} + + return {"status": "pausing trader ..."} + def _rpc_stop(self) -> dict[str, str]: """Handler for stop""" if self._freqtrade.state == State.RUNNING: From f46c8cdc4a20bc8bb5021bca76659e24961f15f2 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 16:58:23 -0400 Subject: [PATCH 04/45] feat: add paused state path to api --- freqtrade/rpc/api_server/api_v1.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index afab46cc8..cb4a169bf 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -364,6 +364,11 @@ def start(rpc: RPC = Depends(get_rpc)): return rpc._rpc_start() +@router.post("/pause", response_model=StatusMsg, tags=["botcontrol"]) +def pause(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_pause() + + @router.post("/stop", response_model=StatusMsg, tags=["botcontrol"]) def stop(rpc: RPC = Depends(get_rpc)): return rpc._rpc_stop() From d09160fff805ca662d1be47797d94c2f16cf0966 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 17:30:32 -0400 Subject: [PATCH 05/45] feat: allow entry only if state is running --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7bfeab592..97e7e88d2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -300,7 +300,7 @@ class FreqtradeBot(LoggingMixin): self.process_open_trade_positions() # Then looking for entry opportunities - if self.get_free_open_trades(): + if self.state == State.RUNNING and self.get_free_open_trades(): self.enter_positions() self._schedule.run_pending() Trade.commit() From fe03bf7ce092a704ef437c51f0dc759e0c8ac557 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 18:16:31 -0400 Subject: [PATCH 06/45] test: add test_worker_paused --- tests/freqtradebot/test_worker.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/freqtradebot/test_worker.py b/tests/freqtradebot/test_worker.py index 1dfdca5b2..0fc129a5b 100644 --- a/tests/freqtradebot/test_worker.py +++ b/tests/freqtradebot/test_worker.py @@ -38,6 +38,26 @@ def test_worker_running(mocker, default_conf, caplog) -> None: assert isinstance(worker.freqtrade.strategy.dp, DataProvider) +def test_worker_paused(mocker, default_conf, caplog) -> None: + mock_throttle = MagicMock() + mocker.patch("freqtrade.worker.Worker._throttle", mock_throttle) + mocker.patch("freqtrade.persistence.Trade.stoploss_reinitialization", MagicMock()) + + worker = get_patched_worker(mocker, default_conf) + + state = worker._worker(old_state=State.RUNNING) + worker.freqtrade.state = State.PAUSED + state = worker._worker(old_state=None) + + assert state is State.PAUSED + assert log_has("Changing state to: PAUSED", caplog) + assert mock_throttle.call_count == 1 + # Check strategy is loaded, and received a dataprovider object + assert worker.freqtrade.strategy + assert worker.freqtrade.strategy.dp + assert isinstance(worker.freqtrade.strategy.dp, DataProvider) + + def test_worker_stopped(mocker, default_conf, caplog) -> None: mock_throttle = MagicMock() mocker.patch("freqtrade.worker.Worker._throttle", mock_throttle) From 5a1f96d35c3179d2df458baca061b52d02656792 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 18:45:49 -0400 Subject: [PATCH 07/45] doc: update the doc with newly added paused state --- docs/advanced-setup.md | 2 +- docs/configuration.md | 2 +- docs/rest-api.md | 4 ++++ docs/telegram-usage.md | 7 ++++++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index b7ab86eac..967beedbd 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -177,7 +177,7 @@ sudo loginctl enable-linger "$USER" If you run the bot as a service, you can use systemd service manager as a software watchdog monitoring freqtrade bot state and restarting it in the case of failures. If the `internals.sd_notify` parameter is set to true in the configuration or the `--sd-notify` command line option is used, the bot will send keep-alive ping messages to systemd -using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running or Stopped) +using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running or Paused or Stopped) when it changes. The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd diff --git a/docs/configuration.md b/docs/configuration.md index f76b9360d..83591ea63 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -266,7 +266,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.
*Defaults to `freqtrade`*
**Datatype:** String | `external_message_consumer` | Enable [Producer/Consumer mode](producer-consumer.md) for more details.
**Datatype:** Dict | | **Other** -| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command.
*Defaults to `stopped`.*
**Datatype:** Enum, either `stopped` or `running` +| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command.
*Defaults to `stopped`.*
**Datatype:** Enum, either `running` or `paused` or `stopped` | `force_entry_enable` | Enables the RPC Commands to force a Trade entry. More information below.
**Datatype:** Boolean | `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `False`*.
**Datatype:** Boolean | `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second.
*Defaults to `5` seconds.*
**Datatype:** Positive Integer diff --git a/docs/rest-api.md b/docs/rest-api.md index 10e4534c0..cea4d2ee9 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -268,6 +268,9 @@ show_config start Start the bot if it's in the stopped state. +pause + Pause the bot if it's in the running state. If triggered on stopped state will handle open positions. + stats Return the stats report (durations, sell-reasons). @@ -333,6 +336,7 @@ All endpoints in the below table need to be prefixed with the base URL of the AP |-----------|--------|--------------------------| | `/ping` | GET | Simple command testing the API Readiness - requires no authentication. | `/start` | POST | Starts the trader. +| `/pause` | POST | Pause the trader. Gracefully handle open trades according to their rules. Do not enter new positions. | `/stop` | POST | Stops the trader. | `/stopbuy` | POST | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/reload_config` | POST | Reloads the configuration file. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index c7c434140..7dedd9d51 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -188,7 +188,7 @@ You can create your own keyboard in `config.json`: !!! Note "Supported Commands" Only the following commands are allowed. Command arguments are not supported! - `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir` + `/start`, `/pause`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopentry`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version`, `/marketdir` ## Telegram commands @@ -200,6 +200,7 @@ official commands. You can ask at any moment for help with `/help`. |----------|-------------| | **System commands** | `/start` | Starts the trader +| `/pause` | Pause the trader. Gracefully handle open trades according to their rules. Do not enter new positions. | `/stop` | Stops the trader | `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/reload_config` | Reloads the configuration file @@ -250,6 +251,10 @@ Below, example of Telegram message you will receive for each command. > **Status:** `running` +### /pause + +> **Status:** `paused` + ### /stop > `Stopping trader ...` From 47151e77e14f3f08b1187ebf25f22e544cec5ca9 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 19:02:48 -0400 Subject: [PATCH 08/45] feat: add paused state to telegram commands --- freqtrade/rpc/telegram.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 739a2af86..6149f2511 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -170,7 +170,7 @@ class Telegram(RPCHandler): self._keyboard: list[list[str | KeyboardButton]] = [ ["/daily", "/profit", "/balance"], ["/status", "/status table", "/performance"], - ["/count", "/start", "/stop", "/help"], + ["/start", "/pause", "/stop", "/help"], ] # do not allow commands with mandatory arguments and critical cmds # TODO: DRY! - its not good to list all valid cmds here. But otherwise @@ -178,6 +178,7 @@ class Telegram(RPCHandler): # problem in _help()). valid_keys: list[str] = [ r"/start$", + r"/pause$", r"/stop$", r"/status$", r"/status table$", @@ -1244,6 +1245,18 @@ class Telegram(RPCHandler): msg = self._rpc._rpc_start() await self._send_msg(f"Status: `{msg['status']}`") + @authorized_only + async def _pause(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /pause. + pauses entry positions on TradeThread + :param bot: telegram bot + :param update: message update + :return: None + """ + msg = self._rpc._rpc_pause() + await self._send_msg(f"Status: `{msg['status']}`") + @authorized_only async def _stop(self, update: Update, context: CallbackContext) -> None: """ @@ -1829,6 +1842,7 @@ class Telegram(RPCHandler): "_Bot Control_\n" "------------\n" "*/start:* `Starts the trader`\n" + "*/pause:* `Pause the new entries for trader, but handles open trades gracefully`\n" "*/stop:* `Stops the trader`\n" "*/stopentry:* `Stops entering, but handles open trades gracefully` \n" "*/forceexit |all:* `Instantly exits the given trade or all trades, " From f70815dd8e5558ddd32f934f5d39d34ec5e133a1 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 19:04:14 -0400 Subject: [PATCH 09/45] test: update test_rpc_telegram with new paused state telegram commands --- tests/rpc/test_rpc_telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 71ce557e2..398bd5262 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2817,7 +2817,7 @@ async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: default_keys_list = [ ["/daily", "/profit", "/balance"], ["/status", "/status table", "/performance"], - ["/count", "/start", "/stop", "/help"], + ["/start", "/pause", "/stop", "/help"], ] default_keyboard = ReplyKeyboardMarkup(default_keys_list) From 3116fd34cf3073c1029af969c64ed02be636339c Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 19:18:57 -0400 Subject: [PATCH 10/45] chore: update wording on initial_state description field of schema --- build_helpers/schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_helpers/schema.json b/build_helpers/schema.json index bc4f2e6f4..63fb9d6ec 100644 --- a/build_helpers/schema.json +++ b/build_helpers/schema.json @@ -1028,7 +1028,7 @@ "type": "boolean" }, "initial_state": { - "description": "Initial state of the system.", + "description": "Initial state of the trading system.", "type": "string", "enum": [ "running", From 728656844778ce9f3bf92e94c31d5e65642f25e0 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 19:49:45 -0400 Subject: [PATCH 11/45] chore: change order of initial_states schema enum --- build_helpers/schema.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build_helpers/schema.json b/build_helpers/schema.json index 63fb9d6ec..ec2e8b2bf 100644 --- a/build_helpers/schema.json +++ b/build_helpers/schema.json @@ -1032,8 +1032,8 @@ "type": "string", "enum": [ "running", - "paused", - "stopped" + "stopped", + "paused" ] }, "force_entry_enable": { From 50d60fad89fe5ef7a9a61a558ce08e38d712c939 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 21 Mar 2025 20:13:04 -0400 Subject: [PATCH 12/45] chore: remove paused state from schema.json enum --- build_helpers/schema.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build_helpers/schema.json b/build_helpers/schema.json index ec2e8b2bf..02fadc6cf 100644 --- a/build_helpers/schema.json +++ b/build_helpers/schema.json @@ -1032,8 +1032,7 @@ "type": "string", "enum": [ "running", - "stopped", - "paused" + "stopped" ] }, "force_entry_enable": { From 324aada2bce5834be08e6cd9e4378bb4123aa282 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sat, 22 Mar 2025 23:24:39 -0400 Subject: [PATCH 13/45] feat: add paused state to config schema, restore it in generated shema.json --- build_helpers/schema.json | 1 + freqtrade/configuration/config_schema.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/build_helpers/schema.json b/build_helpers/schema.json index 02fadc6cf..63fb9d6ec 100644 --- a/build_helpers/schema.json +++ b/build_helpers/schema.json @@ -1032,6 +1032,7 @@ "type": "string", "enum": [ "running", + "paused", "stopped" ] }, diff --git a/freqtrade/configuration/config_schema.py b/freqtrade/configuration/config_schema.py index 166d4f4f6..3b0be5c8d 100644 --- a/freqtrade/configuration/config_schema.py +++ b/freqtrade/configuration/config_schema.py @@ -689,7 +689,7 @@ CONF_SCHEMA = { "initial_state": { "description": "Initial state of the system.", "type": "string", - "enum": ["running", "stopped"], + "enum": ["running", "paused", "stopped"], }, "force_entry_enable": { "description": "Force enable entry.", From 209c2e0ceb81ca8f12f97c1a8e4e7cacb7169bed Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sat, 22 Mar 2025 23:31:38 -0400 Subject: [PATCH 14/45] doc: paused state small wording fix --- docs/advanced-setup.md | 2 +- docs/configuration.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 967beedbd..ec25cb616 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -177,7 +177,7 @@ sudo loginctl enable-linger "$USER" If you run the bot as a service, you can use systemd service manager as a software watchdog monitoring freqtrade bot state and restarting it in the case of failures. If the `internals.sd_notify` parameter is set to true in the configuration or the `--sd-notify` command line option is used, the bot will send keep-alive ping messages to systemd -using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running or Paused or Stopped) +using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running, Paused or Stopped) when it changes. The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd diff --git a/docs/configuration.md b/docs/configuration.md index 83591ea63..89261c3d7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -266,7 +266,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.
*Defaults to `freqtrade`*
**Datatype:** String | `external_message_consumer` | Enable [Producer/Consumer mode](producer-consumer.md) for more details.
**Datatype:** Dict | | **Other** -| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command.
*Defaults to `stopped`.*
**Datatype:** Enum, either `running` or `paused` or `stopped` +| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command.
*Defaults to `stopped`.*
**Datatype:** Enum, either `running`, `paused` or `stopped` | `force_entry_enable` | Enables the RPC Commands to force a Trade entry. More information below.
**Datatype:** Boolean | `disable_dataframe_checks` | Disable checking the OHLCV dataframe returned from the strategy methods for correctness. Only use when intentionally changing the dataframe and understand what you are doing. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `False`*.
**Datatype:** Boolean | `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second.
*Defaults to `5` seconds.*
**Datatype:** Positive Integer From a4416b885afb1aff2385e2736a6469bfca176a23 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sat, 22 Mar 2025 23:45:01 -0400 Subject: [PATCH 15/45] feat: _rpc_stopentry to handle paused state, remove _rpc_pause --- freqtrade/rpc/rpc.py | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 77356e26f..d6776b5c8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -836,21 +836,6 @@ class RPC: self._freqtrade.state = State.RUNNING return {"status": "starting trader ..."} - def _rpc_pause(self) -> dict[str, str]: - """Handler for pause""" - if self._freqtrade.state == State.PAUSED: - return {"status": "already paused"} - - if self._freqtrade.state == State.RUNNING: - self._freqtrade.state = State.PAUSED - return {"status": "pausing trader ..."} - - if self._freqtrade.state == State.STOPPED: - self._freqtrade.state = State.PAUSED - return {"status": "starting bot with trader in paused state..."} - - return {"status": "pausing trader ..."} - def _rpc_stop(self) -> dict[str, str]: """Handler for stop""" if self._freqtrade.state == State.RUNNING: @@ -869,11 +854,14 @@ class RPC: Handler to stop buying, but handle open trades gracefully. """ if self._freqtrade.state == State.RUNNING: - # Set 'max_open_trades' to 0 - self._freqtrade.config["max_open_trades"] = 0 - self._freqtrade.strategy.max_open_trades = 0 + self._freqtrade.state = State.PAUSED + return {"status": "pausing trader ..."} - return {"status": "No more entries will occur from now. Run /reload_config to reset."} + if self._freqtrade.state == State.STOPPED: + self._freqtrade.state = State.PAUSED + return {"status": "starting bot with trader in paused state..."} + + return {"status": "No more entries will occur from now. Run /start to enable entries"} def _rpc_reload_trade_from_exchange(self, trade_id: int) -> dict[str, str]: """ From 1d44f75659f3713dbe311b278417c79fbfd83119 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sat, 22 Mar 2025 23:50:27 -0400 Subject: [PATCH 16/45] feat: update api, change /pause from individual route to alias with /stopentry and /stopbuy --- freqtrade/rpc/api_server/api_v1.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index cb4a169bf..78c871ddd 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -364,16 +364,12 @@ def start(rpc: RPC = Depends(get_rpc)): return rpc._rpc_start() -@router.post("/pause", response_model=StatusMsg, tags=["botcontrol"]) -def pause(rpc: RPC = Depends(get_rpc)): - return rpc._rpc_pause() - - @router.post("/stop", response_model=StatusMsg, tags=["botcontrol"]) def stop(rpc: RPC = Depends(get_rpc)): return rpc._rpc_stop() +@router.post("/pause", response_model=StatusMsg, tags=["botcontrol"]) @router.post("/stopentry", response_model=StatusMsg, tags=["botcontrol"]) @router.post("/stopbuy", response_model=StatusMsg, tags=["botcontrol"]) def stop_buy(rpc: RPC = Depends(get_rpc)): From 46fab5537873e1eac346fb182d20c4c22495f219 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sat, 22 Mar 2025 23:57:07 -0400 Subject: [PATCH 17/45] feat: /pause telegram command now use _rpc_stopentry function --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6149f2511..678c835e1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1254,7 +1254,7 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - msg = self._rpc._rpc_pause() + msg = self._rpc._rpc_stopentry() await self._send_msg(f"Status: `{msg['status']}`") @authorized_only From 4f4f927b97b60e26d44b3a13ec4b91c109b77814 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sat, 22 Mar 2025 23:58:57 -0400 Subject: [PATCH 18/45] chore: wording alignement update between schema.json and .py --- build_helpers/schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build_helpers/schema.json b/build_helpers/schema.json index 63fb9d6ec..bc4f2e6f4 100644 --- a/build_helpers/schema.json +++ b/build_helpers/schema.json @@ -1028,7 +1028,7 @@ "type": "boolean" }, "initial_state": { - "description": "Initial state of the trading system.", + "description": "Initial state of the system.", "type": "string", "enum": [ "running", From 3ec72f88e2a7a55b3e95c259eeaba196f4023d32 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 00:14:22 -0400 Subject: [PATCH 19/45] test: update test_rpc_stopentry, small wording change on _rpc_stopentry status --- freqtrade/rpc/rpc.py | 2 +- tests/rpc/test_rpc.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d6776b5c8..2423a7fdb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -861,7 +861,7 @@ class RPC: self._freqtrade.state = State.PAUSED return {"status": "starting bot with trader in paused state..."} - return {"status": "No more entries will occur from now. Run /start to enable entries"} + return {"status": "No more entries will occur from now. Run /start to enable entries."} def _rpc_reload_trade_from_exchange(self, trade_id: int) -> dict[str, str]: """ diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index cb67d089e..8f4245e50 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -813,12 +813,12 @@ def test_rpc_stopentry(mocker, default_conf) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) - freqtradebot.state = State.RUNNING + freqtradebot.state = State.PAUSED - assert freqtradebot.config["max_open_trades"] != 0 result = rpc._rpc_stopentry() - assert {"status": "No more entries will occur from now. Run /reload_config to reset."} == result - assert freqtradebot.config["max_open_trades"] == 0 + assert { + "status": "No more entries will occur from now. Run /start to enable entries." + } == result def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: From 85772ac7f7c7306ebc361b9997a4e33b573042c5 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 00:27:52 -0400 Subject: [PATCH 20/45] test: update stopentry related tests for rpc api and telegram --- tests/rpc/test_rpc_apiserver.py | 11 ++++++----- tests/rpc/test_rpc_telegram.py | 9 +++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 99d8350e9..5f4c84db2 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -535,21 +535,22 @@ def test_api_reloadconf(botclient): def test_api_stopentry(botclient): ftbot, client = botclient - assert ftbot.config["max_open_trades"] != 0 + + rc = client_post(client, f"{BASE_URI}/stopbuy") + assert_response(rc) + assert rc.json() == {"status": "pausing trader ..."} rc = client_post(client, f"{BASE_URI}/stopbuy") assert_response(rc) assert rc.json() == { - "status": "No more entries will occur from now. Run /reload_config to reset." + "status": "No more entries will occur from now. Run /start to enable entries." } - assert ftbot.config["max_open_trades"] == 0 rc = client_post(client, f"{BASE_URI}/stopentry") assert_response(rc) assert rc.json() == { - "status": "No more entries will occur from now. Run /reload_config to reset." + "status": "No more entries will occur from now. Run /start to enable entries." } - assert ftbot.config["max_open_trades"] == 0 def test_api_balance(botclient, mocker, rpc_balance, tickers): diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 398bd5262..e2e0137af 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1225,14 +1225,11 @@ async def test_stop_handle_already_stopped(default_conf, update, mocker) -> None async def test_stopbuy_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - assert freqtradebot.config["max_open_trades"] != 0 + assert freqtradebot.state == State.RUNNING await telegram._stopentry(update=update, context=MagicMock()) - assert freqtradebot.config["max_open_trades"] == 0 + assert freqtradebot.state == State.PAUSED assert msg_mock.call_count == 1 - assert ( - "No more entries will occur from now. Run /reload_config to reset." - in msg_mock.call_args_list[0][0][0] - ) + assert "pausing trader ..." in msg_mock.call_args_list[0][0][0] async def test_reload_config_handle(default_conf, update, mocker) -> None: From b8dffe0eb08b8302ef9aaa2b74cda6e478f56eac Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 04:09:18 -0400 Subject: [PATCH 21/45] chore: revert telegram keyboard list, remove paused button --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 678c835e1..a595d3d15 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -170,7 +170,7 @@ class Telegram(RPCHandler): self._keyboard: list[list[str | KeyboardButton]] = [ ["/daily", "/profit", "/balance"], ["/status", "/status table", "/performance"], - ["/start", "/pause", "/stop", "/help"], + ["/count", "/start", "/stop", "/help"], ] # do not allow commands with mandatory arguments and critical cmds # TODO: DRY! - its not good to list all valid cmds here. But otherwise From 06c4b661f7371ecf6fac642acbc01be982369a98 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 14:17:06 -0400 Subject: [PATCH 22/45] test: fix test__send_msg_keyboard after pause key removal --- tests/rpc/test_rpc_telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index e2e0137af..65d2c32b7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2814,7 +2814,7 @@ async def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: default_keys_list = [ ["/daily", "/profit", "/balance"], ["/status", "/status table", "/performance"], - ["/start", "/pause", "/stop", "/help"], + ["/count", "/start", "/stop", "/help"], ] default_keyboard = ReplyKeyboardMarkup(default_keys_list) From 445c3a67db5e27f89a39810d37ea01c3a458c27d Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 14:31:36 -0400 Subject: [PATCH 23/45] add pause in telegram commandHandler init --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a595d3d15..3f1c70a6c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -294,7 +294,7 @@ class Telegram(RPCHandler): CommandHandler(["unlock", "delete_locks"], self._delete_locks), CommandHandler(["reload_config", "reload_conf"], self._reload_config), CommandHandler(["show_config", "show_conf"], self._show_config), - CommandHandler(["stopbuy", "stopentry"], self._stopentry), + CommandHandler(["stopbuy", "stopentry", "pause"], self._stopentry), CommandHandler("whitelist", self._whitelist), CommandHandler("blacklist", self._blacklist), CommandHandler(["blacklist_delete", "bl_delete"], self._blacklist_delete), From 0adb264c9bd162460205c5c90d6b3a736fc1e2c5 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 14:43:05 -0400 Subject: [PATCH 24/45] test: update test_telegram_init with pause --- tests/rpc/test_rpc_telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 65d2c32b7..2715426c2 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -169,7 +169,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['stats'], ['daily'], ['weekly'], ['monthly'], " "['count'], ['locks'], ['delete_locks', 'unlock'], " "['reload_conf', 'reload_config'], ['show_conf', 'show_config'], " - "['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " + "['pause', 'stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " "['bl_delete', 'blacklist_delete'], " "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir'], " "['order'], ['list_custom_data'], ['tg_info']]" From b6b3429b62093eb9aa6d16ba7d341d070ee967ce Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 14:44:30 -0400 Subject: [PATCH 25/45] chore: remove _pause handler to use already existing _stopentry handler --- freqtrade/rpc/telegram.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3f1c70a6c..36b873178 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1245,18 +1245,6 @@ class Telegram(RPCHandler): msg = self._rpc._rpc_start() await self._send_msg(f"Status: `{msg['status']}`") - @authorized_only - async def _pause(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /pause. - pauses entry positions on TradeThread - :param bot: telegram bot - :param update: message update - :return: None - """ - msg = self._rpc._rpc_stopentry() - await self._send_msg(f"Status: `{msg['status']}`") - @authorized_only async def _stop(self, update: Update, context: CallbackContext) -> None: """ @@ -1284,7 +1272,7 @@ class Telegram(RPCHandler): @authorized_only async def _stopentry(self, update: Update, context: CallbackContext) -> None: """ - Handler for /stop_buy. + Handler for /stop_buy /stop_entry and /pause. Sets max_open_trades to 0 and gracefully sells all open trades :param bot: telegram bot :param update: message update From df4b44d09e105af9d95dc6e01796bdac0950acdd Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 15:12:42 -0400 Subject: [PATCH 26/45] feat: update worker to make paused state capable of bot startup and running process --- freqtrade/worker.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 61d5d9a64..05ff8fdf2 100644 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -96,7 +96,7 @@ class Worker: logger.info( f"Changing state{f' from {old_state.name}' if old_state else ''} to: {state.name}" ) - if state == State.RUNNING: + if state == State.RUNNING or state == State.PAUSED: self.freqtrade.startup() if state == State.STOPPED: @@ -112,9 +112,10 @@ class Worker: self._throttle(func=self._process_stopped, throttle_secs=self._throttle_secs) - elif state == State.RUNNING: + elif state == State.RUNNING or state == State.PAUSED: + state_str = "RUNNING" if state == State.RUNNING else "PAUSED" # Ping systemd watchdog before throttling - self._notify("WATCHDOG=1\nSTATUS=State: RUNNING.") + self._notify(f"WATCHDOG=1\nSTATUS=State: {state_str}.") # Use an offset of 1s to ensure a new candle has been issued self._throttle( From 53bd2d71a0d3e2131af86ec72f4223ed0b52d74c Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 15:13:57 -0400 Subject: [PATCH 27/45] test: update test_worker_paused --- tests/freqtradebot/test_worker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/freqtradebot/test_worker.py b/tests/freqtradebot/test_worker.py index 0fc129a5b..1e1326b50 100644 --- a/tests/freqtradebot/test_worker.py +++ b/tests/freqtradebot/test_worker.py @@ -45,12 +45,11 @@ def test_worker_paused(mocker, default_conf, caplog) -> None: worker = get_patched_worker(mocker, default_conf) - state = worker._worker(old_state=State.RUNNING) worker.freqtrade.state = State.PAUSED - state = worker._worker(old_state=None) + state = worker._worker(old_state=State.RUNNING) assert state is State.PAUSED - assert log_has("Changing state to: PAUSED", caplog) + assert log_has("Changing state from RUNNING to: PAUSED", caplog) assert mock_throttle.call_count == 1 # Check strategy is loaded, and received a dataprovider object assert worker.freqtrade.strategy From 16576d37b1f22c23f321e134b4e72d44ce79831d Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 15:25:34 -0400 Subject: [PATCH 28/45] feat: allow force_exit, rpc_cancel_open_order and _rpc_count in paused state --- freqtrade/rpc/rpc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2423a7fdb..94a2f9d16 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -933,7 +933,7 @@ class RPC: Sells the given trade at current price """ - if self._freqtrade.state != State.RUNNING: + if self._freqtrade.state == State.STOPPED: raise RPCException("trader is not running") with self._freqtrade._exit_lock: @@ -1049,7 +1049,7 @@ class RPC: raise RPCException(f"Failed to enter position for {pair}.") def _rpc_cancel_open_order(self, trade_id: int): - if self._freqtrade.state != State.RUNNING: + if self._freqtrade.state == State.STOPPED: raise RPCException("trader is not running") with self._freqtrade._exit_lock: # Query for trade @@ -1217,7 +1217,7 @@ class RPC: def _rpc_count(self) -> dict[str, float]: """Returns the number of trades running""" - if self._freqtrade.state != State.RUNNING: + if self._freqtrade.state == State.STOPPED: raise RPCException("trader is not running") trades = Trade.get_open_trades() From 394535c2e811c02cf34e63bf0a62e59912f9c33a Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 22:13:09 -0400 Subject: [PATCH 29/45] chore: update telegram _stopentry handling function description --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 36b873178..df85b5b17 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1273,7 +1273,7 @@ class Telegram(RPCHandler): async def _stopentry(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop_buy /stop_entry and /pause. - Sets max_open_trades to 0 and gracefully sells all open trades + Sets bot state to paused :param bot: telegram bot :param update: message update :return: None From 31625befd132bc74fd61e3209d1112af523fbf94 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 22:19:16 -0400 Subject: [PATCH 30/45] feat: trigger startup function only when the bot leave the stopped state --- freqtrade/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 05ff8fdf2..5647ae114 100644 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -96,7 +96,7 @@ class Worker: logger.info( f"Changing state{f' from {old_state.name}' if old_state else ''} to: {state.name}" ) - if state == State.RUNNING or state == State.PAUSED: + if old_state == State.STOPPED and (state == State.RUNNING or state == State.PAUSED): self.freqtrade.startup() if state == State.STOPPED: From b2898cf742ab78f50e1dd2e50370b7ba37c894a9 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Sun, 23 Mar 2025 22:27:49 -0400 Subject: [PATCH 31/45] feat: add stop state change from pause state --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 94a2f9d16..b3f51a0bd 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -838,7 +838,7 @@ class RPC: def _rpc_stop(self) -> dict[str, str]: """Handler for stop""" - if self._freqtrade.state == State.RUNNING: + if self._freqtrade.state == State.RUNNING or self._freqtrade.state == State.PAUSED: self._freqtrade.state = State.STOPPED return {"status": "stopping trader ..."} From 285867f8c61da1c12f90dedc0a6f8544455b89d1 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 25 Mar 2025 11:35:48 -0400 Subject: [PATCH 32/45] test: add test_worker_lifecycle --- tests/freqtradebot/test_worker.py | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/freqtradebot/test_worker.py b/tests/freqtradebot/test_worker.py index 1e1326b50..f97a9ff86 100644 --- a/tests/freqtradebot/test_worker.py +++ b/tests/freqtradebot/test_worker.py @@ -3,6 +3,7 @@ import time from datetime import timedelta from unittest.mock import MagicMock, PropertyMock +import pytest import time_machine from freqtrade.data.dataprovider import DataProvider @@ -69,6 +70,39 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None: assert mock_throttle.call_count == 1 +@pytest.mark.parametrize( + "old_state, target_state, expected_state, log_fragment", + [ + (State.STOPPED, State.PAUSED, State.PAUSED, "Changing state from STOPPED to: PAUSED"), + (State.RUNNING, State.PAUSED, State.PAUSED, "Changing state from RUNNING to: PAUSED"), + (State.PAUSED, State.RUNNING, State.RUNNING, "Changing state from PAUSED to: RUNNING"), + (State.PAUSED, State.STOPPED, State.STOPPED, "Changing state from PAUSED to: STOPPED"), + ], +) +def test_worker_lifecycle( + mocker, default_conf, caplog, old_state, target_state, expected_state, log_fragment +): + mock_throttle = mocker.MagicMock() + mocker.patch("freqtrade.worker.Worker._throttle", mock_throttle) + mocker.patch("freqtrade.persistence.Trade.stoploss_reinitialization", mocker.MagicMock()) + + worker = get_patched_worker(mocker, default_conf) + worker.freqtrade.state = target_state + + new_state = worker._worker(old_state=old_state) + + assert new_state is expected_state + assert log_has(log_fragment, caplog) + assert mock_throttle.call_count == 1 + + # For any state where the strategy should be initialized + if expected_state in (State.RUNNING, State.PAUSED): + assert worker.freqtrade.strategy + assert isinstance(worker.freqtrade.strategy.dp, DataProvider) + else: + assert new_state is State.STOPPED + + def test_throttle(mocker, default_conf, caplog) -> None: def throttled_func(): return 42 From e77ce8d481c1e3bd06fd4874e2a0623bfcfad191 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 25 Mar 2025 11:39:31 -0400 Subject: [PATCH 33/45] chore: cosmetic worker condition refactoring --- freqtrade/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 5647ae114..5174680fb 100644 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -112,7 +112,7 @@ class Worker: self._throttle(func=self._process_stopped, throttle_secs=self._throttle_secs) - elif state == State.RUNNING or state == State.PAUSED: + elif state in (State.RUNNING, State.PAUSED): state_str = "RUNNING" if state == State.RUNNING else "PAUSED" # Ping systemd watchdog before throttling self._notify(f"WATCHDOG=1\nSTATUS=State: {state_str}.") From 543c77fe004301eed0955ed8d1d2a1987f4968aa Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 25 Mar 2025 11:43:23 -0400 Subject: [PATCH 34/45] chore: slight refactor on rpc stop handler --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b3f51a0bd..e4088a806 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -838,7 +838,7 @@ class RPC: def _rpc_stop(self) -> dict[str, str]: """Handler for stop""" - if self._freqtrade.state == State.RUNNING or self._freqtrade.state == State.PAUSED: + if self._freqtrade.state != State.STOPPED: self._freqtrade.state = State.STOPPED return {"status": "stopping trader ..."} From 4fa8c3f9ab1fa0a1f50adcc06f00fe1238ae9e77 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 25 Mar 2025 11:55:57 -0400 Subject: [PATCH 35/45] fix: worker startup condition --- freqtrade/worker.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 5174680fb..241f7de29 100644 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -96,7 +96,10 @@ class Worker: logger.info( f"Changing state{f' from {old_state.name}' if old_state else ''} to: {state.name}" ) - if old_state == State.STOPPED and (state == State.RUNNING or state == State.PAUSED): + if state in (State.RUNNING, State.PAUSED) and old_state not in ( + State.RUNNING, + State.PAUSED, + ): self.freqtrade.startup() if state == State.STOPPED: From 0553486e559d6e140ec13b3ce306464f20a23121 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 25 Mar 2025 12:09:24 -0400 Subject: [PATCH 36/45] feat: prevent raising position size using adjustment position in paused state --- freqtrade/freqtradebot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 97e7e88d2..d67a2aa3a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -781,6 +781,10 @@ class FreqtradeBot(LoggingMixin): ) if stake_amount is not None and stake_amount > 0.0: + if self.state == State.PAUSED: + logger.debug("Position adjustment aborted because the bot is in PAUSED state") + return + # We should increase our position if self.strategy.max_entry_position_adjustment > -1: count_of_entries = trade.nr_of_successful_entries From 2b01d2e06b30176714db1c728ef38326d9059e08 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 25 Mar 2025 13:38:06 -0400 Subject: [PATCH 37/45] feat: display current status in notification after reload config --- freqtrade/worker.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 241f7de29..8b754ddc3 100644 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -225,7 +225,7 @@ class Worker: # Load and validate config and create new instance of the bot self._init(True) - self.freqtrade.notify_status("config reloaded") + self.freqtrade.notify_status(f"{State(self.freqtrade.state)} after config reloaded") # Tell systemd that we completed reconfiguration self._notify("READY=1") From 948487518d46995ba3d551cb83b4b8f7c7b0f643 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Mar 2025 06:44:39 +0100 Subject: [PATCH 38/45] test: improve test_worker_lifecycle --- tests/freqtradebot/test_worker.py | 48 ++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/tests/freqtradebot/test_worker.py b/tests/freqtradebot/test_worker.py index f97a9ff86..da03eba80 100644 --- a/tests/freqtradebot/test_worker.py +++ b/tests/freqtradebot/test_worker.py @@ -71,20 +71,53 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None: @pytest.mark.parametrize( - "old_state, target_state, expected_state, log_fragment", + "old_state, target_state, expected_state,startup_call, log_fragment", [ - (State.STOPPED, State.PAUSED, State.PAUSED, "Changing state from STOPPED to: PAUSED"), - (State.RUNNING, State.PAUSED, State.PAUSED, "Changing state from RUNNING to: PAUSED"), - (State.PAUSED, State.RUNNING, State.RUNNING, "Changing state from PAUSED to: RUNNING"), - (State.PAUSED, State.STOPPED, State.STOPPED, "Changing state from PAUSED to: STOPPED"), + (State.STOPPED, State.PAUSED, State.PAUSED, True, "Changing state from STOPPED to: PAUSED"), + ( + State.RUNNING, + State.PAUSED, + State.PAUSED, + False, + "Changing state from RUNNING to: PAUSED", + ), + ( + State.PAUSED, + State.RUNNING, + State.RUNNING, + False, + "Changing state from PAUSED to: RUNNING", + ), + ( + State.PAUSED, + State.STOPPED, + State.STOPPED, + False, + "Changing state from PAUSED to: STOPPED", + ), + ( + State.RELOAD_CONFIG, + State.RUNNING, + State.RUNNING, + True, + "Changing state from RELOAD_CONFIG to: RUNNING", + ), ], ) def test_worker_lifecycle( - mocker, default_conf, caplog, old_state, target_state, expected_state, log_fragment + mocker, + default_conf, + caplog, + old_state, + target_state, + expected_state, + startup_call, + log_fragment, ): mock_throttle = mocker.MagicMock() mocker.patch("freqtrade.worker.Worker._throttle", mock_throttle) - mocker.patch("freqtrade.persistence.Trade.stoploss_reinitialization", mocker.MagicMock()) + mocker.patch("freqtrade.persistence.Trade.stoploss_reinitialization") + startup = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.startup") worker = get_patched_worker(mocker, default_conf) worker.freqtrade.state = target_state @@ -94,6 +127,7 @@ def test_worker_lifecycle( assert new_state is expected_state assert log_has(log_fragment, caplog) assert mock_throttle.call_count == 1 + assert startup.call_count == (1 if startup_call else 0) # For any state where the strategy should be initialized if expected_state in (State.RUNNING, State.PAUSED): From bb08880c4acf36ad66aec3ec4aa1f98f0f212dda Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Mar 2025 06:46:06 +0100 Subject: [PATCH 39/45] test: simplify test_worker_lifecycle --- tests/freqtradebot/test_worker.py | 41 +++++++++---------------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/tests/freqtradebot/test_worker.py b/tests/freqtradebot/test_worker.py index da03eba80..6423e63ee 100644 --- a/tests/freqtradebot/test_worker.py +++ b/tests/freqtradebot/test_worker.py @@ -71,36 +71,18 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None: @pytest.mark.parametrize( - "old_state, target_state, expected_state,startup_call, log_fragment", + "old_state,target_state,startup_call,log_fragment", [ - (State.STOPPED, State.PAUSED, State.PAUSED, True, "Changing state from STOPPED to: PAUSED"), - ( - State.RUNNING, - State.PAUSED, - State.PAUSED, - False, - "Changing state from RUNNING to: PAUSED", - ), - ( - State.PAUSED, - State.RUNNING, - State.RUNNING, - False, - "Changing state from PAUSED to: RUNNING", - ), - ( - State.PAUSED, - State.STOPPED, - State.STOPPED, - False, - "Changing state from PAUSED to: STOPPED", - ), + (State.STOPPED, State.PAUSED, True, "Changing state from STOPPED to: PAUSED"), + (State.RUNNING, State.PAUSED, False, "Changing state from RUNNING to: PAUSED"), + (State.PAUSED, State.RUNNING, False, "Changing state from PAUSED to: RUNNING"), + (State.PAUSED, State.STOPPED, False, "Changing state from PAUSED to: STOPPED"), + (State.RELOAD_CONFIG, State.RUNNING, True, "Changing state from RELOAD_CONFIG to: RUNNING"), ( State.RELOAD_CONFIG, - State.RUNNING, - State.RUNNING, - True, - "Changing state from RELOAD_CONFIG to: RUNNING", + State.STOPPED, + False, + "Changing state from RELOAD_CONFIG to: STOPPED", ), ], ) @@ -110,7 +92,6 @@ def test_worker_lifecycle( caplog, old_state, target_state, - expected_state, startup_call, log_fragment, ): @@ -124,13 +105,13 @@ def test_worker_lifecycle( new_state = worker._worker(old_state=old_state) - assert new_state is expected_state + assert new_state is target_state assert log_has(log_fragment, caplog) assert mock_throttle.call_count == 1 assert startup.call_count == (1 if startup_call else 0) # For any state where the strategy should be initialized - if expected_state in (State.RUNNING, State.PAUSED): + if target_state in (State.RUNNING, State.PAUSED): assert worker.freqtrade.strategy assert isinstance(worker.freqtrade.strategy.dp, DataProvider) else: From 38feb90f9e9eba66f999ca0abe27da98e43a0999 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 28 Mar 2025 06:42:07 +0100 Subject: [PATCH 40/45] chore: update function naming --- freqtrade/rpc/api_server/api_v1.py | 4 ++-- freqtrade/rpc/rpc.py | 4 ++-- freqtrade/rpc/telegram.py | 6 +++--- tests/rpc/test_rpc.py | 4 ++-- tests/rpc/test_rpc_apiserver.py | 6 +++--- tests/rpc/test_rpc_telegram.py | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 78c871ddd..0081871f0 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -372,8 +372,8 @@ def stop(rpc: RPC = Depends(get_rpc)): @router.post("/pause", response_model=StatusMsg, tags=["botcontrol"]) @router.post("/stopentry", response_model=StatusMsg, tags=["botcontrol"]) @router.post("/stopbuy", response_model=StatusMsg, tags=["botcontrol"]) -def stop_buy(rpc: RPC = Depends(get_rpc)): - return rpc._rpc_stopentry() +def pause(rpc: RPC = Depends(get_rpc)): + return rpc._rpc_pause() @router.post("/reload_config", response_model=StatusMsg, tags=["botcontrol"]) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e4088a806..946dd9b13 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -849,9 +849,9 @@ class RPC: self._freqtrade.state = State.RELOAD_CONFIG return {"status": "Reloading config ..."} - def _rpc_stopentry(self) -> dict[str, str]: + def _rpc_pause(self) -> dict[str, str]: """ - Handler to stop buying, but handle open trades gracefully. + Handler to pause trading (stop entering new trades), but handle open trades gracefully. """ if self._freqtrade.state == State.RUNNING: self._freqtrade.state = State.PAUSED diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index df85b5b17..f3f4a4196 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -294,7 +294,7 @@ class Telegram(RPCHandler): CommandHandler(["unlock", "delete_locks"], self._delete_locks), CommandHandler(["reload_config", "reload_conf"], self._reload_config), CommandHandler(["show_config", "show_conf"], self._show_config), - CommandHandler(["stopbuy", "stopentry", "pause"], self._stopentry), + CommandHandler(["stopbuy", "stopentry", "pause"], self._pause), CommandHandler("whitelist", self._whitelist), CommandHandler("blacklist", self._blacklist), CommandHandler(["blacklist_delete", "bl_delete"], self._blacklist_delete), @@ -1270,7 +1270,7 @@ class Telegram(RPCHandler): await self._send_msg(f"Status: `{msg['status']}`") @authorized_only - async def _stopentry(self, update: Update, context: CallbackContext) -> None: + async def _pause(self, update: Update, context: CallbackContext) -> None: """ Handler for /stop_buy /stop_entry and /pause. Sets bot state to paused @@ -1278,7 +1278,7 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - msg = self._rpc._rpc_stopentry() + msg = self._rpc._rpc_pause() await self._send_msg(f"Status: `{msg['status']}`") @authorized_only diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 8f4245e50..bebd708cf 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -806,7 +806,7 @@ def test_rpc_stop(mocker, default_conf) -> None: assert freqtradebot.state == State.STOPPED -def test_rpc_stopentry(mocker, default_conf) -> None: +def test_rpc_pause(mocker, default_conf) -> None: mocker.patch("freqtrade.rpc.telegram.Telegram", MagicMock()) mocker.patch.multiple(EXMS, fetch_ticker=MagicMock()) @@ -815,7 +815,7 @@ def test_rpc_stopentry(mocker, default_conf) -> None: rpc = RPC(freqtradebot) freqtradebot.state = State.PAUSED - result = rpc._rpc_stopentry() + result = rpc._rpc_pause() assert { "status": "No more entries will occur from now. Run /start to enable entries." } == result diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5f4c84db2..963a39ec3 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -533,14 +533,14 @@ def test_api_reloadconf(botclient): assert ftbot.state == State.RELOAD_CONFIG -def test_api_stopentry(botclient): +def test_api_pause(botclient): ftbot, client = botclient - rc = client_post(client, f"{BASE_URI}/stopbuy") + rc = client_post(client, f"{BASE_URI}/pause") assert_response(rc) assert rc.json() == {"status": "pausing trader ..."} - rc = client_post(client, f"{BASE_URI}/stopbuy") + rc = client_post(client, f"{BASE_URI}/pause") assert_response(rc) assert rc.json() == { "status": "No more entries will occur from now. Run /start to enable entries." diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2715426c2..81a673ebc 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1222,11 +1222,11 @@ async def test_stop_handle_already_stopped(default_conf, update, mocker) -> None assert "already stopped" in msg_mock.call_args_list[0][0][0] -async def test_stopbuy_handle(default_conf, update, mocker) -> None: +async def test_pause_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) assert freqtradebot.state == State.RUNNING - await telegram._stopentry(update=update, context=MagicMock()) + await telegram._pause(update=update, context=MagicMock()) assert freqtradebot.state == State.PAUSED assert msg_mock.call_count == 1 assert "pausing trader ..." in msg_mock.call_args_list[0][0][0] From 58154d76aefee9490d19690ee0aa2d85ada52482 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 28 Mar 2025 02:31:18 -0400 Subject: [PATCH 41/45] chore: update paused state status message wording --- freqtrade/rpc/rpc.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 946dd9b13..b97f25c35 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -855,13 +855,20 @@ class RPC: """ if self._freqtrade.state == State.RUNNING: self._freqtrade.state = State.PAUSED - return {"status": "pausing trader ..."} + + if self._freqtrade.state == State.PAUSED: + return {"status": "paused, no entries will occur. Run /start to enable entries."} if self._freqtrade.state == State.STOPPED: self._freqtrade.state = State.PAUSED - return {"status": "starting bot with trader in paused state..."} + return { + "status": "starting bot with trader in paused state, no entries will occur. \ + Run /start to enable entries." + } - return {"status": "No more entries will occur from now. Run /start to enable entries."} + return { + "status": "paused, no more entries will occur from now. Run /start to enable entries." + } def _rpc_reload_trade_from_exchange(self, trade_id: int) -> dict[str, str]: """ From 3d8d2fc0c6744c032ed001acbba3ea0c35c22048 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 28 Mar 2025 02:32:15 -0400 Subject: [PATCH 42/45] test: update tests related to paused state status message wording --- tests/rpc/test_rpc.py | 4 +--- tests/rpc/test_rpc_apiserver.py | 10 +++------- tests/rpc/test_rpc_telegram.py | 5 ++++- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index bebd708cf..cf3d2cf18 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -816,9 +816,7 @@ def test_rpc_pause(mocker, default_conf) -> None: freqtradebot.state = State.PAUSED result = rpc._rpc_pause() - assert { - "status": "No more entries will occur from now. Run /start to enable entries." - } == result + assert {"status": "paused, no entries will occur. Run /start to enable entries."} == result def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 963a39ec3..7a2d9f4f9 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -538,19 +538,15 @@ def test_api_pause(botclient): rc = client_post(client, f"{BASE_URI}/pause") assert_response(rc) - assert rc.json() == {"status": "pausing trader ..."} + assert rc.json() == {"status": "paused, no entries will occur. Run /start to enable entries."} rc = client_post(client, f"{BASE_URI}/pause") assert_response(rc) - assert rc.json() == { - "status": "No more entries will occur from now. Run /start to enable entries." - } + assert rc.json() == {"status": "paused, no entries will occur. Run /start to enable entries."} rc = client_post(client, f"{BASE_URI}/stopentry") assert_response(rc) - assert rc.json() == { - "status": "No more entries will occur from now. Run /start to enable entries." - } + assert rc.json() == {"status": "paused, no entries will occur. Run /start to enable entries."} def test_api_balance(botclient, mocker, rpc_balance, tickers): diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 81a673ebc..61569d8f4 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1229,7 +1229,10 @@ async def test_pause_handle(default_conf, update, mocker) -> None: await telegram._pause(update=update, context=MagicMock()) assert freqtradebot.state == State.PAUSED assert msg_mock.call_count == 1 - assert "pausing trader ..." in msg_mock.call_args_list[0][0][0] + assert ( + "paused, no entries will occur. Run /start to enable entries." + in msg_mock.call_args_list[0][0][0] + ) async def test_reload_config_handle(default_conf, update, mocker) -> None: From 91ace759c58266516aea7fb998b1ad6689d6fd67 Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 28 Mar 2025 10:17:16 -0400 Subject: [PATCH 43/45] chore: update _rpc_pause remove specific fallback if bot alreadu paused --- freqtrade/rpc/rpc.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b97f25c35..185bda223 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -856,9 +856,6 @@ class RPC: if self._freqtrade.state == State.RUNNING: self._freqtrade.state = State.PAUSED - if self._freqtrade.state == State.PAUSED: - return {"status": "paused, no entries will occur. Run /start to enable entries."} - if self._freqtrade.state == State.STOPPED: self._freqtrade.state = State.PAUSED return { From 722d5b231953f1f57a1ca0a2012f0c5f41a875be Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Fri, 28 Mar 2025 10:18:21 -0400 Subject: [PATCH 44/45] test: update test of _rpc_pause after removal of specific fallback if bot already paused --- tests/rpc/test_rpc.py | 4 +++- tests/rpc/test_rpc_apiserver.py | 12 +++++++++--- tests/rpc/test_rpc_telegram.py | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index cf3d2cf18..8b9a0835a 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -816,7 +816,9 @@ def test_rpc_pause(mocker, default_conf) -> None: freqtradebot.state = State.PAUSED result = rpc._rpc_pause() - assert {"status": "paused, no entries will occur. Run /start to enable entries."} == result + assert { + "status": "paused, no more entries will occur from now. Run /start to enable entries." + } == result def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7a2d9f4f9..62a054313 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -538,15 +538,21 @@ def test_api_pause(botclient): rc = client_post(client, f"{BASE_URI}/pause") assert_response(rc) - assert rc.json() == {"status": "paused, no entries will occur. Run /start to enable entries."} + assert rc.json() == { + "status": "paused, no more entries will occur from now. Run /start to enable entries." + } rc = client_post(client, f"{BASE_URI}/pause") assert_response(rc) - assert rc.json() == {"status": "paused, no entries will occur. Run /start to enable entries."} + assert rc.json() == { + "status": "paused, no more entries will occur from now. Run /start to enable entries." + } rc = client_post(client, f"{BASE_URI}/stopentry") assert_response(rc) - assert rc.json() == {"status": "paused, no entries will occur. Run /start to enable entries."} + assert rc.json() == { + "status": "paused, no more entries will occur from now. Run /start to enable entries." + } def test_api_balance(botclient, mocker, rpc_balance, tickers): diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 61569d8f4..3409cc0e5 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1230,7 +1230,7 @@ async def test_pause_handle(default_conf, update, mocker) -> None: assert freqtradebot.state == State.PAUSED assert msg_mock.call_count == 1 assert ( - "paused, no entries will occur. Run /start to enable entries." + "paused, no more entries will occur from now. Run /start to enable entries." in msg_mock.call_args_list[0][0][0] ) From ca573a828fc33134bcb2227f66ac9ea7de608aac Mon Sep 17 00:00:00 2001 From: Axel-CH Date: Tue, 1 Apr 2025 00:34:38 -0400 Subject: [PATCH 45/45] chore: update _rpc_pause return wording --- freqtrade/rpc/rpc.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 185bda223..8903c7f2c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -859,8 +859,10 @@ class RPC: if self._freqtrade.state == State.STOPPED: self._freqtrade.state = State.PAUSED return { - "status": "starting bot with trader in paused state, no entries will occur. \ - Run /start to enable entries." + "status": ( + "starting bot with trader in paused state, no entries will occur. " + "Run /start to enable entries." + ) } return {