ibkr_cp_api_client/rest/
endpoints.rs

1use std::collections::HashMap;
2
3use chrono::NaiveDateTime;
4use rust_decimal::Decimal;
5use serde_json::{json, Value};
6
7use crate::client::IBClientPortal;
8use crate::models::account_ledger::AccountLedger;
9use crate::models::contract::SecurityDefinitions;
10use crate::models::contract_detail::ContractDetail;
11use crate::models::definitions::AssetClass;
12use crate::models::futures_contract::FuturesContracts;
13use crate::models::market_data_history::MarketDataHistory;
14use crate::models::order_ticket::OrderTicket;
15use crate::models::positions::Position;
16use crate::models::stock_contracts::StockContracts;
17use crate::models::tickle::AuthStatus;
18use crate::models::tickle::Tickle;
19//https://www.interactivebrokers.com/api/doc.html
20
21pub async fn process_response<T: for<'a> serde::Deserialize<'a>>(
22    response: reqwest::Response,
23) -> Result<T, reqwest::Error> {
24    response.json().await
25}
26
27impl IBClientPortal {
28    /// Returns the url for the given path
29    fn get_url(&self, path: &str) -> String {
30        let base = if self.listen_ssl { "https" } else { "http" };
31        format!("{base}://localhost:{}/v1/api{path}", self.port)
32    }
33    /// Current Authentication status to the Brokerage system.
34    /// Market Data and Trading is not possible if not authenticated, e.g. authenticated shows false
35    pub async fn check_auth_status(&self) -> Result<AuthStatus, reqwest::Error> {
36        let response = self
37            .client
38            .post(self.get_url("/iserver/auth/status"))
39            .header(
40                reqwest::header::CONTENT_LENGTH,
41                reqwest::header::HeaderValue::from_static("0"),
42            )
43            .body("")
44            .send()
45            .await?;
46        response.json().await
47    }
48    /// If the gateway has not received any requests for several minutes an open session will automatically timeout.
49    /// The tickle endpoint pings the server to prevent the session from ending.
50    pub async fn tickle(&self) -> Result<Tickle, reqwest::Error> {
51        let response = self
52            .client
53            .post(self.get_url("/tickle"))
54            .header(
55                reqwest::header::CONTENT_LENGTH,
56                reqwest::header::HeaderValue::from_static("0"),
57            )
58            .body("")
59            .send()
60            .await?;
61        response.json().await
62    }
63    /// Returns a list of positions for the given account.
64    /// The endpoint supports paging, page's default size is 30 positions.
65    /// `/portfolio/accounts` or `/portfolio/subaccounts` must be called prior to this endpoint.
66    pub async fn get_positions(&self, page: i32) -> Result<Vec<Position>, reqwest::Error> {
67        let path = format!("/portfolio/{}/positions/{}", self.account, page);
68        let response = self.client.get(self.get_url(&path)).body("").send().await?;
69        println!("{:#?}", response.status());
70        process_response(response).await
71    }
72    ///Convenience method to call tickle and get the session id. It is necessary to auth the websocket connection.
73    pub async fn get_session_id(&mut self) -> Result<(), reqwest::Error> {
74        let response = self.tickle().await?;
75        self.session_id = Some(response.session);
76        Ok(())
77    }
78    ///Returns a list of security definitions for the given conids
79    pub async fn get_security_definition_by_contract_id(
80        &self,
81        contract_ids: Vec<i64>,
82    ) -> Result<SecurityDefinitions, reqwest::Error> {
83        let path = "/trsrv/secdef";
84        let payload = json!({
85            "conids" : contract_ids,
86        });
87        let request = self.client.post(self.get_url(path));
88        let response = request.body(payload.to_string()).send().await?;
89        process_response(response).await
90    }
91    ///Returns a list of non-expired future contracts for given symbol(s)
92    pub async fn get_futures_by_symbol(
93        &self,
94        symbols: Vec<&str>,
95    ) -> Result<FuturesContracts, reqwest::Error> {
96        let path = "/trsrv/futures";
97        let request = self
98            .client
99            .get(self.get_url(path))
100            .query(&[("symbols", symbols.join(","))]);
101        let response = request.send().await?;
102        process_response(response).await
103    }
104    ///Returns an object contains all stock contracts for given symbol(s)
105    pub async fn get_stocks_by_symbol(
106        &self,
107        symbols: Vec<&str>,
108    ) -> Result<StockContracts, reqwest::Error> {
109        let path = "/trsrv/stocks";
110        let request = self
111            .client
112            .get(self.get_url(path))
113            .query(&[("symbols", symbols.join(","))]);
114        let response = request.send().await?;
115        process_response(response).await
116    }
117    ///Search by underlying symbol or company name. Relays back what derivative contract(s) it has. This endpoint must be called before using /secdef/info.
118    /// If company name is specified will only receive limited response: conid, companyName, companyHeader and symbol.
119    pub async fn search_for_security(
120        &self,
121        symbol_or_name: &str,
122        is_name: bool,
123        sec_type: AssetClass,
124    ) -> Result<Value, reqwest::Error> {
125        let path = "/iserver/secdef/search";
126        let body = json!( {
127            "symbol": symbol_or_name,
128            "name": is_name,
129            "secType": sec_type,
130        });
131        let request = self.client.post(self.get_url(path)).body(body.to_string());
132        let response = request.send().await?;
133        process_response(response).await
134    }
135    ///Provides Contract Details of Futures, Options, Warrants, Cash and CFDs based on conid. To get the strike price for Options/Warrants use "/iserver/secdef/strikes" endpoint.
136    /// Must call /secdef/search for the underlying contract first.
137    pub async fn get_options(
138        &self,
139        underlying_con_id: i64,
140        sectype: AssetClass,
141        month: Option<String>,
142        exchange: Option<String>,
143        strike: Option<Decimal>,
144    ) -> Result<Value, reqwest::Error> {
145        let path = "/iserver/secdef/info";
146        let mut query = vec![
147            ("conid", underlying_con_id.to_string()),
148            ("sectype", sectype.to_string()),
149        ];
150        if let Some(month) = month {
151            query.push(("month", month));
152        }
153        if let Some(exchange) = exchange {
154            query.push(("exchange", exchange));
155        }
156        if let Some(strike) = strike {
157            query.push(("strike", strike.to_string()));
158        }
159        let response = self
160            .client
161            .get(self.get_url(path))
162            .query(&query)
163            .send()
164            .await?;
165        response.json().await
166    }
167    ///Logs the user out of the gateway session. Any further activity requires re-authentication.
168    pub async fn logout(&self) -> Result<Value, reqwest::Error> {
169        let response = self
170            .client
171            .post(self.get_url("/logout"))
172            .header(
173                reqwest::header::CONTENT_LENGTH,
174                reqwest::header::HeaderValue::from_static("0"),
175            )
176            .body("")
177            .send()
178            .await?;
179        response.json().await
180    }
181
182    pub async fn get_account_ledger(
183        &self,
184    ) -> Result<HashMap<String, AccountLedger>, reqwest::Error> {
185        let path = format!("/portfolio/{}/ledger", self.account);
186        let response = self.client.get(self.get_url(&path)).body("").send().await?;
187        process_response(response).await
188    }
189
190    pub async fn place_order(&self, orders: Vec<OrderTicket>) -> Result<Value, reqwest::Error> {
191        let path = format!("/iserver/account/{}/order", self.account);
192        let payload = json!({"orders":orders});
193        let request = self.client.post(self.get_url(&path));
194        let response = request.body(payload.to_string()).send().await?;
195        process_response(response).await
196    }
197
198    /// Get contracts details. Many fields are optional and do not match the api documentation.
199    pub async fn get_contract_detail(&self, conid: i64) -> Result<ContractDetail, reqwest::Error> {
200        let path = format!("/iserver/contract/{}/info", conid);
201        let response = self.client.get(self.get_url(&path)).body("").send().await?;
202        response.json().await
203    }
204
205    /// Get market data history
206    /// tradingDayDuration is not always present if period is less than 1 day
207    /// exchange is optional and will be set to "" if not provided
208    /// startTime is optional and not documented
209    /// to retrieve 1min bars the startTime  should be 2 minutes after the timestamp expected in the last bar of the response
210    pub async fn get_market_data_history(
211        &self,
212        conid: i64,
213        exchange: Option<&str>,
214        period: &str,
215        bar: &str,
216        outside_rth: bool,
217        start_time: Option<NaiveDateTime>,
218    ) -> Result<MarketDataHistory, reqwest::Error> {
219        let path = "/iserver/marketdata/history";
220        let start_time_str = match start_time {
221            Some(start_time) => start_time.format("%Y%m%d-%H:%M:%S").to_string(),
222            None => "".to_string(),
223        };
224
225        let request = self
226            .client
227            .get(self.get_url(path))
228            .query(&[("conid", conid)])
229            .query(&[("period", period)])
230            .query(&[("bar", bar)])
231            .query(&[("exchange", exchange.unwrap_or(""))])
232            .query(&[("outsideRth", outside_rth)])
233            .query(&[("startTime", start_time_str)]);
234        let response = request.send().await?;
235        response.json().await
236    }
237}