Skip to main content

rust_okx/api/account/requests/
risk.rs

1use serde::Serialize;
2
3use crate::model::{
4    InstType, RequestValidationError, ValidateRequest, at_least_one, non_empty, optional_non_empty,
5    optional_one_of, optional_positive_decimal_string,
6};
7
8/// A simulated position used by position-builder and simulated-margin requests.
9#[derive(Debug, Clone, Serialize)]
10pub struct SimulatedPosition {
11    #[serde(rename = "instId")]
12    inst_id: String,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pos: Option<String>,
15    #[serde(rename = "avgPx", skip_serializing_if = "Option::is_none")]
16    avg_px: Option<String>,
17    #[serde(skip_serializing_if = "Option::is_none")]
18    lever: Option<String>,
19}
20
21impl SimulatedPosition {
22    /// Create a simulated position for an instrument.
23    pub fn new(inst_id: impl Into<String>) -> Self {
24        Self {
25            inst_id: inst_id.into(),
26            pos: None,
27            avg_px: None,
28            lever: None,
29        }
30    }
31
32    /// Set the simulated position size.
33    pub fn position(mut self, pos: impl Into<String>) -> Self {
34        self.pos = Some(pos.into());
35        self
36    }
37
38    /// Set the simulated average price.
39    pub fn average_price(mut self, avg_px: impl Into<String>) -> Self {
40        self.avg_px = Some(avg_px.into());
41        self
42    }
43
44    /// Set the simulated leverage.
45    pub fn leverage(mut self, lever: impl Into<String>) -> Self {
46        self.lever = Some(lever.into());
47        self
48    }
49}
50
51/// A simulated asset used by position-builder requests.
52#[derive(Debug, Clone, Serialize)]
53pub struct SimulatedAsset {
54    ccy: String,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    eq: Option<String>,
57}
58
59impl SimulatedAsset {
60    /// Create a simulated asset for a currency.
61    pub fn new(ccy: impl Into<String>) -> Self {
62        Self {
63            ccy: ccy.into(),
64            eq: None,
65        }
66    }
67
68    /// Set the simulated equity.
69    pub fn equity(mut self, eq: impl Into<String>) -> Self {
70        self.eq = Some(eq.into());
71        self
72    }
73}
74
75/// Request body for simulated margin calculation.
76#[derive(Debug, Clone, Default, Serialize)]
77pub struct SimulatedMarginRequest {
78    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
79    inst_type: Option<InstType>,
80    #[serde(rename = "inclRealPos", skip_serializing_if = "Option::is_none")]
81    include_real_positions: Option<bool>,
82    #[serde(rename = "spotOffsetType", skip_serializing_if = "Option::is_none")]
83    spot_offset_type: Option<String>,
84    #[serde(rename = "simPos", skip_serializing_if = "Option::is_none")]
85    simulated_positions: Option<Vec<SimulatedPosition>>,
86}
87
88impl SimulatedMarginRequest {
89    /// Create an empty simulated-margin request.
90    pub fn new() -> Self {
91        Self::default()
92    }
93
94    /// Set the instrument type.
95    pub fn inst_type(mut self, inst_type: InstType) -> Self {
96        self.inst_type = Some(inst_type);
97        self
98    }
99
100    /// Set whether real positions and equity are included.
101    pub fn include_real_positions(mut self, include_real_positions: bool) -> Self {
102        self.include_real_positions = Some(include_real_positions);
103        self
104    }
105
106    /// Set the spot offset type.
107    pub fn spot_offset_type(mut self, spot_offset_type: impl Into<String>) -> Self {
108        self.spot_offset_type = Some(spot_offset_type.into());
109        self
110    }
111
112    /// Set simulated positions.
113    pub fn simulated_positions(mut self, simulated_positions: Vec<SimulatedPosition>) -> Self {
114        self.simulated_positions = Some(simulated_positions);
115        self
116    }
117}
118
119/// Query parameters for account position tiers.
120#[derive(Debug, Clone, Default, Serialize)]
121pub struct AccountPositionTiersRequest {
122    #[serde(rename = "instType", skip_serializing_if = "Option::is_none")]
123    inst_type: Option<InstType>,
124    #[serde(rename = "uly", skip_serializing_if = "Option::is_none")]
125    underlying: Option<String>,
126    #[serde(rename = "instFamily", skip_serializing_if = "Option::is_none")]
127    inst_family: Option<String>,
128}
129
130impl AccountPositionTiersRequest {
131    /// Create an empty account position-tiers query.
132    pub fn new() -> Self {
133        Self::default()
134    }
135
136    /// Set the instrument type filter.
137    pub fn inst_type(mut self, inst_type: InstType) -> Self {
138        self.inst_type = Some(inst_type);
139        self
140    }
141
142    /// Set the underlying filter.
143    pub fn underlying(mut self, underlying: impl Into<String>) -> Self {
144        self.underlying = Some(underlying.into());
145        self
146    }
147
148    /// Set the instrument family filter.
149    pub fn inst_family(mut self, inst_family: impl Into<String>) -> Self {
150        self.inst_family = Some(inst_family.into());
151        self
152    }
153}
154
155/// Request body for position builder.
156#[derive(Debug, Clone, Default, Serialize)]
157pub struct PositionBuilderRequest {
158    #[serde(rename = "acctLv", skip_serializing_if = "Option::is_none")]
159    acct_lv: Option<String>,
160    #[serde(rename = "inclRealPosAndEq", skip_serializing_if = "Option::is_none")]
161    include_real_positions_and_equity: Option<bool>,
162    #[serde(skip_serializing_if = "Option::is_none")]
163    lever: Option<String>,
164    #[serde(rename = "greeksType", skip_serializing_if = "Option::is_none")]
165    greeks_type: Option<String>,
166    #[serde(rename = "simPos", skip_serializing_if = "Option::is_none")]
167    simulated_positions: Option<Vec<SimulatedPosition>>,
168    #[serde(rename = "simAsset", skip_serializing_if = "Option::is_none")]
169    simulated_assets: Option<Vec<SimulatedAsset>>,
170    #[serde(rename = "idxVol", skip_serializing_if = "Option::is_none")]
171    index_volatility: Option<String>,
172}
173
174impl PositionBuilderRequest {
175    /// Create an empty position-builder request.
176    pub fn new() -> Self {
177        Self::default()
178    }
179
180    /// Set the account level.
181    pub fn account_level(mut self, acct_lv: impl Into<String>) -> Self {
182        self.acct_lv = Some(acct_lv.into());
183        self
184    }
185
186    /// Set whether real positions and equity are included.
187    pub fn include_real_positions_and_equity(mut self, include: bool) -> Self {
188        self.include_real_positions_and_equity = Some(include);
189        self
190    }
191
192    /// Set leverage.
193    pub fn leverage(mut self, lever: impl Into<String>) -> Self {
194        self.lever = Some(lever.into());
195        self
196    }
197
198    /// Set greeks display type.
199    pub fn greeks_type(mut self, greeks_type: impl Into<String>) -> Self {
200        self.greeks_type = Some(greeks_type.into());
201        self
202    }
203
204    /// Set simulated positions.
205    pub fn simulated_positions(mut self, simulated_positions: Vec<SimulatedPosition>) -> Self {
206        self.simulated_positions = Some(simulated_positions);
207        self
208    }
209
210    /// Set simulated assets.
211    pub fn simulated_assets(mut self, simulated_assets: Vec<SimulatedAsset>) -> Self {
212        self.simulated_assets = Some(simulated_assets);
213        self
214    }
215
216    /// Set index volatility.
217    pub fn index_volatility(mut self, index_volatility: impl Into<String>) -> Self {
218        self.index_volatility = Some(index_volatility.into());
219        self
220    }
221}
222
223impl SimulatedPosition {
224    fn validate(&self) -> Result<(), RequestValidationError> {
225        non_empty("simPos.instId", &self.inst_id)?;
226        optional_non_empty("simPos.pos", self.pos.as_deref())?;
227        optional_positive_decimal_string("simPos.avgPx", self.avg_px.as_deref())?;
228        optional_positive_decimal_string("simPos.lever", self.lever.as_deref())?;
229        Ok(())
230    }
231}
232
233impl SimulatedAsset {
234    fn validate(&self) -> Result<(), RequestValidationError> {
235        non_empty("simAsset.ccy", &self.ccy)?;
236        optional_non_empty("simAsset.eq", self.eq.as_deref())
237    }
238}
239
240impl ValidateRequest for SimulatedMarginRequest {
241    fn validate(&self) -> Result<(), RequestValidationError> {
242        if matches!(self.inst_type, Some(InstType::Unknown(_))) {
243            return Err(RequestValidationError::InvalidFormat {
244                field: "instType",
245                expected: "SPOT, MARGIN, SWAP, FUTURES, OPTION, or EVENTS",
246            });
247        }
248        optional_one_of(
249            "spotOffsetType",
250            self.spot_offset_type.as_deref(),
251            &["1", "2", "3"],
252            "1, 2, or 3",
253        )?;
254        if self.include_real_positions == Some(false) && self.simulated_positions.is_none() {
255            return Err(RequestValidationError::RequiredWhen {
256                field: "simPos",
257                condition: "inclRealPos is false",
258            });
259        }
260        if let Some(positions) = &self.simulated_positions {
261            if positions.is_empty() {
262                return Err(RequestValidationError::EmptyField { field: "simPos" });
263            }
264            for position in positions {
265                position.validate()?;
266            }
267        }
268        Ok(())
269    }
270}
271
272impl ValidateRequest for AccountPositionTiersRequest {
273    fn validate(&self) -> Result<(), RequestValidationError> {
274        match &self.inst_type {
275            Some(InstType::Swap | InstType::Futures | InstType::Option) => {}
276            Some(_) => {
277                return Err(RequestValidationError::InvalidFormat {
278                    field: "instType",
279                    expected: "SWAP, FUTURES, or OPTION",
280                });
281            }
282            None => {
283                return Err(RequestValidationError::RequiredWhen {
284                    field: "instType",
285                    condition: "querying account position tiers",
286                });
287            }
288        }
289        optional_non_empty("uly", self.underlying.as_deref())?;
290        optional_non_empty("instFamily", self.inst_family.as_deref())?;
291        at_least_one(
292            "uly, instFamily",
293            &[self.underlying.is_some(), self.inst_family.is_some()],
294        )
295    }
296}
297
298impl ValidateRequest for PositionBuilderRequest {
299    fn validate(&self) -> Result<(), RequestValidationError> {
300        optional_one_of(
301            "acctLv",
302            self.acct_lv.as_deref(),
303            &["1", "2", "3", "4"],
304            "1, 2, 3, or 4",
305        )?;
306        optional_positive_decimal_string("lever", self.lever.as_deref())?;
307        optional_one_of(
308            "greeksType",
309            self.greeks_type.as_deref(),
310            &["PA", "BS"],
311            "PA or BS",
312        )?;
313        optional_positive_decimal_string("idxVol", self.index_volatility.as_deref())?;
314        if let Some(positions) = &self.simulated_positions {
315            if positions.is_empty() {
316                return Err(RequestValidationError::EmptyField { field: "simPos" });
317            }
318            for position in positions {
319                position.validate()?;
320            }
321        }
322        if let Some(assets) = &self.simulated_assets {
323            if assets.is_empty() {
324                return Err(RequestValidationError::EmptyField { field: "simAsset" });
325            }
326            for asset in assets {
327                asset.validate()?;
328            }
329        }
330        if self.include_real_positions_and_equity != Some(true) {
331            at_least_one(
332                "simPos, simAsset",
333                &[
334                    self.simulated_positions.is_some(),
335                    self.simulated_assets.is_some(),
336                ],
337            )?;
338        }
339        Ok(())
340    }
341}