Merge branch 'develop' into cyber-forcesell-tg
This commit is contained in:
@@ -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, TradingMode
|
||||
|
||||
|
||||
class Ping(BaseModel):
|
||||
@@ -38,6 +38,11 @@ class Balance(BaseModel):
|
||||
used: float
|
||||
est_stake: float
|
||||
stake: str
|
||||
# Starting with 2.x
|
||||
side: str
|
||||
leverage: float
|
||||
is_position: bool
|
||||
position: float
|
||||
|
||||
|
||||
class Balances(BaseModel):
|
||||
@@ -126,18 +131,18 @@ class Daily(BaseModel):
|
||||
|
||||
|
||||
class UnfilledTimeout(BaseModel):
|
||||
buy: Optional[int]
|
||||
sell: Optional[int]
|
||||
entry: Optional[int]
|
||||
exit: Optional[int]
|
||||
unit: Optional[str]
|
||||
exit_timeout_count: Optional[int]
|
||||
|
||||
|
||||
class OrderTypes(BaseModel):
|
||||
buy: OrderTypeValues
|
||||
sell: OrderTypeValues
|
||||
emergencysell: Optional[OrderTypeValues]
|
||||
forcesell: Optional[OrderTypeValues]
|
||||
forcebuy: Optional[OrderTypeValues]
|
||||
entry: OrderTypeValues
|
||||
exit: OrderTypeValues
|
||||
emergencyexit: Optional[OrderTypeValues]
|
||||
forceexit: Optional[OrderTypeValues]
|
||||
forceentry: Optional[OrderTypeValues]
|
||||
stoploss: OrderTypeValues
|
||||
stoploss_on_exchange: bool
|
||||
stoploss_on_exchange_interval: Optional[int]
|
||||
@@ -148,6 +153,8 @@ class ShowConfig(BaseModel):
|
||||
strategy_version: Optional[str]
|
||||
api_version: float
|
||||
dry_run: bool
|
||||
trading_mode: str
|
||||
short_allowed: bool
|
||||
stake_currency: str
|
||||
stake_amount: str
|
||||
available_capital: Optional[float]
|
||||
@@ -168,8 +175,8 @@ class ShowConfig(BaseModel):
|
||||
exchange: str
|
||||
strategy: Optional[str]
|
||||
forcebuy_enabled: bool
|
||||
ask_strategy: Dict[str, Any]
|
||||
bid_strategy: Dict[str, Any]
|
||||
exit_pricing: Dict[str, Any]
|
||||
entry_pricing: Dict[str, Any]
|
||||
bot_name: str
|
||||
state: str
|
||||
runmode: str
|
||||
@@ -197,12 +204,14 @@ class TradeSchema(BaseModel):
|
||||
trade_id: int
|
||||
pair: str
|
||||
is_open: bool
|
||||
is_short: bool
|
||||
exchange: str
|
||||
amount: float
|
||||
amount_requested: float
|
||||
stake_amount: float
|
||||
strategy: str
|
||||
buy_tag: Optional[str]
|
||||
buy_tag: Optional[str] # Deprecated
|
||||
enter_tag: Optional[str]
|
||||
timeframe: int
|
||||
fee_open: Optional[float]
|
||||
fee_open_cost: Optional[float]
|
||||
@@ -242,6 +251,11 @@ class TradeSchema(BaseModel):
|
||||
open_order_id: Optional[str]
|
||||
orders: List[OrderSchema]
|
||||
|
||||
leverage: Optional[float]
|
||||
interest_rate: Optional[float]
|
||||
funding_fees: Optional[float]
|
||||
trading_mode: Optional[TradingMode]
|
||||
|
||||
|
||||
class OpenTradeSchema(TradeSchema):
|
||||
stoploss_current_dist: Optional[float]
|
||||
@@ -262,7 +276,7 @@ class TradeResponse(BaseModel):
|
||||
total_trades: int
|
||||
|
||||
|
||||
class ForceBuyResponse(BaseModel):
|
||||
class ForceEnterResponse(BaseModel):
|
||||
__root__: Union[TradeSchema, StatusMsg]
|
||||
|
||||
|
||||
@@ -292,15 +306,16 @@ 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]
|
||||
entry_tag: Optional[str]
|
||||
|
||||
|
||||
class ForceSellPayload(BaseModel):
|
||||
class ForceExitPayload(BaseModel):
|
||||
tradeid: str
|
||||
ordertype: Optional[OrderTypeValues]
|
||||
|
||||
@@ -364,6 +379,10 @@ class PairHistory(BaseModel):
|
||||
length: int
|
||||
buy_signals: int
|
||||
sell_signals: int
|
||||
enter_long_signals: int
|
||||
exit_long_signals: int
|
||||
enter_short_signals: int
|
||||
exit_short_signals: int
|
||||
last_analyzed: datetime
|
||||
last_analyzed_ts: int
|
||||
data_start_ts: int
|
||||
|
@@ -9,13 +9,14 @@ from fastapi.exceptions import HTTPException
|
||||
from freqtrade import __version__
|
||||
from freqtrade.constants import USERPATH_STRATEGIES
|
||||
from freqtrade.data.history import get_datahandler
|
||||
from freqtrade.enums import CandleType, TradingMode
|
||||
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, Health, Locks,
|
||||
Logs, OpenTradeSchema, PairHistory,
|
||||
DeleteLockRequest, DeleteTrade, ForceEnterPayload,
|
||||
ForceEnterResponse, ForceExitPayload, Health,
|
||||
Locks, Logs, OpenTradeSchema, PairHistory,
|
||||
PerformanceEntry, Ping, PlotConfig, Profit,
|
||||
ResultMsg, ShowConfig, Stats, StatusMsg,
|
||||
StrategyListResponse, StrategyResponse, SysInfo,
|
||||
@@ -32,8 +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
|
||||
# 1.14: Add entry/exit orders to trade response
|
||||
API_VERSION = 1.14
|
||||
# versions 2.xx -> futures/short branch
|
||||
# 2.14: Add entry/exit orders to trade response
|
||||
API_VERSION = 2.14
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
@@ -133,24 +135,30 @@ 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
|
||||
entry_tag = payload.entry_tag if payload.entry_tag else 'forceentry'
|
||||
|
||||
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount, entry_tag)
|
||||
trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
|
||||
order_type=ordertype, stake_amount=stake_amount,
|
||||
enter_tag=entry_tag)
|
||||
|
||||
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'])
|
||||
@@ -268,16 +276,22 @@ def get_strategy(strategy: str, config=Depends(get_config)):
|
||||
|
||||
@router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data'])
|
||||
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
|
||||
config=Depends(get_config)):
|
||||
candletype: Optional[CandleType] = None, config=Depends(get_config)):
|
||||
|
||||
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', None))
|
||||
|
||||
pair_interval = dh.ohlcv_get_available_data(config['datadir'])
|
||||
trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
|
||||
pair_interval = dh.ohlcv_get_available_data(config['datadir'], trading_mode)
|
||||
|
||||
if timeframe:
|
||||
pair_interval = [pair for pair in pair_interval if pair[1] == timeframe]
|
||||
if stake_currency:
|
||||
pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)]
|
||||
if candletype:
|
||||
pair_interval = [pair for pair in pair_interval if pair[2] == candletype]
|
||||
else:
|
||||
candle_type = CandleType.get_default(trading_mode)
|
||||
pair_interval = [pair for pair in pair_interval if pair[2] == candle_type]
|
||||
|
||||
pair_interval = sorted(pair_interval, key=lambda x: x[0])
|
||||
|
||||
pairs = list({x[0] for x in pair_interval})
|
||||
|
@@ -63,7 +63,7 @@ class CryptoToFiatConverter:
|
||||
except RequestException as request_exception:
|
||||
if "429" in str(request_exception):
|
||||
logger.warning(
|
||||
"Too many requests for Coingecko API, backing off and trying again later.")
|
||||
"Too many requests for CoinGecko API, backing off and trying again later.")
|
||||
# Set backoff timestamp to 60 seconds in the future
|
||||
self._backoff = datetime.datetime.now().timestamp() + 60
|
||||
return
|
||||
@@ -96,7 +96,7 @@ class CryptoToFiatConverter:
|
||||
|
||||
if len(found) > 0:
|
||||
# Wrong!
|
||||
logger.warning(f"Found multiple mappings in goingekko for {crypto_symbol}.")
|
||||
logger.warning(f"Found multiple mappings in CoinGecko for {crypto_symbol}.")
|
||||
return None
|
||||
|
||||
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||
@@ -160,7 +160,7 @@ class CryptoToFiatConverter:
|
||||
|
||||
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
|
||||
"""
|
||||
Call CoinGekko API to retrieve the price in the FIAT
|
||||
Call CoinGecko API to retrieve the price in the FIAT
|
||||
:param crypto_symbol: Crypto-currency you want to convert (e.g btc)
|
||||
:param fiat_symbol: FIAT currency you want to convert to (e.g usd)
|
||||
:return: float, price of the crypto-currency in Fiat
|
||||
|
@@ -18,7 +18,8 @@ from freqtrade import __version__
|
||||
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 import (CandleType, ExitCheckTuple, ExitType, SignalDirection, State,
|
||||
TradingMode)
|
||||
from freqtrade.exceptions import ExchangeError, PricingError
|
||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
|
||||
from freqtrade.loggers import bufferHandler
|
||||
@@ -27,7 +28,7 @@ from freqtrade.persistence import PairLocks, Trade
|
||||
from freqtrade.persistence.models import PairLock
|
||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
|
||||
from freqtrade.strategy.interface import SellCheckTuple
|
||||
from freqtrade.wallets import PositionWallet, Wallet
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -110,6 +111,8 @@ class RPC:
|
||||
'version': __version__,
|
||||
'strategy_version': strategy_version,
|
||||
'dry_run': config['dry_run'],
|
||||
'trading_mode': config.get('trading_mode', 'spot'),
|
||||
'short_allowed': config.get('trading_mode', 'spot') != 'spot',
|
||||
'stake_currency': config['stake_currency'],
|
||||
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
|
||||
'stake_amount': str(config['stake_amount']),
|
||||
@@ -134,8 +137,8 @@ class RPC:
|
||||
'exchange': config['exchange']['name'],
|
||||
'strategy': config['strategy'],
|
||||
'forcebuy_enabled': config.get('forcebuy_enable', False),
|
||||
'ask_strategy': config.get('ask_strategy', {}),
|
||||
'bid_strategy': config.get('bid_strategy', {}),
|
||||
'exit_pricing': config.get('exit_pricing', {}),
|
||||
'entry_pricing': config.get('entry_pricing', {}),
|
||||
'state': str(botstate),
|
||||
'runmode': config['runmode'].value,
|
||||
'position_adjustment_enable': config.get('position_adjustment_enable', False),
|
||||
@@ -153,7 +156,7 @@ class RPC:
|
||||
"""
|
||||
# Fetch open trades
|
||||
if trade_ids:
|
||||
trades = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
|
||||
trades: List[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
|
||||
else:
|
||||
trades = Trade.get_open_trades()
|
||||
|
||||
@@ -169,7 +172,7 @@ class RPC:
|
||||
if trade.is_open:
|
||||
try:
|
||||
current_rate = self._freqtrade.exchange.get_rate(
|
||||
trade.pair, refresh=False, side="sell")
|
||||
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
|
||||
except (ExchangeError, PricingError):
|
||||
current_rate = NAN
|
||||
else:
|
||||
@@ -219,7 +222,7 @@ class RPC:
|
||||
|
||||
def _rpc_status_table(self, stake_currency: str,
|
||||
fiat_display_currency: str) -> Tuple[List, List, float]:
|
||||
trades = Trade.get_open_trades()
|
||||
trades: List[Trade] = Trade.get_open_trades()
|
||||
if not trades:
|
||||
raise RPCException('no active trade')
|
||||
else:
|
||||
@@ -229,11 +232,12 @@ class RPC:
|
||||
# calculate profit and send message to user
|
||||
try:
|
||||
current_rate = self._freqtrade.exchange.get_rate(
|
||||
trade.pair, refresh=False, side="sell")
|
||||
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
|
||||
except (PricingError, ExchangeError):
|
||||
current_rate = NAN
|
||||
trade_profit = trade.calc_profit(current_rate)
|
||||
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
|
||||
direction_str = 'S' if trade.is_short else 'L'
|
||||
if self._fiat_converter:
|
||||
fiat_profit = self._fiat_converter.convert_amount(
|
||||
trade_profit,
|
||||
@@ -245,7 +249,7 @@ class RPC:
|
||||
fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \
|
||||
else fiat_profit_sum + fiat_profit
|
||||
detail_trade = [
|
||||
trade.id,
|
||||
f'{trade.id} {direction_str}',
|
||||
trade.pair + ('*' if (trade.open_order_id is not None
|
||||
and trade.close_rate_requested is None) else '')
|
||||
+ ('**' if (trade.close_rate_requested is not None) else ''),
|
||||
@@ -253,20 +257,19 @@ class RPC:
|
||||
profit_str
|
||||
]
|
||||
if self._config.get('position_adjustment_enable', False):
|
||||
max_buy_str = ''
|
||||
max_entry_str = ''
|
||||
if self._config.get('max_entry_position_adjustment', -1) > 0:
|
||||
max_buy_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
|
||||
filled_buys = trade.nr_of_successful_buys
|
||||
detail_trade.append(f"{filled_buys}{max_buy_str}")
|
||||
max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
|
||||
filled_entries = trade.nr_of_successful_entries
|
||||
detail_trade.append(f"{filled_entries}{max_entry_str}")
|
||||
trades_list.append(detail_trade)
|
||||
profitcol = "Profit"
|
||||
if self._fiat_converter:
|
||||
profitcol += " (" + fiat_display_currency + ")"
|
||||
|
||||
columns = ['ID L/S', 'Pair', 'Since', profitcol]
|
||||
if self._config.get('position_adjustment_enable', False):
|
||||
columns = ['ID', 'Pair', 'Since', profitcol, '# Entries']
|
||||
else:
|
||||
columns = ['ID', 'Pair', 'Since', profitcol]
|
||||
columns.append('# Entries')
|
||||
return trades_list, columns, fiat_profit_sum
|
||||
|
||||
def _rpc_daily_profit(
|
||||
@@ -453,7 +456,7 @@ class RPC:
|
||||
""" Returns cumulative profit statistics """
|
||||
trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
|
||||
Trade.is_open.is_(True))
|
||||
trades = Trade.get_trades(trade_filter).order_by(Trade.id).all()
|
||||
trades: List[Trade] = Trade.get_trades(trade_filter).order_by(Trade.id).all()
|
||||
|
||||
profit_all_coin = []
|
||||
profit_all_ratio = []
|
||||
@@ -483,7 +486,7 @@ class RPC:
|
||||
# Get current rate
|
||||
try:
|
||||
current_rate = self._freqtrade.exchange.get_rate(
|
||||
trade.pair, refresh=False, side="sell")
|
||||
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
|
||||
except (PricingError, ExchangeError):
|
||||
current_rate = NAN
|
||||
profit_ratio = trade.calc_profit_ratio(rate=current_rate)
|
||||
@@ -559,7 +562,7 @@ class RPC:
|
||||
|
||||
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
|
||||
""" Returns current account balance per crypto """
|
||||
output = []
|
||||
currencies = []
|
||||
total = 0.0
|
||||
try:
|
||||
tickers = self._freqtrade.exchange.get_tickers(cached=True)
|
||||
@@ -570,7 +573,8 @@ class RPC:
|
||||
starting_capital = self._freqtrade.wallets.get_starting_balance()
|
||||
starting_cap_fiat = self._fiat_converter.convert_amount(
|
||||
starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
|
||||
|
||||
coin: str
|
||||
balance: Wallet
|
||||
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
|
||||
if not balance.total:
|
||||
continue
|
||||
@@ -579,6 +583,9 @@ class RPC:
|
||||
if coin == stake_currency:
|
||||
rate = 1.0
|
||||
est_stake = balance.total
|
||||
if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
|
||||
# in Futures, "total" includes the locked stake, and therefore all positions
|
||||
est_stake = balance.free
|
||||
else:
|
||||
try:
|
||||
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
|
||||
@@ -591,13 +598,35 @@ class RPC:
|
||||
logger.warning(f" Could not get rate for pair {coin}.")
|
||||
continue
|
||||
total = total + (est_stake or 0)
|
||||
output.append({
|
||||
currencies.append({
|
||||
'currency': coin,
|
||||
# TODO: The below can be simplified if we don't assign None to values.
|
||||
'free': balance.free if balance.free is not None else 0,
|
||||
'balance': balance.total if balance.total is not None else 0,
|
||||
'used': balance.used if balance.used is not None else 0,
|
||||
'est_stake': est_stake or 0,
|
||||
'stake': stake_currency,
|
||||
'side': 'long',
|
||||
'leverage': 1,
|
||||
'position': 0,
|
||||
'is_position': False,
|
||||
})
|
||||
symbol: str
|
||||
position: PositionWallet
|
||||
for symbol, position in self._freqtrade.wallets.get_all_positions().items():
|
||||
total += position.collateral
|
||||
|
||||
currencies.append({
|
||||
'currency': symbol,
|
||||
'free': 0,
|
||||
'balance': 0,
|
||||
'used': 0,
|
||||
'position': position.position,
|
||||
'est_stake': position.collateral,
|
||||
'stake': stake_currency,
|
||||
'leverage': position.leverage,
|
||||
'side': position.side,
|
||||
'is_position': True
|
||||
})
|
||||
|
||||
value = self._fiat_converter.convert_amount(
|
||||
@@ -609,7 +638,7 @@ class RPC:
|
||||
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
|
||||
|
||||
return {
|
||||
'currencies': output,
|
||||
'currencies': currencies,
|
||||
'total': total,
|
||||
'symbol': fiat_display_currency,
|
||||
'value': value,
|
||||
@@ -655,7 +684,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
|
||||
@@ -666,24 +695,24 @@ class RPC:
|
||||
if trade.open_order_id:
|
||||
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
|
||||
|
||||
if order['side'] == 'buy':
|
||||
if order['side'] == trade.enter_side:
|
||||
fully_canceled = self._freqtrade.handle_cancel_enter(
|
||||
trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||
|
||||
if order['side'] == 'sell':
|
||||
if order['side'] == trade.exit_side:
|
||||
# Cancel order - so it is placed anew with a fresh price.
|
||||
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
|
||||
|
||||
if not fully_canceled:
|
||||
# Get current rate and execute sell
|
||||
current_rate = self._freqtrade.exchange.get_rate(
|
||||
trade.pair, refresh=False, side="sell")
|
||||
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
||||
trade.pair, side='exit', is_short=trade.is_short, refresh=True)
|
||||
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_SELL)
|
||||
order_type = ordertype or self._freqtrade.strategy.order_types.get(
|
||||
"forcesell", self._freqtrade.strategy.order_types["sell"])
|
||||
"forceexit", self._freqtrade.strategy.order_types["exit"])
|
||||
|
||||
self._freqtrade.execute_trade_exit(
|
||||
trade, current_rate, sell_reason, ordertype=order_type)
|
||||
trade, current_rate, exit_check, ordertype=order_type)
|
||||
# ---- EOF def _exec_forcesell ----
|
||||
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
@@ -703,7 +732,7 @@ class RPC:
|
||||
trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
|
||||
).first()
|
||||
if not trade:
|
||||
logger.warning('forcesell: Invalid argument received')
|
||||
logger.warning('forceexit: Invalid argument received')
|
||||
raise RPCException('invalid argument')
|
||||
|
||||
_exec_forcesell(trade)
|
||||
@@ -711,20 +740,25 @@ 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,
|
||||
buy_tag: Optional[str] = 'forceentry') -> 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,
|
||||
enter_tag: Optional[str] = 'forceentry') -> 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:
|
||||
@@ -733,8 +767,10 @@ class RPC:
|
||||
# check if valid pair
|
||||
|
||||
# check if pair already has an open pair
|
||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||
trade: Trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||
is_short = (order_side == SignalDirection.SHORT)
|
||||
if trade:
|
||||
is_short = trade.is_short
|
||||
if not self._freqtrade.strategy.position_adjustment_enable:
|
||||
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
||||
|
||||
@@ -745,9 +781,12 @@ class RPC:
|
||||
# execute buy
|
||||
if not order_type:
|
||||
order_type = self._freqtrade.strategy.order_types.get(
|
||||
'forcebuy', self._freqtrade.strategy.order_types['buy'])
|
||||
'forceentry', self._freqtrade.strategy.order_types['entry'])
|
||||
if self._freqtrade.execute_entry(pair, stake_amount, price,
|
||||
ordertype=order_type, trade=trade, buy_tag=buy_tag):
|
||||
ordertype=order_type, trade=trade,
|
||||
is_short=is_short,
|
||||
enter_tag=enter_tag,
|
||||
):
|
||||
Trade.commit()
|
||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||
return trade
|
||||
@@ -802,27 +841,23 @@ class RPC:
|
||||
|
||||
return pair_rates
|
||||
|
||||
def _rpc_buy_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
def _rpc_enter_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for buy tag performance.
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
buy_tags = Trade.get_buy_tag_performance(pair)
|
||||
|
||||
return buy_tags
|
||||
return Trade.get_enter_tag_performance(pair)
|
||||
|
||||
def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for sell reason performance.
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
sell_reasons = Trade.get_sell_reason_performance(pair)
|
||||
|
||||
return sell_reasons
|
||||
return Trade.get_sell_reason_performance(pair)
|
||||
|
||||
def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Handler for mix tag (buy_tag + sell_reason) performance.
|
||||
Handler for mix tag (enter_tag + sell_reason) performance.
|
||||
Shows a performance statistic from finished trades
|
||||
"""
|
||||
mix_tags = Trade.get_mix_tag_performance(pair)
|
||||
@@ -945,20 +980,21 @@ class RPC:
|
||||
def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame,
|
||||
last_analyzed: datetime) -> Dict[str, Any]:
|
||||
has_content = len(dataframe) != 0
|
||||
buy_signals = 0
|
||||
sell_signals = 0
|
||||
signals = {
|
||||
'enter_long': 0,
|
||||
'exit_long': 0,
|
||||
'enter_short': 0,
|
||||
'exit_short': 0,
|
||||
}
|
||||
if has_content:
|
||||
|
||||
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
|
||||
# Move signal close to separate column when signal for easy plotting
|
||||
if 'buy' in dataframe.columns:
|
||||
buy_mask = (dataframe['buy'] == 1)
|
||||
buy_signals = int(buy_mask.sum())
|
||||
dataframe.loc[buy_mask, '_buy_signal_close'] = dataframe.loc[buy_mask, 'close']
|
||||
if 'sell' in dataframe.columns:
|
||||
sell_mask = (dataframe['sell'] == 1)
|
||||
sell_signals = int(sell_mask.sum())
|
||||
dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close']
|
||||
for sig_type in signals.keys():
|
||||
if sig_type in dataframe.columns:
|
||||
mask = (dataframe[sig_type] == 1)
|
||||
signals[sig_type] = int(mask.sum())
|
||||
dataframe.loc[mask, f'_{sig_type}_signal_close'] = dataframe.loc[mask, 'close']
|
||||
|
||||
# band-aid until this is fixed:
|
||||
# https://github.com/pandas-dev/pandas/issues/45836
|
||||
@@ -978,8 +1014,12 @@ class RPC:
|
||||
'columns': list(dataframe.columns),
|
||||
'data': dataframe.values.tolist(),
|
||||
'length': len(dataframe),
|
||||
'buy_signals': buy_signals,
|
||||
'sell_signals': sell_signals,
|
||||
'buy_signals': signals['enter_long'], # Deprecated
|
||||
'sell_signals': signals['exit_long'], # Deprecated
|
||||
'enter_long_signals': signals['enter_long'],
|
||||
'exit_long_signals': signals['exit_long'],
|
||||
'enter_short_signals': signals['enter_short'],
|
||||
'exit_short_signals': signals['exit_short'],
|
||||
'last_analyzed': last_analyzed,
|
||||
'last_analyzed_ts': int(last_analyzed.timestamp()),
|
||||
'data_start': '',
|
||||
@@ -1018,6 +1058,7 @@ class RPC:
|
||||
timeframe=timeframe,
|
||||
timerange=timerange_parsed,
|
||||
data_format=config.get('dataformat_ohlcv', 'json'),
|
||||
candle_type=config.get('candle_type_def', CandleType.SPOT)
|
||||
)
|
||||
if pair not in _data:
|
||||
raise RPCException(f"No data for {pair}, {timeframe} in {timerange} found.")
|
||||
|
@@ -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
|
||||
@@ -22,7 +23,7 @@ from telegram.utils.helpers import escape_markdown
|
||||
|
||||
from freqtrade.__init__ import __version__
|
||||
from freqtrade.constants import DUST_PER_COIN
|
||||
from freqtrade.enums import RPCMessageType
|
||||
from freqtrade.enums import RPCMessageType, SignalDirection, TradingMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.misc import chunks, plural, round_coin_value
|
||||
from freqtrade.persistence import Trade
|
||||
@@ -115,6 +116,7 @@ class Telegram(RPCHandler):
|
||||
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
|
||||
r'/forcebuy$', r'/forcesell$', r'/edge$', r'/health$', r'/help$',
|
||||
r'/version$']
|
||||
|
||||
# Create keys for generation
|
||||
valid_keys_print = [k.replace('$', '') for k in valid_keys]
|
||||
|
||||
@@ -151,12 +153,15 @@ 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),
|
||||
CommandHandler('buys', self._buy_tag_performance),
|
||||
CommandHandler(['buys', 'entries'], self._enter_tag_performance),
|
||||
CommandHandler('sells', self._sell_reason_performance),
|
||||
CommandHandler('mix_tags', self._mix_tag_performance),
|
||||
CommandHandler('stats', self._stats),
|
||||
@@ -186,13 +191,15 @@ class Telegram(RPCHandler):
|
||||
CallbackQueryHandler(self._profit, pattern='update_profit'),
|
||||
CallbackQueryHandler(self._balance, pattern='update_balance'),
|
||||
CallbackQueryHandler(self._performance, pattern='update_performance'),
|
||||
CallbackQueryHandler(self._buy_tag_performance, pattern='update_buy_tag_performance'),
|
||||
CallbackQueryHandler(self._enter_tag_performance,
|
||||
pattern='update_enter_tag_performance'),
|
||||
CallbackQueryHandler(self._sell_reason_performance,
|
||||
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, pattern=r"\S+\/\S+"),
|
||||
CallbackQueryHandler(self._forcesell_inline, pattern=r"[0-9]+\s\S+\/\S+")
|
||||
CallbackQueryHandler(self._forceenter_inline),
|
||||
]
|
||||
for handle in handles:
|
||||
self._updater.dispatcher.add_handler(handle)
|
||||
@@ -225,20 +232,25 @@ class Telegram(RPCHandler):
|
||||
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||
else:
|
||||
msg['stake_amount_fiat'] = 0
|
||||
is_fill = msg['type'] == RPCMessageType.BUY_FILL
|
||||
is_fill = msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]
|
||||
emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
|
||||
|
||||
enter_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['type']
|
||||
in [RPCMessageType.BUY_FILL, RPCMessageType.BUY]
|
||||
else {'enter': 'Short', 'entered': 'Shorted'})
|
||||
message = (
|
||||
f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}"
|
||||
f"{emoji} *{msg['exchange']}:*"
|
||||
f" {enter_side['entered'] if is_fill else enter_side['enter']} {msg['pair']}"
|
||||
f" (#{msg['trade_id']})\n"
|
||||
)
|
||||
message += f"*Buy Tag:* `{msg['buy_tag']}`\n" if msg.get('buy_tag', None) else ""
|
||||
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
|
||||
message += f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
|
||||
message += f"*Leverage:* `{msg['leverage']}`\n"
|
||||
|
||||
if msg['type'] == RPCMessageType.BUY_FILL:
|
||||
if msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]:
|
||||
message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
|
||||
|
||||
elif msg['type'] == RPCMessageType.BUY:
|
||||
elif msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]:
|
||||
message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\
|
||||
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
||||
|
||||
@@ -257,8 +269,11 @@ class Telegram(RPCHandler):
|
||||
microsecond=0) - msg['open_date'].replace(microsecond=0)
|
||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
||||
|
||||
msg['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None
|
||||
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
|
||||
msg['emoji'] = self._get_sell_emoji(msg)
|
||||
msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n"
|
||||
if msg.get('leverage', None) and msg.get('leverage', 1.0) != 1.0
|
||||
else "")
|
||||
|
||||
# Check if all sell properties are available.
|
||||
# This might not be the case if the message origin is triggered by /forcesell
|
||||
@@ -274,15 +289,17 @@ class Telegram(RPCHandler):
|
||||
is_fill = msg['type'] == RPCMessageType.SELL_FILL
|
||||
message = (
|
||||
f"{msg['emoji']} *{msg['exchange']}:* "
|
||||
f"{'Sold' if is_fill else 'Selling'} {msg['pair']} (#{msg['trade_id']})\n"
|
||||
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
|
||||
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
|
||||
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
|
||||
f"*Buy Tag:* `{msg['buy_tag']}`\n"
|
||||
f"*Sell Reason:* `{msg['sell_reason']}`\n"
|
||||
f"*Enter Tag:* `{msg['enter_tag']}`\n"
|
||||
f"*Exit Reason:* `{msg['sell_reason']}`\n"
|
||||
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
|
||||
f"*Direction:* `{msg['direction']}`\n"
|
||||
f"{msg['leverage_text']}"
|
||||
f"*Amount:* `{msg['amount']:.8f}`\n"
|
||||
f"*Open Rate:* `{msg['open_rate']:.8f}`\n")
|
||||
|
||||
f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
|
||||
)
|
||||
if msg['type'] == RPCMessageType.SELL:
|
||||
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
|
||||
f"*Close Rate:* `{msg['limit']:.8f}`")
|
||||
@@ -293,16 +310,19 @@ class Telegram(RPCHandler):
|
||||
return message
|
||||
|
||||
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
|
||||
if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL]:
|
||||
if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL, RPCMessageType.SHORT,
|
||||
RPCMessageType.SHORT_FILL]:
|
||||
message = self._format_buy_msg(msg)
|
||||
|
||||
elif msg_type in [RPCMessageType.SELL, RPCMessageType.SELL_FILL]:
|
||||
message = self._format_sell_msg(msg)
|
||||
|
||||
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
|
||||
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
|
||||
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL,
|
||||
RPCMessageType.SELL_CANCEL):
|
||||
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.BUY_CANCEL,
|
||||
RPCMessageType.SHORT_CANCEL] else 'exit'
|
||||
message = ("\N{WARNING SIGN} *{exchange}:* "
|
||||
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
|
||||
"Cancelling {message_side} Order for {pair} (#{trade_id}). "
|
||||
"Reason: {reason}.".format(**msg))
|
||||
|
||||
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
|
||||
@@ -381,7 +401,7 @@ class Telegram(RPCHandler):
|
||||
first_avg = filled_orders[0]["safe_price"]
|
||||
|
||||
for x, order in enumerate(filled_orders):
|
||||
if order['ft_order_side'] != 'buy':
|
||||
if not order['ft_is_entry']:
|
||||
continue
|
||||
cur_entry_datetime = arrow.get(order["order_filled_date"])
|
||||
cur_entry_amount = order["amount"]
|
||||
@@ -448,14 +468,16 @@ class Telegram(RPCHandler):
|
||||
messages = []
|
||||
for r in results:
|
||||
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
|
||||
r['num_entries'] = len([o for o in r['orders'] if o['ft_order_side'] == 'buy'])
|
||||
r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']])
|
||||
r['sell_reason'] = r.get('sell_reason', "")
|
||||
lines = [
|
||||
"*Trade ID:* `{trade_id}`" +
|
||||
("` (since {open_date_hum})`" if r['is_open'] else ""),
|
||||
"*Current Pair:* {pair}",
|
||||
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
|
||||
"*Leverage:* `{leverage}`" if r.get('leverage') else "",
|
||||
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
|
||||
"*Entry Tag:* `{buy_tag}`" if r['buy_tag'] else "",
|
||||
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
|
||||
"*Exit Reason:* `{sell_reason}`" if r['sell_reason'] else "",
|
||||
]
|
||||
|
||||
@@ -812,13 +834,21 @@ class Telegram(RPCHandler):
|
||||
for curr in result['currencies']:
|
||||
curr_output = ''
|
||||
if curr['est_stake'] > balance_dust_level:
|
||||
curr_output = (
|
||||
f"*{curr['currency']}:*\n"
|
||||
f"\t`Available: {curr['free']:.8f}`\n"
|
||||
f"\t`Balance: {curr['balance']:.8f}`\n"
|
||||
f"\t`Pending: {curr['used']:.8f}`\n"
|
||||
f"\t`Est. {curr['stake']}: "
|
||||
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
|
||||
if curr['is_position']:
|
||||
curr_output = (
|
||||
f"*{curr['currency']}:*\n"
|
||||
f"\t`{curr['side']}: {curr['position']:.8f}`\n"
|
||||
f"\t`Leverage: {curr['leverage']:.1f}`\n"
|
||||
f"\t`Est. {curr['stake']}: "
|
||||
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
|
||||
else:
|
||||
curr_output = (
|
||||
f"*{curr['currency']}:*\n"
|
||||
f"\t`Available: {curr['free']:.8f}`\n"
|
||||
f"\t`Balance: {curr['balance']:.8f}`\n"
|
||||
f"\t`Pending: {curr['used']:.8f}`\n"
|
||||
f"\t`Est. {curr['stake']}: "
|
||||
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
|
||||
elif curr['est_stake'] <= balance_dust_level:
|
||||
total_dust_balance += curr['est_stake']
|
||||
total_dust_currencies += 1
|
||||
@@ -902,7 +932,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
|
||||
@@ -945,20 +975,22 @@ class Telegram(RPCHandler):
|
||||
query.edit_message_text(text=f"Force Selling: {query.data}")
|
||||
self._forcesell_action(trade_id)
|
||||
|
||||
def _forcebuy_action(self, pair, price=None):
|
||||
def _forceenter_action(self, pair, price: Optional[float], order_side: SignalDirection):
|
||||
if pair != 'cancel':
|
||||
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],
|
||||
@@ -971,9 +1003,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
|
||||
@@ -983,15 +1016,19 @@ 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']
|
||||
pair_buttons = [
|
||||
InlineKeyboardButton(text=pair, callback_data=pair) for pair in sorted(whitelist)]
|
||||
InlineKeyboardButton(text=pair, callback_data=f"{pair}_||_{order_side}")
|
||||
for pair in sorted(whitelist)
|
||||
]
|
||||
buttons_aligned = self._layout_inline_keyboard(pair_buttons)
|
||||
|
||||
buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')])
|
||||
self._send_msg(msg="Which pair?", keyboard=buttons_aligned)
|
||||
self._send_msg(msg="Which pair?",
|
||||
keyboard=buttons_aligned,
|
||||
query=update.callback_query)
|
||||
|
||||
@authorized_only
|
||||
def _trades(self, update: Update, context: CallbackContext) -> None:
|
||||
@@ -1082,7 +1119,7 @@ class Telegram(RPCHandler):
|
||||
self._send_msg(str(e))
|
||||
|
||||
@authorized_only
|
||||
def _buy_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||
def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
|
||||
"""
|
||||
Handler for /buys PAIR .
|
||||
Shows a performance statistic from finished trades
|
||||
@@ -1095,11 +1132,11 @@ class Telegram(RPCHandler):
|
||||
if context.args and isinstance(context.args[0], str):
|
||||
pair = context.args[0]
|
||||
|
||||
trades = self._rpc._rpc_buy_tag_performance(pair)
|
||||
trades = self._rpc._rpc_enter_tag_performance(pair)
|
||||
output = "<b>Buy Tag Performance:</b>\n"
|
||||
for i, trade in enumerate(trades):
|
||||
stat_line = (
|
||||
f"{i+1}.\t <code>{trade['buy_tag']}\t"
|
||||
f"{i+1}.\t <code>{trade['enter_tag']}\t"
|
||||
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
|
||||
f"({trade['profit_ratio']:.2%}) "
|
||||
f"({trade['count']})</code>\n")
|
||||
@@ -1111,7 +1148,7 @@ class Telegram(RPCHandler):
|
||||
output += stat_line
|
||||
|
||||
self._send_msg(output, parse_mode=ParseMode.HTML,
|
||||
reload_able=True, callback_path="update_buy_tag_performance",
|
||||
reload_able=True, callback_path="update_enter_tag_performance",
|
||||
query=update.callback_query)
|
||||
except RPCException as e:
|
||||
self._send_msg(str(e))
|
||||
@@ -1357,18 +1394,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 "
|
||||
@@ -1396,7 +1438,7 @@ class Telegram(RPCHandler):
|
||||
" *table :* `will display trades in a table`\n"
|
||||
" `pending buy orders are marked with an asterisk (*)`\n"
|
||||
" `pending sell orders are marked with a double asterisk (**)`\n"
|
||||
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
|
||||
"*/buys <pair|none>:* `Shows the enter_tag performance`\n"
|
||||
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
|
||||
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
|
||||
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
|
||||
@@ -1476,11 +1518,12 @@ 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"
|
||||
f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n"
|
||||
f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n"
|
||||
f"*Entry strategy:* ```\n{json.dumps(val['entry_pricing'])}```\n"
|
||||
f"*Exit strategy:* ```\n{json.dumps(val['exit_pricing'])}```\n"
|
||||
f"{sl_info}"
|
||||
f"{pa_info}"
|
||||
f"*Timeframe:* `{val['timeframe']}`\n"
|
||||
|
@@ -44,11 +44,11 @@ class Webhook(RPCHandler):
|
||||
""" Send a message to telegram channel """
|
||||
try:
|
||||
|
||||
if msg['type'] == RPCMessageType.BUY:
|
||||
if msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]:
|
||||
valuedict = self._config['webhook'].get('webhookbuy', None)
|
||||
elif msg['type'] == RPCMessageType.BUY_CANCEL:
|
||||
elif msg['type'] in [RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL]:
|
||||
valuedict = self._config['webhook'].get('webhookbuycancel', None)
|
||||
elif msg['type'] == RPCMessageType.BUY_FILL:
|
||||
elif msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]:
|
||||
valuedict = self._config['webhook'].get('webhookbuyfill', None)
|
||||
elif msg['type'] == RPCMessageType.SELL:
|
||||
valuedict = self._config['webhook'].get('webhooksell', None)
|
||||
|
Reference in New Issue
Block a user