openlimits_coinbase/
lib.rs

1//! This module provides functionality for communicating with the coinbase API.
2//! # Example
3//! ```no_run
4//! use openlimits::exchange::coinbase::Coinbase;
5//! use openlimits::exchange::coinbase::CoinbaseParameters;
6//! use openlimits::prelude::*;
7//! use openlimits::model::market_pair::*;
8//!
9//! #[tokio::main]
10//! async fn main() {
11//!     let coinbase = Coinbase::new(CoinbaseParameters::production())
12//!                         .await
13//!                         .expect("Couldn't create coinbase client");
14//!
15//!     let market_pair = MarketPair(Currency::BTC, Currency::ETH);
16//!     let order_book = coinbase.order_book(&OrderBookRequest { market_pair })
17//!                         .await
18//!                         .expect("Couldn't get order book");
19
20//!     println!("{:?}", order_book);
21//! }
22//! ```
23
24#[cfg(feature = "bindings")]
25use ligen_macro::inner_ligen;
26
27#[cfg(feature = "bindings")]
28inner_ligen!(ignore);
29
30use std::convert::TryFrom;
31use async_trait::async_trait;
32use chrono::Duration;
33use client::BaseClient;
34use transport::Transport;
35use openlimits_exchange::{
36    errors::OpenLimitsError,
37    model::{
38        AskBid, Balance, CancelAllOrdersRequest, CancelOrderRequest, Candle,
39        GetHistoricRatesRequest, GetHistoricTradesRequest, GetOrderHistoryRequest, GetOrderRequest,
40        GetPriceTickerRequest, Liquidity, OpenLimitOrderRequest, OpenMarketOrderRequest,
41        Order, OrderBookRequest, OrderBookResponse, OrderCanceled, OrderStatus, OrderType,
42        Paginator, Side, Ticker, TimeInForce, Trade, TradeHistoryRequest,
43    },
44};
45use openlimits_exchange::traits::info::*;
46use openlimits_exchange::traits::*;
47use openlimits_exchange::shared::Result;
48use openlimits_exchange::shared::timestamp_to_naive_datetime;
49
50pub mod client;
51pub mod model;
52mod transport;
53mod coinbase_content_error;
54mod coinbase_credentials;
55mod coinbase_parameters;
56
57pub use coinbase_content_error::CoinbaseContentError;
58pub use coinbase_credentials::CoinbaseCredentials;
59pub use coinbase_parameters::CoinbaseParameters;
60pub use openlimits_exchange::shared;
61use openlimits_exchange::exchange::Environment;
62pub use crate::client::stream::CoinbaseWebsocket;
63use openlimits_exchange::model::market_pair::MarketPair;
64
65#[derive(Clone)]
66pub struct Coinbase {
67    pub exchange_info: ExchangeInfo,
68    pub client: BaseClient,
69}
70
71#[async_trait]
72impl Exchange for Coinbase {
73    type InitParams = CoinbaseParameters;
74    type InnerClient = BaseClient;
75
76    async fn new(parameters: Self::InitParams) -> Result<Self> {
77        let coinbase = match parameters.credentials {
78            Some(credentials) => Coinbase {
79                exchange_info: ExchangeInfo::new(),
80                client: BaseClient {
81                    transport: Transport::with_credential(
82                        &credentials.api_key,
83                        &credentials.api_secret,
84                        &credentials.passphrase,
85                        parameters.environment == Environment::Sandbox,
86                    )?,
87                },
88            },
89            None => Coinbase {
90                exchange_info: ExchangeInfo::new(),
91                client: BaseClient {
92                    transport: Transport::new(parameters.environment == Environment::Sandbox)?,
93                },
94            },
95        };
96
97        coinbase.refresh_market_info().await?;
98        Ok(coinbase)
99    }
100
101    fn inner_client(&self) -> Option<&Self::InnerClient> {
102        Some(&self.client)
103    }
104}
105
106#[async_trait]
107impl ExchangeInfoRetrieval for Coinbase {
108    async fn retrieve_pairs(&self) -> Result<Vec<MarketPairInfo>> {
109        self.client.products().await.map(|v| {
110            v.into_iter()
111                .map(|product| MarketPairInfo {
112                    symbol: product.id,
113                    base: product.base_currency,
114                    quote: product.quote_currency,
115                    base_increment: product.base_increment,
116                    quote_increment: product.quote_increment,
117                    min_base_trade_size: None,
118                    min_quote_trade_size: None,
119                })
120                .collect()
121        })
122    }
123
124    async fn refresh_market_info(&self) -> Result<Vec<MarketPairHandle>> {
125        self.exchange_info
126            .refresh(self as &dyn ExchangeInfoRetrieval)
127            .await
128    }
129
130    async fn get_pair(&self, name: &MarketPair) -> Result<MarketPairHandle> {
131        let name = crate::model::MarketPair::from(name.clone()).0;
132        self.exchange_info.get_pair(&name)
133    }
134}
135
136#[async_trait]
137impl ExchangeMarketData for Coinbase {
138    async fn order_book(&self, req: &OrderBookRequest) -> Result<OrderBookResponse> {
139        self.client
140            .book::<model::BookRecordL2, _>(req.market_pair.clone())
141            .await
142            .map(Into::into)
143    }
144
145    async fn get_price_ticker(&self, req: &GetPriceTickerRequest) -> Result<Ticker> {
146        self.client.ticker(req.market_pair.clone()).await.map(Into::into)
147    }
148
149    async fn get_historic_rates(&self, req: &GetHistoricRatesRequest) -> Result<Vec<Candle>> {
150        let params = model::CandleRequestParams::try_from(req)?;
151        self.client
152            .candles(req.market_pair.clone(), Some(&params))
153            .await
154            .map(|v| v.into_iter().map(Into::into).collect())
155    }
156
157    async fn get_historic_trades(&self, _req: &GetHistoricTradesRequest) -> Result<Vec<Trade>> {
158        unimplemented!("Only implemented for Nash right now");
159    }
160}
161
162impl From<model::Book<model::BookRecordL2>> for OrderBookResponse {
163    fn from(book: model::Book<model::BookRecordL2>) -> Self {
164        Self {
165            update_id: Some(book.sequence as u64),
166            last_update_id: None,
167            bids: book.bids.into_iter().map(Into::into).collect(),
168            asks: book.asks.into_iter().map(Into::into).collect(),
169        }
170    }
171}
172
173impl From<model::BookRecordL2> for AskBid {
174    fn from(bids: model::BookRecordL2) -> Self {
175        Self {
176            price: bids.price,
177            qty: bids.size,
178        }
179    }
180}
181
182impl From<model::Order> for Order {
183    fn from(order: model::Order) -> Self {
184        let (price, size, order_type) = match order._type {
185            model::OrderType::Limit {
186                price,
187                size,
188                time_in_force: _,
189            } => (Some(price), size, OrderType::Limit),
190            model::OrderType::Market { size, funds: _ } => (None, size, OrderType::Market),
191        };
192
193        Self {
194            id: order.id,
195            market_pair: order.product_id,
196            client_order_id: None,
197            created_at: Some((order.created_at.timestamp_millis()) as u64),
198            order_type,
199            side: order.side.into(),
200            status: order.status.into(),
201            size,
202            price,
203            remaining: Some(size - order.filled_size),
204            trades: Vec::new(),
205        }
206    }
207}
208
209#[async_trait]
210impl ExchangeAccount for Coinbase {
211    async fn limit_buy(&self, req: &OpenLimitOrderRequest) -> Result<Order> {
212        let pair = self.get_pair(&req.market_pair).await?.read()?;
213        self.client
214            .limit_buy(
215                pair,
216                req.size,
217                req.price,
218                model::OrderTimeInForce::from(req.time_in_force),
219                req.post_only,
220            )
221            .await
222            .map(Into::into)
223    }
224
225    async fn limit_sell(&self, req: &OpenLimitOrderRequest) -> Result<Order> {
226        let pair = self.get_pair(&req.market_pair).await?.read()?;
227        self.client
228            .limit_sell(
229                pair,
230                req.size,
231                req.price,
232                model::OrderTimeInForce::from(req.time_in_force),
233                req.post_only,
234            )
235            .await
236            .map(Into::into)
237    }
238
239    async fn market_buy(&self, req: &OpenMarketOrderRequest) -> Result<Order> {
240        let pair = self.get_pair(&req.market_pair).await?.read()?;
241        self.client.market_buy(pair, req.size).await.map(Into::into)
242    }
243
244    async fn market_sell(&self, req: &OpenMarketOrderRequest) -> Result<Order> {
245        let pair = self.get_pair(&req.market_pair).await?.read()?;
246        self.client
247            .market_sell(pair, req.size)
248            .await
249            .map(Into::into)
250    }
251
252    async fn cancel_order(&self, req: &CancelOrderRequest) -> Result<OrderCanceled> {
253        self.client
254            .cancel_order(req.id.clone(), req.market_pair.as_deref())
255            .await
256            .map(Into::into)
257    }
258
259    async fn cancel_all_orders(&self, req: &CancelAllOrdersRequest) -> Result<Vec<OrderCanceled>> {
260        self.client
261            .cancel_all_orders(req.market_pair.clone())
262            .await
263            .map(|v| v.into_iter().map(Into::into).collect())
264    }
265
266    async fn get_all_open_orders(&self) -> Result<Vec<Order>> {
267        let params = model::GetOrderRequest {
268            status: Some(String::from("open")),
269            paginator: None,
270            product_id: None,
271        };
272
273        self.client
274            .get_orders(Some(&params))
275            .await
276            .map(|v| v.into_iter().map(Into::into).collect())
277    }
278
279    async fn get_order_history(&self, req: &GetOrderHistoryRequest) -> Result<Vec<Order>> {
280        let req: model::GetOrderRequest = req.into();
281
282        self.client
283            .get_orders(Some(&req))
284            .await
285            .map(|v| v.into_iter().map(Into::into).collect())
286    }
287
288    async fn get_trade_history(&self, req: &TradeHistoryRequest) -> Result<Vec<Trade>> {
289        let req = req.into();
290
291        self.client
292            .get_fills(Some(&req))
293            .await
294            .map(|v| v.into_iter().map(Into::into).collect())
295    }
296
297    async fn get_account_balances(&self, paginator: Option<Paginator>) -> Result<Vec<Balance>> {
298        let paginator: Option<model::Paginator> = paginator.map(|p| p.into());
299
300        self.client
301            .get_account(paginator.as_ref())
302            .await
303            .map(|v| v.into_iter().map(Into::into).collect())
304    }
305
306    async fn get_order(&self, req: &GetOrderRequest) -> Result<Order> {
307        let id = req.id.clone();
308
309        self.client.get_order(id).await.map(Into::into)
310    }
311}
312
313impl From<model::Account> for Balance {
314    fn from(account: model::Account) -> Self {
315        Self {
316            asset: account.currency,
317            free: account.available,
318            total: account.balance,
319        }
320    }
321}
322
323impl From<model::Fill> for Trade {
324    fn from(fill: model::Fill) -> Self {
325        let (buyer_order_id, seller_order_id) = match fill.side.as_str() {
326            "buy" => (Some(fill.order_id), None),
327            _ => (None, Some(fill.order_id)),
328        };
329
330        Self {
331            id: fill.trade_id.to_string(),
332            buyer_order_id,
333            seller_order_id,
334            market_pair: fill.product_id,
335            price: fill.price,
336            qty: fill.size,
337            fees: Some(fill.fee),
338            side: match fill.side.as_str() {
339                "buy" => Side::Buy,
340                _ => Side::Sell,
341            },
342            liquidity: match fill.liquidity.as_str() {
343                "M" => Some(Liquidity::Maker),
344                "T" => Some(Liquidity::Taker),
345                _ => None,
346            },
347            created_at: fill.created_at.to_string(),
348        }
349    }
350}
351
352impl From<model::Ticker> for Ticker {
353    fn from(ticker: model::Ticker) -> Self {
354        Self {
355            price: Some(ticker.price),
356            price_24h: None,
357        }
358    }
359}
360
361impl From<model::Candle> for Candle {
362    fn from(candle: model::Candle) -> Self {
363        Self {
364            time: candle.time * 1000,
365            low: candle.low,
366            high: candle.high,
367            open: candle.open,
368            close: candle.close,
369            volume: candle.volume,
370        }
371    }
372}
373
374impl TryFrom<&GetHistoricRatesRequest> for model::CandleRequestParams {
375    type Error = OpenLimitsError;
376    fn try_from(params: &GetHistoricRatesRequest) -> Result<Self> {
377        let granularity = u32::try_from(params.interval)?;
378        Ok(Self {
379            daterange: params.paginator.clone().map(|p| p.into()),
380            granularity: Some(granularity),
381        })
382    }
383}
384
385impl From<&GetOrderHistoryRequest> for model::GetOrderRequest {
386    fn from(req: &GetOrderHistoryRequest) -> Self {
387        Self {
388            product_id: req.market_pair.clone().map(|market| crate::model::MarketPair::from(market).0),
389            paginator: req.paginator.clone().map(|p| p.into()),
390            status: None,
391        }
392    }
393}
394
395impl From<Paginator> for model::Paginator {
396    fn from(paginator: Paginator) -> Self {
397        Self {
398            after: paginator
399                .after
400                .map(|s| s.parse::<u64>().expect("Couldn't parse paginator.")),
401            before: paginator
402                .before
403                .map(|s| s.parse::<u64>().expect("Couldn't parse paginator.")),
404            limit: paginator.limit,
405        }
406    }
407}
408
409impl From<&Paginator> for model::Paginator {
410    fn from(paginator: &Paginator) -> Self {
411        Self {
412            after: paginator
413                .after
414                .as_ref()
415                .map(|s| s.parse().expect("coinbase page id did not parse as u64")),
416            before: paginator
417                .before
418                .as_ref()
419                .map(|s| s.parse().expect("coinbase page id did not parse as u64")),
420            limit: paginator.limit,
421        }
422    }
423}
424
425impl From<Paginator> for model::DateRange {
426    fn from(paginator: Paginator) -> Self {
427        Self {
428            start: paginator.start_time.map(timestamp_to_naive_datetime),
429            end: paginator.end_time.map(timestamp_to_naive_datetime),
430        }
431    }
432}
433
434impl From<&Paginator> for model::DateRange {
435    fn from(paginator: &Paginator) -> Self {
436        Self {
437            start: paginator.start_time.map(timestamp_to_naive_datetime),
438            end: paginator.end_time.map(timestamp_to_naive_datetime),
439        }
440    }
441}
442
443impl From<TimeInForce> for model::OrderTimeInForce {
444    fn from(tif: TimeInForce) -> Self {
445        match tif {
446            TimeInForce::GoodTillCancelled => model::OrderTimeInForce::GTC,
447            TimeInForce::FillOrKill => model::OrderTimeInForce::FOK,
448            TimeInForce::ImmediateOrCancelled => model::OrderTimeInForce::IOC,
449            TimeInForce::GoodTillTime(duration) => {
450                let day: Duration = Duration::days(1);
451                let hour: Duration = Duration::hours(1);
452                let minute: Duration = Duration::minutes(1);
453
454                if duration == day {
455                    model::OrderTimeInForce::GTT {
456                        cancel_after: model::CancelAfter::Day,
457                    }
458                } else if duration == hour {
459                    model::OrderTimeInForce::GTT {
460                        cancel_after: model::CancelAfter::Hour,
461                    }
462                } else if duration == minute {
463                    model::OrderTimeInForce::GTT {
464                        cancel_after: model::CancelAfter::Min,
465                    }
466                } else {
467                    panic!("Coinbase only supports durations of 1 day, 1 hour or 1 minute")
468                }
469            }
470        }
471    }
472}
473
474impl From<&TradeHistoryRequest> for model::GetFillsReq {
475    fn from(req: &TradeHistoryRequest) -> Self {
476        Self {
477            order_id: req.order_id.clone(),
478            paginator: req.paginator.clone().map(|p| p.into()),
479            product_id: req.market_pair.clone().map(|market| crate::model::MarketPair::from(market).0),
480        }
481    }
482}
483
484impl From<model::OrderSide> for Side {
485    fn from(req: model::OrderSide) -> Self {
486        match req {
487            model::OrderSide::Buy => Side::Buy,
488            model::OrderSide::Sell => Side::Sell,
489        }
490    }
491}
492
493impl From<model::OrderStatus> for OrderStatus {
494    fn from(req: model::OrderStatus) -> OrderStatus {
495        match req {
496            model::OrderStatus::Active => OrderStatus::Active,
497            model::OrderStatus::Done => OrderStatus::Filled,
498            model::OrderStatus::Open => OrderStatus::Open,
499            model::OrderStatus::Pending => OrderStatus::Pending,
500            model::OrderStatus::Rejected => OrderStatus::Rejected,
501        }
502    }
503}