Merge pull request #11539 from Axel-CH/feat/add-paused-state

Feature: add paused state
This commit is contained in:
Matthias
2025-04-03 06:48:48 +02:00
committed by GitHub
16 changed files with 149 additions and 47 deletions

View File

@@ -1032,6 +1032,7 @@
"type": "string", "type": "string",
"enum": [ "enum": [
"running", "running",
"paused",
"stopped" "stopped"
] ]
}, },

View File

@@ -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 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 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 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. when it changes.
The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd

View File

@@ -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.<br> *Defaults to `freqtrade`*<br> **Datatype:** String | `bot_name` | Name of the bot. Passed via API to a client - can be shown to distinguish / name bots.<br> *Defaults to `freqtrade`*<br> **Datatype:** String
| `external_message_consumer` | Enable [Producer/Consumer mode](producer-consumer.md) for more details. <br> **Datatype:** Dict | `external_message_consumer` | Enable [Producer/Consumer mode](producer-consumer.md) for more details. <br> **Datatype:** Dict
| | **Other** | | **Other**
| `initial_state` | Defines the initial application state. If set to stopped, then the bot has to be explicitly started via `/start` RPC command. <br>*Defaults to `stopped`.* <br> **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. <br>*Defaults to `stopped`.* <br> **Datatype:** Enum, either `running`, `paused` or `stopped`
| `force_entry_enable` | Enables the RPC Commands to force a Trade entry. More information below. <br> **Datatype:** Boolean | `force_entry_enable` | Enables the RPC Commands to force a Trade entry. More information below. <br> **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).<br> *Defaults to `False`*. <br> **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).<br> *Defaults to `False`*. <br> **Datatype:** Boolean
| `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer | `internals.process_throttle_secs` | Set the process throttle, or minimum loop duration for one bot iteration loop. Value in second. <br>*Defaults to `5` seconds.* <br> **Datatype:** Positive Integer

View File

@@ -268,6 +268,9 @@ show_config
start start
Start the bot if it's in the stopped state. 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 stats
Return the stats report (durations, sell-reasons). 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. | `/ping` | GET | Simple command testing the API Readiness - requires no authentication.
| `/start` | POST | Starts the trader. | `/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. | `/stop` | POST | Stops the trader.
| `/stopbuy` | POST | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/stopbuy` | POST | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `/reload_config` | POST | Reloads the configuration file. | `/reload_config` | POST | Reloads the configuration file.

View File

@@ -188,7 +188,7 @@ You can create your own keyboard in `config.json`:
!!! Note "Supported Commands" !!! Note "Supported Commands"
Only the following commands are allowed. Command arguments are not supported! 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 ## Telegram commands
@@ -200,6 +200,7 @@ official commands. You can ask at any moment for help with `/help`.
|----------|-------------| |----------|-------------|
| **System commands** | **System commands**
| `/start` | Starts the trader | `/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 | `/stop` | Stops the trader
| `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/stopbuy | /stopentry` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `/reload_config` | Reloads the configuration file | `/reload_config` | Reloads the configuration file
@@ -250,6 +251,10 @@ Below, example of Telegram message you will receive for each command.
> **Status:** `running` > **Status:** `running`
### /pause
> **Status:** `paused`
### /stop ### /stop
> `Stopping trader ...` > `Stopping trader ...`

View File

@@ -689,7 +689,7 @@ CONF_SCHEMA = {
"initial_state": { "initial_state": {
"description": "Initial state of the system.", "description": "Initial state of the system.",
"type": "string", "type": "string",
"enum": ["running", "stopped"], "enum": ["running", "paused", "stopped"],
}, },
"force_entry_enable": { "force_entry_enable": {
"description": "Force enable entry.", "description": "Force enable entry.",

View File

@@ -7,8 +7,9 @@ class State(Enum):
""" """
RUNNING = 1 RUNNING = 1
STOPPED = 2 PAUSED = 2
RELOAD_CONFIG = 3 STOPPED = 3
RELOAD_CONFIG = 4
def __str__(self): def __str__(self):
return f"{self.name.lower()}" return f"{self.name.lower()}"

View File

@@ -300,7 +300,7 @@ class FreqtradeBot(LoggingMixin):
self.process_open_trade_positions() self.process_open_trade_positions()
# Then looking for entry opportunities # 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.enter_positions()
self._schedule.run_pending() self._schedule.run_pending()
Trade.commit() Trade.commit()
@@ -781,6 +781,10 @@ class FreqtradeBot(LoggingMixin):
) )
if stake_amount is not None and stake_amount > 0.0: 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 # We should increase our position
if self.strategy.max_entry_position_adjustment > -1: if self.strategy.max_entry_position_adjustment > -1:
count_of_entries = trade.nr_of_successful_entries count_of_entries = trade.nr_of_successful_entries

View File

@@ -369,10 +369,11 @@ def stop(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_stop() return rpc._rpc_stop()
@router.post("/pause", response_model=StatusMsg, tags=["botcontrol"])
@router.post("/stopentry", response_model=StatusMsg, tags=["botcontrol"]) @router.post("/stopentry", response_model=StatusMsg, tags=["botcontrol"])
@router.post("/stopbuy", response_model=StatusMsg, tags=["botcontrol"]) @router.post("/stopbuy", response_model=StatusMsg, tags=["botcontrol"])
def stop_buy(rpc: RPC = Depends(get_rpc)): def pause(rpc: RPC = Depends(get_rpc)):
return rpc._rpc_stopentry() return rpc._rpc_pause()
@router.post("/reload_config", response_model=StatusMsg, tags=["botcontrol"]) @router.post("/reload_config", response_model=StatusMsg, tags=["botcontrol"])

View File

@@ -838,7 +838,7 @@ class RPC:
def _rpc_stop(self) -> dict[str, str]: def _rpc_stop(self) -> dict[str, str]:
"""Handler for stop""" """Handler for stop"""
if self._freqtrade.state == State.RUNNING: if self._freqtrade.state != State.STOPPED:
self._freqtrade.state = State.STOPPED self._freqtrade.state = State.STOPPED
return {"status": "stopping trader ..."} return {"status": "stopping trader ..."}
@@ -849,16 +849,25 @@ class RPC:
self._freqtrade.state = State.RELOAD_CONFIG self._freqtrade.state = State.RELOAD_CONFIG
return {"status": "Reloading 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: if self._freqtrade.state == State.RUNNING:
# Set 'max_open_trades' to 0 self._freqtrade.state = State.PAUSED
self._freqtrade.config["max_open_trades"] = 0
self._freqtrade.strategy.max_open_trades = 0
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]: 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 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") raise RPCException("trader is not running")
with self._freqtrade._exit_lock: with self._freqtrade._exit_lock:
@@ -1046,7 +1055,7 @@ class RPC:
raise RPCException(f"Failed to enter position for {pair}.") raise RPCException(f"Failed to enter position for {pair}.")
def _rpc_cancel_open_order(self, trade_id: int): 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") raise RPCException("trader is not running")
with self._freqtrade._exit_lock: with self._freqtrade._exit_lock:
# Query for trade # Query for trade
@@ -1214,7 +1223,7 @@ class RPC:
def _rpc_count(self) -> dict[str, float]: def _rpc_count(self) -> dict[str, float]:
"""Returns the number of trades running""" """Returns the number of trades running"""
if self._freqtrade.state != State.RUNNING: if self._freqtrade.state == State.STOPPED:
raise RPCException("trader is not running") raise RPCException("trader is not running")
trades = Trade.get_open_trades() trades = Trade.get_open_trades()

View File

@@ -178,6 +178,7 @@ class Telegram(RPCHandler):
# problem in _help()). # problem in _help()).
valid_keys: list[str] = [ valid_keys: list[str] = [
r"/start$", r"/start$",
r"/pause$",
r"/stop$", r"/stop$",
r"/status$", r"/status$",
r"/status table$", r"/status table$",
@@ -293,7 +294,7 @@ class Telegram(RPCHandler):
CommandHandler(["unlock", "delete_locks"], self._delete_locks), CommandHandler(["unlock", "delete_locks"], self._delete_locks),
CommandHandler(["reload_config", "reload_conf"], self._reload_config), CommandHandler(["reload_config", "reload_conf"], self._reload_config),
CommandHandler(["show_config", "show_conf"], self._show_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("whitelist", self._whitelist),
CommandHandler("blacklist", self._blacklist), CommandHandler("blacklist", self._blacklist),
CommandHandler(["blacklist_delete", "bl_delete"], self._blacklist_delete), CommandHandler(["blacklist_delete", "bl_delete"], self._blacklist_delete),
@@ -1269,15 +1270,15 @@ class Telegram(RPCHandler):
await self._send_msg(f"Status: `{msg['status']}`") await self._send_msg(f"Status: `{msg['status']}`")
@authorized_only @authorized_only
async def _stopentry(self, update: Update, context: CallbackContext) -> None: async def _pause(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 Sets bot state to paused
:param bot: telegram bot :param bot: telegram bot
:param update: message update :param update: message update
:return: None :return: None
""" """
msg = self._rpc._rpc_stopentry() msg = self._rpc._rpc_pause()
await self._send_msg(f"Status: `{msg['status']}`") await self._send_msg(f"Status: `{msg['status']}`")
@authorized_only @authorized_only
@@ -1829,6 +1830,7 @@ class Telegram(RPCHandler):
"_Bot Control_\n" "_Bot Control_\n"
"------------\n" "------------\n"
"*/start:* `Starts the trader`\n" "*/start:* `Starts the trader`\n"
"*/pause:* `Pause the new entries for trader, but handles open trades gracefully`\n"
"*/stop:* `Stops the trader`\n" "*/stop:* `Stops the trader`\n"
"*/stopentry:* `Stops entering, but handles open trades gracefully` \n" "*/stopentry:* `Stops entering, but handles open trades gracefully` \n"
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, " "*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "

View File

@@ -96,7 +96,10 @@ class Worker:
logger.info( logger.info(
f"Changing state{f' from {old_state.name}' if old_state else ''} to: {state.name}" 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() self.freqtrade.startup()
if state == State.STOPPED: if state == State.STOPPED:
@@ -112,9 +115,10 @@ class Worker:
self._throttle(func=self._process_stopped, throttle_secs=self._throttle_secs) 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 # 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 # Use an offset of 1s to ensure a new candle has been issued
self._throttle( self._throttle(
@@ -221,7 +225,7 @@ class Worker:
# Load and validate config and create new instance of the bot # Load and validate config and create new instance of the bot
self._init(True) 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 # Tell systemd that we completed reconfiguration
self._notify("READY=1") self._notify("READY=1")

View File

@@ -3,6 +3,7 @@ import time
from datetime import timedelta from datetime import timedelta
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import pytest
import time_machine import time_machine
from freqtrade.data.dataprovider import DataProvider 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) 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: def test_worker_stopped(mocker, default_conf, caplog) -> None:
mock_throttle = MagicMock() mock_throttle = MagicMock()
mocker.patch("freqtrade.worker.Worker._throttle", mock_throttle) 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 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 test_throttle(mocker, default_conf, caplog) -> None:
def throttled_func(): def throttled_func():
return 42 return 42

View File

@@ -806,19 +806,19 @@ def test_rpc_stop(mocker, default_conf) -> None:
assert freqtradebot.state == State.STOPPED 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("freqtrade.rpc.telegram.Telegram", MagicMock())
mocker.patch.multiple(EXMS, fetch_ticker=MagicMock()) mocker.patch.multiple(EXMS, fetch_ticker=MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot = get_patched_freqtradebot(mocker, default_conf)
patch_get_signal(freqtradebot) patch_get_signal(freqtradebot)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING freqtradebot.state = State.PAUSED
assert freqtradebot.config["max_open_trades"] != 0 result = rpc._rpc_pause()
result = rpc._rpc_stopentry() assert {
assert {"status": "No more entries will occur from now. Run /reload_config to reset."} == result "status": "paused, no more entries will occur from now. Run /start to enable entries."
assert freqtradebot.config["max_open_trades"] == 0 } == result
def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None: def test_rpc_force_exit(default_conf, ticker, fee, mocker) -> None:

View File

@@ -533,23 +533,26 @@ def test_api_reloadconf(botclient):
assert ftbot.state == State.RELOAD_CONFIG assert ftbot.state == State.RELOAD_CONFIG
def test_api_stopentry(botclient): def test_api_pause(botclient):
ftbot, client = 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_response(rc)
assert rc.json() == { 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") rc = client_post(client, f"{BASE_URI}/stopentry")
assert_response(rc) assert_response(rc)
assert rc.json() == { 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): def test_api_balance(botclient, mocker, rpc_balance, tickers):

View File

@@ -169,7 +169,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
"['stats'], ['daily'], ['weekly'], ['monthly'], " "['stats'], ['daily'], ['weekly'], ['monthly'], "
"['count'], ['locks'], ['delete_locks', 'unlock'], " "['count'], ['locks'], ['delete_locks', 'unlock'], "
"['reload_conf', 'reload_config'], ['show_conf', 'show_config'], " "['reload_conf', 'reload_config'], ['show_conf', 'show_config'], "
"['stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], " "['pause', 'stopbuy', 'stopentry'], ['whitelist'], ['blacklist'], "
"['bl_delete', 'blacklist_delete'], " "['bl_delete', 'blacklist_delete'], "
"['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir'], " "['logs'], ['edge'], ['health'], ['help'], ['version'], ['marketdir'], "
"['order'], ['list_custom_data'], ['tg_info']]" "['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] 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) 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()) await telegram._pause(update=update, context=MagicMock())
assert freqtradebot.config["max_open_trades"] == 0 assert freqtradebot.state == State.PAUSED
assert msg_mock.call_count == 1 assert msg_mock.call_count == 1
assert ( 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] in msg_mock.call_args_list[0][0][0]
) )