Market-context intelligence with a human approval gate. The recommend domain ingests price bars and news, flags statistically unusual moves with a rolling z-score, explains each one with a single LLM call over structured evidence, and delivers the alert to Telegram. It holds no keys and moves no money — a separate execute domain (M2) owns that authority, reachable only through a signed proposal. Not financial advice, but HODL.
You need Docker, and — for the live explanation and delivery — an Anthropic API key and a Telegram bot token (from @BotFather) plus your numeric chat id (from @userinfobot).
cp .env.example .env # fill in ANTHROPIC_API_KEY and the TELEGRAM_* values
docker compose run --rm app python -m hodlin_recommend.demoThat one command starts Postgres, applies migrations, and runs the pipeline once end-to-end — backfill → explain → notify, printing each step — so the anomaly lands in your Telegram chat without waiting for a scheduler tick.
In the default demo mode, prices come from a committed seed CSV and news is skipped, so only Anthropic and Telegram need real credentials (the Finnhub/Massive placeholders can stay as-is). The first run downloads the FinBERT model (~440 MB, cached in a volume afterward), so give it a minute.
To run it as the real service instead — scheduler, API, and Telegram poller:
docker compose up # builds the image, applies migrations, starts servingIt backfills the seed bars on startup, detects the demo anomaly (a sharp
late-June BTC drop), then explains and delivers it on its own ticks — a few
minutes, since explain runs every 5. Once
http://localhost:8000/health/ready returns 200, the pipeline is live. Note
that an anomaly notifies exactly once, so whichever of the two paths you run
first is the one that delivers the alert.
The project is a uv workspace of three packages —
contracts (shared frozen types), recommend, and execute (M2 stub). The
full local gate — ruff, mypy (strict), import-linter, and pytest (unit +
integration against a real Postgres via testcontainers) — runs with:
uv sync
./scripts/check.shIntegration tests need Docker for Postgres; they skip cleanly if it is
unavailable, or point them at an existing database with
HODLIN_TEST_DATABASE_URL (use the admin account — the isolation test
provisions roles, and the migration tests create tables wherever they are
aimed).
Each domain has its own env file: .env for recommend, .env.execute for
execute (cp .env.execute.example .env.execute). That split is the point rather
than tidiness — an env file is injected into a container whole, so a shared one
would hand the recommend process the execute domain's database credential and
undo the per-role isolation the two databases exist to provide.
GET /health/live— the process is up (no dependencies checked).GET /health/ready— the database answers and the scheduler is running.POST /v1/sentiment— score one text with the served FinBERT model.