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**: Market, price, trade, and account updates over
31//! Lightstreamer, with thread-safe dynamic subscription management.
32//! - **Rate Limiting**: `governor`-backed pacing configured per IG's trading vs
33//! non-trading budgets.
34//! - **Finite Retry**: Exponential backoff with jitter via `RetryConfig`
35//! (bounded — no unbounded retry loops).
36//! - **Type Safety**: Strongly typed request / response DTOs and domain enums.
37//! - **Async**: Built on `tokio` with a shared, pooled `reqwest` client.
38//! - **Persistence (optional)**: PostgreSQL storage via `sqlx`.
39//!
40//! ## Installation
41//!
42//! Add this to your `Cargo.toml`:
43//!
44//! ```toml
45//! [dependencies]
46//! ig-client = "0.12.1"
47//! tokio = { version = "1", features = ["full"] } # Async runtime
48//! tracing = "0.1" # Logging facade
49//! # Optional, only if you use the PostgreSQL persistence layer:
50//! sqlx = { version = "0.9", features = ["runtime-tokio", "tls-native-tls", "postgres"] }
51//! ```
52//!
53//! You do not need to add `dotenv` yourself — `Config::new()` loads a local
54//! `.env` file internally.
55//!
56//! ### Requirements
57//!
58//! - Rust 2024 edition on the stable toolchain.
59//! - An IG Markets account (demo or live) and API credentials.
60//! - A PostgreSQL database (optional, only for the persistence layer).
61//!
62//! ## Configuration
63//!
64//! `Config::new()` reads configuration from the environment (and a local `.env`
65//! file, if present). Create a `.env` file in your project root with the
66//! following variables:
67//!
68//! ```text
69//! IG_USERNAME=your_username
70//! IG_PASSWORD=your_password
71//! IG_API_KEY=your_api_key
72//! IG_ACCOUNT_ID=your_account_id
73//! IG_API_VERSION=3 # 2 (CST/XST) or 3 (OAuth); defaults to 3
74//! IG_REST_BASE_URL=https://demo-api.ig.com/gateway/deal # Use demo or live as needed
75//! IG_REST_TIMEOUT=30 # REST request timeout in seconds
76//! IG_WS_URL=wss://demo-apd.marketdatasystems.com # Lightstreamer endpoint
77//! IG_WS_RECONNECT_INTERVAL=5 # Reconnect interval in seconds
78//! IG_RATE_LIMIT_MAX_REQUESTS=4 # Rate-limiter budget
79//! IG_RATE_LIMIT_PERIOD_SECONDS=12 # Rate-limiter period (seconds)
80//! IG_RATE_LIMIT_BURST_SIZE=3 # Rate-limiter burst size
81//! DATABASE_URL=postgres://user:password@localhost/ig_db # Optional persistence
82//! DATABASE_MAX_CONNECTIONS=5 # Optional connection-pool size
83//! TX_LOOP_INTERVAL_HOURS=1 # Transaction loop interval (hours)
84//! TX_PAGE_SIZE=20 # Transaction page size
85//! TX_DAYS_LOOKBACK=7 # Days to look back for transactions
86//! ```
87//!
88//! Live examples and integration tests default to the IG **demo** environment;
89//! pointing anything at production requires an explicit opt-in via
90//! `IG_REST_BASE_URL` / `IG_WS_URL`.
91//!
92//! ## Usage
93//!
94//! The main entry points are `Config` and `Client`. A single `Client`
95//! implements every REST service trait (`AccountService`, `MarketService`,
96//! `OrderService`, `WatchlistService`, `SentimentService`, `CostsService`,
97//! `OperationsService`). Session login and token refresh are performed
98//! transparently on first use. The prelude re-exports the full public surface,
99//! including the streaming API and the service traits, so a single glob import
100//! is usually enough:
101//!
102//! ```rust
103//! use ig_client::prelude::*;
104//! ```
105//!
106//! ### Client setup and account information
107//!
108//! ```rust,no_run
109//! use ig_client::prelude::*;
110//!
111//! #[tokio::main]
112//! async fn main() -> Result<(), AppError> {
113//! // Configuration is read from the environment / a local `.env` file.
114//! let config = Config::new();
115//! println!("configured API version: {:?}", config.api_version);
116//!
117//! // `try_new` is the fallible constructor for the REST client.
118//! // (`new()` / `default()` no longer exist.) Session login and automatic
119//! // token refresh happen transparently on the first API call.
120//! let client = Client::try_new()?;
121//!
122//! // `Client` implements `AccountService`.
123//! let accounts = client.get_accounts().await?;
124//! println!("{} account(s) available", accounts.accounts.len());
125//! Ok(())
126//! }
127//! ```
128//!
129//! ### Market data
130//!
131//! ```rust,no_run
132//! use ig_client::prelude::*;
133//!
134//! #[tokio::main]
135//! async fn main() -> Result<(), AppError> {
136//! let client = Client::try_new()?;
137//!
138//! // `Client` implements `MarketService`.
139//! let results = client.search_markets("EUR/USD").await?;
140//! if let Some(market) = results.markets.first() {
141//! let details = client.get_market_details(&market.epic).await?;
142//! println!("{}: {}", market.epic, details.instrument.name);
143//! }
144//! Ok(())
145//! }
146//! ```
147//!
148//! ### Placing an order
149//!
150//! ```rust,no_run
151//! use ig_client::prelude::*;
152//!
153//! #[tokio::main]
154//! async fn main() -> Result<(), AppError> {
155//! let client = Client::try_new()?;
156//!
157//! // Build a market order with the typed constructor. `size` is rounded to
158//! // two decimals; a `None` currency code defaults to "EUR".
159//! let order = CreateOrderRequest::market(
160//! "CS.D.EURUSD.CFD.IP".to_string(), // epic
161//! Direction::Buy, // direction
162//! 1.0, // size
163//! Some("USD".to_string()), // currency_code
164//! None, // deal_reference
165//! );
166//!
167//! // `Client` implements `OrderService`.
168//! let confirmation = client.create_order(&order).await?;
169//! println!("deal reference: {}", confirmation.deal_reference);
170//! Ok(())
171//! }
172//! ```
173//!
174//! ### Real-time streaming
175//!
176//! `DynamicMarketStreamer` wraps the lower-level `StreamerClient` and manages
177//! subscriptions in a thread-safe way. Its constructor is **synchronous** — it
178//! only wires up in-memory channels; the network connection is established later
179//! by `start`. Updates arrive as `PriceData`, resolved through the prelude.
180//!
181//! ```rust,no_run
182//! use ig_client::prelude::*;
183//! use std::collections::HashSet;
184//!
185//! #[tokio::main]
186//! async fn main() -> Result<(), AppError> {
187//! // Fields to receive on each market tick.
188//! let fields = HashSet::from([StreamingMarketField::Bid, StreamingMarketField::Offer]);
189//!
190//! // Synchronous construction — no `await`, no fallibility.
191//! let mut streamer = DynamicMarketStreamer::new(fields);
192//!
193//! // Take the receiver before connecting.
194//! let mut receiver = streamer.get_receiver().await?;
195//!
196//! // Subscribe to an instrument and open the Lightstreamer connection.
197//! streamer.add("IX.D.DAX.DAILY.IP".to_string()).await?;
198//! streamer.start().await?;
199//!
200//! // Consume `PriceData` updates.
201//! while let Some(price) = receiver.recv().await {
202//! let price: PriceData = price;
203//! println!("price update: {price}");
204//! }
205//! Ok(())
206//! }
207//! ```
208//!
209//! ## Available Services
210//!
211//! Every service trait below is implemented by `Client`; bring the traits into
212//! scope via `use ig_client::prelude::*;`.
213//!
214//! ### `AccountService`
215//! - `get_accounts()` — all accounts for the authenticated user
216//! - `get_positions()` / `get_positions_w_filter(filter)` — open positions
217//! - `get_working_orders()` — working orders
218//! - `get_activity(from, to)` / `get_activity_with_details(from, to)` — activity
219//! - `get_activity_by_period(period_ms)` — activity for a period in milliseconds
220//! - `get_transactions(from, to)` — transaction history (paginated internally)
221//! - `get_preferences()` / `update_preferences(trailing_stops_enabled)`
222//!
223//! ### `MarketService`
224//! - `search_markets(term)` — search markets by keyword
225//! - `get_market_details(epic)` / `get_multiple_market_details(epics)`
226//! - `get_historical_prices(epic, resolution, from, to)` and
227//! `get_historical_prices_by_date_range(epic, resolution, start, end)`
228//! - `get_historical_prices_by_count_v1` / `_v2(epic, resolution, num_points)`
229//! - `get_recent_prices(params)`
230//! - `get_market_navigation()` / `get_market_navigation_node(node_id)`
231//! - `get_all_markets()` / `get_vec_db_entries()`
232//! - `get_categories()` / `get_category_instruments(category_id, page, size)`
233//!
234//! ### `OrderService`
235//! - `create_order(request)` — open a position
236//! - `get_order_confirmation(deal_reference)` and
237//! `get_order_confirmation_w_retry(deal_reference, retries, delay_ms)`
238//! - `update_position(deal_id, update)` / `update_level_in_position(deal_id, level)`
239//! - `close_position(request)` / `get_position(deal_id)`
240//! - `create_working_order(request)` / `update_working_order(deal_id, update)` /
241//! `delete_working_order(deal_id)`
242//!
243//! ### `WatchlistService`
244//! - `get_watchlists()` / `create_watchlist(name, epics)`
245//! - `get_watchlist(id)` / `delete_watchlist(id)`
246//! - `add_to_watchlist(id, epic)` / `remove_from_watchlist(id, epic)`
247//!
248//! ### `SentimentService`
249//! - `get_client_sentiment(market_ids)`
250//! - `get_client_sentiment_by_market(market_id)`
251//! - `get_related_sentiment(market_id)`
252//!
253//! ### `CostsService`
254//! - `get_indicative_costs_open(request)` / `_close(request)` / `_edit(request)`
255//! - `get_costs_history(from, to)` / `get_durable_medium(quote_reference)`
256//!
257//! ### `OperationsService`
258//! - `get_client_apps()` — API application details
259//! - `disable_client_app()` — disable the current API key
260//!
261//! ## Rate Limiting
262//!
263//! All requests are paced by a `governor`-backed `RateLimiter` so the client
264//! stays within IG's published limits (trading and non-trading endpoints have
265//! different budgets). The budget is configured from the environment via
266//! `IG_RATE_LIMIT_MAX_REQUESTS`, `IG_RATE_LIMIT_PERIOD_SECONDS`, and
267//! `IG_RATE_LIMIT_BURST_SIZE` (see `RateLimiterConfig`). On top of pacing,
268//! transient failures (429 / 5xx / connection errors) are retried with
269//! **finite** exponential backoff and jitter via `RetryConfig`; non-idempotent
270//! trading calls are never retried blindly.
271//!
272//! ## Architecture
273//!
274//! The crate is organized as a module-oriented library under `src/`:
275//!
276//! - **`application`** — all I/O. The REST `Client` and its service-trait
277//! implementations, `Auth` / `Session` (login, refresh, logout, account
278//! switching), `Config`, the `RateLimiter`, and the streaming layer
279//! (`StreamerClient`, `DynamicMarketStreamer`). Service traits live under
280//! `application::interfaces`.
281//! - **`model`** — pure request / response DTOs, streaming message DTOs, session
282//! DTOs, and the retry policy. Serde only; no I/O.
283//! - **`presentation`** — domain entities per area: account, chart, instrument,
284//! market, order, price, trade, transaction. Serde only; no I/O.
285//! - **`storage`** — optional PostgreSQL persistence via `sqlx` (sits on top of
286//! `model` / `presentation`).
287//! - **`utils`** — leaf helpers: env-var config, logging, finance (P&L), parsing,
288//! deal-reference id generation.
289//! - **`error`** — canonical typed error enums: `AppError`, `AuthError`, and
290//! `FetchError`.
291//! - **`constants`** — crate-wide endpoint paths, header names, and defaults.
292//! - **`prelude`** — curated public re-exports (the recommended import surface).
293//!
294//! ### API Documentation
295//!
296//! Browse the API documentation on [docs.rs](https://docs.rs/ig-client) or
297//! generate it locally with:
298//!
299//! ```bash
300//! make doc-open
301//! ```
302//!
303//! ## Project Structure
304//!
305//! ```text
306//! src/
307//! ├── application/ # I/O: client, auth, config, rate limiter, streaming
308//! │ ├── interfaces/ # Service traits (account, market, order, costs, …)
309//! │ ├── auth.rs # Auth / Session: login, refresh, logout, switch
310//! │ ├── client.rs # Client (REST services) + StreamerClient
311//! │ ├── config.rs # Config, Credentials, REST / WS / rate-limiter config
312//! │ ├── rate_limiter.rs # governor-backed request pacing
313//! │ └── dynamic_streamer.rs # DynamicMarketStreamer (subscriptions)
314//! ├── model/ # Pure DTOs: requests, responses, streaming, retry
315//! ├── presentation/ # Domain entities (account, market, order, price, …)
316//! ├── storage/ # Optional PostgreSQL persistence via sqlx
317//! ├── utils/ # config, logger, finance, parsing, id helpers
318//! ├── constants.rs # Endpoint paths, header names, defaults
319//! ├── error.rs # AppError / AuthError / FetchError
320//! ├── prelude.rs # Curated public re-exports
321//! └── lib.rs # Public API and crate docs (README source)
322//! examples/ # Runnable demos (workspace members)
323//! tests/ # unit/ and env-gated integration/ tests
324//! benches/ # Criterion benchmarks
325//! ```
326//!
327//! ## Development
328//!
329//! This project includes a Makefile with common development tasks:
330//!
331//! ```bash
332//! make build # Debug build
333//! make release # Release build
334//! make test # Run all tests
335//! make fmt # Format code with rustfmt
336//! make lint # Run clippy
337//! make readme # Regenerate README.md from these crate docs
338//! make pre-push # Run all checks before pushing
339//! ```
340//!
341//! ## Contributing
342//!
343//! Contributions are welcome:
344//!
345//! 1. Fork the repository.
346//! 2. Create a feature branch: `git checkout -b feature/my-feature`.
347//! 3. Make your changes and commit them.
348//! 4. Run the checks: `make pre-push`.
349//! 5. Push the branch and open a pull request.
350//!
351//! Please make sure your code passes all tests and linting checks before
352//! submitting a pull request.
353
354/// Core application logic and services
355pub mod application;
356
357/// User interface and presentation layer
358pub mod presentation;
359
360/// Module containing global constants used throughout the library
361pub mod constants;
362
363/// Error types and handling
364pub mod error;
365
366/// Data persistence and storage
367pub mod storage;
368
369/// Data models for IG Markets API entities
370pub mod model;
371/// Prelude module for convenient imports
372pub mod prelude;
373/// Utility functions and helpers
374pub mod utils;
375
376/// Library version
377pub const VERSION: &str = env!("CARGO_PKG_VERSION");
378
379/// Returns the library version
380pub fn version() -> &'static str {
381 VERSION
382}