use log::{debug, error, warn};
use time::macros::format_description;
use time::OffsetDateTime;
use time_tz::Tz;
use crate::accounts::AccountUpdate;
use crate::common::timezone::{find_timezone, resolve_local};
use crate::errors::Error;
use crate::messages::{
encode_length, encode_protobuf_message, unknown_message_type_notice, IncomingMessages, Notice, OutgoingMessages, ResponseMessage,
HANDSHAKE_DECODE_FAILURE_CODE, HANDSHAKE_UNKNOWN_FRAME_CODE, MESSAGE_ID_LEN, PROTOBUF_MSG_ID,
};
use crate::orders::{CommissionReport, ExecutionData, OrderData, OrderStatus};
use crate::server_versions;
use crate::transport::common::NoticeSink;
#[derive(Debug)]
#[non_exhaustive]
#[allow(clippy::large_enum_variant)]
pub enum StartupMessage {
OpenOrder(OrderData),
OrderStatus(OrderStatus),
OpenOrderEnd,
AccountUpdate(AccountUpdate),
Execution(ExecutionData),
CommissionReport(CommissionReport),
CompletedOrder(OrderData),
ExecutionDataEnd,
CompletedOrdersEnd,
}
impl StartupMessage {
pub fn message_type(&self) -> IncomingMessages {
match self {
StartupMessage::OpenOrder(_) => IncomingMessages::OpenOrder,
StartupMessage::OrderStatus(_) => IncomingMessages::OrderStatus,
StartupMessage::OpenOrderEnd => IncomingMessages::OpenOrderEnd,
StartupMessage::AccountUpdate(au) => match au {
AccountUpdate::AccountValue(_) => IncomingMessages::AccountValue,
AccountUpdate::PortfolioValue(_) => IncomingMessages::PortfolioValue,
AccountUpdate::UpdateTime(_) => IncomingMessages::AccountUpdateTime,
AccountUpdate::End => IncomingMessages::AccountDownloadEnd,
},
StartupMessage::Execution(_) => IncomingMessages::ExecutionData,
StartupMessage::CommissionReport(_) => IncomingMessages::CommissionsReport,
StartupMessage::CompletedOrder(_) => IncomingMessages::CompletedOrder,
StartupMessage::ExecutionDataEnd => IncomingMessages::ExecutionDataEnd,
StartupMessage::CompletedOrdersEnd => IncomingMessages::CompletedOrdersEnd,
}
}
}
pub(crate) struct StartupHandshakeContext<'a> {
pub startup: Option<&'a (dyn Fn(StartupMessage) + Send + Sync)>,
pub notice_sink: &'a (dyn NoticeSink + Sync),
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct HandshakeData {
pub min_version: i32,
pub max_version: i32,
pub server_version: i32,
pub server_time: String,
}
pub trait ConnectionProtocol {
type Error;
fn format_handshake(&self) -> Vec<u8>;
fn parse_handshake_response(&self, response: &mut ResponseMessage) -> Result<HandshakeData, Self::Error>;
fn format_start_api(&self, client_id: i32, server_version: i32) -> Vec<u8>;
fn parse_account_info(
&self,
server_version: i32,
message: &mut ResponseMessage,
ctx: &StartupHandshakeContext<'_>,
) -> Result<AccountInfo, Self::Error>;
}
#[derive(Debug, Clone, Default)]
pub struct AccountInfo {
pub next_order_id: Option<i32>,
pub managed_accounts: Option<String>,
}
#[derive(Debug)]
pub struct ConnectionHandler {
pub min_version: i32,
pub max_version: i32,
}
impl Default for ConnectionHandler {
fn default() -> Self {
Self {
min_version: server_versions::PROTOBUF_REST_MESSAGES_3,
max_version: server_versions::ODD_LOT_BID_ASK_QUOTES,
}
}
}
impl ConnectionProtocol for ConnectionHandler {
type Error = Error;
fn format_handshake(&self) -> Vec<u8> {
let version_string = format!("v{}..{}", self.min_version, self.max_version);
debug!("Handshake version: {version_string}");
let mut handshake = Vec::from(b"API\0");
handshake.extend_from_slice(&encode_length(&version_string));
handshake
}
fn parse_handshake_response(&self, response: &mut ResponseMessage) -> Result<HandshakeData, Self::Error> {
let server_version = response.next_int()?;
let server_time = response.next_string()?;
Ok(HandshakeData {
min_version: self.min_version,
max_version: self.max_version,
server_version,
server_time,
})
}
fn format_start_api(&self, client_id: i32, _server_version: i32) -> Vec<u8> {
use prost::Message;
let request = crate::proto::StartApiRequest {
client_id: Some(client_id),
optional_capabilities: None,
};
encode_protobuf_message(OutgoingMessages::StartApi as i32, &request.encode_to_vec())
}
fn parse_account_info(
&self,
server_version: i32,
message: &mut ResponseMessage,
ctx: &StartupHandshakeContext<'_>,
) -> Result<AccountInfo, Self::Error> {
use crate::proto::decoders::DecodeProto;
let mut info = AccountInfo::default();
match message.message_type() {
IncomingMessages::NextValidId => {
let proto = crate::proto::NextValidId::decode_proto(message.require_proto()?)?;
info.next_order_id = proto.order_id;
}
IncomingMessages::ManagedAccounts => {
let proto = crate::proto::ManagedAccounts::decode_proto(message.require_proto()?)?;
info.managed_accounts = proto.accounts_list;
}
_ => dispatch_unsolicited_message(server_version, message, ctx),
}
Ok(info)
}
}
pub(crate) fn dispatch_unsolicited_message(_server_version: i32, message: &mut ResponseMessage, ctx: &StartupHandshakeContext<'_>) {
use crate::accounts::common::decode_account_update_message;
use crate::orders::common::{decode_commission_report, decode_completed_order, decode_execution_data, decode_open_order, decode_order_status};
fn dispatch_typed<T>(
ctx: &StartupHandshakeContext<'_>,
kind: IncomingMessages,
decode: impl FnOnce() -> Result<T, Error>,
wrap: impl FnOnce(T) -> StartupMessage,
) {
let Some(cb) = ctx.startup else { return };
match decode() {
Ok(t) => cb(wrap(t)),
Err(e) => ctx.notice_sink.deliver(Notice::synthesized(
HANDSHAKE_DECODE_FAILURE_CODE,
format!("handshake decoder failed for {kind:?}: {e}"),
)),
}
}
fn dispatch_unit(ctx: &StartupHandshakeContext<'_>, msg: StartupMessage) {
if let Some(cb) = ctx.startup {
cb(msg);
}
}
let kind = message.message_type();
match kind {
IncomingMessages::Error => {
let notice = Notice::from(&*message);
notice.log();
ctx.notice_sink.deliver(notice);
}
IncomingMessages::OpenOrder => dispatch_typed(ctx, kind, || decode_open_order(message), StartupMessage::OpenOrder),
IncomingMessages::OrderStatus => dispatch_typed(ctx, kind, || decode_order_status(message), StartupMessage::OrderStatus),
IncomingMessages::OpenOrderEnd => dispatch_unit(ctx, StartupMessage::OpenOrderEnd),
IncomingMessages::AccountValue
| IncomingMessages::PortfolioValue
| IncomingMessages::AccountUpdateTime
| IncomingMessages::AccountDownloadEnd => dispatch_typed(ctx, kind, || decode_account_update_message(message), StartupMessage::AccountUpdate),
IncomingMessages::ExecutionData => dispatch_typed(ctx, kind, || decode_execution_data(message), StartupMessage::Execution),
IncomingMessages::CommissionsReport => dispatch_typed(ctx, kind, || decode_commission_report(message), StartupMessage::CommissionReport),
IncomingMessages::CompletedOrder => dispatch_typed(ctx, kind, || decode_completed_order(message), StartupMessage::CompletedOrder),
IncomingMessages::ExecutionDataEnd => dispatch_unit(ctx, StartupMessage::ExecutionDataEnd),
IncomingMessages::CompletedOrdersEnd => dispatch_unit(ctx, StartupMessage::CompletedOrdersEnd),
IncomingMessages::NotValid => ctx.notice_sink.deliver(unknown_message_type_notice(message)),
_ => {
warn!("unrouted handshake frame: {kind:?}");
ctx.notice_sink.deliver(Notice::synthesized(
HANDSHAKE_UNKNOWN_FRAME_CODE,
format!("unsolicited handshake frame with no typed variant: {kind:?}"),
));
}
}
}
pub(crate) fn require_protobuf_support(server_version: i32) -> Result<(), Error> {
if server_version < server_versions::PROTOBUF_REST_MESSAGES_3 {
return Err(Error::ServerVersion(
server_versions::PROTOBUF_REST_MESSAGES_3,
server_version,
format!(
"protobuf transport — rust-ibapi 3.x requires TWS or IB Gateway with server version {} or later; please upgrade",
server_versions::PROTOBUF_REST_MESSAGES_3
),
));
}
Ok(())
}
pub fn parse_connection_time(connection_time: &str) -> (Option<OffsetDateTime>, Option<&'static Tz>) {
let mut parts = connection_time.splitn(3, ' ');
let (Some(date), Some(time), Some(tz_name)) = (parts.next(), parts.next(), parts.next()) else {
error!("Invalid connection time format: {connection_time}");
return (None, None);
};
let Some(timezone) = find_timezone(tz_name) else {
warn!("{}", Error::UnsupportedTimeZone(tz_name.to_string()));
return (None, None);
};
let format = format_description!("[year][month][day] [hour]:[minute]:[second]");
let date_str = format!("{date} {time}");
let date = time::PrimitiveDateTime::parse(date_str.as_str(), format);
match date {
Ok(connected_at) => (Some(resolve_local(connected_at, timezone)), Some(timezone)),
Err(err) => {
warn!("Could not parse connection time from {date_str}: {err}");
(None, Some(timezone))
}
}
}
pub fn parse_raw_message(data: &[u8]) -> Result<ResponseMessage, Error> {
let Some((header, payload)) = data.split_first_chunk::<MESSAGE_ID_LEN>() else {
return Err(Error::InvalidFrame(format!(
"frame body of {} bytes cannot hold a message id",
data.len()
)));
};
let msg_id = i32::from_be_bytes(*header);
if msg_id > PROTOBUF_MSG_ID {
let real_type = msg_id - PROTOBUF_MSG_ID;
debug!("<- protobuf msg_id={real_type}");
Ok(ResponseMessage::from_protobuf(real_type, payload.to_vec()))
} else {
let raw = String::from_utf8_lossy(payload);
debug!("<- {raw:?}");
let mut fields = vec![msg_id.to_string()];
fields.extend(raw.split_terminator('\0').map(|s| s.to_string()));
Ok(ResponseMessage::from_text_fields(fields))
}
}
#[cfg(test)]
#[path = "common_tests.rs"]
mod tests;