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/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_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 92e490646..f8352085e 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): @@ -249,7 +249,7 @@ class TradeResponse(BaseModel): total_trades: int -class ForceBuyResponse(BaseModel): +class ForceEnterResponse(BaseModel): __root__: Union[TradeSchema, StatusMsg] @@ -279,14 +279,15 @@ 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] -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 30f77edfe..93a160efb 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, ForceExitPayload, 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,23 +135,28 @@ 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', 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 - trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, 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 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('/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 e4fe924c0..cc0402b41 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -18,6 +18,8 @@ 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.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 @@ -662,7 +664,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 @@ -719,19 +721,24 @@ 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, - 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 """ 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') + 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: @@ -754,7 +761,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 diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5e849e81a..a0c3f3daf 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,8 @@ 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.enums.tradingmode import TradingMode from freqtrade.exceptions import OperationalException from freqtrade.misc import chunks, plural, round_coin_value from freqtrade.persistence import Trade @@ -113,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] @@ -150,8 +154,11 @@ class Telegram(RPCHandler): CommandHandler('balance', self._balance), CommandHandler('start', self._start), CommandHandler('stop', self._stop), - CommandHandler('forcesell', self._forcesell), - CommandHandler('forcebuy', self._forcebuy), + 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('trades', self._trades), CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), @@ -190,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) @@ -846,7 +853,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 @@ -860,25 +867,27 @@ 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: 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_forcebuy(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: + def _forceenter_inline(self, update: Update, _: CallbackContext) -> None: if update.callback_query: query = update.callback_query - pair = query.data - query.answer() - query.edit_message_text(text=f"Force Buying: {pair}") - self._forcebuy_action(pair) + 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], @@ -886,9 +895,10 @@ 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 _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 @@ -897,13 +907,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: @@ -1269,18 +1284,23 @@ class Telegram(RPCHandler): :param update: message update :return: None """ - forcebuy_text = ("*/forcebuy []:* `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: + 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" "*/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 ''}" + 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 " @@ -1374,6 +1394,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" 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. diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 1c924caa9..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 @@ -687,7 +688,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 +715,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 +766,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 +794,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 +811,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 @@ -1090,7 +1091,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) @@ -1106,16 +1107,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 +1124,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,11 +1138,11 @@ 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 -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()) @@ -1151,18 +1152,30 @@ 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: +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.'): - rpc._rpc_forcebuy(pair, None) + with pytest.raises(RPCException, match=r'Forceentry not enabled.'): + 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") diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 27fa5db3a..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_forcebuy", fbuy_mock) - rc = client_post(client, f"{BASE_URI}/forcebuy", + mocker.patch("freqtrade.rpc.RPC._rpc_force_entry", fbuy_mock) + 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( @@ -1099,9 +1103,9 @@ 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", + 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 f6ff396d4..82ee9d884 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'], " @@ -940,7 +943,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 +1010,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 +1068,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 +1112,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 +1121,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,36 +1131,37 @@ 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] -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) - 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) - # /forcebuy ETH/BTC + # /forcelong 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' 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_forcebuy', fbuy_mock) - # /forcebuy ETH/BTC 0.055 + mocker.patch('freqtrade.rpc.RPC._rpc_force_entry', fbuy_mock) + # /forcelong 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,24 +1169,24 @@ 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] == '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: +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) - 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) @@ -1190,7 +1194,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,8 +1204,8 @@ 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' - telegram._forcebuy_inline(update, None) + update.callback_query.data = 'XRP/USDT_||_long' + telegram._forceenter_inline(update, None) assert fbuy_mock.call_count == 1 diff --git a/tests/test_integration.py b/tests/test_integration.py index d7da68be3..c11b03b8d 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