den4ik2003

Untitled

Aug 5th, 2026
22
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 35.79 KB | None | 0 0
  1. import asyncio
  2. import logging
  3. import sys
  4. import os
  5. import json
  6. import subprocess
  7. import signal
  8. import psutil
  9. import time
  10. import re
  11. import pika
  12. import pandas as pd
  13. import threading
  14. from openpyxl.styles import Font
  15. from datetime import datetime, timezone, timedelta
  16. from pathlib import Path
  17. from aiogram.types import Message
  18. from aiogram.enums import ParseMode
  19. from aiogram import Bot, Dispatcher, types
  20. from aiogram.filters import CommandObject
  21. from aiogram.filters.command import Command
  22. from io import BytesIO
  23. from aiogram.types import FSInputFile
  24. from aiogram import F
  25. from aiogram.exceptions import TelegramBadRequest
  26. from apscheduler.schedulers.asyncio import AsyncIOScheduler
  27. from db_fetcher import DBReader
  28. from manual import manual_get_ubook2_async
  29.  
  30. logging.basicConfig(level=logging.INFO)
  31. debug = False
  32.  
  33. ##### RABBIT #######
  34. depth = {}
  35.  
  36. def rabbit_consumer():
  37.     def callback(ch, method, properties, body):
  38.         ch.basic_ack(delivery_tag=method.delivery_tag)
  39.  
  40.         msg = json.loads(body.decode())
  41.         name = '_'.join(msg['name'].split('.')[:-1])
  42.  
  43.         if name not in depth:
  44.             depth[name] = {'ts': 0, 'depth': {'asks': [], 'bids': []}}
  45.  
  46.         if msg['mtype'] != 'total_depth':
  47.             print(f"[warning] {int(1000*time.time())} not total_depth type: {msg['mtype']}")
  48.             return
  49.  
  50.         if int(msg['ts']) < depth[name]['ts']:
  51.             print(f"[debug] {int(1000*time.time())} old message: {int(msg['ts'])} < {depth[name]['ts']}")
  52.             return
  53.  
  54.         depth[name] = {'ts': int(msg['ts']), 'depth': msg['msg']}
  55.  
  56.     connection = pika.BlockingConnection(
  57.         pika.ConnectionParameters(host='localhost', port=9669)
  58.     )
  59.     channel = connection.channel()
  60.     channel.queue_declare(queue='depth_queue.prod', durable=True, arguments={'x-message-ttl': 3600000, 'x-queue-mode': 'lazy'})
  61.     channel.basic_consume(queue='depth_queue.prod', on_message_callback=callback)
  62.     channel.start_consuming()
  63.  
  64. rabbit_thread = threading.Thread(target=rabbit_consumer, daemon=True)
  65. rabbit_thread.start()
  66. ####################
  67.  
  68. scheduler = AsyncIOScheduler()
  69. dbReader = DBReader('/home/ubuntu/app/database.ini')
  70. dbSideCashoutReader = DBReader('/home/ubuntu/app/side_tb_database.ini')
  71. bot = Bot(token="8196473785:AAFCkXVPa7X0niky7RTHjjfA_-jMXodyWY0")
  72. dp = Dispatcher()
  73.  
  74. def parse_clients_json():
  75.     with open('clients.json', 'r') as clients_f:
  76.         chat_ids = json.loads(''.join(clients_f.readlines()).replace(' ', '').replace('\n', ''))
  77.         return chat_ids
  78.    
  79. def parse_admins_json():
  80.     with open('admins.json', 'r') as admins_f:
  81.         admin_ids = json.loads(''.join(admins_f.readlines()).replace(' ', '').replace('\n', ''))
  82.         return admin_ids
  83.  
  84. def parse_alarms_json():
  85.     with open('/home/ubuntu/tgbot/chat-id-token.json', 'r') as admins_f:
  86.         # chats_mapping: по токену получить айди чата
  87.         chats_mapping = json.loads(''.join(admins_f.readlines()).replace(' ', '').replace('\n', ''))
  88.     return chats_mapping
  89.  
  90. chats_mapping = parse_alarms_json()
  91. chat_ids = parse_clients_json()
  92. admin_ids = parse_admins_json()
  93.  
  94. timezones = {
  95.     -4788418340: timedelta(hours=9),
  96.     # -1002150192437: timedelta(hours=3),
  97. }
  98.  
  99. @dp.message(Command("liquidity_volume"))
  100. async def volume_balances(message: types.Message):
  101.     chat_id = str(message.chat.id)
  102.     if chat_id not in chat_ids:
  103.         await message.answer("Your chat is not in whitelist")
  104.         return
  105.  
  106.     result = ''
  107.     await dbReader.init_pool()
  108.     tasks = [dbReader.get_volumes_by_group(group) for group in chat_ids[chat_id]]
  109.     result = await asyncio.gather(*tasks, return_exceptions=True)
  110.     await message.answer(''.join([res for res in result if res is not None and not isinstance(res, Exception)]))
  111.  
  112.  
  113. MAX_LENGTH = 4096
  114.  
  115.  
  116. def split_html_safe(text, max_length=MAX_LENGTH):
  117.     """Split text into chunks <= max_length, breaking only on blank-line
  118.    boundaries between symbol blocks so HTML tags stay intact."""
  119.     blocks = text.split('\n\n')
  120.     chunks = []
  121.     buf = ''
  122.     for block in blocks:
  123.         piece = block + '\n\n'
  124.         if len(buf) + len(piece) > max_length:
  125.             if buf:
  126.                 chunks.append(buf)
  127.                 buf = ''
  128.             # block itself bigger than limit — fall back to line-level split
  129.             if len(piece) > max_length:
  130.                 for line in piece.split('\n'):
  131.                     line += '\n'
  132.                     if len(buf) + len(line) > max_length and buf:
  133.                         chunks.append(buf)
  134.                         buf = ''
  135.                     buf += line
  136.                 continue
  137.         buf += piece
  138.     if buf:
  139.         chunks.append(buf)
  140.     return chunks
  141.  
  142.  
  143. @dp.message(Command("trading_balances"))
  144. async def trading_balances(message: types.Message):
  145.     chat_id = str(message.chat.id)
  146.     if chat_id not in chat_ids:
  147.         await message.answer("Your chat is not in whitelist")
  148.         return
  149.  
  150.     result = ''
  151.     await dbReader.init_pool()
  152.     tasks = [dbReader.get_balances_by_group(group, 'V') for group in chat_ids[chat_id]]
  153.     result = await asyncio.gather(*tasks, return_exceptions=True)
  154.     text = ''.join([res for res in result if res is not None and not isinstance(res, Exception)])
  155.     for i in range(0, len(text), MAX_LENGTH):
  156.         await message.answer(text[i:i+MAX_LENGTH])
  157.  
  158.  
  159. @dp.message(Command("mm_balances"))
  160. async def mm_balance(message: types.Message):
  161.     chat_id = str(message.chat.id)
  162.     if chat_id not in chat_ids:
  163.         await message.answer("Your chat is not in whitelist")
  164.         return
  165.  
  166.     result = ''
  167.     await dbReader.init_pool()
  168.     tasks = [dbReader.get_balances_by_group(group, 'M') for group in chat_ids[chat_id]]
  169.     result = await asyncio.gather(*tasks, return_exceptions=True)
  170.     text = ''.join([res for res in result if res is not None and not isinstance(res, Exception)])
  171.     for i in range(0, len(text), MAX_LENGTH):
  172.         await message.answer(text[i:i+MAX_LENGTH])
  173.  
  174. def parse_date_range(args_text):
  175.     """Parse date range from command arguments.
  176.    Supported formats: DD.MM or DD.MM.YYYY
  177.    Returns (date_from, date_to) as strings 'YYYY-MM-DD', or (None, None) on failure.
  178.    Without arguments returns last 7 days.
  179.    """
  180.     if not args_text or not args_text.strip():
  181.         today = datetime.now().date()
  182.         date_from = today - timedelta(days=7)
  183.         return str(date_from), str(today)
  184.  
  185.     parts = args_text.strip().split()
  186.     if len(parts) != 2:
  187.         return None, None
  188.  
  189.     dates = []
  190.     for p in parts:
  191.         segments = p.split('.')
  192.         try:
  193.             if len(segments) == 2:
  194.                 d = datetime.strptime(p, '%d.%m').replace(year=datetime.now().year).date()
  195.             elif len(segments) == 3:
  196.                 d = datetime.strptime(p, '%d.%m.%Y').date()
  197.             else:
  198.                 return None, None
  199.             dates.append(d)
  200.         except ValueError:
  201.             return None, None
  202.  
  203.     return str(dates[0]), str(dates[1])
  204.  
  205.  
  206. @dp.message(Command("report"))
  207. async def report(message: types.Message, command: CommandObject):
  208.     chat_id = str(message.chat.id)
  209.     if chat_id not in chat_ids:
  210.         await message.answer("Your chat is not in whitelist")
  211.         return
  212.  
  213.     date_from, date_to = parse_date_range(command.args)
  214.     if date_from is None:
  215.         await message.answer("Usage: /report DD.MM DD.MM\nExample: /report 04.04 14.04")
  216.         return
  217.  
  218.     await dbReader.init_pool()
  219.     await dbSideCashoutReader.init_pool(side=True)
  220.     # Symbols list is cached at first init_pool; refresh so newly added
  221.     # side instances (e.g. tokens added in side DB after bot start) are seen.
  222.     await dbReader.update_symbols_list()
  223.     await dbSideCashoutReader.update_symbols_list(side=True)
  224.     tasks = [
  225.         dbReader.get_volume_report_by_group(group, date_from, date_to)
  226.         for group in chat_ids[chat_id] if '.' not in group
  227.     ]
  228.     tasks.extend([
  229.         dbSideCashoutReader.get_side_cashout_report_by_group(group, date_from, date_to)
  230.         for group in chat_ids[chat_id] if '.' in group
  231.     ])
  232.     result = await asyncio.gather(*tasks, return_exceptions=True)
  233.     text = ''.join([res for res in result if res is not None and not isinstance(res, Exception)])
  234.     if not text:
  235.         await message.answer("No data for report")
  236.         return
  237.     for chunk in split_html_safe(text):
  238.         await message.answer(chunk, parse_mode=ParseMode.HTML)
  239.  
  240.  
  241. @dp.message(Command("mm_report"))
  242. async def mm_report(message: types.Message, command: CommandObject):
  243.     chat_id = str(message.chat.id)
  244.     if chat_id not in chat_ids:
  245.         await message.answer("Your chat is not in whitelist")
  246.         return
  247.  
  248.     date_from, date_to = parse_date_range(command.args)
  249.     if date_from is None:
  250.         await message.answer("Usage: /mm_report DD.MM DD.MM\nExample: /mm_report 04.04 14.04")
  251.         return
  252.  
  253.     await dbReader.init_pool()
  254.     await dbReader.update_symbols_list()
  255.     tasks = [dbReader.get_mm_report_by_group(group, date_from, date_to) for group in chat_ids[chat_id]]
  256.     result = await asyncio.gather(*tasks, return_exceptions=True)
  257.     text = ''.join([res for res in result if res is not None and not isinstance(res, Exception)])
  258.     if not text:
  259.         await message.answer("No data for MM report")
  260.         return
  261.     for chunk in split_html_safe(text):
  262.         await message.answer(chunk, parse_mode=ParseMode.HTML)
  263.  
  264.  
  265. @dp.message(Command("ping"))
  266. async def ping(message: types.Message):
  267.     await message.reply("pong")
  268.  
  269. @dp.message(Command("cashout_balances"))
  270. async def cashout_balances(message: types.Message):
  271.     chat_id = str(message.chat.id)
  272.     if chat_id not in chat_ids:
  273.         await message.answer("Your chat is not in whitelist")
  274.         return
  275.  
  276.     result = ''
  277.     await dbReader.init_pool()
  278.     await dbSideCashoutReader.init_pool(side=True)
  279.     tasks_tb = [dbReader.get_balances_by_group(group, 'T') for group in chat_ids[chat_id] if '.' not in group]
  280.     tasks_tb.extend([dbSideCashoutReader.get_balances_by_group(group) for group in chat_ids[chat_id] if '.' in group])
  281.  
  282.     result = await asyncio.gather(*tasks_tb, return_exceptions=True)
  283.     await message.answer(''.join([res for res in result if res is not None and not isinstance(res, Exception)]))
  284.  
  285.  
  286. def calculate_book_orders(symbol): # NOTE: проверка на сущесвование symbol делается перед вызовом calculate_book_orders
  287.     book = depth[symbol]['depth']
  288.     book['asks'] = sorted(book['asks'], key=lambda x: x[0], reverse=True)
  289.     book['bids'] = sorted(book['bids'], key=lambda x: x[0], reverse=True)
  290.  
  291.     best_ask = min(book['asks'], key=lambda x: float(x[0]))[0]
  292.     best_bid = max(book['bids'], key=lambda x: float(x[0]))[0]
  293.     book_date = datetime.fromtimestamp(depth[symbol]['ts'] / 1000).strftime('%Y-%m-%d %H:%M:%S')
  294.  
  295.     orders = []
  296.     cur_info_asks = {2: [0, 0], 5: [0, 0], 10: [0, 0], 15: [0, 0], 20: [0, 0], 30: [0, 0], 50: [0, 0], 100: [0, 0]}
  297.     cur_info_bids = {2: [0, 0], 5: [0, 0], 10: [0, 0], 15: [0, 0], 20: [0, 0], 30: [0, 0], 50: [0, 0], 100: [0, 0]}
  298.  
  299.     for order in book['asks']:
  300.         prct = 100 * ((float(order[0]) / best_ask) - 1)
  301.         for key in cur_info_asks:
  302.             if prct <= key:
  303.                 cur_info_asks[key][0] += float(order[1])
  304.                 cur_info_asks[key][1] += float(order[0])*float(order[1])
  305.    
  306.     for order in book['asks'][-100:]:
  307.         orders.append({'symbol': symbol, 'time': book_date, 'price': order[0], 'quantity': order[1], 'side': 'SELL'})
  308.  
  309.     for order in book['bids']:
  310.         prct = 100 * ((best_bid / float(order[0])) - 1)
  311.         for key in cur_info_bids:
  312.             if prct <= key:
  313.                 cur_info_bids[key][0] += float(order[1])
  314.                 cur_info_bids[key][1] += float(order[0])*float(order[1])
  315.  
  316.     for order in book['bids'][:100]:
  317.         orders.append({'symbol': symbol, 'time': book_date, 'price': order[0], 'quantity': order[1], 'side': 'BUY'})
  318.  
  319.     orders.append({'symbol': None, 'time': None, 'price': None, 'side': None})
  320.  
  321.     return [cur_info_bids, cur_info_asks], orders
  322.  
  323.  
  324. @dp.message(Command(re.compile(r"depth(_\w+)?")))
  325. async def get_depth(message: types.Message, command: CommandObject):
  326.     chat_id = str(message.chat.id)
  327.     if chat_id not in chat_ids:
  328.         await message.answer("Your chat is not in whitelist")
  329.         return
  330.  
  331.     exchange = None
  332.     if command.args:
  333.         exchange = command.args.strip().lower()
  334.     else:
  335.         match = re.match(r"/depth_(\w+)", message.text)
  336.         if match:
  337.             exchange = match.group(1).lower()
  338.     if exchange == 'all':
  339.         exchange = None
  340.  
  341.     total_info = {}
  342.     for group in chat_ids[chat_id]:
  343.         symbol = group + '_USDT_' + exchange
  344.         if symbol not in depth:
  345.             continue
  346.         total_info[symbol], _ = calculate_book_orders(symbol)
  347.  
  348.     result = []
  349.     for symbol, depth_info in total_info.items():
  350.         symbol_orders = []
  351.         for prct, values in depth_info[0].items():
  352.             symbol_orders.append({'symbol': symbol, 'percent': prct, 'cumulative quantity': values[0], 'cumulative amount': values[1], 'side': 'BUY'})
  353.         for prct, values in depth_info[1].items():
  354.             symbol_orders.append({'symbol': symbol, 'percent': prct, 'cumulative quantity': values[0], 'cumulative amount': values[1], 'side': 'SELL'})
  355.         symbol_orders.append({'symbol': None, 'percent': None, 'cumulative quantity': None, 'cumulative amount': None, 'side': None})
  356.  
  357.         symbol_orders = pd.DataFrame(symbol_orders)
  358.         sell_sorted = symbol_orders[symbol_orders['side'] == 'SELL'].sort_values('percent', ascending=False)
  359.         buy_sorted = symbol_orders[symbol_orders['side'] == 'BUY'].sort_values('percent', ascending=True)
  360.         result.append(pd.concat([sell_sorted, buy_sorted], ignore_index=True))
  361.  
  362.     result = pd.concat(result, ignore_index=True)
  363.     output = BytesIO()
  364.     with pd.ExcelWriter(output, engine='openpyxl') as writer:
  365.         result.to_excel(writer, index=False, sheet_name='Depth %')
  366.  
  367.         worksheet = writer.sheets['Depth %']
  368.         for column_cells in worksheet.columns:
  369.             length = max(len(str(cell.value)) for cell in column_cells)
  370.             worksheet.column_dimensions[column_cells[0].column_letter].width = length + 2
  371.  
  372.     output.seek(0)
  373.  
  374.     await message.answer_document(
  375.         types.BufferedInputFile(
  376.             file=output.read(),
  377.             filename=f"depth_{exchange if exchange else 'all'}.xlsx"
  378.         ),
  379.         caption=f"Excel file with depth{' for ' + exchange if exchange else ''}"
  380.     )
  381.  
  382. @dp.message(Command(re.compile(r"orderbook(_\w+)?")))
  383. async def orders(message: types.Message, command: CommandObject):
  384.     chat_id = str(message.chat.id)
  385.     if chat_id not in chat_ids:
  386.         await message.answer("Your chat is not in whitelist")
  387.         return
  388.  
  389.     exchange = None
  390.     if command.args:
  391.         exchange = command.args.strip().lower()
  392.     else:
  393.         match = re.match(r"/orderbook_(\w+)", message.text)
  394.         if match:
  395.             exchange = match.group(1).lower()
  396.     if exchange == 'all':
  397.         exchange = None
  398.  
  399.     await dbReader.init_pool()
  400.     tasks = [dbReader.get_active_orders(group) for group in chat_ids[chat_id]]
  401.     result = await asyncio.gather(*tasks, return_exceptions=True)
  402.  
  403.     result_as_list = []
  404.     for res in result:
  405.         if res is not None and not isinstance(res, Exception):
  406.             result_as_list.extend(res)
  407.  
  408.     result = [pd.DataFrame(res) for res in result if res is not None and not isinstance(res, Exception)]
  409.  
  410.     total_orders = []
  411.     for group in chat_ids[chat_id]:
  412.         symbol = group + '_USDT_' + exchange
  413.         if symbol not in depth:
  414.             continue
  415.         _, orders = calculate_book_orders(symbol)
  416.         total_orders.extend(orders)
  417.  
  418.     markups = set()
  419.     for own_order in result_as_list:
  420.         for depth_order in total_orders:
  421.             if own_order['symbol'].replace('/', '_').replace(' ', '_') == depth_order['symbol'] and own_order['price'] == depth_order['price']:
  422.                 markups.add(f"{depth_order['symbol']}/{depth_order['price']}")
  423.  
  424.     all_orders_df = pd.concat(result, ignore_index=True)
  425.     if exchange is not None:
  426.         all_orders_df = all_orders_df[all_orders_df['symbol'].str.contains(exchange, case=False, na=False)]
  427.     all_orders_df = all_orders_df.reset_index(drop=True)
  428.  
  429.     output = BytesIO()
  430.     with pd.ExcelWriter(output, engine='openpyxl') as writer:
  431.         all_orders_df = all_orders_df.copy()
  432.         all_orders_df['side_order'] = all_orders_df['side'].map({'SELL': 0, 'BUY': 1})
  433.         all_orders_df['price_order'] = all_orders_df.apply(
  434.             lambda x: -x['price'] if x['side']=='SELL' else x['price'], axis=1
  435.         )
  436.         all_orders_df = all_orders_df.sort_values(
  437.             by=['symbol', 'side_order', 'price_order'],
  438.             ascending=[True, True, True]
  439.         )
  440.         all_orders_df = all_orders_df.drop(columns=['side_order', 'price_order'])
  441.         all_orders_df = all_orders_df.reset_index()
  442.  
  443.         if not all_orders_df.empty:
  444.             all_orders_df.to_excel(writer, index=True, sheet_name='Actual Orders')
  445.  
  446.         depth_df = pd.DataFrame(total_orders)
  447.         if not depth_df.empty:
  448.             depth_df.to_excel(writer, index=True, sheet_name='Depth')
  449.  
  450.         for sheet_name in writer.sheets:
  451.             worksheet = writer.sheets[sheet_name]
  452.             for column_cells in worksheet.columns:
  453.                 length = max(len(str(cell.value)) for cell in column_cells)
  454.                 worksheet.column_dimensions[column_cells[0].column_letter].width = length + 2
  455.  
  456.         if 'Depth' in writer.sheets:
  457.             depth_ws = writer.sheets['Depth']
  458.             for idx, row in depth_df.iterrows():
  459.                 key = f"{row['symbol']}/{row['price']}"
  460.                 if key in markups:
  461.                     excel_row = idx + 2  # 1 - заголовок, 2 - с учетом индекса
  462.                     for cell in depth_ws[excel_row]:
  463.                         cell.font = Font(bold=True)
  464.  
  465.     output.seek(0)
  466.  
  467.     await message.answer_document(
  468.         types.BufferedInputFile(
  469.             file=output.read(),
  470.             filename=f"orderbook_{exchange if exchange else 'all'}.xlsx"
  471.         ),
  472.         caption=f"Excel file with actual orders{' for ' + exchange if exchange else ''}"
  473.     )
  474.  
  475.  
  476. @dp.message(F.text.lower().contains('add_token_'))
  477. async def handler(message: Message):
  478.     current_chat_id = str(message.chat.id)
  479.     user_id = message.from_user.id
  480.  
  481.     if str(user_id) not in admin_ids:
  482.         await message.reply(f"you haven't admin rights to add token")
  483.         return
  484.  
  485.     if '.' not in message.text:
  486.         token_name = message.text.split('_')[2].upper()
  487.     else:
  488.         token_name = '_'.join(message.text.split('_')[2:])
  489.         token_name = token_name.split('.')[0].upper() + '.' + token_name.split('.')[1].lower()
  490.  
  491.     if '<' in token_name and '>' in token_name:
  492.         token_name = token_name[1:-1]      
  493.  
  494.     if current_chat_id in chat_ids:
  495.         if token_name in chat_ids[current_chat_id]:
  496.             await message.reply(f"{token_name} have already been added")
  497.             return
  498.         chat_ids[current_chat_id].append(token_name)
  499.     else:
  500.         chat_ids[current_chat_id] = [token_name]
  501.  
  502.     with open('clients.json', 'w') as clients_f:
  503.         clients_f.write(json.dumps(chat_ids))
  504.  
  505.     await dbReader.init_pool()
  506.     await dbSideCashoutReader.init_pool(side=True)
  507.  
  508.     await dbReader.update_symbols_list()
  509.     await dbSideCashoutReader.update_symbols_list(side=True)
  510.  
  511.     await message.reply(f"{token_name} was successfully added")
  512.  
  513.  
  514. @dp.message(F.text.lower().contains('delete_token_'))
  515. async def handler(message: Message):
  516.     current_chat_id = str(message.chat.id)
  517.     user_id = message.from_user.id
  518.  
  519.     if str(user_id) not in admin_ids:
  520.         await message.reply(f"you haven't admin rights to add token")
  521.         return
  522.  
  523.     if '.' not in message.text:
  524.         token_name = message.text.split('_')[2].upper()
  525.     else:
  526.         token_name = '_'.join(message.text.split('_')[2:])
  527.         token_name = token_name.split('.')[0].upper() + '.' + token_name.split('.')[1].lower()
  528.  
  529.     if '<' in token_name and '>' in token_name:
  530.         token_name = token_name[1:-1]      
  531.  
  532.     if current_chat_id in chat_ids:
  533.         if token_name not in chat_ids[current_chat_id]:
  534.             await message.reply(f"{token_name} not in tokens list")
  535.             return
  536.         chat_ids[current_chat_id].remove(token_name)    
  537.     else:
  538.         await message.reply(f"Your chat is not in chat ids")
  539.         return
  540.  
  541.     with open('clients.json', 'w') as clients_f:
  542.         clients_f.write(json.dumps(chat_ids))
  543.  
  544.     await dbReader.init_pool()
  545.     await dbSideCashoutReader.init_pool(side=True)
  546.  
  547.     await dbReader.update_symbols_list()
  548.     await dbSideCashoutReader.update_symbols_list(side=True)
  549.  
  550.     await message.reply(f"{token_name} was successfully deleted")
  551.    
  552.    
  553.  
  554. @dp.message(Command("conditions"))
  555. async def market_conditions(message: types.Message):
  556.     current_chat_id = str(message.chat.id)
  557.     await dbReader.init_pool()
  558.    
  559.     result = ''
  560.  
  561.     for group in chat_ids.get(current_chat_id, []):
  562.         for symbol_id in dbReader.symbols_id_in_group.get(group, []):
  563.             try:
  564.                 symbol_data = await dbReader.execute_select(
  565.                     f"""SELECT concat(exchange_name, \'.\', exchange_type) AS exchange_name, symbol, price_prec, qty_prec, hostname
  566.                        FROM symbols
  567.                        JOIN exchanges USING (exchange_id)
  568.                        JOIN id_group_relation USING (symbol_id)
  569.                        JOIN group_host_relation USING (group_id)
  570.                        JOIN hosts USING (host_id)
  571.                        WHERE symbol_id = {symbol_id}
  572.                    """
  573.                 )
  574.  
  575.                 if not symbol_data:
  576.                     continue
  577.  
  578.                 symbol_data = symbol_data[0]
  579.  
  580.                 # TODO market_symbol if market_symbol != {} else symbol_data.symbol ????
  581.                 res = await manual_get_ubook2_async(
  582.                     symbol_data[4], # hostname
  583.                     symbol_data[0], # exchange_name
  584.                     symbol_data[1], # symbol
  585.                     (symbol_data[2], symbol_data[3]) # decimals
  586.                 )
  587.                 if not res:
  588.                     continue
  589.                 ask_p, bid_p, uask2, ubid2 = res[0], res[1], res[2], res[3]
  590.                 spread = round(100*(ask_p - bid_p) / bid_p, 2) if bid_p != 0 else 'None'
  591.  
  592.                 resp = (
  593.                     f"{symbol_data[1]} {symbol_data[0].replace('.', ' ')}\n"
  594.                     f"🔴 Ask: {ask_p}\n"
  595.                     f"🟢 Bid: {bid_p}\n"
  596.                     f"➖ Spread: {spread}%\n"
  597.                     f"🔼 Depth +2%: {uask2}\n"
  598.                     f"🔽 Depth -2%: {ubid2}\n\n"
  599.                 )
  600.  
  601.                 result += resp
  602.            
  603.             except Exception as e:
  604.                 print(f'[Exception] market_conditions {group} {symbol_id}: {e}')
  605.  
  606.     if result:
  607.         await message.answer(result)
  608.  
  609.  
  610. ###############################################################
  611. ######################## ALARM BOT ############################
  612. ###############################################################
  613.  
  614. @dp.message(Command("start"))
  615. async def start(message: types.Message):
  616.     if str(message.from_user.id) not in admin_ids:
  617.         await message.reply(f"you haven't admin rights to start bot")
  618.         return
  619.  
  620.     title = message.chat.title
  621.     if title.find('<') == -1 or title.find('>') == -1 or title.find('<') >= title.find('>'):
  622.         await message.answer("Failed: wrong chat name")
  623.         return
  624.  
  625.     token = title[(title.find('<')+1):title.find('>')].upper()
  626.     chats_mapping[token] = str(message.chat.id)
  627.  
  628.     with open('/home/ubuntu/tgbot/chat-id-token.json', 'w') as token_f:
  629.         token_f.write(json.dumps(chats_mapping))
  630.  
  631.     await message.answer(f'Your chat title: \"{title}\"\nYour token: {token}')
  632.    
  633.  
  634. @dp.message(Command("stop"))
  635. async def stop(message: types.Message):
  636.     if str(message.from_user.id) not in admin_ids:
  637.         await message.reply(f"you haven't admin rights to stop bot")
  638.         return
  639.  
  640.     # TODO: сделать чтобы несколько чатов могли иметь одинаковый токен
  641.     tokens_to_delete = [t for t, cid in chats_mapping.items() if cid == str(message.chat.id)]
  642.    
  643.     if len(tokens_to_delete) == 0:
  644.         await message.answer(f'Your chat already was deleted from alarms list')
  645.         return
  646.        
  647.     for t in tokens_to_delete:
  648.         chats_mapping.pop(t, None)
  649.  
  650.     with open('/home/ubuntu/tgbot/chat-id-token.json', 'w') as token_f:
  651.         token_f.write(json.dumps(chats_mapping))
  652.  
  653.     await message.answer(f'Your chat with token {tokens_to_delete[0]} was successfully deleted from alarms list')
  654.  
  655.  
  656. alarms = {}
  657.  
  658. async def manage_alarm(bot, chat_id, value, alarm_type):
  659.     try:  # на всякий случай
  660.         symbol = value['symbol']
  661.         if symbol not in alarms:
  662.             alarms[symbol] = {
  663.                 'aliveness': {
  664.                     'status': True,
  665.                     'ts': 0  # last alarm time
  666.                 },
  667.                 'spread': {
  668.                     'status': True,
  669.                     'prev_values': [],
  670.                     'ts': 0  # last alarm time
  671.                 },
  672.                 'flat': {
  673.                     'status': True,       # armed: non-flat candle seen since last alert
  674.                     'last_bucket': None,  # epoch of the candle we last alerted on
  675.                     'ts': 0               # last alarm time (2h cooldown)
  676.                 }
  677.             }
  678.  
  679.         info = alarms[symbol]
  680.  
  681.         if alarm_type == 'spread':
  682.             if int(time.time()) - info['spread']['ts'] > 60 * 15:
  683.                 if value['spread'] > 0.03 and info['spread']['status']:
  684.                     try:
  685.                         await bot.send_message(
  686.                             text=f"{symbol}:\n--- spread > 3% ---\n~~~ ask/bid/spread   {value['ask_price']} / {value['bid_price']} / {round(100*value['spread'], 2)}%",
  687.                             chat_id=chat_id
  688.                         )
  689.                     except Exception as e:
  690.                         print('exception', e)
  691.  
  692.                 info['spread']['ts'] = int(time.time())
  693.                 info['spread']['status'] = (value['spread'] <= 0.03)
  694.  
  695.         elif alarm_type == 'aliveness':
  696.             if int(time.time()) - info['aliveness']['ts'] > 60 * 15:
  697.                 if not value['status'] and info['aliveness']['status']:
  698.                     try:
  699.                         await bot.send_message(
  700.                             text=f"{symbol}:\n--- DEAD ---",
  701.                             chat_id=chat_id
  702.                         )
  703.                     except Exception as e:
  704.                         print('exception', e)
  705.  
  706.                 info['aliveness']['ts'] = int(time.time())
  707.                 info['aliveness']['status'] = value['status']
  708.  
  709.         elif alarm_type == 'flat':
  710.             # Two layers of anti-spam, gated by a 2h cooldown:
  711.             #   - cooldown: at most one flat alert per symbol every 2 hours;
  712.             #   - arming: within that, alert only on a *fresh* flat episode —
  713.             #     fire while armed, then disarm; re-arm on a non-flat reading
  714.             #     (the market moved). 'last_bucket' guards against re-alerting
  715.             #     the very same candle.
  716.             # Candles with < 2 trades never reach here: get_flat_candles_by_group
  717.             # excludes them, so a single-trade window is treated as "no data".
  718.             fi = info['flat']
  719.             bucket = value['last_bucket']
  720.             if int(time.time()) - fi['ts'] > 60 * 60 * 2:  # 2h cooldown
  721.                 if value['flat'] and fi['status'] and fi['last_bucket'] != bucket:
  722.                     try:
  723.                         await bot.send_message(
  724.                             text=f"{symbol}:\n--- FLAT ---\n~~~ last two 15m candles closed flat (open == close, ≥2 trades each)",
  725.                             chat_id=chat_id
  726.                         )
  727.                     except Exception as e:
  728.                         print('exception', e)
  729.                     fi['last_bucket'] = bucket
  730.                     fi['status'] = False
  731.                     fi['ts'] = int(time.time())
  732.                 elif not value['flat']:
  733.                     # market moved — re-arm for the next flat episode
  734.                     fi['status'] = True
  735.                     fi['last_bucket'] = bucket
  736.  
  737.     except Exception as e:
  738.         print(f'EXCEPTION: manage_alarm {value} {alarm_type} {e}')
  739.  
  740.  
  741. async def book_checker(bot):
  742.     await dbReader.init_pool()
  743.  
  744.     for group, chat_id in chats_mapping.items():
  745.         if debug and chat_id != '-1002388231434': # debug mock
  746.             continue
  747.         try:
  748.             books = await dbReader.get_book_by_group(group)
  749.         except Exception as e:
  750.             print(f'EXCEPTION: book_checker {books} - {e}')
  751.             continue
  752.         for book in books:
  753.             ts = int(datetime.fromisoformat(book['time']).timestamp())
  754.             if int(time.time()) - ts <= 60:
  755.                 await manage_alarm(bot, chat_id, book, 'spread')
  756.             else:
  757.                 print('NO ACTUAL BOOK', int(time.time()), group, book)
  758.  
  759.  
  760. async def flat_checker(bot):
  761.     await dbReader.init_pool()
  762.  
  763.     for group, chat_id in chats_mapping.items():
  764.         if debug and chat_id != '-1002388231434':  # debug mock
  765.             continue
  766.         try:
  767.             candles = await dbReader.get_flat_candles_by_group(group)
  768.         except Exception as e:
  769.             print(f'EXCEPTION: flat_checker {group} - {e}')
  770.             continue
  771.         for candle in candles:
  772.             await manage_alarm(bot, chat_id, candle, 'flat')
  773.  
  774.  
  775. async def status_checker(bot):
  776.     await dbReader.init_pool()
  777.     statuses = await dbReader.get_last_statuses()
  778.  
  779.     for group, chat_id in chats_mapping.items():
  780.         if debug and chat_id != '-1002388231434': # debug mock
  781.             continue
  782.         if group not in dbReader.symbols_id_in_group:
  783.             continue
  784.         for symbol in dbReader.symbols_id_in_group[group]:
  785.             status_v, status_m = None, None
  786.            
  787.             if symbol not in dbReader.symbol_id_to_ticket:
  788.                 continue
  789.  
  790.             if symbol in statuses:
  791.                 for status in statuses[symbol]:
  792.                     if status['strategy'] == 'M':
  793.                         status_m = status['status']
  794.                     if status['strategy'] == 'V':
  795.                         status_v = status['status']
  796.  
  797.             symbol = dbReader.symbol_id_to_ticket[symbol]
  798.             symbol_v = f'{symbol} Volume Bot'
  799.             symbol_m = f'{symbol} Market Making Bot'
  800.  
  801.             await manage_alarm(bot, chat_id, {'symbol': symbol_v, 'status': status_v}, 'aliveness')
  802.             await manage_alarm(bot, chat_id, {'symbol': symbol_m, 'status': status_m}, 'aliveness')
  803.  
  804. ###############################################################
  805. ###############################################################
  806.  
  807.  
  808. ###############################################################
  809. ##################### LOW BALANCE ALERTER #####################
  810. ###############################################################
  811.  
  812. LOW_BALANCE_THRESHOLD_USDT = 150
  813.  
  814. STRATEGY_LABELS = {
  815.     'V': 'Volume',
  816.     'M': 'Market Making',
  817.     'T': 'Treasury Building',
  818.     'W': 'BBO',
  819. }
  820.  
  821.  
  822. async def _current_price(reader, symbol_id):
  823.     """Best-effort current price: book midpoint, fallback to last public trade."""
  824.     rows = await reader.execute_select(f"""
  825.        SELECT ask_price, bid_price
  826.        FROM book_{symbol_id}
  827.        WHERE ask_price > 0 AND bid_price > 0
  828.        ORDER BY time DESC
  829.        LIMIT 1
  830.    """)
  831.     if rows and rows[0] and rows[0][0]:
  832.         return (float(rows[0][0]) + float(rows[0][1])) / 2
  833.     rows = await reader.execute_select(f"""
  834.        SELECT price
  835.        FROM public_trade_{symbol_id}
  836.        WHERE price > 0
  837.        ORDER BY time DESC
  838.        LIMIT 1
  839.    """)
  840.     if rows and rows[0]:
  841.         return float(rows[0][0])
  842.     return 0
  843.  
  844.  
  845. def _format_low_balance_alert(strategy, market, token, base, quote):
  846.     strat_label = STRATEGY_LABELS.get(strategy, strategy)
  847.     return (
  848.         f"⚠️ <b>ATTENTION on balances</b>\n"
  849.         f"<b>Strategy:</b> {strat_label}\n"
  850.         f"<b>Market:</b> {market}\n"
  851.         f"<b>Current balances:</b>\n"
  852.         f"{token} — <code>{round(float(base), 4)}</code>\n"
  853.         f"USDT — <code>{round(float(quote), 2)}</code>"
  854.     )
  855.  
  856.  
  857. async def low_balance_alerter(bot):
  858.     """Daily check: send an alert to each chat for every (symbol, strategy)
  859.    where quote + base*price < LOW_BALANCE_THRESHOLD_USDT."""
  860.     try:
  861.         await dbReader.init_pool()
  862.         await dbSideCashoutReader.init_pool(side=True)
  863.         await dbReader.update_symbols_list()
  864.         await dbSideCashoutReader.update_symbols_list(side=True)
  865.     except Exception as e:
  866.         print(f'[low_balance_alerter] init failed: {e}')
  867.         return
  868.  
  869.     # Walks the same chats as DEAD / spread alarms — chats_mapping
  870.     # ({token: chat_id}, filled by /start). Side cashout tokens are skipped.
  871.     for token, chat_id in chats_mapping.items():
  872.         if '.' in token:
  873.             continue
  874.         symbols = dbReader.symbols_id_in_group.get(token, [])
  875.         alert_blocks = []
  876.  
  877.         for symbol in symbols:
  878.             market = dbReader.symbol_id_to_ticket.get(symbol, str(symbol))
  879.             base_token = market.split('/')[0]
  880.  
  881.             try:
  882.                 price = await _current_price(dbReader, symbol)
  883.             except Exception as e:
  884.                 print(f'[low_balance_alerter] price fetch failed {symbol}: {e}')
  885.                 price = 0
  886.  
  887.             # Only V (Volume) and M (Market Making) balances are tracked here;
  888.             # treasury / cashout strategies are intentionally skipped.
  889.             rows = await dbReader.execute_select(f"""
  890.                SELECT DISTINCT ON (strategy) strategy, base, quote
  891.                FROM balance_{symbol}
  892.                WHERE strategy IN ('V', 'M')
  893.                  AND time > NOW() - interval '7 days'
  894.                ORDER BY strategy, time DESC
  895.            """)
  896.             if not rows:
  897.                 continue
  898.             for strat, base_v, quote_v in rows:
  899.                 base = float(base_v)
  900.                 quote = float(quote_v)
  901.                 total = quote + base * price
  902.                 if total < LOW_BALANCE_THRESHOLD_USDT:
  903.                     alert_blocks.append(
  904.                         _format_low_balance_alert(strat, market, base_token, base, quote)
  905.                     )
  906.  
  907.         if not alert_blocks:
  908.             continue
  909.  
  910.         text = '\n\n'.join(alert_blocks)
  911.         try:
  912.             for chunk in split_html_safe(text):
  913.                 await bot.send_message(chat_id, chunk, parse_mode=ParseMode.HTML)
  914.         except Exception as e:
  915.             print(f'[low_balance_alerter] send failed chat={chat_id}: {e}')
  916.  
  917.  
  918. @dp.message(F.text)
  919. async def bad_message(message: Message): # нельзя поднимать эту функцию выше!
  920.     pass
  921.  
  922.  
  923. async def main():
  924.     scheduler = AsyncIOScheduler()
  925.     if not debug:
  926.         scheduler.add_job(book_checker, "interval", seconds=75, args=[bot])
  927.         scheduler.add_job(status_checker, "interval", seconds=60, args=[bot])
  928.         scheduler.add_job(flat_checker, "interval", seconds=300, args=[bot])
  929.         scheduler.add_job(
  930.             low_balance_alerter, "cron",
  931.             hour=15, minute=30, timezone="UTC", args=[bot],
  932.         )
  933.     scheduler.start()
  934.  
  935.     await dp.start_polling(bot)
  936.     await dbReader.close_connection()
  937.  
  938. if __name__ == "__main__":
  939.     asyncio.run(main())
  940.  
Advertisement
Add Comment
Please, Sign In to add comment