Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements request positions multi #121

Merged
merged 3 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions examples/positions.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use ibapi::{accounts::PositionResponse, Client};
use ibapi::{accounts::PositionUpdate, Client};

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

let mut positions = client.positions().expect("request failed");
while let Some(position_update) = positions.next() {
match position_update {
PositionResponse::Position(position) => {
PositionUpdate::Position(position) => {
println!("{:4} {:4} @ {}", position.position, position.contract.symbol, position.average_cost)
}
PositionResponse::PositionEnd => {
PositionUpdate::PositionEnd => {
println!("PositionEnd");
// all positions received. could continue listening for new additions or cancel.
positions.cancel();
Expand Down
14 changes: 14 additions & 0 deletions examples/positions_multi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use std::env;

use ibapi::Client;

pub fn main() {
let account = env::var("IBKR_ACCOUNT").expect("Please set IBKR_ACCOUNT environment variable to an account ID");

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

let subscription = client.positions_multi(Some(&account), None).expect("error requesting positions by model");
for position in subscription {
println!("{position:?}")
}
}
104 changes: 81 additions & 23 deletions src/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,8 @@ impl Subscribable<PnL> for PnL {
}

fn cancel_message(_server_version: i32, request_id: Option<i32>) -> Result<RequestMessage, Error> {
if let Some(request_id) = request_id {
encoders::encode_cancel_pnl(request_id)
} else {
Err(Error::Simple("Request id request to encode cancel pnl single".into()))
}
let request_id = request_id.expect("Request ID required to encode cancel pnl");
encoders::encode_cancel_pnl(request_id)
}
}

Expand All @@ -69,11 +66,8 @@ impl Subscribable<PnLSingle> for PnLSingle {
}

fn cancel_message(_server_version: i32, request_id: Option<i32>) -> Result<RequestMessage, Error> {
if let Some(request_id) = request_id {
encoders::encode_cancel_pnl_single(request_id)
} else {
Err(Error::Simple("Request id request to encode cancel pnl single".into()))
}
let request_id = request_id.expect("Request ID required to encode cancel pnl single");
encoders::encode_cancel_pnl_single(request_id)
}
}

Expand All @@ -91,24 +85,24 @@ pub struct Position {

#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
pub enum PositionResponse {
pub enum PositionUpdate {
Position(Position),
PositionEnd,
}

impl From<Position> for PositionResponse {
impl From<Position> for PositionUpdate {
fn from(val: Position) -> Self {
PositionResponse::Position(val)
PositionUpdate::Position(val)
}
}

impl Subscribable<PositionResponse> for PositionResponse {
impl Subscribable<PositionUpdate> for PositionUpdate {
const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[IncomingMessages::Position, IncomingMessages::PositionEnd];

fn decode(_server_version: i32, message: &mut ResponseMessage) -> Result<Self, Error> {
match message.message_type() {
IncomingMessages::Position => Ok(PositionResponse::Position(decoders::decode_position(message)?)),
IncomingMessages::PositionEnd => Ok(PositionResponse::PositionEnd),
IncomingMessages::Position => Ok(PositionUpdate::Position(decoders::decode_position(message)?)),
IncomingMessages::PositionEnd => Ok(PositionUpdate::PositionEnd),
message => Err(Error::Simple(format!("unexpected message: {message:?}"))),
}
}
Expand All @@ -118,6 +112,51 @@ impl Subscribable<PositionResponse> for PositionResponse {
}
}

#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
pub enum PositionUpdateMulti {
Position(PositionMulti),
PositionEnd,
}

impl From<PositionMulti> for PositionUpdateMulti {
fn from(val: PositionMulti) -> Self {
PositionUpdateMulti::Position(val)
}
}

/// Portfolio's open positions.
#[derive(Debug, Clone, Default)]
pub struct PositionMulti {
/// The account holding the position.
account: String,
/// The model code holding the position.
model_code: String,
/// The position's Contract
contract: Contract,
/// The number of positions held.
position: f64,
/// The average cost of the position.
average_cost: f64,
}

impl Subscribable<PositionUpdateMulti> for PositionUpdateMulti {
const RESPONSE_MESSAGE_IDS: &[IncomingMessages] = &[IncomingMessages::PositionMulti, IncomingMessages::PositionMultiEnd];

fn decode(_server_version: i32, message: &mut ResponseMessage) -> Result<Self, Error> {
match message.message_type() {
IncomingMessages::PositionMulti => Ok(PositionUpdateMulti::Position(decoders::decode_position_multi(message)?)),
IncomingMessages::PositionMultiEnd => Ok(PositionUpdateMulti::PositionEnd),
message => Err(Error::Simple(format!("unexpected message: {message:?}"))),
}
}

fn cancel_message(_server_version: i32, request_id: Option<i32>) -> Result<RequestMessage, Error> {
let request_id = request_id.expect("Request ID required to encode cancel positions multi");
Ok(encoders::encode_cancel_positions_multi(request_id)?)
}
}

#[derive(Debug, Default)]
pub struct FamilyCode {
/// Account ID
Expand All @@ -128,12 +167,11 @@ pub struct FamilyCode {

// Subscribes to position updates for all accessible accounts.
// All positions sent initially, and then only updates as positions change.
pub(crate) fn positions(client: &Client) -> Result<Subscription<PositionResponse>, Error> {
pub(crate) fn positions(client: &Client) -> Result<Subscription<PositionUpdate>, Error> {
client.check_server_version(server_versions::ACCOUNT_SUMMARY, "It does not support position requests.")?;

let message = encoders::encode_request_positions()?;

let responses = client.send_shared_request(OutgoingMessages::RequestPositions, message)?;
let request = encoders::encode_request_positions()?;
let responses = client.send_shared_request(OutgoingMessages::RequestPositions, request)?;

Ok(Subscription {
client,
Expand All @@ -143,7 +181,27 @@ pub(crate) fn positions(client: &Client) -> Result<Subscription<PositionResponse
})
}

impl SharesChannel for Subscription<'_, PositionResponse> {}
impl SharesChannel for Subscription<'_, PositionUpdate> {}

pub(crate) fn positions_multi<'a>(
client: &'a Client,
account: Option<&str>,
model_code: Option<&str>,
) -> Result<Subscription<'a, PositionUpdateMulti>, Error> {
client.check_server_version(server_versions::MODELS_SUPPORT, "It does not support positions multi requests.")?;

let request_id = client.next_request_id();

let request = encoders::encode_request_positions_multi(request_id, account, model_code)?;
let responses = client.send_request(request_id, request)?;

Ok(Subscription {
client,
request_id: Some(request_id),
responses,
phantom: PhantomData,
})
}

// Determine whether an account exists under an account family and find the account family code.
pub(crate) fn family_codes(client: &Client) -> Result<Vec<FamilyCode>, Error> {
Expand Down Expand Up @@ -176,7 +234,7 @@ pub(crate) fn pnl<'a>(client: &'a Client, account: &str, model_code: Option<&str

Ok(Subscription {
client,
request_id: None,
request_id: Some(request_id),
responses,
phantom: PhantomData,
})
Expand Down Expand Up @@ -204,7 +262,7 @@ pub(crate) fn pnl_single<'a>(

Ok(Subscription {
client,
request_id: None,
request_id: Some(request_id),
responses,
phantom: PhantomData,
})
Expand Down
31 changes: 30 additions & 1 deletion 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};
use super::{FamilyCode, PnL, PnLSingle, Position, PositionMulti};

pub(crate) fn decode_position(message: &mut ResponseMessage) -> Result<Position, Error> {
message.skip(); // message type
Expand Down Expand Up @@ -38,6 +38,35 @@ pub(crate) fn decode_position(message: &mut ResponseMessage) -> Result<Position,
Ok(position)
}

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

let mut position = PositionMulti {
account: message.next_string()?,
..Default::default()
};

position.contract.contract_id = message.next_int()?;
position.contract.symbol = 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 = message.next_string()?;
position.contract.currency = 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(); // message type

Expand Down
29 changes: 29 additions & 0 deletions src/accounts/decoders/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,35 @@ fn test_decode_positions() {
assert_eq!(position.average_cost, 196.77, "position.average_cost");
}

#[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");

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, "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, "NASDAQ", "position.contract.exchange");
assert_eq!(position.contract.currency, "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_family_codes() {
let mut message = super::ResponseMessage::from("78\01\0*\0\0");
Expand Down
33 changes: 27 additions & 6 deletions src/accounts/encoders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,32 @@ pub(crate) fn encode_cancel_positions() -> Result<RequestMessage, Error> {
encode_simple(OutgoingMessages::CancelPositions, 1)
}

pub(crate) fn encode_request_positions_multi(request_id: i32, account: Option<&str>, model_code: Option<&str>) -> Result<RequestMessage, Error> {
let mut message = RequestMessage::new();

const VERSION: i32 = 1;

message.push_field(&OutgoingMessages::RequestPositionsMulti);
message.push_field(&VERSION);
message.push_field(&request_id);
message.push_field(&account);
message.push_field(&model_code);

Ok(message)
}

pub(crate) fn encode_cancel_positions_multi(request_id: i32) -> Result<RequestMessage, Error> {
let mut message = RequestMessage::new();

const VERSION: i32 = 1;

message.push_field(&OutgoingMessages::CancelPositionsMulti);
message.push_field(&VERSION);
message.push_field(&request_id);

Ok(message)
}

pub(crate) fn encode_request_family_codes() -> Result<RequestMessage, Error> {
encode_simple(OutgoingMessages::RequestFamilyCodes, 1)
}
Expand All @@ -20,12 +46,7 @@ pub(crate) fn encode_request_pnl(request_id: i32, account: &str, model_code: Opt
message.push_field(&OutgoingMessages::RequestPnL);
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);

Ok(message)
}
Expand Down
34 changes: 31 additions & 3 deletions src/accounts/encoders/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,51 @@ use crate::ToField;
use super::*;

#[test]
fn test_request_positions() {
fn test_encode_request_positions() {
let message = super::encode_request_positions().expect("error encoding request");

assert_eq!(message[0], OutgoingMessages::RequestPositions.to_field(), "message.type");
assert_eq!(message[1], "1", "message.version");
}

#[test]
fn test_cancel_positions() {
fn test_encode_cancel_positions() {
let message = super::encode_cancel_positions().expect("error encoding request");

assert_eq!(message[0], OutgoingMessages::CancelPositions.to_field(), "message.type");
assert_eq!(message[1], "1", "message.version");
}

#[test]
fn test_request_family_codes() {
fn test_encode_request_positions_multi() {
let request_id = 9000;
let version = 1;
let account = Some("U1234567");
let model_code = Some("TARGET2024");

let message = super::encode_request_positions_multi(request_id, account, model_code).expect("error encoding request");

assert_eq!(message[0], OutgoingMessages::RequestPositionsMulti.to_field(), "message.type");
assert_eq!(message[1], version.to_field(), "message.version");
assert_eq!(message[2], request_id.to_field(), "message.request_id");
assert_eq!(message[3], account.to_field(), "message.account");
assert_eq!(message[4], model_code.to_field(), "message.model_code");
}

#[test]
fn test_encode_cancel_positions_multi() {
let request_id = 9000;
let version = 1;

let message = super::encode_cancel_positions_multi(request_id).expect("error encoding request");

assert_eq!(message[0], OutgoingMessages::CancelPositionsMulti.to_field(), "message.type");
assert_eq!(message[1], version.to_field(), "message.version");
assert_eq!(message[2], request_id.to_field(), "message.request_id");
}

#[test]
fn test_encode_request_family_codes() {
let message = super::encode_request_family_codes().expect("error encoding request");

assert_eq!(message[0], OutgoingMessages::RequestFamilyCodes.to_field(), "message.type");
Expand Down
Loading
Loading