Merge pull request #6306 from freqtrade/short_forceentry

add `/forcelong` and `/forceshort` commands
This commit is contained in:
Matthias 2022-01-30 07:36:14 +01:00 committed by GitHub
commit 000b8ff281
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 190 additions and 103 deletions

View File

@ -148,6 +148,7 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
| `forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
| `forceenter <pair> <side> [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 <n>` | 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

View File

@ -173,7 +173,8 @@ official commands. You can ask at any moment for help with `/help`.
| `/profit [<n>]` | 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 <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
| `/forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`forcebuy_enable` must be set to True)
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`forcebuy_enable` must be set to True)
| `/forceshort <pair> [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 <n>` | 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 <pair> [rate]
### /forcelong <pair> [rate] | /forceshort <pair> [rate]
> **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
`/forcebuy <pair> [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)

View File

@ -19,6 +19,6 @@ class SignalTagType(Enum):
EXIT_TAG = "exit_tag"
class SignalDirection(Enum):
class SignalDirection(str, Enum):
LONG = 'long'
SHORT = 'short'

View File

@ -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]

View File

@ -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'])

View File

@ -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 <id>.
Sells the given trade at current price
@ -719,7 +721,9 @@ 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_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 <asset> <price>
@ -727,11 +731,14 @@ 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')
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

View File

@ -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 <id>.
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
if query.data and '_||_' in 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,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 <asset> <price>.
Handler for /forcelong <asset> <price> and `/forceshort <asset> <price>
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,8 +1284,13 @@ class Telegram(RPCHandler):
:param update: message update
:return: None
"""
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
forceenter_text = ("*/forcelong <pair> [<rate>]:* `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 <pair> [<rate>]:* `Instantly shorts the given pair. "
"Optionally takes a rate at which to sell "
"(only applies to limit orders).` \n")
message = (
"_BotControl_\n"
@ -1278,9 +1298,9 @@ class Telegram(RPCHandler):
"*/start:* `Starts the trader`\n"
"*/stop:* Stops the trader\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
"*/forceexit <trade_id>|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 <trade_id>:* `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"

View File

@ -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.

View File

@ -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")

View File

@ -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() == {

View File

@ -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

View File

@ -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