AI Analytics
Описание
Модуль aiAnalytics в apps/backend отдаёт рыночную аналитику по крипторынку и отдельным токенам. Redis-only: своих таблиц БД и миграций у модуля нет.
Исторически м одуль умел две вещи — обзор рынка и AI-разбор пула ликвидности Uniswap v3. Токен-ветка (список/поиск/карточка/график/новости/AI-разбор токена) достроена позже поверх нового единого HTTP-клиента CoinGecko.
Полное ТЗ токен-ветки — tz/active/token-analytics. Дизайн-документ с проверенными фактами о CoinGecko и обоснованием решений — docs/superpowers/specs/2026-07-12-token-analytics-design.md в корне репозитория.
Карта эндпоинтов
Все роуты защищены JwtAuthGuard + ApiBearerAuth (в проекте роут публичен по умолчанию, поэтому защита ставится явно на каждом контроллере).
AiAnalyticsController — старые (без изменений в поведении)
| Метод | Путь | Источник | TTL | Кеш-ключ |
|---|---|---|---|---|
GET | /ai-analytics/market/outlook | Fear&Greed (alternative.me) + AI (altseason, структурный JSON) | 3600 c (AI_ANALYTICS_OUTLOOK_TTL_SEC) | ai:outlook:v1 |
POST | /ai-analytics/pool/analytics | Uniswap v3 subgraph + AI (Markdown) | 900 c (AI_ANALYTICS_POOL_TTL_SEC) | ai:pool:{network}:{poolId} |
market/outlook собирает Fear&Greed, глобальную статистику и топ-100 монет (MarketDataService), считает altseason-индикатор и просит модель сформулировать обзор рынка структурным JSON. pool/analytics берёт метрики пула из Uniswap v3 subgraph (UniswapV3GraphService), метаданные обоих токенов — через TokenDataService, и просит модель написать Markdown-разбор пула. Оба сервиса используют общий OPENAI_CLIENT и общее правило «фолбэк не кешируется».
TokenAnalyticsController — токен-ветка
| Метод | Путь | Источник | TTL | Кеш-ключ |
|---|---|---|---|---|
GET | /ai-analytics/market/global | CoinGecko /global + Fear&Greed | 60 c (AI_ANALYTICS_MARKET_TTL_SEC) | ai:cg:v1:global (без провенанса наружу, см. ниже) |
GET | /ai-analytics/tokens | CoinGecko /coins/markets | 60 c | ai:cg:v1:markets:{order}:{page}:{perPage}:{category|-}:{ids|-} |
GET | /ai-analytics/tokens/search | CoinGecko /search | 300 c | ai:cg:v1:search:{normalizedQuery} |
GET | /ai-analytics/tokens/trending | CoinGecko /search/trending | 300 c | ai:cg:v1:trending |
GET | /ai-analytics/tokens/:id | CoinGecko /coins/{id} | 300 c | ai:cg:v1:coin:{id} |
GET | /ai-analytics/tokens/:id/chart | CoinGecko /coins/{id}/market_chart | 300 c | ai:cg:v1:chart:{id}:{days} |
GET | /ai-analytics/tokens/:id/news | CryptoCompare (общий фид) | 600 c | ai:news:v1:feed |
POST | /ai-analytics/tokens/:id/analytics | LLM (структурный JSON + Markdown) | 3600 c | ai:token-analytics:v1:{id} |
Итого 2 старых + 8 новых = 10 эндпоинтов.
/tokens/search и /tokens/trending объявлены в контроллере до /tokens/:id. Оба слова проходят под регекс идентификатора CoinGecko (token-analytics.controller.ts, комментарий прямо над роутами), поэтому если поменять порядок — Nest начнёт матчить их как id, и GET /tokens/search?query=pepe превратится в запрос карточки несуществующего токена search.
Каждый ответ токен-ветки несёт провенанс — cached, stale, fetchedAt (ISO). Единственное исключение — /market/global: он использует MarketDataService, который отдаёт уже маппленые значения и не прокидывает, откуда они взялись. Вписывать cached/stale наугад значило бы врать в Swagger-контракте, поэтому у MarketGlobalResponseDto их просто нет (зафиксировано техдолгом в дизайн-документе).
CoinGeckoClient — единственная точка выхода
apps/backend/src/aiAnalytics/services/coingecko.client.ts — единственное место в модуле, которое ходит в CoinGecko. И TokenMarketService, и мигрировавшие MarketDataService/TokenDataService вызывают только его.
export interface CoinGeckoResult<T> {
data: T;
fetchedAt: number; // epoch ms — когда тело реально получено из CoinGecko
stale: boolean; // апстрим недоступен, отдан просроченный конверт
cached: boolean; // данные из Redis, а не из свежего запроса наружу
}
get<T>(
path: string,
params: Record<string, unknown>,
opts: { cacheKey: string; ttlSec: number; staleTtlSec: number },
): Promise<CoinGeckoResult<T>>;
Клиент возвращает провенанс, а не голое значение — иначе сервисы не смогут проставить cached/stale в ответе.
cacheKey — единственный ключ и кеша, и in-flight дедупа: ни path, ни params в него не подмешиваются. Вызывающий обязан закодировать в ключе всё, что различает ответы. Два разных запроса с одинаковым cacheKey схлопнутся в один, и второй получит чужой ответ — поэтому, например, TokenMarketService.marketsCacheKey() собирает ключ из order/page/perPage/category/ids, а не полагается на дефолты.