Merge branch 'develop' into pr/samgermain/6558

This commit is contained in:
Matthias
2022-04-23 09:16:28 +02:00
196 changed files with 34421 additions and 5193 deletions

View File

@@ -1,13 +1,16 @@
import asyncio
import logging
from copy import deepcopy
from typing import Any, Dict, List
from fastapi import APIRouter, BackgroundTasks, Depends
from freqtrade.configuration.config_validation import validate_config_consistency
from freqtrade.data.btanalysis import get_backtest_resultlist, load_and_merge_backtest_result
from freqtrade.enums import BacktestState
from freqtrade.exceptions import DependencyException
from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse
from freqtrade.rpc.api_server.api_schemas import (BacktestHistoryEntry, BacktestRequest,
BacktestResponse)
from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode
from freqtrade.rpc.api_server.webserver import ApiServer
from freqtrade.rpc.rpc import RPCException
@@ -200,3 +203,30 @@ def api_backtest_abort(ws_mode=Depends(is_webserver_mode)):
"progress": 0,
"status_msg": "Backtest ended",
}
@router.get('/backtest/history', response_model=List[BacktestHistoryEntry], tags=['webserver', 'backtest'])
def api_backtest_history(config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
# Get backtest result history, read from metadata files
return get_backtest_resultlist(config['user_data_dir'] / 'backtest_results')
@router.get('/backtest/history/result', response_model=BacktestResponse, tags=['webserver', 'backtest'])
def api_backtest_history_result(filename: str, strategy: str, config=Depends(get_config), ws_mode=Depends(is_webserver_mode)):
# Get backtest result history, read from metadata files
fn = config['user_data_dir'] / 'backtest_results' / filename
results: Dict[str, Any] = {
'metadata': {},
'strategy': {},
'strategy_comparison': [],
}
load_and_merge_backtest_result(strategy, fn, results)
return {
"status": "ended",
"running": False,
"step": "",
"progress": 1,
"status_msg": "Historic result",
"backtest_result": results,
}

View File

@@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.enums import OrderTypeValues
from freqtrade.enums import OrderTypeValues, SignalDirection, 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):
@@ -108,7 +113,7 @@ class SellReason(BaseModel):
class Stats(BaseModel):
sell_reasons: Dict[str, SellReason]
exit_reasons: Dict[str, SellReason]
durations: Dict[str, Optional[float]]
@@ -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
emergency_exit: Optional[OrderTypeValues]
force_exit: Optional[OrderTypeValues]
force_entry: 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]
@@ -167,9 +174,9 @@ class ShowConfig(BaseModel):
timeframe_min: int
exchange: str
strategy: Optional[str]
forcebuy_enabled: bool
ask_strategy: Dict[str, Any]
bid_strategy: Dict[str, Any]
force_entry_enable: bool
exit_pricing: Dict[str, Any]
entry_pricing: Dict[str, Any]
bot_name: str
state: str
runmode: str
@@ -196,13 +203,17 @@ class OrderSchema(BaseModel):
class TradeSchema(BaseModel):
trade_id: int
pair: str
base_currency: str
quote_currency: 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]
@@ -226,8 +237,9 @@ class TradeSchema(BaseModel):
profit_pct: Optional[float]
profit_abs: Optional[float]
profit_fiat: Optional[float]
sell_reason: Optional[str]
sell_order_status: Optional[str]
sell_reason: Optional[str] # Deprecated
exit_reason: Optional[str]
exit_order_status: Optional[str]
stop_loss_abs: Optional[float]
stop_loss_ratio: Optional[float]
stop_loss_pct: Optional[float]
@@ -242,6 +254,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 +279,7 @@ class TradeResponse(BaseModel):
total_trades: int
class ForceBuyResponse(BaseModel):
class ForceEnterResponse(BaseModel):
__root__: Union[TradeSchema, StatusMsg]
@@ -292,15 +309,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 +382,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
@@ -399,6 +421,13 @@ class BacktestResponse(BaseModel):
backtest_result: Optional[Dict[str, Any]]
class BacktestHistoryEntry(BaseModel):
filename: str
strategy: str
run_id: str
backtest_start_time: int
class SysInfo(BaseModel):
cpu_pct: List[float]
ram_pct: float

View File

@@ -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,10 @@ 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
# 2.15: Add backtest history endpoints
API_VERSION = 2.15
# Public API, requires no auth.
router_public = APIRouter()
@@ -133,24 +136,31 @@ 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 force_entry(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'
entry_tag = payload.entry_tag if payload.entry_tag else 'force_entry'
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}."})
# /forcesell is deprecated with short addition. use /forceexit instead
@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 forceexit(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_force_exit(payload.tradeid, ordertype)
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
@@ -269,16 +279,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})

View File

@@ -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
@@ -86,7 +86,7 @@ class CryptoToFiatConverter:
return None
else:
return None
found = [x for x in self._coinlistings if x['symbol'] == crypto_symbol]
found = [x for x in self._coinlistings if x['symbol'].lower() == crypto_symbol]
if crypto_symbol in coingecko_mapping.keys():
found = [x for x in self._coinlistings if x['id'] == coingecko_mapping[crypto_symbol]]
@@ -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

View File

@@ -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']),
@@ -133,9 +136,9 @@ class RPC:
) if 'timeframe' in config else 0,
'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', {}),
'force_entry_enable': config.get('force_entry_enable', False),
'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:
@@ -194,7 +197,6 @@ class RPC:
trade_dict = trade.to_json()
trade_dict.update(dict(
base_currency=self._freqtrade.config['stake_currency'],
close_profit=trade.close_profit if trade.close_profit is not None else None,
current_rate=current_rate,
current_profit=current_profit, # Deprecated
@@ -219,7 +221,8 @@ 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()
nonspot = self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT
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 nonspot else ''
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,23 @@ 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' if nonspot else 'ID',
'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(
@@ -425,13 +432,13 @@ class RPC:
return 'losses'
else:
return 'draws'
trades = trades = Trade.get_trades([Trade.is_open.is_(False)])
trades: List[Trade] = Trade.get_trades([Trade.is_open.is_(False)])
# Sell reason
sell_reasons = {}
exit_reasons = {}
for trade in trades:
if trade.sell_reason not in sell_reasons:
sell_reasons[trade.sell_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1
if trade.exit_reason not in exit_reasons:
exit_reasons[trade.exit_reason] = {'wins': 0, 'losses': 0, 'draws': 0}
exit_reasons[trade.exit_reason][trade_win_loss(trade)] += 1
# Duration
dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []}
@@ -445,7 +452,7 @@ class RPC:
losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else None
durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur}
return {'sell_reasons': sell_reasons, 'durations': durations}
return {'exit_reasons': exit_reasons, 'durations': durations}
def _rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str,
@@ -453,7 +460,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 +490,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 +566,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 +577,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 +587,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 +602,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 +642,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,35 +688,35 @@ 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_force_exit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
"""
Handler for forcesell <id>.
Handler for forceexit <id>.
Sells the given trade at current price
"""
def _exec_forcesell(trade: Trade) -> None:
def _exec_force_exit(trade: Trade) -> None:
# Check if there is there is an open order
fully_canceled = False
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.entry_side:
fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_SELL'])
trade, order, CANCEL_REASON['FORCE_EXIT'])
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'])
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_EXIT'])
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_EXIT)
order_type = ordertype or self._freqtrade.strategy.order_types.get(
"forcesell", self._freqtrade.strategy.order_types["sell"])
"force_exit", 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:
@@ -693,7 +726,7 @@ class RPC:
if trade_id == 'all':
# Execute sell for all open orders
for trade in Trade.get_open_trades():
_exec_forcesell(trade)
_exec_force_exit(trade)
Trade.commit()
self._freqtrade.wallets.update()
return {'result': 'Created sell orders for all open trades.'}
@@ -703,28 +736,33 @@ 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('force_exit: Invalid argument received')
raise RPCException('invalid argument')
_exec_forcesell(trade)
_exec_force_exit(trade)
Trade.commit()
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] = 'force_entry') -> 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.')
if not self._freqtrade.config.get('force_entry_enable', False):
raise RPCException('Force_entry 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 +771,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,14 +785,17 @@ class RPC:
# execute buy
if not order_type:
order_type = self._freqtrade.strategy.order_types.get(
'forcebuy', self._freqtrade.strategy.order_types['buy'])
'force_entry', 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
else:
return None
raise RPCException(f'Failed to enter position for {pair}.')
def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]:
"""
@@ -802,27 +845,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 Trade.get_enter_tag_performance(pair)
return buy_tags
def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
def _rpc_exit_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
"""
Handler for sell reason performance.
Handler for exit reason performance.
Shows a performance statistic from finished trades
"""
sell_reasons = Trade.get_sell_reason_performance(pair)
return sell_reasons
return Trade.get_exit_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 + exit_reason) performance.
Shows a performance statistic from finished trades
"""
mix_tags = Trade.get_mix_tag_performance(pair)
@@ -945,20 +984,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 +1018,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 +1062,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.")

View File

@@ -7,6 +7,7 @@ import json
import logging
import re
from datetime import date, datetime, timedelta
from functools import partial
from html import escape
from itertools import chain
from math import isnan
@@ -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
@@ -102,18 +103,20 @@ class Telegram(RPCHandler):
['/count', '/start', '/stop', '/help']
]
# do not allow commands with mandatory arguments and critical cmds
# like /forcesell and /forcebuy
# TODO: DRY! - its not good to list all valid cmds here. But otherwise
# this needs refactoring of the whole telegram module (same
# problem in _help()).
valid_keys: List[str] = [r'/start$', r'/stop$', r'/status$', r'/status table$',
r'/trades$', r'/performance$', r'/buys', r'/sells', r'/mix_tags',
r'/trades$', r'/performance$', r'/buys', r'/entries',
r'/sells', r'/exits', r'/mix_tags',
r'/daily$', r'/daily \d+$', r'/profit$', r'/profit \d+',
r'/stats$', r'/count$', r'/locks$', r'/balance$',
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'/health$', r'/help$', r'/version$']
r'/forcebuy$', r'/forcelong$', r'/forceshort$',
r'/forcesell$', r'/forceexit$',
r'/edge$', r'/health$', r'/help$', r'/version$']
# Create keys for generation
valid_keys_print = [k.replace('$', '') for k in valid_keys]
@@ -150,13 +153,16 @@ 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', 'fx'], self._force_exit),
CommandHandler(['forcebuy', 'forcelong'], partial(
self._force_enter, order_side=SignalDirection.LONG)),
CommandHandler('forceshort', partial(
self._force_enter, order_side=SignalDirection.SHORT)),
CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade),
CommandHandler('performance', self._performance),
CommandHandler('buys', self._buy_tag_performance),
CommandHandler('sells', self._sell_reason_performance),
CommandHandler(['buys', 'entries'], self._enter_tag_performance),
CommandHandler(['sells', 'exits'], self._exit_reason_performance),
CommandHandler('mix_tags', self._mix_tag_performance),
CommandHandler('stats', self._stats),
CommandHandler('daily', self._daily),
@@ -185,12 +191,14 @@ 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._sell_reason_performance,
pattern='update_sell_reason_performance'),
CallbackQueryHandler(self._enter_tag_performance,
pattern='update_enter_tag_performance'),
CallbackQueryHandler(self._exit_reason_performance,
pattern='update_exit_reason_performance'),
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
CallbackQueryHandler(self._count, pattern='update_count'),
CallbackQueryHandler(self._forcebuy_inline),
CallbackQueryHandler(self._force_exit_inline, pattern=r"force_exit__\S+"),
CallbackQueryHandler(self._force_enter_inline, pattern=r"\S+\/\S+"),
]
for handle in handles:
self._updater.dispatcher.add_handler(handle)
@@ -217,26 +225,30 @@ class Telegram(RPCHandler):
# This can take up to `timeout` from the call to `start_polling`.
self._updater.stop()
def _format_buy_msg(self, msg: Dict[str, Any]) -> str:
def _format_entry_msg(self, msg: Dict[str, Any]) -> str:
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
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.ENTRY_FILL]
emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
entry_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['direction'] == 'Long'
else {'enter': 'Short', 'entered': 'Shorted'})
message = (
f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}"
f"{emoji} *{msg['exchange']}:*"
f" {entry_side['entered'] if is_fill else entry_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.ENTRY_FILL]:
message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
elif msg['type'] == RPCMessageType.BUY:
elif msg['type'] in [RPCMessageType.ENTRY]:
message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
@@ -248,18 +260,21 @@ class Telegram(RPCHandler):
message += ")`"
return message
def _format_sell_msg(self, msg: Dict[str, Any]) -> str:
def _format_exit_msg(self, msg: Dict[str, Any]) -> str:
msg['amount'] = round(msg['amount'], 8)
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
msg['duration'] = msg['close_date'].replace(
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
# This might not be the case if the message origin is triggered by /forceexit
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
and self._rpc._fiat_converter):
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
@@ -269,38 +284,40 @@ class Telegram(RPCHandler):
f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']})")
else:
msg['profit_extra'] = ''
is_fill = msg['type'] == RPCMessageType.SELL_FILL
is_fill = msg['type'] == RPCMessageType.EXIT_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['exit_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")
if msg['type'] == RPCMessageType.SELL:
f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
)
if msg['type'] == RPCMessageType.EXIT:
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Close Rate:* `{msg['limit']:.8f}`")
elif msg['type'] == RPCMessageType.SELL_FILL:
elif msg['type'] == RPCMessageType.EXIT_FILL:
message += f"*Close Rate:* `{msg['close_rate']:.8f}`"
return message
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL]:
message = self._format_buy_msg(msg)
if msg_type in [RPCMessageType.ENTRY, RPCMessageType.ENTRY_FILL]:
message = self._format_entry_msg(msg)
elif msg_type in [RPCMessageType.SELL, RPCMessageType.SELL_FILL]:
message = self._format_sell_msg(msg)
elif msg_type in [RPCMessageType.EXIT, RPCMessageType.EXIT_FILL]:
message = self._format_exit_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.ENTRY_CANCEL, RPCMessageType.EXIT_CANCEL):
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.ENTRY_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:
@@ -335,14 +352,14 @@ class Telegram(RPCHandler):
msg_type = msg['type']
noti = ''
if msg_type == RPCMessageType.SELL:
if msg_type == RPCMessageType.EXIT:
sell_noti = self._config['telegram'] \
.get('notification_settings', {}).get(str(msg_type), {})
# For backward compatibility sell still can be string
if isinstance(sell_noti, str):
noti = sell_noti
else:
noti = sell_noti.get(str(msg['sell_reason']), default_noti)
noti = sell_noti.get(str(msg['exit_reason']), default_noti)
else:
noti = self._config['telegram'] \
.get('notification_settings', {}).get(str(msg_type), default_noti)
@@ -365,12 +382,12 @@ class Telegram(RPCHandler):
return "\N{ROCKET}"
elif float(msg['profit_percent']) >= 0.0:
return "\N{EIGHT SPOKED ASTERISK}"
elif msg['sell_reason'] == "stop_loss":
elif msg['exit_reason'] == "stop_loss":
return "\N{WARNING SIGN}"
else:
return "\N{CROSS MARK}"
def _prepare_entry_details(self, filled_orders: List, base_currency: str, is_open: bool):
def _prepare_entry_details(self, filled_orders: List, quote_currency: str, is_open: bool):
"""
Prepare details of trade with entry adjustment enabled
"""
@@ -379,7 +396,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"]
@@ -388,7 +405,7 @@ class Telegram(RPCHandler):
if x == 0:
lines.append(f"*Entry #{x+1}:*")
lines.append(
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average Entry Price:* {cur_entry_average}")
else:
sumA = 0
@@ -402,7 +419,8 @@ class Telegram(RPCHandler):
if prev_avg_price:
minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price
dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"])
dur_entry = cur_entry_datetime - arrow.get(
filled_orders[x - 1]["order_filled_date"])
days = dur_entry.days
hours, remainder = divmod(dur_entry.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
@@ -411,7 +429,7 @@ class Telegram(RPCHandler):
lines.append("({})".format(cur_entry_datetime
.humanize(granularity=["day", "hour", "minute"])))
lines.append(
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})")
f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})")
lines.append(f"*Average Entry Price:* {cur_entry_average} "
f"({price_to_1st_entry:.2%} from 1st entry rate)")
lines.append(f"*Order filled at:* {order['order_filled_date']}")
@@ -446,15 +464,17 @@ 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['sell_reason'] = r.get('sell_reason', "")
r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']])
r['exit_reason'] = r.get('exit_reason', "")
lines = [
"*Trade ID:* `{trade_id}`" +
("` (since {open_date_hum})`" if r['is_open'] else ""),
"*Current Pair:* {pair}",
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
"*Entry Tag:* `{buy_tag}`" if r['buy_tag'] else "",
"*Exit Reason:* `{sell_reason}`" if r['sell_reason'] else "",
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
"*Leverage:* `{leverage}`" if r.get('leverage') else "",
"*Amount:* `{amount} ({stake_amount} {quote_currency})`",
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
"*Exit Reason:* `{exit_reason}`" if r['exit_reason'] else "",
]
if position_adjust:
@@ -484,13 +504,13 @@ class Telegram(RPCHandler):
lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` "
"`({stoploss_current_dist_ratio:.2%})`")
if r['open_order']:
if r['sell_order_status']:
lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`")
if r['exit_order_status']:
lines.append("*Open Order:* `{open_order}` - `{exit_order_status}`")
else:
lines.append("*Open Order:* `{open_order}`")
lines_detail = self._prepare_entry_details(
r['orders'], r['base_currency'], r['is_open'])
r['orders'], r['quote_currency'], r['is_open'])
lines.extend(lines_detail if lines_detail else "")
# Filter empty lines using list-comprehension
@@ -746,27 +766,27 @@ class Telegram(RPCHandler):
'stop_loss': 'Stoploss',
'trailing_stop_loss': 'Trail. Stop',
'stoploss_on_exchange': 'Stoploss',
'sell_signal': 'Sell Signal',
'force_sell': 'Forcesell',
'emergency_sell': 'Emergency Sell',
'exit_signal': 'Exit Signal',
'force_exit': 'Force Exit',
'emergency_exit': 'Emergency Exit',
}
sell_reasons_tabulate = [
exit_reasons_tabulate = [
[
reason_map.get(reason, reason),
sum(count.values()),
count['wins'],
count['losses']
] for reason, count in stats['sell_reasons'].items()
] for reason, count in stats['exit_reasons'].items()
]
sell_reasons_msg = 'No trades yet.'
for reason in chunks(sell_reasons_tabulate, 25):
sell_reasons_msg = tabulate(
exit_reasons_msg = 'No trades yet.'
for reason in chunks(exit_reasons_tabulate, 25):
exit_reasons_msg = tabulate(
reason,
headers=['Sell Reason', 'Sells', 'Wins', 'Losses']
headers=['Exit Reason', 'Exits', 'Wins', 'Losses']
)
if len(sell_reasons_tabulate) > 25:
self._send_msg(sell_reasons_msg, ParseMode.MARKDOWN)
sell_reasons_msg = ''
if len(exit_reasons_tabulate) > 25:
self._send_msg(exit_reasons_msg, ParseMode.MARKDOWN)
exit_reasons_msg = ''
durations = stats['durations']
duration_msg = tabulate(
@@ -778,7 +798,7 @@ class Telegram(RPCHandler):
],
headers=['', 'Avg. Duration']
)
msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""")
msg = (f"""```\n{exit_reasons_msg}```\n```\n{duration_msg}```""")
self._send_msg(msg, ParseMode.MARKDOWN)
@@ -810,13 +830,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
@@ -900,50 +928,93 @@ class Telegram(RPCHandler):
self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only
def _forcesell(self, update: Update, context: CallbackContext) -> None:
def _force_exit(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /forcesell <id>.
Handler for /forceexit <id>.
Sells the given trade at current price
:param bot: telegram bot
:param update: message update
:return: None
"""
trade_id = context.args[0] if context.args and len(context.args) > 0 else None
if not trade_id:
self._send_msg("You must specify a trade-id or 'all'.")
return
try:
msg = self._rpc._rpc_forcesell(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):
if pair != 'cancel':
if context.args:
trade_id = context.args[0]
self._force_exit_action(trade_id)
else:
fiat_currency = self._config.get('fiat_display_currency', '')
try:
self._rpc._rpc_forcebuy(pair, price)
statlist, head, fiat_profit_sum = self._rpc._rpc_status_table(
self._config['stake_currency'], fiat_currency)
except RPCException:
self._send_msg(msg='No open trade found.')
return
trades = []
for trade in statlist:
trades.append((trade[0], f"{trade[0]} {trade[1]} {trade[2]} {trade[3]}"))
trade_buttons = [
InlineKeyboardButton(text=trade[1], callback_data=f"force_exit__{trade[0]}")
for trade in trades]
buttons_aligned = self._layout_inline_keyboard_onecol(trade_buttons)
buttons_aligned.append([InlineKeyboardButton(
text='Cancel', callback_data='force_exit__cancel')])
self._send_msg(msg="Which trade?", keyboard=buttons_aligned)
def _force_exit_action(self, trade_id):
if trade_id != 'cancel':
try:
self._rpc._rpc_force_exit(trade_id)
except RPCException as e:
self._send_msg(str(e))
def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None:
def _force_exit_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:
# Input data is "force_exit__<tradid|cancel>"
trade_id = query.data.split("__")[1].split(' ')[0]
if trade_id == 'cancel':
query.answer()
query.edit_message_text(text="Force exit canceled.")
return
trade: Trade = Trade.get_trades(trade_filter=Trade.id == trade_id).first()
query.answer()
query.edit_message_text(text=f"Manually exiting Trade #{trade_id}, {trade.pair}")
self._force_exit_action(trade_id)
def _force_enter_action(self, pair, price: Optional[float], order_side: SignalDirection):
if pair != 'cancel':
try:
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
except RPCException as e:
self._send_msg(str(e))
def _force_enter_inline(self, update: Update, _: CallbackContext) -> None:
if update.callback_query:
query = update.callback_query
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._force_enter_action(pair, None, order_side)
@staticmethod
def _layout_inline_keyboard(buttons: List[InlineKeyboardButton],
cols=3) -> List[List[InlineKeyboardButton]]:
def _layout_inline_keyboard(
buttons: List[InlineKeyboardButton], cols=3) -> List[List[InlineKeyboardButton]]:
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
@staticmethod
def _layout_inline_keyboard_onecol(
buttons: List[InlineKeyboardButton], cols=1) -> List[List[InlineKeyboardButton]]:
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
@authorized_only
def _forcebuy(self, update: Update, context: CallbackContext) -> None:
def _force_enter(
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
@@ -952,16 +1023,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._force_enter_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)
keyboard=buttons_aligned,
query=update.callback_query)
@authorized_only
def _trades(self, update: Update, context: CallbackContext) -> None:
@@ -1052,7 +1126,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
@@ -1065,11 +1139,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)
output = "<b>Buy Tag Performance:</b>\n"
trades = self._rpc._rpc_enter_tag_performance(pair)
output = "<b>Entry 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")
@@ -1081,13 +1155,13 @@ 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))
@authorized_only
def _sell_reason_performance(self, update: Update, context: CallbackContext) -> None:
def _exit_reason_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /sells.
Shows a performance statistic from finished trades
@@ -1100,11 +1174,11 @@ class Telegram(RPCHandler):
if context.args and isinstance(context.args[0], str):
pair = context.args[0]
trades = self._rpc._rpc_sell_reason_performance(pair)
output = "<b>Sell Reason Performance:</b>\n"
trades = self._rpc._rpc_exit_reason_performance(pair)
output = "<b>Exit Reason Performance:</b>\n"
for i, trade in enumerate(trades):
stat_line = (
f"{i+1}.\t <code>{trade['sell_reason']}\t"
f"{i+1}.\t <code>{trade['exit_reason']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n")
@@ -1116,7 +1190,7 @@ class Telegram(RPCHandler):
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_sell_reason_performance",
reload_able=True, callback_path="update_exit_reason_performance",
query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@@ -1327,18 +1401,24 @@ 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")
force_enter_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:
force_enter_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 ''}"
"*/fe <trade_id>|all:* `Alias to /forceexit`"
f"{force_enter_text if self._config.get('force_entry_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 "
@@ -1366,9 +1446,9 @@ 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"
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
"*/buys <pair|none>:* `Shows the enter_tag performance`\n"
"*/sells <pair|none>:* `Shows the exit reason performance`\n"
"*/mix_tags <pair|none>:* `Shows combined entry tag + exit reason performance`\n"
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
"*/profit [<n>]:* `Lists cumulative profit from all finished trades, "
"over the last n days`\n"
@@ -1446,11 +1526,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"

View File

@@ -43,23 +43,23 @@ class Webhook(RPCHandler):
def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """
try:
if msg['type'] == RPCMessageType.BUY:
valuedict = self._config['webhook'].get('webhookbuy', None)
elif msg['type'] == RPCMessageType.BUY_CANCEL:
valuedict = self._config['webhook'].get('webhookbuycancel', None)
elif msg['type'] == RPCMessageType.BUY_FILL:
valuedict = self._config['webhook'].get('webhookbuyfill', None)
elif msg['type'] == RPCMessageType.SELL:
valuedict = self._config['webhook'].get('webhooksell', None)
elif msg['type'] == RPCMessageType.SELL_FILL:
valuedict = self._config['webhook'].get('webhooksellfill', None)
elif msg['type'] == RPCMessageType.SELL_CANCEL:
valuedict = self._config['webhook'].get('webhooksellcancel', None)
whconfig = self._config['webhook']
if msg['type'] in [RPCMessageType.ENTRY]:
valuedict = whconfig.get('webhookentry', None)
elif msg['type'] in [RPCMessageType.ENTRY_CANCEL]:
valuedict = whconfig.get('webhookentrycancel', None)
elif msg['type'] in [RPCMessageType.ENTRY_FILL]:
valuedict = whconfig.get('webhookentryfill', None)
elif msg['type'] == RPCMessageType.EXIT:
valuedict = whconfig.get('webhookexit', None)
elif msg['type'] == RPCMessageType.EXIT_FILL:
valuedict = whconfig.get('webhookexitfill', None)
elif msg['type'] == RPCMessageType.EXIT_CANCEL:
valuedict = whconfig.get('webhookexitcancel', None)
elif msg['type'] in (RPCMessageType.STATUS,
RPCMessageType.STARTUP,
RPCMessageType.WARNING):
valuedict = self._config['webhook'].get('webhookstatus', None)
valuedict = whconfig.get('webhookstatus', None)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))
if not valuedict: