Skip to content

Commit

Permalink
Implements APIs to get WSH event data (#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
wboayue authored Nov 5, 2024
1 parent ce58a09 commit 2172e5c
Show file tree
Hide file tree
Showing 9 changed files with 433 additions and 99 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ cargo add ibapi
Or add the following line to your Cargo.toml:

```toml
ibapi = "0.5.0"
ibapi = "1.0.0"
```

## Examples
Expand Down
21 changes: 21 additions & 0 deletions examples/wsh_event_data_by_contract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use ibapi::Client;

// This example demonstrates requesting Wall Street Horizon event data by contract ID.

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

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

let contract_id = 76792991; // TSLA
let start_date = None;
let end_date = None;
let limit = None;
let auto_fill = None;

let event_data = client
.wsh_event_data_by_contract(contract_id, start_date, end_date, limit, auto_fill)
.expect("request wsh event data failed");

println!("{}", event_data.data_json);
}
20 changes: 20 additions & 0 deletions examples/wsh_event_data_by_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use ibapi::Client;

// This example demonstrates requesting Wall Street Horizon event data by filter.
// This featured does not appear to be released yet.

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

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

let filter = ""; // filter as JSON string.

let subscription = client
.wsh_event_data_by_filter(filter, None, None)
.expect("request wsh event data failed");

for event_data in subscription {
println!("{:?}", event_data);
}
}
12 changes: 12 additions & 0 deletions examples/wsh_metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use ibapi::Client;

// This example demonstrates requesting Wall Street Horizon metadata.

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

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

let metadata = client.wsh_metadata().expect("request wsh metadata failed");
println!("{}", metadata.data_json);
}
18 changes: 9 additions & 9 deletions src/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ pub(crate) fn positions(client: &Client) -> Result<Subscription<PositionUpdate>,

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

pub(crate) fn positions_multi<'a>(
pub(super) fn positions_multi<'a>(
client: &'a Client,
account: Option<&str>,
model_code: Option<&str>,
Expand All @@ -410,7 +410,7 @@ pub(crate) fn positions_multi<'a>(
}

// 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> {
pub(super) fn family_codes(client: &Client) -> Result<Vec<FamilyCode>, Error> {
client.check_server_version(server_versions::REQ_FAMILY_CODES, "It does not support family codes requests.")?;

let request = encoders::encode_request_family_codes()?;
Expand All @@ -430,7 +430,7 @@ pub(crate) fn family_codes(client: &Client) -> Result<Vec<FamilyCode>, Error> {
// * `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> {
pub(super) 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();
Expand All @@ -447,7 +447,7 @@ pub(crate) fn pnl<'a>(client: &'a Client, account: &str, model_code: Option<&str
// * `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>(
pub(super) fn pnl_single<'a>(
client: &'a Client,
account: &str,
contract_id: i32,
Expand All @@ -462,7 +462,7 @@ pub(crate) fn pnl_single<'a>(
Ok(Subscription::new(client, subscription, ResponseContext::default()))
}

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

let request_id = client.next_request_id();
Expand All @@ -472,14 +472,14 @@ pub fn account_summary<'a>(client: &'a Client, group: &str, tags: &[&str]) -> Re
Ok(Subscription::new(client, subscription, ResponseContext::default()))
}

pub fn account_updates<'a>(client: &'a Client, account: &str) -> Result<Subscription<'a, AccountUpdate>, Error> {
pub(super) fn account_updates<'a>(client: &'a Client, account: &str) -> Result<Subscription<'a, AccountUpdate>, Error> {
let request = encoders::encode_request_account_updates(client.server_version(), account)?;
let subscription = client.send_shared_request(OutgoingMessages::RequestAccountData, request)?;

Ok(Subscription::new(client, subscription, ResponseContext::default()))
}

pub fn account_updates_multi<'a>(
pub(super) fn account_updates_multi<'a>(
client: &'a Client,
account: Option<&str>,
model_code: Option<&str>,
Expand All @@ -493,7 +493,7 @@ pub fn account_updates_multi<'a>(
Ok(Subscription::new(client, subscription, ResponseContext::default()))
}

pub fn managed_accounts(client: &Client) -> Result<Vec<String>, Error> {
pub(super) fn managed_accounts(client: &Client) -> Result<Vec<String>, Error> {
let request = encoders::encode_request_managed_accounts()?;
let subscription = client.send_shared_request(OutgoingMessages::RequestManagedAccounts, request)?;

Expand All @@ -511,7 +511,7 @@ pub fn managed_accounts(client: &Client) -> Result<Vec<String>, Error> {
}
}

pub fn server_time(client: &Client) -> Result<OffsetDateTime, Error> {
pub(super) fn server_time(client: &Client) -> Result<OffsetDateTime, Error> {
let request = encoders::encode_request_server_time()?;
let subscription = client.send_shared_request(OutgoingMessages::RequestCurrentTime, request)?;

Expand Down
83 changes: 81 additions & 2 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::sync::{Arc, Mutex};
use std::time::Duration;

use log::{debug, error, warn};
use time::OffsetDateTime;
use time::{Date, OffsetDateTime};
use time_tz::Tz;

use crate::accounts::{AccountSummaries, AccountUpdate, AccountUpdateMulti, FamilyCode, PnL, PnLSingle, PositionUpdate, PositionUpdateMulti};
Expand All @@ -20,7 +20,8 @@ use crate::news::NewsArticle;
use crate::orders::{CancelOrder, Executions, ExerciseOptions, Order, Orders, PlaceOrder};
use crate::scanner::ScannerData;
use crate::transport::{Connection, ConnectionMetadata, InternalSubscription, MessageBus, TcpMessageBus};
use crate::{accounts, contracts, market_data, news, orders, scanner};
use crate::wsh::AutoFill;
use crate::{accounts, contracts, market_data, news, orders, scanner, wsh};

#[cfg(test)]
mod tests;
Expand Down Expand Up @@ -1434,6 +1435,84 @@ impl Client {
scanner::scanner_subscription(self, subscription, filter)
}

// == Wall Street Horizon

/// Requests metadata from the WSH calendar.
///
/// # Examples
///
/// ```no_run
/// use ibapi::Client;
///
/// let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed");
///
/// let metadata = client.wsh_metadata().expect("request wsh metadata failed");
/// println!("{:?}", metadata);
/// ```
pub fn wsh_metadata(&self) -> Result<wsh::WshMetadata, Error> {
wsh::wsh_metadata(self)
}

/// Requests event data for a specified contract from the Wall Street Horizons (WSH) calendar.
///
/// # Arguments
///
/// * `contract_id` - Contract identifier for the event request.
/// * `start_date` - Start date of the event request.
/// * `end_date` - End date of the event request.
/// * `limit` - Maximum number of events to return. Maximum of 100.
/// * `auto_fill` - Fields to automatically fill in. See [AutoFill] for more information.
///
/// # Examples
///
/// ```no_run
/// use ibapi::Client;
///
/// let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed");
///
/// let contract_id = 76792991; // TSLA
/// let event_data = client.wsh_event_data_by_contract(contract_id, None, None, None, None).expect("request wsh event data failed");
/// println!("{:?}", event_data);
/// ```
pub fn wsh_event_data_by_contract(
&self,
contract_id: i32,
start_date: Option<Date>,
end_date: Option<Date>,
limit: Option<i32>,
auto_fill: Option<AutoFill>,
) -> Result<wsh::WshEventData, Error> {
wsh::wsh_event_data_by_contract(self, contract_id, start_date, end_date, limit, auto_fill)
}

/// Requests event data from the Wall Street Horizons (WSH) calendar using a JSON filter.
///
/// # Arguments
///
/// * `filter` - Json-formatted string containing all filter values.
/// * `limit` - Maximum number of events to return. Maximum of 100.
/// * `auto_fill` - Fields to automatically fill in. See [AutoFill] for more information.
///
/// # Examples
///
/// ```no_run
/// use ibapi::Client;
///
/// let client = Client::connect("127.0.0.1:4002", 100).expect("connection failed");
///
/// let filter = ""; // see https://www.interactivebrokers.com/campus/ibkr-api-page/twsapi-doc/#wsheventdata-object
/// let event_data = client.wsh_event_data_by_filter(filter, None, None).expect("request wsh event data failed");
/// println!("{:?}", event_data);
/// ```
pub fn wsh_event_data_by_filter(
&self,
filter: &str,
limit: Option<i32>,
auto_fill: Option<AutoFill>,
) -> Result<Subscription<wsh::WshEventData>, Error> {
wsh::wsh_event_data_by_filter(self, filter, limit, auto_fill)
}

// == Internal Use ==

#[cfg(test)]
Expand Down
117 changes: 31 additions & 86 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,94 +6,14 @@
//!
//! <br>
//!
//! An implementation of the Interactive Brokers [TWS API](https://interactivebrokers.github.io/tws-api/introduction.html) for Rust.
//! This implementation is not a direct port of the official TWS API.
//! It provides a synchronous API that simplifies the development of trading strategies.
//! A comprehensive Rust implementation of the Interactive Brokers [TWS API](https://interactivebrokers.github.io/tws-api/introduction.html), providing a robust and
//! user-friendly interface for TWS and IB Gateway. Designed with simplicity in mind, it integrates smoothly into trading systems.
//!
//! This is a work in progress and was tested using TWS 10.19. The primary reference for this implementation is the [C# source code](https://github.com/InteractiveBrokers/tws-api-public).
//! This fully featured API enables the retrieval of account information, access to real-time and historical market data, order management,
//! market scanning, and access to news and Wall Street Horizons (WSH) event data. Future updates will focus on bug fixes,
//! maintaining parity with the official API, and enhancing usability.
//!
//! The following example gives a flavor of the API style. It is not a trading strategy recommendation and not a complete implementation.
//!
//!```no_run
//! use std::collections::VecDeque;
//!
//! use ibapi::contracts::Contract;
//! use ibapi::market_data::realtime::{BarSize, Bar, WhatToShow};
//! use ibapi::orders::{order_builder, Action, PlaceOrder};
//! use ibapi::Client;
//!
//! let client = Client::connect("127.0.0.1:4002", 100).unwrap();
//!
//! let symbol = "TSLA";
//! let contract = Contract::stock(symbol); // defaults to USD and SMART exchange.
//!
//! let bars = client.realtime_bars(&contract, BarSize::Sec5, WhatToShow::Trades, false).unwrap();
//!
//! let mut channel = BreakoutChannel::new(30);
//!
//! for bar in bars.iter() {
//! channel.add_bar(&bar);
//!
//! // Ensure enough bars and no open positions.
//! if !channel.ready() {
//! continue;
//! }
//!
//! let action = if bar.close > channel.high() {
//! Action::Buy
//! } else if bar.close < channel.low() {
//! Action::Sell
//! } else {
//! continue;
//! };
//!
//! let order_id = client.next_order_id();
//! let order = order_builder::market_order(action, 100.0);
//!
//! let notices = client.place_order(order_id, &contract, &order).unwrap();
//! for notice in notices {
//! if let PlaceOrder::ExecutionData(data) = notice {
//! println!("{} {} shares of {}", data.execution.side, data.execution.shares, data.contract.symbol);
//! } else {
//! println!("{:?}", notice);
//! }
//! }
//! }
//!
//! struct BreakoutChannel {
//! ticks: VecDeque<(f64, f64)>,
//! size: usize,
//! }
//!
//! impl BreakoutChannel {
//! fn new(size: usize) -> BreakoutChannel {
//! BreakoutChannel {
//! ticks: VecDeque::with_capacity(size + 1),
//! size,
//! }
//! }
//!
//! fn ready(&self) -> bool {
//! self.ticks.len() >= self.size
//! }
//!
//! fn add_bar(&mut self, bar: &Bar) {
//! self.ticks.push_back((bar.high, bar.low));
//!
//! if self.ticks.len() > self.size {
//! self.ticks.pop_front();
//! }
//! }
//!
//! fn high(&self) -> f64 {
//! self.ticks.iter().map(|x| x.0).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap()
//! }
//!
//! fn low(&self) -> f64 {
//! self.ticks.iter().map(|x| x.1).max_by(|a, b| a.partial_cmp(b).unwrap()).unwrap()
//! }
//! }
//!```
//! For an overview of API usage, refer to the [README](https://github.com/wboayue/rust-ibapi/blob/main/README.md).
/// Describes items present in an account.
pub mod accounts;
Expand All @@ -120,6 +40,8 @@ pub mod news;
pub mod orders;
/// APIs for working with the market scanner.
pub mod scanner;
/// APIs for working with Wall Street Horizon: Earnings Calendar & Event Data.
pub mod wsh;

mod server_versions;

Expand All @@ -128,6 +50,11 @@ pub use errors::Error;

#[doc(inline)]
pub use client::Client;
use std::sync::LazyLock;
use time::{
format_description::{self, BorrowedFormatItem},
Date,
};

#[cfg(test)]
pub(crate) mod stubs;
Expand Down Expand Up @@ -208,6 +135,24 @@ impl ToField for Option<f64> {
}
}

fn date_format() -> Vec<BorrowedFormatItem<'static>> {
format_description::parse("YYYYMMDD").unwrap()
}

static DATE_FORMAT: LazyLock<Vec<BorrowedFormatItem<'static>>> = LazyLock::new(date_format);

impl ToField for Date {
fn to_field(&self) -> String {
self.format(&DATE_FORMAT).unwrap()
}
}

impl ToField for Option<Date> {
fn to_field(&self) -> String {
encode_option_field(self)
}
}

fn encode_option_field<T: ToField>(val: &Option<T>) -> String {
match val {
Some(val) => val.to_field(),
Expand Down
Loading

0 comments on commit 2172e5c

Please sign in to comment.