Merge pull request #6272 from stash86/fix-docs

Add more info on Telegram's status message
This commit is contained in:
Matthias 2022-02-05 16:22:45 +01:00 committed by GitHub
commit 6ed237a72a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 170 additions and 8 deletions

View File

@ -165,6 +165,29 @@ class Order(_DECL_BASE):
self.order_filled_date = datetime.now(timezone.utc) self.order_filled_date = datetime.now(timezone.utc)
self.order_update_date = datetime.now(timezone.utc) self.order_update_date = datetime.now(timezone.utc)
def to_json(self) -> Dict[str, Any]:
return {
'amount': self.amount,
'average': round(self.average, 8) if self.average else 0,
'cost': self.cost if self.cost else 0,
'filled': self.filled,
'ft_order_side': self.ft_order_side,
'is_open': self.ft_is_open,
'order_date': self.order_date.strftime(DATETIME_PRINT_FORMAT)
if self.order_date else None,
'order_timestamp': int(self.order_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.order_date else None,
'order_filled_date': self.order_filled_date.strftime(DATETIME_PRINT_FORMAT)
if self.order_filled_date else None,
'order_filled_timestamp': int(self.order_filled_date.replace(
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
'order_type': self.order_type,
'pair': self.ft_pair,
'price': self.price,
'remaining': self.remaining,
'status': self.status,
}
@staticmethod @staticmethod
def update_orders(orders: List['Order'], order: Dict[str, Any]): def update_orders(orders: List['Order'], order: Dict[str, Any]):
""" """
@ -282,6 +305,16 @@ class LocalTrade():
return self.close_date.replace(tzinfo=timezone.utc) return self.close_date.replace(tzinfo=timezone.utc)
def to_json(self) -> Dict[str, Any]: def to_json(self) -> Dict[str, Any]:
filled_orders = self.select_filled_orders()
filled_entries = []
filled_exits = []
if len(filled_orders) > 0:
for order in filled_orders:
if order.ft_order_side == 'buy':
filled_entries.append(order.to_json())
if order.ft_order_side == 'sell':
filled_exits.append(order.to_json())
return { return {
'trade_id': self.id, 'trade_id': self.id,
'pair': self.pair, 'pair': self.pair,
@ -345,6 +378,8 @@ class LocalTrade():
'max_rate': self.max_rate, 'max_rate': self.max_rate,
'open_order_id': self.open_order_id, 'open_order_id': self.open_order_id,
'filled_entry_orders': filled_entries,
'filled_exit_orders': filled_exits,
} }
@staticmethod @staticmethod
@ -615,14 +650,14 @@ class LocalTrade():
else: else:
return None return None
def select_filled_orders(self, order_side: str) -> List['Order']: def select_filled_orders(self, order_side: Optional[str] = None) -> List['Order']:
""" """
Finds filled orders for this orderside. Finds filled orders for this orderside.
:param order_side: Side of the order (either 'buy' or 'sell') :param order_side: Side of the order (either 'buy', 'sell', or None)
:return: array of Order objects :return: array of Order objects
""" """
return [o for o in self.orders if o.ft_order_side == order_side and return [o for o in self.orders if ((o.ft_order_side == order_side) or (order_side is None))
o.ft_is_open is False and and o.ft_is_open is False and
(o.filled or 0) > 0 and (o.filled or 0) > 0 and
o.status in NON_OPEN_EXCHANGE_STATES] o.status in NON_OPEN_EXCHANGE_STATES]

View File

@ -370,6 +370,47 @@ class Telegram(RPCHandler):
else: else:
return "\N{CROSS MARK}" return "\N{CROSS MARK}"
def _prepare_buy_details(self, filled_orders, base_currency):
"""
Prepare details of trade with buy adjustment enabled
"""
lines = []
for x, order in enumerate(filled_orders):
current_buy_datetime = arrow.get(order["order_filled_date"])
cur_buy_amount = order["amount"]
cur_buy_average = order["average"]
lines.append(" ")
if x == 0:
lines.append("*Buy #{}:*".format(x+1))
lines.append("*Buy Amount:* {} ({:.8f} {})"
.format(cur_buy_amount, order["cost"], base_currency))
lines.append("*Average Buy Price:* {}".format(cur_buy_average))
else:
sumA = 0
sumB = 0
for y in range(x):
sumA += (filled_orders[y]["amount"] * filled_orders[y]["average"])
sumB += filled_orders[y]["amount"]
prev_avg_price = sumA/sumB
price_to_1st_buy = ((cur_buy_average - filled_orders[0]["average"])
/ filled_orders[0]["average"])
minus_on_buy = (cur_buy_average - prev_avg_price)/prev_avg_price
dur_buys = current_buy_datetime - arrow.get(filled_orders[x-1]["order_filled_date"])
days = dur_buys.days
hours, remainder = divmod(dur_buys.seconds, 3600)
minutes, seconds = divmod(remainder, 60)
lines.append("*Buy #{}:* at {:.2%} avg profit".format(x+1, minus_on_buy))
lines.append("({})".format(current_buy_datetime
.humanize(granularity=["day", "hour", "minute"])))
lines.append("*Buy Amount:* {} ({:.8f} {})"
.format(cur_buy_amount, order["cost"], base_currency))
lines.append("*Average Buy Price:* {} ({:.2%} from 1st buy rate)"
.format(cur_buy_average, price_to_1st_buy))
lines.append("*Order filled at:* {}".format(order["order_filled_date"]))
lines.append("({}d {}h {}m {}s from previous buy)"
.format(days, hours, minutes, seconds))
return lines
@authorized_only @authorized_only
def _status(self, update: Update, context: CallbackContext) -> None: def _status(self, update: Update, context: CallbackContext) -> None:
""" """
@ -393,21 +434,33 @@ class Telegram(RPCHandler):
trade_ids = [int(i) for i in context.args if i.isnumeric()] trade_ids = [int(i) for i in context.args if i.isnumeric()]
results = self._rpc._rpc_trade_status(trade_ids=trade_ids) results = self._rpc._rpc_trade_status(trade_ids=trade_ids)
position_adjust = self._config.get('position_adjustment_enable', False)
messages = [] messages = []
for r in results: for r in results:
r['open_date_hum'] = arrow.get(r['open_date']).humanize() r['open_date_hum'] = arrow.get(r['open_date']).humanize()
r['num_entries'] = len(r['filled_entry_orders'])
r['sell_reason'] = r.get('sell_reason', "")
lines = [ lines = [
"*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Trade ID:* `{trade_id}` `(since {open_date_hum})`",
"*Current Pair:* {pair}", "*Current Pair:* {pair}",
"*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Amount:* `{amount} ({stake_amount} {base_currency})`",
"*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "",
"*Sell Reason:* `{sell_reason}`" if r['sell_reason'] else "",
]
if position_adjust:
lines.append("*Number of Buy(s):* `{num_entries}`")
lines.extend([
"*Open Rate:* `{open_rate:.8f}`", "*Open Rate:* `{open_rate:.8f}`",
"*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "",
"*Open Date:* `{open_date}`",
"*Close Date:* `{close_date}`" if r['close_date'] else "",
"*Current Rate:* `{current_rate:.8f}`", "*Current Rate:* `{current_rate:.8f}`",
("*Current Profit:* " if r['is_open'] else "*Close Profit: *") ("*Current Profit:* " if r['is_open'] else "*Close Profit: *")
+ "`{profit_ratio:.2%}`", + "`{profit_ratio:.2%}`",
] ])
if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] if (r['stop_loss_abs'] != r['initial_stop_loss_abs']
and r['initial_stop_loss_ratio'] is not None): and r['initial_stop_loss_ratio'] is not None):
# Adding initial stoploss only if it is different from stoploss # Adding initial stoploss only if it is different from stoploss
@ -425,6 +478,11 @@ class Telegram(RPCHandler):
else: else:
lines.append("*Open Order:* `{open_order}`") lines.append("*Open Order:* `{open_order}`")
if len(r['filled_entry_orders']) > 1:
lines_detail = self._prepare_buy_details(
r['filled_entry_orders'], r['base_currency'])
lines.extend(lines_detail)
# Filter empty lines using list-comprehension # Filter empty lines using list-comprehension
messages.append("\n".join([line for line in lines if line]).format(**r)) messages.append("\n".join([line for line in lines if line]).format(**r))

View File

@ -14,6 +14,7 @@ def mock_order_1():
'side': 'buy', 'side': 'buy',
'type': 'limit', 'type': 'limit',
'price': 0.123, 'price': 0.123,
'average': 0.123,
'amount': 123.0, 'amount': 123.0,
'filled': 123.0, 'filled': 123.0,
'remaining': 0.0, 'remaining': 0.0,

View File

@ -108,6 +108,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10448878, 'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None, 'open_order': None,
'exchange': 'binance', 'exchange': 'binance',
'filled_entry_orders': [{
'amount': 91.07468123, 'average': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
'is_open': False, 'pair': 'ETH/BTC',
'remaining': ANY, 'status': ANY}],
'filled_exit_orders': []
} }
mocker.patch('freqtrade.exchange.Exchange.get_rate', mocker.patch('freqtrade.exchange.Exchange.get_rate',
@ -175,6 +183,14 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist_ratio': -0.10448878, 'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None, 'open_order': None,
'exchange': 'binance', 'exchange': 'binance',
'filled_entry_orders': [{
'amount': 91.07468123, 'average': 1.098e-05,
'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy',
'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY,
'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05,
'is_open': False, 'pair': 'ETH/BTC',
'remaining': ANY, 'status': ANY}],
'filled_exit_orders': []
} }

View File

@ -23,6 +23,7 @@ from freqtrade.exceptions import OperationalException
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.loggers import setup_logging from freqtrade.loggers import setup_logging
from freqtrade.persistence import PairLocks, Trade from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.models import Order
from freqtrade.rpc import RPC from freqtrade.rpc import RPC
from freqtrade.rpc.rpc import RPCException from freqtrade.rpc.rpc import RPCException
from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.rpc.telegram import Telegram, authorized_only
@ -201,7 +202,8 @@ def test_telegram_status(default_conf, update, mocker) -> None:
'stoploss_current_dist_ratio': -0.0002, 'stoploss_current_dist_ratio': -0.0002,
'stop_loss_ratio': -0.0001, 'stop_loss_ratio': -0.0001,
'open_order': '(limit buy rem=0.00000000)', 'open_order': '(limit buy rem=0.00000000)',
'is_open': True 'is_open': True,
'filled_entry_orders': []
}]), }]),
) )
@ -217,6 +219,52 @@ def test_telegram_status(default_conf, update, mocker) -> None:
assert status_table.call_count == 1 assert status_table.call_count == 1
@pytest.mark.usefixtures("init_persistence")
def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None:
update.message.chat.id = "123"
default_conf['telegram']['enabled'] = False
default_conf['telegram']['chat_id'] = "123"
default_conf['position_adjustment_enable'] = True
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_order=MagicMock(return_value=None),
get_rate=MagicMock(return_value=0.22),
)
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
create_mock_trades(fee)
trades = Trade.get_open_trades()
trade = trades[0]
trade.orders.append(Order(
order_id='5412vbb',
ft_order_side='buy',
ft_pair=trade.pair,
ft_is_open=False,
status="closed",
symbol=trade.pair,
order_type="market",
side="buy",
price=trade.open_rate * 0.95,
average=trade.open_rate * 0.95,
filled=trade.amount,
remaining=0,
cost=trade.amount,
order_date=trade.open_date,
order_filled_date=trade.open_date,
)
)
trade.recalc_trade_from_orders()
Trade.commit()
telegram._status(update=update, context=MagicMock())
assert msg_mock.call_count == 4
msg = msg_mock.call_args_list[0][0][0]
assert re.search(r'Number of Buy.*2', msg)
assert re.search(r'Average Buy Price', msg)
assert re.search(r'Order filled at', msg)
def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
default_conf['max_open_trades'] = 3 default_conf['max_open_trades'] = 3
mocker.patch.multiple( mocker.patch.multiple(

View File

@ -903,6 +903,8 @@ def test_to_json(default_conf, fee):
'buy_tag': None, 'buy_tag': None,
'timeframe': None, 'timeframe': None,
'exchange': 'binance', 'exchange': 'binance',
'filled_entry_orders': [],
'filled_exit_orders': []
} }
# Simulate dry_run entries # Simulate dry_run entries
@ -970,6 +972,8 @@ def test_to_json(default_conf, fee):
'buy_tag': 'buys_signal_001', 'buy_tag': 'buys_signal_001',
'timeframe': None, 'timeframe': None,
'exchange': 'binance', 'exchange': 'binance',
'filled_entry_orders': [],
'filled_exit_orders': []
} }