wide notifications fixes

This commit is contained in:
Yazeed Al Oyoun 2020-02-08 21:02:52 +01:00
parent fff8ced3b0
commit f3b1161640
12 changed files with 288 additions and 85 deletions

View File

@ -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.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.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.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.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* | `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.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* | `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details. <br> ***Datatype:*** *IPv4*

View File

@ -55,7 +55,7 @@ official commands. You can ask at any moment for help with `/help`.
| `/reload_conf` | | Reloads the configuration file | `/reload_conf` | | Reloads the configuration file
| `/show_config` | | Shows part of the current configuration with relevant settings to operation | `/show_config` | | Shows part of the current configuration with relevant settings to operation
| `/status` | | Lists all open trades | `/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(*)
| `/count` | | Displays number of trades used and available | `/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 | `/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`). | `/forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).

View File

@ -15,11 +15,21 @@ Sample configuration (tested using IFTTT).
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}" "value3": "{stake_amount:8f} {stake_currency}"
}, },
"webhookbuycancel": {
"value1": "Cancelling Buy {pair}",
"value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}"
},
"webhooksell": { "webhooksell": {
"value1": "Selling {pair}", "value1": "Selling {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency}" "value3": "profit: {profit_amount:8f} {stake_currency}"
}, },
"webhooksellcancel": {
"value1": "Cancelling Sell {pair}",
"value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency}"
},
"webhookstatus": { "webhookstatus": {
"value1": "Status: {status}", "value1": "Status: {status}",
"value2": "", "value2": "",
@ -40,10 +50,29 @@ Possible parameters are:
* `exchange` * `exchange`
* `pair` * `pair`
* `limit` * `limit`
* `amount`
* `stake_amount` * `stake_amount`
* `stake_currency` * `stake_currency`
* `fiat_currency` * `fiat_currency`
* `order_type` * `order_type`
* `open_rate`
* `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`
* `stake_amount`
* `stake_currency`
* `fiat_currency`
* `order_type`
* `open_rate`
* `current_rate`
### Webhooksell ### Webhooksell
@ -57,6 +86,7 @@ Possible parameters are:
* `amount` * `amount`
* `open_rate` * `open_rate`
* `current_rate` * `current_rate`
* `close_rate`
* `profit_amount` * `profit_amount`
* `profit_percent` * `profit_percent`
* `stake_currency` * `stake_currency`
@ -66,6 +96,27 @@ Possible parameters are:
* `open_date` * `open_date`
* `close_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`
* `close_rate`
* `profit_amount`
* `profit_percent`
* `stake_currency`
* `fiat_currency`
* `sell_reason`
* `order_type`
* `open_date`
### Webhookstatus ### Webhookstatus
The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format. The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format.

View File

@ -78,7 +78,7 @@ CONF_SCHEMA = {
'amend_last_stake_amount': {'type': 'boolean', 'default': False}, 'amend_last_stake_amount': {'type': 'boolean', 'default': False},
'last_stake_amount_min_ratio': { 'last_stake_amount_min_ratio': {
'type': 'number', 'minimum': 0.0, 'maximum': 1.0, 'default': 0.5 'type': 'number', 'minimum': 0.0, 'maximum': 1.0, 'default': 0.5
}, },
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'}, 'dry_run': {'type': 'boolean'},
'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET}, 'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET},
@ -191,7 +191,9 @@ CONF_SCHEMA = {
'properties': { 'properties': {
'enabled': {'type': 'boolean'}, 'enabled': {'type': 'boolean'},
'webhookbuy': {'type': 'object'}, 'webhookbuy': {'type': 'object'},
'webhookbuycancel': {'type': 'object'},
'webhooksell': {'type': 'object'}, 'webhooksell': {'type': 'object'},
'webhooksellcancel': {'type': 'object'},
'webhookstatus': {'type': 'object'}, 'webhookstatus': {'type': 'object'},
}, },
}, },

View File

@ -234,7 +234,7 @@ class FreqtradeBot:
return trades_created return trades_created
def get_buy_rate(self, pair: str, tick: Dict = None) -> float: def get_buy_rate(self, pair: str, refresh: bool = False, tick: Dict = None) -> float:
""" """
Calculates bid target between current ask price and last price Calculates bid target between current ask price and last price
:return: float: Price :return: float: Price
@ -253,7 +253,7 @@ class FreqtradeBot:
else: else:
if not tick: if not tick:
logger.info('Using Last Ask / Last Price') logger.info('Using Last Ask / Last Price')
ticker = self.exchange.fetch_ticker(pair) ticker = self.exchange.fetch_ticker(pair, refresh)
else: else:
ticker = tick ticker = tick
if ticker['ask'] < ticker['last']: if ticker['ask'] < ticker['last']:
@ -404,7 +404,7 @@ class FreqtradeBot:
stake_amount = self.get_trade_stake_amount(pair) stake_amount = self.get_trade_stake_amount(pair)
if not stake_amount: 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 return False
logger.info(f"Buy signal found: about create a new trade with stake_amount: " 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 if ((bid_check_dom.get('enabled', False)) and
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)): (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
if self._check_depth_of_market_buy(pair, bid_check_dom): if self._check_depth_of_market_buy(pair, bid_check_dom):
logger.info(f'Executed Buy for {pair}.')
return self.execute_buy(pair, stake_amount) return self.execute_buy(pair, stake_amount)
else: else:
return False return False
logger.info(f'Executed Buy for {pair}')
return self.execute_buy(pair, stake_amount) return self.execute_buy(pair, stake_amount)
else: else:
return False return False
@ -450,7 +452,7 @@ class FreqtradeBot:
""" """
Executes a limit buy for the given pair Executes a limit buy for the given pair
:param pair: pair for which we want to create a LIMIT_BUY :param pair: pair for which we want to create a LIMIT_BUY
:return: None :return: bool
""" """
time_in_force = self.strategy.order_time_in_force['buy'] time_in_force = self.strategy.order_time_in_force['buy']
@ -458,7 +460,7 @@ class FreqtradeBot:
buy_limit_requested = price buy_limit_requested = price
else: else:
# Calculate price # 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) 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: if min_stake_amount is not None and min_stake_amount > stake_amount:
@ -547,11 +549,37 @@ class FreqtradeBot:
'type': RPCMessageType.BUY_NOTIFICATION, 'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': self.exchange.name.capitalize(), 'exchange': self.exchange.name.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
'limit': trade.open_rate, 'limit': trade.open_rate_requested,
'order_type': order_type, 'order_type': order_type,
'stake_amount': trade.stake_amount, 'stake_amount': trade.stake_amount,
'stake_currency': self.config['stake_currency'], 'stake_currency': self.config['stake_currency'],
'fiat_currency': self.config.get('fiat_display_currency', None), '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, False)
msg = {
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
'limit': trade.open_rate_requested,
'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 # Send the message
@ -587,7 +615,7 @@ class FreqtradeBot:
return trades_closed return trades_closed
def get_sell_rate(self, pair: str, refresh: bool) -> float: def get_sell_rate(self, pair: str, refresh: bool = False) -> float:
""" """
Get sell rate - either using get-ticker bid or first bid based on orderbook Get sell rate - either using get-ticker bid or first bid based on orderbook
The orderbook portion is only used for rpc messaging, which would otherwise fail The orderbook portion is only used for rpc messaging, which would otherwise fail
@ -751,7 +779,7 @@ class FreqtradeBot:
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
# cancelling the current stoploss on exchange first # 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']) 'in order to add another one ...', order['id'])
try: try:
self.exchange.cancel_order(order['id'], trade.pair) self.exchange.cancel_order(order['id'], trade.pair)
@ -777,7 +805,7 @@ class FreqtradeBot:
if should_sell.sell_flag: if should_sell.sell_flag:
self.execute_sell(trade, sell_rate, should_sell.sell_type) self.execute_sell(trade, sell_rate, should_sell.sell_type)
logger.info('executed sell, reason: %s', should_sell.sell_type) logger.info(f'Executed Sell for {trade.pair}. Reason: {should_sell.sell_type}')
return True return True
return False return False
@ -820,41 +848,41 @@ class FreqtradeBot:
if ((order['side'] == 'buy' and order['status'] == 'canceled') if ((order['side'] == 'buy' and order['status'] == 'canceled')
or (self._check_timed_out('buy', order))): or (self._check_timed_out('buy', order))):
self.handle_timedout_limit_buy(trade, order) self.handle_timedout_limit_buy(trade, order)
self.wallets.update() 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') elif ((order['side'] == 'sell' and order['status'] == 'canceled')
or (self._check_timed_out('sell', order))): or (self._check_timed_out('sell', order))):
self.handle_timedout_limit_sell(trade, order) self.handle_timedout_limit_sell(trade, order)
self.wallets.update() 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: def delete_trade(self, trade: Trade) -> None:
"""Close trade in database and send message""" """Delete trade in database"""
Trade.session.delete(trade) Trade.session.delete(trade)
Trade.session.flush() 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: def handle_timedout_limit_buy(self, trade: Trade, order: Dict) -> bool:
""" """
Buy timeout - cancel order Buy timeout - cancel order
:return: True if order was fully cancelled :return: True if order was fully cancelled
""" """
reason = "cancelled due to timeout"
if order['status'] != 'canceled': if order['status'] != 'canceled':
reason = "cancelled due to timeout"
corder = self.exchange.cancel_order(trade.open_order_id, trade.pair) corder = self.exchange.cancel_order(trade.open_order_id, trade.pair)
logger.info('Buy order %s for %s.', reason, trade)
else: else:
# Order was cancelled already, so we can reuse the existing dict # Order was cancelled already, so we can reuse the existing dict
corder = order 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 corder.get('remaining', order['remaining']) == order['amount']:
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
self.handle_buy_order_full_cancel(trade, reason) self.delete_trade(trade)
return True return True
# if trade is partially complete, edit the stake details for the trade # if trade is partially complete, edit the stake details for the trade
@ -878,10 +906,6 @@ class FreqtradeBot:
trade.open_order_id = None trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade) logger.info('Partial buy order timeout for %s.', trade)
self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'Remaining buy order for {trade.pair} cancelled due to timeout'
})
return False return False
def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool: def handle_timedout_limit_sell(self, trade: Trade, order: Dict) -> bool:
@ -889,24 +913,22 @@ class FreqtradeBot:
Sell timeout - cancel order and update trade Sell timeout - cancel order and update trade
:return: True if order was fully cancelled :return: True if order was fully cancelled
""" """
# if trade is not partially completed, just cancel the trade
if order['remaining'] == order['amount']: if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
if order["status"] != "canceled": 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) 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: else:
reason = "on exchange" reason = "cancelled on exchange"
logger.info('Sell order canceled on exchange for %s.', trade) logger.info('Sell order %s for %s.', reason, trade)
trade.close_rate = None trade.close_rate = None
trade.close_profit = None trade.close_profit = None
trade.close_date = None trade.close_date = None
trade.is_open = True trade.is_open = True
trade.open_order_id = None 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 return True
@ -938,13 +960,13 @@ class FreqtradeBot:
raise DependencyException( raise DependencyException(
f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") 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 Executes a limit sell for the given trade and limit
:param trade: Trade instance :param trade: Trade instance
:param limit: limit rate for the sell order :param limit: limit rate for the sell order
:param sellreason: Reason the sell was triggered :param sellreason: Reason the sell was triggered
:return: None :return: bool
""" """
sell_type = 'sell' sell_type = 'sell'
if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): if sell_reason in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
@ -965,7 +987,7 @@ class FreqtradeBot:
order_type = self.strategy.order_types[sell_type] order_type = self.strategy.order_types[sell_type]
if sell_reason == SellType.EMERGENCY_SELL: if sell_reason == SellType.EMERGENCY_SELL:
# Emergencysells (default to market!) # Emergency sells (default to market!)
order_type = self.strategy.order_types.get("emergencysell", "market") order_type = self.strategy.order_types.get("emergencysell", "market")
amount = self._safe_sell_amount(trade.pair, trade.amount) amount = self._safe_sell_amount(trade.pair, trade.amount)
@ -990,6 +1012,8 @@ class FreqtradeBot:
self._notify_sell(trade, order_type) self._notify_sell(trade, order_type)
return True
def _notify_sell(self, trade: Trade, order_type: str) -> None: def _notify_sell(self, trade: Trade, order_type: str) -> None:
""" """
Sends rpc notification when a sell occured. Sends rpc notification when a sell occured.
@ -1006,7 +1030,7 @@ class FreqtradeBot:
'exchange': trade.exchange.capitalize(), 'exchange': trade.exchange.capitalize(),
'pair': trade.pair, 'pair': trade.pair,
'gain': gain, 'gain': gain,
'limit': trade.close_rate_requested, 'limit': profit_rate,
'order_type': order_type, 'order_type': order_type,
'amount': trade.amount, 'amount': trade.amount,
'open_rate': trade.open_rate, 'open_rate': trade.open_rate,
@ -1017,6 +1041,45 @@ class FreqtradeBot:
'open_date': trade.open_date, 'open_date': trade.open_date,
'close_date': trade.close_date or datetime.utcnow(), 'close_date': trade.close_date or datetime.utcnow(),
'stake_currency': self.config['stake_currency'], '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)
# Use cached ticker here - it was updated seconds ago.
current_rate = self.get_sell_rate(trade.pair, False)
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: if 'fiat_display_currency' in self.config:

View File

@ -26,7 +26,9 @@ class RPCMessageType(Enum):
WARNING_NOTIFICATION = 'warning' WARNING_NOTIFICATION = 'warning'
CUSTOM_NOTIFICATION = 'custom' CUSTOM_NOTIFICATION = 'custom'
BUY_NOTIFICATION = 'buy' BUY_NOTIFICATION = 'buy'
BUY_CANCEL_NOTIFICATION = 'buy_cancel'
SELL_NOTIFICATION = 'sell' SELL_NOTIFICATION = 'sell'
SELL_CANCEL_NOTIFICATION = 'sell_cancel'
def __repr__(self): def __repr__(self):
return self.value return self.value
@ -39,6 +41,7 @@ class RPCException(Exception):
raise RPCException('*Status:* `no active trade`') raise RPCException('*Status:* `no active trade`')
""" """
def __init__(self, message: str) -> None: def __init__(self, message: str) -> None:
super().__init__(self) super().__init__(self)
self.message = message self.message = message
@ -157,15 +160,16 @@ class RPC:
profit_str = f'{trade_perc:.2f}%' profit_str = f'{trade_perc:.2f}%'
if self._fiat_converter: if self._fiat_converter:
fiat_profit = self._fiat_converter.convert_amount( fiat_profit = self._fiat_converter.convert_amount(
trade_profit, trade_profit,
stake_currency, stake_currency,
fiat_display_currency fiat_display_currency
) )
if fiat_profit and not isnan(fiat_profit): if fiat_profit and not isnan(fiat_profit):
profit_str += f" ({fiat_profit:.2f})" profit_str += f" ({fiat_profit:.2f})"
trades_list.append([ trades_list.append([
trade.id, trade.id,
trade.pair, trade.pair + ['', '*'][trade.open_order_id is not None
and trade.close_rate_requested is None],
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
profit_str profit_str
]) ])

View File

@ -134,13 +134,18 @@ class Telegram(RPC):
msg['stake_amount_fiat'] = 0 msg['stake_amount_fiat'] = 0
message = ("*{exchange}:* Buying {pair}\n" message = ("*{exchange}:* Buying {pair}\n"
"at rate `{limit:.8f}\n" "*Amount:* `{amount:.8f}`\n"
"({stake_amount:.6f} {stake_currency}").format(**msg) "*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): 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 += ")`" message += ")`"
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
message = "*{exchange}:* Cancelling Buy {pair}".format(**msg)
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
msg['amount'] = round(msg['amount'], 8) msg['amount'] = round(msg['amount'], 8)
msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)
@ -149,10 +154,10 @@ class Telegram(RPC):
msg['duration_min'] = msg['duration'].total_seconds() / 60 msg['duration_min'] = msg['duration'].total_seconds() / 60
message = ("*{exchange}:* Selling {pair}\n" message = ("*{exchange}:* Selling {pair}\n"
"*Rate:* `{limit:.8f}`\n"
"*Amount:* `{amount:.8f}`\n" "*Amount:* `{amount:.8f}`\n"
"*Open Rate:* `{open_rate:.8f}`\n" "*Open Rate:* `{open_rate:.8f}`\n"
"*Current Rate:* `{current_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n"
"*Close Rate:* `{limit:.8f}`\n"
"*Sell Reason:* `{sell_reason}`\n" "*Sell Reason:* `{sell_reason}`\n"
"*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Duration:* `{duration} ({duration_min:.1f} min)`\n"
"*Profit:* `{profit_percent:.2f}%`").format(**msg) "*Profit:* `{profit_percent:.2f}%`").format(**msg)
@ -163,8 +168,11 @@ class Telegram(RPC):
and self._fiat_converter): and self._fiat_converter):
msg['profit_fiat'] = self._fiat_converter.convert_amount( msg['profit_fiat'] = self._fiat_converter.convert_amount(
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
message += ('` ({gain}: {profit_amount:.8f} {stake_currency}`' message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
'` / {profit_fiat:.3f} {fiat_currency})`').format(**msg) ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
message = "*{exchange}:* Cancelling Sell {pair}".format(**msg)
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
message = '*Status:* `{status}`'.format(**msg) message = '*Status:* `{status}`'.format(**msg)

View File

@ -41,8 +41,12 @@ class Webhook(RPC):
if msg['type'] == RPCMessageType.BUY_NOTIFICATION: if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
valuedict = self._config['webhook'].get('webhookbuy', None) 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: elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
valuedict = self._config['webhook'].get('webhooksell', None) 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, elif msg['type'] in(RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.CUSTOM_NOTIFICATION, RPCMessageType.CUSTOM_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION): RPCMessageType.WARNING_NOTIFICATION):

View File

@ -122,7 +122,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert "Since" in headers assert "Since" in headers
assert "Pair" in headers assert "Pair" in headers
assert 'instantly' == result[0][2] assert 'instantly' == result[0][2]
assert 'ETH/BTC' == result[0][1] assert 'ETH/BTC' in result[0][1]
assert '-0.59%' == result[0][3] assert '-0.59%' == result[0][3]
# Test with fiatconvert # Test with fiatconvert
@ -131,7 +131,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert "Since" in headers assert "Since" in headers
assert "Pair" in headers assert "Pair" in headers
assert 'instantly' == result[0][2] 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] assert '-0.59% (-0.09)' == result[0][3]
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', 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 = {} rpc._freqtrade.exchange._cached_ticker = {}
result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD')
assert 'instantly' == result[0][2] assert 'instantly' == result[0][2]
assert 'ETH/BTC' == result[0][1] assert 'ETH/BTC' in result[0][1]
assert 'nan%' == result[0][3] assert 'nan%' == result[0][3]

View File

@ -284,7 +284,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ') fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ')
assert int(fields[0]) == 1 assert int(fields[0]) == 1
assert fields[1] == 'ETH/BTC' assert 'ETH/BTC' in fields[1]
assert msg_mock.call_count == 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': 0.001,
'stake_amount_fiat': 0.0, 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', '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] \ assert msg_mock.call_args[0][0] \
== '*Bittrex:* Buying ETH/BTC\n' \ == '*Bittrex:* Buying ETH/BTC\n' \
'at rate `0.00001099\n' \ '*Amount:* `1333.33333333`\n' \
'(0.001000 BTC,0.000 USD)`' '*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 Buy ETH/BTC')
def test_send_msg_sell_notification(default_conf, mocker) -> None: 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] \ assert msg_mock.call_args[0][0] \
== ('*Binance:* Selling KEY/ETH\n' == ('*Binance:* Selling KEY/ETH\n'
'*Rate:* `0.00003201`\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n' '*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`\n'
'*Sell Reason:* `stop_loss`\n' '*Sell Reason:* `stop_loss`\n'
'*Duration:* `1:00:00 (60.0 min)`\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() msg_mock.reset_mock()
telegram.send_msg({ telegram.send_msg({
@ -1267,10 +1290,10 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
}) })
assert msg_mock.call_args[0][0] \ assert msg_mock.call_args[0][0] \
== ('*Binance:* Selling KEY/ETH\n' == ('*Binance:* Selling KEY/ETH\n'
'*Rate:* `0.00003201`\n'
'*Amount:* `1333.33333333`\n' '*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00007500`\n' '*Open Rate:* `0.00007500`\n'
'*Current Rate:* `0.00003201`\n' '*Current Rate:* `0.00003201`\n'
'*Close Rate:* `0.00003201`\n'
'*Sell Reason:* `stop_loss`\n' '*Sell Reason:* `stop_loss`\n'
'*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n'
'*Profit:* `-57.41%`') '*Profit:* `-57.41%`')
@ -1278,6 +1301,37 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
telegram._fiat_converter.convert_amount = old_convamount 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 Sell 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 Sell 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: def test_send_msg_status_notification(default_conf, mocker) -> None:
msg_mock = MagicMock() msg_mock = MagicMock()
mocker.patch.multiple( 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': 0.001,
'stake_amount_fiat': 0.0, 'stake_amount_fiat': 0.0,
'stake_currency': 'BTC', '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] \ assert msg_mock.call_args[0][0] \
== '*Bittrex:* Buying ETH/BTC\n' \ == '*Bittrex:* Buying ETH/BTC\n' \
'at rate `0.00001099\n' \ '*Amount:* `1333.33333333`\n' \
'(0.001000 BTC)`' '*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: 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] \ assert msg_mock.call_args[0][0] \
== '*Binance:* Selling KEY/ETH\n' \ == '*Binance:* Selling KEY/ETH\n' \
'*Rate:* `0.00003201`\n' \
'*Amount:* `1333.33333333`\n' \ '*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00007500`\n' \ '*Open Rate:* `0.00007500`\n' \
'*Current Rate:* `0.00003201`\n' \ '*Current Rate:* `0.00003201`\n' \
'*Close Rate:* `0.00003201`\n' \
'*Sell Reason:* `stop_loss`\n' \ '*Sell Reason:* `stop_loss`\n' \
'*Duration:* `2:35:03 (155.1 min)`\n' \ '*Duration:* `2:35:03 (155.1 min)`\n' \
'*Profit:* `-57.41%`' '*Profit:* `-57.41%`'

View File

@ -13,24 +13,34 @@ from tests.conftest import get_patched_freqtradebot, log_has
def get_webhook_dict() -> dict: def get_webhook_dict() -> dict:
return { return {
"enabled": True, "enabled": True,
"url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/", "url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/",
"webhookbuy": { "webhookbuy": {
"value1": "Buying {pair}", "value1": "Buying {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}" "value3": "{stake_amount:8f} {stake_currency}"
}, },
"webhooksell": { "webhookbuycancel": {
"value1": "Selling {pair}", "value1": "Cancelling Buy {pair}",
"value2": "limit {limit:8f}", "value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency}" "value3": "{stake_amount:8f} {stake_currency}"
}, },
"webhookstatus": { "webhooksell": {
"value1": "Status: {status}", "value1": "Selling {pair}",
"value2": "", "value2": "limit {limit:8f}",
"value3": "" "value3": "profit: {profit_amount:8f} {stake_currency}"
} },
} "webhooksellcancel": {
"value1": "Cancelling Sell {pair}",
"value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency}"
},
"webhookstatus": {
"value1": "Status: {status}",
"value2": "",
"value3": ""
}
}
def test__init__(mocker, default_conf): def test__init__(mocker, default_conf):

View File

@ -300,7 +300,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
# stoploss shoud be hit # stoploss shoud be hit
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
assert log_has('executed sell, reason: SellType.STOP_LOSS', caplog) assert log_has('Executed Sell for NEO/BTC. Reason: SellType.STOP_LOSS', caplog)
assert trade.sell_reason == SellType.STOP_LOSS.value assert trade.sell_reason == SellType.STOP_LOSS.value
@ -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() trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
nb_trades = len(trades) nb_trades = len(trades)
assert nb_trades == 0 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, 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 cancel_order_mock.call_count == 0
assert rpc_mock.call_count == 1 assert rpc_mock.call_count == 1
assert open_trade.is_open is True 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, def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial,