ig_client/lib.rs
1//! # IG Markets API Client for Rust
2//!
3//! A comprehensive Rust client for the IG Markets trading API. This library
4//! provides a type-safe, async-first way to access IG Markets' REST and
5//! real-time streaming APIs for trading and market-data retrieval.
6//!
7//! ## Overview
8//!
9//! The IG Markets API Client for Rust offers a reliable interface to the IG
10//! Markets trading platform. It handles authentication and session management,
11//! automatic token refresh, rate limiting, finite retry with backoff, and
12//! real-time streaming over the Lightstreamer protocol, exposing a clean,
13//! idiomatic Rust API.
14//!
15//! ## Features
16//!
17//! - **Authentication**: IG session v2 (`CST` / `X-SECURITY-TOKEN` headers) and
18//! v3 (OAuth bearer) with automatic, transparent token refresh and account
19//! switching.
20//! - **Account Management**: Accounts, balances, positions, working orders,
21//! preferences, activity, and transaction history.
22//! - **Market Data**: Market search, instrument details, market navigation, and
23//! historical prices at several resolutions.
24//! - **Order Management**: Create, update, and close positions and working
25//! orders with typed request builders.
26//! - **Watchlists**: Full CRUD over watchlists and their instruments.
27//! - **Client Sentiment**: Sentiment for single, multiple, and related markets.
28//! - **Indicative Costs**: Costs and charges for opening, closing, or editing
29//! positions, plus cost history.
30//! - **Real-time Streaming** (feature `streaming`, on by default): Market,
31//! price, trade, and account updates over Lightstreamer, with thread-safe
32//! dynamic subscription management.
33//! - **Rate Limiting**: `governor`-backed pacing configured per IG's trading vs
34//! non-trading budgets.
35//! - **Finite Retry**: Exponential backoff with jitter via `RetryConfig`
36//! (bounded — no unbounded retry loops).
37//! - **Type Safety**: Strongly typed request / response DTOs and domain enums.
38//! - **Async**: Built on `tokio` with a shared, pooled `reqwest` client.
39//! - **Persistence** (feature `persistence`, on by default): PostgreSQL storage
40//! via `sqlx`.
41//!
42//! ## Installation
43//!
44//! Add this to your `Cargo.toml`:
45//!
46//! ```toml
47//! [dependencies]
48//! ig-client = "0.12.3"
49//! tokio = { version = "1", features = ["full"] } # Async runtime
50//! tracing = "0.1" # Logging facade
51//! # Optional, only if you use the PostgreSQL persistence layer:
52//! sqlx = { version = "0.9", features = ["runtime-tokio", "tls-native-tls", "postgres"] }
53//! ```
54//!
55//! You do not need to add `dotenv` yourself — `Config::new()` loads a local
56//! `.env` file internally. If your application supplies its own configuration
57//! and must not read a `.env` file or the `IG_*` namespace, use
58//! `Config::from_credentials()` with `Client::with_config()` instead (see
59//! [Programmatic configuration](#programmatic-configuration-embedders)).
60//!
61//! ### Cargo features
62//!
63//! | Feature | Default | Pulls in | Gives you |
64//! |---|---|---|---|
65//! | `streaming` | on | `lightstreamer-rs` | `StreamerClient`, `DynamicMarketStreamer`, `StreamingUpdate` and its DTO adapters, `application::interfaces::listener` |
66//! | `persistence` | on | `sqlx` | the `storage` module: `MarketDatabaseService`, historical prices, connection pooling |
67//!
68//! Both are on by default, so the crate behaves exactly as before unless you
69//! opt out. Turn them off for a REST/poll-only client:
70//!
71//! ```toml
72//! [dependencies]
73//! ig-client = { version = "0.12.3", default-features = false }
74//! ```
75//!
76//! That leaves `Client`, `Client::with_config`, every REST service trait
77//! (`MarketService`, `AccountService`, `OrderService`, …), the DTOs and the
78//! rate limiter fully available, with neither `lightstreamer-rs` nor `sqlx` in
79//! the dependency graph. Two reasons to care:
80//!
81//! - **Dependency surface**: `lightstreamer-rs` brings a WebSocket stack that
82//! a REST-only integration never opens. (It was GPL-3.0-only up to 0.3.x;
83//! 1.0 is a clean-room rewrite under MIT, so the licence reason for turning
84//! `streaming` off has gone away.)
85//! - **Build weight**: `sqlx` brings a full PostgreSQL driver that a
86//! market-data-only integration never uses.
87//!
88//! Turning `streaming` off removes the streaming client types; the
89//! `Streaming*Field` selectors and the presentation DTOs are plain data types
90//! and stay available either way. Turning `persistence` off removes everything
91//! under `storage` except `storage::config` (a re-export of the sqlx-free
92//! `application::config::DatabaseConfig`), along with the `AppError::Db`
93//! variant.
94//!
95//! One side effect worth knowing: `sqlx` enables `tracing`'s `log` feature, so
96//! with `persistence` on this crate's events are also emitted as `log` records.
97//! With it off they are tracing events only, which matters if your application
98//! collects logs through the `log` facade.
99//!
100//! ### Requirements
101//!
102//! - Rust 2024 edition on the stable toolchain.
103//! - An IG Markets account (demo or live) and API credentials.
104//! - A PostgreSQL database (optional, only for the persistence layer).
105//!
106//! ## Configuration
107//!
108//! `Config::new()` reads configuration from the environment (and a local `.env`
109//! file, if present). Create a `.env` file in your project root with the
110//! following variables:
111//!
112//! ```text
113//! IG_USERNAME=your_username
114//! IG_PASSWORD=your_password
115//! IG_API_KEY=your_api_key
116//! IG_ACCOUNT_ID=your_account_id
117//! IG_API_VERSION=3 # 2 (CST/XST) or 3 (OAuth); defaults to 3
118//! IG_REST_BASE_URL=https://demo-api.ig.com/gateway/deal # Use demo or live as needed
119//! IG_REST_TIMEOUT=30 # REST request timeout in seconds
120//! IG_WS_URL=wss://demo-apd.marketdatasystems.com # Lightstreamer endpoint
121//! IG_WS_RECONNECT_INTERVAL=5 # Reconnect interval in seconds
122//! IG_RATE_LIMIT_MAX_REQUESTS=4 # Rate-limiter budget
123//! IG_RATE_LIMIT_PERIOD_SECONDS=12 # Rate-limiter period (seconds)
124//! IG_RATE_LIMIT_BURST_SIZE=3 # Rate-limiter burst size
125//! DATABASE_URL=postgres://user:password@localhost/ig_db # Optional persistence
126//! DATABASE_MAX_CONNECTIONS=5 # Optional connection-pool size
127//! TX_LOOP_INTERVAL_HOURS=1 # Transaction loop interval (hours)
128//! TX_PAGE_SIZE=20 # Transaction page size
129//! TX_DAYS_LOOKBACK=7 # Days to look back for transactions
130//! ```
131//!
132//! Live examples and integration tests default to the IG **demo** environment;
133//! pointing anything at production requires an explicit opt-in via
134//! `IG_REST_BASE_URL` / `IG_WS_URL`.
135//!
136//! ### Programmatic configuration (embedders)
137//!
138//! There are two configuration paths and they do not mix:
139//!
140//! - `Config::new()` / `Client::try_new()` — the **convenience path**. Loads a
141//! local `.env` file and reads the global `IG_*` namespace, as above.
142//! - `Config::from_credentials()` / `Client::with_config()` — the **injection
143//! path**. Reads no environment variable and loads no `.env` file, for an
144//! embedding application that owns its configuration source (its own
145//! namespaced variables, a config file, a secrets manager). Useful under
146//! `#![forbid(unsafe_code)]`, where pre-populating `IG_*` is not an option
147//! because `std::env::set_var` is `unsafe` on edition 2024.
148//!
149//! ```rust,no_run
150//! use ig_client::prelude::*;
151//!
152//! // Fail fast on a missing variable: an empty credential would only surface
153//! // later as a confusing authentication failure. Never log the value.
154//! fn required_var(name: &str) -> Result<String, AppError> {
155//! std::env::var(name).map_err(|_| AppError::InvalidInput(format!("{name} is not set")))
156//! }
157//!
158//! # fn main() -> Result<(), AppError> {
159//! let credentials = Credentials::new(
160//! required_var("MYAPP_IG_USERNAME")?,
161//! required_var("MYAPP_IG_PASSWORD")?,
162//! required_var("MYAPP_IG_ACCOUNT_ID")?,
163//! required_var("MYAPP_IG_API_KEY")?,
164//! );
165//!
166//! // Struct update overrides individual sections and stays env-free,
167//! // because the base value is the env-free constructor.
168//! let config = Config {
169//! rest_api: RestApiConfig {
170//! base_url: "https://demo-api.ig.com/gateway/deal".to_string(),
171//! timeout: 30,
172//! },
173//! ..Config::from_credentials(credentials)
174//! };
175//!
176//! let client = Client::with_config(config)?;
177//! println!("REST base URL: {}", client.config().rest_api.base_url);
178//! # Ok(())
179//! # }
180//! ```
181//!
182//! Non-credential fields default to the IG **demo** endpoints; see
183//! `examples/simples/src/bin/client_with_config.rs` for a runnable version.
184//!
185//! For streaming (feature `streaming`), build the streamer from the injected
186//! client with `StreamerClient::with_client(&client)` — `StreamerClient::new()`
187//! goes through `Client::try_new()` and therefore back to `.env` / `IG_*`.
188//!
189//! Two knobs are still resolved from the process environment on the injected
190//! path, because they are not part of `Config`:
191//!
192//! - Retry policy — `MAX_RETRY_COUNT` and `RETRY_DELAY_SECS` are read per
193//! request via `RetryConfig::default()`; unset means the crate defaults.
194//! - `IG_PRICING_ADAPTER` — the Lightstreamer price adapter name, defaulting
195//! to `Pricing` when unset.
196//!
197//! Neither carries a credential, and both have safe defaults, so an embedder
198//! that sets neither variable is unaffected. Moving them into `Config` is a
199//! breaking change scheduled for a future minor release.
200//!
201//! ## Usage
202//!
203//! The main entry points are `Config` and `Client`. A single `Client`
204//! implements every REST service trait (`AccountService`, `MarketService`,
205//! `OrderService`, `WatchlistService`, `SentimentService`, `CostsService`,
206//! `OperationsService`). Session login and token refresh are performed
207//! transparently on first use. The prelude re-exports the full public surface,
208//! including the streaming API and the service traits, so a single glob import
209//! is usually enough:
210//!
211//! ```rust
212//! use ig_client::prelude::*;
213//! ```
214//!
215//! ### Client setup and account information
216//!
217//! ```rust,no_run
218//! use ig_client::prelude::*;
219//!
220//! #[tokio::main]
221//! async fn main() -> Result<(), AppError> {
222//! // Configuration is read from the environment / a local `.env` file.
223//! let config = Config::new();
224//! println!("configured API version: {:?}", config.api_version);
225//!
226//! // `try_new` is the fallible constructor for the REST client.
227//! // (`new()` / `default()` no longer exist.) Session login and automatic
228//! // token refresh happen transparently on the first API call.
229//! let client = Client::try_new()?;
230//!
231//! // `Client` implements `AccountService`.
232//! let accounts = client.get_accounts().await?;
233//! println!("{} account(s) available", accounts.accounts.len());
234//! Ok(())
235//! }
236//! ```
237//!
238//! ### Market data
239//!
240//! ```rust,no_run
241//! use ig_client::prelude::*;
242//!
243//! #[tokio::main]
244//! async fn main() -> Result<(), AppError> {
245//! let client = Client::try_new()?;
246//!
247//! // `Client` implements `MarketService`.
248//! let results = client.search_markets("EUR/USD").await?;
249//! if let Some(market) = results.markets.first() {
250//! let details = client.get_market_details(&market.epic).await?;
251//! println!("{}: {}", market.epic, details.instrument.name);
252//! }
253//! Ok(())
254//! }
255//! ```
256//!
257//! ### Placing an order
258//!
259//! ```rust,no_run
260//! use ig_client::prelude::*;
261//!
262//! #[tokio::main]
263//! async fn main() -> Result<(), AppError> {
264//! let client = Client::try_new()?;
265//!
266//! // Build a market order with the typed constructor. `size` is rounded to
267//! // two decimals; a `None` currency code defaults to "EUR".
268//! let order = CreateOrderRequest::market(
269//! "CS.D.EURUSD.CFD.IP".to_string(), // epic
270//! Direction::Buy, // direction
271//! 1.0, // size
272//! Some("USD".to_string()), // currency_code
273//! None, // deal_reference
274//! );
275//!
276//! // `Client` implements `OrderService`.
277//! let confirmation = client.create_order(&order).await?;
278//! println!("deal reference: {}", confirmation.deal_reference);
279//! Ok(())
280//! }
281//! ```
282//!
283//! ### Real-time streaming
284//!
285//! *Requires the default `streaming` feature.*
286//!
287//! `DynamicMarketStreamer` wraps the lower-level `StreamerClient` and manages
288//! subscriptions in a thread-safe way. Its constructor is **synchronous** — it
289//! only wires up in-memory channels; the network connection is established later
290//! by `start`. Updates arrive as `PriceData`, resolved through the prelude.
291//!
292//! ```rust,no_run
293//! use ig_client::prelude::*;
294//! use std::collections::HashSet;
295//!
296//! # #[cfg(feature = "streaming")]
297//! #[tokio::main]
298//! async fn main() -> Result<(), AppError> {
299//! // Fields to receive on each market tick.
300//! let fields = HashSet::from([StreamingMarketField::Bid, StreamingMarketField::Offer]);
301//!
302//! // Synchronous construction — no `await`, no fallibility.
303//! let mut streamer = DynamicMarketStreamer::new(fields);
304//!
305//! // Take the receiver before connecting.
306//! let mut receiver = streamer.get_receiver().await?;
307//!
308//! // Subscribe to an instrument and open the Lightstreamer connection.
309//! streamer.add("IX.D.DAX.DAILY.IP".to_string()).await?;
310//! streamer.start().await?;
311//!
312//! // Consume `PriceData` updates.
313//! while let Some(price) = receiver.recv().await {
314//! let price: PriceData = price;
315//! println!("price update: {price}");
316//! }
317//! Ok(())
318//! }
319//! # #[cfg(not(feature = "streaming"))]
320//! # fn main() {}
321//! ```
322//!
323//! ## Available Services
324//!
325//! Every service trait below is implemented by `Client`; bring the traits into
326//! scope via `use ig_client::prelude::*;`.
327//!
328//! ### `AccountService`
329//! - `get_accounts()` — all accounts for the authenticated user
330//! - `get_positions()` / `get_positions_w_filter(filter)` — open positions
331//! - `get_working_orders()` — working orders
332//! - `get_activity(from, to)` / `get_activity_with_details(from, to)` — activity
333//! - `get_activity_by_period(period_ms)` — activity for a period in milliseconds
334//! - `get_transactions(from, to)` — transaction history (paginated internally)
335//! - `get_preferences()` / `update_preferences(trailing_stops_enabled)`
336//!
337//! ### `MarketService`
338//! - `search_markets(term)` — search markets by keyword
339//! - `get_market_details(epic)` / `get_multiple_market_details(epics)`
340//! - `get_historical_prices(epic, resolution, from, to)` and
341//! `get_historical_prices_by_date_range(epic, resolution, start, end)`
342//! - `get_historical_prices_by_count_v1` / `_v2(epic, resolution, num_points)`
343//! - `get_recent_prices(params)`
344//! - `get_market_navigation()` / `get_market_navigation_node(node_id)`
345//! - `get_all_markets()` / `get_vec_db_entries()`
346//! - `get_categories()` / `get_category_instruments(category_id, page, size)`
347//!
348//! ### `OrderService`
349//! - `create_order(request)` — open a position
350//! - `get_order_confirmation(deal_reference)` and
351//! `get_order_confirmation_w_retry(deal_reference, retries, delay_ms)`
352//! - `update_position(deal_id, update)` / `update_level_in_position(deal_id, level)`
353//! - `close_position(request)` / `get_position(deal_id)`
354//! - `create_working_order(request)` / `update_working_order(deal_id, update)` /
355//! `delete_working_order(deal_id)`
356//!
357//! ### `WatchlistService`
358//! - `get_watchlists()` / `create_watchlist(name, epics)`
359//! - `get_watchlist(id)` / `delete_watchlist(id)`
360//! - `add_to_watchlist(id, epic)` / `remove_from_watchlist(id, epic)`
361//!
362//! ### `SentimentService`
363//! - `get_client_sentiment(market_ids)`
364//! - `get_client_sentiment_by_market(market_id)`
365//! - `get_related_sentiment(market_id)`
366//!
367//! ### `CostsService`
368//! - `get_indicative_costs_open(request)` / `_close(request)` / `_edit(request)`
369//! - `get_costs_history(from, to)` / `get_durable_medium(quote_reference)`
370//!
371//! ### `OperationsService`
372//! - `get_client_apps()` — API application details
373//! - `disable_client_app()` — disable the current API key
374//!
375//! ## Rate Limiting
376//!
377//! All requests are paced by a `governor`-backed `RateLimiter` so the client
378//! stays within IG's published limits (trading and non-trading endpoints have
379//! different budgets). The budget is configured from the environment via
380//! `IG_RATE_LIMIT_MAX_REQUESTS`, `IG_RATE_LIMIT_PERIOD_SECONDS`, and
381//! `IG_RATE_LIMIT_BURST_SIZE` (see `RateLimiterConfig`). On top of pacing,
382//! transient failures (429 / 5xx / connection errors) are retried with
383//! **finite** exponential backoff and jitter via `RetryConfig`; non-idempotent
384//! trading calls are never retried blindly.
385//!
386//! ## Architecture
387//!
388//! The crate is organized as a module-oriented library under `src/`:
389//!
390//! - **`application`** — all I/O. The REST `Client` and its service-trait
391//! implementations, `Auth` / `Session` (login, refresh, logout, account
392//! switching), `Config`, the `RateLimiter`, and the streaming layer
393//! (`StreamerClient`, `DynamicMarketStreamer`). Service traits live under
394//! `application::interfaces`.
395//! - **`model`** — pure request / response DTOs, streaming message DTOs, session
396//! DTOs, and the retry policy. Serde only; no I/O.
397//! - **`presentation`** — domain entities per area: account, chart, instrument,
398//! market, order, price, trade, transaction. Serde only; no I/O.
399//! - **`storage`** — PostgreSQL persistence via `sqlx`, behind the
400//! `persistence` feature (sits on top of `model` / `presentation`).
401//! - **`utils`** — leaf helpers: env-var config, logging, finance (P&L), parsing,
402//! deal-reference id generation.
403//! - **`error`** — canonical typed error enums: `AppError`, `AuthError`, and
404//! `FetchError`.
405//! - **`constants`** — crate-wide endpoint paths, header names, and defaults.
406//! - **`prelude`** — curated public re-exports (the recommended import surface).
407//!
408//! ### API Documentation
409//!
410//! Browse the API documentation on [docs.rs](https://docs.rs/ig-client) or
411//! generate it locally with:
412//!
413//! ```bash
414//! make doc-open
415//! ```
416//!
417//! ## Project Structure
418//!
419//! ```text
420//! src/
421//! ├── application/ # I/O: client, auth, config, rate limiter, streaming
422//! │ ├── interfaces/ # Service traits (account, market, order, costs, …)
423//! │ ├── auth.rs # Auth / Session: login, refresh, logout, switch
424//! │ ├── client.rs # Client (REST services) + StreamerClient
425//! │ ├── config.rs # Config, Credentials, REST / WS / rate-limiter config
426//! │ ├── rate_limiter.rs # governor-backed request pacing
427//! │ └── dynamic_streamer.rs # DynamicMarketStreamer (subscriptions)
428//! ├── model/ # Pure DTOs: requests, responses, streaming, retry
429//! ├── presentation/ # Domain entities (account, market, order, price, …)
430//! ├── storage/ # PostgreSQL persistence via sqlx (feature `persistence`)
431//! ├── utils/ # config, logger, finance, parsing, id helpers
432//! ├── constants.rs # Endpoint paths, header names, defaults
433//! ├── error.rs # AppError / AuthError / FetchError
434//! ├── prelude.rs # Curated public re-exports
435//! └── lib.rs # Public API and crate docs (README source)
436//! examples/ # Runnable demos (workspace members)
437//! tests/ # unit/ and env-gated integration/ tests
438//! benches/ # Criterion benchmarks
439//! ```
440//!
441//! ## Development
442//!
443//! This project includes a Makefile with common development tasks:
444//!
445//! ```bash
446//! make build # Debug build
447//! make release # Release build
448//! make test # Run all tests
449//! make fmt # Format code with rustfmt
450//! make lint # Run clippy
451//! make readme # Regenerate README.md from these crate docs
452//! make pre-push # Run all checks before pushing
453//! ```
454//!
455//! ## Contributing
456//!
457//! Contributions are welcome:
458//!
459//! 1. Fork the repository.
460//! 2. Create a feature branch: `git checkout -b feature/my-feature`.
461//! 3. Make your changes and commit them.
462//! 4. Run the checks: `make pre-push`.
463//! 5. Push the branch and open a pull request.
464//!
465//! Please make sure your code passes all tests and linting checks before
466//! submitting a pull request.
467
468// Enables the "Available on crate feature ..." badges on docs.rs. Inert on
469// stable: nothing sets `docsrs` outside the docs.rs builder.
470#![cfg_attr(docsrs, feature(doc_cfg))]
471
472/// Core application logic and services
473pub mod application;
474
475/// User interface and presentation layer
476pub mod presentation;
477
478/// Module containing global constants used throughout the library
479pub mod constants;
480
481/// Error types and handling
482pub mod error;
483
484/// Data persistence and storage
485pub mod storage;
486
487/// Data models for IG Markets API entities
488pub mod model;
489/// Prelude module for convenient imports
490pub mod prelude;
491/// Utility functions and helpers
492pub mod utils;
493
494/// Library version
495pub const VERSION: &str = env!("CARGO_PKG_VERSION");
496
497/// Returns the library version
498pub fn version() -> &'static str {
499 VERSION
500}