# alpaca-data

`alpaca-data` is an async Rust client for the Alpaca Market Data HTTP API.

Quick links:

- crates.io: [https://crates.io/crates/alpaca-data](https://crates.io/crates/alpaca-data)
- Documentation: [https://wmzhai.github.io/alpaca-data-rs/](https://wmzhai.github.io/alpaca-data-rs/)

The crate is built around two constraints:

- The official Alpaca Market Data HTTP API is the only semantic source of truth.
- Public Rust naming is idiomatic Rust, while request and response field words use the official API terms.

## Status

- Current branch baseline: `v0.11.0`
- Implemented resource families: `stocks`, `options`, `crypto`, `news`, `corporate_actions`
- This repository does not cover Trading API, Broker API, WebSocket, or SSE
- This crate is async-only
- crates.io package: `alpaca-data`
- Public docs include a GitHub Pages site, generated API reference pages, rustdoc links, API coverage docs, and a tag-triggered release workflow

Release automation is intentionally tag-triggered only and follows GitHub-hosted `stable`. After the initial manual crates.io bootstrap release, the `github-pages` workflow runs the standard Rust validation steps, publishes the crate to crates.io through Trusted Publishing, deploys the documentation build, and then creates or updates the matching GitHub Release from the corresponding `CHANGELOG.md` section only on pushed release tags such as `vX.Y.Z`. The manifest intentionally omits `rust-version` until an audited MSRV policy exists.

## Design Contract

### Official HTTP API first

- Endpoint semantics follow the official Alpaca Market Data API.
- Request fields and response fields use the original official words.
- Rust-specific adaptation is kept minimal and only used where the language requires it, such as `r#type`.

### Idiomatic Rust public API

- Crate name: `alpaca_data`
- Root client: `alpaca_data::Client`
- Resource accessors: `stocks()`, `options()`, `crypto()`, `news()`, `corporate_actions()`
- Modules are lowercase, types are `PascalCase`, methods and fields are `snake_case`

### Two API layers

The crate exposes two layers:

- Mirror layer: direct Rust wrappers for the official HTTP endpoints
- Convenience layer: `*_all` and `*_stream` helpers on top of official paginated endpoints

The convenience layer never changes the official payload words. It only automates pagination.

Typed numeric public API fields and request filters use exact `rust_decimal::Decimal` values so the client preserves API precision and trailing-zero scale with exact decimal decoding.

### Request guardrails stay official-only

- The crate fails fast with `Error::InvalidRequest` only for clearly documented Alpaca request rules.
- Examples include empty required symbol lists, blank required single-path identifiers such as `symbol` or `underlying_symbol`, documented `limit` bounds, the options symbol-list cap, and the corporate-actions `ids` exclusivity rule.
- The crate does not silently auto-chunk mirror requests to work around documented hard limits.

## Coverage Summary

### Stocks

- Historical batch: `auctions`, `bars`, `quotes`, `trades`
- Historical single-symbol: `auctions_single`, `bars_single`, `quotes_single`, `trades_single`
- Convenience: `auctions_all`, `auctions_stream`, `bars_all`, `bars_stream`, `quotes_all`, `quotes_stream`, `trades_all`, `trades_stream`
- Single-symbol convenience: `auctions_single_all`, `auctions_single_stream`, `bars_single_all`, `bars_single_stream`, `quotes_single_all`, `quotes_single_stream`, `trades_single_all`, `trades_single_stream`
- Latest: `latest_bars`, `latest_bar`, `latest_quotes`, `latest_quote`, `latest_trades`, `latest_trade`
- Snapshots and metadata: `snapshots`, `snapshot`, `condition_codes`, `exchange_codes`

### Options

- Historical: `bars`, `trades`
- Convenience: `bars_all`, `bars_stream`, `trades_all`, `trades_stream`
- Latest: `latest_quotes`, `latest_trades`
- Snapshot family: `snapshots`, `snapshots_all`, `snapshots_stream`, `chain`, `chain_all`, `chain_stream`
- Metadata: `condition_codes`, `exchange_codes`

### Crypto

- Historical: `bars`, `quotes`, `trades`
- Convenience: `bars_all`, `bars_stream`, `quotes_all`, `quotes_stream`, `trades_all`, `trades_stream`
- Latest: `latest_bars`, `latest_quotes`, `latest_trades`, `latest_orderbooks`
- Snapshots: `snapshots`

### News

- `list`, `list_all`, `list_stream`

### Corporate Actions

- `list`, `list_all`, `list_stream`

## Quick Start

Install from crates.io:

```toml
[dependencies]
alpaca-data = "0.11.0"
rust_decimal = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

Build a client:

```rust
use alpaca_data::Client;

let client = Client::new();
let client = Client::builder().build()?;
# Ok::<(), alpaca_data::Error>(())
```

Tune transport retries when a service integration needs stronger transient-failure handling:

```rust
use std::time::Duration;
use alpaca_data::Client;

let client = Client::builder()
    .timeout(Duration::from_secs(5))
    .max_retries(2)
    .retry_on_429(true)
    .respect_retry_after(true)
    .base_backoff(Duration::from_millis(100))
    .max_backoff(Duration::from_millis(500))
    .max_in_flight(32)
    .build()?;
# let _ = client;
# Ok::<(), alpaca_data::Error>(())
```

The default retry behavior stays conservative: retrying 429 responses and honoring `Retry-After` remain opt-in.

When you set `total_retry_budget(...)`, the retry loop is bounded by the remaining elapsed-time budget, and each scheduled retry wait is capped by that remaining budget, including waits derived from `Retry-After` and waits with jitter enabled.

Inject a custom `reqwest::Client` when you need to own the underlying network stack:

```rust
use std::time::Duration;
use alpaca_data::Client;

let reqwest_client = reqwest::Client::builder()
    .timeout(Duration::from_secs(5))
    .build()?;

let client = Client::builder()
    .reqwest_client(reqwest_client)
    .max_retries(2)
    .retry_on_429(true)
    .respect_retry_after(true)
    .base_backoff(Duration::from_millis(100))
    .max_backoff(Duration::from_millis(500))
    .max_in_flight(32)
    .build()?;
# let _ = client;
# Ok::<(), Box<dyn std::error::Error>>(())
```

When `reqwest_client(...)` is used, reqwest-level settings such as `timeout(...)` must be configured on the injected client instead of on `ClientBuilder`.

Optionally load paired credentials from process environment when a service prefers env-driven wiring:

```rust
use alpaca_data::Client;

let client = Client::builder()
    .credentials_from_env()?
    .build()?;
# let _ = client;
# Ok::<(), alpaca_data::Error>(())
```

Observe successful transport completions without changing endpoint return types:

```rust
use std::sync::Arc;
use alpaca_data::{Client, ObservedResponseMeta, TransportObserver};

struct LoggingObserver;

impl TransportObserver for LoggingObserver {
    fn on_response(&self, meta: &ObservedResponseMeta) {
        println!(
            "endpoint={}, status={}, request_id={:?}",
            meta.endpoint_name, meta.status, meta.request_id
        );
    }
}

let client = Client::builder()
    .observer(Arc::new(LoggingObserver))
    .build()?;
# let _ = client;
# Ok::<(), alpaca_data::Error>(())
```

Observers only receive successful-response metadata such as endpoint name, URL, status, request ID, retry attempt count, and elapsed time.

Fetch stock latest bars:

```rust
use alpaca_data::{Client, stocks};

#[tokio::main]
async fn main() -> Result<(), alpaca_data::Error> {
    let client = Client::builder()
        .api_key(std::env::var("APCA_API_KEY_ID").expect("APCA_API_KEY_ID is required"))
        .secret_key(std::env::var("APCA_API_SECRET_KEY").expect("APCA_API_SECRET_KEY is required"))
        .build()?;

    let response = client
        .stocks()
        .latest_bars(stocks::LatestBarsRequest {
            symbols: vec!["AAPL".into()],
            feed: None,
            currency: None,
        })
        .await?;

    println!("{response:?}");
    Ok(())
}
```

Collect all pages from a paginated endpoint:

```rust
use alpaca_data::{Client, news};

#[tokio::main]
async fn main() -> Result<(), alpaca_data::Error> {
    let client = Client::builder()
        .api_key(std::env::var("APCA_API_KEY_ID").expect("APCA_API_KEY_ID is required"))
        .secret_key(std::env::var("APCA_API_SECRET_KEY").expect("APCA_API_SECRET_KEY is required"))
        .build()?;

    let response = client
        .news()
        .list_all(news::ListRequest {
            start: Some("2026-04-01T00:00:00Z".into()),
            end: Some("2026-04-04T00:00:00Z".into()),
            sort: Some(news::Sort::Desc),
            symbols: Some(vec!["AAPL".into()]),
            limit: Some(50),
            include_content: Some(false),
            exclude_contentless: Some(true),
            page_token: None,
        })
        .await?;

    println!("{}", response.news.len());
    Ok(())
}
```

## Authentication

Authentication behavior follows the implemented endpoint rules:

- `stocks`, `options`, `news`, and `corporate_actions` require credentials
- The currently implemented `crypto` HTTP endpoints can run without credentials

The primary application path is still explicit credentials on the builder:

```rust
use alpaca_data::Client;

let client = Client::builder()
    .api_key(std::env::var("APCA_API_KEY_ID").expect("APCA_API_KEY_ID is required"))
    .secret_key(std::env::var("APCA_API_SECRET_KEY").expect("APCA_API_SECRET_KEY is required"))
    .build()?;
# let _ = client;
# Ok::<(), alpaca_data::Error>(())
```

`credentials_from_env()` and `credentials_from_env_names(...)` are optional ergonomics for integrations that already manage credentials through environment variables. They only load paired values; a partial pair returns `Error::InvalidConfiguration`.

Live tests in this repository use:

- `APCA_API_KEY_ID`
- `APCA_API_SECRET_KEY`
- `ALPACA_LIVE_TESTS=1`

If you keep local secrets in a `.env` file under different names such as `ALPACA_DATA_API_KEY` and `ALPACA_DATA_SECRET_KEY`, export them into the `APCA_*` names before running live tests or examples.

See [docs/authentication.md](docs/authentication.md) for the current auth contract.

## Documentation Map

<!-- docs-site:start -->
- Documentation site: https://wmzhai.github.io/alpaca-data-rs/
- Primary API host after publish: https://docs.rs/alpaca-data
- Project Pages route: `https://wmzhai.github.io/alpaca-data-rs/`
- Local generation: `./tools/docs/generate-doc-site`
- Local site install: `npm install --prefix website`
- Local site start: `npm run start --prefix website`
- Local site build: `npm run build --prefix website`
- API reference sections:
  - [Documentation](docs/index.md)
  - [Getting Started](docs/getting-started.md)
  - [Authentication](docs/authentication.md)
  - [Layers](docs/layers.md)
  - [Project Structure](docs/project-structure.md)
  - [API Reference](docs/reference/index.md)
  - [API Coverage](docs/api-coverage.md)
  - [Examples](docs/examples.md)
  - [Release Checklist](docs/release-checklist.md)
- Generated module references: `docs/reference/stocks.md`, `docs/reference/options.md`, `docs/reference/crypto.md`, `docs/reference/news.md`, `docs/reference/corporate-actions.md`, `docs/reference/common.md`, `docs/reference/transport.md`
<!-- docs-site:end -->


## API Audit Script

The repository includes one local entry point for official Market Data API audit work.

Requirements:

- `bash`
- `curl`
- `jq`

Run the read-only parity audit against the local coverage manifest and source tree:

```bash
./scripts/api-sync-audit
```

The script accepts no command-line arguments. It prints the complete audit report directly to the terminal and exits non-zero when blocking drift is detected. The report covers mirror-path coverage, parameter signatures, response-field signatures, enum gaps, and convenience helpers that require re-validation after mirror changes.

## Testing

Default checks:

```bash
cargo test
```

Enable live tests:

```bash
ALPACA_LIVE_TESTS=1 cargo test --test live_crypto_latest_quotes_smoke -- --nocapture
ALPACA_LIVE_TESTS=1 cargo test --test live_stocks_batch_historical -- --nocapture
ALPACA_LIVE_TESTS=1 cargo test --test live_stocks_single_historical -- --nocapture
ALPACA_LIVE_TESTS=1 cargo test --test live_stocks_latest_snapshot -- --nocapture
ALPACA_LIVE_TESTS=1 cargo test --test live_stocks_metadata -- --nocapture
ALPACA_LIVE_TESTS=1 cargo test --test live_options_historical -- --nocapture
ALPACA_LIVE_TESTS=1 cargo test --test live_options_latest_metadata -- --nocapture
ALPACA_LIVE_TESTS=1 cargo test --test live_options_snapshots_chain -- --nocapture
ALPACA_LIVE_TESTS=1 cargo test --test live_news -- --nocapture
ALPACA_LIVE_TESTS=1 cargo test --test live_corporate_actions -- --nocapture
```

Normal success-path coverage must use the real Alpaca API whenever credentials can cover the scenario. Mocks are reserved for fault injection paths such as malformed JSON, repeated pagination tokens, timeouts, and HTTP failures.

## Benchmarks

Local micro-benchmark baselines currently live in:

- `benches/shared_core.rs`
- `benches/stocks.rs`
- `benches/options.rs`
- `benches/crypto.rs`
- `benches/news_corporate_actions.rs`

Compile benchmark targets without running a full sample:

```bash
cargo bench --no-run
```
