All notable changes to trading-ig will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
StreamingClient::subscribe_market. IG deprecated the LightstreamerMARKETsubscription (and itsL1alias): end of life 1 May 2026, decommissioned 8 May 2026. Usesubscribe_priceinstead. The method carries#[deprecated]and also emits atracing::warn!once per process, so already-deployed bots see the notice in their logs and not just at compile time.
PRICEsubscription —subscribe_price(epic)andsubscribe_price_for_account(account_id, epic), returning the newPriceUpdate. The item name isPRICE:<accountId>:<epic>and the subscription selects thePricingdata adapter.LS_data_adapteroncontrol.txt. The Lightstreamer control request can now select a data adapter;PRICEis the only family that needs one.PriceUpdateexposes the 5-tier dealing ladder (bid_prices,ask_prices,bid_sizes,ask_sizes), the dealing quote IDs (bid_quote_id,ask_quote_id) and the ladder currency — none of whichMARKETprovided.- Seven
CHART:<epic>:<scale>fields that were missing from the candle subscription:LTV,TTV,DAY_OPEN_MID,DAY_NET_CHG_MID,DAY_PERC_CHG_MID,DAY_HIGH,DAY_LOW. ACCOUNTfieldsPNL_LR/PNL_NLR(AccountUpdate::pnl_lr/pnl_nlr).
- Breaking (pre-1.0):
ChartCandleUpdateandAccountUpdategained public fields, so exhaustive struct literals and patterns need updating. Both deriveDefault, so..Default::default()is the cheap fix. - Breaking (pre-1.0):
CandleScalegained aSecondvariant, matching theSECONDscale IG documents. The enum is not#[non_exhaustive], so an exhaustivematchon it stops compiling until the arm is added. - Breaking for
#![deny(warnings)]crates: the#[deprecated]onsubscribe_marketturns into a hard error there. Intentional — the subscription is past IG's decommission date. examples/streaming_market.rsrenamed toexamples/streaming_price.rsand switched tosubscribe_price.subscribe_priceuses the account the Lightstreamer session authenticated with atconnect(), which a laterswitch_accountdoes not change. Usesubscribe_price_for_accountto override.
MARKET → PRICE is not a rename. Beyond the item name gaining the
account id, two fields change meaning: UPDATE_TIME (UK-local
HH:MM:SS string) becomes timestamp (UTC milliseconds), and
MARKET_STATE becomes dlg_flag, which uses a different vocabulary
(DEAL where MARKET said TRADEABLE). Full mapping table in
_knowledge/api/streaming.md.
LS_data_adapter=Pricing is taken from IG's published reference and has
not yet been exercised against a live demo session.
- Bumped vulnerable transitive dependencies flagged by
cargo audit/cargo deny.Cargo.lockonly — no public API change.quinn-proto0.11.14 → 0.11.16 — RUSTSEC-2026-0185 (remote memory exhaustion via unbounded out-of-order stream reassembly).crossbeam-epoch0.9.18 → 0.9.20 — RUSTSEC-2026-0204 (invalid pointer dereference infmt::Display).anyhow1.0.102 → 1.0.104 — RUSTSEC-2026-0190 (unsoundness inError::downcast_mut()).memmap20.9.10 → 0.9.11 — RUSTSEC-2026-0186 (unchecked pointer offset).spin0.9.8 → 0.9.9 — replaces the yanked 0.9.8.
close_positionrejected withvalidation.null-not-allowed.request. IG silently drops the body of realDELETErequests, so every required field onClosePositionRequestwas reported as null. The transport now mirrors the official Pythontrading-igclient and rewrites body-carryingDELETEs asPOSTwith an_method: DELETEheader. Bodyless DELETEs (sessionlogout,watchlistsdelete,workingordersdelete) are untouched. Validated live on a demo account: position fully closed, expected P/L confirmed.- Defence in depth:
ClosePositionRequestnow carries#[serde(skip_serializing_if = "Option::is_none")]on every optional field, so explicitnulls never reach IG even if a future code path bypasses the transport rewrite.
examples/diag_close_position.rs— list open positions and close the first, logging the outbound JSON. Handy when reproducing the null-not-allowed bug or validating the rewrite against a fresh demo account.tests/dealing_positions.rs::close_position_rewrites_delete_to_post_with_method_overridepins the rewrite + asserts the JSON body survives intact.tests/support/matchers::HasMethodOverrideandIgMockServer::mount_delete_errorhelpers for tests that target body-carrying DELETE endpoints.
session().login_with_encryption()(behind theencryptioncargo feature) — log in v3 with an RSA-encrypted password instead of plaintext. Recommended for accounts holding real funds (live or funded demo). Internally fetches the encryption key, encrypts the password with PKCS#1 v1.5, and posts to/sessionwithencryptedPassword=true.SECURITY.md— vulnerability reporting policy (FR + EN), maintainer contact email thibault.barske@kolombo.xyz, encryption recommendation for funded accounts, defensive practices the crate enforces.- README "Recommended for funded accounts" section pointing at the new helper.
Cargo.toml: corrected therepositoryURL to point attibs245/trading-ig-rust. Addedhomepageanddocumentationmetadata fields, and configured[package.metadata.docs.rs]so docs.rs builds with all features.streaming::reconnectdoc comment: fix broken intra-doc link toStreamingApi::connect_with.streaming::events::MarketUpdate::from_rawdoc comment: drop intra-doc link to a privateMARKET_FIELDSconstant (wascargo doc -D warningsfailure on Rust 1.95).- Two
Duration::from_secs(60)call sites updated toDuration::from_mins(1)to satisfy Rust 1.95's newclippy::duration_suboptimal_unitslint.
- RUSTSEC-2023-0071 (
rsacrate Marvin timing side-channel on PKCS#1 v1.5 decryption): acknowledged but not applicable to this crate — we only ever encrypt with IG's public key, never decrypt. Anignoreis documented indeny.tomland in thecargo auditinvocations of both CI workflows. SeeSECURITY.mdfor the full rationale. cargo denyis now wired into the weekly security workflow (.github/workflows/audit.yml) — checks advisories, licenses, banned crates, and source provenance.cargo auditis also added to thepre-pushgit hook (skipped if the binary isn't installed locally; CI runs it unconditionally).
Initial release. Async Rust port of the trading-ig
Python client. Covers the full IG Markets REST surface and a Lightstreamer
streaming client with auto-reconnect.
- Session:
login(v3 OAuth),login_v2(CST/XST headers),refresh,read(fetch_tokens),switch_account,logout,encryption_key(behind theencryptionfeature). - Accounts:
list,preferences,update_preferences. - Markets:
search,get,get_many(v2 bulk),navigation,navigation_node. - Prices (3 distinct endpoints):
history_v3+history_v3_all(auto-paginated),history_by_num_points_v2,history_by_date_range_v2,history_by_date_range_v1. - Dealing — positions:
list,list_v1,list_v2,get,open(type-state builder),update,close,confirm(with 5× × 1 s retry). - Dealing — working orders:
list,list_v1,list_v2,create(type-state builder),update,delete. - Watchlists:
list,create,markets,add_market,remove_market,delete. - Client sentiment:
get,get_many,related. - History:
activity_v3(auto-paginated next-URL),activity_by_period_v1,activity_by_date_range_v1,transactions_v2,transactions_by_period_v1. - Operations:
applications,update_application,disable_current_key. - Repeat dealing:
window,window_for(epic)(with 5× × 1 s retry).
- Lightstreamer TLCP client with
MARKET,CHART:TICK,CHART:<scale>,ACCOUNT,TRADEsubscriptions. - Each subscription returns a
tokio::sync::mpsc::Receiver<T>of typed updates. - Auto-reconnect on server
END/ unrecoverable session loss, with configurable back-off (AutoReconnectpolicy). Subscribers' channels are reused transparently across reconnects. - A
StreamingEventchannel is returned alongside theStreamingClientso callers can observeReconnected,ReconnectFailed, andDisconnectedevents.
- Strongly typed: every request / response is a
serdestruct or enum. Newtypes forEpic,DealId,DealReference,Currency. Common enums (Direction,OrderType,TimeInForce,InstrumentType,MarketStatus) undertrading_ig::models. - Single crate-level
Errorenum surfacing IG'serrorCodepayload viaError::Api { status, source: ApiError { error_code, .. } }. Helper predicatesis_auth()andis_rate_limited(). - Structured logs / spans via the
tracingcrate. Tokens and credentials are never logged. - Date / time helpers in
trading_ig::timecovering IG's three input formats (v1Y:m:d-H:M:S, v2Y/m/d H:M:S, v3 ISO-8601).
| feature | default | description |
|---|---|---|
rustls-tls |
yes | TLS via rustls |
native-tls |
no | TLS via system OpenSSL |
stream |
no | Lightstreamer streaming client |
encryption |
no | Encrypted-password login (RSA PKCS#1v15) |
- No client-side rate limiter yet (IG enforces 30 req/min trading, 60 req/min non-trading).
- No automatic OAuth-on-401 refresh in the transport — call
session().refresh()explicitly when needed. - No sync wrapper. Async-only on
tokio. - Lightstreamer reconnect logic is covered by the public-API tests but not yet by an end-to-end TCP-level mock; the network path is exercised in live smoke tests against demo-api.ig.com.