2023-12-10 11:52:49 +01:00 | | | import os
|
| | | import sys
|
| | | from datetime import timedelta, datetime
|
| | |
|
| | | import asyncio
|
| | |
|
| | | import pandas as pd
|
| | | import requests
|
| | |
|
| | | from service.App import *
|
| | | from common.utils import *
|
| | |
|
| | | import logging
|
| | | log = logging.getLogger('notifier')
|
| | |
|
| | |
|
2025-03-25 19:56:20 +01:00 | | | async def send_score_notification(df, model: dict, config: dict):
|
| | | symbol = config["symbol"]
|
| | | freq = config["freq"]
|
2023-12-10 11:52:49 +01:00 | | |
|
| | | score_column_names = model.get("score_column_names")
|
| | | if not score_column_names:
|
| | | log.error(f"Empty list of score columns in score notifier. At least one column name with a score has to be provided in config. Ignore")
|
| | | return
|
| | |
|
| | | row = df.iloc[-1] # Last row stores the latest values we need
|
| | |
|
2024-09-18 13:25:20 +02:00 | | | interval_length = pd.Timedelta(freq).to_pytimedelta()
|
| | | close_time = row.name + interval_length # Add interval length because timestamp is start of the interval
|
2023-12-10 11:52:49 +01:00 | | | close_price = row["close"]
|
| | | trade_scores = [row[col] for col in score_column_names]
|
| | | trade_score_primary = trade_scores[0]
|
| | | trade_score_secondary = trade_scores[1] if len(trade_scores) > 1 else None
|
| | |
|
2025-03-30 14:07:33 +02:00 | | | #
|
2023-12-10 11:52:49 +01:00 | | | # Determine the band for the current score
|
2025-03-30 14:07:33 +02:00 | | | #
|
| | | band_no, band = _find_score_band(trade_score_primary, model)
|
2023-12-10 11:52:49 +01:00 | | |
|
| | | #
|
| | | # To message or not to message depending on score value and time
|
| | | #
|
| | |
|
2025-03-30 14:07:33 +02:00 | | | # Determine if the band was changed since the last time. Essentially, this means absolute signal strength increased
|
| | | # We store the previous band no as the model attribute
|
2023-12-10 11:52:49 +01:00 | | | prev_band_no = model.get("prev_band_no")
|
2025-04-06 11:03:34 +02:00 | | | if prev_band_no is not None:
|
2025-03-30 14:07:33 +02:00 | | | band_up = abs(band_no) > abs(prev_band_no) # Examples: 0 -> 1, 1 -> 2, -1 -> 2
|
| | | band_dn = abs(band_no) < abs(prev_band_no) # Examples: -2 -> 0, 2 -> -1, -2 -> -1
|
| | | else:
|
2025-04-06 11:03:34 +02:00 | | | band_up = True
|
| | | band_dn = True
|
2025-03-30 14:07:33 +02:00 | | | model["prev_band_no"] = band_no # Store for the next time as an additional run-time attribute
|
2023-12-10 11:52:49 +01:00 | | |
|
2025-03-30 20:26:03 +02:00 | | | if band and band.get("frequency"):
|
2023-12-10 11:52:49 +01:00 | | | new_to_time_interval = close_time.minute % band.get("frequency") == 0
|
| | | else:
|
| | | new_to_time_interval = False
|
| | |
|
2025-03-30 14:07:33 +02:00 | | | # Send only if one of these conditions is true or entered new time interval (current time)
|
2023-12-10 11:52:49 +01:00 | | | notification_is_needed = (
|
2025-03-30 14:07:33 +02:00 | | | (model.get("notify_band_up") and band_up) or # entered a higher band (absolute score increased). always notify when band changed
|
| | | (model.get("notify_band_dn") and band_dn) or # returned to a lower band (absolute score decreased). always notify when band changed
|
| | | new_to_time_interval # new time interval is started like 10 minutes (minimum frequency independent of the band changes)
|
2023-12-10 11:52:49 +01:00 | | | )
|
2025-03-30 14:07:33 +02:00 | | | # We might also exclude any notifications in case of no band (neutral zone)
|
2023-12-10 11:52:49 +01:00 | | |
|
| | | if not notification_is_needed:
|
| | | return # Nothing important happened: within the same band and same time interval
|
| | |
|
| | | #
|
| | | # Build a message with parameters from the current band
|
| | | #
|
| | |
|
| | | # Crypto Currency Symbols: https://github.com/yonilevy/crypto-currency-symbols
|
| | | if symbol == "BTCUSDT":
|
| | | symbol_char = "₿"
|
| | | elif symbol == "ETHUSDT":
|
| | | symbol_char = "Ξ"
|
| | | else:
|
| | | symbol_char = symbol
|
| | |
|
| | | if band_up:
|
| | | band_change_char = "↑"
|
| | | elif band_dn:
|
| | | band_change_char = "↓"
|
| | | else:
|
| | | band_change_char = ""
|
| | |
|
| | | primary_score_str = f"{trade_score_primary:+.2f} {band_change_char} "
|
| | | secondary_score_str = f"{trade_score_secondary:+.2f}" if trade_score_secondary is not None else ''
|
| | |
|
2025-03-30 14:07:33 +02:00 | | | if band:
|
2025-04-06 11:03:34 +02:00 | | | message = f"{band.get('sign', '')} {symbol_char} {int(close_price):,} Indicator: {primary_score_str} {secondary_score_str} {band.get('text', '')} {freq}"
|
2025-03-30 14:07:33 +02:00 | | | if band.get("bold"):
|
| | | message = "*" + message + "*"
|
| | | else:
|
| | | # Default message if the score in the neutral (very weak) zone which is not covered by the config bands
|
2025-04-06 11:03:34 +02:00 | | | message = f"{symbol_char} {int(close_price):,} Indicator: {primary_score_str} {secondary_score_str} {freq}"
|
2023-12-10 11:52:49 +01:00 | | |
|
| | | message = message.replace("+", "%2B") # For Telegram to display plus sign
|
| | |
|
| | | #
|
| | | # Send notification
|
| | | #
|
2025-03-25 19:56:20 +01:00 | | | bot_token = config["telegram_bot_token"]
|
| | | chat_id = config["telegram_chat_id"]
|
2023-12-10 11:52:49 +01:00 | | |
|
| | | try:
|
| | | url = 'https://api.telegram.org/bot' + bot_token + '/sendMessage?chat_id=' + chat_id + '&parse_mode=markdown&text=' + message
|
| | | response = requests.get(url)
|
| | | response_json = response.json()
|
| | | if not response_json.get('ok'):
|
| | | log.error(f"Error sending notification.")
|
| | | except Exception as e:
|
| | | log.error(f"Error sending notification: {e}")
|
| | |
|
| | |
|
2025-03-30 14:07:33 +02:00 | | | def _find_score_band(score_value, model):
|
| | | """
|
| | | Find band number and the band object given two lists with thresholds.
|
| | |
|
| | | The first list specifies lower bounds for the score and the function returns the first largest band which is less
|
| | | than or equal to the score. Band number is positive: 1, 2,...
|
| | |
|
| | | The second list specifies upper bounds for the score and the function returns the first smallest band which greater
|
| | | than the score. Band number is negative: -1, -2,---
|
| | |
|
| | | If the score does not fit into any band, then band number is 0 and None for the band object are returned.
|
| | | """
|
| | |
|
| | | # First, check if the score falls within some positive thresholds (with greater than condition)
|
| | | bands = model.get("positive_bands", [])
|
| | | bands = sorted(bands, key=lambda x: x.get("edge"), reverse=True) # Large thresholds first
|
| | | # Find first entry with the edge equal or less than the score
|
| | | band_no, band = next(((i, x) for i, x in enumerate(bands) if score_value >= x.get("edge")), (len(bands), None))
|
| | | band_no = len(bands) - band_no
|
| | | if not band: # Score is too small - smaller than all thresholds
|
| | | bands = model.get("negative_bands", [])
|
| | | bands = sorted(bands, key=lambda x: x.get("edge"), reverse=False) # Small thresholds first
|
| | | band_no, band = next(((i, x) for i, x in enumerate(bands) if score_value < x.get("edge")), (len(bands), None))
|
| | | band_no = -(len(bands) - band_no)
|
| | |
|
| | | return band_no, band
|
| | |
|
| | |
|
2023-12-10 11:52:49 +01:00 | | | if __name__ == '__main__':
|
| | | pass
|