# Schwab Marketdata Ops En

> |

- **Type:** Skill
- **Install:** `agentstack add skill-kevinkda-schwab-marketdata-skill-schwab-marketdata-ops-en`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [kevinkda](https://agentstack.voostack.com/s/kevinkda)
- **Installs:** 0
- **Category:** [Agent Skills](https://agentstack.voostack.com/c/agent-skills)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [kevinkda](https://github.com/kevinkda)
- **Source:** https://github.com/kevinkda/schwab-marketdata-skill/tree/main/schwab-marketdata-ops-en

## Install

```sh
agentstack add skill-kevinkda-schwab-marketdata-skill-schwab-marketdata-ops-en
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# schwab-marketdata-ops-en

> **Responsibility disclaimer**: This skill drives the Schwab Market Data
> Production API. Users are required to read
>  and
> , and bear sole responsibility for
> compliance of their usage. The authors of `schwab-marketdata-mcp` and
> `schwab-marketdata-skill` are not liable for any usage that violates the
> Schwab Terms of Service.

## Activation handshake (run first)

When activating this skill, the **first step** must be a call to
`get_server_info` to confirm that the returned `server_version` falls
within the frontmatter `compatible_mcp_version` range:

```text
get_server_info()
→ { "server_version": "0.1.x", "supported_tools": [...12 names...] }
```

If the version does not match or the call fails: **stop immediately**,
tell the user to upgrade either `schwab-marketdata-mcp` or this skill,
and **do not** continue any business calls.

Full handshake (including token health check):

```text
1. get_server_info()  → verify server_version ∈ compatible_mcp_version
2. health_check()     → verify token_state == "valid"
                       and token_expires_in_days >= 0.5
3. If either step fails → stop immediately and follow the references
   to repair before continuing.
```

## Quick Start — 7 steps from zero to hello-world

For a first-time onboarding or a new machine, read the following 7 step
files in order:

| Step | File | Expected outcome |
| ---- | ---- | ---------------- |
| 1 | [`references/quick-start/step-1-developer-portal-app.md`](references/quick-start/step-1-developer-portal-app.md) | Schwab Developer Portal app created, App Key / Secret in hand |
| 2 | [`references/quick-start/step-2-credentials-env.md`](references/quick-start/step-2-credentials-env.md) | `.env` populated (chmod 600), pre-commit hooks installed |
| 3 | [`references/quick-start/step-3-first-oauth.md`](references/quick-start/step-3-first-oauth.md) | `token.json` written to disk (chmod 600) |
| 4 | [`references/quick-start/step-4-token-health-check.md`](references/quick-start/step-4-token-health-check.md) | `health` module exits 0, `token_state == "valid"` |
| 5 | [`references/quick-start/step-5-first-mcp-tool-call.md`](references/quick-start/step-5-first-mcp-tool-call.md) | Minimal Python MCP client successfully calls `get_quote("VOO")` |
| 6 | [`references/quick-start/step-6-cursor-integration.md`](references/quick-start/step-6-cursor-integration.md) | `~/.cursor/mcp.json` registered, agent can invoke all 12 tools |
| 7 | [`references/quick-start/step-7-cron-launchd-setup.md`](references/quick-start/step-7-cron-launchd-setup.md) | cron / launchd health probes enabled, desktop notification channel verified |

## Common usage scenarios

Following the helis "I want to..." routing pattern, dispatch directly
to the right tool and reference doc based on user intent.

### "I want a real-time quote"

| Scenario | What to use | Notes |
| -------- | ----------- | ----- |
| Single stock / ETF / index | `get_quote("AAPL")` / `get_quote("$SPX")` | symbol must be UPPERCASE; indexes need a `$` prefix |
| Batch (≤ 50) | `get_quotes(symbols=[…])` | beyond 50, batch on the client side, otherwise `SchwabValidationError(field="symbols")` |
| Option contract | `get_quote("AAPL  240119C00170000")` | **OSI 21 chars**: 6-char root (space-padded) + YYMMDD + C/P + 8-digit strike(×1000) |

→ Full schema in [`references/tools/tool-reference-quotes.md`](references/tools/tool-reference-quotes.md).

### "I want to research an option chain"

1. `get_option_expiration_chain(symbol)` — fetch the list of available expirations first
2. `get_option_chain(symbol, contract_type=…, strike_count=…, strike_range=…)` — pull concrete contracts by ATM/OTM/ITM
3. Multi-step research workflow → switch to the `schwab-marketdata-workflows-en` skill's `option-chain-research.md` playbook

→ Schema in [`references/tools/tool-reference-options.md`](references/tools/tool-reference-options.md);
Greeks freshness covered in the playbook's Cautions section and
[`references/concepts/osi-option-symbol.md`](references/concepts/osi-option-symbol.md).

### "Token expired, what now?"

```text
1. health_check()
   → inspect token_state ∈ {"valid","missing","malformed","insecure_perms"}
   → inspect token_expires_in_days (< 0.5 strongly suggests immediate reauthorize)
2. If SchwabAuthError(reason="refresh_token_expired" | "token_not_initialized"):
   uv run python -m schwab_marketdata_mcp.auth login_flow   # default
   # or manual_flow (headless / SSH-only / WSL2)
3. Re-run health_check() to confirm token_state == "valid"
```

→ Full mapping table and step-by-step remediation in
[`references/troubleshooting/auth-overview.md`](references/troubleshooting/auth-overview.md);
OAuth flow walkthrough in
[`references/oauth/oauth-overview.md`](references/oauth/oauth-overview.md);
token lifecycle in
[`references/oauth/oauth-token-lifecycle.md`](references/oauth/oauth-token-lifecycle.md).

### "Got rate-limited, what now?"

| Symptom | Remediation |
| ------- | ----------- |
| `SchwabRateLimitError(retry_after_seconds=N)` | Wait N seconds and retry; if it happens twice in a row, surface to the user |
| stderr emits `{"event":"rate_limit_warning","remaining":<20}` | Switch bulk requests to `get_quotes` (50 per call); or lower `SCHWAB_RATE_LIMIT_PER_MIN` |
| 0 slots, raises immediately | Apply retry-with-backoff on the agent side (schwab-py already retries `SCHWAB_MAX_RETRIES` times internally) |

→ Token-bucket behavior and triage in
[`references/operations/rate-limit-token-bucket.md`](references/operations/rate-limit-token-bucket.md);
4-symptom rate-limit triage in
[`references/troubleshooting/rate-limit-overview.md`](references/troubleshooting/rate-limit-overview.md).

### "I want to inspect server health"

```text
get_server_info()  → server_version / mcp_sdk_version / schwab_py_version / supported_tools
health_check()     → token_state / token_expires_in_days / rate_limit_remaining_per_min / recent_error_count_24h
```

The two together answer "Is the MCP server healthy, is the token valid,
and have we exhausted the rate limit?" in under 30 seconds.

→ Schema in [`references/tools/tool-reference-meta.md`](references/tools/tool-reference-meta.md).

### "I want candlesticks / historical data"

```text
get_price_history(symbol, period_type, period?, frequency_type, frequency?, ...)
```

The `(period_type, period, frequency_type, frequency)` 4-tuple only
accepts a **restricted set of combinations** — illegal combinations are
silently 400'd by the server. The MCP server pre-rejects them at the
Pydantic layer.

→ Schema + legal-combination table in
[`references/tools/tool-reference-price-history.md`](references/tools/tool-reference-price-history.md);
Cartesian-product error remediation in
[`references/troubleshooting/validation-pricehistory-cartesian.md`](references/troubleshooting/validation-pricehistory-cartesian.md).

### "I want instrument metadata / company fundamentals"

| Scenario | What to use |
| -------- | ----------- |
| Known ticker → metadata | `search_instruments(symbols=["AAPL"], projection="SYMBOL_SEARCH")` |
| Known ticker → fundamentals | `search_instruments(symbols=["AAPL"], projection="FUNDAMENTAL")` |
| Known 9-digit CUSIP | `get_instrument_by_cusip(cusip="037833100")` |
| Fuzzy company-name lookup | `search_instruments(symbols=["TESLA"], projection="DESCRIPTION_SEARCH")` |

→ Schema in [`references/tools/tool-reference-instruments.md`](references/tools/tool-reference-instruments.md).

### "I want top movers / unusual activity today"

```text
get_movers(index="DJI"|"COMPX"|"SPX"|..., sort_order=..., frequency=...)
```

The `index` enum value must be passed as the **enum name** (`"DJI"`),
not the wire value (`"$DJI"`).

→ Schema + enum reference in [`references/tools/tool-reference-movers.md`](references/tools/tool-reference-movers.md).

### "I want to call this from my own Python / TypeScript / Rust app"

| Client | File |
| ------ | ---- |
| Python (mcp SDK) | [`references/integration/python-mcp-client.md`](references/integration/python-mcp-client.md) |
| TypeScript / Node | [`references/integration/typescript-mcp-client.md`](references/integration/typescript-mcp-client.md) |
| Rust (rmcp or hand-rolled) | [`references/integration/rust-mcp-client.md`](references/integration/rust-mcp-client.md) |
| Shell + jq pipe | [`references/integration/cli-jq-pipe.md`](references/integration/cli-jq-pipe.md) |

## Data coverage clarifications

### `get_price_history` is the candlestick / kline endpoint

If you're looking for OHLCV bars (candles, klines, candlesticks),
`get_price_history` is the tool. The response carries a `candles[]`
array, each entry exposing `open` / `high` / `low` / `close` / `volume`
/ `datetime` (epoch milliseconds).

Supported granularity comes from the `(period_type, frequency_type,
frequency)` triple:

- `period_type=DAY`: `MINUTE` × {1, 5, 10, 15, 30} (~48 days for 1-min,
  ~9 months for 5–30 min).
- `period_type=MONTH`: `DAILY` / `WEEKLY` (up to 6 months).
- `period_type=YEAR`: `DAILY` / `WEEKLY` / `MONTHLY` (**up to 20
  years**).
- `period_type=YEAR_TO_DATE`: `DAILY` / `WEEKLY` (year-to-date).

Sub-minute candles (seconds, ticks) are not in the Schwab Market Data
API surface.

→ Full legal-combination table + Cartesian-product error remediation in
[`references/tools/tool-reference-price-history.md`](references/tools/tool-reference-price-history.md)
and the MCP repo's
[README → "Data coverage clarifications"](https://github.com/kevinkda/schwab-marketdata-mcp/blob/main/README.md#data-coverage-clarifications).

### What the Schwab Market Data API does NOT provide

The following data is **architecturally unavailable** through the Schwab
Market Data Production API and would require a third-party provider:

- **Time & sales / tape (trade-level)** — not in REST or Streaming
  since Schwab's 2024 API migration removed the `TIMESALE_*` services.
- **Tick-by-tick history** — not in the Schwab API.
- **Level 2 historical snapshots** — Streaming only, no REST history.
- **Fundamental / earnings time series** (EPS history, revenue
  history, etc.) — `quotes` carry FUNDAMENTAL fields but there is **no**
  historical endpoint.
- **News / SEC filings** — not in the Market Data API.

Recommended third-party providers (Polygon.io, Tiingo, Alpaca,
Databento, FMP, SEC EDGAR) for each data class are listed in the MCP
repo's
[README → "Data coverage clarifications"](https://github.com/kevinkda/schwab-marketdata-mcp/blob/main/README.md#data-coverage-clarifications).

### Trader API is out of scope

**Trader API endpoints** (account, orders, transactions, positions)
are explicitly out of scope — this skill covers **read-only Market
Data only**. If the user asks to place an order or modify positions,
refuse immediately and point them at the MCP README's "Responsible
use" section.

## Decision tree — pick the right tool

| User intent                                | What to use                                              |
| ------------------------------------------ | -------------------------------------------------------- |
| Single stock/ETF/index spot price          | `get_quote(symbol="AAPL")`                               |
| One-shot multi-symbol quotes (≤50)         | `get_quotes(symbols=[...])`                              |
| Historical candles / OHLC                  | `get_price_history(symbol, period_type, …)`              |
| Option chain snapshot                      | `get_option_chain(symbol, contract_type=…)`              |
| Option expiration list                     | `get_option_expiration_chain(symbol)`                    |
| Multi-market open/close status             | `get_market_hours(markets_list=[…])`                     |
| Single-market open/close status            | `get_market_hour_single(market_id)`                      |
| Today's top movers                         | `get_movers(index, sort_order)`                          |
| Fuzzy instrument lookup by ticker          | `search_instruments(symbols, projection)`                |
| Exact lookup by 9-digit CUSIP              | `get_instrument_by_cusip(cusip)`                         |
| Inspect token state / error counters       | `health_check()`                                         |
| Get server metadata (version, 12 tools)    | `get_server_info()`                                      |

**Enum values** must always use the **enum names** defined in
`models.py` (e.g. `"VOLUME"`, `"NASDAQ"`, `"DAY"`); the MCP server
internally translates them into schwab-py wire values.

## Key Concepts

| Term | Definition |
| ---- | ---------- |
| **access_token** | Schwab OAuth bearer token, valid for **90 minutes**; schwab-py auto-renews it before each API call, so the agent never has to think about it. |
| **refresh_token** | **rotate-on-use**: every refresh issues a new refresh_token, with a hard **7-day** lifetime. After expiry you must run `login_flow` again — there is no shortcut (Schwab OAuth is designed this way). |
| **TokenState** | One of 4 enums returned by `health_check()`: `valid` (usable) / `missing` (no token.json) / `insecure_perms` (file mode is not 600/700) / `malformed` (JSON corrupt or missing fields). The first three are fixed by `auth login_flow`; the last typically requires backing up and deleting token.json first. |
| **SchwabAuthError reason** | A 6-way enum attached to auth failures during business calls: `refresh_token_expired_soon` / `refresh_token_expired` / `token_not_initialized` / `token_corrupted` / `insecure_token_perms` / `callback_url_mismatch`. Each reason has its own 5-section remediation page in [`references/troubleshooting/`](references/troubleshooting/). |
| **rate_limit_bucket** | The MCP server uses a **token bucket + sliding window** (capacity = `SCHWAB_RATE_LIMIT_PER_MIN`, default 120) instead of `asyncio.Semaphore`. Difference: a Semaphore would hold its slot during retry sleep, blocking other concurrent tools; the token bucket releases the slot during sleep, letting other tools proceed. |
| **OSI option symbol** | A 21-character fixed format: `{ROOT:6}{YYMMDD:6}{C\|P:1}{STRIKE×1000:8}`, with the root right-padded with spaces if shorter than 6 chars. Example: `"AAPL  240119C00170000"` = AAPL 2024-01-19 Call $170.00; `"BRK/B 240419P00350000"` (contains `/`). Pass this directly to `get_quote`. |
| **pricehistory cartesian product** | Schwab only accepts a **restricted set of combinations** for the `(period_type, period, frequency_type, frequency)` tuple; illegal combinations silently return 400. The MCP server pre-rejects them at the Pydantic layer to avoid wasting billed quota. The full legal-combination table lives in [`references/tools/tool-reference-price-history.md`](references/tools/tool-reference-price-history.md). |
| **Pydantic Literal: enum name vs wire value** | The public API always takes the **enum name** (e.g. `MoversIndex.DJI` = `"DJI"`); the MCP server translates internally to the schwab-py wire value (e.g. `"$DJI"`). This translation layer means the agent never has to remember prefix characters (`$` / lowercase / mixed case). **Never** pass a wire value directly — it triggers `SchwabValidationError`. |
| **Activation handshake** | The two mandatory steps after skill activation: `get_server_info` to verify version compatibility, `health_check` to verify token state. Either failure stops the flow immediately — no silent fallback allowed. |
| **error normalization** | All schwab-py exceptions are wrapped by the server into `{"error": "Schwab*Error", ...}` dicts and returned to the caller — they **do not** propagate as exceptions through the MCP protocol. The agent must always check the `error` field before reading the payload. |
| **non-redistributable** | Schwab Market Data is non-redistributable; any markdown / report writes must stay inside private repositories (the workflows skill enforces this with `gh repo view --json isPrivate`). |

## Architecture overview

```text
┌────────────────────┐
│  AI agent (this

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [kevinkda](https://github.com/kevinkda)
- **Source:** [kevinkda/schwab-marketdata-skill](https://github.com/kevinkda/schwab-marketdata-skill)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-kevinkda-schwab-marketdata-skill-schwab-marketdata-ops-en
- Seller: https://agentstack.voostack.com/s/kevinkda
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
