Skip to main content

ibapi/
messages.rs

1//! Message encoding, decoding, and routing for TWS API communication.
2//!
3//! This module handles the low-level message protocol between the client and TWS,
4//! including request/response message formatting, field encoding/decoding,
5//! and message type definitions.
6
7use std::fmt::Display;
8use std::io::Write;
9use std::ops::Index;
10use std::str::{self, FromStr};
11
12use byteorder::{BigEndian, WriteBytesExt};
13
14use log::debug;
15use serde::{Deserialize, Serialize};
16use time::OffsetDateTime;
17
18use crate::{Error, ToField};
19
20pub mod parser_registry;
21pub(crate) mod shared_channel_configuration;
22#[cfg(test)]
23mod tests;
24
25#[cfg(test)]
26mod from_str_tests {
27    use super::*;
28    use std::str::FromStr;
29
30    #[test]
31    fn test_outgoing_messages_from_str() {
32        // Test some common message types
33        assert_eq!(OutgoingMessages::from_str("1").unwrap(), OutgoingMessages::RequestMarketData);
34        assert_eq!(OutgoingMessages::from_str("17").unwrap(), OutgoingMessages::RequestManagedAccounts);
35        assert_eq!(OutgoingMessages::from_str("49").unwrap(), OutgoingMessages::RequestCurrentTime);
36        assert_eq!(OutgoingMessages::from_str("61").unwrap(), OutgoingMessages::RequestPositions);
37
38        // Test error cases
39        assert!(OutgoingMessages::from_str("999").is_err());
40        assert!(OutgoingMessages::from_str("abc").is_err());
41        assert!(OutgoingMessages::from_str("").is_err());
42    }
43
44    #[test]
45    fn test_outgoing_messages_roundtrip() {
46        // Test that we can convert to string and back
47        let msg = OutgoingMessages::RequestCurrentTime;
48        let as_string = msg.to_string();
49        let parsed = OutgoingMessages::from_str(&as_string).unwrap();
50        assert_eq!(parsed, OutgoingMessages::RequestCurrentTime);
51
52        // Test with another message type
53        let msg = OutgoingMessages::RequestManagedAccounts;
54        let as_string = msg.to_string();
55        let parsed = OutgoingMessages::from_str(&as_string).unwrap();
56        assert_eq!(parsed, OutgoingMessages::RequestManagedAccounts);
57    }
58
59    #[test]
60    fn test_incoming_messages_from_str() {
61        // Test some common message types
62        assert_eq!(IncomingMessages::from_str("4").unwrap(), IncomingMessages::Error);
63        assert_eq!(IncomingMessages::from_str("15").unwrap(), IncomingMessages::ManagedAccounts);
64        assert_eq!(IncomingMessages::from_str("49").unwrap(), IncomingMessages::CurrentTime);
65        assert_eq!(IncomingMessages::from_str("61").unwrap(), IncomingMessages::Position);
66
67        // Test NotValid for unknown values
68        assert_eq!(IncomingMessages::from_str("999").unwrap(), IncomingMessages::NotValid);
69        assert_eq!(IncomingMessages::from_str("0").unwrap(), IncomingMessages::NotValid);
70        assert_eq!(IncomingMessages::from_str("-1").unwrap(), IncomingMessages::NotValid);
71
72        // Test error cases for non-numeric strings
73        assert!(IncomingMessages::from_str("abc").is_err());
74        assert!(IncomingMessages::from_str("").is_err());
75        assert!(IncomingMessages::from_str("1.5").is_err());
76    }
77
78    #[test]
79    fn test_incoming_messages_roundtrip() {
80        // Test with CurrentTime message
81        let n = 49;
82        let msg = IncomingMessages::from(n);
83        let as_string = n.to_string();
84        let parsed = IncomingMessages::from_str(&as_string).unwrap();
85        assert_eq!(parsed, msg);
86
87        // Test with ManagedAccounts message
88        let n = 15;
89        let msg = IncomingMessages::from(n);
90        let as_string = n.to_string();
91        let parsed = IncomingMessages::from_str(&as_string).unwrap();
92        assert_eq!(parsed, msg);
93
94        // Test with NotValid (unknown value)
95        let n = 999;
96        let msg = IncomingMessages::from(n);
97        let as_string = n.to_string();
98        let parsed = IncomingMessages::from_str(&as_string).unwrap();
99        assert_eq!(parsed, msg);
100        assert_eq!(parsed, IncomingMessages::NotValid);
101    }
102}
103
104const INFINITY_STR: &str = "Infinity";
105const UNSET_DOUBLE: &str = "1.7976931348623157E308";
106const UNSET_INTEGER: &str = "2147483647";
107const UNSET_LONG: &str = "9223372036854775807";
108
109/// Messages emitted by TWS/Gateway over the market data socket.
110#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
111pub enum IncomingMessages {
112    /// Gateway initiated shutdown.
113    Shutdown = -2,
114    /// Unknown or unsupported message id.
115    NotValid = -1,
116    /// Tick price update.
117    TickPrice = 1,
118    /// Tick size update.
119    TickSize = 2,
120    /// Order status update.
121    OrderStatus = 3,
122    /// Error (includes request id and code).
123    Error = 4,
124    /// Open order description.
125    OpenOrder = 5,
126    /// Account value key/value pair.
127    AccountValue = 6,
128    /// Portfolio value line.
129    PortfolioValue = 7,
130    /// Account update timestamp.
131    AccountUpdateTime = 8,
132    /// Next valid order id notification.
133    NextValidId = 9,
134    /// Contract details payload.
135    ContractData = 10,
136    /// Execution data update.
137    ExecutionData = 11,
138    /// Level 1 market depth row update.
139    MarketDepth = 12,
140    /// Level 2 market depth row update.
141    MarketDepthL2 = 13,
142    /// News bulletin broadcast.
143    NewsBulletins = 14,
144    /// List of managed accounts.
145    ManagedAccounts = 15,
146    /// Financial advisor configuration data.
147    ReceiveFA = 16,
148    /// Historical bar data payload.
149    HistoricalData = 17,
150    /// Bond contract details payload.
151    BondContractData = 18,
152    /// Scanner parameter definitions.
153    ScannerParameters = 19,
154    /// Scanner subscription results.
155    ScannerData = 20,
156    /// Option computation tick.
157    TickOptionComputation = 21,
158    /// Generic numeric tick (e.g. implied volatility).
159    TickGeneric = 45,
160    /// String-valued tick (exchange names, etc.).
161    TickString = 46,
162    /// Exchange for Physical tick update.
163    TickEFP = 47, //TICK EFP 47
164    /// Current world clock time.
165    CurrentTime = 49,
166    /// Real-time bars update.
167    RealTimeBars = 50,
168    /// Fundamental data response.
169    FundamentalData = 51,
170    /// End marker for contract details batches.
171    ContractDataEnd = 52,
172    /// End marker for open order batches.
173    OpenOrderEnd = 53,
174    /// End marker for account download.
175    AccountDownloadEnd = 54,
176    /// End marker for execution data.
177    ExecutionDataEnd = 55,
178    /// Delta-neutral validation response.
179    DeltaNeutralValidation = 56,
180    /// End of tick snapshot.
181    TickSnapshotEnd = 57,
182    /// Market data type acknowledgment.
183    MarketDataType = 58,
184    /// Commissions report payload.
185    CommissionsReport = 59,
186    /// Position update.
187    Position = 61,
188    /// End marker for position updates.
189    PositionEnd = 62,
190    /// Account summary update.
191    AccountSummary = 63,
192    /// End marker for account summary stream.
193    AccountSummaryEnd = 64,
194    /// API verification challenge.
195    VerifyMessageApi = 65,
196    /// API verification completion.
197    VerifyCompleted = 66,
198    /// Display group list response.
199    DisplayGroupList = 67,
200    /// Display group update.
201    DisplayGroupUpdated = 68,
202    /// Auth + verification challenge.
203    VerifyAndAuthMessageApi = 69,
204    /// Auth + verification completion.
205    VerifyAndAuthCompleted = 70,
206    /// Multi-account position update.
207    PositionMulti = 71,
208    /// End marker for multi-account position stream.
209    PositionMultiEnd = 72,
210    /// Multi-account account update.
211    AccountUpdateMulti = 73,
212    /// End marker for multi-account account stream.
213    AccountUpdateMultiEnd = 74,
214    /// Option security definition parameters.
215    SecurityDefinitionOptionParameter = 75,
216    /// End marker for option security definition stream.
217    SecurityDefinitionOptionParameterEnd = 76,
218    /// Soft dollar tier information.
219    SoftDollarTier = 77,
220    /// Family code response.
221    FamilyCodes = 78,
222    /// Matching symbol samples.
223    SymbolSamples = 79,
224    /// Exchanges offering market depth.
225    MktDepthExchanges = 80,
226    /// Tick request parameter info.
227    TickReqParams = 81,
228    /// Smart component routing map.
229    SmartComponents = 82,
230    /// News article content.
231    NewsArticle = 83,
232    /// News headline tick.
233    TickNews = 84,
234    /// Available news providers.
235    NewsProviders = 85,
236    /// Historical news headlines.
237    HistoricalNews = 86,
238    /// End marker for historical news.
239    HistoricalNewsEnd = 87,
240    /// Head timestamp for historical data.
241    HeadTimestamp = 88,
242    /// Histogram data response.
243    HistogramData = 89,
244    /// Streaming historical data update.
245    HistoricalDataUpdate = 90,
246    /// Market data request reroute notice.
247    RerouteMktDataReq = 91,
248    /// Market depth request reroute notice.
249    RerouteMktDepthReq = 92,
250    /// Market rule response.
251    MarketRule = 93,
252    /// Account PnL update.
253    PnL = 94,
254    /// Single position PnL update.
255    PnLSingle = 95,
256    /// Historical tick data (midpoint).
257    HistoricalTick = 96,
258    /// Historical tick data (bid/ask).
259    HistoricalTickBidAsk = 97,
260    /// Historical tick data (trades).
261    HistoricalTickLast = 98,
262    /// Tick-by-tick streaming data.
263    TickByTick = 99,
264    /// Order bound notification for API multiple endpoints.
265    OrderBound = 100,
266    /// Completed order information.
267    CompletedOrder = 101,
268    /// End marker for completed orders.
269    CompletedOrdersEnd = 102,
270    /// End marker for FA profile replacement.
271    ReplaceFAEnd = 103,
272    /// Wall Street Horizon metadata update.
273    WshMetaData = 104,
274    /// Wall Street Horizon event payload.
275    WshEventData = 105,
276    /// Historical schedule response.
277    HistoricalSchedule = 106,
278    /// User information response.
279    UserInfo = 107,
280    /// End marker for historical data.
281    HistoricalDataEnd = 108,
282    /// Current time in milliseconds.
283    CurrentTimeInMillis = 109,
284    /// Configuration response.
285    ConfigResponse = 110,
286    /// Update configuration response.
287    UpdateConfigResponse = 111,
288}
289
290impl From<i32> for IncomingMessages {
291    fn from(value: i32) -> IncomingMessages {
292        match value {
293            -2 => IncomingMessages::Shutdown,
294            1 => IncomingMessages::TickPrice,
295            2 => IncomingMessages::TickSize,
296            3 => IncomingMessages::OrderStatus,
297            4 => IncomingMessages::Error,
298            5 => IncomingMessages::OpenOrder,
299            6 => IncomingMessages::AccountValue,
300            7 => IncomingMessages::PortfolioValue,
301            8 => IncomingMessages::AccountUpdateTime,
302            9 => IncomingMessages::NextValidId,
303            10 => IncomingMessages::ContractData,
304            11 => IncomingMessages::ExecutionData,
305            12 => IncomingMessages::MarketDepth,
306            13 => IncomingMessages::MarketDepthL2,
307            14 => IncomingMessages::NewsBulletins,
308            15 => IncomingMessages::ManagedAccounts,
309            16 => IncomingMessages::ReceiveFA,
310            17 => IncomingMessages::HistoricalData,
311            18 => IncomingMessages::BondContractData,
312            19 => IncomingMessages::ScannerParameters,
313            20 => IncomingMessages::ScannerData,
314            21 => IncomingMessages::TickOptionComputation,
315            45 => IncomingMessages::TickGeneric,
316            46 => IncomingMessages::TickString,
317            47 => IncomingMessages::TickEFP, //TICK EFP 47
318            49 => IncomingMessages::CurrentTime,
319            50 => IncomingMessages::RealTimeBars,
320            51 => IncomingMessages::FundamentalData,
321            52 => IncomingMessages::ContractDataEnd,
322            53 => IncomingMessages::OpenOrderEnd,
323            54 => IncomingMessages::AccountDownloadEnd,
324            55 => IncomingMessages::ExecutionDataEnd,
325            56 => IncomingMessages::DeltaNeutralValidation,
326            57 => IncomingMessages::TickSnapshotEnd,
327            58 => IncomingMessages::MarketDataType,
328            59 => IncomingMessages::CommissionsReport,
329            61 => IncomingMessages::Position,
330            62 => IncomingMessages::PositionEnd,
331            63 => IncomingMessages::AccountSummary,
332            64 => IncomingMessages::AccountSummaryEnd,
333            65 => IncomingMessages::VerifyMessageApi,
334            66 => IncomingMessages::VerifyCompleted,
335            67 => IncomingMessages::DisplayGroupList,
336            68 => IncomingMessages::DisplayGroupUpdated,
337            69 => IncomingMessages::VerifyAndAuthMessageApi,
338            70 => IncomingMessages::VerifyAndAuthCompleted,
339            71 => IncomingMessages::PositionMulti,
340            72 => IncomingMessages::PositionMultiEnd,
341            73 => IncomingMessages::AccountUpdateMulti,
342            74 => IncomingMessages::AccountUpdateMultiEnd,
343            75 => IncomingMessages::SecurityDefinitionOptionParameter,
344            76 => IncomingMessages::SecurityDefinitionOptionParameterEnd,
345            77 => IncomingMessages::SoftDollarTier,
346            78 => IncomingMessages::FamilyCodes,
347            79 => IncomingMessages::SymbolSamples,
348            80 => IncomingMessages::MktDepthExchanges,
349            81 => IncomingMessages::TickReqParams,
350            82 => IncomingMessages::SmartComponents,
351            83 => IncomingMessages::NewsArticle,
352            84 => IncomingMessages::TickNews,
353            85 => IncomingMessages::NewsProviders,
354            86 => IncomingMessages::HistoricalNews,
355            87 => IncomingMessages::HistoricalNewsEnd,
356            88 => IncomingMessages::HeadTimestamp,
357            89 => IncomingMessages::HistogramData,
358            90 => IncomingMessages::HistoricalDataUpdate,
359            91 => IncomingMessages::RerouteMktDataReq,
360            92 => IncomingMessages::RerouteMktDepthReq,
361            93 => IncomingMessages::MarketRule,
362            94 => IncomingMessages::PnL,
363            95 => IncomingMessages::PnLSingle,
364            96 => IncomingMessages::HistoricalTick,
365            97 => IncomingMessages::HistoricalTickBidAsk,
366            98 => IncomingMessages::HistoricalTickLast,
367            99 => IncomingMessages::TickByTick,
368            100 => IncomingMessages::OrderBound,
369            101 => IncomingMessages::CompletedOrder,
370            102 => IncomingMessages::CompletedOrdersEnd,
371            103 => IncomingMessages::ReplaceFAEnd,
372            104 => IncomingMessages::WshMetaData,
373            105 => IncomingMessages::WshEventData,
374            106 => IncomingMessages::HistoricalSchedule,
375            107 => IncomingMessages::UserInfo,
376            108 => IncomingMessages::HistoricalDataEnd,
377            109 => IncomingMessages::CurrentTimeInMillis,
378            110 => IncomingMessages::ConfigResponse,
379            111 => IncomingMessages::UpdateConfigResponse,
380            _ => IncomingMessages::NotValid,
381        }
382    }
383}
384
385impl FromStr for IncomingMessages {
386    type Err = Error;
387
388    fn from_str(s: &str) -> Result<Self, Self::Err> {
389        match s.parse::<i32>() {
390            Ok(n) => Ok(IncomingMessages::from(n)),
391            Err(_) => Err(Error::Simple(format!("Invalid incoming message type: {}", s))),
392        }
393    }
394}
395
396/// Return the message field index containing the order id, if present.
397pub fn order_id_index(kind: IncomingMessages) -> Option<usize> {
398    match kind {
399        IncomingMessages::OpenOrder | IncomingMessages::OrderStatus => Some(1),
400        IncomingMessages::ExecutionData | IncomingMessages::ExecutionDataEnd => Some(2),
401        _ => None,
402    }
403}
404
405/// Return the message field index containing the request id, if present.
406pub fn request_id_index(kind: IncomingMessages) -> Option<usize> {
407    match kind {
408        IncomingMessages::AccountSummary => Some(2),
409        IncomingMessages::AccountSummaryEnd => Some(2),
410        IncomingMessages::AccountUpdateMulti => Some(2),
411        IncomingMessages::AccountUpdateMultiEnd => Some(2),
412        IncomingMessages::ContractData => Some(1),
413        IncomingMessages::ContractDataEnd => Some(2),
414        // Error uses version-dependent indices; use ResponseMessage::error_request_id() instead.
415        IncomingMessages::ExecutionData => Some(1),
416        IncomingMessages::ExecutionDataEnd => Some(2),
417        IncomingMessages::HeadTimestamp => Some(1),
418        IncomingMessages::HistogramData => Some(1),
419        IncomingMessages::HistoricalData => Some(1),
420        IncomingMessages::HistoricalDataEnd => Some(1),
421        IncomingMessages::HistoricalDataUpdate => Some(1),
422        IncomingMessages::HistoricalNews => Some(1),
423        IncomingMessages::HistoricalNewsEnd => Some(1),
424        IncomingMessages::HistoricalSchedule => Some(1),
425        IncomingMessages::HistoricalTick => Some(1),
426        IncomingMessages::HistoricalTickBidAsk => Some(1),
427        IncomingMessages::HistoricalTickLast => Some(1),
428        IncomingMessages::MarketDepth => Some(2),
429        IncomingMessages::MarketDepthL2 => Some(2),
430        IncomingMessages::NewsArticle => Some(1),
431        IncomingMessages::OpenOrder => Some(1),
432        IncomingMessages::PnL => Some(1),
433        IncomingMessages::PnLSingle => Some(1),
434        IncomingMessages::PositionMulti => Some(2),
435        IncomingMessages::PositionMultiEnd => Some(2),
436        IncomingMessages::RealTimeBars => Some(2),
437        IncomingMessages::ScannerData => Some(2),
438        IncomingMessages::SecurityDefinitionOptionParameter => Some(1),
439        IncomingMessages::SecurityDefinitionOptionParameterEnd => Some(1),
440        IncomingMessages::SymbolSamples => Some(1),
441        IncomingMessages::TickByTick => Some(1),
442        IncomingMessages::TickEFP => Some(2),
443        IncomingMessages::TickGeneric => Some(2),
444        IncomingMessages::TickNews => Some(1),
445        IncomingMessages::TickOptionComputation => Some(1),
446        IncomingMessages::TickPrice => Some(2),
447        IncomingMessages::TickReqParams => Some(1),
448        IncomingMessages::TickSize => Some(2),
449        IncomingMessages::TickSnapshotEnd => Some(2),
450        IncomingMessages::TickString => Some(2),
451        IncomingMessages::WshEventData => Some(1),
452        IncomingMessages::WshMetaData => Some(1),
453        IncomingMessages::DisplayGroupList => Some(2),
454        IncomingMessages::DisplayGroupUpdated => Some(2),
455
456        _ => {
457            debug!("could not determine request id index for {kind:?} (this message type may not have a request id).");
458            None
459        }
460    }
461}
462
463/// Outgoing message opcodes understood by TWS/Gateway.
464#[allow(dead_code)]
465#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
466pub enum OutgoingMessages {
467    /// Request streaming market data.
468    RequestMarketData = 1,
469    /// Cancel streaming market data.
470    CancelMarketData = 2,
471    /// Submit a new order.
472    PlaceOrder = 3,
473    /// Cancel an existing order.
474    CancelOrder = 4,
475    /// Request the current open orders.
476    RequestOpenOrders = 5,
477    /// Request account value updates.
478    RequestAccountData = 6,
479    /// Request execution reports.
480    RequestExecutions = 7,
481    /// Request a block of valid order ids.
482    RequestIds = 8,
483    /// Request contract details.
484    RequestContractData = 9,
485    /// Request level-two market depth.
486    RequestMarketDepth = 10,
487    /// Cancel level-two market depth.
488    CancelMarketDepth = 11,
489    /// Subscribe to news bulletins.
490    RequestNewsBulletins = 12,
491    /// Cancel news bulletin subscription.
492    CancelNewsBulletin = 13,
493    /// Change the server log level.
494    ChangeServerLog = 14,
495    /// Request auto-open orders.
496    RequestAutoOpenOrders = 15,
497    /// Request all open orders.
498    RequestAllOpenOrders = 16,
499    /// Request managed accounts list.
500    RequestManagedAccounts = 17,
501    /// Request financial advisor configuration.
502    RequestFA = 18,
503    /// Replace financial advisor configuration.
504    ReplaceFA = 19,
505    /// Request historical bar data.
506    RequestHistoricalData = 20,
507    /// Exercise an option contract.
508    ExerciseOptions = 21,
509    /// Subscribe to a market scanner.
510    RequestScannerSubscription = 22,
511    /// Cancel a market scanner subscription.
512    CancelScannerSubscription = 23,
513    /// Request scanner parameter definitions.
514    RequestScannerParameters = 24,
515    /// Cancel an in-flight historical data request.
516    CancelHistoricalData = 25,
517    /// Request the current TWS/Gateway time.
518    RequestCurrentTime = 49,
519    /// Request real-time bars.
520    RequestRealTimeBars = 50,
521    /// Cancel real-time bars.
522    CancelRealTimeBars = 51,
523    /// Request fundamental data.
524    RequestFundamentalData = 52,
525    /// Cancel fundamental data.
526    CancelFundamentalData = 53,
527    /// Request implied volatility calculation.
528    ReqCalcImpliedVolat = 54,
529    /// Request option price calculation.
530    ReqCalcOptionPrice = 55,
531    /// Cancel implied volatility calculation.
532    CancelImpliedVolatility = 56,
533    /// Cancel option price calculation.
534    CancelOptionPrice = 57,
535    /// Issue a global cancel request.
536    RequestGlobalCancel = 58,
537    /// Change the active market data type.
538    RequestMarketDataType = 59,
539    /// Subscribe to position updates.
540    RequestPositions = 61,
541    /// Subscribe to account summary.
542    RequestAccountSummary = 62,
543    /// Cancel account summary subscription.
544    CancelAccountSummary = 63,
545    /// Cancel position subscription.
546    CancelPositions = 64,
547    /// Begin API verification handshake.
548    VerifyRequest = 65,
549    /// Respond to verification handshake.
550    VerifyMessage = 66,
551    /// Query display groups.
552    QueryDisplayGroups = 67,
553    /// Subscribe to display group events.
554    SubscribeToGroupEvents = 68,
555    /// Update a display group subscription.
556    UpdateDisplayGroup = 69,
557    /// Unsubscribe from display group events.
558    UnsubscribeFromGroupEvents = 70,
559    /// Start the API session.
560    StartApi = 71,
561    /// Verification handshake with auth.
562    VerifyAndAuthRequest = 72,
563    /// Verification message with auth.
564    VerifyAndAuthMessage = 73,
565    /// Request multi-account/model positions.
566    RequestPositionsMulti = 74,
567    /// Cancel multi-account/model positions.
568    CancelPositionsMulti = 75,
569    /// Request multi-account/model updates.
570    RequestAccountUpdatesMulti = 76,
571    /// Cancel multi-account/model updates.
572    CancelAccountUpdatesMulti = 77,
573    /// Request option security definition parameters.
574    RequestSecurityDefinitionOptionalParameters = 78,
575    /// Request soft-dollar tier definitions.
576    RequestSoftDollarTiers = 79,
577    /// Request family codes.
578    RequestFamilyCodes = 80,
579    /// Request matching symbols.
580    RequestMatchingSymbols = 81,
581    /// Request exchanges that support depth.
582    RequestMktDepthExchanges = 82,
583    /// Request smart routing component map.
584    RequestSmartComponents = 83,
585    /// Request detailed news article.
586    RequestNewsArticle = 84,
587    /// Request available news providers.
588    RequestNewsProviders = 85,
589    /// Request historical news headlines.
590    RequestHistoricalNews = 86,
591    /// Request earliest timestamp for historical data.
592    RequestHeadTimestamp = 87,
593    /// Request histogram snapshot.
594    RequestHistogramData = 88,
595    /// Cancel histogram snapshot.
596    CancelHistogramData = 89,
597    /// Cancel head timestamp request.
598    CancelHeadTimestamp = 90,
599    /// Request market rule definition.
600    RequestMarketRule = 91,
601    /// Request account-wide PnL stream.
602    RequestPnL = 92,
603    /// Cancel account-wide PnL stream.
604    CancelPnL = 93,
605    /// Request single-position PnL stream.
606    RequestPnLSingle = 94,
607    /// Cancel single-position PnL stream.
608    CancelPnLSingle = 95,
609    /// Request historical tick data.
610    RequestHistoricalTicks = 96,
611    /// Request tick-by-tick data.
612    RequestTickByTickData = 97,
613    /// Cancel tick-by-tick data.
614    CancelTickByTickData = 98,
615    /// Request completed order history.
616    RequestCompletedOrders = 99,
617    /// Request Wall Street Horizon metadata.
618    RequestWshMetaData = 100,
619    /// Cancel Wall Street Horizon metadata.
620    CancelWshMetaData = 101,
621    /// Request Wall Street Horizon event data.
622    RequestWshEventData = 102,
623    /// Cancel Wall Street Horizon event data.
624    CancelWshEventData = 103,
625    /// Request user information.
626    RequestUserInfo = 104,
627    /// Request current time in milliseconds.
628    RequestCurrentTimeInMillis = 105,
629    /// Cancel contract data request.
630    CancelContractData = 106,
631    /// Cancel historical ticks request.
632    CancelHistoricalTicks = 107,
633    /// Request configuration.
634    ReqConfig = 108,
635    /// Update configuration.
636    UpdateConfig = 109,
637}
638
639impl ToField for OutgoingMessages {
640    fn to_field(&self) -> String {
641        (*self as i32).to_string()
642    }
643}
644
645impl std::fmt::Display for OutgoingMessages {
646    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
647        write!(f, "{}", *self as i32)
648    }
649}
650
651impl FromStr for OutgoingMessages {
652    type Err = Error;
653
654    fn from_str(s: &str) -> Result<Self, Self::Err> {
655        match s.parse::<i32>() {
656            Ok(1) => Ok(OutgoingMessages::RequestMarketData),
657            Ok(2) => Ok(OutgoingMessages::CancelMarketData),
658            Ok(3) => Ok(OutgoingMessages::PlaceOrder),
659            Ok(4) => Ok(OutgoingMessages::CancelOrder),
660            Ok(5) => Ok(OutgoingMessages::RequestOpenOrders),
661            Ok(6) => Ok(OutgoingMessages::RequestAccountData),
662            Ok(7) => Ok(OutgoingMessages::RequestExecutions),
663            Ok(8) => Ok(OutgoingMessages::RequestIds),
664            Ok(9) => Ok(OutgoingMessages::RequestContractData),
665            Ok(10) => Ok(OutgoingMessages::RequestMarketDepth),
666            Ok(11) => Ok(OutgoingMessages::CancelMarketDepth),
667            Ok(12) => Ok(OutgoingMessages::RequestNewsBulletins),
668            Ok(13) => Ok(OutgoingMessages::CancelNewsBulletin),
669            Ok(14) => Ok(OutgoingMessages::ChangeServerLog),
670            Ok(15) => Ok(OutgoingMessages::RequestAutoOpenOrders),
671            Ok(16) => Ok(OutgoingMessages::RequestAllOpenOrders),
672            Ok(17) => Ok(OutgoingMessages::RequestManagedAccounts),
673            Ok(18) => Ok(OutgoingMessages::RequestFA),
674            Ok(19) => Ok(OutgoingMessages::ReplaceFA),
675            Ok(20) => Ok(OutgoingMessages::RequestHistoricalData),
676            Ok(21) => Ok(OutgoingMessages::ExerciseOptions),
677            Ok(22) => Ok(OutgoingMessages::RequestScannerSubscription),
678            Ok(23) => Ok(OutgoingMessages::CancelScannerSubscription),
679            Ok(24) => Ok(OutgoingMessages::RequestScannerParameters),
680            Ok(25) => Ok(OutgoingMessages::CancelHistoricalData),
681            Ok(49) => Ok(OutgoingMessages::RequestCurrentTime),
682            Ok(50) => Ok(OutgoingMessages::RequestRealTimeBars),
683            Ok(51) => Ok(OutgoingMessages::CancelRealTimeBars),
684            Ok(52) => Ok(OutgoingMessages::RequestFundamentalData),
685            Ok(53) => Ok(OutgoingMessages::CancelFundamentalData),
686            Ok(54) => Ok(OutgoingMessages::ReqCalcImpliedVolat),
687            Ok(55) => Ok(OutgoingMessages::ReqCalcOptionPrice),
688            Ok(56) => Ok(OutgoingMessages::CancelImpliedVolatility),
689            Ok(57) => Ok(OutgoingMessages::CancelOptionPrice),
690            Ok(58) => Ok(OutgoingMessages::RequestGlobalCancel),
691            Ok(59) => Ok(OutgoingMessages::RequestMarketDataType),
692            Ok(61) => Ok(OutgoingMessages::RequestPositions),
693            Ok(62) => Ok(OutgoingMessages::RequestAccountSummary),
694            Ok(63) => Ok(OutgoingMessages::CancelAccountSummary),
695            Ok(64) => Ok(OutgoingMessages::CancelPositions),
696            Ok(65) => Ok(OutgoingMessages::VerifyRequest),
697            Ok(66) => Ok(OutgoingMessages::VerifyMessage),
698            Ok(67) => Ok(OutgoingMessages::QueryDisplayGroups),
699            Ok(68) => Ok(OutgoingMessages::SubscribeToGroupEvents),
700            Ok(69) => Ok(OutgoingMessages::UpdateDisplayGroup),
701            Ok(70) => Ok(OutgoingMessages::UnsubscribeFromGroupEvents),
702            Ok(71) => Ok(OutgoingMessages::StartApi),
703            Ok(72) => Ok(OutgoingMessages::VerifyAndAuthRequest),
704            Ok(73) => Ok(OutgoingMessages::VerifyAndAuthMessage),
705            Ok(74) => Ok(OutgoingMessages::RequestPositionsMulti),
706            Ok(75) => Ok(OutgoingMessages::CancelPositionsMulti),
707            Ok(76) => Ok(OutgoingMessages::RequestAccountUpdatesMulti),
708            Ok(77) => Ok(OutgoingMessages::CancelAccountUpdatesMulti),
709            Ok(78) => Ok(OutgoingMessages::RequestSecurityDefinitionOptionalParameters),
710            Ok(79) => Ok(OutgoingMessages::RequestSoftDollarTiers),
711            Ok(80) => Ok(OutgoingMessages::RequestFamilyCodes),
712            Ok(81) => Ok(OutgoingMessages::RequestMatchingSymbols),
713            Ok(82) => Ok(OutgoingMessages::RequestMktDepthExchanges),
714            Ok(83) => Ok(OutgoingMessages::RequestSmartComponents),
715            Ok(84) => Ok(OutgoingMessages::RequestNewsArticle),
716            Ok(85) => Ok(OutgoingMessages::RequestNewsProviders),
717            Ok(86) => Ok(OutgoingMessages::RequestHistoricalNews),
718            Ok(87) => Ok(OutgoingMessages::RequestHeadTimestamp),
719            Ok(88) => Ok(OutgoingMessages::RequestHistogramData),
720            Ok(89) => Ok(OutgoingMessages::CancelHistogramData),
721            Ok(90) => Ok(OutgoingMessages::CancelHeadTimestamp),
722            Ok(91) => Ok(OutgoingMessages::RequestMarketRule),
723            Ok(92) => Ok(OutgoingMessages::RequestPnL),
724            Ok(93) => Ok(OutgoingMessages::CancelPnL),
725            Ok(94) => Ok(OutgoingMessages::RequestPnLSingle),
726            Ok(95) => Ok(OutgoingMessages::CancelPnLSingle),
727            Ok(96) => Ok(OutgoingMessages::RequestHistoricalTicks),
728            Ok(97) => Ok(OutgoingMessages::RequestTickByTickData),
729            Ok(98) => Ok(OutgoingMessages::CancelTickByTickData),
730            Ok(99) => Ok(OutgoingMessages::RequestCompletedOrders),
731            Ok(100) => Ok(OutgoingMessages::RequestWshMetaData),
732            Ok(101) => Ok(OutgoingMessages::CancelWshMetaData),
733            Ok(102) => Ok(OutgoingMessages::RequestWshEventData),
734            Ok(103) => Ok(OutgoingMessages::CancelWshEventData),
735            Ok(104) => Ok(OutgoingMessages::RequestUserInfo),
736            Ok(105) => Ok(OutgoingMessages::RequestCurrentTimeInMillis),
737            Ok(106) => Ok(OutgoingMessages::CancelContractData),
738            Ok(107) => Ok(OutgoingMessages::CancelHistoricalTicks),
739            Ok(108) => Ok(OutgoingMessages::ReqConfig),
740            Ok(109) => Ok(OutgoingMessages::UpdateConfig),
741            Ok(n) => Err(Error::Simple(format!("Unknown outgoing message type: {}", n))),
742            Err(_) => Err(Error::Simple(format!("Invalid outgoing message type: {}", s))),
743        }
744    }
745}
746
747/// Encode the outbound message length prefix using the IB wire format.
748pub fn encode_length(message: &str) -> Vec<u8> {
749    let data = message.as_bytes();
750
751    let mut packet: Vec<u8> = Vec::with_capacity(data.len() + 4);
752
753    packet.write_u32::<BigEndian>(data.len() as u32).unwrap();
754    packet.write_all(data).unwrap();
755    packet
756}
757
758/// Builder for outbound TWS/Gateway request messages.
759#[derive(Default, Debug, Clone)]
760pub struct RequestMessage {
761    pub(crate) fields: Vec<String>,
762}
763
764impl RequestMessage {
765    /// Create a new empty request message.
766    pub fn new() -> Self {
767        Self::default()
768    }
769
770    pub(crate) fn push_field<T: ToField>(&mut self, val: &T) -> &RequestMessage {
771        let field = val.to_field();
772        self.fields.push(field);
773        self
774    }
775
776    /// Serialize all fields into the NUL-delimited wire format.
777    pub fn encode(&self) -> String {
778        let mut data = self.fields.join("\0");
779        data.push('\0');
780        data
781    }
782
783    #[cfg(test)]
784    pub(crate) fn len(&self) -> usize {
785        self.fields.len()
786    }
787
788    #[cfg(test)]
789    /// Serialize the message as a pipe-delimited string (test helper).
790    pub(crate) fn encode_simple(&self) -> String {
791        let mut data = self.fields.join("|");
792        data.push('|');
793        data
794    }
795    #[cfg(test)]
796    /// Construct a request message from a NUL-delimited string (test helper).
797    pub fn from(fields: &str) -> RequestMessage {
798        RequestMessage {
799            fields: fields.split_terminator('\x00').map(|x| x.to_string()).collect(),
800        }
801    }
802    #[cfg(test)]
803    /// Construct a request message from a pipe-delimited string (test helper).
804    pub fn from_simple(fields: &str) -> RequestMessage {
805        RequestMessage {
806            fields: fields.split_terminator('|').map(|x| x.to_string()).collect(),
807        }
808    }
809}
810
811impl Index<usize> for RequestMessage {
812    type Output = String;
813
814    fn index(&self, i: usize) -> &Self::Output {
815        &self.fields[i]
816    }
817}
818
819/// Parsed inbound message from TWS/Gateway.
820#[derive(Clone, Default, Debug)]
821pub struct ResponseMessage {
822    /// Cursor index for incremental decoding.
823    pub i: usize,
824    /// Raw field buffer backing this message.
825    pub fields: Vec<String>,
826    /// Server version for version-gated decoding (e.g. error message format).
827    pub server_version: i32,
828}
829
830impl ResponseMessage {
831    /// Number of fields present in the message.
832    pub fn len(&self) -> usize {
833        self.fields.len()
834    }
835
836    /// Returns `true` if the message contains no fields.
837    pub fn is_empty(&self) -> bool {
838        self.fields.is_empty()
839    }
840
841    /// Returns `true` if the message informs about API shutdown.
842    pub fn is_shutdown(&self) -> bool {
843        self.message_type() == IncomingMessages::Shutdown
844    }
845
846    /// Return the discriminator identifying the message payload.
847    pub fn message_type(&self) -> IncomingMessages {
848        if self.fields.is_empty() {
849            IncomingMessages::NotValid
850        } else {
851            let message_id = i32::from_str(&self.fields[0]).unwrap_or(-1);
852            IncomingMessages::from(message_id)
853        }
854    }
855
856    /// Try to extract the request id from the message.
857    pub fn request_id(&self) -> Option<i32> {
858        if let Some(i) = request_id_index(self.message_type()) {
859            if let Ok(request_id) = self.peek_int(i) {
860                return Some(request_id);
861            }
862        }
863        None
864    }
865
866    /// Try to extract the order id from the message.
867    pub fn order_id(&self) -> Option<i32> {
868        if let Some(i) = order_id_index(self.message_type()) {
869            if let Ok(order_id) = self.peek_int(i) {
870                return Some(order_id);
871            }
872        }
873        None
874    }
875
876    /// Try to extract the execution id from the message.
877    pub fn execution_id(&self) -> Option<String> {
878        match self.message_type() {
879            IncomingMessages::ExecutionData => Some(self.peek_string(14)),
880            IncomingMessages::CommissionsReport => Some(self.peek_string(2)),
881            _ => None,
882        }
883    }
884
885    /// Peek an integer field without advancing the cursor.
886    pub fn peek_int(&self, i: usize) -> Result<i32, Error> {
887        if i >= self.fields.len() {
888            return Err(Error::Simple("expected int and found end of message".into()));
889        }
890
891        let field = &self.fields[i];
892        match field.parse() {
893            Ok(val) => Ok(val),
894            Err(err) => Err(Error::Parse(i, field.into(), err.to_string())),
895        }
896    }
897
898    /// Peek a long field without advancing the cursor.
899    pub fn peek_long(&self, i: usize) -> Result<i64, Error> {
900        if i >= self.fields.len() {
901            return Err(Error::Simple("expected long and found end of message".into()));
902        }
903
904        let field = &self.fields[i];
905        match field.parse() {
906            Ok(val) => Ok(val),
907            Err(err) => Err(Error::Parse(i, field.into(), err.to_string())),
908        }
909    }
910
911    /// Peek a string field without advancing the cursor.
912    pub fn peek_string(&self, i: usize) -> String {
913        self.fields[i].to_owned()
914    }
915
916    /// Consume and parse the next integer field.
917    pub fn next_int(&mut self) -> Result<i32, Error> {
918        if self.i >= self.fields.len() {
919            return Err(Error::Simple("expected int and found end of message".into()));
920        }
921
922        let field = &self.fields[self.i];
923        self.i += 1;
924
925        match field.parse() {
926            Ok(val) => Ok(val),
927            Err(err) => Err(Error::Parse(self.i, field.into(), err.to_string())),
928        }
929    }
930
931    /// Consume the next field returning `None` when unset.
932    pub fn next_optional_int(&mut self) -> Result<Option<i32>, Error> {
933        if self.i >= self.fields.len() {
934            return Err(Error::Simple("expected optional int and found end of message".into()));
935        }
936
937        let field = &self.fields[self.i];
938        self.i += 1;
939
940        if field.is_empty() || field == UNSET_INTEGER {
941            return Ok(None);
942        }
943
944        match field.parse::<i32>() {
945            Ok(val) => Ok(Some(val)),
946            Err(err) => Err(Error::Parse(self.i, field.into(), err.to_string())),
947        }
948    }
949
950    /// Consume the next field as a boolean (`"0"` or `"1"`).
951    pub fn next_bool(&mut self) -> Result<bool, Error> {
952        if self.i >= self.fields.len() {
953            return Err(Error::Simple("expected bool and found end of message".into()));
954        }
955
956        let field = &self.fields[self.i];
957        self.i += 1;
958
959        Ok(field == "1")
960    }
961
962    /// Consume and parse the next i64 field.
963    pub fn next_long(&mut self) -> Result<i64, Error> {
964        if self.i >= self.fields.len() {
965            return Err(Error::Simple("expected long and found end of message".into()));
966        }
967
968        let field = &self.fields[self.i];
969        self.i += 1;
970
971        match field.parse() {
972            Ok(val) => Ok(val),
973            Err(err) => Err(Error::Parse(self.i, field.into(), err.to_string())),
974        }
975    }
976
977    /// Consume the next field as an optional i64.
978    pub fn next_optional_long(&mut self) -> Result<Option<i64>, Error> {
979        if self.i >= self.fields.len() {
980            return Err(Error::Simple("expected optional long and found end of message".into()));
981        }
982
983        let field = &self.fields[self.i];
984        self.i += 1;
985
986        if field.is_empty() || field == UNSET_LONG {
987            return Ok(None);
988        }
989
990        match field.parse::<i64>() {
991            Ok(val) => Ok(Some(val)),
992            Err(err) => Err(Error::Parse(self.i, field.into(), err.to_string())),
993        }
994    }
995
996    /// Consume the next field and parse it as a UTC timestamp.
997    pub fn next_date_time(&mut self) -> Result<OffsetDateTime, Error> {
998        if self.i >= self.fields.len() {
999            return Err(Error::Simple("expected datetime and found end of message".into()));
1000        }
1001
1002        let field = &self.fields[self.i];
1003        self.i += 1;
1004
1005        if field.is_empty() {
1006            return Err(Error::Simple("expected timestamp and found empty string".into()));
1007        }
1008
1009        // from_unix_timestamp
1010        let timestamp: i64 = field.parse()?;
1011        match OffsetDateTime::from_unix_timestamp(timestamp) {
1012            Ok(val) => Ok(val),
1013            Err(err) => Err(Error::Parse(self.i, field.into(), err.to_string())),
1014        }
1015    }
1016
1017    /// Consume the next field as a string.
1018    pub fn next_string(&mut self) -> Result<String, Error> {
1019        if self.i >= self.fields.len() {
1020            return Err(Error::Simple("expected string and found end of message".into()));
1021        }
1022
1023        let field = &self.fields[self.i];
1024        self.i += 1;
1025        Ok(String::from(field))
1026    }
1027
1028    /// Consume and parse the next floating-point field.
1029    pub fn next_double(&mut self) -> Result<f64, Error> {
1030        if self.i >= self.fields.len() {
1031            return Err(Error::Simple("expected double and found end of message".into()));
1032        }
1033
1034        let field = &self.fields[self.i];
1035        self.i += 1;
1036
1037        if field.is_empty() || field == "0" || field == "0.0" {
1038            return Ok(0.0);
1039        }
1040
1041        match field.parse() {
1042            Ok(val) => Ok(val),
1043            Err(err) => Err(Error::Parse(self.i, field.into(), err.to_string())),
1044        }
1045    }
1046
1047    /// Consume the next field as an optional floating-point value.
1048    pub fn next_optional_double(&mut self) -> Result<Option<f64>, Error> {
1049        if self.i >= self.fields.len() {
1050            return Err(Error::Simple("expected optional double and found end of message".into()));
1051        }
1052
1053        let field = &self.fields[self.i];
1054        self.i += 1;
1055
1056        if field.is_empty() || field == UNSET_DOUBLE {
1057            return Ok(None);
1058        }
1059
1060        if field == INFINITY_STR {
1061            return Ok(Some(f64::INFINITY));
1062        }
1063
1064        match field.parse() {
1065            Ok(val) => Ok(Some(val)),
1066            Err(err) => Err(Error::Parse(self.i, field.into(), err.to_string())),
1067        }
1068    }
1069
1070    /// Offset applied to error field indices based on server version.
1071    /// New format (>= ERROR_TIME) drops the version field, shifting indices by -1.
1072    fn error_field_offset(&self) -> usize {
1073        if self.server_version >= crate::server_versions::ERROR_TIME {
1074            0
1075        } else {
1076            1
1077        }
1078    }
1079
1080    /// Field index of the request ID in an error message.
1081    pub fn error_request_id_index(&self) -> usize {
1082        1 + self.error_field_offset()
1083    }
1084
1085    /// Field index of the error code in an error message.
1086    pub fn error_code_index(&self) -> usize {
1087        2 + self.error_field_offset()
1088    }
1089
1090    /// Field index of the error message text in an error message.
1091    pub fn error_message_index(&self) -> usize {
1092        3 + self.error_field_offset()
1093    }
1094
1095    /// Extract the request ID from an error message.
1096    pub fn error_request_id(&self) -> i32 {
1097        self.peek_int(self.error_request_id_index()).unwrap_or(-1)
1098    }
1099
1100    /// Extract the error code from an error message.
1101    pub fn error_code(&self) -> i32 {
1102        self.peek_int(self.error_code_index()).unwrap_or(0)
1103    }
1104
1105    /// Extract the error message text from an error message.
1106    pub fn error_message(&self) -> String {
1107        let idx = self.error_message_index();
1108        if idx < self.fields.len() {
1109            self.peek_string(idx)
1110        } else {
1111            String::from("Unknown error")
1112        }
1113    }
1114
1115    /// Extract the error timestamp from an error message.
1116    /// Only present for server versions >= ERROR_TIME.
1117    pub fn error_time(&self) -> Option<OffsetDateTime> {
1118        if self.server_version >= crate::server_versions::ERROR_TIME {
1119            // New format: msg_type, request_id, error_code, error_msg, advanced_order_reject_json, error_time
1120            let idx = self.error_message_index() + 2;
1121            let millis = self.peek_long(idx).ok()?;
1122            OffsetDateTime::from_unix_timestamp_nanos(millis as i128 * 1_000_000).ok()
1123        } else {
1124            None
1125        }
1126    }
1127
1128    /// Build a response message from a NUL-delimited payload.
1129    pub fn from(fields: &str) -> ResponseMessage {
1130        ResponseMessage {
1131            i: 0,
1132            fields: fields.split_terminator('\x00').map(|x| x.to_string()).collect(),
1133            server_version: 0,
1134        }
1135    }
1136    #[cfg(test)]
1137    /// Build a response message from a pipe-delimited payload (test helper).
1138    pub fn from_simple(fields: &str) -> ResponseMessage {
1139        ResponseMessage {
1140            i: 0,
1141            fields: fields.split_terminator('|').map(|x| x.to_string()).collect(),
1142            server_version: 0,
1143        }
1144    }
1145
1146    /// Set the server version for version-gated decoding (builder style).
1147    pub fn with_server_version(mut self, server_version: i32) -> Self {
1148        self.server_version = server_version;
1149        self
1150    }
1151
1152    /// Advance the cursor past the next field.
1153    pub fn skip(&mut self) {
1154        self.i += 1;
1155    }
1156
1157    /// Encode the message back into a NUL-delimited string.
1158    pub fn encode(&self) -> String {
1159        let mut data = self.fields.join("\0");
1160        data.push('\0');
1161        data
1162    }
1163
1164    #[cfg(test)]
1165    /// Serialize the message into a pipe-delimited format (test helper).
1166    pub fn encode_simple(&self) -> String {
1167        let mut data = self.fields.join("|");
1168        data.push('|');
1169        data
1170    }
1171}
1172
1173/// An error message from the TWS API.
1174#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
1175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
1176pub struct Notice {
1177    /// Error code reported by TWS.
1178    pub code: i32,
1179    /// Human-readable error message text.
1180    pub message: String,
1181    /// Timestamp when the error occurred.
1182    /// Only present for server versions >= ERROR_TIME (194).
1183    pub error_time: Option<OffsetDateTime>,
1184}
1185
1186/// Error code indicating an order was cancelled (confirmation, not an error).
1187pub const ORDER_CANCELLED_CODE: i32 = 202;
1188
1189/// Range of error codes that are considered warnings (2100-2169).
1190pub const WARNING_CODE_RANGE: std::ops::RangeInclusive<i32> = 2100..=2169;
1191
1192/// System message codes indicating connectivity status.
1193/// - 1100: Connectivity lost
1194/// - 1101: Connectivity restored, market data lost (resubscribe needed)
1195/// - 1102: Connectivity restored, market data maintained
1196/// - 1300: Socket port reset during active connection
1197pub const SYSTEM_MESSAGE_CODES: [i32; 4] = [1100, 1101, 1102, 1300];
1198
1199impl Notice {
1200    #[allow(private_interfaces)]
1201    /// Construct a notice from a response message.
1202    pub fn from(message: &ResponseMessage) -> Notice {
1203        let code = message.error_code();
1204        let error_time = message.error_time();
1205        let message = message.error_message();
1206        Notice { code, message, error_time }
1207    }
1208
1209    /// Returns `true` if this notice indicates an order was cancelled (code 202).
1210    ///
1211    /// Code 202 is sent by TWS to confirm an order cancellation. This is an
1212    /// informational message, not an error.
1213    pub fn is_cancellation(&self) -> bool {
1214        self.code == ORDER_CANCELLED_CODE
1215    }
1216
1217    /// Returns `true` if this is a warning message (codes 2100-2169).
1218    pub fn is_warning(&self) -> bool {
1219        WARNING_CODE_RANGE.contains(&self.code)
1220    }
1221
1222    /// Returns `true` if this is a system/connectivity message (codes 1100-1102, 1300).
1223    ///
1224    /// System messages indicate connectivity status changes:
1225    /// - 1100: Connectivity between IB and TWS lost
1226    /// - 1101: Connectivity restored, market data lost (resubscribe needed)
1227    /// - 1102: Connectivity restored, market data maintained
1228    /// - 1300: Socket port reset during active connection
1229    pub fn is_system_message(&self) -> bool {
1230        SYSTEM_MESSAGE_CODES.contains(&self.code)
1231    }
1232
1233    /// Returns `true` if this is an informational notice (not an error).
1234    ///
1235    /// Informational notices include cancellation confirmations, warnings,
1236    /// and system/connectivity messages.
1237    pub fn is_informational(&self) -> bool {
1238        self.is_cancellation() || self.is_warning() || self.is_system_message()
1239    }
1240
1241    /// Returns `true` if this is an error requiring attention.
1242    ///
1243    /// Returns `false` for informational messages like cancellation confirmations,
1244    /// warnings, and system messages.
1245    pub fn is_error(&self) -> bool {
1246        !self.is_informational()
1247    }
1248}
1249
1250impl Display for Notice {
1251    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1252        write!(f, "[{}] {}", self.code, self.message)
1253    }
1254}