questrade-client 0.1.0

Async Rust client for the Questrade REST API with automatic OAuth token management
Documentation

questrade-client

Crates.io Docs.rs CI

Async Rust client for the Questrade REST API.

Handles OAuth token refresh, typed market-data access (quotes, option chains, candles), and account-data access (positions, balances, activities, orders, executions).

Features

  • Automatic OAuth token management with single-use refresh token rotation
  • Token caching to skip OAuth round-trips on subsequent runs
  • Transparent 401 retry — forces a token refresh and retries once on Unauthorized
  • Rate-limit retry with exponential backoff and jitter on 429 responses
  • Fully typed request/response types with serde deserialization
  • tracing instrumented for structured logging at debug/trace levels
  • Raw response logging mode for API debugging

Quick start

Add to your Cargo.toml:

[dependencies]
questrade-client = "0.1"
tokio = { version = "1", features = ["full"] }
anyhow = "1"
use questrade_client::{TokenManager, QuestradeClient};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let manager = TokenManager::new(
        "your_refresh_token".to_string(),
        false, // false = live account, true = practice account
        None,  // optional token-refresh callback
        None,  // optional cached token to skip initial refresh
    ).await?;

    let client = QuestradeClient::new(manager)?;
    let accounts = client.get_accounts().await?;
    println!("accounts: {:?}", accounts);
    Ok(())
}

Auth flow

Questrade uses OAuth 2.0 with single-use refresh tokens. Every time you exchange a refresh token for an access token, the old refresh token is invalidated and a new one is returned. If you lose the rotated token, you must generate a new one from the Questrade API Hub.

Token persistence

Pass an OnTokenRefresh callback to TokenManager::new to persist the rotated token after every automatic refresh:

use std::sync::Arc;
use questrade_client::{OnTokenRefresh, TokenManager, CachedToken};

let on_refresh: OnTokenRefresh = Arc::new(|token| {
    // Save token.refresh_token to disk, database, etc.
    // Also save token.access_token + token.api_server for caching
    std::fs::write("/tmp/refresh_token", &token.refresh_token).ok();
});

let manager = TokenManager::new(
    refresh_token,
    false,
    Some(on_refresh),
    None, // or pass a CachedToken to skip the initial refresh
).await?;

To skip the OAuth round-trip on subsequent runs, pass a CachedToken:

let cached = CachedToken {
    access_token: "saved_access_token".to_string(),
    api_server: "https://api01.iq.questrade.com/".to_string(),
    expires_at: saved_expiry,
};

let manager = TokenManager::new(
    refresh_token,
    false,
    Some(on_refresh),
    Some(cached),
).await?;

See the token_manager example for a complete working implementation.

API coverage

Category Method Questrade endpoint
Auth TokenManager::new GET /oauth2/token
Server get_server_time GET /v1/time
Markets get_markets GET /v1/markets
Symbols resolve_symbol GET /v1/symbols/search
Symbols get_symbol GET /v1/symbols/:id
Quotes get_raw_quote GET /v1/markets/quotes/:id
Options get_option_chain_structure GET /v1/symbols/:id/options
Options get_option_quotes_by_ids POST /v1/markets/quotes/options
Options get_option_quotes_raw POST /v1/markets/quotes/options
Candles get_candles GET /v1/markets/candles/:id
Accounts get_accounts GET /v1/accounts
Positions get_positions GET /v1/accounts/:id/positions
Balances get_account_balances GET /v1/accounts/:id/balances
Activities get_activities GET /v1/accounts/:id/activities
Orders get_orders GET /v1/accounts/:id/orders
Executions get_executions GET /v1/accounts/:id/executions
Raw get_text Any GET /v1/* endpoint

Not yet implemented

Endpoint Description Notes
POST /v1/markets/quotes/strategies Multi-leg strategy quotes (up to 4 legs) Read-only (read_md scope). Useful for evaluating spreads, iron condors, etc.
POST /v1/accounts/:id/orders Place a new order Requires trade scope (partner-only)
POST /v1/accounts/:id/orders/:orderId Replace/modify an existing order Requires trade scope (partner-only)
DELETE /v1/accounts/:id/orders/:orderId Cancel an order Requires trade scope (partner-only)
POST /v1/accounts/:id/orders/:orderId/impact Preview order impact (buying power, commission) Requires trade scope (partner-only)

The order management endpoints require the trade OAuth scope, which Questrade restricts to approved partner developers.

Partial coverage notes

  • POST /v1/markets/quotes/options supports two modes: by explicit IDs (implemented) and by filters (underlyingId + expiryDate + optional strike range + option type). Only the ID-based mode is currently exposed.
  • GET /v1/symbols/search is wrapped by resolve_symbol() which returns only the first exact-match symbol ID. The raw search response (multiple partial matches) is not directly exposed as a public method.

Automatic windowing

The get_activities and get_executions methods automatically split date ranges longer than 30 days into compliant sub-windows (Questrade limits queries to 31-day windows). Results are combined and sorted chronologically.

Examples

License

MIT License. See LICENSE for details.