Skip to content

Commit

Permalink
Implements account summary (#123)
Browse files Browse the repository at this point in the history
  • Loading branch information
wboayue authored Oct 13, 2024
1 parent 81a383e commit 45df44b
Show file tree
Hide file tree
Showing 19 changed files with 305 additions and 84 deletions.
18 changes: 18 additions & 0 deletions examples/account_summary.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use ibapi::accounts::{AccountSummaryTags, AccountUpdate};
use ibapi::Client;

fn main() {
let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed");

let group = "All";

let subscription = client
.account_summary(group, AccountSummaryTags::ALL)
.expect("error requesting account summary");
for update in &subscription {
match update {
AccountUpdate::Summary(summary) => println!("{summary:?}"),
AccountUpdate::End => subscription.cancel().expect("cancel failed"),
}
}
}
7 changes: 4 additions & 3 deletions examples/pnl_single.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use ibapi::Client;
fn main() {
env_logger::init();

let matches = Command::new("pnl")
let matches = Command::new("pnl_single")
.about("Gets realtime profit and loss updates of single contract")
.arg(arg!(--connection_url <VALUE>).default_value("127.0.0.1:4002"))
.arg(arg!(--account <ACCOUNT>).required(true))
Expand All @@ -22,14 +22,15 @@ fn main() {

// Get next item non-blocking
if let Some(pnl) = subscription.try_next() {
println!("non-blocking PnL: {:?}", pnl);
println!("non-blocking PnL single: {:?}", pnl);
}

// Consume items blocking for next
while let Some(pnl) = subscription.next() {
println!("PnL: {:?}", pnl);
println!("PnL single: {:?}", pnl);

// After processing items subscription could be cancelled.
subscription.cancel();
break;
}
}
6 changes: 5 additions & 1 deletion examples/positions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@ fn main() {
while let Some(position_update) = positions.next() {
match position_update {
PositionUpdate::Position(position) => {
println!("{:4} {:4} @ {}", position.position, position.contract.symbol, position.average_cost)
println!(
"{:4} {:4} {} @ {}",
position.position, position.contract.symbol, position.contract.contract_id, position.average_cost
)
}
PositionUpdate::PositionEnd => {
println!("PositionEnd");
// all positions received. could continue listening for new additions or cancel.
positions.cancel();
break;
}
}
}
Expand Down
137 changes: 130 additions & 7 deletions src/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,113 @@ use crate::{server_versions, Client, Error};
mod decoders;
mod encoders;

#[derive(Debug, Default)]
/// Account information as it appears in the TWS’ Account Summary Window
pub struct AccountSummary {
/// The account identifier.
pub account: String,
/// The account’s attribute.
pub tag: String,
/// The account’s attribute’s value.
pub value: String,
/// The currency in which the value is expressed.
pub currency: String,
}

pub struct AccountSummaryTags {}

impl AccountSummaryTags {
pub const ACCOUNT_TYPE: &str = "AccountType";
pub const NET_LIQUIDATION: &str = "NetLiquidation";
pub const TOTAL_CASH_VALUE: &str = "TotalCashValue";
pub const SETTLED_CASH: &str = "SettledCash";
pub const ACCRUED_CASH: &str = "AccruedCash";
pub const BUYING_POWER: &str = "BuyingPower";
pub const EQUITY_WITH_LOAN_VALUE: &str = "EquityWithLoanValue";
pub const PREVIOUS_DAY_EQUITY_WITH_LOAN_VALUE: &str = "PreviousDayEquityWithLoanValue";
pub const GROSS_POSITION_VALUE: &str = "GrossPositionValue";
pub const REQ_T_EQUITY: &str = "ReqTEquity";
pub const REQ_T_MARGIN: &str = "ReqTMargin";
pub const SMA: &str = "SMA";
pub const INIT_MARGIN_REQ: &str = "InitMarginReq";
pub const MAINT_MARGIN_REQ: &str = "MaintMarginReq";
pub const AVAILABLE_FUNDS: &str = "AvailableFunds";
pub const EXCESS_LIQUIDITY: &str = "ExcessLiquidity";
pub const CUSHION: &str = "Cushion";
pub const FULL_INIT_MARGIN_REQ: &str = "FullInitMarginReq";
pub const FULL_MAINT_MARGIN_REQ: &str = "FullMaintMarginReq";
pub const FULL_AVAILABLE_FUNDS: &str = "FullAvailableFunds";
pub const FULL_EXCESS_LIQUIDITY: &str = "FullExcessLiquidity";
pub const LOOK_AHEAD_NEXT_CHANGE: &str = "LookAheadNextChange";
pub const LOOK_AHEAD_INIT_MARGIN_REQ: &str = "LookAheadInitMarginReq";
pub const LOOK_AHEAD_MAINT_MARGIN_REQ: &str = "LookAheadMaintMarginReq";
pub const LOOK_AHEAD_AVAILABLE_FUNDS: &str = "LookAheadAvailableFunds";
pub const LOOK_AHEAD_EXCESS_LIQUIDITY: &str = "LookAheadExcessLiquidity";
pub const HIGHEST_SEVERITY: &str = "HighestSeverity";
pub const DAY_TRADES_REMAINING: &str = "DayTradesRemaining";
pub const LEVERAGE: &str = "Leverage";

pub const ALL: &[&str] = &[
Self::ACCOUNT_TYPE,
Self::NET_LIQUIDATION,
Self::TOTAL_CASH_VALUE,
Self::SETTLED_CASH,
Self::ACCRUED_CASH,
Self::BUYING_POWER,
Self::EQUITY_WITH_LOAN_VALUE,
Self::PREVIOUS_DAY_EQUITY_WITH_LOAN_VALUE,
Self::GROSS_POSITION_VALUE,
Self::REQ_T_EQUITY,
Self::REQ_T_MARGIN,
Self::SMA,
Self::INIT_MARGIN_REQ,
Self::MAINT_MARGIN_REQ,
Self::AVAILABLE_FUNDS,
Self::EXCESS_LIQUIDITY,
Self::CUSHION,
Self::FULL_INIT_MARGIN_REQ,
Self::FULL_MAINT_MARGIN_REQ,
Self::FULL_AVAILABLE_FUNDS,
Self::FULL_EXCESS_LIQUIDITY,
Self::LOOK_AHEAD_NEXT_CHANGE,
Self::LOOK_AHEAD_INIT_MARGIN_REQ,
Self::LOOK_AHEAD_MAINT_MARGIN_REQ,
Self::LOOK_AHEAD_AVAILABLE_FUNDS,
Self::LOOK_AHEAD_EXCESS_LIQUIDITY,
Self::HIGHEST_SEVERITY,
Self::DAY_TRADES_REMAINING,
Self::LEVERAGE,
];
}

#[derive(Debug)]
pub enum AccountUpdate {
Summary(AccountSummary),
End,
}

impl From<AccountSummary> for AccountUpdate {
fn from(val: AccountSummary) -> Self {
AccountUpdate::Summary(val)
}
}

impl Subscribable<AccountUpdate> for AccountUpdate {
const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[IncomingMessages::AccountSummary, IncomingMessages::AccountSummaryEnd];

fn decode(server_version: i32, message: &mut ResponseMessage) -> Result<Self, Error> {
match message.message_type() {
IncomingMessages::AccountSummary => Ok(AccountUpdate::Summary(decoders::decode_account_summary(server_version, message)?)),
IncomingMessages::AccountSummaryEnd => Ok(AccountUpdate::End),
message => Err(Error::Simple(format!("unexpected message: {message:?}"))),
}
}

fn cancel_message(_server_version: i32, _request_id: Option<i32>) -> Result<RequestMessage, Error> {
encoders::encode_cancel_positions()
}
}

// Realtime PnL update for account.
#[derive(Debug, Default)]
pub struct PnL {
Expand Down Expand Up @@ -51,9 +158,9 @@ pub struct PnLSingle {
/// DailyPnL for the position
pub daily_pnl: f64,
/// UnrealizedPnL total unrealized PnL for the position (since inception) updating in real time.
pub unrealized_pnl: Option<f64>,
pub unrealized_pnl: f64,
/// Realized PnL for the position
pub realized_pnl: Option<f64>,
pub realized_pnl: f64,
/// Current market value of the position
pub value: f64,
}
Expand Down Expand Up @@ -176,7 +283,7 @@ pub(crate) fn positions(client: &Client) -> Result<Subscription<PositionUpdate>,
Ok(Subscription {
client,
request_id: None,
responses,
subscription: responses,
phantom: PhantomData,
})
}
Expand All @@ -198,7 +305,7 @@ pub(crate) fn positions_multi<'a>(
Ok(Subscription {
client,
request_id: Some(request_id),
responses,
subscription: responses,
phantom: PhantomData,
})
}
Expand Down Expand Up @@ -234,7 +341,7 @@ pub(crate) fn pnl<'a>(client: &'a Client, account: &str, model_code: Option<&str
Ok(Subscription {
client,
request_id: Some(request_id),
responses,
subscription: responses,
phantom: PhantomData,
})
}
Expand All @@ -252,7 +359,7 @@ pub(crate) fn pnl_single<'a>(
contract_id: i32,
model_code: Option<&str>,
) -> Result<Subscription<'a, PnLSingle>, Error> {
client.check_server_version(server_versions::PNL, "It does not support PnL requests.")?;
client.check_server_version(server_versions::REALIZED_PNL, "It does not support PnL requests.")?;

let request_id = client.next_request_id();

Expand All @@ -262,7 +369,23 @@ pub(crate) fn pnl_single<'a>(
Ok(Subscription {
client,
request_id: Some(request_id),
responses,
subscription: responses,
phantom: PhantomData,
})
}

pub fn account_summary<'a>(client: &'a Client, group: &str, tags: &[&str]) -> Result<Subscription<'a, AccountUpdate>, Error> {
client.check_server_version(server_versions::ACCOUNT_SUMMARY, "It does not support account summary requests.")?;

let request_id = client.next_request_id();

let request = encoders::encode_request_account_summary(request_id, group, tags)?;
let responses = client.send_request(request_id, request)?;

Ok(Subscription {
client,
request_id: Some(request_id),
subscription: responses,
phantom: PhantomData,
})
}
Expand Down
30 changes: 18 additions & 12 deletions src/accounts/decoders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::contracts::SecurityType;
use crate::messages::ResponseMessage;
use crate::{server_versions, Error};

use super::{FamilyCode, PnL, PnLSingle, Position, PositionMulti};
use super::{AccountSummary, FamilyCode, PnL, PnLSingle, Position, PositionMulti};

pub(crate) fn decode_position(message: &mut ResponseMessage) -> Result<Position, Error> {
message.skip(); // message type
Expand Down Expand Up @@ -112,21 +112,14 @@ pub(crate) fn decode_pnl(server_version: i32, message: &mut ResponseMessage) ->
})
}

pub(crate) fn decode_pnl_single(server_version: i32, message: &mut ResponseMessage) -> Result<PnLSingle, Error> {
pub(crate) fn decode_pnl_single(_server_version: i32, message: &mut ResponseMessage) -> Result<PnLSingle, Error> {
message.skip(); // message type
message.skip(); // request id

let position = message.next_double()?;
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
};
let unrealized_pnl = message.next_double()?;
let realized_pnl = message.next_double()?;
let value = message.next_double()?;

Ok(PnLSingle {
Expand All @@ -138,5 +131,18 @@ pub(crate) fn decode_pnl_single(server_version: i32, message: &mut ResponseMessa
})
}

pub(crate) fn decode_account_summary(_server_version: i32, message: &mut ResponseMessage) -> Result<AccountSummary, Error> {
message.skip(); // message type
message.skip(); // version
message.skip(); // request id

Ok(AccountSummary {
account: message.next_string()?,
tag: message.next_string()?,
value: message.next_string()?,
currency: message.next_string()?,
})
}

#[cfg(test)]
mod tests;
36 changes: 17 additions & 19 deletions src/accounts/decoders/tests.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::server_versions;
use crate::{accounts::AccountSummaryTags, server_versions};

#[test]
fn test_decode_positions() {
Expand Down Expand Up @@ -97,27 +97,25 @@ fn test_decode_pnl() {

#[test]
fn test_decode_pnl_single() {
let mut message = super::ResponseMessage::from("94\09000\00.1\00.2\00.3\0");

let pnl = super::decode_pnl_single(server_versions::REALIZED_PNL, &mut message).expect("error decoding pnl");
let mut message = super::ResponseMessage::from("95\09000\0100.0\00.1\00.2\00.3\00.4\0");

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 pnl_single = super::decode_pnl_single(server_versions::REALIZED_PNL, &mut message).expect("error decoding pnl single");

let mut message = super::ResponseMessage::from("94\09000\00.1\00.2\00.3\0");

let pnl = super::decode_pnl_single(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");
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");
}

let mut message = super::ResponseMessage::from("94\09000\00.1\00.2\00.3\0");
#[test]
fn test_decode_account_summary() {
let mut message = super::ResponseMessage::from("94\01\09000\0DU1234567\0AccountType\0FA\0");

let pnl = super::decode_pnl_single(server_versions::PNL, &mut message).expect("error decoding pnl");
let account_summary = super::decode_account_summary(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, None, "pnl.unrealized_pnl");
assert_eq!(pnl.realized_pnl, None, "pnl.realized_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");
}
22 changes: 15 additions & 7 deletions src/accounts/encoders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,7 @@ pub(crate) fn encode_request_pnl_single(request_id: i32, account: &str, contract
message.push_field(&OutgoingMessages::RequestPnLSingle);
message.push_field(&request_id);
message.push_field(&account);

if let Some(model_code) = model_code {
message.push_field(&model_code);
} else {
message.push_field(&"");
}

message.push_field(&model_code);
message.push_field(&contract_id);

Ok(message)
Expand All @@ -77,6 +71,20 @@ pub(crate) fn encode_cancel_pnl_single(request_id: i32) -> Result<RequestMessage
encode_simple_with_request_id(OutgoingMessages::CancelPnLSingle, request_id)
}

pub(crate) fn encode_request_account_summary(request_id: i32, group: &str, tags: &[&str]) -> Result<RequestMessage, Error> {
const VERSION: i32 = 1;

let mut message = RequestMessage::new();

message.push_field(&OutgoingMessages::RequestAccountSummary);
message.push_field(&VERSION);
message.push_field(&request_id);
message.push_field(&group);
message.push_field(&tags.join(","));

Ok(message)
}

fn encode_simple(message_type: OutgoingMessages, version: i32) -> Result<RequestMessage, Error> {
let mut message = RequestMessage::new();

Expand Down
Loading

0 comments on commit 45df44b

Please sign in to comment.