Перейти к основному содержимому

ТЗ: Токен-аналитика (CoinGecko + AI) в модуле aiAnalytics

Метаданные

ПараметрЗначение
Дата создания2026-07-12
Дата последнего изменения2026-07-12
Статус апрува✅ Одобрено
Дата апрува2026-07-12

1. Назначение

Модуль aiAnalytics сегодня умеет две вещи: обзор рынка (GET /ai-analytics/market/outlook) и разбор пула ликвидности (POST /ai-analytics/pool/analytics). Токенов в нём нет.

Задача — достроить ветку токенов, чтобы фронт мог показать страницу каталога токенов, карточку конкретного токена со всей информацией, которую отдаёт CoinGecko, поиск, новости — и по запросу получить AI-аналитику (summary) по токену.

Backend-only. Референс поведения — /Users/vovilonn/Documents/work/defilab/zenfi (apps/backend/src/markets/).


2. Функциональные требования

2.1. Пользовательские сценарии

  1. Пользователь открывает страницу аналитики → видит список токенов с ценой, изменением за 1ч/24ч/7д/30д, капитализацией, объёмом и мини-графиком; листает страницы; сортирует по капитализации, объёму или изменению цены.
  2. Пользователь вводит запрос в поиск → видит подходящие токены и переходит в любой из них.
  3. Пользователь открывает карточку токена → видит подробные рыночные метрики, описание проекта, ссылку на сайт и whitepaper, соцсети, репозитории, категории, ATH/ATL, supply, метрики сообщества и разработки.
  4. Пользователь смотрит график цены за выбранный период (1д…365д).
  5. Пользователь читает новости, релевантные токену.
  6. Пользователь нажимает «AI-аналитика» → получает structured-summary: вердикт, ключевые наблюдения, риски, сценарии и готовый Markdown для рендера.

2.2. Бизнес-логика

Эндпоинты

Все — @UseGuards(JwtAuthGuard) + @ApiBearerAuth(), @ApiTags('AI Analytics'). Каждый ответ несёт провенанс данных: cached, stale, fetchedAt.

МетодПутьИсточникTTL
GET/ai-analytics/tokensCoinGecko /coins/markets60 c
GET/ai-analytics/tokens/searchCoinGecko /search300 c
GET/ai-analytics/tokens/trendingCoinGecko /search/trending300 c
GET/ai-analytics/tokens/:idCoinGecko /coins/{id}300 c
GET/ai-analytics/tokens/:id/chartCoinGecko /coins/{id}/market_chart300 c
GET/ai-analytics/tokens/:id/newsCryptoCompare (общий фид)600 c
GET/ai-analytics/market/globalCoinGecko /global + alternative.me60 c
POST/ai-analytics/tokens/:id/analyticsLLM3600 c

/search и /trending объявляются до /:id, иначе Nest сматчит их как id.

Ключевые правила

  1. Исходящий запрос списка фиксирован сервером: price_change_percentage=1h,24h,7d,30d — константа, клиентом не управляется. Без неё CoinGecko вообще не возвращает поля 1ч/7д/30д.
  2. Валюта только USD. Поля DTO названы *Usd, /coins/{id} валюту не принимает. Параметр vsCurrency в контракт не выносится.
  3. Сортировка по изменению цены — в памяти. CoinGecko умеет order только по капитализации, объёму и id. При sort=change24h (и аналогах) сервис берёт один кешированный пул топ-250 по капитализации, сортирует и пагинирует в памяти. Ограничение документируется: ранжирование идёт только внутри топ-250.
  4. Новости — общий фид + матчинг в памяти. Пер-токенного фильтра у CryptoCompare нет: categories валидируется по его собственному фиксированному справочнику, и для длинного хвоста токенов категории не существует. Токен матчится по categoriestagstitle элемента ленты. Ноль совпадений → общая лента с флагом matchedBySymbol: false.
  5. Правило детерминизма. Каждое число, которое видит пользователь, считается в коде и передаётся модели дословно. Модель пишет только прозу.
  6. Фолбэки не кешируются. Ни AI-фолбэк, ни пустая лента новостей не попадают в кеш — следующий запрос повторит попытку.
  7. Постоянная ошибка апстрима не подменяется stale-данными. Право на stale имеет только ретраибельная корзина (429/5xx/сеть).

AI-аналитика

Модель получает: цену и изменения 1ч/24ч/7д/30д/1г, капитализацию и ранг, FDV, объём 24ч, отношение объёма к капитализации, supply (circulating/total/max), ATH/ATL и расстояние до них, RSI(14), волатильность 7д, тренд, категории, описание проекта, метрики разработки и сообщества, sentiment-голоса, Fear&Greed, доминацию BTC, до 5 заголовков новостей.

Ответ (строгий JSON, валидируется и нормализуется в коде):

{
"verdict": "bullish|bearish|neutral",
"summary": "2–3 предложения",
"highlights": ["3–5 ключевых наблюдений"],
"risks": ["2–4 риска"],
"scenarios": [{ "trigger": "…", "direction": "up|down|sideways", "range": "3–6%", "note": "…" }],
"disclaimer": "… Не является финансовой рекомендацией."
}

Markdown рендерится в коде из этого же JSON, вторым вызовом LLM не ходим: один вызов модели, ноль расхождений между analysis и markdown.

2.3. UI/UX требования

Не применимо (backend-only). Ответ AI-эндпоинта содержит и машинные поля, и готовый Markdown для прямого рендера.


3. Техническая реализация

3.1. Архитектура

Подмодуль внутри aiAnalytics + единый HTTP-клиент CoinGecko.

apps/backend/src/aiAnalytics/
token-analytics.controller.ts # роуты /ai-analytics/tokens + /ai-analytics/market/global
config/coingecko.config.ts # DI-токен COINGECKO_CONFIG: base URL, заголовки, TTL, прокси, таймауты
services/
proxy-pool.service.ts # пул «выходов» (прокси + DIRECT) с cooldown
coingecko.client.ts # кеш-конверт + прокси + retry + stale-on-error
token-market.service.ts # list/search/trending/detail/chart → DTO
token-news.service.ts # общий фид CryptoCompare + матчинг по токену
token-analytics.service.ts # сбор контекста + LLM → JSON + Markdown
market-data.service.ts # МИГРИРУЕТ на CoinGeckoClient
token-data.service.ts # МИГРИРУЕТ на CoinGeckoClient
prompts/token-analytics.prompt.ts
dto/token-analytics.dto.ts
utils/
indicators.util.ts # RSI(14) Уайлдера, волатильность, тренд
markdown-render.util.ts # детерминированный рендер Markdown из AI-JSON
coingecko-map.util.ts # чистые мапперы сырых ответов CoinGecko → DTO

Транспорт — axios + https-proxy-agent (весь бэкенд на axios; undici, как в zenfi, не вводим).

3.2. Описание технической реализации

CoinGeckoClient

Единственная точка выхода в CoinGecko. Возвращает провенанс, а не голое значение:

get<T>(path, params, opts: { cacheKey: string; ttlSec: number; staleTtlSec: number })
: Promise<{ data: T; fetchedAt: number; stale: boolean }>

Порядок работы:

  1. Резолв базы и заголовковconfig/coingecko.config.ts): COINGECKO_API_PLAN=demohttps://api.coingecko.com/api/v3 + x-cg-demo-api-key; prohttps://pro-api.coingecko.com/api/v3 + x-cg-pro-api-key. COINGECKO_URL перекрывает хост. Pro-ключ на demo-хосте отвергается — поэтому план обязан управлять хостом, а не только заголовком.

  2. Конверт из Redis{ data, fetchedAt }, Redis-TTL равен staleTtlSec. Свежий, если now - fetchedAt < ttlSec.

  3. In-flight дедуп по cacheKey; retry живёт внутри дедуплицированного промиса.

  4. Fetch через выход из ProxyPoolService.next(), с жёстким дедлайном: axios timeout и AbortController. Одного timeout недостаточно — за прокси-агентом он не покрывает фазу установления соединения, и промис может не зарезолвиться никогда, заклинив дедуп.

  5. Закрытая классификация ошибки (ни одна не выходит неклассифицированной):

    КорзинаCooldown выходаРетрайStaleНаружу
    429, 5xx, сеть, таймаутдададаstale или 503
    404нетнетнет404 token_not_found
    401 + 10012нетнетнет400 days_out_of_range
    401 + 10002/10005нетнетнет503 + лог error
    прочий 4xxнетнетнет502
  6. Stale-on-error — только для ретраибельной корзины. Конверта нет → 503.

ProxyPoolService

Пул выходов: [...прокси, DIRECT], где DIRECT — псевдовыход с agent: undefined. Прямой запрос с продового IP управляется той же машинерией cooldown, а «пул пуст» и «все прокси на cooldown» перестают быть неразличимы.

reportSuccess(key, pickedAt) снимает cooldown, только если он был поставлен до выдачи выхода: иначе поздний успех медленного запроса воскресил бы выход, который параллельный запрос только что убил.

Источник списка — getEnv('MARKET_PROXIES') (CSV), а не JSON-файл относительно cwd: в продовом контейнере другой cwd (turborepo prune), и файла там бы не было. getEnv — существующая конвенция репозитория (/run/secrets/{name}process.env).

Кеш

Только Redis (CACHE_MANAGER), без новых таблиц БД. TTL передаётся объектом: cache.set(key, value, { ttl }).

Stale-окно зависит от кардинальности ключа — Redis общий с auth/session, и нельзя держать сутки на ключах, которых клиент может породить сколько угодно:

Класс ключаКардинальностьStale-окно
/global, /search/trending, лента новостей, дефолтная страница спискафиксированная86400 c
/coins/{id}, чарт, /search?query=, произвольные страницы списказадаётся клиентом900 c

Ключи версионируются (ai:cg:v1:*) и никогда не переиспользуют существующие. При миграции MarketDataService/TokenDataService значение под ключом меняет форму дважды: маппленое → сырое тело CoinGecko, и голое → конверт. При rolling-деплое старые и новые поды делят один Redis — переиспользование ключа означало бы чтение чужой формы.

Изменения в существующем коде

  1. providers/openai.provider.ts — добавить timeout: AI_REQUEST_TIMEOUT_MS (30000) и maxRetries: 0. Сейчас клиент создаётся без них, а дефолты SDK — 10 минут таймаута × 2 ретрая, то есть зависший прокси держит запрос до получаса, и обещание «LLM недоступен → 200 с фолбэком» недостижимо. OPENAI_CLIENT — общий провайдер, поэтому правка распространяется и на market/outlook, и на pool/analytics: у них сегодня та же проблема.
  2. services/market-data.service.ts, services/token-data.service.ts — миграция на CoinGeckoClient; убирается дублирование cgHeaders/cacheGet/cacheSet; чинится дефект, из-за которого getGlobal()/getCoins() берут TTL из AI_ANALYTICS_FNG_TTL_SEC (переменной индекса страха и жадности).
  3. Радиус поражения миграции — оба сервиса конструируются вне DI в трёх местах, все три правятся вместе: market-data.service.spec.ts, token-data.service.spec.ts, scripts/ai-analytics-live-check.ts.
  4. app.module.ts — новые env в Joi-схему. .env.example — те же переменные.
  5. package.jsonhttps-proxy-agent в dependencies (сейчас только транзитивно).

Новые переменные окружения

ПеременнаяНазначениеДефолт
COINGECKO_API_PLANdemo | pro — base URL и заголовокdemo
COINGECKO_MAX_RETRIESпопыток при 429/5xx/сети3
COINGECKO_TIMEOUT_MSдедлайн одной попытки15000
MARKET_PROXIESCSV egress-прокси (через getEnv)— (пусто → только DIRECT)
MARKET_PROXY_COOLDOWN_MScooldown упавшего выхода60000
CRYPTOCOMPARE_API_KEYключ ленты новостей
AI_REQUEST_TIMEOUT_MSтаймаут OpenAI-клиента30000
AI_ANALYTICS_MARKET_TTL_SECTTL списка и /global60
AI_ANALYTICS_TOKEN_DETAIL_TTL_SECTTL карточки и чарта300
AI_ANALYTICS_SEARCH_TTL_SECTTL поиска и trending300
AI_ANALYTICS_NEWS_TTL_SECTTL ленты новостей600
AI_ANALYTICS_TOKEN_AI_TTL_SECTTL AI-аналитики3600
AI_ANALYTICS_STALE_TTL_SECstale-окно для ключей фиксированной кардинальности86400
AI_ANALYTICS_STALE_SHORT_TTL_SECstale-окно для ключей, задаваемых клиентом900

Существующие и переиспользуемые: COINGECKO_URL, COINGECKO_API_KEY, OPENAI_BASE_URL, OPENAI_API_KEY, AI_MODEL, AI_ANALYTICS_FNG_TTL_SEC.

Авторизация

Все роуты — @UseGuards(JwtAuthGuard) + @ApiBearerAuth(), без пейволла. В проекте роут публичен по умолчанию, поэтому защита ставится явно.

Тестирование

  1. Юниты: мапперы CoinGecko → DTO (полный ответ, все null, отсутствующие вложенные объекты, форматированные строки в trending); indicators.util (RSI(14) на канонической серии Уайлдера, сглаживание, только рост → 100, плоский ряд → 50, короткий ряд → null); промпт-билдер; нормализатор AI-ответа (в т.ч. дописывание дисклеймера); Markdown-рендерер; ProxyPoolService (round-robin, cooldown, pickedAt-гвард, пустой список → остаётся DIRECT).
  2. Сервисные: CoinGeckoClient (cache-hit без похода наружу; 429 → cooldown → retry; ретраи исчерпаны → stale; stale нет → 503; 404 → 404 без cooldown; 401/10012 → 400 и не подменяется stale, даже когда конверт есть; прочий 4xx → 502; in-flight дедуп); TokenMarketService (маппинг, сортировка в памяти); TokenNewsService (матчинг, Data не массив → пустой список); TokenAnalyticsService (LLM ок / LLM упал → фолбэк не кешируется / 404 / дедуп).
  3. Контроллер (supertest + overrideGuard): 200 на всех роутах; 403 при отказе guard; 400 на кривом id, пустом query, недопустимом days, переполненном ids; 404 на неизвестном токене; /search и /trending не матчатся как /:id.
  4. Живой прогон: scripts/token-analytics-live-check.ts против реального CoinGecko и реального AI-прокси. Режимы list | search | trending | detail | chart | news | ai | all.

Конфиг инъектится, не читается из process.env на уровне модуля. Существующие сервисы делают const CG = process.env.COINGECKO_URL || ... при импорте, из-за чего подмена env в тестах на них не действует. Новый код получает конфиг через DI-токен COINGECKO_CONFIG.


4. Проблемы и компромиссы

4.1. Известные ограничения

  1. Лимиты CoinGecko на keyless-тарифе.
    • Проверено вживую: 429 уже на 6-м запросе подряд.
    • Решение — ротация egress-прокси + Redis-кеш + stale-on-error. Пока MARKET_PROXIES пуст, всё держится на кеше.
  2. Новости матчатся по токену эвристически.
    • У CryptoCompare нет пер-токенного фильтра: categories — фиксированный справочник, для длинного хвоста токенов категории не существует.
    • Влияние: для мелких токенов совпадений может не быть → отдаётся общая лента с matchedBySymbol: false.
  3. Поиск не отдаёт цены. Ограничение CoinGecko /search; максимум 25 результатов. Фронт добирает цены через GET /ai-analytics/tokens?ids=.
  4. Сортировка по изменению цены ранжирует только внутри топ-250 по капитализации. CoinGecko не умеет такой order.
  5. График ограничен 365 днями. days=max даёт 401 (error_code 10012); бесплатный Demo-ключ этого не снимает.
  6. In-flight дедуп внутрипроцессный. При нескольких репликах одновременная просрочка ключа даёт по одному апстрим-запросу на реплику.
  7. Rate-limit на стороне нашего API отсутствует@nestjs/throttler в бэкенде нет вообще. Защита от абьюза: JWT + кеш + дедуп.

4.2. Технический долг

  • Per-user rate-limit (Redis, 429) на LLM-эндпоинты — общий с существующим модулем.
  • Персистентное хранение истории AI-аналитик (сейчас Redis-only, после рестарта Redis всё генерируется заново).
  • Кросс-процессный lock на прогрев кеша.
  • /coins/{id}/tickers — площадки, где торгуется токен.
  • Engineering-документация модуля aiAnalytics — не написана до сих пор; закрывается этой задачей.

4.3. Риски

РискМитигация
CoinGecko блокирует продовый IP по лимитамПул прокси + cooldown на DIRECT + stale-on-error; при отсутствии прокси — агрессивный кеш
CryptoCompare-ключ исчерпан / отозванЛента деградирует в пустой список; карточка токена и AI-аналитика продолжают работать
Локальный AI-прокси недоступенtimeout: 30 c, maxRetries: 0, детерминированный фолбэк, ответ 200
Разрастание Redis-keyspace (общий с auth/session)ids ≤ 50, короткое stale-окно для ключей клиентской кардинальности, версионированные ключи
Изменение схемы CoinGeckoВсе поля DTO nullable; мапперы — чистые функции с тестами на отсутствующие вложенные объекты

5. Вопросы на дополнительное обсуждение

Все вопросы, блокировавшие проектирование, закрыты:

  • Лимиты CoinGecko → ротация прокси без ключа (решение заказчика).
  • Источник новостей → CryptoCompare; ключ выдан.
  • Хранение AI-результатов → только Redis с TTL, без миграций.
  • Формат AI-ответа → структурный JSON + Markdown.
  • Список прокси — пока не предоставлен; до этого момента живой трафик держится только на кеше.

6. План реализации

6.1. Этапы разработки

  • Этап 1: Чистые утилиты и конфиг (indicators.util, coingecko-map.util, markdown-render.util, coingecko.config) + юнит-тесты.
  • Этап 2: ProxyPoolService + CoinGeckoClient + тесты (ретраи, классификация ошибок, stale, дедуп).
  • Этап 3: Миграция MarketDataService/TokenDataService на клиент (включая три места конструирования вне DI).
  • Этап 4: TokenMarketService, TokenNewsService + тесты.
  • Этап 5: Промпт, TokenAnalyticsService, правка OpenAI-провайдера + тесты.
  • Этап 6: DTO, контроллер, wiring модуля, Joi, .env.example + интеграционный тест контроллера.
  • Этап 7: Живой прогон против реального CoinGecko и AI-прокси.
  • Этап 8: Engineering-документация.

6.2. Критические зависимости

  • Локальный CLIProxyAPIPlus (ChatGPT Plus) для живого прогона AI — поднят, отвечает.
  • CRYPTOCOMPARE_API_KEYвыдан, лежит в apps/backend/.env.
  • MARKET_PROXIES — не предоставлен; живой прогон идёт с одного IP, запросы разносятся во времени.

7. Тестирование

Пирамида: чистые юниты → сервисные тесты с рукописными фейками (axios мокается модульно) → интеграционный тест контроллера через supertest с overrideGuard → живой прогон против реальных апстримов.

Базовый прогон модуля до начала работ: 13 сьютов, 52 теста — зелёные.


8. Документация

  • API документация (Swagger) — генерируется из @ApiProperty/@ApiOperation
  • Engineering docs: apps/docs/docs/engineering/Сервисы/backend/ai-analytics.md
  • .env.example

9. Ссылки