Merge pull request #6306 from freqtrade/short_forceentry
add `/forcelong` and `/forceshort` commands
This commit is contained in:
commit
000b8ff281
@ -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 <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
| `forcesell all` | Instantly sells all open trades (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)
|
| `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.
|
| `performance` | Show performance of each finished trade grouped by pair.
|
||||||
| `balance` | Show account balance per currency.
|
| `balance` | Show account balance per currency.
|
||||||
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
|
| `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 pair: Pair to buy (ETH/BTC)
|
||||||
:param price: Optional - price to buy
|
: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
|
forcesell
|
||||||
Force-sell a trade.
|
Force-sell a trade.
|
||||||
|
|
||||||
@ -285,6 +293,9 @@ strategy
|
|||||||
|
|
||||||
:param strategy: Strategy class name
|
:param strategy: Strategy class name
|
||||||
|
|
||||||
|
sysinfo
|
||||||
|
Provides system information (CPU, RAM usage)
|
||||||
|
|
||||||
trade
|
trade
|
||||||
Return specific trade
|
Return specific trade
|
||||||
|
|
||||||
|
@ -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)
|
| `/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 <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
| `/forcesell all` | Instantly sells all open trades (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
|
| `/performance` | Show performance of each finished trade grouped by pair
|
||||||
| `/balance` | Show account balance per currency
|
| `/balance` | Show account balance per currency
|
||||||
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
|
| `/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)`
|
> **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)
|
![Telegram force-buy screenshot](assets/telegram_forcebuy.png)
|
||||||
|
|
||||||
|
@ -19,6 +19,6 @@ class SignalTagType(Enum):
|
|||||||
EXIT_TAG = "exit_tag"
|
EXIT_TAG = "exit_tag"
|
||||||
|
|
||||||
|
|
||||||
class SignalDirection(Enum):
|
class SignalDirection(str, Enum):
|
||||||
LONG = 'long'
|
LONG = 'long'
|
||||||
SHORT = 'short'
|
SHORT = 'short'
|
||||||
|
@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional, Union
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
from freqtrade.constants import DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.enums import OrderTypeValues
|
from freqtrade.enums import OrderTypeValues, SignalDirection
|
||||||
|
|
||||||
|
|
||||||
class Ping(BaseModel):
|
class Ping(BaseModel):
|
||||||
@ -249,7 +249,7 @@ class TradeResponse(BaseModel):
|
|||||||
total_trades: int
|
total_trades: int
|
||||||
|
|
||||||
|
|
||||||
class ForceBuyResponse(BaseModel):
|
class ForceEnterResponse(BaseModel):
|
||||||
__root__: Union[TradeSchema, StatusMsg]
|
__root__: Union[TradeSchema, StatusMsg]
|
||||||
|
|
||||||
|
|
||||||
@ -279,14 +279,15 @@ class Logs(BaseModel):
|
|||||||
logs: List[List]
|
logs: List[List]
|
||||||
|
|
||||||
|
|
||||||
class ForceBuyPayload(BaseModel):
|
class ForceEnterPayload(BaseModel):
|
||||||
pair: str
|
pair: str
|
||||||
|
side: SignalDirection = SignalDirection.LONG
|
||||||
price: Optional[float]
|
price: Optional[float]
|
||||||
ordertype: Optional[OrderTypeValues]
|
ordertype: Optional[OrderTypeValues]
|
||||||
stakeamount: Optional[float]
|
stakeamount: Optional[float]
|
||||||
|
|
||||||
|
|
||||||
class ForceSellPayload(BaseModel):
|
class ForceExitPayload(BaseModel):
|
||||||
tradeid: str
|
tradeid: str
|
||||||
ordertype: Optional[OrderTypeValues]
|
ordertype: Optional[OrderTypeValues]
|
||||||
|
|
||||||
|
@ -14,8 +14,8 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC
|
||||||
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
|
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
|
||||||
BlacklistResponse, Count, Daily,
|
BlacklistResponse, Count, Daily,
|
||||||
DeleteLockRequest, DeleteTrade, ForceBuyPayload,
|
DeleteLockRequest, DeleteTrade, ForceEnterPayload,
|
||||||
ForceBuyResponse, ForceSellPayload, Locks, Logs,
|
ForceEnterResponse, ForceExitPayload, Locks, Logs,
|
||||||
OpenTradeSchema, PairHistory, PerformanceEntry,
|
OpenTradeSchema, PairHistory, PerformanceEntry,
|
||||||
Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
|
Ping, PlotConfig, Profit, ResultMsg, ShowConfig,
|
||||||
Stats, StatusMsg, StrategyListResponse,
|
Stats, StatusMsg, StrategyListResponse,
|
||||||
@ -33,7 +33,9 @@ logger = logging.getLogger(__name__)
|
|||||||
# 1.11: forcebuy and forcesell accept ordertype
|
# 1.11: forcebuy and forcesell accept ordertype
|
||||||
# 1.12: add blacklist delete endpoint
|
# 1.12: add blacklist delete endpoint
|
||||||
# 1.13: forcebuy supports stake_amount
|
# 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.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
@ -133,23 +135,28 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
|
# /forcebuy is deprecated with short addition. use ForceEntry instead
|
||||||
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
@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
|
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||||
stake_amount = payload.stakeamount if payload.stakeamount 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:
|
if trade:
|
||||||
return ForceBuyResponse.parse_obj(trade.to_json())
|
return ForceEnterResponse.parse_obj(trade.to_json())
|
||||||
else:
|
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'])
|
@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
|
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'])
|
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
||||||
|
@ -18,6 +18,8 @@ from freqtrade.configuration.timerange import TimeRange
|
|||||||
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
|
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
|
||||||
from freqtrade.data.history import load_data
|
from freqtrade.data.history import load_data
|
||||||
from freqtrade.enums import SellType, State
|
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.exceptions import ExchangeError, PricingError
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||||
from freqtrade.loggers import bufferHandler
|
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.'}
|
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>.
|
Handler for forcesell <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
@ -719,19 +721,24 @@ class RPC:
|
|||||||
self._freqtrade.wallets.update()
|
self._freqtrade.wallets.update()
|
||||||
return {'result': f'Created sell order for trade {trade_id}.'}
|
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], *,
|
||||||
stake_amount: Optional[float] = None) -> Optional[Trade]:
|
order_type: Optional[str] = None,
|
||||||
|
order_side: SignalDirection = SignalDirection.LONG,
|
||||||
|
stake_amount: Optional[float] = None) -> Optional[Trade]:
|
||||||
"""
|
"""
|
||||||
Handler for forcebuy <asset> <price>
|
Handler for forcebuy <asset> <price>
|
||||||
Buys a pair trade at the given or current price
|
Buys a pair trade at the given or current price
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if not self._freqtrade.config.get('forcebuy_enable', False):
|
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:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
raise RPCException('trader is not 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.
|
# Check if pair quote currency equals to the stake currency.
|
||||||
stake_currency = self._freqtrade.config.get('stake_currency')
|
stake_currency = self._freqtrade.config.get('stake_currency')
|
||||||
if not self._freqtrade.exchange.get_pair_quote_currency(pair) == 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(
|
order_type = self._freqtrade.strategy.order_types.get(
|
||||||
'forcebuy', self._freqtrade.strategy.order_types['buy'])
|
'forcebuy', self._freqtrade.strategy.order_types['buy'])
|
||||||
if self._freqtrade.execute_entry(pair, stake_amount, price,
|
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.commit()
|
||||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||||
return trade
|
return trade
|
||||||
|
@ -7,6 +7,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from datetime import date, datetime, timedelta
|
from datetime import date, datetime, timedelta
|
||||||
|
from functools import partial
|
||||||
from html import escape
|
from html import escape
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
from math import isnan
|
from math import isnan
|
||||||
@ -23,6 +24,8 @@ from telegram.utils.helpers import escape_markdown
|
|||||||
from freqtrade.__init__ import __version__
|
from freqtrade.__init__ import __version__
|
||||||
from freqtrade.constants import DUST_PER_COIN
|
from freqtrade.constants import DUST_PER_COIN
|
||||||
from freqtrade.enums import RPCMessageType
|
from freqtrade.enums import RPCMessageType
|
||||||
|
from freqtrade.enums.signaltype import SignalDirection
|
||||||
|
from freqtrade.enums.tradingmode import TradingMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import chunks, plural, round_coin_value
|
from freqtrade.misc import chunks, plural, round_coin_value
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
@ -113,7 +116,8 @@ class Telegram(RPCHandler):
|
|||||||
r'/stopbuy$', r'/reload_config$', r'/show_config$',
|
r'/stopbuy$', r'/reload_config$', r'/show_config$',
|
||||||
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
|
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
|
||||||
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
|
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
|
# Create keys for generation
|
||||||
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
||||||
|
|
||||||
@ -150,8 +154,11 @@ class Telegram(RPCHandler):
|
|||||||
CommandHandler('balance', self._balance),
|
CommandHandler('balance', self._balance),
|
||||||
CommandHandler('start', self._start),
|
CommandHandler('start', self._start),
|
||||||
CommandHandler('stop', self._stop),
|
CommandHandler('stop', self._stop),
|
||||||
CommandHandler('forcesell', self._forcesell),
|
CommandHandler(['forcesell', 'forceexit'], self._forceexit),
|
||||||
CommandHandler('forcebuy', self._forcebuy),
|
CommandHandler(['forcebuy', 'forcelong'], partial(
|
||||||
|
self._forceenter, order_side=SignalDirection.LONG)),
|
||||||
|
CommandHandler('forceshort', partial(
|
||||||
|
self._forceenter, order_side=SignalDirection.SHORT)),
|
||||||
CommandHandler('trades', self._trades),
|
CommandHandler('trades', self._trades),
|
||||||
CommandHandler('delete', self._delete_trade),
|
CommandHandler('delete', self._delete_trade),
|
||||||
CommandHandler('performance', self._performance),
|
CommandHandler('performance', self._performance),
|
||||||
@ -190,7 +197,7 @@ class Telegram(RPCHandler):
|
|||||||
pattern='update_sell_reason_performance'),
|
pattern='update_sell_reason_performance'),
|
||||||
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
|
||||||
CallbackQueryHandler(self._count, pattern='update_count'),
|
CallbackQueryHandler(self._count, pattern='update_count'),
|
||||||
CallbackQueryHandler(self._forcebuy_inline),
|
CallbackQueryHandler(self._forceenter_inline),
|
||||||
]
|
]
|
||||||
for handle in handles:
|
for handle in handles:
|
||||||
self._updater.dispatcher.add_handler(handle)
|
self._updater.dispatcher.add_handler(handle)
|
||||||
@ -846,7 +853,7 @@ class Telegram(RPCHandler):
|
|||||||
self._send_msg('Status: `{status}`'.format(**msg))
|
self._send_msg('Status: `{status}`'.format(**msg))
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _forcesell(self, update: Update, context: CallbackContext) -> None:
|
def _forceexit(self, update: Update, context: CallbackContext) -> None:
|
||||||
"""
|
"""
|
||||||
Handler for /forcesell <id>.
|
Handler for /forcesell <id>.
|
||||||
Sells the given trade at current price
|
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'.")
|
self._send_msg("You must specify a trade-id or 'all'.")
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
msg = self._rpc._rpc_forcesell(trade_id)
|
msg = self._rpc._rpc_forceexit(trade_id)
|
||||||
self._send_msg('Forcesell Result: `{result}`'.format(**msg))
|
self._send_msg('Forcesell Result: `{result}`'.format(**msg))
|
||||||
|
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(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:
|
try:
|
||||||
self._rpc._rpc_forcebuy(pair, price)
|
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
|
||||||
except RPCException as e:
|
except RPCException as e:
|
||||||
self._send_msg(str(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:
|
if update.callback_query:
|
||||||
query = update.callback_query
|
query = update.callback_query
|
||||||
pair = query.data
|
if query.data and '_||_' in query.data:
|
||||||
query.answer()
|
pair, side = query.data.split('_||_')
|
||||||
query.edit_message_text(text=f"Force Buying: {pair}")
|
order_side = SignalDirection(side)
|
||||||
self._forcebuy_action(pair)
|
query.answer()
|
||||||
|
query.edit_message_text(text=f"Manually entering {order_side} for {pair}")
|
||||||
|
self._forceenter_action(pair, None, order_side)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _layout_inline_keyboard(buttons: List[InlineKeyboardButton],
|
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)]
|
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
|
||||||
|
|
||||||
@authorized_only
|
@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
|
Buys a pair trade at the given or current price
|
||||||
:param bot: telegram bot
|
:param bot: telegram bot
|
||||||
:param update: message update
|
:param update: message update
|
||||||
@ -897,13 +907,18 @@ class Telegram(RPCHandler):
|
|||||||
if context.args:
|
if context.args:
|
||||||
pair = context.args[0]
|
pair = context.args[0]
|
||||||
price = float(context.args[1]) if len(context.args) > 1 else None
|
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:
|
else:
|
||||||
whitelist = self._rpc._rpc_whitelist()['whitelist']
|
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?",
|
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
|
@authorized_only
|
||||||
def _trades(self, update: Update, context: CallbackContext) -> None:
|
def _trades(self, update: Update, context: CallbackContext) -> None:
|
||||||
@ -1269,18 +1284,23 @@ class Telegram(RPCHandler):
|
|||||||
:param update: message update
|
:param update: message update
|
||||||
:return: None
|
: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 "
|
"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:
|
||||||
|
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 = (
|
message = (
|
||||||
"_BotControl_\n"
|
"_BotControl_\n"
|
||||||
"------------\n"
|
"------------\n"
|
||||||
"*/start:* `Starts the trader`\n"
|
"*/start:* `Starts the trader`\n"
|
||||||
"*/stop:* Stops the trader\n"
|
"*/stop:* Stops the trader\n"
|
||||||
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \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"
|
"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"
|
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
|
||||||
"*/whitelist:* `Show current whitelist` \n"
|
"*/whitelist:* `Show current whitelist` \n"
|
||||||
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
|
||||||
@ -1374,6 +1394,7 @@ class Telegram(RPCHandler):
|
|||||||
self._send_msg(
|
self._send_msg(
|
||||||
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
|
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
|
||||||
f"*Exchange:* `{val['exchange']}`\n"
|
f"*Exchange:* `{val['exchange']}`\n"
|
||||||
|
f"*Market: * `{val['trading_mode']}`\n"
|
||||||
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
|
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
|
||||||
f"*Max open Trades:* `{val['max_open_trades']}`\n"
|
f"*Max open Trades:* `{val['max_open_trades']}`\n"
|
||||||
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
|
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
|
||||||
|
@ -261,6 +261,20 @@ class FtRestClient():
|
|||||||
}
|
}
|
||||||
return self._post("forcebuy", data=data)
|
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):
|
def forcesell(self, tradeid):
|
||||||
"""Force-sell a trade.
|
"""Force-sell a trade.
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from numpy import isnan
|
|||||||
|
|
||||||
from freqtrade.edge import PairInfo
|
from freqtrade.edge import PairInfo
|
||||||
from freqtrade.enums import State, TradingMode
|
from freqtrade.enums import State, TradingMode
|
||||||
|
from freqtrade.enums.signaltype import SignalDirection
|
||||||
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
|
from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.persistence.pairlock_middleware import PairLocks
|
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
|
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())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
@ -714,29 +715,29 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
|||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||||
rpc._rpc_forcesell(None)
|
rpc._rpc_forceexit(None)
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
with pytest.raises(RPCException, match=r'.*invalid argument*'):
|
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.'}
|
assert msg == {'result': 'Created sell orders for all open trades.'}
|
||||||
|
|
||||||
freqtradebot.enter_positions()
|
freqtradebot.enter_positions()
|
||||||
msg = rpc._rpc_forcesell('all')
|
msg = rpc._rpc_forceexit('all')
|
||||||
assert msg == {'result': 'Created sell orders for all open trades.'}
|
assert msg == {'result': 'Created sell orders for all open trades.'}
|
||||||
|
|
||||||
freqtradebot.enter_positions()
|
freqtradebot.enter_positions()
|
||||||
msg = rpc._rpc_forcesell('2')
|
msg = rpc._rpc_forceexit('2')
|
||||||
assert msg == {'result': 'Created sell order for trade 2.'}
|
assert msg == {'result': 'Created sell order for trade 2.'}
|
||||||
|
|
||||||
freqtradebot.state = State.STOPPED
|
freqtradebot.state = State.STOPPED
|
||||||
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
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*'):
|
with pytest.raises(RPCException, match=r'.*trader is not running*'):
|
||||||
rpc._rpc_forcesell('all')
|
rpc._rpc_forceexit('all')
|
||||||
|
|
||||||
freqtradebot.state = State.RUNNING
|
freqtradebot.state = State.RUNNING
|
||||||
assert cancel_order_mock.call_count == 0
|
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
|
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
|
||||||
# and trade amount is updated
|
# and trade amount is updated
|
||||||
rpc._rpc_forcesell('3')
|
rpc._rpc_forceexit('3')
|
||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
assert trade.amount == filled_amount
|
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
|
# 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 msg == {'result': 'Created sell order for trade 4.'}
|
||||||
assert cancel_order_mock.call_count == 2
|
assert cancel_order_mock.call_count == 2
|
||||||
assert trade.amount == amount
|
assert trade.amount == amount
|
||||||
@ -810,7 +811,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
|
|||||||
'filled': 0.0
|
'filled': 0.0
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
msg = rpc._rpc_forcesell('3')
|
msg = rpc._rpc_forceexit('3')
|
||||||
assert msg == {'result': 'Created sell order for trade 3.'}
|
assert msg == {'result': 'Created sell order for trade 3.'}
|
||||||
# status quo, no exchange calls
|
# status quo, no exchange calls
|
||||||
assert cancel_order_mock.call_count == 3
|
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
|
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
|
default_conf['forcebuy_enable'] = True
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
buy_mm = MagicMock(return_value=limit_buy_order_open)
|
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)
|
patch_get_signal(freqtradebot)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
trade = rpc._rpc_forcebuy(pair, None)
|
trade = rpc._rpc_force_entry(pair, None)
|
||||||
assert isinstance(trade, Trade)
|
assert isinstance(trade, Trade)
|
||||||
assert trade.pair == pair
|
assert trade.pair == pair
|
||||||
assert trade.open_rate == ticker()['bid']
|
assert trade.open_rate == ticker()['bid']
|
||||||
|
|
||||||
# Test buy duplicate
|
# Test buy duplicate
|
||||||
with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'):
|
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'
|
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 isinstance(trade, Trade)
|
||||||
assert trade.pair == pair
|
assert trade.pair == pair
|
||||||
assert trade.open_rate == 0.0001
|
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
|
# Test buy pair not with stakes
|
||||||
with pytest.raises(RPCException,
|
with pytest.raises(RPCException,
|
||||||
match=r'Wrong pair selected. Only pairs with stake-currency.*'):
|
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
|
# Test with defined stake_amount
|
||||||
pair = 'LTC/BTC'
|
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
|
assert trade.stake_amount == 0.05
|
||||||
|
|
||||||
# Test not buying
|
# Test not buying
|
||||||
@ -1137,11 +1138,11 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) ->
|
|||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
pair = 'TKN/BTC'
|
pair = 'TKN/BTC'
|
||||||
trade = rpc._rpc_forcebuy(pair, None)
|
trade = rpc._rpc_force_entry(pair, None)
|
||||||
assert trade is 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['forcebuy_enable'] = True
|
||||||
default_conf['initial_state'] = 'stopped'
|
default_conf['initial_state'] = 'stopped'
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
@ -1151,18 +1152,30 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None:
|
|||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
with pytest.raises(RPCException, match=r'trader is not running'):
|
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())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
with pytest.raises(RPCException, match=r'Forcebuy not enabled.'):
|
with pytest.raises(RPCException, match=r'Forceentry not enabled.'):
|
||||||
rpc._rpc_forcebuy(pair, 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")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@ -543,7 +543,7 @@ def test_api_show_config(botclient):
|
|||||||
assert 'unfilledtimeout' in response
|
assert 'unfilledtimeout' in response
|
||||||
assert 'version' in response
|
assert 'version' in response
|
||||||
assert 'api_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):
|
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
|
# 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
|
ftbot, client = botclient
|
||||||
|
|
||||||
rc = client_post(client, f"{BASE_URI}/forcebuy",
|
rc = client_post(client, f"{BASE_URI}/{endpoint}",
|
||||||
data='{"pair": "ETH/BTC"}')
|
data='{"pair": "ETH/BTC"}')
|
||||||
assert_response(rc, 502)
|
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
|
# enable forcebuy
|
||||||
ftbot.config['forcebuy_enable'] = True
|
ftbot.config['forcebuy_enable'] = True
|
||||||
|
|
||||||
fbuy_mock = MagicMock(return_value=None)
|
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",
|
rc = client_post(client, f"{BASE_URI}/{endpoint}",
|
||||||
data='{"pair": "ETH/BTC"}')
|
data='{"pair": "ETH/BTC"}')
|
||||||
assert_response(rc)
|
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
|
# Test creating trade
|
||||||
fbuy_mock = MagicMock(return_value=Trade(
|
fbuy_mock = MagicMock(return_value=Trade(
|
||||||
@ -1099,9 +1103,9 @@ def test_api_forcebuy(botclient, mocker, fee):
|
|||||||
timeframe=5,
|
timeframe=5,
|
||||||
strategy=CURRENT_TEST_STRATEGY
|
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"}')
|
data='{"pair": "ETH/BTC"}')
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json() == {
|
assert rc.json() == {
|
||||||
|
@ -19,6 +19,7 @@ from freqtrade import __version__
|
|||||||
from freqtrade.constants import CANCEL_REASON
|
from freqtrade.constants import CANCEL_REASON
|
||||||
from freqtrade.edge import PairInfo
|
from freqtrade.edge import PairInfo
|
||||||
from freqtrade.enums import RPCMessageType, RunMode, SellType, State
|
from freqtrade.enums import RPCMessageType, RunMode, SellType, State
|
||||||
|
from freqtrade.enums.signaltype import SignalDirection
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.loggers import setup_logging
|
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
|
assert start_polling.start_polling.call_count == 1
|
||||||
|
|
||||||
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
||||||
"['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], "
|
"['balance'], ['start'], ['stop'], "
|
||||||
"['delete'], ['performance'], ['buys', 'entries'], ['sells'], ['mix_tags'], "
|
"['forcesell', 'forceexit'], ['forcebuy', 'forcelong'], ['forceshort'], "
|
||||||
|
"['trades'], ['delete'], ['performance'], "
|
||||||
|
"['buys', 'entries'], ['sells'], ['mix_tags'], "
|
||||||
"['stats'], ['daily'], ['weekly'], ['monthly'], "
|
"['stats'], ['daily'], ['weekly'], ['monthly'], "
|
||||||
"['count'], ['locks'], ['unlock', 'delete_locks'], "
|
"['count'], ['locks'], ['unlock', 'delete_locks'], "
|
||||||
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
|
"['reload_config', 'reload_conf'], ['show_config', 'show_conf'], "
|
||||||
@ -940,7 +943,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
|
|||||||
# /forcesell 1
|
# /forcesell 1
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["1"]
|
context.args = ["1"]
|
||||||
telegram._forcesell(update=update, context=context)
|
telegram._forceexit(update=update, context=context)
|
||||||
|
|
||||||
assert msg_mock.call_count == 4
|
assert msg_mock.call_count == 4
|
||||||
last_msg = msg_mock.call_args_list[-2][0][0]
|
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
|
# /forcesell 1
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["1"]
|
context.args = ["1"]
|
||||||
telegram._forcesell(update=update, context=context)
|
telegram._forceexit(update=update, context=context)
|
||||||
|
|
||||||
assert msg_mock.call_count == 4
|
assert msg_mock.call_count == 4
|
||||||
|
|
||||||
@ -1065,7 +1068,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
|
|||||||
# /forcesell all
|
# /forcesell all
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["all"]
|
context.args = ["all"]
|
||||||
telegram._forcesell(update=update, context=context)
|
telegram._forceexit(update=update, context=context)
|
||||||
|
|
||||||
# Called for each trade 2 times
|
# Called for each trade 2 times
|
||||||
assert msg_mock.call_count == 8
|
assert msg_mock.call_count == 8
|
||||||
@ -1109,7 +1112,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
|||||||
# /forcesell 1
|
# /forcesell 1
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["1"]
|
context.args = ["1"]
|
||||||
telegram._forcesell(update=update, context=context)
|
telegram._forceexit(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'not running' in msg_mock.call_args_list[0][0][0]
|
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
|
freqtradebot.state = State.RUNNING
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = []
|
context.args = []
|
||||||
telegram._forcesell(update=update, context=context)
|
telegram._forceexit(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert "You must specify a trade-id or 'all'." in msg_mock.call_args_list[0][0][0]
|
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
|
# /forcesell 123456
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["123456"]
|
context.args = ["123456"]
|
||||||
telegram._forcesell(update=update, context=context)
|
telegram._forceexit(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'invalid argument' in msg_mock.call_args_list[0][0][0]
|
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)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
|
|
||||||
fbuy_mock = MagicMock(return_value=None)
|
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)
|
telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
|
|
||||||
# /forcebuy ETH/BTC
|
# /forcelong ETH/BTC
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["ETH/BTC"]
|
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_count == 1
|
||||||
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
|
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][0][1] is None
|
||||||
|
assert fbuy_mock.call_args_list[0][1]['order_side'] == SignalDirection.LONG
|
||||||
|
|
||||||
# Reset and retry with specified price
|
# Reset and retry with specified price
|
||||||
fbuy_mock = MagicMock(return_value=None)
|
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
|
# /forcelong ETH/BTC 0.055
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = ["ETH/BTC", "0.055"]
|
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_count == 1
|
||||||
assert fbuy_mock.call_args_list[0][0][0] == 'ETH/BTC'
|
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
|
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)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
|
|
||||||
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
patch_get_signal(freqtradebot)
|
patch_get_signal(freqtradebot)
|
||||||
|
|
||||||
update.message.text = '/forcebuy ETH/Nonepair'
|
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_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)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
|
|
||||||
fbuy_mock = MagicMock(return_value=None)
|
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)
|
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 = MagicMock()
|
||||||
context.args = []
|
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 fbuy_mock.call_count == 0
|
||||||
assert msg_mock.call_count == 1
|
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
|
assert reduce(lambda acc, x: acc + len(x), keyboard, 0) == 4
|
||||||
update = MagicMock()
|
update = MagicMock()
|
||||||
update.callback_query = MagicMock()
|
update.callback_query = MagicMock()
|
||||||
update.callback_query.data = 'XRP/USDT'
|
update.callback_query.data = 'XRP/USDT_||_long'
|
||||||
telegram._forcebuy_inline(update, None)
|
telegram._forceenter_inline(update, None)
|
||||||
assert fbuy_mock.call_count == 1
|
assert fbuy_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@ -179,7 +179,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, mocker, balance_rati
|
|||||||
assert len(trades) == 4
|
assert len(trades) == 4
|
||||||
assert freqtrade.wallets.get_trade_stake_amount('XRP/BTC') == result1
|
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()
|
trades = Trade.query.all()
|
||||||
assert len(trades) == 5
|
assert len(trades) == 5
|
||||||
|
Loading…
Reference in New Issue
Block a user