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"
]
},
diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md
index b7ab86eac..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 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 f76b9360d..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 `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`, `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 ...`
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.",
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()}"
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 4156b50e1..1baf253c0 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()
@@ -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
diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py
index afab46cc8..0081871f0 100644
--- a/freqtrade/rpc/api_server/api_v1.py
+++ b/freqtrade/rpc/api_server/api_v1.py
@@ -369,10 +369,11 @@ 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)):
- 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 075ddd374..8903c7f2c 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.STOPPED:
self._freqtrade.state = State.STOPPED
return {"status": "stopping trader ..."}
@@ -849,16 +849,25 @@ 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:
- # 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": "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, no entries will occur. "
+ "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]:
"""
@@ -930,7 +939,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:
@@ -1046,7 +1055,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
@@ -1214,7 +1223,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()
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 739a2af86..f3f4a4196 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -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$",
@@ -293,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._pause),
CommandHandler("whitelist", self._whitelist),
CommandHandler("blacklist", self._blacklist),
CommandHandler(["blacklist_delete", "bl_delete"], self._blacklist_delete),
@@ -1269,15 +1270,15 @@ 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.
- Sets max_open_trades to 0 and gracefully sells all open trades
+ Handler for /stop_buy /stop_entry and /pause.
+ Sets bot state to paused
:param bot: telegram bot
: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
@@ -1829,6 +1830,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, "
diff --git a/freqtrade/worker.py b/freqtrade/worker.py
index 61d5d9a64..8b754ddc3 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 state == State.RUNNING:
+ if state in (State.RUNNING, State.PAUSED) and old_state not in (
+ State.RUNNING,
+ State.PAUSED,
+ ):
self.freqtrade.startup()
if state == State.STOPPED:
@@ -112,9 +115,10 @@ class Worker:
self._throttle(func=self._process_stopped, throttle_secs=self._throttle_secs)
- elif state == State.RUNNING:
+ elif state in (State.RUNNING, 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(
@@ -221,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")
diff --git a/tests/freqtradebot/test_worker.py b/tests/freqtradebot/test_worker.py
index 1dfdca5b2..6423e63ee 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
@@ -38,6 +39,25 @@ 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)
+
+ worker.freqtrade.state = State.PAUSED
+ state = worker._worker(old_state=State.RUNNING)
+
+ assert state is State.PAUSED
+ 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
+ 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)
@@ -50,6 +70,54 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None:
assert mock_throttle.call_count == 1
+@pytest.mark.parametrize(
+ "old_state,target_state,startup_call,log_fragment",
+ [
+ (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.STOPPED,
+ False,
+ "Changing state from RELOAD_CONFIG to: STOPPED",
+ ),
+ ],
+)
+def test_worker_lifecycle(
+ mocker,
+ default_conf,
+ caplog,
+ old_state,
+ target_state,
+ startup_call,
+ log_fragment,
+):
+ mock_throttle = mocker.MagicMock()
+ mocker.patch("freqtrade.worker.Worker._throttle", mock_throttle)
+ 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
+
+ new_state = worker._worker(old_state=old_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 target_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
diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py
index cb67d089e..8b9a0835a 100644
--- a/tests/rpc/test_rpc.py
+++ b/tests/rpc/test_rpc.py
@@ -806,19 +806,19 @@ 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())
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
+ result = rpc._rpc_pause()
+ 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 99d8350e9..62a054313 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -533,23 +533,26 @@ def test_api_reloadconf(botclient):
assert ftbot.state == State.RELOAD_CONFIG
-def test_api_stopentry(botclient):
+def test_api_pause(botclient):
ftbot, client = botclient
- assert ftbot.config["max_open_trades"] != 0
- 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 /reload_config to reset."
+ "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 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": "paused, 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 71ce557e2..3409cc0e5 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']]"
@@ -1222,15 +1222,15 @@ 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.config["max_open_trades"] != 0
- await telegram._stopentry(update=update, context=MagicMock())
- assert freqtradebot.config["max_open_trades"] == 0
+ assert freqtradebot.state == State.RUNNING
+ await telegram._pause(update=update, context=MagicMock())
+ assert freqtradebot.state == State.PAUSED
assert msg_mock.call_count == 1
assert (
- "No more entries will occur from now. Run /reload_config to reset."
+ "paused, no more entries will occur from now. Run /start to enable entries."
in msg_mock.call_args_list[0][0][0]
)