schwab-rs
Rust client library and schwab-agent structured JSON CLI for the Schwab API.
Wraps the Schwab Market Data and Trader REST APIs with typed methods and models so callers don't need to build URLs or parse raw JSON. The same crate also ships schwab-agent, an agent-oriented CLI for auth, market data, account discovery, option workflows, technical analysis, and guarded order actions.
[!IMPORTANT]
schwab-rsis an unofficial project. It is not affiliated with, endorsed by, or sponsored by Charles Schwab & Co., Inc., Schwab brokerage services, or thinkorswim.
Features
- Market Data - quotes, option chains, expiration chains, instruments, market hours, movers, price history
- Schwab response compatibility - typed model deserialization follows observed Schwab variants, including both
NASandNASDAQNasdaq exchange spellings - Trader - accounts, orders (place/replace/cancel/preview), transactions, user preferences
- Order builder - typed equity helpers, single-leg option helpers, OCO, and first-triggers-second order composition
- Repeat orders - convert supported historical
Orderresponses intoOrderBuilderpayloads for reuse - Typed order states - known lifecycle statuses such as
WORKING,FILLED,CANCELED, andREJECTED, plus order activity execution types such asFILLandCANCELED, deserialize to typed variants withUnknownfallbacks for future Schwab values - Streaming - WebSocket session engine for account activity, level-one equities, options, futures, futures options, forex, chart equity, chart futures, screener equity, and screener option with broadcast events and automatic reconnect
- OAuth2 auth - PKCE authorization code flow, file-backed token storage, automatic refresh via
Provider - schwab-agent CLI - structured JSON command-line workflows for auth, quotes, history, accounts, orders, options, technical analysis, and multi-symbol analysis
- Async - built on
tokioandreqwestwithrustlsfor TLS
Quick start
Add schwab from crates.io:
[]
= "0.3"
The default feature set includes the bundled schwab-agent CLI. Library-only consumers that do not need the binary can avoid CLI-only dependencies with:
[]
= { = "0.3", = false }
use ;
async
Authentication
Schwab requires OAuth2 with a browser approval step. For local development, set the app credentials in environment variables and run the auth example:
SCHWAB_CLIENT_ID='your-app-key' \
SCHWAB_CLIENT_SECRET='your-app-secret' \
SCHWAB_CALLBACK_URL='https://127.0.0.1:8182/callback' \
SCHWAB_TOKEN_PATH='schwab-token.json' \
The auth example writes a token file that Provider::from_token_file can refresh and turn into a ready-to-use Client. See docs/auth.md, examples/auth.rs, and examples/quotes.rs for the full flow.
Do not commit Schwab client secrets, authorization codes, access tokens, refresh tokens, token files, or account data. Prefer environment variables or a secret manager for credentials, and see SECURITY.md for reporting and token-handling guidance.
schwab-agent CLI
Install the bundled JSON CLI from the published crate:
schwab-agent prints raw JSON payloads on success and structured JSON errors with stable code, message, category, retryable, and hint fields on application failure. Set SCHWAB_AGENT_JSON_ERRORS=1 to request the same JSON shape for clap usage errors such as unknown commands, invalid values, missing required arguments, and conflicting arguments; when unset, the default human-readable clap stderr remains available. auth refresh maps Schwab OAuth invalid_grant token responses to auth.refresh_token_invalid with an auth exit code and a full re-authentication hint instead of a generic HTTP-status error. The completions <shell> command and singular alias completion <shell> are the only raw stdout exceptions because shells need completion scripts for bash, zsh, fish, powershell, and elvish; write failures emit a short stderr diagnostic and exit non-zero. Credentials come from SCHWAB_CLIENT_ID, SCHWAB_CLIENT_SECRET, and optional SCHWAB_CALLBACK_URL, or from ~/.config/schwab-agent/config.json. Precedence is command flags, environment variables, config file, then defaults. The token path can be overridden with a non-empty SCHWAB_TOKEN_PATH; the default remains $XDG_CONFIG_HOME/schwab-agent-rs/token.json for compatibility with existing agent installs, falling back to the platform config directory when XDG_CONFIG_HOME is unset. Set RUST_LOG, for example RUST_LOG=schwab=debug, to enable tracing diagnostics on stderr without changing JSON stdout.
Use schwab-agent schema for machine-readable discovery. It reports the CLI version, supported commands and aliases, read-only/mutating/local-only classification, output formats, environment variables, exit codes, field selectors, and docs URL without requiring account data. Use schwab-agent doctor or schwab-agent config show to inspect sanitized setup and auth/environment health. config show produces the same sanitized output as config status, which reports config and token paths, file presence, credential sources, mutable-operation guard state, precedence, known environment variable names, and whether RUST_LOG is active. These discovery commands do not print tokens, client secrets, account numbers, account hashes, balances, or order IDs. High-frequency aliases are available for common guesses: quote for market quote, history for market history, orders for order get, and positions for account --positions. Legacy stock buy and stock sell are not restored; they return structured usage.migration errors with exact order equity buy or order equity sell replacements.
For the full LLM-facing command contract, workflows, and safety rules, see SKILL.md, which points to the detailed binary guide under src/bin/schwab-agent/.
SCHWAB_AGENT_JSON_ERRORS=1
Command-specific --help output includes copyable examples for the main market, option, technical analysis, and analyze workflows so agents can discover usage without leaving the terminal. analyze combines live quote data with TA values derived from completed candles; derived fields include analysis.derived.price_basis, price_basis_value, and price_basis_timestamp so downstream callers do not confuse live quote timestamps with the price used for daily indicator comparisons. schwab-agent completions <shell> generates raw shell completion scripts for bash, zsh, fish, powershell, and elvish; schwab-agent completion <shell> is a singular alias. Install bash completions by writing schwab-agent completions bash > schwab-agent.bash and sourcing that file from your shell startup, or install zsh completions by writing schwab-agent completion zsh > _schwab-agent into a directory on fpath. market history --from and --to accept YYYY-MM-DD, RFC3339, or epoch milliseconds. Date-only values use inclusive UTC calendar-day boundaries. Option chain and screen help list the valid --type values: call, put, and all. Option screen numeric filters reject non-finite values such as NaN and infinity before making API calls, and screen output serializes numeric values through the crate's active Number type so default and decimal builds stay consistent.
schwab-agent account and schwab-agent account --positions include balance cash_balance, true_cash, and true_cash_status fields for downstream funding decisions when Schwab provides them. Margin cash_balance comes from Schwab currentBalances.cashBalance, the Cash & Sweep Vehicle value shown in thinkorswim, and verified margin true_cash uses that value first. Use true_cash only when true_cash_status is verified; treat unavailable as a prompt for confirmation before selling SGOV or assuming idle cash. Do not treat buying_power, available_funds, cash_available_for_trading, cash_available_for_withdrawal, option buying power, or similar capacity fields as margin-safe cash for SGOV sweep and entry funding decisions.
Mutable order commands are disabled unless ~/.config/schwab-agent/config.json contains "i-also-like-to-live-dangerously": true. Before drafting or placing a symbol-specific order, use schwab-agent order get --symbol AAPL or its schwab-agent orders --symbol AAPL alias to inspect active open orders for that public ticker, adding --account HASH when the account scope is known. Use unfiltered order get or order get --account HASH when you need a broader conflict check across symbols or strategies. Use --order-id ORDER_ID as the canonical order ID spelling for exact order get, order replace, order repeat, and order cancel workflows; positional order IDs remain accepted for cancel and repeat compatibility, but mixed positional and --order-id values must agree. Direct equity and option builders support explicit local draft mode with --dry-run or its --preview alias; choose one local draft flag, and either one prints order JSON without requiring an account, auth token, Schwab preview API call, or placement. Omitting --account remains the same compatibility draft mode. The order session accepts normal/regular, am/pre, pm/post, and seamless/extended; duration accepts lowercase names plus uppercase aliases such as DAY and GTC. The recommended agent workflow for live orders is save-preview-then-place: pass --account HASH --save-preview to call Schwab previewOrder and save a digest, then submit the exact saved payload by digest after review. --preview-first is distinct: it calls Schwab preview and then places automatically if preview succeeds. Saved previews use owner-only file permissions, but they are tamper-evident rather than encrypted. Mutable actions resolve account nicknames to canonical Schwab hashes and perform best-effort post-action order verification.
Order builder
OrderBuilder creates serializable order payloads for place_order, replace_order, and preview_order. The common equity constructors choose the buy/sell instruction for you, and the option constructors choose buy-to-open, sell-to-open, buy-to-close, or sell-to-close for single-leg option orders. Lower-level equity_* and option_* constructors remain available when you need to pass an explicit instruction. Each public helper's rustdoc documents its arguments, default fields, serialized payload effects, and an example so downstream CLIs can generate command help from the API docs.
use ;
# async
Single-leg option helpers use the Schwab option symbol you pass and set assetType to OPTION:
use ;
let quantity: Number = "1".parse.unwrap;
let price: Number = "2.50".parse.unwrap;
let open = option_buy_to_open_market;
let close = option_sell_to_close_limit;
Compose orders before submission when the Schwab payload needs nested strategies. Use one_cancels_other for an OCO exit order, or first_triggers_second when the second order should stay pending until the first fills:
use ;
let quantity: Number = "1".parse.unwrap;
let limit_price: Number = "140.00".parse.unwrap;
let stop_price: Number = "120.00".parse.unwrap;
let oco = one_cancels_other;
let buy_with_stop_loss = first_triggers_second;
let bracket = first_triggers_second;
OrderBuilder can also rebuild supported historical orders returned by the Trader API. Conversion keeps request fields, validates common response-level quantity against supported single-leg payloads, omits response metadata such as order IDs and status, and fails with Error::OrderConversion when an order is partial or uses unsupported order or leg shapes.
use ;
# async
Supported repeat-order strategies are SINGLE, TRIGGER, and OCO with equity or option legs.
Streaming
Streaming support is built around StreamingSession, which owns a background WebSocket task and broadcasts typed StreamEvent values to any number of receivers. The session supports account activity, level-one equities, options, futures, futures options, forex, chart equity, chart futures, screener equity, and screener option subscriptions. All subscription methods share the same input trimming, field-index serialization, validation, active-subscription recording, and SUBS command delivery path. It sends LOGOUT on disconnect(), records active subscriptions, and replays them after reconnecting.
The streaming protocol parser accepts command response IDs as either JSON strings or numbers and maps data messages into typed account activity, level-one, chart, and screener payloads.
WebSocket transport failures surface as Error::WebSocket, while HTTP response bodies remain redacted in debug output.
Reconnect behavior uses 10 attempts with exponential backoff starting at 1 second, doubling to a 30 second cap, plus 0-500ms jitter. A LOGIN_DENIED response with code 3 stops reconnecting so callers can create a new session with fresh credentials.
Note: v1 does not refresh the bearer token after reconnect. If the token expires during a long-running session, create a new session with a fresh token.
use ;
# async
Feature flags
| Feature | Default | Purpose |
|---|---|---|
cli |
Yes | Enables the bundled schwab-agent binary and CLI-only dependencies, including clap parsing, shell completions, browser opening, config directory lookup, preview digests, and RUST_LOG tracing setup. |
decimal |
No | Enables rust_decimal support for models where decimal precision is preferable to floating-point values. |
test_online |
No | Enables live integration tests that call the Schwab API. Use only with explicit credentials and never in untrusted CI. |
Enable optional features with Cargo:
Run live tests only when you intentionally want network access:
Development
Use the Makefile for the same checks CI expects:
make check runs formatting, clippy, tests, and rustdoc checks. Clippy and tests run with default features, with --features decimal, with --lib --no-default-features, and with --lib --no-default-features --features decimal so the Number alias stays valid and library consumers can build without CLI dependencies.
Offline tests include cli feature-gated compiled-binary smoke checks for schwab-agent help output, shell completions, clap usage errors, structured JSON error output, and hermetic dry-run order payloads. Live Schwab API tests remain gated behind test_online.
make coverage runs offline tests through nightly cargo llvm-cov with the coverage_nightly cfg enabled and enforces 90% line coverage. It does not enable test_online, because live Schwab API tests require explicit credentials and must never run in CI.
make patch-coverage generates lcov.info and runs diff-cover against PATCH_COVERAGE_BASE (default main) with PATCH_COVERAGE_FAIL_UNDER (default 100). Set DIFF_COVER if you use uvx diff-cover or another wrapper.
make machete runs cargo machete to catch unused dependencies before CI does.
Generated lcov.info is ignored by git and CodeRabbit. CI pins the installed cargo-llvm-cov and cargo-machete versions, disables install-action fallback, gates Codecov upload with a non-secret presence flag, and scopes Codecov upload secrets only to the upload step.
Keep source, docs, fixtures, and copied API reference text ASCII unless the Schwab wire format explicitly requires Unicode. Decorative separators, mojibake, and non-breaking spaces can trigger Renovate hidden-Unicode warnings, so use plain ASCII equivalents.
Release automation
release-plz runs through .github/workflows/release-plz.yml. It keeps a release PR current from Conventional Commits and the cliff.toml changelog configuration, refuses dirty working trees, and does not update dependencies because Renovate owns dependency bumps through the org-level inherited config. This repo intentionally has no per-repo renovate.json unless a future repo-specific override is needed; validate that setup with npx --yes --package renovate renovate-config-validator before changing Renovate policy.
When the release PR is merged, release-plz creates the version tag. That tag triggers .github/workflows/release.yml, where cargo-dist builds schwab-agent binary artifacts, creates the GitHub Release, and publishes schwab to crates.io with GitHub Actions OIDC Trusted Publishing. The crates.io Trusted Publisher is configured for workflow file release.yml; never add CARGO_REGISTRY_TOKEN or another long-lived crates.io token.
API stability
schwab-rs is pre-1.0. Public APIs may change while the crate tracks Schwab API behavior and fills out coverage for Market Data and Trader endpoints. Pin an exact crate version for production use.
Minimum supported Rust version
This crate requires Rust 1.96 or later and uses Edition 2024.
License
Apache-2.0. See LICENSE for details.