Merge pull request #6306 from freqtrade/short_forceentry
add `/forcelong` and `/forceshort` commands
This commit is contained in:
@@ -19,6 +19,6 @@ class SignalTagType(Enum):
|
||||
EXIT_TAG = "exit_tag"
|
||||
|
||||
|
||||
class SignalDirection(Enum):
|
||||
class SignalDirection(str, Enum):
|
||||
LONG = 'long'
|
||||
SHORT = 'short'
|
||||
|
@@ -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]
|
||||
|
||||
|
@@ -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'])
|
||||
|
@@ -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,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 <asset> <price>
|
||||
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
|
||||
|
@@ -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
|
||||
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 <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,18 +1284,23 @@ class Telegram(RPCHandler):
|
||||
:param update: message update
|
||||
:return: None
|
||||
"""
|
||||
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
|
||||
"Optionally takes a rate at which to buy "
|
||||
"(only applies to limit orders).` \n")
|
||||
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"
|
||||
"------------\n"
|
||||
"*/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"
|
||||
|
Reference in New Issue
Block a user