SLOPSHOPPER

token-usage

Status line with context-window fill, session cost and rate-limit use

status
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · token-usage
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ token-usage: ctx 97k/200k (49%) · $0.42 · 5h 31%
README

Agentic Research Pipeline

Agent pipelines for investment research at scale: build thematic company universes, extract schema-defined data points from disclosures, and analyse portfolios — designed for up to ~4,000 companies per run.

Two design rules hold everywhere: the LLM plans, deterministic code computes, and every citation is re-verified programmatically against the source document rather than trusted from the model's self-report.

Python (FastAPI + Typer CLI) backend, React/TypeScript (Vite) frontend, Postgres + pgvector as the system of record (required).

Functions

#FunctionWhat it does
1Thematic Universe BuilderTurns a macro theme into business activities, matched companies, exposure estimates and cited rationale via an Advocate/Opposing/Adjudicator debate.
2Taxonomy LibraryReusable, versioned theme definitions with an explicit ratification step; five derivation methods (industry-anchored, authority-source, empirical, news/transcript mining, ETF holdings) plus compare/merge and NACE/NAICS/SIC/GICS crosswalks.
3Data-Point Extraction EnginePulls schema-defined figures (e.g. green capex) from disclosures with an independent verifier on a different model, programmatic grounding, review/override/reject with a permanent per-field audit trail.
4Company Financials ExtractionBusiness segments + total CapEx + R&D in one combined pass per company, each figure with a grounded description of what it funds. SEC XBRL facts resolve CapEx/R&D totals ahead of the LLM where available.
5Company Identity ResolutionAgentic resolution of messy company identifiers to a single canonical entity.
6Document DiscoveryFinds each company's IR site, crawls it (bounded, same-domain, robots.txt-respecting), downloads new/changed disclosures. Manual or scheduled.
7Indirect Exposure Tier (opt-in)Structural supply-chain exposure via OECD ICIO input-output propagation. Purely quantitative, zero LLM calls.
8Transition Plan AssessmentReplication of Colesanti Senni et al. (2024): 64 fixed indicators scored "walk" vs. "talk", each with a grounded RAG verdict. Runs as the transition_plan_indicators preset on the generic extraction pipeline.
9Transition Barrier AssessmentSector-level counterpart: a 105-cell matrix (35 criteria × 9 hard-to-abate sectors × EU/US/China) backed by 86 verified sources, with staleness tracking and a propose-never-apply EUR-Lex refresh pipeline.
10Portfolio Risk & Exposure MonitoringDeterministic holdings aggregation, an NL Q&A agent (LLM drafts the query, the engine computes the number), a Dashboards (Superset) tab that embeds every arp- dashboard (a provisioned exposure dashboard with Fund/Sector/Country filters, hand-built ones, and drafts from the Superset designer), and Climate Analytics (WACI, PCAF-style financed emissions, coverage).
11Investment Strategy ReplicationReduces a strategy paper to an executable spec, then backtests it deterministically in- and out-of-sample, with deflated Sharpe, PBO via purged/embargoed CSCV, and regime stratification.
12Emerging Themes ScannerBottom-up theme discovery from EDGAR full-text search, GDELT and regulatory RSS, with cross-period cluster lineage and an action-score promotion gate (corporate action, not mention counts).
13Presentation & Reporting ToolOne LLM call drafts a report plan; deterministic renderers emit pptx/docx/pdf, reusing an ingested .pptx template's layouts, colors and fonts. House decks (house_deck) draft a storyline for you to approve, then fill, lint, fit and visually check each slide in the app's own design system, and can be re-run on fresh pipeline data.
15Decision StudioTurns any per-entity table the functions above produce into a scored, ranked and tiered decision — entities are companies, sectors in a jurisdiction, themes or strategies, since the engine scores rows. Correlated criteria are grouped so one theme measured seven ways doesn't earn seven times the weight; criteria are normalised within peer cohorts; direction is inferred and flagged where it is a guess. Gates resolve before the average, a sufficiency gate precedes scoring, and every entity carries its rank range across four specifications. Frameworks are versioned and ratifiable; the audit log separates what the data proposed from what a person changed. Zero LLM calls.
16Equity Index ConstructionBuilds an index methodology by composing named rules — ordered screens, one selection rule (best-in-class to a market-cap or count coverage target with hysteresis buffers, an absolute threshold, or top-N), a weighting scheme, bounded multiplicative tilts, a deterministic capping waterfall (single-name, group, UCITS 5/10/40) and the path-dependent EU PAB/CTB decarbonisation trajectory. Zero LLM calls in the numbers: a model-derived thematic score enters only as a frozen, effective-dated snapshot. The composition saves as a versioned, effective-dated calibration, and a review resolves the version in force on its review date, so today's parameters cannot rewrite a past one. Optionally (.[optimize]) swaps the waterfall for a convex programme — least-squares projection, minimum tracking error, or score maximisation under a TE budget on an estimated or vendor risk model — or a mixed-integer one on SCIP for cardinality limits and a genuinely enforced minimum weight. Every constraint is re-verified in plain Python afterwards; a solver's own "optimal" is never taken as proof.
14Standing agentsTaxonomy Researcher and Calibration Agent run on a schedule and propose changes for human review — they never apply them.
17Superset BI Designer (opt-in)Describe a dashboard in words and get an unpublished draft in Apache Superset. The LLM plans, deterministic code compiles, Superset computes: a closed catalogue of datasets and metrics bounds the plan, a validator rejects anything outside it, and the model never writes SQL or sees a number. Reads only the bi views over Postgres, so company facts are the reviewed ones. Drafts only; a person publishes.
18XBRL FactsSeparate from Data-Point Extraction, which it does not touch. Downloads each company's SEC companyfacts JSON and latest 10-K (inline XBRL) as filed, or for EU companies (--market esef, keyed by LEI; the default --market auto routes each company by country, ISIN prefix or identifiers and marks the rest no_source or unrouted) the latest ESEF annual report's xBRL-JSON and package from filings.xbrl.org, extracts every tagged fact (or only a chosen set), always resolves revenue and capex, and offers a searchable dropdown over the full us-gaap / ifrs-full / dei / esrs taxonomies. Files and facts show in tables (flat and by year), and an extraction run can be verified against the filer's tagged values (a run that copied XBRL is refused). CLI, API and page; files only, no database.
19Argus UniverseLanding page of both Argus processes: bring a universe, map it through the security master by exact identifier, see what is already stored per company (identity, documents, extraction runs, XBRL files) and the suggested XBRL source, then hand the whole universe or a selection to Extraction or XBRL Facts. Read-only; it starts no run.

Architecture

backend/   Python (FastAPI + Typer CLI) — every agent pipeline
frontend/  React + TypeScript (Vite) — authoring, monitoring, review UI
Postgres   system of record (+ pgvector): companies, runs, review, facts, documents, embeddings, holdings
runs/      run side files with no table: snapshots, events, restatement candidates
taxonomies/ portfolios/ reports/ report_templates/ data/documents/
frameworks/ versioned decision frameworks + the tables they are applied to
indices/   index construction calibrations (versioned, effective-dated) + reviews
  • The Anthropic SDK (anthropic) is the LLM client, behind one narrow interface (arp/llm/base.py) — schema-forced structured output, a bounded validation-retry loop, and a disk-backed response cache.
  • LangGraph models each per-company multi-step flow as an explicit state graph (the classification debate, the extractor/verifier pair, …). Batch fan-out, checkpointing and resumability stay outside the graphs, in the Postgres run store (arp/db/runs.py).
  • LlamaIndex backs chunking and BM25 evidence selection; hybrid (BM25 + local multilingual embedding) retrieval is on by default.
  • arp/grounding.py is independent of all three and re-verifies every citation against the original document text.

Every run type is checkpointed and resumable — each company's result is committed to Postgres in one transaction as it finishes, and a resume skips every result key already stored, so an interrupted batch picks back up without redoing completed work. Runs can be cancelled cooperatively (arp runs cancel) and resumed later.

Setup

Windows / corporate proxy / VS Code + conda: see docs/INSTALLATION.md, which ships scripts under scripts/windows/ that automate everything below.

# Backend
cd backend
python3 -m venv .venv && source .venv/bin/activate   # Python 3.11
pip install -e ".[dev]"
cp .env.example .env            # fill in ARP_ANTHROPIC_API_KEY; ARP_POSTGRES_DSN is preset
docker compose up -d postgres   # required: Postgres + pgvector
docker compose up -d neo4j      # optional: company graph for the theme exposure layers
arp db init-postgres            # schema; re-run after every upgrade
pytest -q                       # needs ARP_TEST_POSTGRES_DSN (a separate, empty database)
uvicorn arp.api.main:app --reload            # API on :8000

# Frontend
cd frontend && npm install
cp .env.example .env            # VITE_API_BASE, VITE_SUPERSET_URL
npm run dev                                   # UI on :5173

Sign-in

Every API route except /api/health (and the proxy-voting routes) needs a signed-in user, and every review, ratification and approval is recorded against that user.

Current default: dev mode (ARP_AUTH_MODE=dev). Every request from 127.0.0.1/::1 is signed in as an approver named ARP_DEV_USER (default dev) without a token; the sidebar shows "Signed in as dev (approver)". Use it only on your own machine, or with the provided docker-compose.yml, which publishes the API on 127.0.0.1 only and adds the Docker bridge range (172.16.0.0/12) to ARP_DEV_TRUSTED_NETWORKS because the browser's requests reach the container from the bridge, not loopback. That range also covers every other container on the compose network (e.g. Superset): while dev mode is on, they get dev approver access too. On Docker hosts whose compose subnets come from 192.168.0.0/16 (check docker network inspect), adjust ARP_DEV_TRUSTED_NETWORKS to match. The bypass trusts the socket address: never run dev mode behind a proxy on the same host (every proxied request would arrive from loopback) and never publish the port on a LAN-reachable address. Requests from anywhere else still need a token. Dev mode also refuses browser requests from foreign origins (an Origin header not in ARP_ALLOWED_ORIGINS gets no bypass, so another website cannot act as you), and the API only answers the host names in ARP_TRUSTED_HOSTS (default localhost, 127.0.0.1, [::1], backend), which blocks DNS rebinding; this host check applies to every route, voting included. Dev mode has a single user, so four-eyes steps (calibration approval, co-sign) need local mode.

Switching to local mode (real per-person sign-in):

  1. cp config/users.example.json config/users.json and give each person a long random token (openssl rand -hex 24), a unique user_id, a name and a role: viewer (read), analyst (change), approver (also approve calibrations and co-sign). Tokens and user_ids must be unique and non-empty, or the API refuses to start. config/users.json is gitignored; ARP_USERS_FILE points elsewhere.
  2. Set ARP_AUTH_MODE=local in backend/.env (Docker: in the backend service's environment: in docker-compose.yml, and remove ARP_DEV_TRUSTED_NETWORKS there).
  3. Restart the API (also after every later edit of the users file: it is read once).
  4. Each person pastes their token into the sidebar; the UI keeps it in the browser's localStorage until they sign out (or the API rejects it).
  5. CLI commands that write (arp index calibration-save, arp index approve, …) need ARP_CLI_TOKEN set to the person's token, in either mode.

ARP_ALLOWED_ORIGINS is the CORS allow-list for the browser UI, as JSON, e.g. ARP_ALLOWED_ORIGINS='["https://arp.example.com"]'.

[dev] is enough to run the app, but not enough for a green test suite — the reporting tests render through headless Chromium and a few storage tests need the optional backends. For the full suite, as CI installs it:

pip install -e ".[dev,postgres,opensearch,object_storage,emerging_themes]"
python -m playwright install chromium

Budget ~1.8 GB and around ten minutes: docling pulls in torch, torchvision, transformers, onnxruntime and opencv (217 packages for [dev] alone). Nothing is downloaded at import time — docling and fastembed are both imported lazily — but they are hard dependencies, so the install pays for them regardless.

No conda needed. environment.yml and scripts/windows/setup.ps1 use it to supply Python 3.11 and Node 22, but any 3.11 interpreter works; uv venv --python 3.11 fetches one in seconds if the system Python is a different version. Python 3.11 is what CI and environment.yml pin and the only version verified here.

On a first extraction or theme-matching run, hybrid retrieval downloads a ~120 MB embedding model (it is on by default); ARP_HYBRID_RETRIEVAL_ENABLED=false keeps a first run fully offline.

Docker is an alternative to both:

cp backend/.env.example backend/.env
docker compose up backend frontend --build    # same ports, state in named volumes

There is no authentication in front of either path today — see docs/CORPORATE_READINESS_PLAN.md.

Contributors: pre-commit install enables the secret-scanning hook that also runs in CI.

Data handling. Document text and company data go to Anthropic's API; outbound requests reach SEC EDGAR, GDELT, regulatory RSS and crawled IR sites. Everything else stays on local disk. Review this against your organization's data-handling policy before pointing it at real holdings.

Superset BI

Optional. Reads the bi views arp db init-postgres creates over the Postgres tables.

Set ARP_SUPERSET_PASSWORD, ARP_BI_READER_PASSWORD, SUPERSET_SECRET_KEY and SUPERSET_GUEST_TOKEN_JWT_SECRET in backend/.env. None has a default and the change-me placeholders are refused; the two Superset secrets need 32+ characters. Generate each with openssl rand -base64 42. ARP_SUPERSET_URL (default http://127.0.0.1:8088) and ARP_SUPERSET_USER (default arp_designer) have defaults; ARP_BI_SUPERSET_DB_HOST (default postgres:5432) is the host:port Superset uses to reach Postgres.

docker compose --env-file backend/.env up -d postgres superset   # first start builds superset/Dockerfile
arp db init-postgres                                             # schema + `bi` views
arp portfolio seed-demo                                          # demo holdings into Postgres
arp bi bootstrap                                                 # views, bi_reader role, Superset database, datasets, metrics, descriptions, templates (idempotent)
arp golden-set bi                                                # planner eval; needs an API key, not run in CI

arp bi bootstrap re-applies the bi views (including holdings_history), registers the six datasets and their metrics, and copies descriptions from backend/arp/bi/catalog.py. Each run overwrites the dataset description and the descriptions of catalogue-named columns, so edit them there, not in Superset. It then provisions the standard dashboards in backend/arp/bi/templates/ (today arp-risk-exposure: 8 charts, native filters Fund, Sector and Country) and prints {"templates": {"arp-risk-exposure": "created" | "rebuilt" | "unchanged"}} with the rest of its output. The dashboard is created unpublished; publish it in Superset. One that has at least the template's charts is left alone. One with fewer is deleted and rebuilt (its old charts stay), and the rebuild is unpublished again. Existing deployments must re-run arp bi bootstrap after upgrading: the AI designer needs every catalogue dataset, including holdings_history.

Upgrading to the monthly run's published datasets: run arp db init-postgres (creates the bi_published table and its bi views), then arp bi bootstrap (bi_reader is granted per existing object, and the climate, alerts, triggers and company-profile datasets and dashboards are registered there). Until then Company Profile embeds fail closed with a 502 ("Superset has no dataset bi.alerts"). The published datasets fill only when a monthly run (arp portfolio monthly-run --month YYYY-MM) succeeds, and a month runs only with an ok holdings load for every portfolio and an ok ESG load for that month (upload it, or arp portfolio esg-pull --month YYYY-MM). Holdings Intake shows what a month is still missing.

News comes from Refinitiv News over the Refinitiv Data Platform: set ARP_NEWS_API_CLIENT_ID and ARP_NEWS_API_CLIENT_SECRET (an RDP service account; ARP_NEWS_API_URL defaults to https://api.refinitiv.com), then pull from Data Hub · Feeds ("Pull now") or on a schedule with arp portfolio news-pull. Only issuers with a permid in the security master are asked for, and each story is tied to them by PermID exact match only (see backend/arp/portfolio/news/api_source.py). ESG can be pulled the same way from Feeds once ARP_ESG_API_URL and ARP_ESG_API_TOKEN are set.

Set VITE_SUPERSET_URL in frontend/.env, then open Risk Monitoring, Dashboards (Superset). This one tab replaces Pivot Explorer, Generative BI and Superset BI (old links land on it), and it is the default Risk Monitoring tab. The in-app Standard Analytics tab is gone; Monitoring & Alerts, Company Profiles and Ask the Portfolio are unchanged. The picker lists every Superset dashboard whose slug starts with arp- and opens on arp-risk-exposure. To add a dashboard you built by hand, set its slug to arp-<name> in Superset's dashboard Properties and reload the tab. The weighted-average climate pivots Pivot Explorer had are available through the climate API endpoints.

The UI embeds a draft through POST /api/bi/embed-token {dashboard_id}, which answers {token, embedded_id} (service.embed_token returns (embedded_id, token)). Only arp- dashboards (slug arp-..., the scratch one and hand-built ones included) are embeddable: another dashboard id gets 403, an unknown one 404.

Caveats:

  • Drafts only. Publishing a dashboard is a human step in Superset.
  • The superset/Dockerfile build (adds psycopg2 to apache/superset:5.0.0) is unverified end to end: the sandbox proxy's TLS blocked it, and it was tested with an equivalent local image. Run docker compose build superset on a normal machine before relying on it.
  • Embedding is verified in Chromium only.
  • The first-embed lock is in-process, so run a single uvicorn worker.
  • Seed demo holdings (arp portfolio seed-demo) before the views have anything to show.
  • Postgres 15 or later is assumed (compose runs pg16). Before 15, every role, bi_reader included, gets CREATE on schema public through PUBLIC, which bootstrap's REVOKE does not remove.
  • frame-ancestors lists only http://localhost:5173 and http://127.0.0.1:5173 (the dev UI). Add production origins in superset/superset_config.py.

Projects

A project is a folder under ARP_PROJECTS_DIR (default projects/, git-ignored) holding replayable data sources (uploaded DWS constituent .xlsx files plus the assumed notional) and the dashboards stored for it. The data files are user data and are never committed. Opening a project re-imports its files into Postgres (tagged project:<id>, portfolio ids <project>-dws-<fund isin>) and provisions its dashboards, both idempotently.

UI (Risk Monitoring, Dashboards): pick a project in the selector or choose New project, upload the files and notional, and the project opens automatically. A generated dashboard is saved to the project with Save to project. Dashboards you built by hand in Superset (slug arp-...) appear under Other arp- dashboards and can be exported into a project from there; opening the project re-imports one that is missing (always unpublished, never over an existing slug). Saving the same title again is an explicit Save and rebuilds that dashboard in Superset (only the dashboard is deleted, charts stay; it comes back unpublished); opening a project never changes an existing dashboard.

arp project create alpha --name "Alpha review"
arp project add-data alpha Constituent_IE00B4L5Y983.xlsx --notional-eur 50000000
arp project list
arp project open alpha          # import data, provision dashboards; prints created / rebuilt / unchanged

API (/api/projects): GET "", POST "", GET /{id}, POST /{id}/data (multipart: file, notional_eur), POST /{id}/open, POST /{id}/dashboards (save a designed dashboard plan), POST /{id}/dashboards/export (store a hand-built dashboard).

Scoping: bi.holdings and bi.holdings_history have a trailing project_id column taken from the portfolio tag. Project dashboards carry the chart filter project_id == <id> and their native filters (Fund, Sector, Country) are pre-filtered the same way, so options list only that project's data. The STANDARD dashboards (such as arp-risk-exposure) stay unfiltered and show ALL data in the shared Postgres, every opened project included. Securities and companies are global and keyed by ISIN: the last import wins for sector and country labels, so a conflicting file in project B can change project A's sector/country breakdowns (holdings themselves never mix). Hand-built dashboards are stored as Superset export bundles and are not auto-scoped; filter them on project_id yourself if they must be.

Upgrade: re-run arp bi bootstrap so the views gain project_id. Project dashboards created before the native-filter scoping fix keep global filter options until rebuilt: delete the dashboard in Superset and open the project again.

Limits: the only data kind is the DWS constituent .xlsx; uploads are .xlsx up to 50 MB; there is no project delete, versioning or authentication; opening needs the Postgres portfolio backend. ARP_BI_READER_PASSWORD (not a placeholder) is needed only to re-import stored hand-built bundles.

CLI

The CLI drives the same pipelines as the API and is the intended path for large unattended batches. arp --help lists every command; a representative slice:

arp theme decompose "Electrification" --out theme.json
arp theme run --theme theme.json --universe companies.csv
arp taxonomy create "Electrification" --method industry_anchored
arp extract run --schema schema.json --universe companies.csv
arp extract financials-run --universe companies.csv
arp transition-barrier scores --region China --pillar Regulation
arp emerging-themes run --universe companies.csv
arp replicate backtest --spec spec.json --prices prices.csv --tickers universe.csv
arp climate waci --group-by portfolio_id
arp decision derive --source transition_plan_run --run-id <run_id> --save   # no API key: zero LLM calls
arp decision score --source transition_barrier --region "European Union"  # entity = sector, not company
arp decision score --dataset <dataset_id>
Source 1 files
hooks/register.ts 24 lines
1import type { Register, SessionContextUsage, SessionRateLimit } from 'claude-code'
2
3const LABELS: Record<string, string> = { five_hour: '5h', seven_day: '7d' }
4const k = (n: number) => (n >= 1000 ? `${Math.round(n / 1000)}k` : `${n}`)
5
6export const format = (context: SessionContextUsage, rateLimits: SessionRateLimit[], usd?: number) =>
7  [
8    context.tokens === undefined
9      ? `ctx –/${k(context.window)}`
10      : `ctx ${k(context.tokens)}/${k(context.window)} (${Math.round(context.percent ?? (100 * context.tokens) / context.window)}%)`,
11    usd === undefined ? undefined : `$${usd.toFixed(2)}`,
12    ...rateLimits.map(r => `${LABELS[r.kind] ?? r.kind} ${Math.round(r.percentUsed)}%`),
13  ]
14    .filter(Boolean)
15    .join(' · ')
16
17export const register: Register = on => {
18  on('session.measure', async ($, e, next) => {
19    const { cost } = await $.session.usage()
20    $.ui.status(format(e.context, e.rateLimits, cost?.usd))
21    return next(e)
22  })
23}
24