From 67651e013ed61a0fda37e959682cd963aa0299b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Jan 2022 07:10:38 +0100 Subject: [PATCH 01/10] Add /forceenter endpoint --- freqtrade/rpc/api_server/api_schemas.py | 7 ++++--- freqtrade/rpc/api_server/api_v1.py | 17 ++++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 83cd8ad8e..efe107346 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional, Union from pydantic import BaseModel from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.enums import OrderTypeValues +from freqtrade.enums import OrderTypeValues, SignalDirection class Ping(BaseModel): @@ -247,7 +247,7 @@ class TradeResponse(BaseModel): total_trades: int -class ForceBuyResponse(BaseModel): +class ForceEnterResponse(BaseModel): __root__: Union[TradeSchema, StatusMsg] @@ -277,8 +277,9 @@ class Logs(BaseModel): logs: List[List] -class ForceBuyPayload(BaseModel): +class ForceEnterPayload(BaseModel): pair: str + side: SignalDirection = SignalDirection.LONG price: Optional[float] ordertype: Optional[OrderTypeValues] stakeamount: Optional[float] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 30f77edfe..d0b39aec3 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -14,8 +14,8 @@ from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count, Daily, - DeleteLockRequest, DeleteTrade, ForceBuyPayload, - ForceBuyResponse, ForceSellPayload, Locks, Logs, + DeleteLockRequest, DeleteTrade, ForceEnterPayload, + ForceEnterResponse, ForceSellPayload, Locks, Logs, OpenTradeSchema, PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, ShowConfig, Stats, StatusMsg, StrategyListResponse, @@ -33,7 +33,9 @@ logger = logging.getLogger(__name__) # 1.11: forcebuy and forcesell accept ordertype # 1.12: add blacklist delete endpoint # 1.13: forcebuy supports stake_amount -API_VERSION = 1.13 +# versions 2.xx -> futures/short branch +# 2.13: addition of Forceenter +API_VERSION = 2.13 # Public API, requires no auth. router_public = APIRouter() @@ -133,17 +135,18 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g return resp -@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading']) -def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)): +# /forcebuy is deprecated with short addition. use ForceEntry instead +@router.post(['/forceenter', '/forcebuy'], response_model=ForceEnterResponse, tags=['trading']) +def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None stake_amount = payload.stakeamount if payload.stakeamount else None trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount) if trade: - return ForceBuyResponse.parse_obj(trade.to_json()) + return ForceEnterResponse.parse_obj(trade.to_json()) else: - return ForceBuyResponse.parse_obj({"status": f"Error buying pair {payload.pair}."}) + return ForceEnterResponse.parse_obj({"status": f"Error entering {payload.side} trade for pair {payload.pair}."}) @router.post('/forcesell', response_model=ResultMsg, tags=['trading']) From 4998f3bdd7b5b58773190ebe471eefc94bba9a26 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Jan 2022 19:07:44 +0100 Subject: [PATCH 02/10] Add order_side to forcebuy endpoint --- freqtrade/rpc/api_server/api_v1.py | 3 ++- freqtrade/rpc/rpc.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index d0b39aec3..78ef65ef5 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -141,7 +141,8 @@ def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None stake_amount = payload.stakeamount if payload.stakeamount else None - trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount) + trade = rpc._rpc_forcebuy(payload.pair, payload.price, order_side=payload.side, + order_type=ordertype, stake_amount=stake_amount) if trade: return ForceEnterResponse.parse_obj(trade.to_json()) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 20f7a6b38..90759857e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -18,6 +18,7 @@ from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT from freqtrade.data.history import load_data from freqtrade.enums import SellType, State +from freqtrade.enums.signaltype import SignalDirection from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler @@ -713,7 +714,8 @@ class RPC: self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} - def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None, + def _rpc_forcebuy(self, pair: str, price: Optional[float], *, order_type: Optional[str] = None, + order_side: SignalDirection = SignalDirection.LONG, stake_amount: Optional[float] = None) -> Optional[Trade]: """ Handler for forcebuy @@ -721,7 +723,7 @@ class RPC: """ if not self._freqtrade.config.get('forcebuy_enable', False): - raise RPCException('Forcebuy not enabled.') + raise RPCException('Forceentry not enabled.') if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') @@ -748,7 +750,9 @@ class RPC: order_type = self._freqtrade.strategy.order_types.get( 'forcebuy', self._freqtrade.strategy.order_types['buy']) if self._freqtrade.execute_entry(pair, stake_amount, price, - ordertype=order_type, trade=trade): + ordertype=order_type, trade=trade, + is_short=(order_side == SignalDirection.SHORT) + ): Trade.commit() trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade From 48d8cd82af57bb785c65db848f5fe8498b996cb7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Jan 2022 19:08:37 +0100 Subject: [PATCH 03/10] _rpc_forcebuy --- freqtrade/rpc/api_server/api_v1.py | 7 ++++--- freqtrade/rpc/rpc.py | 7 ++++--- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc.py | 16 ++++++++-------- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 6 +++--- tests/test_integration.py | 2 +- 7 files changed, 23 insertions(+), 21 deletions(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 78ef65ef5..71f251021 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -141,13 +141,14 @@ def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None stake_amount = payload.stakeamount if payload.stakeamount else None - trade = rpc._rpc_forcebuy(payload.pair, payload.price, order_side=payload.side, - order_type=ordertype, stake_amount=stake_amount) + trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side, + order_type=ordertype, stake_amount=stake_amount) if trade: return ForceEnterResponse.parse_obj(trade.to_json()) else: - return ForceEnterResponse.parse_obj({"status": f"Error entering {payload.side} trade for pair {payload.pair}."}) + return ForceEnterResponse.parse_obj( + {"status": f"Error entering {payload.side} trade for pair {payload.pair}."}) @router.post('/forcesell', response_model=ResultMsg, tags=['trading']) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 90759857e..13be803c8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -714,9 +714,10 @@ class RPC: self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} - def _rpc_forcebuy(self, pair: str, price: Optional[float], *, order_type: Optional[str] = None, - order_side: SignalDirection = SignalDirection.LONG, - stake_amount: Optional[float] = None) -> Optional[Trade]: + def _rpc_force_entry(self, pair: str, price: Optional[float], *, + order_type: Optional[str] = None, + order_side: SignalDirection = SignalDirection.LONG, + stake_amount: Optional[float] = None) -> Optional[Trade]: """ Handler for forcebuy Buys a pair trade at the given or current price diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 0769e0277..4ca728ef6 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -868,7 +868,7 @@ class Telegram(RPCHandler): def _forcebuy_action(self, pair, price=None): try: - self._rpc._rpc_forcebuy(pair, price) + self._rpc._rpc_force_entry(pair, price) except RPCException as e: self._send_msg(str(e)) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 1c924caa9..47c7d4db7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1106,16 +1106,16 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) pair = 'ETH/BTC' - trade = rpc._rpc_forcebuy(pair, None) + trade = rpc._rpc_force_entry(pair, None) assert isinstance(trade, Trade) assert trade.pair == pair assert trade.open_rate == ticker()['bid'] # Test buy duplicate with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'): - rpc._rpc_forcebuy(pair, 0.0001) + rpc._rpc_force_entry(pair, 0.0001) pair = 'XRP/BTC' - trade = rpc._rpc_forcebuy(pair, 0.0001, order_type='limit') + trade = rpc._rpc_force_entry(pair, 0.0001, order_type='limit') assert isinstance(trade, Trade) assert trade.pair == pair assert trade.open_rate == 0.0001 @@ -1123,11 +1123,11 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> # Test buy pair not with stakes with pytest.raises(RPCException, match=r'Wrong pair selected. Only pairs with stake-currency.*'): - rpc._rpc_forcebuy('LTC/ETH', 0.0001) + rpc._rpc_force_entry('LTC/ETH', 0.0001) # Test with defined stake_amount pair = 'LTC/BTC' - trade = rpc._rpc_forcebuy(pair, 0.0001, order_type='limit', stake_amount=0.05) + trade = rpc._rpc_force_entry(pair, 0.0001, order_type='limit', stake_amount=0.05) assert trade.stake_amount == 0.05 # Test not buying @@ -1137,7 +1137,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) pair = 'TKN/BTC' - trade = rpc._rpc_forcebuy(pair, None) + trade = rpc._rpc_force_entry(pair, None) assert trade is None @@ -1151,7 +1151,7 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None: rpc = RPC(freqtradebot) pair = 'ETH/BTC' with pytest.raises(RPCException, match=r'trader is not running'): - rpc._rpc_forcebuy(pair, None) + rpc._rpc_force_entry(pair, None) def test_rpcforcebuy_disabled(mocker, default_conf) -> None: @@ -1162,7 +1162,7 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None: rpc = RPC(freqtradebot) pair = 'ETH/BTC' with pytest.raises(RPCException, match=r'Forcebuy not enabled.'): - rpc._rpc_forcebuy(pair, None) + rpc._rpc_force_entry(pair, None) @pytest.mark.usefixtures("init_persistence") diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 27fa5db3a..d3b992d63 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1074,7 +1074,7 @@ def test_api_forcebuy(botclient, mocker, fee): ftbot.config['forcebuy_enable'] = True fbuy_mock = MagicMock(return_value=None) - mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) + mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock) rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') assert_response(rc) @@ -1099,7 +1099,7 @@ def test_api_forcebuy(botclient, mocker, fee): timeframe=5, strategy=CURRENT_TEST_STRATEGY )) - mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) + mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock) rc = client_post(client, f"{BASE_URI}/forcebuy", data='{"pair": "ETH/BTC"}') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f6ff396d4..6b227ccaf 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1137,7 +1137,7 @@ def test_forcebuy_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) - mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) + mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock) telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) @@ -1153,7 +1153,7 @@ def test_forcebuy_handle(default_conf, update, mocker) -> None: # Reset and retry with specified price fbuy_mock = MagicMock(return_value=None) - mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) + mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock) # /forcebuy ETH/BTC 0.055 context = MagicMock() context.args = ["ETH/BTC", "0.055"] @@ -1182,7 +1182,7 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) - mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) + mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) diff --git a/tests/test_integration.py b/tests/test_integration.py index 13bcac351..74518c691 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -179,7 +179,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati assert len(trades) == 4 assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') == result1 - rpc._rpc_forcebuy('TKN/BTC', None) + rpc._rpc_force_entry('TKN/BTC', None) trades = Trade.query.all() assert len(trades) == 5 From be7ce208dc6b40726c1c436770f06d728e5b5c71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Jan 2022 19:24:01 +0100 Subject: [PATCH 04/10] Update tests to test forceenter endpoint --- freqtrade/enums/signaltype.py | 2 +- freqtrade/rpc/api_server/api_v1.py | 3 ++- tests/rpc/test_rpc.py | 8 ++++---- tests/rpc/test_rpc_apiserver.py | 18 +++++++++++------- tests/rpc/test_rpc_telegram.py | 2 +- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index a8e9ef55e..f706fd4dc 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -19,6 +19,6 @@ class SignalTagType(Enum): EXIT_TAG = "exit_tag" -class SignalDirection(Enum): +class SignalDirection(str, Enum): LONG = 'long' SHORT = 'short' diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 71f251021..4797b38e0 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -136,7 +136,8 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g # /forcebuy is deprecated with short addition. use ForceEntry instead -@router.post(['/forceenter', '/forcebuy'], response_model=ForceEnterResponse, tags=['trading']) +@router.post('/forceenter', response_model=ForceEnterResponse, tags=['trading']) +@router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading']) def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None stake_amount = payload.stakeamount if payload.stakeamount else None diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 47c7d4db7..26c98d6a1 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -1090,7 +1090,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None: assert counts["current"] == 1 -def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> None: +def test_rpc_forceentry(mocker, default_conf, ticker, fee, limit_buy_order_open) -> None: default_conf['forcebuy_enable'] = True mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) buy_mm = MagicMock(return_value=limit_buy_order_open) @@ -1141,7 +1141,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> assert trade is None -def test_rpcforcebuy_stopped(mocker, default_conf) -> None: +def test_rpc_forceentry_stopped(mocker, default_conf) -> None: default_conf['forcebuy_enable'] = True default_conf['initial_state'] = 'stopped' mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -1154,14 +1154,14 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None: rpc._rpc_force_entry(pair, None) -def test_rpcforcebuy_disabled(mocker, default_conf) -> None: +def test_rpc_forceentry_disabled(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot) rpc = RPC(freqtradebot) pair = 'ETH/BTC' - with pytest.raises(RPCException, match=r'Forcebuy not enabled.'): + with pytest.raises(RPCException, match=r'Forceentry not enabled.'): rpc._rpc_force_entry(pair, None) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d3b992d63..88585f15c 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -543,7 +543,7 @@ def test_api_show_config(botclient): assert 'unfilledtimeout' in response assert 'version' in response assert 'api_version' in response - assert 1.1 <= response['api_version'] <= 1.2 + assert 2.1 <= response['api_version'] <= 2.2 def test_api_daily(botclient, mocker, ticker, fee, markets): @@ -1062,23 +1062,27 @@ def test_api_whitelist(botclient): # TODO -lev: add test for forcebuy (short) when feature is supported -def test_api_forcebuy(botclient, mocker, fee): +@pytest.mark.parametrize('endpoint', [ + 'forcebuy', + 'forceenter', +]) +def test_api_forceentry(botclient, mocker, fee, endpoint): ftbot, client = botclient - rc = client_post(client, f"{BASE_URI}/forcebuy", + rc = client_post(client, f"{BASE_URI}/{endpoint}", data='{"pair": "ETH/BTC"}') assert_response(rc, 502) - assert rc.json() == {"error": "Error querying /api/v1/forcebuy: Forcebuy not enabled."} + assert rc.json() == {"error": f"Error querying /api/v1/{endpoint}: Forceentry not enabled."} # enable forcebuy ftbot.config['forcebuy_enable'] = True fbuy_mock = MagicMock(return_value=None) mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock) - rc = client_post(client, f"{BASE_URI}/forcebuy", + rc = client_post(client, f"{BASE_URI}/{endpoint}", data='{"pair": "ETH/BTC"}') assert_response(rc) - assert rc.json() == {"status": "Error buying pair ETH/BTC."} + assert rc.json() == {"status": "Error entering long trade for pair ETH/BTC."} # Test creating trade fbuy_mock = MagicMock(return_value=Trade( @@ -1101,7 +1105,7 @@ def test_api_forcebuy(botclient, mocker, fee): )) mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock) - rc = client_post(client, f"{BASE_URI}/forcebuy", + rc = client_post(client, f"{BASE_URI}/{endpoint}", data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json() == { diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 6b227ccaf..c44756ae7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1175,7 +1175,7 @@ def test_forcebuy_handle_exception(default_conf, update, mocker) -> None: telegram._forcebuy(update=update, context=MagicMock()) assert msg_mock.call_count == 1 - assert msg_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.' + assert msg_mock.call_args_list[0][0][0] == 'Forceentry not enabled.' def test_forcebuy_no_pair(default_conf, update, mocker) -> None: From e2ddea79ee7f9524c2d6ddb2b3095d364c872249 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Jan 2022 19:49:15 +0100 Subject: [PATCH 05/10] Add "market" to /show_config --- freqtrade/rpc/telegram.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4ca728ef6..cd80e2b5c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -1366,6 +1366,7 @@ class Telegram(RPCHandler): self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" + f"*Market: * `{val['trading_mode']}`\n" f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n" f"*Max open Trades:* `{val['max_open_trades']}`\n" f"*Minimum ROI:* `{val['minimal_roi']}`\n" From 7afaf4b5d45166896976970290ca58c9cb844302 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Jan 2022 19:53:46 +0100 Subject: [PATCH 06/10] Add `/forceshort` command --- freqtrade/rpc/rpc.py | 4 ++++ freqtrade/rpc/telegram.py | 30 ++++++++++++++++++++---------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 13be803c8..7eedd27db 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -19,6 +19,7 @@ from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT from freqtrade.data.history import load_data from freqtrade.enums import SellType, State from freqtrade.enums.signaltype import SignalDirection +from freqtrade.enums.tradingmode import TradingMode from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler @@ -729,6 +730,9 @@ class RPC: if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') + if order_side == SignalDirection.SHORT and self._freqtrade.trading_mode == TradingMode.SPOT: + raise RPCException("Can't go short on Spot markets") + # Check if pair quote currency equals to the stake currency. stake_currency = self._freqtrade.config.get('stake_currency') if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index cd80e2b5c..6eac52bf0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -7,6 +7,7 @@ import json import logging import re from datetime import date, datetime, timedelta +from functools import partial from html import escape from itertools import chain from math import isnan @@ -23,6 +24,7 @@ from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.constants import DUST_PER_COIN from freqtrade.enums import RPCMessageType +from freqtrade.enums.signaltype import SignalDirection from freqtrade.exceptions import OperationalException from freqtrade.misc import chunks, plural, round_coin_value from freqtrade.persistence import Trade @@ -151,7 +153,8 @@ class Telegram(RPCHandler): CommandHandler('start', self._start), CommandHandler('stop', self._stop), CommandHandler('forcesell', self._forcesell), - CommandHandler('forcebuy', self._forcebuy), + CommandHandler(['forcebuy', 'forcelong'], partial(self._forcebuy, order_side=SignalDirection.LONG)), + CommandHandler('forceshort', partial(self._forcebuy, order_side=SignalDirection.SHORT)), CommandHandler('trades', self._trades), CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), @@ -866,19 +869,20 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) - def _forcebuy_action(self, pair, price=None): + def _forceenter_action(self, pair, price: Optional[float], order_side: SignalDirection): try: - self._rpc._rpc_force_entry(pair, price) + self._rpc._rpc_force_entry(pair, price, order_side=order_side) except RPCException as e: self._send_msg(str(e)) def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query - pair = query.data + pair, side = query.data.split('_||_') + order_side = SignalDirection(side) query.answer() - query.edit_message_text(text=f"Force Buying: {pair}") - self._forcebuy_action(pair) + query.edit_message_text(text=f"Manually entering {order_side} for {pair}") + self._forceenter_action(pair, None, order_side) @staticmethod def _layout_inline_keyboard(buttons: List[InlineKeyboardButton], @@ -886,7 +890,8 @@ class Telegram(RPCHandler): return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] @authorized_only - def _forcebuy(self, update: Update, context: CallbackContext) -> None: + def _forcebuy( + self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None: """ Handler for /forcebuy . Buys a pair trade at the given or current price @@ -897,13 +902,18 @@ class Telegram(RPCHandler): if context.args: pair = context.args[0] price = float(context.args[1]) if len(context.args) > 1 else None - self._forcebuy_action(pair, price) + self._forceenter_action(pair, price, order_side) else: whitelist = self._rpc._rpc_whitelist()['whitelist'] - pairs = [InlineKeyboardButton(text=pair, callback_data=pair) for pair in whitelist] + pair_buttons = [ + InlineKeyboardButton(text=pair, callback_data=f"{pair}_||_{order_side}") + for pair in whitelist + ] self._send_msg(msg="Which pair?", - keyboard=self._layout_inline_keyboard(pairs)) + keyboard=self._layout_inline_keyboard(pair_buttons), + callback_path="update_forcelong", + query=update.callback_query) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: From 066fb3ce00bf553299e769d52d78e37a5a8999ed Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Jan 2022 20:07:58 +0100 Subject: [PATCH 07/10] Update rest-client with forceenter --- docs/rest-api.md | 11 +++++++++++ docs/telegram-usage.md | 11 +++++++---- scripts/rest_client.py | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 8c2599cbc..5a6b1b7a0 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -148,6 +148,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `forcebuy [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) +| `forceenter [rate]` | Instantly longs or shorts the given pair. Rate is optional. (`forcebuy_enable` must be set to True) | `performance` | Show performance of each finished trade grouped by pair. | `balance` | Show account balance per currency. | `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7). @@ -215,6 +216,13 @@ forcebuy :param pair: Pair to buy (ETH/BTC) :param price: Optional - price to buy +forceenter + Force entering a trade + + :param pair: Pair to buy (ETH/BTC) + :param side: 'long' or 'short' + :param price: Optional - price to buy + forcesell Force-sell a trade. @@ -285,6 +293,9 @@ strategy :param strategy: Strategy class name +sysinfo + Provides system information (CPU, RAM usage) + trade Return specific trade diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 54e6f50cb..ebdd062ee 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -173,7 +173,8 @@ official commands. You can ask at any moment for help with `/help`. | `/profit []` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default) | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). -| `/forcebuy [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`forcebuy_enable` must be set to True) +| `/forcelong [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`forcebuy_enable` must be set to True) +| `/forceshort [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`forcebuy_enable` must be set to True) | `/performance` | Show performance of each finished trade grouped by pair | `/balance` | Show account balance per currency | `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) @@ -275,11 +276,13 @@ Starting capital is either taken from the `available_capital` setting, or calcul > **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)` -### /forcebuy [rate] +### /forcelong [rate] | /forceshort [rate] -> **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`) +`/forcebuy [rate]` is also supported for longs but should be considered deprecated. -Omitting the pair will open a query asking for the pair to buy (based on the current whitelist). +> **BITTREX:** Long ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`) + +Omitting the pair will open a query asking for the pair to trade (based on the current whitelist). ![Telegram force-buy screenshot](assets/telegram_forcebuy.png) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index b1234d329..e23954dd4 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -261,6 +261,20 @@ class FtRestClient(): } return self._post("forcebuy", data=data) + def forceenter(self, pair, side, price=None): + """Force entering a trade + + :param pair: Pair to buy (ETH/BTC) + :param side: 'long' or 'short' + :param price: Optional - price to buy + :return: json object of the trade + """ + data = {"pair": pair, + "side": side, + "price": price, + } + return self._post("forceenter", data=data) + def forcesell(self, tradeid): """Force-sell a trade. From 0a52d79208fa7514749c78a5cda8289173f7243f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Jan 2022 20:17:00 +0100 Subject: [PATCH 08/10] Update forcesell to work as forceexit --- freqtrade/rpc/api_server/api_schemas.py | 2 +- freqtrade/rpc/api_server/api_v1.py | 7 ++++--- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/telegram.py | 21 ++++++++++++++------- tests/rpc/test_rpc.py | 22 +++++++++++----------- tests/rpc/test_rpc_telegram.py | 12 ++++++------ 6 files changed, 37 insertions(+), 29 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index efe107346..dee566cea 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -285,7 +285,7 @@ class ForceEnterPayload(BaseModel): stakeamount: Optional[float] -class ForceSellPayload(BaseModel): +class ForceExitPayload(BaseModel): tradeid: str ordertype: Optional[OrderTypeValues] diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 4797b38e0..93a160efb 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -15,7 +15,7 @@ from freqtrade.rpc import RPC from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, BlacklistResponse, Count, Daily, DeleteLockRequest, DeleteTrade, ForceEnterPayload, - ForceEnterResponse, ForceSellPayload, Locks, Logs, + ForceEnterResponse, ForceExitPayload, Locks, Logs, OpenTradeSchema, PairHistory, PerformanceEntry, Ping, PlotConfig, Profit, ResultMsg, ShowConfig, Stats, StatusMsg, StrategyListResponse, @@ -152,10 +152,11 @@ def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)): {"status": f"Error entering {payload.side} trade for pair {payload.pair}."}) +@router.post('/forceexit', response_model=ResultMsg, tags=['trading']) @router.post('/forcesell', response_model=ResultMsg, tags=['trading']) -def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)): +def forcesell(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)): ordertype = payload.ordertype.value if payload.ordertype else None - return rpc._rpc_forcesell(payload.tradeid, ordertype) + return rpc._rpc_forceexit(payload.tradeid, ordertype) @router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist']) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 7eedd27db..b9d7cedd4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -658,7 +658,7 @@ class RPC: return {'status': 'No more buy will occur from now. Run /reload_config to reset.'} - def _rpc_forcesell(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]: + def _rpc_forceexit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]: """ Handler for forcesell . Sells the given trade at current price diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6eac52bf0..23872286e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -25,6 +25,7 @@ from freqtrade.__init__ import __version__ from freqtrade.constants import DUST_PER_COIN from freqtrade.enums import RPCMessageType from freqtrade.enums.signaltype import SignalDirection +from freqtrade.enums.tradingmode import TradingMode from freqtrade.exceptions import OperationalException from freqtrade.misc import chunks, plural, round_coin_value from freqtrade.persistence import Trade @@ -115,7 +116,8 @@ class Telegram(RPCHandler): r'/stopbuy$', r'/reload_config$', r'/show_config$', r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$', r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$', - r'/forcebuy$', r'/edge$', r'/help$', r'/version$'] + r'/forcebuy$', r'/forcelong$', r'/forceshort$', + r'/edge$', r'/help$', r'/version$'] # Create keys for generation valid_keys_print = [k.replace('$', '') for k in valid_keys] @@ -152,7 +154,7 @@ class Telegram(RPCHandler): CommandHandler('balance', self._balance), CommandHandler('start', self._start), CommandHandler('stop', self._stop), - CommandHandler('forcesell', self._forcesell), + CommandHandler(['forcesell', 'forceexit'], self._forceexit), CommandHandler(['forcebuy', 'forcelong'], partial(self._forcebuy, order_side=SignalDirection.LONG)), CommandHandler('forceshort', partial(self._forcebuy, order_side=SignalDirection.SHORT)), CommandHandler('trades', self._trades), @@ -849,7 +851,7 @@ class Telegram(RPCHandler): self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only - def _forcesell(self, update: Update, context: CallbackContext) -> None: + def _forceexit(self, update: Update, context: CallbackContext) -> None: """ Handler for /forcesell . Sells the given trade at current price @@ -863,7 +865,7 @@ class Telegram(RPCHandler): self._send_msg("You must specify a trade-id or 'all'.") return try: - msg = self._rpc._rpc_forcesell(trade_id) + msg = self._rpc._rpc_forceexit(trade_id) self._send_msg('Forcesell Result: `{result}`'.format(**msg)) except RPCException as e: @@ -1279,16 +1281,21 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - forcebuy_text = ("*/forcebuy []:* `Instantly buys the given pair. " + forcebuy_text = ("*/forcelong []:* `Instantly buys the given pair. " "Optionally takes a rate at which to buy " - "(only applies to limit orders).` \n") + "(only applies to limit orders).` \n" + ) + if self._rpc_._freqtrade.trading_mode != TradingMode.SPOT: + forcebuy_text += ("*/forceshort []:* `Instantly shorts the given pair. " + "Optionally takes a rate at which to sell " + "(only applies to limit orders).` \n") message = ( "_BotControl_\n" "------------\n" "*/start:* `Starts the trader`\n" "*/stop:* Stops the trader\n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" - "*/forcesell |all:* `Instantly sells the given trade or all trades, " + "*/forceexit |all:* `Instantly exits the given trade or all trades, " "regardless of profit`\n" f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" "*/delete :* `Instantly delete the given trade in the database`\n" diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 26c98d6a1..1afda4b3d 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -687,7 +687,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None: assert freqtradebot.config['max_open_trades'] == 0 -def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: +def test_rpc_forceexit(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) cancel_order_mock = MagicMock() @@ -714,29 +714,29 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: freqtradebot.state = State.STOPPED with pytest.raises(RPCException, match=r'.*trader is not running*'): - rpc._rpc_forcesell(None) + rpc._rpc_forceexit(None) freqtradebot.state = State.RUNNING with pytest.raises(RPCException, match=r'.*invalid argument*'): - rpc._rpc_forcesell(None) + rpc._rpc_forceexit(None) - msg = rpc._rpc_forcesell('all') + msg = rpc._rpc_forceexit('all') assert msg == {'result': 'Created sell orders for all open trades.'} freqtradebot.enter_positions() - msg = rpc._rpc_forcesell('all') + msg = rpc._rpc_forceexit('all') assert msg == {'result': 'Created sell orders for all open trades.'} freqtradebot.enter_positions() - msg = rpc._rpc_forcesell('2') + msg = rpc._rpc_forceexit('2') assert msg == {'result': 'Created sell order for trade 2.'} freqtradebot.state = State.STOPPED with pytest.raises(RPCException, match=r'.*trader is not running*'): - rpc._rpc_forcesell(None) + rpc._rpc_forceexit(None) with pytest.raises(RPCException, match=r'.*trader is not running*'): - rpc._rpc_forcesell('all') + rpc._rpc_forceexit('all') freqtradebot.state = State.RUNNING assert cancel_order_mock.call_count == 0 @@ -765,7 +765,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: ) # check that the trade is called, which is done by ensuring exchange.cancel_order is called # and trade amount is updated - rpc._rpc_forcesell('3') + rpc._rpc_forceexit('3') assert cancel_order_mock.call_count == 1 assert trade.amount == filled_amount @@ -793,7 +793,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: } ) # check that the trade is called, which is done by ensuring exchange.cancel_order is called - msg = rpc._rpc_forcesell('4') + msg = rpc._rpc_forceexit('4') assert msg == {'result': 'Created sell order for trade 4.'} assert cancel_order_mock.call_count == 2 assert trade.amount == amount @@ -810,7 +810,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: 'filled': 0.0 } ) - msg = rpc._rpc_forcesell('3') + msg = rpc._rpc_forceexit('3') assert msg == {'result': 'Created sell order for trade 3.'} # status quo, no exchange calls assert cancel_order_mock.call_count == 3 diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index c44756ae7..7b83fd1a6 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -940,7 +940,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, # /forcesell 1 context = MagicMock() context.args = ["1"] - telegram._forcesell(update=update, context=context) + telegram._forceexit(update=update, context=context) assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-2][0][0] @@ -1007,7 +1007,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, # /forcesell 1 context = MagicMock() context.args = ["1"] - telegram._forcesell(update=update, context=context) + telegram._forceexit(update=update, context=context) assert msg_mock.call_count == 4 @@ -1065,7 +1065,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None # /forcesell all context = MagicMock() context.args = ["all"] - telegram._forcesell(update=update, context=context) + telegram._forceexit(update=update, context=context) # Called for each trade 2 times assert msg_mock.call_count == 8 @@ -1109,7 +1109,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: # /forcesell 1 context = MagicMock() context.args = ["1"] - telegram._forcesell(update=update, context=context) + telegram._forceexit(update=update, context=context) assert msg_mock.call_count == 1 assert 'not running' in msg_mock.call_args_list[0][0][0] @@ -1118,7 +1118,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: freqtradebot.state = State.RUNNING context = MagicMock() context.args = [] - telegram._forcesell(update=update, context=context) + telegram._forceexit(update=update, context=context) assert msg_mock.call_count == 1 assert "You must specify a trade-id or 'all'." in msg_mock.call_args_list[0][0][0] @@ -1128,7 +1128,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: # /forcesell 123456 context = MagicMock() context.args = ["123456"] - telegram._forcesell(update=update, context=context) + telegram._forceexit(update=update, context=context) assert msg_mock.call_count == 1 assert 'invalid argument' in msg_mock.call_args_list[0][0][0] From 6e72effbf07ee556035ac763d462ffa8a26b3f8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jan 2022 06:31:45 +0100 Subject: [PATCH 09/10] Update forcebuy telegram tests --- freqtrade/rpc/telegram.py | 10 +++++----- tests/rpc/test_rpc_telegram.py | 23 +++++++++++++---------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 23872286e..1eee4e98b 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -155,8 +155,8 @@ class Telegram(RPCHandler): CommandHandler('start', self._start), CommandHandler('stop', self._stop), CommandHandler(['forcesell', 'forceexit'], self._forceexit), - CommandHandler(['forcebuy', 'forcelong'], partial(self._forcebuy, order_side=SignalDirection.LONG)), - CommandHandler('forceshort', partial(self._forcebuy, order_side=SignalDirection.SHORT)), + CommandHandler(['forcebuy', 'forcelong'], partial(self._forceenter, order_side=SignalDirection.LONG)), + CommandHandler('forceshort', partial(self._forceenter, order_side=SignalDirection.SHORT)), CommandHandler('trades', self._trades), CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), @@ -892,10 +892,10 @@ class Telegram(RPCHandler): return [buttons[i:i + cols] for i in range(0, len(buttons), cols)] @authorized_only - def _forcebuy( + def _forceenter( self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None: """ - Handler for /forcebuy . + Handler for /forcelong and `/forceshort Buys a pair trade at the given or current price :param bot: telegram bot :param update: message update @@ -1285,7 +1285,7 @@ class Telegram(RPCHandler): "Optionally takes a rate at which to buy " "(only applies to limit orders).` \n" ) - if self._rpc_._freqtrade.trading_mode != TradingMode.SPOT: + if self._rpc._freqtrade.trading_mode != TradingMode.SPOT: forcebuy_text += ("*/forceshort []:* `Instantly shorts the given pair. " "Optionally takes a rate at which to sell " "(only applies to limit orders).` \n") diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7b83fd1a6..4ba81f930 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -19,6 +19,7 @@ from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo from freqtrade.enums import RPCMessageType, RunMode, SellType, State +from freqtrade.enums.signaltype import SignalDirection from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging @@ -93,8 +94,10 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: assert start_polling.start_polling.call_count == 1 message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " - "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " - "['delete'], ['performance'], ['buys', 'entries'], ['sells'], ['mix_tags'], " + "['balance'], ['start'], ['stop'], " + "['forcesell', 'forceexit'], ['forcebuy', 'forcelong'], ['forceshort'], " + "['trades'], ['delete'], ['performance'], " + "['buys', 'entries'], ['sells'], ['mix_tags'], " "['stats'], ['daily'], ['weekly'], ['monthly'], " "['count'], ['locks'], ['unlock', 'delete_locks'], " "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " @@ -1133,7 +1136,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: assert 'invalid argument' in msg_mock.call_args_list[0][0][0] -def test_forcebuy_handle(default_conf, update, mocker) -> None: +def test_forceenter_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) @@ -1145,7 +1148,7 @@ def test_forcebuy_handle(default_conf, update, mocker) -> None: # /forcebuy ETH/BTC context = MagicMock() context.args = ["ETH/BTC"] - telegram._forcebuy(update=update, context=context) + telegram._forceenter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 1 assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' @@ -1157,7 +1160,7 @@ def test_forcebuy_handle(default_conf, update, mocker) -> None: # /forcebuy ETH/BTC 0.055 context = MagicMock() context.args = ["ETH/BTC", "0.055"] - telegram._forcebuy(update=update, context=context) + telegram._forceenter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 1 assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' @@ -1165,20 +1168,20 @@ def test_forcebuy_handle(default_conf, update, mocker) -> None: assert fbuy_mock.call_args_list[0][0][1] == 0.055 -def test_forcebuy_handle_exception(default_conf, update, mocker) -> None: +def test_forceenter_handle_exception(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) update.message.text = '/forcebuy ETH/Nonepair' - telegram._forcebuy(update=update, context=MagicMock()) + telegram._forceenter(update=update, context=MagicMock(), order_side=SignalDirection.LONG) assert msg_mock.call_count == 1 assert msg_mock.call_args_list[0][0][0] == 'Forceentry not enabled.' -def test_forcebuy_no_pair(default_conf, update, mocker) -> None: +def test_forceenter_no_pair(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) fbuy_mock = MagicMock(return_value=None) @@ -1190,7 +1193,7 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: context = MagicMock() context.args = [] - telegram._forcebuy(update=update, context=context) + telegram._forceenter(update=update, context=context, order_side=SignalDirection.LONG) assert fbuy_mock.call_count == 0 assert msg_mock.call_count == 1 @@ -1200,7 +1203,7 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4 update = MagicMock() update.callback_query = MagicMock() - update.callback_query.data = 'XRP/USDT' + update.callback_query.data = 'XRP/USDT_||_long' telegram._forcebuy_inline(update, None) assert fbuy_mock.call_count == 1 From c4f71cc103ccb114fa4758bbd9e61c55ffec7e0c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Jan 2022 06:40:41 +0100 Subject: [PATCH 10/10] More forceenter updates --- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/telegram.py | 37 ++++++++++++++++++---------------- tests/rpc/test_rpc.py | 13 ++++++++++++ tests/rpc/test_rpc_telegram.py | 7 ++++--- 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b9d7cedd4..989cd9aca 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -731,7 +731,7 @@ class RPC: raise RPCException('trader is not running') if order_side == SignalDirection.SHORT and self._freqtrade.trading_mode == TradingMode.SPOT: - raise RPCException("Can't go short on Spot markets") + raise RPCException("Can't go short on Spot markets.") # Check if pair quote currency equals to the stake currency. stake_currency = self._freqtrade.config.get('stake_currency') diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1eee4e98b..ea105e0f8 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -155,8 +155,10 @@ class Telegram(RPCHandler): CommandHandler('start', self._start), CommandHandler('stop', self._stop), CommandHandler(['forcesell', 'forceexit'], self._forceexit), - CommandHandler(['forcebuy', 'forcelong'], partial(self._forceenter, order_side=SignalDirection.LONG)), - CommandHandler('forceshort', partial(self._forceenter, order_side=SignalDirection.SHORT)), + CommandHandler(['forcebuy', 'forcelong'], partial( + self._forceenter, order_side=SignalDirection.LONG)), + CommandHandler('forceshort', partial( + self._forceenter, order_side=SignalDirection.SHORT)), CommandHandler('trades', self._trades), CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), @@ -195,7 +197,7 @@ class Telegram(RPCHandler): pattern='update_sell_reason_performance'), CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'), CallbackQueryHandler(self._count, pattern='update_count'), - CallbackQueryHandler(self._forcebuy_inline), + CallbackQueryHandler(self._forceenter_inline), ] for handle in handles: self._updater.dispatcher.add_handler(handle) @@ -877,14 +879,15 @@ class Telegram(RPCHandler): except RPCException as e: self._send_msg(str(e)) - def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None: + def _forceenter_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query - pair, side = query.data.split('_||_') - order_side = SignalDirection(side) - query.answer() - query.edit_message_text(text=f"Manually entering {order_side} for {pair}") - self._forceenter_action(pair, None, order_side) + if query.data and '_||_' in query.data: + pair, side = query.data.split('_||_') + order_side = SignalDirection(side) + query.answer() + query.edit_message_text(text=f"Manually entering {order_side} for {pair}") + self._forceenter_action(pair, None, order_side) @staticmethod def _layout_inline_keyboard(buttons: List[InlineKeyboardButton], @@ -1281,14 +1284,14 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - forcebuy_text = ("*/forcelong []:* `Instantly buys the given pair. " - "Optionally takes a rate at which to buy " - "(only applies to limit orders).` \n" - ) + forceenter_text = ("*/forcelong []:* `Instantly buys the given pair. " + "Optionally takes a rate at which to buy " + "(only applies to limit orders).` \n" + ) if self._rpc._freqtrade.trading_mode != TradingMode.SPOT: - forcebuy_text += ("*/forceshort []:* `Instantly shorts the given pair. " - "Optionally takes a rate at which to sell " - "(only applies to limit orders).` \n") + forceenter_text += ("*/forceshort []:* `Instantly shorts the given pair. " + "Optionally takes a rate at which to sell " + "(only applies to limit orders).` \n") message = ( "_BotControl_\n" "------------\n" @@ -1297,7 +1300,7 @@ class Telegram(RPCHandler): "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/forceexit |all:* `Instantly exits the given trade or all trades, " "regardless of profit`\n" - f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" + f"{forceenter_text if self._config.get('forcebuy_enable', False) else ''}" "*/delete :* `Instantly delete the given trade in the database`\n" "*/whitelist:* `Show current whitelist` \n" "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 1afda4b3d..90587160c 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -9,6 +9,7 @@ from numpy import isnan from freqtrade.edge import PairInfo from freqtrade.enums import State, TradingMode +from freqtrade.enums.signaltype import SignalDirection from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError from freqtrade.persistence import Trade from freqtrade.persistence.pairlock_middleware import PairLocks @@ -1165,6 +1166,18 @@ def test_rpc_forceentry_disabled(mocker, default_conf) -> None: rpc._rpc_force_entry(pair, None) +def test_rpc_forceentry_wrong_mode(mocker, default_conf) -> None: + default_conf['forcebuy_enable'] = True + mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot) + rpc = RPC(freqtradebot) + pair = 'ETH/BTC' + with pytest.raises(RPCException, match="Can't go short on Spot markets."): + rpc._rpc_force_entry(pair, None, order_side=SignalDirection.SHORT) + + @pytest.mark.usefixtures("init_persistence") def test_rpc_delete_lock(mocker, default_conf): freqtradebot = get_patched_freqtradebot(mocker, default_conf) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 4ba81f930..82ee9d884 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1145,7 +1145,7 @@ def test_forceenter_handle(default_conf, update, mocker) -> None: telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot) - # /forcebuy ETH/BTC + # /forcelong ETH/BTC context = MagicMock() context.args = ["ETH/BTC"] telegram._forceenter(update=update, context=context, order_side=SignalDirection.LONG) @@ -1153,11 +1153,12 @@ def test_forceenter_handle(default_conf, update, mocker) -> None: assert fbuy_mock.call_count == 1 assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC' assert fbuy_mock.call_args_list[0][0][1] is None + assert fbuy_mock.call_args_list[0][1]['order_side'] == SignalDirection.LONG # Reset and retry with specified price fbuy_mock = MagicMock(return_value=None) mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock) - # /forcebuy ETH/BTC 0.055 + # /forcelong ETH/BTC 0.055 context = MagicMock() context.args = ["ETH/BTC", "0.055"] telegram._forceenter(update=update, context=context, order_side=SignalDirection.LONG) @@ -1204,7 +1205,7 @@ def test_forceenter_no_pair(default_conf, update, mocker) -> None: update = MagicMock() update.callback_query = MagicMock() update.callback_query.data = 'XRP/USDT_||_long' - telegram._forcebuy_inline(update, None) + telegram._forceenter_inline(update, None) assert fbuy_mock.call_count == 1