Skip to main content

polyester/models/
trading.rs

1//! Trading read/write models (Go `models/trading.go` parity).
2
3use crate::types::{AssetAmount, Price, Quantity};
4use buffa_types::google::protobuf::Timestamp;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct Order {
8    pub order_id: String,
9    pub symbol_id: u32,
10    pub client_order_id: String,
11    pub side: String,
12    pub status: String,
13    pub order_type: String,
14    pub tif: String,
15    pub orig_qty: Option<Quantity>,
16    pub cum_qty: Option<Quantity>,
17    pub leaves_qty: Option<Quantity>,
18    pub price: Option<Price>,
19    pub avg_px: Option<Price>,
20    pub created_ts_ns: String,
21    pub version: u32,
22    pub post_only: bool,
23    /// Asset selected to pay fees: `quote`, `base`, or an
24    /// `UNKNOWN(<number>)` forward-compatible enum value.
25    pub fee_asset: String,
26    /// Hard all-in quote debit submitted with quote-budget sizing, when used.
27    pub submitted_max_quote_debit_scaled: Option<i64>,
28    /// Attached risk policy when requested via `include_attached_risk`.
29    pub attached_risk: Option<AttachedRisk>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct OrdersList {
34    pub orders: Vec<Order>,
35    pub next_page_token: String,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct OrderMutationResult {
40    pub status: String,
41    pub order_id: String,
42    pub client_order_id: String,
43    /// Gross base quantity resolved by the admission service.
44    pub resolved_base_qty: Option<Quantity>,
45    /// Hard all-in quote debit submitted with quote-budget sizing, when used.
46    pub submitted_max_quote_debit_scaled: Option<i64>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct GetOrderResult {
51    pub order: Option<Order>,
52    pub trades: Vec<UserTrade>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct UserTrade {
57    pub symbol_id: u32,
58    pub match_id: String,
59    pub order_id: String,
60    pub side: String,
61    pub is_maker: bool,
62    pub price: Option<Price>,
63    pub qty: Option<Quantity>,
64    /// Exact fee magnitude in fixed 18-decimal units of `fee_asset`.
65    pub fee_amount_e18: String,
66    /// Asset used to pay/credit the fee: `quote`, `base`, or an
67    /// `UNKNOWN(<number>)` forward-compatible enum value.
68    pub fee_asset: String,
69    /// Exact referral share magnitude in fixed 18-decimal units of `fee_asset`.
70    pub referral_share_amount_e18: String,
71    pub ts_ns: String,
72    /// True when `fee_amount_e18` is a rebate credit instead of a fee debit.
73    /// Proto3 omits false, so sparse wire encoding only sets this for rebates.
74    pub fee_is_rebate: bool,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct UserTradesList {
79    pub trades: Vec<UserTrade>,
80    pub next_page_token: String,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ModifyOrderResult {
85    pub action_taken: String,
86    pub old_order_id: String,
87    pub final_order_id: String,
88    pub code: String,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct CancelAllOrdersResult {
93    pub status: String,
94    pub matched_orders: u32,
95    pub submitted_cancels: u32,
96    pub failed_cancels: u32,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct BatchCreateResultItem {
101    pub status: String,
102    pub order_id: String,
103    pub client_order_id: String,
104    pub code: String,
105}
106
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct BatchCreateOrdersResult {
109    pub results: Vec<BatchCreateResultItem>,
110    pub accepted_count: u32,
111    pub rejected_count: u32,
112}
113
114/// Identifies an order by exactly one of exchange order id or client order id.
115///
116/// Matches TypeScript/Go oneOf semantics for get/cancel/modify and batch items.
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum OrderKey {
119    OrderId(String),
120    ClientOrderId(String),
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct BatchCancelItem {
125    pub key: OrderKey,
126    pub symbol_id: Option<u32>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct BatchCancelResultItem {
131    pub status: String,
132    pub order_id: String,
133    pub client_order_id: String,
134    pub code: String,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct BatchCancelOrdersResult {
139    pub results: Vec<BatchCancelResultItem>,
140    pub accepted_count: u32,
141    pub rejected_count: u32,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct BatchReplaceItem {
146    pub key: OrderKey,
147    pub new_price: Option<Price>,
148    pub new_qty: Option<Quantity>,
149    pub new_attached_risk: Option<AttachedRisk>,
150    pub new_client_order_id: Option<String>,
151}
152
153/// Typed single-order modify params.
154#[derive(Debug, Clone)]
155pub struct ModifyOrderParams {
156    pub symbol: String,
157    pub key: OrderKey,
158    pub subaccount_id: Option<u64>,
159    /// Optional mutation request id (API-required on the wire).
160    ///
161    /// When omitted or blank, the SDK generates a unique id (TypeScript/Go/Python parity).
162    /// Set a stable non-empty value when you may retry the same logical modification after an
163    /// ambiguous failure, and reuse that same value on retry. A blind retry that omits
164    /// `request_id` mints a *new* id and is not an idempotent replay.
165    pub request_id: Option<String>,
166    pub new_price: Option<Price>,
167    pub new_qty: Option<Quantity>,
168    pub new_attached_risk: Option<AttachedRisk>,
169    pub behavior: Option<String>,
170    pub new_client_order_id: Option<String>,
171}
172
173/// Price source requested for trigger evaluation.
174///
175/// Attached order risk currently evaluates against last trade and cannot
176/// encode a caller-selected source. Standalone triggers expose their own
177/// supported semantics.
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179pub enum TriggerPriceSourceKind {
180    LastPrice,
181    IndexPrice,
182    MarkPrice,
183}
184
185/// Take-profit or stop-loss leg (trigger + optional LIMIT child).
186#[derive(Debug, Clone, PartialEq, Eq)]
187pub struct RiskLeg {
188    pub trigger_price: Price,
189    /// Deprecated for attached risk: any supplied value is rejected because
190    /// the wire contract always evaluates against last trade.
191    #[deprecated(
192        note = "attached risk always uses last trade; supplying trigger_price_source is rejected"
193    )]
194    pub trigger_price_source: Option<TriggerPriceSourceKind>,
195    pub order_type: Option<CreateOrderType>,
196    pub limit_price: Option<Price>,
197}
198
199/// Trailing-stop distance (exactly one of ticks or bps).
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201pub enum TrailingDistance {
202    Ticks(i64),
203    Bps(i32),
204}
205
206/// Optional max slippage for trailing-stop MARKET children.
207#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum MaxSlippage {
209    /// Quote ticks (proto field is int32).
210    Ticks(i32),
211    Bps(i32),
212}
213
214/// Trailing-stop attached-risk leg.
215///
216/// Distance and optional max slippage must be positive. The child is always a
217/// market-IOC execution evaluated against last trade; supplying
218/// [`trigger_price_source`](Self::trigger_price_source) or
219/// [`order_type`](Self::order_type) is rejected.
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct TrailingStop {
222    pub distance: TrailingDistance,
223    pub activation_price: Option<Price>,
224    /// Deprecated for attached trailing: any supplied value is rejected because
225    /// the wire contract always evaluates against last trade.
226    #[deprecated(
227        note = "attached trailing always uses last trade; supplying trigger_price_source is rejected"
228    )]
229    pub trigger_price_source: Option<TriggerPriceSourceKind>,
230    /// Deprecated for attached trailing: any supplied value is rejected because
231    /// the child is always an implicit market execution.
232    #[deprecated(
233        note = "attached trailing child is always market; supplying order_type is rejected"
234    )]
235    pub order_type: Option<CreateOrderType>,
236    pub max_slippage: Option<MaxSlippage>,
237}
238
239/// Typed attached risk policy for order create/modify (TP/SL/trailing).
240///
241/// Prefer this over raw proto/`map` escape hatches: trigger/limit prices use [`Price`].
242#[derive(Debug, Clone, Default, PartialEq, Eq)]
243pub struct AttachedRisk {
244    pub take_profit: Option<RiskLeg>,
245    pub stop_loss: Option<RiskLeg>,
246    pub trailing_stop: Option<TrailingStop>,
247    /// When true, take-profit and the stop leg form an OCO pair.
248    pub oco: bool,
249}
250
251/// Typed internal-transfer create params.
252#[derive(Debug, Clone)]
253pub struct CreateInternalTransferParams {
254    pub asset_id: u32,
255    pub quantity: AssetAmount,
256    pub idempotency_key: String,
257    pub subaccount_id: Option<u64>,
258    pub destination_account_id: Option<String>,
259    pub destination_subaccount_id: Option<String>,
260    pub destination_smart_account_address: Option<String>,
261    /// Input quantity scale when `quantity` does not carry one. Wire
262    /// `amount_e18` is always rescaled exactly to 18 decimals.
263    pub quantity_scale: Option<u32>,
264}
265
266/// Typed trading-withdraw create params.
267#[derive(Debug, Clone)]
268pub struct CreateTradingWithdrawParams {
269    pub asset_id: u32,
270    pub amount: AssetAmount,
271    pub payload_signature: Vec<u8>,
272    pub destination_address: String,
273    /// Stable key for this logical withdrawal. Persist it and reuse it for
274    /// every retry; generating a new key per attempt defeats deduplication.
275    pub idempotency_key: String,
276    /// Input amount scale when `amount` does not carry one. Wire `amount_e18`
277    /// is always rescaled exactly to 18 decimals.
278    pub amount_scale: Option<u32>,
279    /// Exact deadline covered by `payload_signature`. Required for this
280    /// precomputed-signature path.
281    pub deadline_ts_sec: Option<u64>,
282    /// Non-zero nonce included in the signed withdrawal payload.
283    pub nonce: u128,
284}
285
286/// API-key trading-withdraw params for SDK-owned payload construction/signing.
287#[derive(Debug, Clone)]
288pub struct CreateApiKeyTradingWithdrawParams {
289    pub asset_id: u32,
290    pub amount: AssetAmount,
291    pub destination_address: String,
292    /// Stable key for this logical withdrawal.
293    pub idempotency_key: String,
294    /// Input amount scale when `amount` does not carry one. Wire `amount_e18`
295    /// is always rescaled exactly to 18 decimals.
296    pub amount_scale: Option<u32>,
297    /// Optional explicit deadline. The SDK uses now + five minutes when absent.
298    pub deadline_ts_sec: Option<u64>,
299    /// Optional explicit nonce. The SDK generates a secure non-zero nonce when absent.
300    pub nonce: Option<u128>,
301}
302
303/// Typed wallet trading-withdraw create params.
304#[derive(Debug, Clone)]
305pub struct CreateWalletTradingWithdrawParams {
306    pub action: String,
307    pub asset_id: u32,
308    pub amount: AssetAmount,
309    pub idempotency_key: String,
310    pub payload_signature: Vec<u8>,
311    pub signer_wallet: String,
312    pub destination_chain_id: u64,
313    pub destination_address: String,
314    pub subaccount_id: Option<u64>,
315    /// Input amount scale when `amount` does not carry one. Wire `amount_e18`
316    /// is always rescaled exactly to 18 decimals.
317    pub amount_scale: Option<u32>,
318    /// Exact deadline covered by `payload_signature`. Required for this
319    /// precomputed-signature path.
320    pub deadline_ts_sec: Option<u64>,
321    /// Non-zero nonce included in the signed withdrawal payload.
322    pub nonce: u128,
323}
324
325#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct BatchReplaceAdmissionItem {
327    pub item_index: u32,
328    pub status: String,
329    pub old_order_id: String,
330    pub client_order_id: String,
331    pub replacement_order_id: String,
332    pub code: String,
333}
334
335#[derive(Debug, Clone, PartialEq, Eq)]
336pub struct BatchReplaceOrdersResult {
337    pub batch_request_id: String,
338    pub status: String,
339    pub results: Vec<BatchReplaceAdmissionItem>,
340    pub accepted_count: u32,
341    pub rejected_count: u32,
342    pub accepted_ts_ns: u64,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq)]
346pub struct BatchReplaceStatusItem {
347    pub item_index: u32,
348    pub phase: String,
349    pub old_order_id: String,
350    pub replacement_order_id: String,
351    pub order_status: String,
352    pub code: String,
353    pub updated_ts_ns: u64,
354}
355
356#[derive(Debug, Clone, PartialEq, Eq)]
357pub struct BatchReplaceStatusResult {
358    pub batch_request_id: String,
359    pub admission_status: String,
360    pub items: Vec<BatchReplaceStatusItem>,
361    pub accepted_count: u32,
362    pub rejected_count: u32,
363    pub accepted_ts_ns: u64,
364    pub updated_ts_ns: u64,
365}
366
367impl BatchReplaceStatusResult {
368    /// Returns true once every item has left admission processing.
369    ///
370    /// `working` means the replacement is live, not that it has reached an
371    /// execution terminal state. Continue polling/reconciling order state when
372    /// execution finality is required.
373    pub fn is_settled(&self) -> bool {
374        is_batch_replace_settled(self)
375    }
376}
377
378/// Returns true once every batch-replace item is `working`, `rejected`, or
379/// `terminal`. An empty status is not considered settled.
380pub fn is_batch_replace_settled(status: &BatchReplaceStatusResult) -> bool {
381    !status.items.is_empty()
382        && status
383            .items
384            .iter()
385            .all(|item| matches!(item.phase.as_str(), "working" | "rejected" | "terminal"))
386}
387
388#[derive(Debug, Clone, PartialEq, Eq)]
389pub struct CancelAllAfterResult {
390    pub status: String,
391    pub effective_timeout_sec: u32,
392    pub expires_at_ts_ns: String,
393}
394
395/// Options for [`crate::services::OrdersService::list_open_with`].
396#[derive(Debug, Clone, Default)]
397pub struct ListOpenOrdersOpts {
398    pub subaccount_id: Option<u64>,
399    pub page_token: Option<String>,
400    pub limit: Option<u32>,
401    pub include_attached_risk: bool,
402    pub include_attached_risk_state: bool,
403}
404
405/// Options for [`crate::services::OrdersService::list_history_with`].
406#[derive(Debug, Clone, Default)]
407pub struct ListOrderHistoryOpts {
408    pub subaccount_id: Option<u64>,
409    pub symbol: Option<String>,
410    pub symbol_id: Option<u32>,
411    pub page_token: Option<String>,
412    pub limit: Option<u32>,
413    pub include_attached_risk: bool,
414    pub include_attached_risk_state: bool,
415}
416
417/// Options for [`crate::services::OrdersService::get_with`].
418#[derive(Debug, Clone)]
419pub struct GetOrderOpts {
420    pub key: OrderKey,
421    pub subaccount_id: Option<u64>,
422    pub include_attached_risk: bool,
423    pub include_attached_risk_state: bool,
424}
425
426/// Params for [`crate::services::OrdersService::cancel_with`].
427#[derive(Debug, Clone)]
428pub struct CancelOrderParams {
429    pub key: OrderKey,
430    pub symbol: Option<String>,
431    pub symbol_id: Option<u32>,
432    pub subaccount_id: Option<u64>,
433}
434
435/// Options for [`crate::services::OrdersService::cancel_all_with`].
436#[derive(Debug, Clone, Default)]
437pub struct CancelAllOpts {
438    pub symbol: Option<String>,
439    pub dry_run: bool,
440    pub subaccount_id: Option<u64>,
441    pub side: Option<String>,
442    /// Optional mutation request id (API-required on the wire).
443    ///
444    /// When omitted or blank, the SDK generates a unique id (TypeScript/Go/Python parity).
445    /// Set a stable non-empty value when you may retry the same logical cancel-all after an
446    /// ambiguous failure, and reuse that same value on retry. A blind retry that omits
447    /// `request_id` mints a *new* id and is not an idempotent replay.
448    pub request_id: Option<String>,
449}
450
451#[derive(Debug, Clone)]
452pub struct CreateOrderParams {
453    pub symbol: String,
454    pub side: CreateSide,
455    pub order_type: CreateOrderType,
456    /// Base quantity. Set exactly one of this and `max_quote_debit_scaled`.
457    pub quantity: Option<Quantity>,
458    /// Hard all-in quote debit limit. The [`Quantity`] must use
459    /// [`crate::types::QuantityDomain::OrderQuote`] and carry the pair's
460    /// catalog quote scale. Set exactly one of this and `quantity`.
461    pub max_quote_debit_scaled: Option<Quantity>,
462    pub price: Option<Price>,
463    pub time_in_force: Option<CreateTimeInForce>,
464    /// Optional client order id (API-optional).
465    ///
466    /// Set a stable non-empty value when you may retry after an ambiguous failure
467    /// (`Error::mutation_outcome_unknown`), and reuse that same value on retry.
468    /// Omit (`None`) for one-shot creates where you will not reconcile by client id.
469    pub client_order_id: Option<String>,
470    pub subaccount_id: Option<u64>,
471    pub post_only: Option<bool>,
472    /// Client reference price for MARKET order reservation (price ticks domain).
473    pub market_client_ref_price: Option<Price>,
474    /// Fee asset. `Base` is valid only for BUY orders; SELL orders use `Quote`.
475    pub fee_asset: Option<FeeAsset>,
476    /// Self-trade prevention policy for this order.
477    pub self_trade_prevention: Option<OrderSelfTradePrevention>,
478    /// Optional market-order slippage guard.
479    pub market_max_slippage: Option<MaxSlippage>,
480    /// Optional TP/SL/trailing controls that arm after the parent fills.
481    pub attached_risk: Option<AttachedRisk>,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
485pub enum FeeAsset {
486    Quote,
487    Base,
488}
489
490/// Legacy name for [`FeeAsset`].
491///
492/// `Received` was removed by the API contract; use `FeeAsset::Base` for a
493/// BUY fee deducted from received base quantity.
494#[deprecated(note = "renamed to FeeAsset; use FeeAsset::Base instead of the removed Received")]
495pub type OrderFeeSource = FeeAsset;
496
497/// Order inputs accepted by [`crate::services::OrdersService::preview`].
498///
499/// Preview uses the same [`OrderIntent`](crate::proto::orders::v1::OrderIntent)
500/// contract as create. The host performs an admissibility check only: no hold is
501/// placed, and `client_order_id` is accepted but not claimed.
502#[derive(Debug, Clone)]
503pub struct PreviewOrderParams {
504    pub symbol: String,
505    pub side: CreateSide,
506    pub order_type: CreateOrderType,
507    /// Base quantity. Set exactly one of this and `max_quote_debit_scaled`.
508    pub quantity: Option<Quantity>,
509    /// Hard all-in quote debit limit. The [`Quantity`] must use
510    /// [`crate::types::QuantityDomain::OrderQuote`] and carry the pair's
511    /// catalog quote scale. Set exactly one of this and `quantity`.
512    pub max_quote_debit_scaled: Option<Quantity>,
513    pub price: Option<Price>,
514    pub time_in_force: Option<CreateTimeInForce>,
515    /// Optional client order id. Accepted for shape parity with create; preview
516    /// does not claim it.
517    pub client_order_id: Option<String>,
518    pub subaccount_id: Option<u64>,
519    pub post_only: Option<bool>,
520    pub market_client_ref_price: Option<Price>,
521    pub fee_asset: Option<FeeAsset>,
522    pub self_trade_prevention: Option<OrderSelfTradePrevention>,
523    pub market_max_slippage: Option<MaxSlippage>,
524    /// Optional TP/SL/trailing controls. Preview validates the full intent;
525    /// nothing is armed until a subsequent create.
526    pub attached_risk: Option<AttachedRisk>,
527}
528
529/// One actionable field-level validation failure from preview/create rejection.
530#[derive(Debug, Clone, PartialEq, Eq)]
531pub struct OrderFieldViolation {
532    pub field_path: String,
533    pub rule_id: String,
534    pub message: String,
535}
536
537/// Typed rejection detail when a preview (or related) admission check fails.
538#[derive(Debug, Clone, PartialEq, Eq)]
539pub struct OrderErrorDetail {
540    /// Public error code label (for example `BAD_QTY`), or
541    /// `UNKNOWN_ERROR_CODE(<n>)` for open-enum forward compatibility.
542    pub code: String,
543    pub violations: Vec<OrderFieldViolation>,
544}
545
546/// Advisory admission result for [`crate::services::OrdersService::preview`].
547///
548/// Preview no longer returns fee/quote estimates. It reports whether the intent
549/// is currently admissible, any typed rejection, and any sizing / price-
550/// protection values resolved during evaluation.
551#[derive(Debug, Clone, PartialEq, Eq)]
552pub struct PreviewOrderResult {
553    pub admissible: Option<bool>,
554    pub rejection: Option<OrderErrorDetail>,
555    pub resolved_base_qty: Option<Quantity>,
556    /// Protective execution boundary (renamed from `price_bound`).
557    pub protected_price_bound: Option<Price>,
558    /// Evaluation completion time as epoch milliseconds.
559    pub evaluated_at_ms: i64,
560}
561
562#[derive(Debug, Clone, Copy, PartialEq, Eq)]
563pub enum OrderSelfTradePrevention {
564    ExpireTaker,
565    ExpireMaker,
566    ExpireBoth,
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
570pub enum CreateSide {
571    Buy,
572    Sell,
573}
574
575#[derive(Debug, Clone, Copy, PartialEq, Eq)]
576pub enum CreateOrderType {
577    Limit,
578    Market,
579}
580
581#[derive(Debug, Clone, Copy, PartialEq, Eq)]
582pub enum CreateTimeInForce {
583    Gtc,
584    Ioc,
585    Fok,
586}
587
588#[derive(Debug, Clone, PartialEq, Eq)]
589pub struct InternalTransferResult {
590    pub request_id: String,
591    pub transfer_id: String,
592    pub asset_id: u32,
593    pub asset_code: String,
594    pub quantity: Option<AssetAmount>,
595}
596
597#[derive(Debug, Clone, PartialEq, Eq)]
598pub struct DepositAddress {
599    pub chain_id: u32,
600    pub deposit_address: String,
601}
602
603#[derive(Debug, Clone, PartialEq, Eq)]
604pub struct DepositAddressesList {
605    pub addresses: Vec<DepositAddress>,
606}
607
608#[derive(Debug, Clone, PartialEq, Eq)]
609pub struct WithdrawIntentResult {
610    pub intent_id: String,
611    pub status: String,
612    pub flow_id: String,
613}
614
615#[derive(Debug, Clone, PartialEq)]
616pub struct ApiKeySummary {
617    pub key_id: String,
618    pub label: String,
619    pub status: String,
620    pub public_key_ed25519: String,
621    pub created_at: Option<Timestamp>,
622    pub last_used_at: Option<Timestamp>,
623    pub updated_at: Option<Timestamp>,
624    /// Monotonic resource revision for conditional updates.
625    pub revision: u64,
626}
627
628#[derive(Debug, Clone, PartialEq)]
629pub struct ApiKeysList {
630    pub keys: Vec<ApiKeySummary>,
631}
632
633#[derive(Debug, Clone, PartialEq, Eq)]
634pub struct ResolvedAccount {
635    pub account_id: String,
636    pub username: String,
637    pub smart_account_address: String,
638}
639
640#[derive(Debug, Clone, PartialEq, Eq)]
641pub struct ResolvedAccountsList {
642    pub accounts: Vec<ResolvedAccount>,
643}