Skip to main content

binance_sdk/w3w_prediction/rest_api/apis/
position_api.rs

1/*
2 * Prediction Trading REST API
3 *
4 * Place and manage prediction market orders, query positions, and transfer funds via the Prediction Trading REST API.
5 *
6 * The version of the OpenAPI document: 1.0.0
7 *
8 *
9 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
10 * https://openapi-generator.tech
11 * Do not edit the class manually.
12 */
13
14#![allow(unused_imports)]
15use async_trait::async_trait;
16use derive_builder::Builder;
17use reqwest;
18use rust_decimal::prelude::*;
19use serde::{Deserialize, Serialize};
20use serde_json::{Value, json};
21use std::collections::BTreeMap;
22
23use crate::common::{
24    config::ConfigurationRestApi,
25    models::{ParamBuildError, RestApiResponse},
26    utils::send_request,
27};
28use crate::w3w_prediction::rest_api::models;
29
30const HAS_TIME_UNIT: bool = false;
31
32#[async_trait]
33pub trait PositionApi: Send + Sync {
34    async fn get_position_by_token(
35        &self,
36        params: GetPositionByTokenParams,
37    ) -> anyhow::Result<RestApiResponse<models::GetPositionByTokenResponse>>;
38    async fn query_pn_l(
39        &self,
40        params: QueryPnLParams,
41    ) -> anyhow::Result<RestApiResponse<models::QueryPnLResponse>>;
42    async fn query_positions(
43        &self,
44        params: QueryPositionsParams,
45    ) -> anyhow::Result<RestApiResponse<models::QueryPositionsResponse>>;
46    async fn query_positions_by_filter(
47        &self,
48        params: QueryPositionsByFilterParams,
49    ) -> anyhow::Result<RestApiResponse<models::QueryPositionsByFilterResponse>>;
50    async fn query_settled_position_history(
51        &self,
52        params: QuerySettledPositionHistoryParams,
53    ) -> anyhow::Result<RestApiResponse<models::QuerySettledPositionHistoryResponse>>;
54}
55
56#[derive(Debug, Clone)]
57pub struct PositionApiClient {
58    configuration: ConfigurationRestApi,
59}
60
61impl PositionApiClient {
62    pub fn new(configuration: ConfigurationRestApi) -> Self {
63        Self { configuration }
64    }
65}
66
67/// Request parameters for the [`get_position_by_token`] operation.
68///
69/// This struct holds all of the inputs you can pass when calling
70/// [`get_position_by_token`](#method.get_position_by_token).
71#[derive(Clone, Debug, Builder, Deserialize)]
72#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
73pub struct GetPositionByTokenParams {
74    /// User's prediction wallet address
75    ///
76    /// This field is **required.
77    #[builder(setter(into))]
78    #[serde(rename = "walletAddress")]
79    pub wallet_address: String,
80    /// Prediction outcome token ID
81    ///
82    /// This field is **required.
83    #[builder(setter(into))]
84    #[serde(rename = "tokenId")]
85    pub token_id: String,
86    /// Request validity window in milliseconds
87    ///
88    /// This field is **optional.
89    #[builder(setter(into), default)]
90    #[serde(rename = "recvWindow", default)]
91    pub recv_window: Option<i64>,
92}
93
94impl GetPositionByTokenParams {
95    /// Create a builder for [`get_position_by_token`].
96    ///
97    /// Required parameters:
98    ///
99    /// * `wallet_address` — User's prediction wallet address
100    /// * `token_id` — Prediction outcome token ID
101    ///
102    #[must_use]
103    pub fn builder(wallet_address: String, token_id: String) -> GetPositionByTokenParamsBuilder {
104        GetPositionByTokenParamsBuilder::default()
105            .wallet_address(wallet_address)
106            .token_id(token_id)
107    }
108}
109/// Request parameters for the [`query_pn_l`] operation.
110///
111/// This struct holds all of the inputs you can pass when calling
112/// [`query_pn_l`](#method.query_pn_l).
113#[derive(Clone, Debug, Builder, Deserialize)]
114#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
115pub struct QueryPnLParams {
116    /// User's prediction wallet address
117    ///
118    /// This field is **required.
119    #[builder(setter(into))]
120    #[serde(rename = "walletAddress")]
121    pub wallet_address: String,
122    /// Filter by prediction token ID
123    ///
124    /// This field is **optional.
125    #[builder(setter(into), default)]
126    #[serde(rename = "tokenId", default)]
127    pub token_id: Option<String>,
128    /// Filter by market ID. Must be > 0
129    ///
130    /// This field is **optional.
131    #[builder(setter(into), default)]
132    #[serde(rename = "marketId", default)]
133    pub market_id: Option<i64>,
134    /// Filter by market topic ID. Must be > 0
135    ///
136    /// This field is **optional.
137    #[builder(setter(into), default)]
138    #[serde(rename = "marketTopicId", default)]
139    pub market_topic_id: Option<i64>,
140    /// If `true`, return only active (unresolved) positions
141    ///
142    /// This field is **optional.
143    #[builder(setter(into), default)]
144    #[serde(rename = "activeOnly", default)]
145    pub active_only: Option<bool>,
146    /// Request validity window in milliseconds
147    ///
148    /// This field is **optional.
149    #[builder(setter(into), default)]
150    #[serde(rename = "recvWindow", default)]
151    pub recv_window: Option<i64>,
152}
153
154impl QueryPnLParams {
155    /// Create a builder for [`query_pn_l`].
156    ///
157    /// Required parameters:
158    ///
159    /// * `wallet_address` — User's prediction wallet address
160    ///
161    #[must_use]
162    pub fn builder(wallet_address: String) -> QueryPnLParamsBuilder {
163        QueryPnLParamsBuilder::default().wallet_address(wallet_address)
164    }
165}
166/// Request parameters for the [`query_positions`] operation.
167///
168/// This struct holds all of the inputs you can pass when calling
169/// [`query_positions`](#method.query_positions).
170#[derive(Clone, Debug, Builder, Deserialize)]
171#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
172pub struct QueryPositionsParams {
173    /// User's prediction wallet address
174    ///
175    /// This field is **required.
176    #[builder(setter(into))]
177    #[serde(rename = "walletAddress")]
178    pub wallet_address: String,
179    /// Position status tab. Values from `PositionQueryType`. Default `ONGOING`
180    ///
181    /// This field is **optional.
182    #[builder(setter(into), default)]
183    #[serde(rename = "tab", default)]
184    pub tab: Option<String>,
185    /// Pagination offset. Default `0`
186    ///
187    /// This field is **optional.
188    #[builder(setter(into), default)]
189    #[serde(rename = "offset", default)]
190    pub offset: Option<i32>,
191    /// Page size. Default `20`, range 1–100
192    ///
193    /// This field is **optional.
194    #[builder(setter(into), default)]
195    #[serde(rename = "limit", default)]
196    pub limit: Option<i32>,
197    /// Request validity window in milliseconds
198    ///
199    /// This field is **optional.
200    #[builder(setter(into), default)]
201    #[serde(rename = "recvWindow", default)]
202    pub recv_window: Option<i64>,
203}
204
205impl QueryPositionsParams {
206    /// Create a builder for [`query_positions`].
207    ///
208    /// Required parameters:
209    ///
210    /// * `wallet_address` — User's prediction wallet address
211    ///
212    #[must_use]
213    pub fn builder(wallet_address: String) -> QueryPositionsParamsBuilder {
214        QueryPositionsParamsBuilder::default().wallet_address(wallet_address)
215    }
216}
217/// Request parameters for the [`query_positions_by_filter`] operation.
218///
219/// This struct holds all of the inputs you can pass when calling
220/// [`query_positions_by_filter`](#method.query_positions_by_filter).
221#[derive(Clone, Debug, Builder, Deserialize, Default)]
222#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
223pub struct QueryPositionsByFilterParams {
224    /// User's prediction wallet address
225    ///
226    /// This field is **optional.
227    #[builder(setter(into), default)]
228    #[serde(rename = "walletAddress", default)]
229    pub wallet_address: Option<String>,
230    /// Filter by market topic ID
231    ///
232    /// This field is **optional.
233    #[builder(setter(into), default)]
234    #[serde(rename = "marketTopicId", default)]
235    pub market_topic_id: Option<i64>,
236    /// Request validity window in milliseconds
237    ///
238    /// This field is **optional.
239    #[builder(setter(into), default)]
240    #[serde(rename = "recvWindow", default)]
241    pub recv_window: Option<i64>,
242}
243
244impl QueryPositionsByFilterParams {
245    /// Create a builder for [`query_positions_by_filter`].
246    ///
247    #[must_use]
248    pub fn builder() -> QueryPositionsByFilterParamsBuilder {
249        QueryPositionsByFilterParamsBuilder::default()
250    }
251}
252/// Request parameters for the [`query_settled_position_history`] operation.
253///
254/// This struct holds all of the inputs you can pass when calling
255/// [`query_settled_position_history`](#method.query_settled_position_history).
256#[derive(Clone, Debug, Builder, Deserialize)]
257#[builder(pattern = "owned", build_fn(error = "ParamBuildError"))]
258pub struct QuerySettledPositionHistoryParams {
259    /// User's prediction wallet address
260    ///
261    /// This field is **required.
262    #[builder(setter(into))]
263    #[serde(rename = "walletAddress")]
264    pub wallet_address: String,
265    /// Filter by level-1 category
266    ///
267    /// This field is **optional.
268    #[builder(setter(into), default)]
269    #[serde(rename = "l1Category", default)]
270    pub l1_category: Option<String>,
271    /// Settlement result filter
272    ///
273    /// This field is **optional.
274    #[builder(setter(into), default)]
275    #[serde(rename = "result", default)]
276    pub result: Option<i32>,
277    /// Start date. Format: `yyyy-MM-dd`. Must be ≤ `endDate`
278    ///
279    /// This field is **optional.
280    #[builder(setter(into), default)]
281    #[serde(rename = "startDate", default)]
282    pub start_date: Option<String>,
283    /// End date. Format: `yyyy-MM-dd`. Must be ≥ `startDate`
284    ///
285    /// This field is **optional.
286    #[builder(setter(into), default)]
287    #[serde(rename = "endDate", default)]
288    pub end_date: Option<String>,
289    /// Pagination offset. Default `0`
290    ///
291    /// This field is **optional.
292    #[builder(setter(into), default)]
293    #[serde(rename = "offset", default)]
294    pub offset: Option<i32>,
295    /// Page size. Default `20`, range 1–100
296    ///
297    /// This field is **optional.
298    #[builder(setter(into), default)]
299    #[serde(rename = "limit", default)]
300    pub limit: Option<i32>,
301    /// Request validity window in milliseconds
302    ///
303    /// This field is **optional.
304    #[builder(setter(into), default)]
305    #[serde(rename = "recvWindow", default)]
306    pub recv_window: Option<i64>,
307}
308
309impl QuerySettledPositionHistoryParams {
310    /// Create a builder for [`query_settled_position_history`].
311    ///
312    /// Required parameters:
313    ///
314    /// * `wallet_address` — User's prediction wallet address
315    ///
316    #[must_use]
317    pub fn builder(wallet_address: String) -> QuerySettledPositionHistoryParamsBuilder {
318        QuerySettledPositionHistoryParamsBuilder::default().wallet_address(wallet_address)
319    }
320}
321
322#[async_trait]
323impl PositionApi for PositionApiClient {
324    async fn get_position_by_token(
325        &self,
326        params: GetPositionByTokenParams,
327    ) -> anyhow::Result<RestApiResponse<models::GetPositionByTokenResponse>> {
328        let GetPositionByTokenParams {
329            wallet_address,
330            token_id,
331            recv_window,
332        } = params;
333
334        let mut query_params = BTreeMap::new();
335        let body_params = BTreeMap::new();
336
337        query_params.insert("walletAddress".to_string(), json!(wallet_address));
338
339        query_params.insert("tokenId".to_string(), json!(token_id));
340
341        if let Some(rw) = recv_window {
342            query_params.insert("recvWindow".to_string(), json!(rw));
343        }
344
345        send_request::<models::GetPositionByTokenResponse>(
346            &self.configuration,
347            "/sapi/v1/w3w/wallet/prediction/position/token",
348            reqwest::Method::GET,
349            query_params,
350            body_params,
351            if HAS_TIME_UNIT {
352                self.configuration.time_unit
353            } else {
354                None
355            },
356            true,
357        )
358        .await
359    }
360
361    async fn query_pn_l(
362        &self,
363        params: QueryPnLParams,
364    ) -> anyhow::Result<RestApiResponse<models::QueryPnLResponse>> {
365        let QueryPnLParams {
366            wallet_address,
367            token_id,
368            market_id,
369            market_topic_id,
370            active_only,
371            recv_window,
372        } = params;
373
374        let mut query_params = BTreeMap::new();
375        let body_params = BTreeMap::new();
376
377        query_params.insert("walletAddress".to_string(), json!(wallet_address));
378
379        if let Some(rw) = token_id {
380            query_params.insert("tokenId".to_string(), json!(rw));
381        }
382
383        if let Some(rw) = market_id {
384            query_params.insert("marketId".to_string(), json!(rw));
385        }
386
387        if let Some(rw) = market_topic_id {
388            query_params.insert("marketTopicId".to_string(), json!(rw));
389        }
390
391        if let Some(rw) = active_only {
392            query_params.insert("activeOnly".to_string(), json!(rw));
393        }
394
395        if let Some(rw) = recv_window {
396            query_params.insert("recvWindow".to_string(), json!(rw));
397        }
398
399        send_request::<models::QueryPnLResponse>(
400            &self.configuration,
401            "/sapi/v1/w3w/wallet/prediction/pnl/query",
402            reqwest::Method::GET,
403            query_params,
404            body_params,
405            if HAS_TIME_UNIT {
406                self.configuration.time_unit
407            } else {
408                None
409            },
410            true,
411        )
412        .await
413    }
414
415    async fn query_positions(
416        &self,
417        params: QueryPositionsParams,
418    ) -> anyhow::Result<RestApiResponse<models::QueryPositionsResponse>> {
419        let QueryPositionsParams {
420            wallet_address,
421            tab,
422            offset,
423            limit,
424            recv_window,
425        } = params;
426
427        let mut query_params = BTreeMap::new();
428        let body_params = BTreeMap::new();
429
430        query_params.insert("walletAddress".to_string(), json!(wallet_address));
431
432        if let Some(rw) = tab {
433            query_params.insert("tab".to_string(), json!(rw));
434        }
435
436        if let Some(rw) = offset {
437            query_params.insert("offset".to_string(), json!(rw));
438        }
439
440        if let Some(rw) = limit {
441            query_params.insert("limit".to_string(), json!(rw));
442        }
443
444        if let Some(rw) = recv_window {
445            query_params.insert("recvWindow".to_string(), json!(rw));
446        }
447
448        send_request::<models::QueryPositionsResponse>(
449            &self.configuration,
450            "/sapi/v1/w3w/wallet/prediction/position/list",
451            reqwest::Method::GET,
452            query_params,
453            body_params,
454            if HAS_TIME_UNIT {
455                self.configuration.time_unit
456            } else {
457                None
458            },
459            true,
460        )
461        .await
462    }
463
464    async fn query_positions_by_filter(
465        &self,
466        params: QueryPositionsByFilterParams,
467    ) -> anyhow::Result<RestApiResponse<models::QueryPositionsByFilterResponse>> {
468        let QueryPositionsByFilterParams {
469            wallet_address,
470            market_topic_id,
471            recv_window,
472        } = params;
473
474        let mut query_params = BTreeMap::new();
475        let body_params = BTreeMap::new();
476
477        if let Some(rw) = wallet_address {
478            query_params.insert("walletAddress".to_string(), json!(rw));
479        }
480
481        if let Some(rw) = market_topic_id {
482            query_params.insert("marketTopicId".to_string(), json!(rw));
483        }
484
485        if let Some(rw) = recv_window {
486            query_params.insert("recvWindow".to_string(), json!(rw));
487        }
488
489        send_request::<models::QueryPositionsByFilterResponse>(
490            &self.configuration,
491            "/sapi/v1/w3w/wallet/prediction/position/filter",
492            reqwest::Method::GET,
493            query_params,
494            body_params,
495            if HAS_TIME_UNIT {
496                self.configuration.time_unit
497            } else {
498                None
499            },
500            true,
501        )
502        .await
503    }
504
505    async fn query_settled_position_history(
506        &self,
507        params: QuerySettledPositionHistoryParams,
508    ) -> anyhow::Result<RestApiResponse<models::QuerySettledPositionHistoryResponse>> {
509        let QuerySettledPositionHistoryParams {
510            wallet_address,
511            l1_category,
512            result,
513            start_date,
514            end_date,
515            offset,
516            limit,
517            recv_window,
518        } = params;
519
520        let mut query_params = BTreeMap::new();
521        let body_params = BTreeMap::new();
522
523        query_params.insert("walletAddress".to_string(), json!(wallet_address));
524
525        if let Some(rw) = l1_category {
526            query_params.insert("l1Category".to_string(), json!(rw));
527        }
528
529        if let Some(rw) = result {
530            query_params.insert("result".to_string(), json!(rw));
531        }
532
533        if let Some(rw) = start_date {
534            query_params.insert("startDate".to_string(), json!(rw));
535        }
536
537        if let Some(rw) = end_date {
538            query_params.insert("endDate".to_string(), json!(rw));
539        }
540
541        if let Some(rw) = offset {
542            query_params.insert("offset".to_string(), json!(rw));
543        }
544
545        if let Some(rw) = limit {
546            query_params.insert("limit".to_string(), json!(rw));
547        }
548
549        if let Some(rw) = recv_window {
550            query_params.insert("recvWindow".to_string(), json!(rw));
551        }
552
553        send_request::<models::QuerySettledPositionHistoryResponse>(
554            &self.configuration,
555            "/sapi/v1/w3w/wallet/prediction/position/settled-history",
556            reqwest::Method::GET,
557            query_params,
558            body_params,
559            if HAS_TIME_UNIT {
560                self.configuration.time_unit
561            } else {
562                None
563            },
564            true,
565        )
566        .await
567    }
568}
569
570#[cfg(all(test, feature = "w3w_prediction"))]
571mod tests {
572    use super::*;
573    use crate::TOKIO_SHARED_RT;
574    use crate::{errors::ConnectorError, models::DataFuture, models::RestApiRateLimit};
575    use async_trait::async_trait;
576    use std::collections::HashMap;
577
578    struct DummyRestApiResponse<T> {
579        inner: Box<dyn FnOnce() -> DataFuture<Result<T, ConnectorError>> + Send + Sync>,
580        status: u16,
581        headers: HashMap<String, String>,
582        rate_limits: Option<Vec<RestApiRateLimit>>,
583    }
584
585    impl<T> From<DummyRestApiResponse<T>> for RestApiResponse<T> {
586        fn from(dummy: DummyRestApiResponse<T>) -> Self {
587            Self {
588                data_fn: dummy.inner,
589                status: dummy.status,
590                headers: dummy.headers,
591                rate_limits: dummy.rate_limits,
592            }
593        }
594    }
595
596    struct MockPositionApiClient {
597        force_error: bool,
598    }
599
600    #[async_trait]
601    impl PositionApi for MockPositionApiClient {
602        async fn get_position_by_token(
603            &self,
604            _params: GetPositionByTokenParams,
605        ) -> anyhow::Result<RestApiResponse<models::GetPositionByTokenResponse>> {
606            if self.force_error {
607                return Err(ConnectorError::ConnectorClientError {
608                    msg: "ResponseError".to_string(),
609                    code: None,
610                }
611                .into());
612            }
613
614            let resp_json: Value = serde_json::from_str(r#"{"position":{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}}"#).unwrap_or_else(|_| serde_json::json!({}));
615            let dummy_response: models::GetPositionByTokenResponse =
616                serde_json::from_value(resp_json.clone())
617                    .expect("should parse into models::GetPositionByTokenResponse");
618
619            let dummy = DummyRestApiResponse {
620                inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
621                status: 200,
622                headers: HashMap::new(),
623                rate_limits: None,
624            };
625
626            Ok(dummy.into())
627        }
628
629        async fn query_pn_l(
630            &self,
631            _params: QueryPnLParams,
632        ) -> anyhow::Result<RestApiResponse<models::QueryPnLResponse>> {
633            if self.force_error {
634                return Err(ConnectorError::ConnectorClientError {
635                    msg: "ResponseError".to_string(),
636                    code: None,
637                }
638                .into());
639            }
640
641            let resp_json: Value = serde_json::from_str(r#"{"chainId":"56","walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","pnl":{},"pnlList":[{"id":10001,"walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","marketTopicId":4229564,"marketId":5567895,"tokenId":"112233","vendor":"PREDICT_FUN","currentShares":"1923.07","avgPrice":"0.52","currentPrice":"0.55","realizedPnl":"0.00","unrealizedPnl":"0.06","totalPnl":"0.06","pnlPercentage":"6.00","isResolved":false}],"totalCount":1,"totalRealizedPnl":"0.00","totalUnrealizedPnl":"0.06","totalPnl":"0.06"}"#).unwrap_or_else(|_| serde_json::json!({}));
642            let dummy_response: models::QueryPnLResponse =
643                serde_json::from_value(resp_json.clone())
644                    .expect("should parse into models::QueryPnLResponse");
645
646            let dummy = DummyRestApiResponse {
647                inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
648                status: 200,
649                headers: HashMap::new(),
650                rate_limits: None,
651            };
652
653            Ok(dummy.into())
654        }
655
656        async fn query_positions(
657            &self,
658            _params: QueryPositionsParams,
659        ) -> anyhow::Result<RestApiResponse<models::QueryPositionsResponse>> {
660            if self.force_error {
661                return Err(ConnectorError::ConnectorClientError {
662                    msg: "ResponseError".to_string(),
663                    code: None,
664                }
665                .into());
666            }
667
668            let resp_json: Value = serde_json::from_str(r#"{"summary":{"totalValue":"1523.45","positionValue":"523.45","walletBalance":"1000.00","totalClaimableAmount":"50.00","todayRealizedPnl":"15.30","todayRealizedPnlPercent":"3.10","todayTotalCost":"493.55"},"counts":{"ongoingCount":3,"endedCount":12,"pendingClaimCount":1},"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","toWin":"1923.07","positionStatus":"OPEN","canClaim":false,"endDate":1748134800000,"unrealizedPnl":"0.06","unrealizedPnlPercent":"6.00","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
669            let dummy_response: models::QueryPositionsResponse =
670                serde_json::from_value(resp_json.clone())
671                    .expect("should parse into models::QueryPositionsResponse");
672
673            let dummy = DummyRestApiResponse {
674                inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
675                status: 200,
676                headers: HashMap::new(),
677                rate_limits: None,
678            };
679
680            Ok(dummy.into())
681        }
682
683        async fn query_positions_by_filter(
684            &self,
685            _params: QueryPositionsByFilterParams,
686        ) -> anyhow::Result<RestApiResponse<models::QueryPositionsByFilterResponse>> {
687            if self.force_error {
688                return Err(ConnectorError::ConnectorClientError {
689                    msg: "ResponseError".to_string(),
690                    code: None,
691                }
692                .into());
693            }
694
695            let resp_json: Value = serde_json::from_str(r#"{"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
696            let dummy_response: models::QueryPositionsByFilterResponse =
697                serde_json::from_value(resp_json.clone())
698                    .expect("should parse into models::QueryPositionsByFilterResponse");
699
700            let dummy = DummyRestApiResponse {
701                inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
702                status: 200,
703                headers: HashMap::new(),
704                rate_limits: None,
705            };
706
707            Ok(dummy.into())
708        }
709
710        async fn query_settled_position_history(
711            &self,
712            _params: QuerySettledPositionHistoryParams,
713        ) -> anyhow::Result<RestApiResponse<models::QuerySettledPositionHistoryResponse>> {
714            if self.force_error {
715                return Err(ConnectorError::ConnectorClientError {
716                    msg: "ResponseError".to_string(),
717                    code: None,
718                }
719                .into());
720            }
721
722            let resp_json: Value = serde_json::from_str(r#"{"totalCount":5,"positions":[{"positionId":998,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112200","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229500,"marketId":5567800,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"5000.00","avgPrice":"0.30","totalCost":"1.50","value":"5000.00","currentPrice":"1.00","toWin":"5000.00","positionStatus":"CLAIMED","isWinner":true,"redeemStatus":"CONFIRMED","endDate":1748040000000,"finalOutcome":"YES","realizedPnl":"4998.50","pnl":"4998.50","claimAmount":"5000.00","settledDate":1748040000000,"createdTime":1748020000000,"updatedTime":1748040500000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
723            let dummy_response: models::QuerySettledPositionHistoryResponse =
724                serde_json::from_value(resp_json.clone())
725                    .expect("should parse into models::QuerySettledPositionHistoryResponse");
726
727            let dummy = DummyRestApiResponse {
728                inner: Box::new(move || Box::pin(async move { Ok(dummy_response) })),
729                status: 200,
730                headers: HashMap::new(),
731                rate_limits: None,
732            };
733
734            Ok(dummy.into())
735        }
736    }
737
738    #[test]
739    fn get_position_by_token_required_params_success() {
740        TOKIO_SHARED_RT.block_on(async {
741            let client = MockPositionApiClient { force_error: false };
742
743            let params = GetPositionByTokenParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),"112233".to_string(),).build().unwrap();
744
745            let resp_json: Value = serde_json::from_str(r#"{"position":{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}}"#).unwrap_or_else(|_| serde_json::json!({}));
746            let expected_response : models::GetPositionByTokenResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetPositionByTokenResponse");
747
748            let resp = client.get_position_by_token(params).await.expect("Expected a response");
749            let data_future = resp.data();
750            let actual_response = data_future.await.unwrap();
751            assert_eq!(actual_response, expected_response);
752        });
753    }
754
755    #[test]
756    fn get_position_by_token_optional_params_success() {
757        TOKIO_SHARED_RT.block_on(async {
758            let client = MockPositionApiClient { force_error: false };
759
760            let params = GetPositionByTokenParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),"112233".to_string(),).recv_window(5000).build().unwrap();
761
762            let resp_json: Value = serde_json::from_str(r#"{"position":{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}}"#).unwrap_or_else(|_| serde_json::json!({}));
763            let expected_response : models::GetPositionByTokenResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::GetPositionByTokenResponse");
764
765            let resp = client.get_position_by_token(params).await.expect("Expected a response");
766            let data_future = resp.data();
767            let actual_response = data_future.await.unwrap();
768            assert_eq!(actual_response, expected_response);
769        });
770    }
771
772    #[test]
773    fn get_position_by_token_response_error() {
774        TOKIO_SHARED_RT.block_on(async {
775            let client = MockPositionApiClient { force_error: true };
776
777            let params = GetPositionByTokenParams::builder(
778                "0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),
779                "112233".to_string(),
780            )
781            .build()
782            .unwrap();
783
784            match client.get_position_by_token(params).await {
785                Ok(_) => panic!("Expected an error"),
786                Err(err) => {
787                    assert_eq!(err.to_string(), "Connector client error: ResponseError");
788                }
789            }
790        });
791    }
792
793    #[test]
794    fn query_pn_l_required_params_success() {
795        TOKIO_SHARED_RT.block_on(async {
796            let client = MockPositionApiClient { force_error: false };
797
798            let params = QueryPnLParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).build().unwrap();
799
800            let resp_json: Value = serde_json::from_str(r#"{"chainId":"56","walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","pnl":{},"pnlList":[{"id":10001,"walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","marketTopicId":4229564,"marketId":5567895,"tokenId":"112233","vendor":"PREDICT_FUN","currentShares":"1923.07","avgPrice":"0.52","currentPrice":"0.55","realizedPnl":"0.00","unrealizedPnl":"0.06","totalPnl":"0.06","pnlPercentage":"6.00","isResolved":false}],"totalCount":1,"totalRealizedPnl":"0.00","totalUnrealizedPnl":"0.06","totalPnl":"0.06"}"#).unwrap_or_else(|_| serde_json::json!({}));
801            let expected_response : models::QueryPnLResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPnLResponse");
802
803            let resp = client.query_pn_l(params).await.expect("Expected a response");
804            let data_future = resp.data();
805            let actual_response = data_future.await.unwrap();
806            assert_eq!(actual_response, expected_response);
807        });
808    }
809
810    #[test]
811    fn query_pn_l_optional_params_success() {
812        TOKIO_SHARED_RT.block_on(async {
813            let client = MockPositionApiClient { force_error: false };
814
815            let params = QueryPnLParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).token_id("112233".to_string()).market_id(5567895).market_topic_id(4229564).active_only(false).recv_window(5000).build().unwrap();
816
817            let resp_json: Value = serde_json::from_str(r#"{"chainId":"56","walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","pnl":{},"pnlList":[{"id":10001,"walletAddress":"0x12e32db8817e292508c34111cbc4b23340df542c","marketTopicId":4229564,"marketId":5567895,"tokenId":"112233","vendor":"PREDICT_FUN","currentShares":"1923.07","avgPrice":"0.52","currentPrice":"0.55","realizedPnl":"0.00","unrealizedPnl":"0.06","totalPnl":"0.06","pnlPercentage":"6.00","isResolved":false}],"totalCount":1,"totalRealizedPnl":"0.00","totalUnrealizedPnl":"0.06","totalPnl":"0.06"}"#).unwrap_or_else(|_| serde_json::json!({}));
818            let expected_response : models::QueryPnLResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPnLResponse");
819
820            let resp = client.query_pn_l(params).await.expect("Expected a response");
821            let data_future = resp.data();
822            let actual_response = data_future.await.unwrap();
823            assert_eq!(actual_response, expected_response);
824        });
825    }
826
827    #[test]
828    fn query_pn_l_response_error() {
829        TOKIO_SHARED_RT.block_on(async {
830            let client = MockPositionApiClient { force_error: true };
831
832            let params =
833                QueryPnLParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string())
834                    .build()
835                    .unwrap();
836
837            match client.query_pn_l(params).await {
838                Ok(_) => panic!("Expected an error"),
839                Err(err) => {
840                    assert_eq!(err.to_string(), "Connector client error: ResponseError");
841                }
842            }
843        });
844    }
845
846    #[test]
847    fn query_positions_required_params_success() {
848        TOKIO_SHARED_RT.block_on(async {
849            let client = MockPositionApiClient { force_error: false };
850
851            let params = QueryPositionsParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).build().unwrap();
852
853            let resp_json: Value = serde_json::from_str(r#"{"summary":{"totalValue":"1523.45","positionValue":"523.45","walletBalance":"1000.00","totalClaimableAmount":"50.00","todayRealizedPnl":"15.30","todayRealizedPnlPercent":"3.10","todayTotalCost":"493.55"},"counts":{"ongoingCount":3,"endedCount":12,"pendingClaimCount":1},"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","toWin":"1923.07","positionStatus":"OPEN","canClaim":false,"endDate":1748134800000,"unrealizedPnl":"0.06","unrealizedPnlPercent":"6.00","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
854            let expected_response : models::QueryPositionsResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPositionsResponse");
855
856            let resp = client.query_positions(params).await.expect("Expected a response");
857            let data_future = resp.data();
858            let actual_response = data_future.await.unwrap();
859            assert_eq!(actual_response, expected_response);
860        });
861    }
862
863    #[test]
864    fn query_positions_optional_params_success() {
865        TOKIO_SHARED_RT.block_on(async {
866            let client = MockPositionApiClient { force_error: false };
867
868            let params = QueryPositionsParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).tab("ONGOING".to_string()).offset(0).limit(20).recv_window(5000).build().unwrap();
869
870            let resp_json: Value = serde_json::from_str(r#"{"summary":{"totalValue":"1523.45","positionValue":"523.45","walletBalance":"1000.00","totalClaimableAmount":"50.00","todayRealizedPnl":"15.30","todayRealizedPnlPercent":"3.10","todayTotalCost":"493.55"},"counts":{"ongoingCount":3,"endedCount":12,"pendingClaimCount":1},"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","toWin":"1923.07","positionStatus":"OPEN","canClaim":false,"endDate":1748134800000,"unrealizedPnl":"0.06","unrealizedPnlPercent":"6.00","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
871            let expected_response : models::QueryPositionsResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPositionsResponse");
872
873            let resp = client.query_positions(params).await.expect("Expected a response");
874            let data_future = resp.data();
875            let actual_response = data_future.await.unwrap();
876            assert_eq!(actual_response, expected_response);
877        });
878    }
879
880    #[test]
881    fn query_positions_response_error() {
882        TOKIO_SHARED_RT.block_on(async {
883            let client = MockPositionApiClient { force_error: true };
884
885            let params = QueryPositionsParams::builder(
886                "0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),
887            )
888            .build()
889            .unwrap();
890
891            match client.query_positions(params).await {
892                Ok(_) => panic!("Expected an error"),
893                Err(err) => {
894                    assert_eq!(err.to_string(), "Connector client error: ResponseError");
895                }
896            }
897        });
898    }
899
900    #[test]
901    fn query_positions_by_filter_required_params_success() {
902        TOKIO_SHARED_RT.block_on(async {
903            let client = MockPositionApiClient { force_error: false };
904
905            let params = QueryPositionsByFilterParams::builder().build().unwrap();
906
907            let resp_json: Value = serde_json::from_str(r#"{"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
908            let expected_response : models::QueryPositionsByFilterResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPositionsByFilterResponse");
909
910            let resp = client.query_positions_by_filter(params).await.expect("Expected a response");
911            let data_future = resp.data();
912            let actual_response = data_future.await.unwrap();
913            assert_eq!(actual_response, expected_response);
914        });
915    }
916
917    #[test]
918    fn query_positions_by_filter_optional_params_success() {
919        TOKIO_SHARED_RT.block_on(async {
920            let client = MockPositionApiClient { force_error: false };
921
922            let params = QueryPositionsByFilterParams::builder().wallet_address("0x12e32db8817e292508c34111cbc4b23340df542c".to_string()).market_topic_id(4229564).recv_window(5000).build().unwrap();
923
924            let resp_json: Value = serde_json::from_str(r#"{"positions":[{"positionId":1001,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112233","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229564,"marketId":5567895,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"1923.07","avgPrice":"0.52","totalCost":"1.00","value":"1.06","currentPrice":"0.55","positionStatus":"OPEN","endDate":1748134800000,"unrealizedPnl":"0.06","realizedPnl":"0.00","pnl":"0.06","createdTime":1748131500000,"updatedTime":1748132000000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
925            let expected_response : models::QueryPositionsByFilterResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QueryPositionsByFilterResponse");
926
927            let resp = client.query_positions_by_filter(params).await.expect("Expected a response");
928            let data_future = resp.data();
929            let actual_response = data_future.await.unwrap();
930            assert_eq!(actual_response, expected_response);
931        });
932    }
933
934    #[test]
935    fn query_positions_by_filter_response_error() {
936        TOKIO_SHARED_RT.block_on(async {
937            let client = MockPositionApiClient { force_error: true };
938
939            let params = QueryPositionsByFilterParams::builder().build().unwrap();
940
941            match client.query_positions_by_filter(params).await {
942                Ok(_) => panic!("Expected an error"),
943                Err(err) => {
944                    assert_eq!(err.to_string(), "Connector client error: ResponseError");
945                }
946            }
947        });
948    }
949
950    #[test]
951    fn query_settled_position_history_required_params_success() {
952        TOKIO_SHARED_RT.block_on(async {
953            let client = MockPositionApiClient { force_error: false };
954
955            let params = QuerySettledPositionHistoryParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).build().unwrap();
956
957            let resp_json: Value = serde_json::from_str(r#"{"totalCount":5,"positions":[{"positionId":998,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112200","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229500,"marketId":5567800,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"5000.00","avgPrice":"0.30","totalCost":"1.50","value":"5000.00","currentPrice":"1.00","toWin":"5000.00","positionStatus":"CLAIMED","isWinner":true,"redeemStatus":"CONFIRMED","endDate":1748040000000,"finalOutcome":"YES","realizedPnl":"4998.50","pnl":"4998.50","claimAmount":"5000.00","settledDate":1748040000000,"createdTime":1748020000000,"updatedTime":1748040500000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
958            let expected_response : models::QuerySettledPositionHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QuerySettledPositionHistoryResponse");
959
960            let resp = client.query_settled_position_history(params).await.expect("Expected a response");
961            let data_future = resp.data();
962            let actual_response = data_future.await.unwrap();
963            assert_eq!(actual_response, expected_response);
964        });
965    }
966
967    #[test]
968    fn query_settled_position_history_optional_params_success() {
969        TOKIO_SHARED_RT.block_on(async {
970            let client = MockPositionApiClient { force_error: false };
971
972            let params = QuerySettledPositionHistoryParams::builder("0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),).l1_category("crypto".to_string()).result(1).start_date("2026-05-01".to_string()).end_date("2026-05-25".to_string()).offset(0).limit(20).recv_window(5000).build().unwrap();
973
974            let resp_json: Value = serde_json::from_str(r#"{"totalCount":5,"positions":[{"positionId":998,"vendor":"PREDICT_FUN","chainId":"56","tokenId":"112200","collateralSymbol":"USDT","topicType":"FLAT","marketTopicId":4229500,"marketId":5567800,"marketTopicTitle":"BTC Price 1h Up or Down?","marketTitle":"UP","outcomeName":"YES","outcomeIndex":0,"shares":"5000.00","avgPrice":"0.30","totalCost":"1.50","value":"5000.00","currentPrice":"1.00","toWin":"5000.00","positionStatus":"CLAIMED","isWinner":true,"redeemStatus":"CONFIRMED","endDate":1748040000000,"finalOutcome":"YES","realizedPnl":"4998.50","pnl":"4998.50","claimAmount":"5000.00","settledDate":1748040000000,"createdTime":1748020000000,"updatedTime":1748040500000}]}"#).unwrap_or_else(|_| serde_json::json!({}));
975            let expected_response : models::QuerySettledPositionHistoryResponse = serde_json::from_value(resp_json.clone()).expect("should parse into models::QuerySettledPositionHistoryResponse");
976
977            let resp = client.query_settled_position_history(params).await.expect("Expected a response");
978            let data_future = resp.data();
979            let actual_response = data_future.await.unwrap();
980            assert_eq!(actual_response, expected_response);
981        });
982    }
983
984    #[test]
985    fn query_settled_position_history_response_error() {
986        TOKIO_SHARED_RT.block_on(async {
987            let client = MockPositionApiClient { force_error: true };
988
989            let params = QuerySettledPositionHistoryParams::builder(
990                "0x12e32db8817e292508c34111cbc4b23340df542c".to_string(),
991            )
992            .build()
993            .unwrap();
994
995            match client.query_settled_position_history(params).await {
996                Ok(_) => panic!("Expected an error"),
997                Err(err) => {
998                    assert_eq!(err.to_string(), "Connector client error: ResponseError");
999                }
1000            }
1001        });
1002    }
1003}