three_commas_client/
deals.rs

1use crate::{middleware::RequestBuilderExt, ThreeCommasClient};
2use futures::{future::BoxFuture, stream::FusedStream, FutureExt, Stream};
3use smol_str::SmolStr;
4use std::{
5  fmt,
6  pin::Pin,
7  task::{Context, Poll},
8  usize, vec,
9};
10use surf::http::Result;
11use three_commas_types::Deal;
12use tracing::{event, Level};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum DealsScope {
16  /// active deals
17  Active,
18  /// finished deals
19  Finished,
20  /// successfully completed
21  Completed,
22  /// cancelled deals
23  Cancelled,
24  /// failed deals
25  Failed,
26}
27
28impl DealsScope {
29  pub fn as_str(&self) -> &'static str {
30    match self {
31      DealsScope::Active => "active",
32      DealsScope::Finished => "finished",
33      DealsScope::Completed => "completed",
34      DealsScope::Cancelled => "cancelled",
35      DealsScope::Failed => "failed",
36    }
37  }
38}
39
40impl fmt::Display for DealsScope {
41  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42    f.write_str(self.as_str())
43  }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum DealsOrder {
48  CreatedAt(DealsOrderDirection),
49  UpdatedAt(DealsOrderDirection),
50  ClosedAt(DealsOrderDirection),
51  Profit(DealsOrderDirection),
52  ProfitPercentage(DealsOrderDirection),
53}
54
55impl DealsOrder {
56  fn to_string_parts(self) -> (&'static str, &'static str) {
57    match self {
58      DealsOrder::CreatedAt(order) => ("created_at", order.as_str()),
59      DealsOrder::UpdatedAt(order) => ("updated_at", order.as_str()),
60      DealsOrder::ClosedAt(order) => ("closed_at", order.as_str()),
61      DealsOrder::Profit(order) => ("profit", order.as_str()),
62      DealsOrder::ProfitPercentage(order) => ("profit_percentage", order.as_str()),
63    }
64  }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
68pub enum DealsOrderDirection {
69  Asc,
70  Desc,
71}
72
73impl DealsOrderDirection {
74  pub fn as_str(&self) -> &'static str {
75    match self {
76      DealsOrderDirection::Asc => "asc",
77      DealsOrderDirection::Desc => "desc",
78    }
79  }
80}
81
82impl fmt::Display for DealsOrderDirection {
83  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84    f.write_str(self.as_str())
85  }
86}
87
88type FetchResponse = Vec<Deal>;
89
90struct Inner {
91  client: ThreeCommasClient,
92  /// Limit records. Max: 1_000
93  limit: usize,
94  /// Offset records
95  offset: Option<usize>,
96  /// Account to show bots on. Return all if not specified. Gather this from GET /ver1/accounts
97  account_id: Option<usize>,
98  /// Bot show deals on. Return all if not specified
99  bot_id: Option<usize>,
100  /// Limit deals to those in a certain state
101  scope: Option<DealsScope>,
102  /// Order the results
103  order: Option<DealsOrder>,
104  /// Base currency
105  base: Option<SmolStr>,
106  /// Quote currency
107  quote: Option<SmolStr>,
108}
109
110impl Inner {
111  /// Returns a list of deals, and the limit (number of deals requested).
112  /// If deals.len() == limit - there might be more deals to load.
113  fn request(&self, offset: usize) -> Result<(BoxFuture<'static, Result<FetchResponse>>, usize)> {
114    let mut params = form_urlencoded::Serializer::new(String::new());
115
116    let limit_num = self.limit;
117    let limit = limit_num.to_string();
118    let offset = (self.offset.unwrap_or_default() + offset).to_string();
119    let account_id = self.account_id.map(|v| v.to_string());
120    let bot_id = self.bot_id.map(|v| v.to_string());
121    let scope = self.scope.map(|v| v.as_str());
122    let order = self.order.map(|v| v.to_string_parts());
123    let base = self.base.as_deref();
124    let quote = self.quote.as_deref();
125
126    params.append_pair("limit", &*limit);
127    params.append_pair("offset", &*offset);
128
129    if let Some(account_id) = &account_id {
130      params.append_pair("account_id", &**account_id);
131    }
132
133    if let Some(bot_id) = &bot_id {
134      params.append_pair("bot_id", &**bot_id);
135    }
136
137    if let Some(scope) = &scope {
138      params.append_pair("scope", *scope);
139    }
140
141    if let Some((order, direction)) = &order {
142      params.append_pair("order", *order);
143      params.append_pair("order_direction", *direction);
144    }
145
146    if let Some(base) = &base {
147      params.append_pair("base", *base);
148    }
149
150    if let Some(quote) = &quote {
151      params.append_pair("quote", *quote);
152    }
153
154    let client = self.client.client.clone();
155    let mut url = String::from("ver1/deals?");
156    url += &params.finish();
157    let deals_fut = async move {
158      let req = client.get(url).signed();
159      let deals: Result<FetchResponse> = client.recv_json(req).await;
160      deals
161    };
162    Ok((Box::pin(deals_fut), limit_num))
163  }
164}
165
166enum State {
167  Init,
168  Fetch(usize),
169  Fetching {
170    fut: BoxFuture<'static, Result<FetchResponse>>,
171    limit: usize,
172    offset: usize,
173  },
174  Yielding {
175    iter: vec::IntoIter<Deal>,
176    next_offset: Option<usize>,
177  },
178  Done,
179}
180
181pub struct Deals {
182  inner: Inner,
183  state: State,
184}
185
186impl Deals {
187  pub(crate) fn new(client: ThreeCommasClient) -> Self {
188    Self {
189      inner: Inner {
190        client,
191        limit: 50,
192        offset: None,
193        account_id: None,
194        bot_id: None,
195        scope: None,
196        order: None,
197        base: None,
198        quote: None,
199      },
200      state: State::Init,
201    }
202  }
203
204  pub fn limit(mut self, limit: usize) -> Self {
205    assert!(!(limit > 1000), "limit cannot be greater than 1000");
206    assert!(!(limit == 0), "limit cannot be 0");
207
208    self.state = State::Init;
209    self.inner.limit = limit;
210    self
211  }
212
213  pub fn offset(mut self, offset: Option<usize>) -> Self {
214    self.state = State::Init;
215    self.inner.offset = offset;
216    self
217  }
218
219  pub fn account_id(mut self, account_id: Option<usize>) -> Self {
220    self.state = State::Init;
221    self.inner.account_id = account_id;
222    self
223  }
224
225  pub fn bot_id(mut self, bot_id: Option<usize>) -> Self {
226    self.state = State::Init;
227    self.inner.bot_id = bot_id;
228    self
229  }
230
231  pub fn scope(mut self, scope: Option<DealsScope>) -> Self {
232    self.state = State::Init;
233    self.inner.scope = scope;
234    self
235  }
236
237  pub fn order(mut self, order: Option<DealsOrder>) -> Self {
238    self.state = State::Init;
239    self.inner.order = order;
240    self
241  }
242
243  pub fn base(mut self, base: Option<impl AsRef<str>>) -> Self {
244    self.state = State::Init;
245    self.inner.base = base.map(|v| SmolStr::from(v.as_ref()));
246    self
247  }
248
249  pub fn quote(mut self, quote: Option<impl AsRef<str>>) -> Self {
250    self.state = State::Init;
251    self.inner.quote = quote.map(|v| SmolStr::from(v.as_ref()));
252    self
253  }
254}
255
256impl Stream for Deals {
257  type Item = Result<Deal>;
258
259  fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
260    let this = self.get_mut();
261    loop {
262      let inner = &this.inner;
263      let next = match &mut this.state {
264        State::Init => Ok(State::Fetch(0)),
265        State::Fetch(offset) => match inner.request(*offset) {
266          Ok((fut, limit)) => Ok(State::Fetching {
267            fut,
268            limit,
269            offset: *offset,
270          }),
271          Err(error) => Err(error),
272        },
273
274        State::Fetching { fut, limit, offset } => match fut.poll_unpin(cx) {
275          Poll::Ready(Ok(deals)) => {
276            let len = deals.len();
277            let has_more = len == *limit;
278            let iter = deals.into_iter();
279            let next_offset = has_more.then(|| len + *offset);
280            event!(
281              target: "3commas::client::deals",
282              Level::DEBUG,
283              deals_len = %len,
284              offset = %*offset,
285              next_offset = ?next_offset,
286              "Got {} deals when requesting {}, next offset = {:?}",
287              len,
288              *limit,
289              next_offset
290            );
291
292            Ok(State::Yielding { iter, next_offset })
293          }
294          Poll::Ready(Err(error)) => Err(error),
295          Poll::Pending => return Poll::Pending,
296        },
297
298        State::Yielding { iter, next_offset } => {
299          if let Some(next) = iter.next() {
300            return Poll::Ready(Some(Ok(next)));
301          }
302
303          match next_offset {
304            None => Ok(State::Done),
305            Some(offset) => Ok(State::Fetch(*offset)),
306          }
307        }
308
309        State::Done => return Poll::Ready(None),
310      };
311
312      match next {
313        Err(error) => {
314          this.state = State::Done;
315          return Poll::Ready(Some(Err(error)));
316        }
317        Ok(state) => this.state = state,
318      }
319    }
320  }
321
322  fn size_hint(&self) -> (usize, Option<usize>) {
323    match &self.state {
324      State::Yielding {
325        iter,
326        next_offset: _,
327      } => (iter.len(), None),
328      State::Done => (0, Some(0)),
329      _ => (0, None),
330    }
331  }
332}
333
334impl FusedStream for Deals {
335  fn is_terminated(&self) -> bool {
336    matches!(&self.state, State::Done)
337  }
338}