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,
18 Finished,
20 Completed,
22 Cancelled,
24 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: usize,
94 offset: Option<usize>,
96 account_id: Option<usize>,
98 bot_id: Option<usize>,
100 scope: Option<DealsScope>,
102 order: Option<DealsOrder>,
104 base: Option<SmolStr>,
106 quote: Option<SmolStr>,
108}
109
110impl Inner {
111 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) = "e {
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 += ¶ms.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}