Skip to main content

Crate rustyschwab

Crate rustyschwab 

Source
Expand description

§rustyschwab

An unofficial Rust SDK for the Charles Schwab Trader and Market Data APIs, not affiliated with or endorsed by Charles Schwab & Co., Inc.: OAuth with encrypted or keychain token storage, a REST client with retries, a shared rate budget and a circuit breaker, the streamer protocol, a durable order journal, and a paper-trading venue.

Wire types live in rustyschwab-types and are re-exported as rustyschwab::types. The repository README covers the MCP server, operations and the full documentation set.

[dependencies]
rustyschwab = { version = "0.2", features = ["callback-server"] }
tokio = { version = "1", features = ["full"] }

callback-server adds the local HTTPS listener that captures the OAuth redirect. It serves the certificate in OAuthConfig::callback_cert_file and callback_key_file (for example one from mkcert 127.0.0.1, which the browser trusts), or a self-signed one the browser warns about. AuthManager::authorize_with hands you the consent URL before it waits. Without the feature, AuthManager::authorize returns the URL and you pass the redirect to exchange_code yourself.

§Configure and authorize

SchwabConfig::from_env reads the SCHWAB_* variables listed in the repository’s .env.example (SCHWAB_APP_KEY, SCHWAB_APP_SECRET, SCHWAB_OAUTH_CALLBACK_URL, SCHWAB_TOKENS_FILE, SCHWAB_TOKEN_STORE, …) and validates them.

use rustyschwab::prelude::*;

let client = SchwabClient::new(SchwabConfig::from_env()?)?;
let auth = client.auth_manager();
if !auth.has_valid_tokens() {
    // Opens nothing by itself: print the URL, sign in, and the callback
    // server (or `exchange_code`) stores the tokens.
    let (url, _code) = auth.authorize().await?;
    println!("Sign in at {url}");
}
client.init().await?; // background refresh

§Read market data and accounts

use rustyschwab::prelude::*;
use rustyschwab::types::market_data::{FrequencyType, PeriodType, PriceHistoryParams};

let quotes = client.get_quotes(&["AAPL", "MSFT"]).await?;
let history = client
    .get_price_history(&PriceHistoryParams {
        period_type: Some(PeriodType::Day),
        period: Some(10),
        frequency_type: Some(FrequencyType::Minute),
        frequency: Some(5),
        ..PriceHistoryParams::new("AAPL")
    })
    .await?;
let accounts = client.get_account_numbers().await?;

§Place an order

Orders are built from exact decimals and validated before anything leaves the process. A submission is sent once and never retried: an ambiguous outcome is Error::SubmissionUnknown, and the account’s next submission waits until the journal reconciles it against the broker.

use rustyschwab::SchwabClient;
use rustyschwab::types::Decimal;
use rustyschwab::types::trading::{OrderInstruction, OrderRequest, OrderRequestLeg};

// Buy 10 AAPL at 150.25, then take profit at 160 or stop out at 145.
let order = OrderRequest::limit(
    OrderRequestLeg::equity(OrderInstruction::Buy, "AAPL", 10),
    "150.25".parse()?,
)
.bracket(Decimal::from(160), Decimal::from(145))?;

let response = client.place_order(account_hash, &order).await?;
println!("order id: {:?}", response.order_id);

§Stream quotes

The streamer identity (customer ID, channel, function ID) comes from the user-preferences response. Consumers must handle StreamMessage::State: when ready is false or the generation changes, derived state is stale.

use rustyschwab::{SchwabClient, StreamClient, StreamMessage};
use rustyschwab::config::StreamConfig;
use rustyschwab::types::streaming::{LevelOneEquity, StreamService};

let prefs = client.get_user_preferences().await?;
let info = prefs
    .streamer_info
    .as_deref()
    .and_then(<[_]>::first)
    .ok_or("no streamer info")?;
let stream = StreamClient::builder()
    .config(StreamConfig::default())
    .auth_manager(client.auth_manager())
    .streamer_info(info)
    .build()?;
stream.connect().await?;
stream
    .subscribe(StreamService::LeveloneEquities, vec!["AAPL".into()])
    .await?;

let mut receiver = stream.get_receiver().ok_or("receiver already taken")?;
while let Some(message) = receiver.recv().await {
    match message {
        StreamMessage::State(status) if !status.ready => {
            // Session lost or messages dropped: re-baseline from REST.
        }
        StreamMessage::Data(data) => {
            for item in data.data.iter().filter(|i| i.service() == Some(StreamService::LeveloneEquities)) {
                for content in &item.content {
                    let quote = LevelOneEquity::from_content(content)?;
                    println!("{} bid {:?} ask {:?}", quote.key, quote.bid_price, quote.ask_price);
                }
            }
        }
        _ => {}
    }
}

§Features

FeatureDefaultAdds
rustlsyesrustls TLS for HTTP and WebSocket
native-tlsnothe platform TLS stack instead
callback-servernolocal HTTPS OAuth callback capture
schemarsnoJSON Schema derives on the request types
telemetryyesrustyschwab::telemetry: ready-made tracing-subscriber setups
otelnotelemetry::init_otel: OTLP/HTTP export of spans and, through the metrics facade, metrics, configured by the standard OTEL_* environment
fullnorustls, callback-server, schemars, telemetry

§License

MIT or Apache-2.0, at your option.

Re-exports§

pub use auth::AuthManager;
pub use auth::AuthResult;
pub use auth::OAuthConfig;
pub use auth::TokenSet;
pub use circuit_breaker::CircuitBreaker;
pub use circuit_breaker::CircuitState;
pub use client::SchwabClient;
pub use client::SchwabClientBuilder;
pub use config::ChannelKind;
pub use config::CircuitBreakerConfig;
pub use config::ClientConfig;
pub use config::SchwabConfig;
pub use config::StreamConfig;
pub use error::Error;
pub use error::ErrorEnvelope;
pub use error::ErrorKind;
pub use error::Result;
pub use streaming::MessageReceiver;
pub use streaming::StreamClient;
pub use streaming::Subscription;
pub use rustyschwab_types as types;

Modules§

audit_chain
Tamper evidence for the audit table.
audit_delivery
Acknowledged, replay-safe delivery to an organization’s immutable audit sink.
auth
OAuth 2.0 authentication with PKCE and secure token storage.
broker
Common gateway execution boundary for live and isolated paper backends.
circuit_breaker
Circuit breaker pattern for cascading failure prevention.
client
Main Schwab API client for REST endpoints.
config
Configuration types for the Schwab SDK.
coordination
Coordination contract for local workers or a deployment-owned remote service.
error
Error types and handling for the Schwab SDK.
execution
Durable submission intent/outcome journal. Pending intents survive task cancellation and process failure, and block new submissions for that account.
history
A durable local cache of price history and recorded stream data.
metrics
Metric names, label conventions, and small helpers for SDK-side instrumentation.
paper
Isolated, durable long-equity paper execution and deterministic replay.
performance
Performance analytics over fills and mark-to-market equity curves.
prelude
Convenient re-exports for common Schwab SDK usage.
readiness
Readiness is local evidence, not a synthetic live broker probe.
reconciliation
Broker reconciliation is conservative: absence never proves rejection.
recovery
Restartable account recovery worker. Consumers can subscribe to readiness.
remote
Authenticated remote backends sharing one transactional authority. Use the broker gateway when workers must not possess broker credentials.
retry
Retry policy with exponential backoff and configurable behavior.
risk
Pluggable pre-trade risk with durable reservations for outstanding orders.
security
Security Module
storage
Private, transactional local state. SQLite files must be on a local filesystem.
streaming
telemetrytelemetry
Telemetry and observability initialization for the Schwab SDK.
transport
HTTP and WebSocket transport layer.
utils
Helpers shared by the digest, audit and reconciliation paths.

Structs§

RequestBudgetUsage
The in-process request budget of one credential at one instant.

Enums§

StreamMessage

Constants§

VERSION
Library version string in the format rustyschwab X.Y.Z.