Skip to content

Commit 9611360

Browse files
authored
Add read-only Client::config() (TWS 10.43 / server 219) (#707)
* Add read-only Client::config() (TWS 10.43 / server 219) New config domain module exposing a one-shot Client::config() (sync + async) that reads the TWS/Gateway configuration: API settings, order precautions, smart-routing, lock-and-exit, and message prompts. - ReqConfig -> ConfigResponse proto round-trip via one_shot_request_with_retry - Idiomatic config::Config snapshot (no proto leak); every field optional - Features::CONFIG gate (server_versions::CONFIG = 219) - Register ConfigResponse in text_request_id_field so it routes by request_id - Field-minimal testdata builders; 100% coverage on the new module Read path only; update_config (write path with warning-ack) deferred. * Add config examples (sync + async) Smoke-tested against live IB Gateway paper (127.0.0.1:4002): the full Config snapshot (lock-and-exit, message prompts, API settings, precautions, smart-routing) decodes from real TWS wire data. * Collapse redundant decode_config wrapper; narrow visibility decode_config_message now calls require_proto()+decode inline; drop the one-line decode_config indirection and the unused pub(crate) exposure (decode_config_proto is module-private). No cross-module consumers. * Reuse MessageBusStub::with_ordered_responses in config tests Replace the hand-rolled stub struct literal with the existing named constructor; drops the now-unused RwLock import.
1 parent 48a9fd3 commit 9611360

18 files changed

Lines changed: 820 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `Client::config()` (sync + async) to read the TWS/Gateway configuration (API settings, order precautions, smart-routing, lock-and-exit) added upstream for server version 219 (TWS 10.43); returns the new `config::Config` snapshot type and is gated behind `server_versions::CONFIG`.
1213
- `Order.hedge_max_size` (`Option<i32>`) for the maximum size of a hedge order, added upstream for server version 223 (TWS 10.45); gated behind `server_versions::HEDGE_MAX_SIZE` when placing orders (#706).
1314
- Server-version support advertised through 225 (`ODD_LOT_BID_ASK_QUOTES`), with new `server_versions` constants `FRACTIONAL_LAST_SIZE` (222), `HEDGE_MAX_SIZE` (223), `USE_PRECISION_FROM_SEC_DEF` (224), and `ODD_LOT_BID_ASK_QUOTES` (225) (#706).
1415
- `generic_tick::ODD_LOT` (`"787"`) request-side generic tick to subscribe odd-lot bid/ask quotes (server 225 / TWS 10.46) via `reqMktData` (#706).

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,11 @@ name = "async_soft_dollar_tiers"
110110
path = "examples/async/soft_dollar_tiers.rs"
111111
required-features = ["async"]
112112

113+
[[example]]
114+
name = "async_config"
115+
path = "examples/async/config.rs"
116+
required-features = ["async"]
117+
113118
[[example]]
114119
name = "async_user_info"
115120
path = "examples/async/user_info.rs"
@@ -617,6 +622,11 @@ name = "soft_dollar_tiers"
617622
path = "examples/sync/soft_dollar_tiers.rs"
618623
required-features = ["sync"]
619624

625+
[[example]]
626+
name = "config"
627+
path = "examples/sync/config.rs"
628+
required-features = ["sync"]
629+
620630
[[example]]
621631
name = "user_info"
622632
path = "examples/sync/user_info.rs"

examples/async/config.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//! Config example (async).
2+
//!
3+
//! Reads the TWS/Gateway configuration (API settings, order precautions,
4+
//! smart-routing, lock-and-exit) the gateway is currently running with.
5+
//!
6+
//! ```bash
7+
//! cargo run --example async_config
8+
//! ```
9+
10+
use ibapi::prelude::*;
11+
12+
#[tokio::main]
13+
async fn main() {
14+
env_logger::init();
15+
16+
let client = Client::connect("127.0.0.1:4002", 100).await.expect("connection failed");
17+
18+
let config = client.config().await.expect("config request failed");
19+
20+
println!("{config:#?}");
21+
}

examples/sync/config.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//! Config example (sync).
2+
//!
3+
//! Reads the TWS/Gateway configuration (API settings, order precautions,
4+
//! smart-routing, lock-and-exit) the gateway is currently running with.
5+
//!
6+
//! # Usage
7+
//!
8+
//! ```bash
9+
//! cargo run --features sync --example config
10+
//! ```
11+
12+
use ibapi::client::blocking::Client;
13+
14+
fn main() {
15+
env_logger::init();
16+
17+
let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed");
18+
19+
let config = client.config().expect("config request failed");
20+
21+
println!("{config:#?}");
22+
}

src/config/async.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//! Asynchronous implementation of configuration retrieval.
2+
3+
use crate::{
4+
common::request_helpers,
5+
protocol::{check_version, Features},
6+
Client, Error,
7+
};
8+
9+
use super::{common::decoders, encoders, Config};
10+
11+
impl Client {
12+
/// Reads the TWS/Gateway configuration (API, precautions, orders, and
13+
/// lock-and-exit settings) the gateway is currently running with.
14+
///
15+
/// This is a read-only snapshot; fields the gateway does not report are
16+
/// left as `None`.
17+
///
18+
/// # Examples
19+
///
20+
/// ```no_run
21+
/// use ibapi::prelude::*;
22+
///
23+
/// #[tokio::main]
24+
/// async fn main() {
25+
/// let client = Client::connect("127.0.0.1:4002", 100).await.expect("connection failed");
26+
/// let config = client.config().await.expect("request config failed");
27+
/// println!("{config:?}");
28+
/// }
29+
/// ```
30+
pub async fn config(&self) -> Result<Config, Error> {
31+
check_version(self.server_version(), Features::CONFIG)?;
32+
33+
request_helpers::one_shot_request_with_retry(self, encoders::encode_request_config, decoders::decode_config_message, || {
34+
Err(Error::UnexpectedEndOfStream)
35+
})
36+
.await
37+
}
38+
}
39+
40+
#[cfg(test)]
41+
#[path = "async_tests.rs"]
42+
mod tests;

src/config/async_tests.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use super::*;
2+
use crate::common::test_utils::helpers::{assert_request, proto_response, TEST_REQ_ID_FIRST};
3+
use crate::messages::IncomingMessages;
4+
use crate::stubs::MessageBusStub;
5+
use crate::testdata::builders::config::{config_request, config_response};
6+
use crate::testdata::builders::ResponseProtoEncoder;
7+
use std::sync::Arc;
8+
9+
fn stub(responses: Vec<crate::messages::ResponseMessage>) -> Arc<MessageBusStub> {
10+
Arc::new(MessageBusStub::with_ordered_responses(responses))
11+
}
12+
13+
#[tokio::test]
14+
async fn test_config_request_body() {
15+
let message_bus = stub(vec![proto_response(
16+
IncomingMessages::ConfigResponse,
17+
config_response().request_id(TEST_REQ_ID_FIRST).encode_proto(),
18+
)]);
19+
20+
let client = Client::stubbed(message_bus.clone(), crate::server_versions::CONFIG);
21+
client.config().await.expect("config request failed");
22+
23+
assert_request(&message_bus, 0, &config_request().request_id(TEST_REQ_ID_FIRST));
24+
}
25+
26+
#[tokio::test]
27+
async fn test_config_round_trip() {
28+
let message_bus = stub(vec![proto_response(
29+
IncomingMessages::ConfigResponse,
30+
config_response()
31+
.request_id(TEST_REQ_ID_FIRST)
32+
.read_only_api(true)
33+
.socket_port(4002)
34+
.seek_price_improvement(true)
35+
.encode_proto(),
36+
)]);
37+
38+
let client = Client::stubbed(message_bus, crate::server_versions::CONFIG);
39+
let config = client.config().await.expect("config request failed");
40+
41+
assert_eq!(config.api.unwrap().settings.unwrap().socket_port, Some(4002));
42+
assert_eq!(config.orders.unwrap().smart_routing.unwrap().seek_price_improvement, Some(true));
43+
}
44+
45+
#[tokio::test]
46+
async fn test_config_unexpected_end_of_stream() {
47+
let message_bus = stub(vec![]);
48+
49+
let client = Client::stubbed(message_bus, crate::server_versions::CONFIG);
50+
match client.config().await {
51+
Err(Error::UnexpectedEndOfStream) => {}
52+
other => panic!("expected UnexpectedEndOfStream, got {other:?}"),
53+
}
54+
}
55+
56+
#[tokio::test]
57+
async fn test_config_server_version_error() {
58+
let message_bus = stub(vec![]);
59+
60+
let client = Client::stubbed(message_bus, crate::server_versions::CONFIG - 1);
61+
match client.config().await {
62+
Err(Error::ServerVersion(_, _, _)) => {}
63+
other => panic!("expected ServerVersion error, got {other:?}"),
64+
}
65+
}

src/config/common/decoders.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//! Decoders for configuration messages. Proto-only; text framing surfaces as
2+
//! `Error::UnexpectedResponse` via `require_proto()`.
3+
4+
use prost::Message;
5+
6+
use crate::config::{ApiConfig, ApiPrecautions, ApiSettings, Config, LockAndExit, MessageSetting, OrdersConfig, OrdersSmartRouting};
7+
use crate::messages::{IncomingMessages, ResponseMessage};
8+
use crate::proto;
9+
use crate::Error;
10+
11+
/// Dispatch on the incoming message type and forward to the typed decoder.
12+
/// Routes `Error` frames into `Error::from` and any other variant into
13+
/// `Error::UnexpectedResponse`.
14+
pub(in crate::config) fn decode_config_message(message: &ResponseMessage) -> Result<Config, Error> {
15+
match message.message_type() {
16+
IncomingMessages::ConfigResponse => decode_config_proto(message.require_proto()?),
17+
IncomingMessages::Error => Err(Error::from(message)),
18+
_ => Err(Error::unexpected_response(message)),
19+
}
20+
}
21+
22+
fn decode_config_proto(bytes: &[u8]) -> Result<Config, Error> {
23+
let p = proto::ConfigResponse::decode(bytes)?;
24+
Ok(Config {
25+
lock_and_exit: p.lock_and_exit.map(convert_lock_and_exit),
26+
messages: p.messages.into_iter().map(convert_message).collect(),
27+
api: p.api.map(convert_api),
28+
orders: p.orders.map(convert_orders),
29+
})
30+
}
31+
32+
fn convert_lock_and_exit(p: proto::LockAndExitConfig) -> LockAndExit {
33+
LockAndExit {
34+
auto_logoff_time: p.auto_logoff_time,
35+
auto_logoff_period: p.auto_logoff_period,
36+
auto_logoff_type: p.auto_logoff_type,
37+
}
38+
}
39+
40+
fn convert_message(p: proto::MessageConfig) -> MessageSetting {
41+
MessageSetting {
42+
id: p.id,
43+
title: p.title,
44+
message: p.message,
45+
default_action: p.default_action,
46+
enabled: p.enabled,
47+
}
48+
}
49+
50+
fn convert_api(p: proto::ApiConfig) -> ApiConfig {
51+
ApiConfig {
52+
precautions: p.precautions.map(convert_precautions),
53+
settings: p.settings.map(convert_settings),
54+
}
55+
}
56+
57+
fn convert_precautions(p: proto::ApiPrecautionsConfig) -> ApiPrecautions {
58+
ApiPrecautions {
59+
bypass_order_precautions: p.bypass_order_precautions,
60+
bypass_bond_warning: p.bypass_bond_warning,
61+
bypass_negative_yield_confirmation: p.bypass_negative_yield_confirmation,
62+
bypass_called_bond_warning: p.bypass_called_bond_warning,
63+
bypass_same_action_pair_trade_warning: p.bypass_same_action_pair_trade_warning,
64+
bypass_flagged_accounts_warning: p.bypass_flagged_accounts_warning,
65+
bypass_price_based_volatility_warning: p.bypass_price_based_volatility_warning,
66+
bypass_redirect_order_warning: p.bypass_redirect_order_warning,
67+
bypass_no_overfill_protection: p.bypass_no_overfill_protection,
68+
bypass_route_marketable_to_bbo: p.bypass_route_marketable_to_bbo,
69+
}
70+
}
71+
72+
fn convert_settings(p: proto::ApiSettingsConfig) -> ApiSettings {
73+
ApiSettings {
74+
read_only_api: p.read_only_api,
75+
total_quantity_for_mutual_funds: p.total_quantity_for_mutual_funds,
76+
download_open_orders_on_connection: p.download_open_orders_on_connection,
77+
include_virtual_fx_positions: p.include_virtual_fx_positions,
78+
prepare_daily_pnl: p.prepare_daily_pn_l,
79+
send_status_updates_for_volatility_orders: p.send_status_updates_for_volatility_orders,
80+
encode_api_messages: p.encode_api_messages,
81+
socket_port: p.socket_port,
82+
use_negative_auto_range: p.use_negative_auto_range,
83+
create_api_message_log_file: p.create_api_message_log_file,
84+
include_market_data_in_log_file: p.include_market_data_in_log_file,
85+
expose_trading_schedule_to_api: p.expose_trading_schedule_to_api,
86+
split_insured_deposit_from_cash_balance: p.split_insured_deposit_from_cash_balance,
87+
send_zero_positions_for_today_only: p.send_zero_positions_for_today_only,
88+
let_api_account_requests_switch_subscription: p.let_api_account_requests_switch_subscription,
89+
use_account_groups_with_allocation_methods: p.use_account_groups_with_allocation_methods,
90+
logging_level: p.logging_level,
91+
master_client_id: p.master_client_id,
92+
bulk_data_timeout: p.bulk_data_timeout,
93+
component_exch_separator: p.component_exch_separator,
94+
show_forex_data_in_1_10_pips: p.show_forex_data_in1_10pips,
95+
allow_forex_trading_in_1_10_pips: p.allow_forex_trading_in1_10pips,
96+
round_account_values_to_nearest_whole_number: p.round_account_values_to_nearest_whole_number,
97+
send_market_data_in_lots_for_us_stocks: p.send_market_data_in_lots_for_us_stocks,
98+
show_advanced_order_reject_in_ui: p.show_advanced_order_reject_in_ui,
99+
reject_messages_above_max_rate: p.reject_messages_above_max_rate,
100+
maintain_connection_on_incorrect_fields: p.maintain_connection_on_incorrect_fields,
101+
compatibility_mode_nasdaq_stocks: p.compatibility_mode_nasdaq_stocks,
102+
send_instrument_timezone: p.send_instrument_timezone,
103+
send_forex_data_in_compatibility_mode: p.send_forex_data_in_compatibility_mode,
104+
maintain_and_resubmit_orders_on_reconnect: p.maintain_and_resubmit_orders_on_reconnect,
105+
historical_data_max_size: p.historical_data_max_size,
106+
auto_report_netting_event_contract_trades: p.auto_report_netting_event_contract_trades,
107+
option_exercise_request_type: p.option_exercise_request_type,
108+
allow_localhost_only: p.allow_localhost_only,
109+
trusted_ips: p.trusted_i_ps,
110+
}
111+
}
112+
113+
fn convert_orders(p: proto::OrdersConfig) -> OrdersConfig {
114+
OrdersConfig {
115+
smart_routing: p.smart_routing.map(convert_smart_routing),
116+
}
117+
}
118+
119+
fn convert_smart_routing(p: proto::OrdersSmartRoutingConfig) -> OrdersSmartRouting {
120+
OrdersSmartRouting {
121+
seek_price_improvement: p.seek_price_improvement,
122+
pre_open_reroute: p.pre_open_reroute,
123+
do_not_route_to_dark_pools: p.do_not_route_to_dark_pools,
124+
default_algorithm: p.default_algorithm,
125+
}
126+
}
127+
128+
#[cfg(test)]
129+
#[path = "decoders_tests.rs"]
130+
mod tests;

0 commit comments

Comments
 (0)