Marmennz / Top Tokens with Markers – Extended Alerts

// ==UserScript==
// @name         Top Tokens with Markers – Extended Alerts
// @namespace    http://tampermonkey.net/
// @version      1.21-mod
// @description  Расширенные уведомления с двумя порогами объёма и тремя индикаторами (или прочерками) в таблице. Также сохраняет самый волатильный токен (топ-1 по объёму) в localStorage.
// @author       Mars
// @match        https://photon-sol.tinyastro.io/en/trending*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const NOTIFIED_TOKENS_KEY = 'notifiedTokenAddresses';
    const TELEGRAM_BOT_TOKEN = '7871085451:AAGMRpygqWTSmYNsopvarYfspbiyxJ1S6pQ';
    const TELEGRAM_CHAT_ID = '-4792720092';

    const VOLUME_THRESHOLD_500K = 500000;
    const VOLUME_THRESHOLD_2M = 1200000;
    // Минимальный объём для уведомления по росту
    const VOLUME_MIN_GROWTH = 200000;

    // Новый диапазон ликвидности для уведомлений
    const LIQUIDITY_THRESHOLD_MIN = 100000;
    const LIQUIDITY_THRESHOLD_MAX = 700000;

    const PRICE_JUMP_MIN = 150;
    const PRICE_JUMP_MAX = 400;
    const PRICE_DROP_THRESHOLD = -50;
    const PRICE_DROP_30M_THRESHOLD = -50;

    const TRIGGER_SYMBOLS = {
        vol2M: '2',
        vol500: '5',
        growth: '↑'
    };

    // Здесь будем хранить массив триггеров для каждого токена
    const notificationTriggers = {};
    const newTokensWithHighLiquidity = new Set();

    // Для контроля интервала между уведомлениями о росте (10 минут)
    const GROWTH_COOLDOWN = 10 * 60 * 1000;
    const lastGrowthAlertTimes = {};

    function getNotifiedTokens() {
        const notified = JSON.parse(localStorage.getItem(NOTIFIED_TOKENS_KEY)) || [];
        const oneDayInMilliseconds = 24 * 60 * 60 * 1000;
        const currentTime = Date.now();

        return notified.filter(token => (currentTime - token.timestamp) <= oneDayInMilliseconds);
    }

    function addNotifiedToken(address) {
        let notified = getNotifiedTokens();
        const currentTime = Date.now();
        notified = notified.filter(token => currentTime - token.timestamp <= 24 * 60 * 60 * 1000);

        notified.push({ address, timestamp: currentTime });
        localStorage.setItem(NOTIFIED_TOKENS_KEY, JSON.stringify(notified));
    }

    // Для объёмных уведомлений – ростовые будут отправляться без этой проверки
    const notifiedTokens = new Set(getNotifiedTokens().map(token => token.address));

    function sendTelegramMessage(message) {
        const url = `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`;
        const params = new URLSearchParams();
        params.append('chat_id', TELEGRAM_CHAT_ID);
        params.append('text', message);

        fetch(url, {
            method: 'POST',
            body: params
        })
        .then(response => response.json())
        .then(data => {
            if (!data.ok) {
                console.error('Ошибка отправки в Telegram:', data);
            } else {
                console.log('Отправлено в Telegram:', message);
            }
        })
        .catch(error => console.error('Ошибка при запросе в Telegram:', error));
    }

    function formatTokenMessage(token, reason) {
        const { tokenAddress, symbol, volume, cur_liq, gains } = token;
        const liquidity = cur_liq?.usd ? formatNumber(cur_liq.usd) : '–';
        const volumeFormatted = formatNumber(volume);
        const growth5m = gains?.["5m"] ? gains["5m"].toFixed(2) + "%" : "–";
        const lpLink = `https://photon-sol.tinyastro.io/en/lp/${tokenAddress}?handle=727809998130fdf6f5aa6`;

        if (reason.includes("Рост")) {
            return `🚀 ${symbol} вырос на ${growth5m} за 5 минут!\n\n` +
                   `💰 Volume: ${volumeFormatted} USD\n` +
                   `📊 Liquidity: ${liquidity} USD\n\n` +
                   `${lpLink}`;
        } else {
            const emoji = volume > VOLUME_THRESHOLD_2M ? "🔥🔥" : "👁️";
            return `${emoji} Volume ${volumeFormatted} USD on ${symbol}!\n\n` +
                `💰 Volume: ${volumeFormatted} USD\n` +
                `📊 Liquidity: ${liquidity} USD\n` +
                `📈 Pumps for last 5 mins: ${growth5m}\n\n` +
                `${lpLink}`;
        }
    }

    function formatNumber(number) {
        const value = Number(number);
        if (value >= 1000000) {
            return (value / 1000000).toFixed(1) + 'M';
        } else if (value >= 1000) {
            return Math.floor(value / 1000) + 'K';
        } else {
            return value.toFixed(0);
        }
    }

    function fetchTokens() {
        const url = 'https://photon-sol.tinyastro.io/api/trending?dexes=raydium&period=1m&usd_liq_from=10000&usd_liq_to=2000000';
        fetch(url)
            .then(response => response.json())
            .then(data => {
                if (!data || !data.data) return;

                let tokens = data.data.filter(token => {
                    const hasMarker = token.fromPump || token.fromMoonshot || (token.fromMemeDex && token.fromMemeDex !== false);
                    const hasLiquidity = token.cur_liq?.usd && token.cur_liq.usd >= 10000 && token.cur_liq.usd <= 2000000;
                    return hasMarker && hasLiquidity;
                });

            // Сортировка токенов по объёму (от большего к меньшему)
            tokens.sort((a, b) => b.volume - a.volume);

            // Сохраняем топ-1 токен (самый волатильный по объёму) в localStorage,
            // используя поле "address" из API
            if (tokens.length > 0) {
                const topTokenAddress = tokens[0].address;
                localStorage.setItem('topToken', JSON.stringify({ address: topTokenAddress }));
            }

                const now = Date.now();

                tokens.forEach(token => {
                    const tokenAddress = token.tokenAddress;
                    const volume = Number(token.volume);
                    const liquidity = token.cur_liq?.usd ? Number(token.cur_liq.usd) : 0;

                    const growth1m = token.gains?.["1m"] || 0;
                    const growth5m = token.gains?.["5m"] || 0;
                    const growth30m = token.gains?.["30m"] || 0;
                    const growth1h = token.gains?.["1h"] || 0;
                    const timestamp = token.timestamp ? new Date(token.timestamp * 1000) : null;

                    // Пропуск токенов с сильным падением цены
                    if (growth1m <= PRICE_DROP_THRESHOLD ||
                        growth5m <= PRICE_DROP_THRESHOLD ||
                        growth30m <= PRICE_DROP_30M_THRESHOLD ||
                        growth1h <= PRICE_DROP_THRESHOLD) {
                        return;
                    }

                    // Функция для добавления триггера в массив уведомлений для токена
                    function addTrigger(trigger) {
                        if (!notificationTriggers[tokenAddress]) {
                            notificationTriggers[tokenAddress] = [];
                        }
                        if (!notificationTriggers[tokenAddress].includes(trigger)) {
                            notificationTriggers[tokenAddress].push(trigger);
                        }
                    }

                    // Уведомления по росту цены (price jump)
                    if (liquidity >= LIQUIDITY_THRESHOLD_MIN && liquidity <= LIQUIDITY_THRESHOLD_MAX) {
                        if (growth5m >= PRICE_JUMP_MIN && growth5m <= PRICE_JUMP_MAX && volume >= VOLUME_MIN_GROWTH) {
                            if (!lastGrowthAlertTimes[tokenAddress] || (now - lastGrowthAlertTimes[tokenAddress] >= GROWTH_COOLDOWN)) {
                                sendTelegramMessage(formatTokenMessage(token, `Рост ${growth5m.toFixed(2)}% за 5 минут`));
                                lastGrowthAlertTimes[tokenAddress] = now;
                                addTrigger('growth');
                            }
                        }
                    }

                    // Уведомления по объёму – отдельно для каждого порога
                    if (liquidity >= LIQUIDITY_THRESHOLD_MIN && liquidity <= LIQUIDITY_THRESHOLD_MAX) {
                        const tokenKey500 = tokenAddress + '_500K';
                        const tokenKey2M = tokenAddress + '_2M';

                        if (volume > VOLUME_THRESHOLD_500K && !notifiedTokens.has(tokenKey500)) {
                            sendTelegramMessage(formatTokenMessage(token, `Объём ${volume.toFixed(2)} USD`));
                            notifiedTokens.add(tokenKey500);
                            addNotifiedToken(tokenKey500);
                            addTrigger('vol500');
                        }
                        if (volume > VOLUME_THRESHOLD_2M && !notifiedTokens.has(tokenKey2M)) {
                            sendTelegramMessage(formatTokenMessage(token, `Объём ${volume.toFixed(2)} USD`));
                            notifiedTokens.add(tokenKey2M);
                            addNotifiedToken(tokenKey2M);
                            addTrigger('vol2M');
                        }
                    }

                    // Трекинг новых токенов с высокой ликвидностью
                    if (liquidity >= 700000 && timestamp && (new Date() - timestamp <= 10 * 60 * 1000)) {
                        newTokensWithHighLiquidity.add(tokenAddress);
                    }
                });

                updateTable(tokens);
            })
            .catch(error => console.error('Ошибка при получении токенов:', error));
    }

    // Формируем ячейку таблицы с 3 столбцами-индикаторами: для vol2M, vol500 и growth.
    // Если уведомления не было, выводится прочерк "–".
    function formatNotificationCell(tokenAddress) {
        const triggers = ['vol2M', 'vol500', 'growth'];
        return triggers.map(trigger => {
            return (notificationTriggers[tokenAddress] && notificationTriggers[tokenAddress].includes(trigger))
                   ? TRIGGER_SYMBOLS[trigger]
                   : '–';
        }).join(' ');
    }

    function updateTable(tokens) {
        const tbody = document.getElementById('token-tbody');
        tbody.innerHTML = '';

        tokens.forEach(token => {
            const tr = document.createElement('tr');
            const change5m = token.gains?.["5m"] ? token.gains["5m"].toFixed(2) + "%" : "–";
            const notificationStatus = formatNotificationCell(token.tokenAddress);

            const liquidity = token.cur_liq?.usd ? Number(token.cur_liq.usd) : 0;
            const volume = Number(token.volume);

            let rowStyle = '';
            const drop5m = token.gains?.["5m"] || 0;
            const drop30m = token.gains?.["30m"] || 0;
            const isDrop = (drop5m <= -50 || drop30m <= -50);

            if (!isDrop) {
                if (volume > VOLUME_THRESHOLD_2M) {
                    rowStyle = 'background-color: #ff6347;'; // Красный для объёма > 1.2M
                } else if (volume > VOLUME_THRESHOLD_500K) {
                    rowStyle = 'background-color: #6495ed;'; // Синий для объёма > 500K
                }
            }
            if (!rowStyle && liquidity > LIQUIDITY_THRESHOLD_MIN) {
                rowStyle = 'background-color: #dbdbdb;';
            }

            tr.innerHTML = `
                <td style="color: black;">${token.symbol}</td>
                <td style="color: black;">${formatNumber(token.volume)}</td>
                <td style="color: black;">${formatNumber(token.cur_liq?.usd)}</td>
                <td style="color: black;">${change5m}</td>
                <td style="color: black; text-align: center;">${notificationStatus}</td>
            `;
            tr.style = rowStyle;
            tbody.appendChild(tr);
        });
    }

    const container = document.createElement('div');
    container.style.position = 'fixed';
    container.style.bottom = '10px';
    container.style.left = '10px';
    container.style.backgroundColor = 'white';
    container.style.borderRadius = '8px';
    container.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.2)';
    container.style.zIndex = '9999';
    container.style.padding = '10px';

    container.innerHTML = `
        <table border="1" style="width:100%; margin-top:10px;">
            <thead>
                <tr>
                    <th style="color: black;">Symbol</th>
                    <th style="color: black;">Volume</th>
                    <th style="color: black;">Liquidity</th>
                    <th style="color: black;">5 min</th>
                    <th style="color: black;">TG</th>
                </tr>
            </thead>
            <tbody id="token-tbody"></tbody>
        </table>
    `;

    document.body.appendChild(container);

    fetchTokens();
    setInterval(fetchTokens, 5000);

    // Глобальная функция для очистки внутреннего хранилища уведомлений и localStorage.
    window.clearNotifiedTokens = function() {
        notifiedTokens.clear();
        localStorage.removeItem(NOTIFIED_TOKENS_KEY);
        // Сбрасываем триггеры уведомлений для таблицы
        for (const key in notificationTriggers) {
            if (notificationTriggers.hasOwnProperty(key)) {
                notificationTriggers[key] = [];
            }
        }
        console.log('notifiedTokens, notificationTriggers и localStorage очищены');
    }
})();