1#[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(¶ms))
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(¶ms))
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}