Merge pull request #6272 from stash86/fix-docs
Add more info on Telegram's status message
This commit is contained in:
commit
6ed237a72a
@ -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]
|
||||||
|
|
||||||
|
@ -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))
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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': []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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': []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user