Skip to main content
Glama
justin-hsun

ta-catalyst

by justin-hsun
README.md
# ta-catalyst

Explains **why a stock moved** over a past window — and says plainly when it
cannot.

The pipeline runs three reads that are **blind to one another**, then
reconciles them against a shared list of significant trading days:

| read | what it sees | what it must not see |
|---|---|---|
| **Technical** | price and volume, plus a distilled method playbook | any news, ever |
| **Company news** | a local SQLite `news` table | the chart, the macro read |
| **Macro** | QQQ, SPY, dollar index, 10y yield, gold, VIX — then macro news | the chart, company news |

Blindness is the point. Tell a model "NVDA missed earnings on March 5" and it
will narrate the chart to fit that story, whether or not price action supports
it. Two independent signals collapse into one, and you lose the ability to
detect disagreement.

## The product is the reconciliation

Each significant day gets one verdict:

| classification | meaning |
|---|---|
| `explained` | move with a matching catalyst of proportionate size |
| `unexplained` | a real move, news **was** searched, nothing found |
| `overreaction` | move much larger than the catalyst warrants |
| `underreaction` | catalyst much larger than the move — priced in, or disbelieved |
| `no_ta` / `no_catalyst_coverage` | that stream never ran |

`unexplained` is the most informative outcome, not a failure. It implies flow,
positioning or genuine technical behaviour — and it is exactly where the
technical read deserves the **most** weight. None of these are derivable from
any single branch, which is why the branches stay blind.

Every result carries a `coverage` block. **Absence of a search is not absence
of a catalyst**, and the last two classifications exist so a gap in coverage is
never silently reported as a quiet market.

## Quick start

Runs fully offline against deterministic fixtures — no API key, no network:

```bash
uv sync
uv run pytest -q
TA_MODE=stub uv run ta-catalyst-mcp
```

Market-data providers are optional extras, imported lazily — install only the
one you use:

```bash
uv sync --extra futu       # Futu OpenAPI (needs the OpenD gateway running)
uv sync --extra yfinance   # Yahoo Finance
```

To configure real data and models:

```bash
cp .env.example .env     # fill in ANTHROPIC_API_KEY, PINECONE_API_KEY, ...
uv run ta-catalyst-check # reports what is configured and what is missing
```

## The MCP tool

One tool, `history_move_analysis`. The caller is already an LLM that parsed the
user's request, so it passes structured arguments — an LLM choosing among
branches you will almost always run is pure failure surface.

```jsonc
// claude_desktop_config.json (or any MCP client)
{
  "mcpServers": {
    "ta-catalyst": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/ta-catalyst", "ta-catalyst-mcp"],
      "env": { "TA_MODE": "real" }
    }
  }
}
```

```python
history_move_analysis(
    ticker="NVDA",
    months=3,                  # or explicit start_date / end_date
    as_of="2026-09-20",        # point-in-time cutoff for backtests
    include_technical=True,
    include_company_news=True,
    include_macro=True,
)
```

Returns `narrative`, `verdicts`, `coverage`, `significant_days`, `catalysts`,
`technical_read`, `macro_context`, `diagnostics`.

Two fields worth reading first: **`coverage`**, because `false` means *not
searched*; and **`technical_read.methods_read`**, because an empty list means
the model answered from generic technical-analysis priors instead of the
playbook — the failure mode that most looks like success.

## How it fits together

```
START → scope → spine ─┬→ technical ──────────────────┐
                       ├→ company news ───────────────┼→ reconcile ⇄ followup
                       └→ macro context → macro news ─┘        └→ narrative → END
```

| node | model call? | does |
|---|---|---|
| `scope` | thin | resolves ticker, window, branches (skipped when the caller passes them) |
| `spine` | **no** | scans the window for significant days — the shared calendar every branch anchors on |
| `technical` | yes | regime + method cards, blind to news |
| `ticker_cat` | yes | queries the `news` table, headlines first, bodies on demand |
| `macro_context` | **no** | six macro instruments, threshold crossings, ticker-vs-benchmark excess |
| `macro_cat` | yes | semantic search over Pinecone, driven by the macro observations |
| `reconcile` | yes | the only node that sees all three streams |
| `ta_followup` | yes | one bounded second look at unexplained days |
| `narrative` | yes | the written answer |

### Design rules the code actually enforces

**Anomaly detection is code, not an agent decision.** Which days mattered is
`abs(return) > k·ATR` plus gap and volume checks. Deterministic means
reproducible, cheap, and impossible to talk out of flagging something.

**Measurements are tools; definitions are cards.** The market-data layer emits
`ma_order`, `dist_ma50_atr`, `ma50_slope_20d_atr` — arithmetic with no free
parameters. What counts as an *uptrend* is handbook content, so it lives in a
versioned, citable markdown card, not in a Python function with magic numbers.
A test asserts no judgment labels leak into the extractor.

**The deterministic node describes; the agent interprets.** The macro node
emits `"QQQ +2.86% (1.5 ATR)"`, never `"tech rally"`. The agent composes the
search query. Code finds the location, the model reads the window.

**Point-in-time everywhere.** Every source hard-truncates at `as_of` —
prices, the `news` table, and the Pinecone filter. Lookahead leakage looks
exactly like success, so the guard is wired even on live paths.

**Raw candles never reach model context.** Selection reads ~30 scalars
(~400 tokens). The playbook index is ~950 tokens resident; card bodies
(600–1000 tokens each) load on demand.

## Data stores

| store | contract | notes |
|---|---|---|
| Playbook | `playbook/<name>/SKILL.md` | 14 method cards distilled from a technical-analysis handbook, with source page citations |
| Company news | [`resources/news_schema.sql`](resources/news_schema.sql) | SQLite. Optional FTS in [`news_optional_fts.sql`](resources/news_optional_fts.sql) |
| Macro news | [`resources/macro_vectorstore_schema.md`](resources/macro_vectorstore_schema.md) | Pinecone metadata contract — no DDL exists, so the doc *is* the schema |

Create a news database from the shipped schema and fixture:

```bash
sqlite3 catalysts.db < resources/news_schema.sql
sqlite3 catalysts.db < resources/news_seed_fixture.sql   # synthetic, fictional tickers
```

Three things the `news` table does not store, and where they went:
`impact_session` is **derived** (a release after the close prices in the next
session; weekends roll to Monday), dedup is **heuristic** on title similarity,
and ingestion coverage is **approximated** from the ticker's row span. Each is
weaker than an explicit column would be, and each is documented in the schema.

## Development

```bash
uv run pytest -q                    # full suite, offline
uv run ruff check src tests
uv run ruff format src tests
```

Tests cover the deterministic layers against hand-built fixtures, graph
topology across every branch subset, both data stores, and the MCP surface via
a real client round-trip. Tests touching live Futu data self-skip when the
OpenD gateway is not running.

## Status

Working and tested end to end **on stubs and fixtures**. Not yet exercised
against live models or live data — the deterministic layers, graph topology,
retrieval invariants and MCP contract are verified; agent output quality is
not.

Known gaps, in rough priority order:

- No golden set yet. Method selection should be evaluated **separately** from
  analysis quality — they fail for different reasons, and mixing them makes the
  eval uninformative.
- The playbook covers chart patterns (one handbook chapter). Point-and-figure,
  candlesticks and short-term patterns are not distilled.
- Session mapping rolls weekends but **does not know market holidays**, so an
  item published the evening before a holiday is attributed one session early.
- The spine's event budget is tuned for a ~7-month window; on shorter windows
  clustered gaps can crowd out genuinely significant days.
- The forward-looking half of the original design (technical outlook + upcoming
  catalysts) is not built.

## License

[MIT](LICENSE) — code, schemas and configuration.

One carve-out worth understanding before you redistribute: the method cards in
`playbook/` are **distilled from a third-party technical-analysis handbook**,
and carry source page citations for exactly that reason. The MIT grant covers
this repository's own work; it cannot relicense the underlying material, and
the base rates quoted throughout the cards originate in Bulkowski's research.
If you publish this repository, or swap in cards distilled from another book,
that provenance travels with them.

TDQS

A4.4/5.0

Scored across 1 tool

Disambiguation5/5

With only one tool, there is no possibility of confusion or overlap. The tool's purpose is clearly defined, so an agent cannot misselect among alternatives.

Naming Consistency5/5

The single tool name follows a clean verb_noun pattern (history_move_analysis) and is descriptive of its functionality. Consistency is trivially maintained with one tool.

Tool Count3/5

A single tool feels thin for a server, even a specialized one. It is borderline acceptable given the complexity of the tool, but the calibration suggests 1-2 tools is on the low side.

Completeness5/5

The tool thoroughly covers the stated domain of explaining stock moves by integrating technical, catalyst, and macro analysis, with options to toggle branches. It provides a complete workflow for its narrow purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues