use time::OffsetDateTime;
use crate::contracts::{Contract, Currency, Exchange, SecurityType, Symbol};
use crate::messages::ResponseMessage;
use crate::{server_versions, Error};
use super::super::{
AccountMultiValue, AccountPortfolioValue, AccountSummary, AccountUpdateTime, AccountValue, FamilyCode, PnL, PnLSingle, Position, PositionMulti,
};
pub(crate) fn decode_position(message: &mut ResponseMessage) -> Result<Position, Error> {
message.skip();
let message_version = message.next_int()?;
let mut position = Position {
account: message.next_string()?,
..Default::default()
};
position.contract.contract_id = message.next_int()?;
position.contract.symbol = Symbol::from(message.next_string()?);
position.contract.security_type = SecurityType::from(&message.next_string()?);
position.contract.last_trade_date_or_contract_month = message.next_string()?;
position.contract.strike = message.next_double()?;
position.contract.right = message.next_string()?;
position.contract.multiplier = message.next_string()?;
position.contract.exchange = Exchange::from(message.next_string()?);
position.contract.currency = Currency::from(message.next_string()?);
position.contract.local_symbol = message.next_string()?;
if message_version >= 2 {
position.contract.trading_class = message.next_string()?;
}
position.position = message.next_double()?;
if message_version >= 3 {
position.average_cost = message.next_double()?;
}
Ok(position)
}
pub(crate) fn decode_position_multi(message: &mut ResponseMessage) -> Result<PositionMulti, Error> {
message.skip(); message.skip(); message.skip();
let mut position = PositionMulti {
account: message.next_string()?,
..Default::default()
};
position.contract.contract_id = message.next_int()?;
position.contract.symbol = Symbol::from(message.next_string()?);
position.contract.security_type = SecurityType::from(&message.next_string()?);
position.contract.last_trade_date_or_contract_month = message.next_string()?;
position.contract.strike = message.next_double()?;
position.contract.right = message.next_string()?;
position.contract.multiplier = message.next_string()?;
position.contract.exchange = Exchange::from(message.next_string()?);
position.contract.currency = Currency::from(message.next_string()?);
position.contract.local_symbol = message.next_string()?;
position.contract.trading_class = message.next_string()?;
position.position = message.next_double()?;
position.average_cost = message.next_double()?;
position.model_code = message.next_string()?;
Ok(position)
}
pub(crate) fn decode_family_codes(message: &mut ResponseMessage) -> Result<Vec<FamilyCode>, Error> {
message.skip();
let family_codes_count = message.next_int()?;
if family_codes_count < 1 {
return Ok(Vec::default());
}
let mut family_codes: Vec<FamilyCode> = Vec::with_capacity(family_codes_count as usize);
for _ in 0..family_codes_count {
let family_code = FamilyCode {
account_id: message.next_string()?,
family_code: message.next_string()?,
};
family_codes.push(family_code);
}
Ok(family_codes)
}
pub(crate) fn decode_pnl(server_version: i32, message: &mut ResponseMessage) -> Result<PnL, Error> {
message.skip(); message.skip();
let daily_pnl = message.next_double()?;
let unrealized_pnl = if server_version >= server_versions::UNREALIZED_PNL {
Some(message.next_double()?)
} else {
None
};
let realized_pnl = if server_version >= server_versions::REALIZED_PNL {
Some(message.next_double()?)
} else {
None
};
Ok(PnL {
daily_pnl,
unrealized_pnl,
realized_pnl,
})
}
pub(crate) fn decode_pnl_single(_server_version: i32, message: &mut ResponseMessage) -> Result<PnLSingle, Error> {
message.skip(); message.skip();
let position = message.next_double()?;
let daily_pnl = message.next_double()?;
let unrealized_pnl = message.next_double()?;
let realized_pnl = message.next_double()?;
let value = message.next_double()?;
Ok(PnLSingle {
position,
daily_pnl,
unrealized_pnl,
realized_pnl,
value,
})
}
pub(crate) fn decode_account_summary(_server_version: i32, message: &mut ResponseMessage) -> Result<AccountSummary, Error> {
message.skip(); message.skip(); message.skip();
Ok(AccountSummary {
account: message.next_string()?,
tag: message.next_string()?,
value: message.next_string()?,
currency: message.next_string()?,
})
}
pub(crate) fn decode_account_value(message: &mut ResponseMessage) -> Result<AccountValue, Error> {
message.skip();
let message_version = message.next_int()?;
let mut account_value = AccountValue {
key: message.next_string()?,
value: message.next_string()?,
currency: message.next_string()?,
..Default::default()
};
if message_version >= 2 {
account_value.account = Some(message.next_string()?);
}
Ok(account_value)
}
pub(crate) fn decode_account_portfolio_value(server_version: i32, message: &mut ResponseMessage) -> Result<AccountPortfolioValue, Error> {
message.skip();
let message_version = message.next_int()?;
let mut contract = Contract::default();
if message_version < 7 {
contract.primary_exchange = Exchange::from("");
}
if message_version >= 6 {
contract.contract_id = message.next_int()?;
}
contract.symbol = Symbol::from(message.next_string()?);
contract.security_type = SecurityType::from(&message.next_string()?);
contract.last_trade_date_or_contract_month = message.next_string()?;
contract.strike = message.next_double()?;
contract.right = message.next_string()?;
if message_version >= 7 {
contract.multiplier = message.next_string()?;
contract.primary_exchange = Exchange::from(message.next_string()?);
}
contract.currency = Currency::from(message.next_string()?);
if message_version >= 2 {
contract.local_symbol = message.next_string()?;
}
if message_version >= 8 {
contract.trading_class = message.next_string()?;
}
let mut portfolio_value = AccountPortfolioValue {
contract,
..Default::default()
};
portfolio_value.position = message.next_double()?;
portfolio_value.market_price = message.next_double()?;
portfolio_value.market_value = message.next_double()?;
if message_version >= 3 {
portfolio_value.average_cost = message.next_double()?;
portfolio_value.unrealized_pnl = message.next_double()?;
portfolio_value.realized_pnl = message.next_double()?;
}
if message_version >= 4 {
portfolio_value.account = Some(message.next_string()?);
}
if message_version == 6 && server_version == 39 {
portfolio_value.contract.primary_exchange = Exchange::from(message.next_string()?)
}
Ok(portfolio_value)
}
pub(crate) fn decode_account_update_time(message: &mut ResponseMessage) -> Result<AccountUpdateTime, Error> {
message.skip(); message.skip();
Ok(AccountUpdateTime {
timestamp: message.next_string()?,
})
}
pub(crate) fn decode_server_time_millis(message: &mut ResponseMessage) -> Result<OffsetDateTime, Error> {
message.skip(); let millis = message.next_long()?;
match OffsetDateTime::from_unix_timestamp_nanos(millis as i128 * 1_000_000) {
Ok(date) => Ok(date),
Err(e) => Err(Error::Simple(format!("Error parsing date: {e}"))),
}
}
pub(crate) fn decode_account_multi_value(message: &mut ResponseMessage) -> Result<AccountMultiValue, Error> {
message.skip(); message.skip(); message.skip();
let value = AccountMultiValue {
account: message.next_string()?,
model_code: message.next_string()?,
key: message.next_string()?,
value: message.next_string()?,
currency: message.next_string()?,
};
Ok(value)
}
#[cfg(test)]
mod tests {
use crate::{
accounts::AccountSummaryTags,
contracts::{Currency, Exchange, Symbol},
server_versions,
testdata::responses,
};
#[test]
fn test_decode_positions() {
let mut message = super::ResponseMessage::from("61\03\0DU1234567\076792991\0TSLA\0STK\0\00.0\0\0\0NASDAQ\0USD\0TSLA\0NMS\0500\0196.77\0");
let position = super::decode_position(&mut message).expect("error decoding position");
assert_eq!(position.account, "DU1234567", "position.account");
assert_eq!(position.contract.contract_id, 76792991, "position.contract.contract_id");
assert_eq!(position.contract.symbol, Symbol::from("TSLA"), "position.contract.symbol");
assert_eq!(
position.contract.security_type,
super::SecurityType::Stock,
"position.contract.security_type"
);
assert_eq!(
position.contract.last_trade_date_or_contract_month, "",
"position.contract.last_trade_date_or_contract_month"
);
assert_eq!(position.contract.strike, 0.0, "position.contract.strike");
assert_eq!(position.contract.right, "", "position.contract.right");
assert_eq!(position.contract.multiplier, "", "position.contract.multiplier");
assert_eq!(position.contract.exchange, Exchange::from("NASDAQ"), "position.contract.exchange");
assert_eq!(position.contract.currency, Currency::from("USD"), "position.contract.currency");
assert_eq!(position.contract.local_symbol, "TSLA", "position.contract.local_symbol");
assert_eq!(position.contract.trading_class, "NMS", "position.contract.trading_class");
assert_eq!(position.position, 500.0, "position.position");
assert_eq!(position.average_cost, 196.77, "position.average_cost");
}
#[test]
fn test_decode_position_v1_message() {
let mut message =
super::ResponseMessage::from("61\x001\x00DU123\x00123\x00SYM\x00STK\x00251212\x000.0\x00P\x00MULT\x00EXCH\x00USD\x00LOCSYM\x00100.0\x00");
let result = super::decode_position(&mut message).expect("Failed to decode position");
assert_eq!(result.account, "DU123", "account");
assert_eq!(result.contract.contract_id, 123, "contract.contract_id");
assert_eq!(result.contract.symbol, Symbol::from("SYM"), "contract.symbol");
assert_eq!(
result.contract.security_type,
crate::contracts::SecurityType::Stock,
"contract.security_type"
);
assert_eq!(
result.contract.last_trade_date_or_contract_month, "251212",
"contract.last_trade_date_or_contract_month"
);
assert_eq!(result.contract.strike, 0.0, "contract.strike");
assert_eq!(result.contract.right, "P", "contract.right");
assert_eq!(result.contract.multiplier, "MULT", "contract.multiplier");
assert_eq!(result.contract.exchange, Exchange::from("EXCH"), "contract.exchange");
assert_eq!(result.contract.currency, Currency::from("USD"), "contract.currency");
assert_eq!(result.contract.local_symbol, "LOCSYM", "contract.local_symbol");
assert_eq!(result.position, 100.0, "position");
assert_eq!(result.contract.trading_class, "", "contract.trading_class should be empty for v1");
assert_eq!(result.average_cost, 0.0, "average_cost should be 0.0 for v1");
}
#[test]
fn test_decode_position_v2_message() {
let mut message = super::ResponseMessage::from(
"61\x002\x00DU123\x00123\x00SYM\x00STK\x00251212\x000.0\x00P\x00MULT\x00EXCH\x00USD\x00LOCSYM\x00TRDCLS\x00100.0\x00",
);
let result = super::decode_position(&mut message).expect("Failed to decode position");
assert_eq!(result.account, "DU123", "account");
assert_eq!(result.contract.contract_id, 123, "contract.contract_id");
assert_eq!(result.contract.symbol, Symbol::from("SYM"), "contract.symbol");
assert_eq!(
result.contract.security_type,
crate::contracts::SecurityType::Stock,
"contract.security_type"
);
assert_eq!(
result.contract.last_trade_date_or_contract_month, "251212",
"contract.last_trade_date_or_contract_month"
);
assert_eq!(result.contract.strike, 0.0, "contract.strike");
assert_eq!(result.contract.right, "P", "contract.right");
assert_eq!(result.contract.multiplier, "MULT", "contract.multiplier");
assert_eq!(result.contract.exchange, Exchange::from("EXCH"), "contract.exchange");
assert_eq!(result.contract.currency, Currency::from("USD"), "contract.currency");
assert_eq!(result.contract.local_symbol, "LOCSYM", "contract.local_symbol");
assert_eq!(result.contract.trading_class, "TRDCLS", "contract.trading_class");
assert_eq!(result.position, 100.0, "position");
assert_eq!(result.average_cost, 0.0, "average_cost should be 0.0 for v2");
}
#[test]
fn test_decode_position_multi() {
let mut message =
super::ResponseMessage::from("61\03\06\0DU1234567\076792991\0TSLA\0STK\0\00.0\0\0\0NASDAQ\0USD\0TSLA\0NMS\0500\0196.77\0\0");
let position = super::decode_position_multi(&mut message).expect("error decoding position multi");
assert_eq!(position.account, "DU1234567", "position.account");
assert_eq!(position.contract.contract_id, 76792991, "position.contract.contract_id");
assert_eq!(position.contract.symbol, Symbol::from("TSLA"), "position.contract.symbol");
assert_eq!(
position.contract.security_type,
super::SecurityType::Stock,
"position.contract.security_type"
);
assert_eq!(
position.contract.last_trade_date_or_contract_month, "",
"position.contract.last_trade_date_or_contract_month"
);
assert_eq!(position.contract.strike, 0.0, "position.contract.strike");
assert_eq!(position.contract.right, "", "position.contract.right");
assert_eq!(position.contract.multiplier, "", "position.contract.multiplier");
assert_eq!(position.contract.exchange, Exchange::from("NASDAQ"), "position.contract.exchange");
assert_eq!(position.contract.currency, Currency::from("USD"), "position.contract.currency");
assert_eq!(position.contract.local_symbol, "TSLA", "position.contract.local_symbol");
assert_eq!(position.contract.trading_class, "NMS", "position.contract.trading_class");
assert_eq!(position.position, 500.0, "position.position");
assert_eq!(position.average_cost, 196.77, "position.average_cost");
assert_eq!(position.model_code, "", "position.model_code");
}
#[test]
fn test_decode_family_codes() {
let mut message = super::ResponseMessage::from("78\01\0*\0\0");
let family_codes = super::decode_family_codes(&mut message).expect("error decoding family codes");
assert_eq!(family_codes[0].account_id, "*", "family_codes.account_id");
assert_eq!(family_codes[0].family_code, "", "family_codes.family_code");
}
#[test]
fn test_decode_family_codes_empty_list() {
let mut message = super::ResponseMessage::from("78\x000\x00");
let result = super::decode_family_codes(&mut message).expect("Failed to decode family codes");
assert!(result.is_empty(), "Result should be an empty list");
}
#[test]
fn test_decode_family_codes_multiple_codes() {
let mut message = super::ResponseMessage::from("78\x002\x00ACC1\x00FC1\x00ACC2\x00FC2\x00");
let result = super::decode_family_codes(&mut message).expect("Failed to decode family codes");
assert_eq!(result.len(), 2, "Should have 2 family codes");
assert_eq!(result[0].account_id, "ACC1", "First account_id");
assert_eq!(result[0].family_code, "FC1", "First family_code");
assert_eq!(result[1].account_id, "ACC2", "Second account_id");
assert_eq!(result[1].family_code, "FC2", "Second family_code");
}
#[test]
fn test_decode_pnl() {
let mut message = super::ResponseMessage::from("94\09000\00.1\00.2\00.3\0");
let pnl = super::decode_pnl(server_versions::REALIZED_PNL, &mut message).expect("error decoding pnl");
assert_eq!(pnl.daily_pnl, 0.10, "pnl.daily_pnl");
assert_eq!(pnl.unrealized_pnl, Some(0.20), "pnl.unrealized_pnl");
assert_eq!(pnl.realized_pnl, Some(0.30), "pnl.realized_pnl");
let mut message = super::ResponseMessage::from("94\09000\00.1\00.2\00.3\0");
let pnl = super::decode_pnl(server_versions::UNREALIZED_PNL, &mut message).expect("error decoding pnl");
assert_eq!(pnl.daily_pnl, 0.10, "pnl.daily_pnl");
assert_eq!(pnl.unrealized_pnl, Some(0.20), "pnl.unrealized_pnl");
assert_eq!(pnl.realized_pnl, None, "pnl.realized_pnl");
let mut message = super::ResponseMessage::from("94\09000\00.1\00.2\00.3\0");
let pnl = super::decode_pnl(server_versions::PNL, &mut message).expect("error decoding pnl");
assert_eq!(pnl.daily_pnl, 0.10, "pnl.daily_pnl");
assert_eq!(pnl.unrealized_pnl, None, "pnl.unrealized_pnl");
assert_eq!(pnl.realized_pnl, None, "pnl.realized_pnl");
}
#[test]
fn test_decode_pnl_single() {
let mut message = super::ResponseMessage::from("95\09000\0100.0\00.1\00.2\00.3\00.4\0");
let pnl_single = super::decode_pnl_single(server_versions::REALIZED_PNL, &mut message).expect("error decoding pnl single");
assert_eq!(pnl_single.position, 100., "pnl_single.position");
assert_eq!(pnl_single.daily_pnl, 0.10, "pnl_single.daily_pnl");
assert_eq!(pnl_single.unrealized_pnl, 0.20, "pnl_single.unrealized_pnl");
assert_eq!(pnl_single.realized_pnl, 0.30, "pnl_single.realized_pnl");
assert_eq!(pnl_single.value, 0.40, "pnl_single.value");
}
#[test]
fn test_decode_account_summary() {
let mut message = super::ResponseMessage::from("94\01\09000\0DU1234567\0AccountType\0FA\0\0");
let account_summary = super::decode_account_summary(server_versions::REALIZED_PNL, &mut message).expect("error decoding pnl");
assert_eq!(account_summary.account, "DU1234567", "account_summary.account");
assert_eq!(account_summary.tag, AccountSummaryTags::ACCOUNT_TYPE, "account_summary.tag");
assert_eq!(account_summary.value, "FA", "account_summary.value");
assert_eq!(account_summary.currency, "", "account_summary.currency");
}
#[test]
fn test_decode_account_multi_value() {
let mut message = super::ResponseMessage::from_simple(responses::ACCOUNT_UPDATE_MULTI_CURRENCY);
let value = super::decode_account_multi_value(&mut message).expect("error decoding account multi value");
assert_eq!(value.account, "DU1234567", "value.account");
assert_eq!(value.model_code, "", "value.model_code");
assert_eq!(value.key, "Currency", "value.key");
assert_eq!(value.value, "USD", "value.value");
assert_eq!(value.currency, "USD", "value.currency");
}
#[test]
fn test_decode_account_portfolio_value_version_matrix() {
struct TestCase {
name: &'static str,
message_version: i32,
server_version: i32,
message_string: String,
expected_contract_id: i32,
expected_symbol: &'static str,
expected_sec_type: crate::contracts::SecurityType,
expected_expiry: &'static str,
expected_strike: f64,
expected_right: &'static str,
expected_currency: &'static str,
expected_multiplier: &'static str,
expected_primary_exchange: &'static str,
expected_local_symbol: &'static str,
expected_trading_class: &'static str,
expected_position: f64,
expected_market_price: f64,
expected_market_value: f64,
expected_average_cost: Option<f64>,
expected_unrealized_pnl: Option<f64>,
expected_realized_pnl: Option<f64>,
}
#[allow(clippy::too_many_arguments)]
fn construct_portfolio_message(
msg_ver: i32,
sv_ver: i32,
con_id: &str,
sym: &str,
sec_t: &str,
exp: &str,
strike: &str,
right: &str,
mult: &str,
prim_exch: &str,
curr: &str,
local_sym: &str,
trading_class: &str,
pos: &str,
m_price: &str,
m_val: &str,
avg_c: &str,
un_pnl: &str,
r_pnl: &str,
acc_name: &str,
prim_exch_override_for_sv39: Option<&str>,
) -> String {
let msg_ver_str = msg_ver.to_string();
let mut fields = vec!["9", &msg_ver_str];
if msg_ver >= 6 {
fields.push(con_id);
}
fields.push(sym);
fields.push(sec_t);
fields.push(exp);
fields.push(strike);
fields.push(right);
if msg_ver >= 7 {
fields.push(mult);
}
if msg_ver >= 7 {
fields.push(prim_exch);
}
fields.push(curr);
if msg_ver >= 2 {
fields.push(local_sym);
}
if msg_ver >= 8 {
fields.push(trading_class);
}
fields.push(pos);
fields.push(m_price);
fields.push(m_val);
if msg_ver >= 3 {
fields.push(avg_c);
}
if msg_ver >= 3 {
fields.push(un_pnl);
}
if msg_ver >= 3 {
fields.push(r_pnl);
}
if msg_ver >= 4 {
fields.push(acc_name);
}
if msg_ver >= 6 && sv_ver == 39 {
if let Some(pe_override) = prim_exch_override_for_sv39 {
fields.push(pe_override);
}
}
fields.join("\x00") + "\x00"
}
let tests = [
TestCase {
name: "mv1_sv_any_no_localsymbol_no_tradingclass",
message_version: 1,
server_version: server_versions::SIZE_RULES,
message_string: construct_portfolio_message(
1,
server_versions::SIZE_RULES,
"",
"SYM",
"STK",
"251212",
"0.0",
"P",
"",
"",
"USD",
"",
"",
"100.0",
"10.0",
"1000.0",
"",
"",
"",
"",
None,
),
expected_contract_id: 0,
expected_symbol: "SYM",
expected_sec_type: crate::contracts::SecurityType::Stock,
expected_expiry: "251212",
expected_strike: 0.0,
expected_right: "P",
expected_currency: "USD",
expected_multiplier: "",
expected_primary_exchange: "",
expected_local_symbol: "",
expected_trading_class: "",
expected_position: 100.0,
expected_market_price: 10.0,
expected_market_value: 1000.0,
expected_average_cost: None,
expected_unrealized_pnl: None,
expected_realized_pnl: None,
},
TestCase {
name: "mv2_sv_any_has_local_symbol",
message_version: 2,
server_version: server_versions::SIZE_RULES,
message_string: construct_portfolio_message(
2,
server_versions::SIZE_RULES,
"",
"SYM",
"STK",
"251212",
"0.0",
"P",
"",
"",
"USD",
"LOCSYM",
"",
"100.0",
"10.0",
"1000.0",
"",
"",
"",
"",
None,
),
expected_contract_id: 0,
expected_symbol: "SYM",
expected_sec_type: crate::contracts::SecurityType::Stock,
expected_expiry: "251212",
expected_strike: 0.0,
expected_right: "P",
expected_currency: "USD",
expected_multiplier: "",
expected_primary_exchange: "",
expected_local_symbol: "LOCSYM",
expected_trading_class: "",
expected_position: 100.0,
expected_market_price: 10.0,
expected_market_value: 1000.0,
expected_average_cost: None,
expected_unrealized_pnl: None,
expected_realized_pnl: None,
},
TestCase {
name: "mv5_sv_any_has_avgcost_pnl_accname",
message_version: 5,
server_version: server_versions::SIZE_RULES,
message_string: construct_portfolio_message(
5,
server_versions::SIZE_RULES,
"",
"SYM",
"STK",
"251212",
"0.0",
"P",
"",
"",
"USD",
"LOCSYM",
"",
"100.0",
"10.0",
"1000.0",
"9.0",
"100.0",
"50.0",
"ACC1",
None,
),
expected_contract_id: 0,
expected_symbol: "SYM",
expected_sec_type: crate::contracts::SecurityType::Stock,
expected_expiry: "251212",
expected_strike: 0.0,
expected_right: "P",
expected_currency: "USD",
expected_multiplier: "",
expected_primary_exchange: "",
expected_local_symbol: "LOCSYM",
expected_trading_class: "",
expected_position: 100.0,
expected_market_price: 10.0,
expected_market_value: 1000.0,
expected_average_cost: Some(9.0),
expected_unrealized_pnl: Some(100.0),
expected_realized_pnl: Some(50.0),
},
TestCase {
name: "mv6_sv_not39_has_conid",
message_version: 6,
server_version: server_versions::SIZE_RULES,
message_string: construct_portfolio_message(
6,
server_versions::SIZE_RULES,
"123",
"SYM",
"STK",
"251212",
"0.0",
"P",
"",
"",
"USD",
"LOCSYM",
"",
"100.0",
"10.0",
"1000.0",
"9.0",
"100.0",
"50.0",
"ACC1",
None,
),
expected_contract_id: 123,
expected_symbol: "SYM",
expected_sec_type: crate::contracts::SecurityType::Stock,
expected_expiry: "251212",
expected_strike: 0.0,
expected_right: "P",
expected_currency: "USD",
expected_multiplier: "",
expected_primary_exchange: "",
expected_local_symbol: "LOCSYM",
expected_trading_class: "",
expected_position: 100.0,
expected_market_price: 10.0,
expected_market_value: 1000.0,
expected_average_cost: Some(9.0),
expected_unrealized_pnl: Some(100.0),
expected_realized_pnl: Some(50.0),
},
TestCase {
name: "mv6_sv39_has_conid_prim_exch_override",
message_version: 6,
server_version: 39,
message_string: construct_portfolio_message(
6,
39,
"123",
"SYM",
"STK",
"251212",
"0.0",
"P",
"",
"",
"USD",
"LOCSYM",
"",
"100.0",
"10.0",
"1000.0",
"9.0",
"100.0",
"50.0",
"ACC1",
Some("OVERRIDE_EXCH"),
),
expected_contract_id: 123,
expected_symbol: "SYM",
expected_sec_type: crate::contracts::SecurityType::Stock,
expected_expiry: "251212",
expected_strike: 0.0,
expected_right: "P",
expected_currency: "USD",
expected_multiplier: "",
expected_primary_exchange: "OVERRIDE_EXCH",
expected_local_symbol: "LOCSYM",
expected_trading_class: "",
expected_position: 100.0,
expected_market_price: 10.0,
expected_market_value: 1000.0,
expected_average_cost: Some(9.0),
expected_unrealized_pnl: Some(100.0),
expected_realized_pnl: Some(50.0),
},
TestCase {
name: "mv7_sv_any_has_mult_prim_exch",
message_version: 7,
server_version: server_versions::SIZE_RULES,
message_string: construct_portfolio_message(
7,
server_versions::SIZE_RULES,
"123",
"SYM",
"STK",
"251212",
"0.0",
"P",
"MULT1",
"PRIMEXCH1",
"USD",
"LOCSYM",
"",
"100.0",
"10.0",
"1000.0",
"9.0",
"100.0",
"50.0",
"ACC1",
None,
),
expected_contract_id: 123,
expected_symbol: "SYM",
expected_sec_type: crate::contracts::SecurityType::Stock,
expected_expiry: "251212",
expected_strike: 0.0,
expected_right: "P",
expected_currency: "USD",
expected_multiplier: "MULT1",
expected_primary_exchange: "PRIMEXCH1",
expected_local_symbol: "LOCSYM",
expected_trading_class: "",
expected_position: 100.0,
expected_market_price: 10.0,
expected_market_value: 1000.0,
expected_average_cost: Some(9.0),
expected_unrealized_pnl: Some(100.0),
expected_realized_pnl: Some(50.0),
},
TestCase {
name: "mv7_sv39_has_mult_prim_exch_with_override_field",
message_version: 7,
server_version: 39,
message_string: construct_portfolio_message(
7,
39,
"123",
"SYM",
"STK",
"251212",
"0.0",
"P",
"MULT1",
"PRIMEXCH1",
"USD",
"LOCSYM",
"",
"100.0",
"10.0",
"1000.0",
"9.0",
"100.0",
"50.0",
"ACC1",
Some("OVERRIDE_EXCH_V7_SV39"),
),
expected_contract_id: 123,
expected_symbol: "SYM",
expected_sec_type: crate::contracts::SecurityType::Stock,
expected_expiry: "251212",
expected_strike: 0.0,
expected_right: "P",
expected_currency: "USD",
expected_multiplier: "MULT1",
expected_primary_exchange: "PRIMEXCH1",
expected_local_symbol: "LOCSYM",
expected_trading_class: "",
expected_position: 100.0,
expected_market_price: 10.0,
expected_market_value: 1000.0,
expected_average_cost: Some(9.0),
expected_unrealized_pnl: Some(100.0),
expected_realized_pnl: Some(50.0),
},
TestCase {
name: "mv8_sv_any_has_trading_class",
message_version: 8,
server_version: server_versions::SIZE_RULES,
message_string: construct_portfolio_message(
8,
server_versions::SIZE_RULES,
"123",
"SYM",
"STK",
"251212",
"0.0",
"P",
"MULT1",
"PRIMEXCH1",
"USD",
"LOCSYM",
"TRDCLS1",
"100.0",
"10.0",
"1000.0",
"9.0",
"100.0",
"50.0",
"ACC1",
None,
),
expected_contract_id: 123,
expected_symbol: "SYM",
expected_sec_type: crate::contracts::SecurityType::Stock,
expected_expiry: "251212",
expected_strike: 0.0,
expected_right: "P",
expected_currency: "USD",
expected_multiplier: "MULT1",
expected_primary_exchange: "PRIMEXCH1",
expected_local_symbol: "LOCSYM",
expected_trading_class: "TRDCLS1",
expected_position: 100.0,
expected_market_price: 10.0,
expected_market_value: 1000.0,
expected_average_cost: Some(9.0),
expected_unrealized_pnl: Some(100.0),
expected_realized_pnl: Some(50.0),
},
TestCase {
name: "mv8_sv39_has_trading_class_with_override_field",
message_version: 8,
server_version: 39,
message_string: construct_portfolio_message(
8,
39,
"123",
"SYM",
"STK",
"251212",
"0.0",
"P",
"MULT1",
"PRIMEXCH1",
"USD",
"LOCSYM",
"TRDCLS1",
"100.0",
"10.0",
"1000.0",
"9.0",
"100.0",
"50.0",
"ACC1",
Some("OVERRIDE_EXCH_V8_SV39"),
),
expected_contract_id: 123,
expected_symbol: "SYM",
expected_sec_type: crate::contracts::SecurityType::Stock,
expected_expiry: "251212",
expected_strike: 0.0,
expected_right: "P",
expected_currency: "USD",
expected_multiplier: "MULT1",
expected_primary_exchange: "PRIMEXCH1",
expected_local_symbol: "LOCSYM",
expected_trading_class: "TRDCLS1",
expected_position: 100.0,
expected_market_price: 10.0,
expected_market_value: 1000.0,
expected_average_cost: Some(9.0),
expected_unrealized_pnl: Some(100.0),
expected_realized_pnl: Some(50.0),
},
];
for tc in tests.iter() {
let mut message = super::ResponseMessage::from(tc.message_string.as_str());
let result = super::decode_account_portfolio_value(tc.server_version, &mut message)
.unwrap_or_else(|e| panic!("Test case '{}' failed decoding: {:?}", tc.name, e));
assert_eq!(result.contract.contract_id, tc.expected_contract_id, "Case: {} - contract_id", tc.name);
assert_eq!(result.contract.symbol, Symbol::from(tc.expected_symbol), "Case: {} - symbol", tc.name);
assert_eq!(result.contract.security_type, tc.expected_sec_type, "Case: {} - sec_type", tc.name);
assert_eq!(
result.contract.last_trade_date_or_contract_month, tc.expected_expiry,
"Case: {} - expiry",
tc.name
);
assert_eq!(result.contract.strike, tc.expected_strike, "Case: {} - strike", tc.name);
assert_eq!(result.contract.right, tc.expected_right, "Case: {} - right", tc.name);
assert_eq!(result.contract.multiplier, tc.expected_multiplier, "Case: {} - multiplier", tc.name);
if tc.message_version >= 6 && tc.server_version == 39 {
assert_eq!(
result.contract.primary_exchange,
Exchange::from(tc.expected_primary_exchange),
"Case: {} - primary_exchange (sv39 override)",
tc.name
);
} else {
assert_eq!(
result.contract.primary_exchange,
Exchange::from(tc.expected_primary_exchange),
"Case: {} - primary_exchange",
tc.name
);
}
assert_eq!(
result.contract.currency,
Currency::from(tc.expected_currency),
"Case: {} - currency",
tc.name
);
assert_eq!(result.contract.local_symbol, tc.expected_local_symbol, "Case: {} - local_symbol", tc.name);
assert_eq!(
result.contract.trading_class, tc.expected_trading_class,
"Case: {} - trading_class",
tc.name
);
assert_eq!(result.position, tc.expected_position, "Case: {} - position", tc.name);
assert_eq!(result.market_price, tc.expected_market_price, "Case: {} - market_price", tc.name);
assert_eq!(result.market_value, tc.expected_market_value, "Case: {} - market_value", tc.name);
assert_eq!(
result.average_cost,
tc.expected_average_cost.unwrap_or(0.0),
"Case: {} - average_cost",
tc.name
);
assert_eq!(
result.unrealized_pnl,
tc.expected_unrealized_pnl.unwrap_or(0.0),
"Case: {} - unrealized_pnl",
tc.name
);
assert_eq!(
result.realized_pnl,
tc.expected_realized_pnl.unwrap_or(0.0),
"Case: {} - realized_pnl",
tc.name
);
}
}
#[test]
fn test_decode_account_value_versions() {
struct TestCase {
name: &'static str,
message_fields: Vec<&'static str>,
expected_key: &'static str,
expected_value: &'static str,
expected_currency: &'static str,
expected_account_name: Option<&'static str>,
}
let tests = [
TestCase {
name: "v1_no_account_name",
message_fields: vec!["6", "1", "CashBalance", "1000.00", "USD"],
expected_key: "CashBalance",
expected_value: "1000.00",
expected_currency: "USD",
expected_account_name: None,
},
TestCase {
name: "v2_with_account_name",
message_fields: vec!["6", "2", "EquityWithLoanValue", "1200.00", "CAD", "ACC123"],
expected_key: "EquityWithLoanValue",
expected_value: "1200.00",
expected_currency: "CAD",
expected_account_name: Some("ACC123"),
},
];
for tc in tests.iter() {
let message_string = tc.message_fields.join("\x00") + "\x00";
let mut message = super::ResponseMessage::from(message_string.as_str());
let result = super::decode_account_value(&mut message).unwrap_or_else(|e| panic!("Test case '{}' failed: {:?}", tc.name, e));
assert_eq!(result.key, tc.expected_key, "Case: {} - key", tc.name);
assert_eq!(result.value, tc.expected_value, "Case: {} - value", tc.name);
assert_eq!(result.currency, tc.expected_currency, "Case: {} - currency", tc.name);
assert_eq!(result.account.as_deref(), tc.expected_account_name, "Case: {} - account_name", tc.name);
}
}
#[test]
fn test_decode_account_update_time_success() {
let mut message = super::ResponseMessage::from("8\x001\x0012:34:56\x00");
let result = super::decode_account_update_time(&mut message);
assert!(result.is_ok(), "Decoding failed: {:?}", result.err());
assert_eq!(result.unwrap().timestamp, "12:34:56", "Timestamp mismatch");
}
}