# AGENTS.md — tradingkit
## Project Overview
Rust library crate for cryptocurrency and equity exchange trading. Provides
exchange-agnostic domain models, a data-access layer with cache-first reads,
and concrete exchange adapters (Alpaca live/paper, OKX signing). Uses serde for
serialization, reqwest for HTTP, and HMAC-SHA256 for payload signing.
**Workspace layout:**
```
tradingkit/ # Library crate (Rust 2024 edition)
src/
lib.rs # Crate root — module declarations + crate-level docs only
crypto.rs # HMAC-SHA256 signing, base64 encoding
model.rs # Exchange-agnostic domain types (orders, accounts, options)
data/
mod.rs # Source / Store / Repository abstractions, InMemoryStore, DataApi
exchange/
mod.rs # Credentials, RequestHeaders, Exchange traits
alpaca.rs # Alpaca REST adapter + Source impls + TradingApi/DataApi impls
okx.rs # OKX credential/signature adapter
trading/
mod.rs # TradingApi trait for write-side order operations
strategy/
test-strategy/ # Binary crate (Rust 2021 edition) — depends on tradingkit
```
## Toolchain
- **Rust nightly** (1.94.0-nightly) — required for `edition = "2024"`.
- No `rust-toolchain.toml` — ensure nightly is active: `rustup default nightly`.
- No `rustfmt.toml`, `clippy.toml`, or `.cargo/config.toml`.
## Build / Lint / Test Commands
```bash
cargo build # Build entire workspace
cargo build -p tradingkit # Build only the library
cargo build -p test-strategy # Build only the test-strategy binary
cargo check # Check without codegen (fast)
cargo clippy -- -D warnings # Lint
cargo fmt # Format
cargo fmt --check # CI-style format check
cargo test # Run all tests (unit + doc-tests)
cargo test -p tradingkit # Run tests for a single crate
cargo test -p tradingkit <name> # Run a single test by name substring
cargo test -p tradingkit <name> -- --nocapture # Single test with stdout
cargo run -p test-strategy # Run the test-strategy binary (needs .env)
```
## Dependencies
| `base64` (0.22) | Base64 encoding for signatures |
| `dotenvy` (0.15) | `.env` file loading for credentials |
| `hmac` (0.12) | HMAC message authentication |
| `reqwest` (0.12) | HTTP client (with `json` feature) |
| `reqwest-websocket` | WebSocket support via reqwest |
| `serde` (1.0) | Serialization/deserialization (derive) |
| `serde_json` (1.0) | JSON parsing and generation |
| `sha2` (0.10) | SHA-256 hashing |
| `tokio` (dev-dep) | Async runtime for tests |
## Code Style
### Module Structure
- Crate root (`lib.rs`) contains **only `mod`/`pub mod` declarations** and `//!` crate docs.
- Directory modules use `mod.rs` files (not the flat `module_name.rs` style).
- Submodule declarations go at the **bottom** of `mod.rs`, after all type and trait definitions.
### Imports
Grouped in this order, separated by blank lines:
```rust
use super::{Credentials, Exchange, RequestHeaders}; // 1. Parent re-exports
use std::collections::HashMap; // 2. Standard library
use std::error::Error;
use reqwest::{Client, Method}; // 3. External crates
use serde::{Deserialize, Serialize};
use crate::model::{Order, OrderRequest, TradingAccount}; // 4. Internal crate
use tradingkit::trading::TradingApi; // 5. Public trait imports
```
- Import specific items — no glob imports (exception: `use super::*;` in `#[cfg(test)]` modules).
- Alias long paths at import site when used repeatedly (e.g., `STANDARD as base64`).
- In newer modules (alpaca), prefer explicit `use super::{...}` over `use super::*`.
### Naming Conventions
| Functions | `snake_case` | `encode_base64`, `send_get` |
| Variables | `snake_case` | `secret_key`, `base_url` |
| Types/Structs | `PascalCase` | `OKXCredentials`, `Alpaca` |
| Traits | `PascalCase` | `TradingApi`, `Source`, `Store` |
| Constants | `SCREAMING_SNAKE` | `ALPACA_PAPER_URL` |
| Modules | `snake_case` | `exchange`, `crypto`, `data` |
| Crates | `kebab-case` | `test-strategy` |
| Enums | `PascalCase` variants| `OrderSide::Buy`, `AssetClass::UsOption` |
- Exchange-specific types are prefixed with the exchange name: `Alpaca*`, `OKX*`.
- Private exchange-specific request/response DTOs: `AlpacaOrderRequest`, `AlpacaOrderResponse`.
- Canonical (public) types live in `model.rs` without exchange prefix.
### Error Handling
- All fallible functions return `Result<T, Box<dyn Error>>`.
- Propagate errors with the `?` operator — no manual matching/unwrapping.
- For quick ad-hoc errors: `std::io::Error::other("message")` converted to `Box<dyn Error>`.
- No custom error types — `Box<dyn Error>` is the current convention throughout.
### Type Patterns
- Use `&str` and `&[u8]` for function parameters (idiomatic Rust borrowing).
- Struct fields own their data (`String`, not `&str`).
- Use field shorthand in constructors when variable name matches field name.
- Request DTOs use lifetime references (`AlpacaOrderRequest<'a>`) borrowing from canonical types.
### Serde Patterns
- Canonical domain types derive `Serialize, Deserialize` and live in `model.rs`.
- Exchange-specific DTOs are private; convert via `From`/`TryFrom` impls to canonical types.
- Enums use `#[serde(rename_all = "snake_case")]` or `"lowercase"` as the API requires.
- Unknown enum variants use `#[serde(other)]` catch-all: `Unknown`.
- Optional request fields use `#[serde(skip_serializing_if = "Option::is_none")]`.
- Response field name mapping via `#[serde(rename = "...")]` or `#[serde(alias = "...")]`.
### Async Trait Pattern
Async methods in traits use native `async fn` with `#[allow(async_fn_in_trait)]`:
```rust
#[allow(async_fn_in_trait)]
pub trait TradingApi {
async fn submit_order(&self, order: &OrderRequest) -> Result<Order, Box<dyn Error>>;
}
```
### Formatting
- Default `rustfmt` settings (no config file). 4-space indentation.
- Trailing commas in multi-line struct literals and function args.
- Opening brace on same line as declaration.
### Documentation
- Module-level `//!` doc comments on all public modules.
- `///` doc comments on all public types, traits, and methods.
- Include `# Examples` sections with runnable doc-tests.
- Use `rust,no_run` for examples requiring network/env; `rust` for pure examples.
## Architecture Notes
### Trait Hierarchy
- `Credentials` — payload signing (`sign`).
- `RequestHeaders` — HTTP header construction for auth.
- `Exchange` — associated `Credentials` type + constructor.
- `TradingApi` — write-only trading surface in `trading/mod.rs` (submit, replace, cancel, cancel_all).
- `DataApi` — read-only exchange data surface in `data/mod.rs` (get_account, get_order, get_order_by_client_order_id, list_orders, get_option_chain).
- `Source<Q, T>` / `Store<Q, T>` — data-access read/write abstractions.
- `Repository<S, C>` — cache-first facade composing Source + Store.
### Adding a New Exchange
1. Create `src/exchange/<name>.rs`.
2. Define `<Name>Credentials` implementing `Credentials` (and `RequestHeaders` if needed).
3. Define `<Name>` struct implementing `Exchange`, `TradingApi`, and `DataApi`.
4. Add `Source<...>` impls in the exchange adapter file to integrate with the data layer.
5. Add `pub mod <name>;` at the bottom of `src/exchange/mod.rs`.
### Strategy Crates
Strategy binaries live under `strategy/` and are separate workspace members.
They depend on `tradingkit` via path: `tradingkit = { path = "../../" }`.
Add new strategies to `[workspace] members` in the root `Cargo.toml`.
## Testing Guidelines
- **Unit tests**: In-file `#[cfg(test)] mod tests { use super::*; ... }`.
- **Async tests**: `#[tokio::test]` (tokio is already a dev-dependency).
- **Doc-tests**: All public items have `# Examples` sections that are compiled as tests.
- **Test naming**: `test_<subject>_<action>_<expected_behavior>`.
- **Fakes**: Use trait-implementing test doubles (e.g., `CountingSource` in `data/mod.rs`).
- Run `cargo test` to execute all 9 unit tests + 24 doc-tests.
## Common Pitfalls
- The `base64` crate requires importing the `Engine` trait alongside the engine constant.
- `Hmac::new_from_slice` can fail — always propagate the error with `?`.
- `reqwest` and async traits require a tokio runtime — strategy binaries use `#[tokio::main]`.
- Workspace uses mixed editions (2024 root, 2021 test-strategy) — be mindful of differences.
- Credentials loaded from `.env` via `dotenvy::dotenv()` — the `.env` file is not committed.
- Private exchange DTOs exist alongside public canonical types — convert via `From`/`TryFrom`.