oanda-client 1.0.0

An async, provider-native Rust client for OANDA's v20 REST API
Documentation
oanda-client-1.0.0 has been yanked.

oanda-rs

CI crates.io docs.rs MSRV: Rust 1.95.0 license: MIT-0 v20 operations: 32/32 typed Practice probes: 3/3 passed

An async, provider-native Rust client for the OANDA REST-v20 API. The Cargo package and import name are oanda-client and oanda_client; -rs belongs to the repository name. This is an independent, unofficial client, unaffiliated with OANDA. It is designed as an inner provider client that SharurPlatform can later consume through its own adapter. The adapter is outside this repository.

The project is licensed under MIT No Attribution (MIT-0). The minimum supported Rust version is 1.95.0.

Version 1 follows Semantic Versioning. Public API changes that require downstream source changes will be released under a new major version; additive APIs and fixes use minor and patch releases. Provider contract changes can still require callers to update operational behavior, so review the changelog before upgrading and keep recovery around ambiguous money-moving outcomes.

Status

The six OANDA v20 endpoint pages and their linked definitions, reviewed on 2026-09-25, are the contract authority. The pinned official OpenAPI revision is a cross-check; documented differences are recorded rather than silently adopting older operations.

Capability Current surface
Documented operations 32/32 callable: 30 REST methods and 2 HTTP streams
Documented definitions 168/168 typed contracts
Variants 503 scalar enum values and 55 tagged order, request, and transaction variants tested locally
Environments Paired Practice and Live REST and stream hosts
Runtime and credentials Caller-owned Tokio runtime and injected bearer token
Authenticated validation Three read-only Practice probes and one temporary limit-order creation/cancellation probe passed on 2026-09-25; no Live environment testing

The exhaustive coverage ledger names each operation and definition, its source URL, public method or type, and local test. The coverage notes explain the website/OpenAPI differences, the offline CI check, and the rejection-evidence contract. The LAW-INVARIANT audit records the semantic review of fabricated fallbacks and swallowed failures, with a disposition for every candidate.

Installation

cargo add oanda-client@1

Or add the current major release directly:

[dependencies]
oanda-client = "1"

The complete public API is available on docs.rs.

The crate uses Rust edition 2024 and exposes capability modules for accounts, orders, trades, positions, transactions, pricing, instruments, transport, and primitives.

Quick start

The caller obtains and stores the token. The library does not read .env files or discover credentials from ambient process state.

use oanda_client::{AccountID, Client, Environment};

#[tokio::main(flavor = "current_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let token = std::env::var("OANDA_TOKEN")?;
    let account = AccountID::new(std::env::var("OANDA_ACCOUNT_ID")?)?;
    let client = Client::builder(Environment::Practice, token).build()?;
    let summary = client.account_summary(&account).await?;
    println!("request ID: {:?}", summary.request_id);
    println!("last transaction: {:?}", summary.last_transaction_id);
    Ok(())
}

This is the runnable read-only Practice example. With those two environment variables set, run cargo run --example practice_account. Select Environment::Live only after validating the application and its account reconciliation flow.

API and transport

  • Account details, summary, instruments, configuration, and changes are available alongside order, trade, position, transaction, candle, and pricing methods. Responses include typed provider fields, typed rejection bodies, request IDs, and pagination metadata where documented.
  • A definitive rejection reports only what OANDA sent. Its code and message are None when OANDA supplied no readable reason, and its typed body is a Supplied<T> that separates a body OANDA did not send from one that arrived and could not be decoded. The client never invents a reason. Upgrading from an earlier build: Rejected::message and Error::Provider::message are now Option<String>, Rejected::body is Supplied<R>, and acknowledge_reconciliation returns Result — match arms and acknowledgement call sites need updating. A response's request_id and next_page are likewise Supplied: Undecoded means OANDA offered a value this client could not read, which is a different fact from no value being offered.
  • Prices, units, balances, and other financial values use exact rust_decimal::Decimal values parsed from OANDA decimal strings. Price-bucket liquidity also accepts JSON numbers exactly. Required and optional financial fields reject nonrepresentable coefficient/scale values during deserialization rather than rounding them. Provider IDs and timestamps have dedicated types.
  • REST response bodies are bounded. Cloned clients share conservative rate admission and provider cooldown, which honours both Retry-After forms RFC 9110 defines. Requests are single-attempt; a mutation is never automatically retried.
  • An ambiguous mutation fences further mutations for its account across client clones. Read OANDA account and transaction state before calling acknowledge_reconciliation, which reports whether it released the fence.
  • Pricing and transaction streams use HTTP/1.1, including when a consumer enables HTTP/2 on reqwest through Cargo feature unification. REST protocol negotiation is independent. The streams are incremental and newline-delimited, with typed heartbeats. Records are bounded individually by a local hostile-input limit of 1 MiB, and the buffered backlog by a local cap of 16 MiB that ends the generation explicitly rather than evicting records already accepted. HTTP chunk segmentation is a transport read, not a provider record boundary, so a large chunk carrying many valid records is processed in order rather than rejected, and buffered records are never evicted for a slow caller. Malformed or oversized records, timeouts, and connection loss end that stream generation and require caller-owned recovery. The library does not reconnect or maintain account and price projections.
  • OANDA's pricing stream is sampled at most four prices per second per instrument. It does not carry every price update.

Read SECURITY.md before handling credentials or mutations.

Optional exit reserve

The outer wrapper opts in and sizes the reserve for its workload. The default is zero. ClientBuilder::exit_reserve(R) sets the initial policy; Client::set_exit_reserve(R) replaces it at runtime, and zero releases the reserve. It keeps R requests inside the existing conservative local 100-request, one-second window: ordinary requests enter below 100 - R, and eligible exits enter below 100. R must be less than 100. This local cap follows OANDA's established-connection recommendation.

Operation Admission
cancel_order, close_trade, close_position Can consume reserved request capacity
Reads, stream starts, placement, replacement, account configuration and other mutations Ordinary capacity
All requests Same aggregate window and provider cooldown across one client and all clones

For example, a wrapper can pass its chosen reserve when constructing its shared client:

use oanda_client::{Client, Environment, Result};

fn configured_client(token: String, reserve_requests: u32) -> Result<Client> {
    Client::builder(Environment::Practice, token)
        .exit_reserve(reserve_requests)
        .build()
}

fn update_reserve(client: &Client) -> Result<()> {
    let reads = client.clone();
    reads.set_exit_reserve(40)?;
    client.set_exit_reserve(0)?;
    Ok(())
}

The configuration and runtime changes are exercised by the credential-free configuration example: cargo run --example exit_reserve. The example's number is illustrative, not a sizing recommendation. Share clones for reads and exits. One outer coordinator owns this shared policy: serialized updates replace the previous value, with the last update winning; they are not additive leases or promised permits. Every admission attempt reads the current policy. A change wakes waiting reads and stream starts so they can reconsider capacity immediately. Changes never refund requests, reset timestamps, clear cooldowns or revoke admitted attempts. Raising the reserve after the aggregate budget is spent creates no new exit capacity; releasing it restores ordinary access only to unspent capacity. Independently constructed clients and separate processes do not share these counters; the caller must coordinate those consumers of the same provider quota.

[!IMPORTANT] This is finite shared capacity, not an exclusive or unlimited Flatten allowance. Provider cooldown, the aggregate limit, account reconciliation fences and in-flight mutation exclusion still apply to exits. A mutation refused by local rate admission returns Error::RateLimited immediately without a wire attempt; an admitted mutation is sent once, never queued or retried. Reads and stream starts can wait. The separate stream-count and connection-start limits are unchanged. Trading state, reserve sizing, account selection and recovery remain the outer wrapper's work.

Testing status

Normal CI needs no OANDA credentials. It runs formatting, Clippy, tests, documentation, package verification, dependency and license checks, full-history secret scanning, and the offline coverage check on Rust 1.95. Clippy uses pedantic warnings as errors. The manual release-readiness workflow repeats the Rust, coverage, and supply-chain checks, checks the requested version, and runs a crates.io publish dry run; it does not publish a release. Dependabot checks Cargo and GitHub Actions updates weekly. The 30 REST methods have deterministic loopback success and rejection fixtures, including method, URL, query, header, and mutation-body checks. Both streams have local framing, heartbeat, error, and gap tests. These tests establish the documented wire surface; they do not establish behavior against an authenticated OANDA account.

Weekly and manually dispatched CI also check the public OANDA release notes for versions beyond the last reviewed baseline, initially 3.0.25. Newer versions and fetch/parsing failures fail the dedicated job with an actionable summary. PR and push runs test the checker offline. See release-note monitoring for the review and baseline-update procedure.

Ignored, explicitly armed read-only Practice probes cover representative REST queries and both streams:

OANDA_READ_ONLY_PROBE=I_ACCEPT_READ_ONLY_PRACTICE \
OANDA_TOKEN=... OANDA_ACCOUNT_ID=... \
cargo test --features live-tests --test practice_read_only -- --ignored

All three read-only Practice probes passed on 2026-09-25. A separate, temporary probe placed a one-unit EUR/USD limit order well below the current bid, observed a price and heartbeat on the pricing stream, received creation and cancellation updates on the transaction stream, and verified the order's final state was CANCELLED. The temporary probe was removed; no mutation probe runs in CI or remains in this repository. See coverage and contract drift for the reviewed source and verification details.

Contributor build cache

Kache is optional for local builds. Use the pinned version 1.0.0. GitHub-hosted CI uses the repository's Rust cache setup without Kache. Start with the official Kache quick start, configuration reference and CI guide.

cargo install kache --version 1.0.0 --locked
kache --version

From the repository root, enable it for the current shell:

export RUSTC_WRAPPER=kache
export KACHE_CONFIG="$PWD/.kache.toml"
export KACHE_LOCAL_ONLY=1
export KACHE_DEFERRED_DISCOVERY=0
export KACHE_MODIFIED_INPUT_GUARD=1
export KACHE_KEY_SALT=sharurtrading-kache-prepass-2026-10-11
export KACHE_VERIFY_RESTORES=always
export CARGO_INCREMENTAL=0
export CARGO_TARGET_DIR="$PWD/target/kache-prepass-v1"
export KACHE_CACHE_DIR="$PWD/target/.kache-cache/prepass-v1"

Run the repository's usual Cargo quality commands after this setup. .kache.toml defines the reviewed policy:

Setting Behavior
local_only = true Disable Kache remote backends and the planner
deferred_discovery = false Hash discovered inputs before compilation
modified_input_guard = true Reject inputs newer than the build start
key_salt Isolate entries under this discovery policy
gc_max_age_hours = 48 Use Kache GC for age retention
KACHE_VERIFY_RESTORES=always Verify restored blob checksums

[!IMPORTANT] Keep source files unchanged while a build runs; use a separate checkout for concurrent edits. The input guard and checksum verification do not establish cache-key correctness for arbitrary edits during compilation. Cargo can reuse already-fresh outputs without calling Kache, so investigate a suspect result in a fresh target directory with the wrapper disabled:

CARGO_TARGET_DIR="$(mktemp -d)" RUSTC_WRAPPER= cargo check --locked

Inspect reuse with kache stats --last-build, kache explain and kache doctor. For persistent local setup, kache init --check previews Cargo configuration changes before kache init applies them. Environment variables take precedence over the repository file; retain the policy above and restart any existing daemon when changing its environment, as described in the configuration reference.

CI build cache

GitHub-hosted Rust jobs use Swatinem/rust-cache to persist Cargo build outputs and downloaded dependencies. Kache is used only when a contributor opts into local builds with the setup above. CI retains its existing quality gates and runner selections.

License

Licensed under the MIT No Attribution License (MIT-0). Notable changes between releases are recorded in the changelog.