Merge pull request #2887 from yazeed/rpc_notification_fixes
Wide RPC notifications fixes
This commit is contained in:
commit
2dea362eda
@ -92,7 +92,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
||||
| `webhook.enabled` | Enable usage of Webhook notifications <br> **Datatype:** Boolean
|
||||
| `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||
| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||
| `webhook.webhookbuycancel` | Payload to send on buy order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||
| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||
| `webhook.webhooksellcancel` | Payload to send on sell order cancel. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||
| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. <br> **Datatype:** String
|
||||
| `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** Boolean
|
||||
| `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> **Datatype:** IPv4
|
||||
|
@ -55,7 +55,7 @@ official commands. You can ask at any moment for help with `/help`.
|
||||
| `/reload_conf` | | Reloads the configuration file
|
||||
| `/show_config` | | Shows part of the current configuration with relevant settings to operation
|
||||
| `/status` | | Lists all open trades
|
||||
| `/status table` | | List all open trades in a table format
|
||||
| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
|
||||
| `/count` | | Displays number of trades used and available
|
||||
| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
|
||||
| `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||
|
@ -15,11 +15,21 @@ Sample configuration (tested using IFTTT).
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "{stake_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhookbuycancel": {
|
||||
"value1": "Cancelling Open Buy Order for {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "{stake_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhooksell": {
|
||||
"value1": "Selling {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "profit: {profit_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhooksellcancel": {
|
||||
"value1": "Cancelling Open Sell Order for {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "profit: {profit_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhookstatus": {
|
||||
"value1": "Status: {status}",
|
||||
"value2": "",
|
||||
@ -40,10 +50,29 @@ Possible parameters are:
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `limit`
|
||||
* `amount`
|
||||
* `open_date`
|
||||
* `stake_amount`
|
||||
* `stake_currency`
|
||||
* `fiat_currency`
|
||||
* `order_type`
|
||||
* `current_rate`
|
||||
|
||||
### Webhookbuycancel
|
||||
|
||||
The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `limit`
|
||||
* `amount`
|
||||
* `open_date`
|
||||
* `stake_amount`
|
||||
* `stake_currency`
|
||||
* `fiat_currency`
|
||||
* `order_type`
|
||||
* `current_rate`
|
||||
|
||||
### Webhooksell
|
||||
|
||||
@ -66,6 +95,27 @@ Possible parameters are:
|
||||
* `open_date`
|
||||
* `close_date`
|
||||
|
||||
### Webhooksellcancel
|
||||
|
||||
The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format.
|
||||
Possible parameters are:
|
||||
|
||||
* `exchange`
|
||||
* `pair`
|
||||
* `gain`
|
||||
* `limit`
|
||||
* `amount`
|
||||
* `open_rate`
|
||||
* `current_rate`
|
||||
* `profit_amount`
|
||||
* `profit_percent`
|
||||
* `stake_currency`
|
||||
* `fiat_currency`
|
||||
* `sell_reason`
|
||||
* `order_type`
|
||||
* `open_date`
|
||||
* `close_date`
|
||||
|
||||
### Webhookstatus
|
||||
|
||||
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.
|
||||
|
@ -78,7 +78,7 @@ CONF_SCHEMA = {
|
||||
'amend_last_stake_amount': {'type': 'boolean', 'default': False},
|
||||
'last_stake_amount_min_ratio': {
|
||||
'type': 'number', 'minimum': 0.0, 'maximum': 1.0, 'default': 0.5
|
||||
},
|
||||
},
|
||||
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
||||
'dry_run': {'type': 'boolean'},
|
||||
'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET},
|
||||
@ -191,7 +191,9 @@ CONF_SCHEMA = {
|
||||
'properties': {
|
||||
'enabled': {'type': 'boolean'},
|
||||
'webhookbuy': {'type': 'object'},
|
||||
'webhookbuycancel': {'type': 'object'},
|
||||
'webhooksell': {'type': 'object'},
|
||||
'webhooksellcancel': {'type': 'object'},
|
||||
'webhookstatus': {'type': 'object'},
|
||||
},
|
||||
},
|
||||
|
@ -234,7 +234,7 @@ class FreqtradeBot:
|
||||
|
||||
return trades_created
|
||||
|
||||
def get_buy_rate(self, pair: str, tick: Dict = None) -> float:
|
||||
def get_buy_rate(self, pair: str, refresh: bool, tick: Dict = None) -> float:
|
||||
"""
|
||||
Calculates bid target between current ask price and last price
|
||||
:return: float: Price
|
||||
@ -253,7 +253,7 @@ class FreqtradeBot:
|
||||
else:
|
||||
if not tick:
|
||||
logger.info('Using Last Ask / Last Price')
|
||||
ticker = self.exchange.fetch_ticker(pair)
|
||||
ticker = self.exchange.fetch_ticker(pair, refresh)
|
||||
else:
|
||||
ticker = tick
|
||||
if ticker['ask'] < ticker['last']:
|
||||
@ -404,7 +404,7 @@ class FreqtradeBot:
|
||||
|
||||
stake_amount = self.get_trade_stake_amount(pair)
|
||||
if not stake_amount:
|
||||
logger.debug("Stake amount is 0, ignoring possible trade for {pair}.")
|
||||
logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.")
|
||||
return False
|
||||
|
||||
logger.info(f"Buy signal found: about create a new trade with stake_amount: "
|
||||
@ -414,10 +414,12 @@ class FreqtradeBot:
|
||||
if ((bid_check_dom.get('enabled', False)) and
|
||||
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
|
||||
if self._check_depth_of_market_buy(pair, bid_check_dom):
|
||||
logger.info(f'Executing Buy for {pair}.')
|
||||
return self.execute_buy(pair, stake_amount)
|
||||
else:
|
||||
return False
|
||||
|
||||
logger.info(f'Executing Buy for {pair}')
|
||||
return self.execute_buy(pair, stake_amount)
|
||||
else:
|
||||
return False
|
||||
@ -450,7 +452,7 @@ class FreqtradeBot:
|
||||
"""
|
||||
Executes a limit buy for the given pair
|
||||
:param pair: pair for which we want to create a LIMIT_BUY
|
||||
:return: None
|
||||
:return: True if a buy order is created, false if it fails.
|
||||
"""
|
||||
time_in_force = self.strategy.order_time_in_force['buy']
|
||||
|
||||
@ -458,7 +460,7 @@ class FreqtradeBot:
|
||||
buy_limit_requested = price
|
||||
else:
|
||||
# Calculate price
|
||||
buy_limit_requested = self.get_buy_rate(pair)
|
||||
buy_limit_requested = self.get_buy_rate(pair, True)
|
||||
|
||||
min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested)
|
||||
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
||||
@ -552,6 +554,32 @@ class FreqtradeBot:
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'amount': trade.amount,
|
||||
'open_date': trade.open_date or datetime.utcnow(),
|
||||
'current_rate': trade.open_rate_requested,
|
||||
}
|
||||
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_buy_cancel(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a buy cancel occured.
|
||||
"""
|
||||
current_rate = self.get_buy_rate(trade.pair, True)
|
||||
|
||||
msg = {
|
||||
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'limit': trade.open_rate,
|
||||
'order_type': order_type,
|
||||
'stake_amount': trade.stake_amount,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
'amount': trade.amount,
|
||||
'open_date': trade.open_date,
|
||||
'current_rate': current_rate,
|
||||
}
|
||||
|
||||
# Send the message
|
||||
@ -751,7 +779,7 @@ class FreqtradeBot:
|
||||
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
||||
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
||||
# cancelling the current stoploss on exchange first
|
||||
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})'
|
||||
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) '
|
||||
'in order to add another one ...', order['id'])
|
||||
try:
|
||||
self.exchange.cancel_order(order['id'], trade.pair)
|
||||
@ -776,8 +804,8 @@ class FreqtradeBot:
|
||||
)
|
||||
|
||||
if should_sell.sell_flag:
|
||||
logger.info(f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}')
|
||||
self.execute_sell(trade, sell_rate, should_sell.sell_type)
|
||||
logger.info('executed sell, reason: %s', should_sell.sell_type)
|
||||
return True
|
||||
return False
|
||||
|
||||
@ -820,41 +848,41 @@ class FreqtradeBot:
|
||||
|
||||
if ((order['side'] == 'buy' and order['status'] == 'canceled')
|
||||
or (self._check_timed_out('buy', order))):
|
||||
|
||||
self.handle_timedout_limit_buy(trade, order)
|
||||
self.wallets.update()
|
||||
order_type = self.strategy.order_types['buy']
|
||||
self._notify_buy_cancel(trade, order_type)
|
||||
|
||||
elif ((order['side'] == 'sell' and order['status'] == 'canceled')
|
||||
or (self._check_timed_out('sell', order))):
|
||||
self.handle_timedout_limit_sell(trade, order)
|
||||
self.wallets.update()
|
||||
order_type = self.strategy.order_types['sell']
|
||||
self._notify_sell_cancel(trade, order_type)
|
||||
|
||||
def handle_buy_order_full_cancel(self, trade: Trade, reason: str) -> None:
|
||||
"""Close trade in database and send message"""
|
||||
def delete_trade(self, trade: Trade) -> None:
|
||||
"""Delete trade in database"""
|
||||
Trade.session.delete(trade)
|
||||
Trade.session.flush()
|
||||
logger.info('Buy order %s for %s.', reason, trade)
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': f'Unfilled buy order for {trade.pair} {reason}'
|
||||
})
|
||||
|
||||
def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
|
||||
"""
|
||||
Buy timeout - cancel order
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
reason = "cancelled due to timeout"
|
||||
if order['status'] != 'canceled':
|
||||
reason = "cancelled due to timeout"
|
||||
corder = self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||
logger.info('Buy order %s for %s.', reason, trade)
|
||||
else:
|
||||
# Order was cancelled already, so we can reuse the existing dict
|
||||
corder = order
|
||||
reason = "canceled on Exchange"
|
||||
reason = "cancelled on exchange"
|
||||
logger.info('Buy order %s for %s.', reason, trade)
|
||||
|
||||
if corder.get('remaining', order['remaining']) == order['amount']:
|
||||
# if trade is not partially completed, just delete the trade
|
||||
self.handle_buy_order_full_cancel(trade, reason)
|
||||
self.delete_trade(trade)
|
||||
return True
|
||||
|
||||
# if trade is partially complete, edit the stake details for the trade
|
||||
@ -889,24 +917,22 @@ class FreqtradeBot:
|
||||
Sell timeout - cancel order and update trade
|
||||
:return: True if order was fully cancelled
|
||||
"""
|
||||
# if trade is not partially completed, just cancel the trade
|
||||
if order['remaining'] == order['amount']:
|
||||
# if trade is not partially completed, just cancel the trade
|
||||
if order["status"] != "canceled":
|
||||
reason = "due to timeout"
|
||||
reason = "cancelled due to timeout"
|
||||
# if trade is not partially completed, just delete the trade
|
||||
self.exchange.cancel_order(trade.open_order_id, trade.pair)
|
||||
logger.info('Sell order timeout for %s.', trade)
|
||||
logger.info('Sell order %s for %s.', reason, trade)
|
||||
else:
|
||||
reason = "on exchange"
|
||||
logger.info('Sell order canceled on exchange for %s.', trade)
|
||||
reason = "cancelled on exchange"
|
||||
logger.info('Sell order %s for %s.', reason, trade)
|
||||
|
||||
trade.close_rate = None
|
||||
trade.close_profit = None
|
||||
trade.close_date = None
|
||||
trade.is_open = True
|
||||
trade.open_order_id = None
|
||||
self.rpc.send_msg({
|
||||
'type': RPCMessageType.STATUS_NOTIFICATION,
|
||||
'status': f'Unfilled sell order for {trade.pair} cancelled {reason}'
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
@ -938,13 +964,13 @@ class FreqtradeBot:
|
||||
raise DependencyException(
|
||||
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
||||
|
||||
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None:
|
||||
def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> bool:
|
||||
"""
|
||||
Executes a limit sell for the given trade and limit
|
||||
:param trade: Trade instance
|
||||
:param limit: limit rate for the sell order
|
||||
:param sellreason: Reason the sell was triggered
|
||||
:return: None
|
||||
:return: True if it succeeds (supported) False (not supported)
|
||||
"""
|
||||
sell_type = 'sell'
|
||||
if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||
@ -965,7 +991,7 @@ class FreqtradeBot:
|
||||
|
||||
order_type = self.strategy.order_types[sell_type]
|
||||
if sell_reason == SellType.EMERGENCY_SELL:
|
||||
# Emergencysells (default to market!)
|
||||
# Emergency sells (default to market!)
|
||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||
|
||||
amount = self._safe_sell_amount(trade.pair, trade.amount)
|
||||
@ -990,6 +1016,8 @@ class FreqtradeBot:
|
||||
|
||||
self._notify_sell(trade, order_type)
|
||||
|
||||
return True
|
||||
|
||||
def _notify_sell(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell occured.
|
||||
@ -1006,7 +1034,7 @@ class FreqtradeBot:
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
'limit': trade.close_rate_requested,
|
||||
'limit': profit_rate,
|
||||
'order_type': order_type,
|
||||
'amount': trade.amount,
|
||||
'open_rate': trade.open_rate,
|
||||
@ -1017,6 +1045,44 @@ class FreqtradeBot:
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date or datetime.utcnow(),
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
}
|
||||
|
||||
if 'fiat_display_currency' in self.config:
|
||||
msg.update({
|
||||
'fiat_currency': self.config['fiat_display_currency'],
|
||||
})
|
||||
|
||||
# Send the message
|
||||
self.rpc.send_msg(msg)
|
||||
|
||||
def _notify_sell_cancel(self, trade: Trade, order_type: str) -> None:
|
||||
"""
|
||||
Sends rpc notification when a sell cancel occured.
|
||||
"""
|
||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||
current_rate = self.get_sell_rate(trade.pair, True)
|
||||
profit_percent = trade.calc_profit_ratio(profit_rate)
|
||||
gain = "profit" if profit_percent > 0 else "loss"
|
||||
|
||||
msg = {
|
||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
'limit': profit_rate,
|
||||
'order_type': order_type,
|
||||
'amount': trade.amount,
|
||||
'open_rate': trade.open_rate,
|
||||
'current_rate': current_rate,
|
||||
'profit_amount': profit_trade,
|
||||
'profit_percent': profit_percent,
|
||||
'sell_reason': trade.sell_reason,
|
||||
'open_date': trade.open_date,
|
||||
'close_date': trade.close_date,
|
||||
'stake_currency': self.config['stake_currency'],
|
||||
'fiat_currency': self.config.get('fiat_display_currency', None),
|
||||
}
|
||||
|
||||
if 'fiat_display_currency' in self.config:
|
||||
|
@ -26,7 +26,9 @@ class RPCMessageType(Enum):
|
||||
WARNING_NOTIFICATION = 'warning'
|
||||
CUSTOM_NOTIFICATION = 'custom'
|
||||
BUY_NOTIFICATION = 'buy'
|
||||
BUY_CANCEL_NOTIFICATION = 'buy_cancel'
|
||||
SELL_NOTIFICATION = 'sell'
|
||||
SELL_CANCEL_NOTIFICATION = 'sell_cancel'
|
||||
|
||||
def __repr__(self):
|
||||
return self.value
|
||||
@ -39,6 +41,7 @@ class RPCException(Exception):
|
||||
|
||||
raise RPCException('*Status:* `no active trade`')
|
||||
"""
|
||||
|
||||
def __init__(self, message: str) -> None:
|
||||
super().__init__(self)
|
||||
self.message = message
|
||||
@ -157,15 +160,17 @@ class RPC:
|
||||
profit_str = f'{trade_perc:.2f}%'
|
||||
if self._fiat_converter:
|
||||
fiat_profit = self._fiat_converter.convert_amount(
|
||||
trade_profit,
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
)
|
||||
trade_profit,
|
||||
stake_currency,
|
||||
fiat_display_currency
|
||||
)
|
||||
if fiat_profit and not isnan(fiat_profit):
|
||||
profit_str += f" ({fiat_profit:.2f})"
|
||||
trades_list.append([
|
||||
trade.id,
|
||||
trade.pair,
|
||||
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 '',
|
||||
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
|
||||
profit_str
|
||||
])
|
||||
|
@ -134,13 +134,18 @@ class Telegram(RPC):
|
||||
msg['stake_amount_fiat'] = 0
|
||||
|
||||
message = ("*{exchange}:* Buying {pair}\n"
|
||||
"at rate `{limit:.8f}\n"
|
||||
"({stake_amount:.6f} {stake_currency}").format(**msg)
|
||||
"*Amount:* `{amount:.8f}`\n"
|
||||
"*Open Rate:* `{limit:.8f}`\n"
|
||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||
"*Total:* `({stake_amount:.6f} {stake_currency}").format(**msg)
|
||||
|
||||
if msg.get('fiat_currency', None):
|
||||
message += ",{stake_amount_fiat:.3f} {fiat_currency}".format(**msg)
|
||||
message += ", {stake_amount_fiat:.3f} {fiat_currency}".format(**msg)
|
||||
message += ")`"
|
||||
|
||||
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
|
||||
message = "*{exchange}:* Cancelling Open Buy Order for {pair}".format(**msg)
|
||||
|
||||
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
||||
msg['amount'] = round(msg['amount'], 8)
|
||||
msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)
|
||||
@ -149,10 +154,10 @@ class Telegram(RPC):
|
||||
msg['duration_min'] = msg['duration'].total_seconds() / 60
|
||||
|
||||
message = ("*{exchange}:* Selling {pair}\n"
|
||||
"*Rate:* `{limit:.8f}`\n"
|
||||
"*Amount:* `{amount:.8f}`\n"
|
||||
"*Open Rate:* `{open_rate:.8f}`\n"
|
||||
"*Current Rate:* `{current_rate:.8f}`\n"
|
||||
"*Close Rate:* `{limit:.8f}`\n"
|
||||
"*Sell Reason:* `{sell_reason}`\n"
|
||||
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
|
||||
"*Profit:* `{profit_percent:.2f}%`").format(**msg)
|
||||
@ -163,8 +168,11 @@ class Telegram(RPC):
|
||||
and self._fiat_converter):
|
||||
msg['profit_fiat'] = self._fiat_converter.convert_amount(
|
||||
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
|
||||
message += ('` ({gain}: {profit_amount:.8f} {stake_currency}`'
|
||||
'` / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
||||
message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
|
||||
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
|
||||
|
||||
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
|
||||
message = "*{exchange}:* Cancelling Open Sell Order for {pair}".format(**msg)
|
||||
|
||||
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
|
||||
message = '*Status:* `{status}`'.format(**msg)
|
||||
@ -553,6 +561,8 @@ class Telegram(RPC):
|
||||
"*/stop:* `Stops the trader`\n" \
|
||||
"*/status [table]:* `Lists all open trades`\n" \
|
||||
" *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" \
|
||||
"*/profit:* `Lists cumulative profit from all finished trades`\n" \
|
||||
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, " \
|
||||
"regardless of profit`\n" \
|
||||
|
@ -41,8 +41,12 @@ class Webhook(RPC):
|
||||
|
||||
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhookbuy', None)
|
||||
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhookbuycancel', None)
|
||||
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhooksell', None)
|
||||
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
|
||||
valuedict = self._config['webhook'].get('webhooksellcancel', None)
|
||||
elif msg['type'] in(RPCMessageType.STATUS_NOTIFICATION,
|
||||
RPCMessageType.CUSTOM_NOTIFICATION,
|
||||
RPCMessageType.WARNING_NOTIFICATION):
|
||||
|
@ -122,7 +122,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
assert "Since" in headers
|
||||
assert "Pair" in headers
|
||||
assert 'instantly' == result[0][2]
|
||||
assert 'ETH/BTC' == result[0][1]
|
||||
assert 'ETH/BTC' in result[0][1]
|
||||
assert '-0.59%' == result[0][3]
|
||||
# Test with fiatconvert
|
||||
|
||||
@ -131,7 +131,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
assert "Since" in headers
|
||||
assert "Pair" in headers
|
||||
assert 'instantly' == result[0][2]
|
||||
assert 'ETH/BTC' == result[0][1]
|
||||
assert 'ETH/BTC' in result[0][1]
|
||||
assert '-0.59% (-0.09)' == result[0][3]
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
@ -140,7 +140,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
|
||||
rpc._freqtrade.exchange._cached_ticker = {}
|
||||
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
|
||||
assert 'instantly' == result[0][2]
|
||||
assert 'ETH/BTC' == result[0][1]
|
||||
assert 'ETH/BTC' in result[0][1]
|
||||
assert 'nan%' == result[0][3]
|
||||
|
||||
|
||||
|
@ -284,7 +284,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
|
||||
|
||||
assert int(fields[0]) == 1
|
||||
assert fields[1] == 'ETH/BTC'
|
||||
assert 'ETH/BTC' in fields[1]
|
||||
assert msg_mock.call_count == 1
|
||||
|
||||
|
||||
@ -1200,12 +1200,35 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
|
||||
'stake_amount': 0.001,
|
||||
'stake_amount_fiat': 0.0,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'USD'
|
||||
'fiat_currency': 'USD',
|
||||
'current_rate': 1.099e-05,
|
||||
'amount': 1333.3333333333335,
|
||||
'open_date': arrow.utcnow().shift(hours=-1)
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== '*Bittrex:* Buying ETH/BTC\n' \
|
||||
'at rate `0.00001099\n' \
|
||||
'(0.001000 BTC,0.000 USD)`'
|
||||
'*Amount:* `1333.33333333`\n' \
|
||||
'*Open Rate:* `0.00001099`\n' \
|
||||
'*Current Rate:* `0.00001099`\n' \
|
||||
'*Total:* `(0.001000 BTC, 0.000 USD)`'
|
||||
|
||||
|
||||
def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== ('*Bittrex:* Cancelling Open Buy Order for ETH/BTC')
|
||||
|
||||
|
||||
def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||
@ -1239,13 +1262,13 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== ('*Binance:* Selling KEY/ETH\n'
|
||||
'*Rate:* `0.00003201`\n'
|
||||
'*Amount:* `1333.33333333`\n'
|
||||
'*Open Rate:* `0.00007500`\n'
|
||||
'*Current Rate:* `0.00003201`\n'
|
||||
'*Close Rate:* `0.00003201`\n'
|
||||
'*Sell Reason:* `stop_loss`\n'
|
||||
'*Duration:* `1:00:00 (60.0 min)`\n'
|
||||
'*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`')
|
||||
'*Profit:* `-57.41%` `(loss: -0.05746268 ETH / -24.812 USD)`')
|
||||
|
||||
msg_mock.reset_mock()
|
||||
telegram.send_msg({
|
||||
@ -1267,10 +1290,10 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== ('*Binance:* Selling KEY/ETH\n'
|
||||
'*Rate:* `0.00003201`\n'
|
||||
'*Amount:* `1333.33333333`\n'
|
||||
'*Open Rate:* `0.00007500`\n'
|
||||
'*Current Rate:* `0.00003201`\n'
|
||||
'*Close Rate:* `0.00003201`\n'
|
||||
'*Sell Reason:* `stop_loss`\n'
|
||||
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
|
||||
'*Profit:* `-57.41%`')
|
||||
@ -1278,6 +1301,37 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||
telegram._fiat_converter.convert_amount = old_convamount
|
||||
|
||||
|
||||
def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.rpc.telegram.Telegram',
|
||||
_init=MagicMock(),
|
||||
_send_msg=msg_mock
|
||||
)
|
||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||
telegram = Telegram(freqtradebot)
|
||||
old_convamount = telegram._fiat_converter.convert_amount
|
||||
telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
||||
'exchange': 'Binance',
|
||||
'pair': 'KEY/ETH',
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH')
|
||||
|
||||
msg_mock.reset_mock()
|
||||
telegram.send_msg({
|
||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
||||
'exchange': 'Binance',
|
||||
'pair': 'KEY/ETH',
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== ('*Binance:* Cancelling Open Sell Order for KEY/ETH')
|
||||
# Reset singleton function to avoid random breaks
|
||||
telegram._fiat_converter.convert_amount = old_convamount
|
||||
|
||||
|
||||
def test_send_msg_status_notification(default_conf, mocker) -> None:
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
@ -1360,12 +1414,17 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
||||
'stake_amount': 0.001,
|
||||
'stake_amount_fiat': 0.0,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': None
|
||||
'fiat_currency': None,
|
||||
'current_rate': 1.099e-05,
|
||||
'amount': 1333.3333333333335,
|
||||
'open_date': arrow.utcnow().shift(hours=-1)
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== '*Bittrex:* Buying ETH/BTC\n' \
|
||||
'at rate `0.00001099\n' \
|
||||
'(0.001000 BTC)`'
|
||||
'*Amount:* `1333.33333333`\n' \
|
||||
'*Open Rate:* `0.00001099`\n' \
|
||||
'*Current Rate:* `0.00001099`\n' \
|
||||
'*Total:* `(0.001000 BTC)`'
|
||||
|
||||
|
||||
def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||
@ -1398,10 +1457,10 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== '*Binance:* Selling KEY/ETH\n' \
|
||||
'*Rate:* `0.00003201`\n' \
|
||||
'*Amount:* `1333.33333333`\n' \
|
||||
'*Open Rate:* `0.00007500`\n' \
|
||||
'*Current Rate:* `0.00003201`\n' \
|
||||
'*Close Rate:* `0.00003201`\n' \
|
||||
'*Sell Reason:* `stop_loss`\n' \
|
||||
'*Duration:* `2:35:03 (155.1 min)`\n' \
|
||||
'*Profit:* `-57.41%`'
|
||||
|
@ -13,24 +13,34 @@ from tests.conftest import get_patched_freqtradebot, log_has
|
||||
|
||||
def get_webhook_dict() -> dict:
|
||||
return {
|
||||
"enabled": True,
|
||||
"url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
|
||||
"webhookbuy": {
|
||||
"value1": "Buying {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "{stake_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhooksell": {
|
||||
"value1": "Selling {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "profit: {profit_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhookstatus": {
|
||||
"value1": "Status: {status}",
|
||||
"value2": "",
|
||||
"value3": ""
|
||||
}
|
||||
}
|
||||
"enabled": True,
|
||||
"url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
|
||||
"webhookbuy": {
|
||||
"value1": "Buying {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "{stake_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhookbuycancel": {
|
||||
"value1": "Cancelling Open Buy Order for {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "{stake_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhooksell": {
|
||||
"value1": "Selling {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "profit: {profit_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhooksellcancel": {
|
||||
"value1": "Cancelling Open Sell Order for {pair}",
|
||||
"value2": "limit {limit:8f}",
|
||||
"value3": "profit: {profit_amount:8f} {stake_currency}"
|
||||
},
|
||||
"webhookstatus": {
|
||||
"value1": "Status: {status}",
|
||||
"value2": "",
|
||||
"value3": ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test__init__(mocker, default_conf):
|
||||
@ -44,6 +54,9 @@ def test_send_msg(default_conf, mocker):
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
webhook = Webhook(get_patched_freqtradebot(mocker, default_conf))
|
||||
# Test buy
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
msg = {
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
@ -54,8 +67,6 @@ def test_send_msg(default_conf, mocker):
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'EUR'
|
||||
}
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
webhook.send_msg(msg=msg)
|
||||
assert msg_mock.call_count == 1
|
||||
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||
@ -64,6 +75,27 @@ def test_send_msg(default_conf, mocker):
|
||||
default_conf["webhook"]["webhookbuy"]["value2"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg))
|
||||
# Test buy cancel
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
msg = {
|
||||
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'limit': 0.005,
|
||||
'stake_amount': 0.8,
|
||||
'stake_amount_fiat': 500,
|
||||
'stake_currency': 'BTC',
|
||||
'fiat_currency': 'EUR'
|
||||
}
|
||||
webhook.send_msg(msg=msg)
|
||||
assert msg_mock.call_count == 1
|
||||
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||
default_conf["webhook"]["webhookbuycancel"]["value1"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value2"] ==
|
||||
default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||
default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg))
|
||||
# Test sell
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
@ -90,7 +122,32 @@ def test_send_msg(default_conf, mocker):
|
||||
default_conf["webhook"]["webhooksell"]["value2"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||
default_conf["webhook"]["webhooksell"]["value3"].format(**msg))
|
||||
|
||||
# Test sell cancel
|
||||
msg_mock = MagicMock()
|
||||
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
|
||||
msg = {
|
||||
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': "profit",
|
||||
'limit': 0.005,
|
||||
'amount': 0.8,
|
||||
'order_type': 'limit',
|
||||
'open_rate': 0.004,
|
||||
'current_rate': 0.005,
|
||||
'profit_amount': 0.001,
|
||||
'profit_percent': 0.20,
|
||||
'stake_currency': 'BTC',
|
||||
'sell_reason': SellType.STOP_LOSS.value
|
||||
}
|
||||
webhook.send_msg(msg=msg)
|
||||
assert msg_mock.call_count == 1
|
||||
assert (msg_mock.call_args[0][0]["value1"] ==
|
||||
default_conf["webhook"]["webhooksellcancel"]["value1"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value2"] ==
|
||||
default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg))
|
||||
assert (msg_mock.call_args[0][0]["value3"] ==
|
||||
default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg))
|
||||
for msgtype in [RPCMessageType.STATUS_NOTIFICATION,
|
||||
RPCMessageType.WARNING_NOTIFICATION,
|
||||
RPCMessageType.CUSTOM_NOTIFICATION]:
|
||||
|
@ -300,7 +300,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
|
||||
|
||||
# stoploss shoud be hit
|
||||
assert freqtrade.handle_trade(trade) is True
|
||||
assert log_has('executed sell, reason: SellType.STOP_LOSS', caplog)
|
||||
assert log_has('Executing Sell for NEO/BTC. Reason: SellType.STOP_LOSS', caplog)
|
||||
assert trade.sell_reason == SellType.STOP_LOSS.value
|
||||
|
||||
|
||||
@ -921,7 +921,7 @@ def test_get_buy_rate(mocker, default_conf, ask, last, last_ab, expected) -> Non
|
||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
|
||||
MagicMock(return_value={'ask': ask, 'last': last}))
|
||||
|
||||
assert freqtrade.get_buy_rate('ETH/BTC') == expected
|
||||
assert freqtrade.get_buy_rate('ETH/BTC', True) == expected
|
||||
|
||||
|
||||
def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None:
|
||||
@ -1964,7 +1964,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
nb_trades = len(trades)
|
||||
assert nb_trades == 0
|
||||
assert log_has_re("Buy order canceled on Exchange for Trade.*", caplog)
|
||||
assert log_has_re("Buy order cancelled on exchange for Trade.*", caplog)
|
||||
|
||||
|
||||
def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old, open_trade,
|
||||
@ -2045,7 +2045,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old,
|
||||
assert cancel_order_mock.call_count == 0
|
||||
assert rpc_mock.call_count == 1
|
||||
assert open_trade.is_open is True
|
||||
assert log_has_re("Sell order canceled on exchange for Trade.*", caplog)
|
||||
assert log_has_re("Sell order cancelled on exchange for Trade.*", caplog)
|
||||
|
||||
|
||||
def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,
|
||||
@ -2067,7 +2067,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
|
||||
# note this is for a partially-complete buy order
|
||||
freqtrade.check_handle_timedout()
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 2
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
assert len(trades) == 1
|
||||
assert trades[0].amount == 23.0
|
||||
@ -2101,7 +2101,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap
|
||||
assert log_has_re(r"Applying fee on amount for Trade.* Order", caplog)
|
||||
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 2
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
assert len(trades) == 1
|
||||
# Verify that tradehas been updated
|
||||
@ -2140,7 +2140,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade,
|
||||
assert log_has_re(r"Could not update trade amount: .*", caplog)
|
||||
|
||||
assert cancel_order_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 1
|
||||
assert rpc_mock.call_count == 2
|
||||
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
|
||||
assert len(trades) == 1
|
||||
# Verify that tradehas been updated
|
||||
@ -3524,7 +3524,7 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None:
|
||||
default_conf['telegram']['enabled'] = False
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
assert freqtrade.get_buy_rate('ETH/BTC') == 0.043935
|
||||
assert freqtrade.get_buy_rate('ETH/BTC', True) == 0.043935
|
||||
assert ticker_mock.call_count == 0
|
||||
|
||||
|
||||
@ -3549,7 +3549,7 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2) -> None:
|
||||
|
||||
freqtrade = FreqtradeBot(default_conf)
|
||||
# orderbook shall be used even if tickers would be lower.
|
||||
assert freqtrade.get_buy_rate('ETH/BTC') != 0.042
|
||||
assert freqtrade.get_buy_rate('ETH/BTC', True) != 0.042
|
||||
assert ticker_mock.call_count == 0
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user