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

Adds request PNL APIs #119

Merged
merged 15 commits into from
Oct 7, 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
32 changes: 32 additions & 0 deletions examples/pnl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use clap::{arg, Command};
use ibapi::Client;

fn main() {
env_logger::init();

let matches = Command::new("pnl")
.about("Gets realtime profit and loss updates")
.arg(arg!(--connection_url <VALUE>).default_value("127.0.0.1:4002"))
.arg(arg!(--account <ACCOUNT>).required(true))
.get_matches();

let gateway_url = matches.get_one::<String>("connection_url").expect("connection_string is required");
let account = matches.get_one::<String>("account").expect("account is required");

let client = Client::connect(&gateway_url, 919).expect("connection failed");

let mut subscription = client.pnl(&account, None).expect("pnl request failed");

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

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

// After processing items subscription could be cancelled.
subscription.cancel();
}
}
36 changes: 36 additions & 0 deletions examples/pnl_single.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use clap::{arg, Command};
use ibapi::Client;
use time::format_description::parse;

fn main() {
env_logger::init();

let matches = Command::new("pnl")
.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))
.arg(arg!(--contract_id <CONTRACT>).required(true))
.get_matches();

let gateway_url = matches.get_one::<String>("connection_url").expect("connection_string is required");
let account = matches.get_one::<String>("account").expect("account is required");
let contract_id = matches.get_one::<String>("contract_id").expect("contract_id is required");
let contract_id = contract_id.parse::<i32>().expect("invalid number");

let client = Client::connect(&gateway_url, 919).expect("connection failed");

let mut subscription = client.pnl_single(&account, contract_id, None).expect("pnl single request failed");

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

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

// After processing items subscription could be cancelled.
subscription.cancel();
}
}
110 changes: 109 additions & 1 deletion src/accounts.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,69 @@
//! # Account Management
//!
//! This module provides functionality for managing positions and profit and loss (PnL)
//! information in a trading system. It includes structures and implementations for:
//!
//! - Position tracking
//! - Daily, unrealized, and realized PnL calculations
//! - Family code management
//! - Real-time PnL updates for individual positions
//!

use std::marker::PhantomData;

use log::error;

use crate::client::transport::GlobalResponseIterator;
use crate::client::{Subscribable, Subscription};
use crate::contracts::Contract;
use crate::messages::IncomingMessages;
use crate::messages::{IncomingMessages, ResponseMessage};
use crate::{server_versions, Client, Error};

mod decoders;
mod encoders;

// Realtime PnL update for account.
#[derive(Debug, Default)]
pub struct PnL {
/// 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>,
/// Realized PnL for the position
pub realized_pnl: Option<f64>,
}

impl Subscribable<PnL> for PnL {
const INCOMING_MESSAGE_ID: IncomingMessages = IncomingMessages::PnL;

fn decode(server_version: i32, message: &mut ResponseMessage) -> Result<Self, Error> {
decoders::decode_pnl(server_version, message)
}
}

// Realtime PnL update for a position in account.
#[derive(Debug, Default)]
pub struct PnLSingle {
// Current size of the position
pub position: f64,
/// 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>,
/// Realized PnL for the position
pub realized_pnl: Option<f64>,
/// Current market value of the position
pub value: f64,
}

impl Subscribable<PnLSingle> for PnLSingle {
const INCOMING_MESSAGE_ID: IncomingMessages = IncomingMessages::PnLSingle;

fn decode(server_version: i32, message: &mut ResponseMessage) -> Result<Self, Error> {
decoders::decode_pnl_single(server_version, message)
}
}

#[derive(Debug, Default)]
pub struct Position {
/// Account holding position
Expand Down Expand Up @@ -64,6 +120,55 @@ pub(crate) fn family_codes(client: &Client) -> Result<Vec<FamilyCode>, Error> {
Ok(Vec::default())
}
}

// Creates subscription for real time daily PnL and unrealized PnL updates
//
// # Arguments
// * `client` - client
// * `account` - account for which to receive PnL updates
// * `model_code` - specify to request PnL updates for a specific model
pub(crate) fn pnl<'a>(client: &'a Client, account: &str, model_code: Option<&str>) -> Result<Subscription<'a, PnL>, Error> {
client.check_server_version(server_versions::PNL, "It does not support PnL requests.")?;

let request_id = client.next_request_id();

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

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

// Requests real time updates for daily PnL of individual positions.
//
// # Arguments
// * `client` - Client
// * `account` - Account in which position exists
// * `contract_id` - Contract ID of contract to receive daily PnL updates for. Note: does not return message if invalid conId is entered
// * `model_code` - Model in which position exists
pub(crate) fn pnl_single<'a>(
client: &'a Client,
account: &str,
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.")?;

let request_id = client.next_request_id();

let request = encoders::encode_request_pnl_single(request_id, account, contract_id, model_code)?;
let responses = client.send_durable_request(request_id, request)?;

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

// Supports iteration over [Position].
pub(crate) struct PositionIterator<'a> {
client: &'a Client,
Expand Down Expand Up @@ -100,3 +205,6 @@ impl<'a> Iterator for PositionIterator<'a> {
}
}
}

#[cfg(test)]
mod tests;
100 changes: 52 additions & 48 deletions src/accounts/decoders.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
use crate::contracts::SecurityType;
use core::f64;

use crate::messages::ResponseMessage;
use crate::Error;
use crate::{contracts::SecurityType, Client};
use crate::{server_versions, Error};

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

pub(crate) fn decode_position(message: &mut ResponseMessage) -> Result<Position, Error> {
message.skip(); // message type
Expand Down Expand Up @@ -60,52 +62,54 @@ pub(crate) fn decode_family_codes(message: &mut ResponseMessage) -> Result<Vec<F
Ok(family_codes)
}

mod tests {

#[test]
fn decode_positions() {
let mut message = super::ResponseMessage::from("61\03\0DU1236109\076792991\0TSLA\0STK\0\00.0\0\0\0NASDAQ\0USD\0TSLA\0NMS\0500\0196.77\0");

let results = super::decode_position(&mut message);

if let Ok(position) = results {
assert_eq!(position.account, "DU1236109", "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");
} else if let Err(err) = results {
assert!(false, "error decoding position: {err}");
}
}
pub(crate) fn decode_pnl(server_version: i32, message: &mut ResponseMessage) -> Result<PnL, Error> {
message.skip(); // message type
message.skip(); // request id

#[test]
fn decode_family_codes() {
let mut message = super::ResponseMessage::from("78\01\0*\0\0");
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 results = super::decode_family_codes(&mut message);
Ok(PnL {
daily_pnl,
unrealized_pnl,
realized_pnl,
})
}

if let Ok(family_codes) = results {
assert_eq!(family_codes[0].account_id, "*", "family_codes.account_id");
assert_eq!(family_codes[0].family_code, "", "family_codes.family_code");
} else if let Err(err) = results {
panic!("Error decoding family_codes: {}", err);
}
}
pub(crate) fn decode_pnl_single(server_version: i32, message: &mut ResponseMessage) -> Result<PnLSingle, Error> {
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 value = message.next_double()?;

Ok(PnLSingle {
position,
daily_pnl,
unrealized_pnl,
realized_pnl,
value,
})
}

#[cfg(test)]
mod tests;
Loading
Loading