Забор крипто-новостей, вынесенный из js-конвейера paperhands в отдельный
python-сервис (ТЗ автора backtest-kit, 16.07.2026: «вынеси забор новостей в
отдельный проект с swagger, веб сервером и docker»). Стиль и структура —
канон main.py автора (trading-agents-docker): тонкий main.py, pydantic
req/resp-модели, use_vendor-инъекция источников, /api/v1/*, / → /docs,
PORT из env, CORS «всем можно».
main.py — FastAPI-обвязка: модели, эндпоинты, use_vendor (канон автора)
tools/news.py — вендоры: tavily (пул-13 + 4 clean-запроса, 1:1 из
paperhands news_collect.mjs) и ddgs (бесплатный, вендор
новостей самого автора); фильтр «дата не 00:00Z» (Tavily)
tools/journal.py — sqlite-журнал: дедуп по url (PRIMARY KEY), статистика,
счётчик Tavily-кредитов по UTC-дням
scripts/import_raw_jsonl.py — разовый сид журнала из news-raw.jsonl (идемпотентен)
data/news.db — sqlite (bind-mount ./data:/data в docker)
Классы запросов: etf | fomc | crash-rally | regulation. Пул-13 доменов и
формулировки запросов заморожены (правило №65в: состав меняется только после
валидационной пробы).
| Метод | Путь | Что |
|---|---|---|
| GET | /api/v1/health |
{status, vendors_loaded} |
| GET | /api/v1/news?since=<ISO>&cls=<класс>&vendor=<tavily|ddgs> |
забор (если разрешён) + выдача журнала: [{url, domain, title, content, publishedDate, cls, score, fetchedAt}] |
| GET | /api/v1/journal/stats |
счётчики журнала + состояние лимитера |
| GET | / → /docs |
swagger |
Семантика /news: сначала fetch у вендора (все 4 класса или один cls),
новое пишется в журнал (дедуп по url), затем отдаются строки журнала этого
вендора с фильтрами cls/since. since старше 36 часов переключает окно
поиска day → week. У ddgs score=null и кредиты не тратятся.
| Переменная | Дефолт | Смысл |
|---|---|---|
PORT |
8080 | порт uvicorn |
TAVILY_API_KEY |
— | ключ Tavily |
NEWS_SERVICE_DRY |
1 | обкатка: Tavily НЕ вызывается, /news?vendor=tavily отдаёт журнал; ddgs работает всегда |
TAVILY_DAILY_CREDIT_LIMIT |
8 | лимитер внутри сервиса: advanced-запрос = 2 кредита; при превышении — 429 ДО обращения к API |
DB_PATH |
./data/news.db | путь sqlite (в docker переопределён на /data/news.db) |
# локально
uv sync
uv run python main.py # env подхватить самому (set -a; . ./.env; set +a)
# docker (наружу только 127.0.0.1:8080)
docker compose up -d --build
# сид журнала историей paperhands (127 записей, идемпотентно)
uv run python scripts/import_raw_jsonl.py \
../paperhands/agent/notes/news-dataset/news-raw.jsonl
# смоук
curl -s localhost:8080/api/v1/health
curl -s "localhost:8080/api/v1/news?vendor=ddgs&cls=etf" # 1 запрос, бесплатно
curl -s localhost:8080/api/v1/journal/statsЭтап 0 (сейчас): крон как был — прямой Tavily из news_collect.mjs; сервис
крутится в NEWS_SERVICE_DRY=1, кредитов не тратит.
Этап 1 (обкатка тракта): в news_collect.mjs появился флаг
--from-service http://localhost:8080 — тот же jsonl-формат на выходе
(source:"service" в записях), дедуп и формат конвейера не меняются.
Проверка тракта без трат: сервис в DRY отдаёт журнал.
Этап 2 (полный переход, по команде владельца): в .env сервиса
NEWS_SERVICE_DRY=0, в крон-строке у news_collect.mjs добавить
--from-service http://localhost:8080. Бюджет ≤8 кредитов/день стережёт уже
сервис (429 при превышении). Прямой Tavily-путь остаётся в скрипте как
фолбэк: убрать флаг — вернулись на этап 0.