Skip to main content

oanda_rs/models/transaction/
mod.rs

1//! Transaction models: the `Transaction` discriminated union (36 subtypes)
2//! and its supporting enums.
3
4mod account_tx;
5mod fill_cancel_tx;
6mod misc_tx;
7mod order_tx;
8
9pub use account_tx::*;
10pub use fill_cancel_tx::*;
11pub use misc_tx::*;
12pub use order_tx::*;
13
14use serde::{Deserialize, Serialize};
15
16use crate::models::{AccountId, DateTime, RequestId, TransactionId};
17
18/// Internal helper running the same expression against the inner value of
19/// every known [`Transaction`] variant.
20macro_rules! for_each_transaction {
21    ($tx:expr, $inner:ident => $body:expr, $unknown:expr) => {
22        match $tx {
23            Transaction::Create($inner) => $body,
24            Transaction::Close($inner) => $body,
25            Transaction::Reopen($inner) => $body,
26            Transaction::ClientConfigure($inner) => $body,
27            Transaction::ClientConfigureReject($inner) => $body,
28            Transaction::TransferFunds($inner) => $body,
29            Transaction::TransferFundsReject($inner) => $body,
30            Transaction::MarketOrder($inner) => $body,
31            Transaction::MarketOrderReject($inner) => $body,
32            Transaction::FixedPriceOrder($inner) => $body,
33            Transaction::LimitOrder($inner) => $body,
34            Transaction::LimitOrderReject($inner) => $body,
35            Transaction::StopOrder($inner) => $body,
36            Transaction::StopOrderReject($inner) => $body,
37            Transaction::MarketIfTouchedOrder($inner) => $body,
38            Transaction::MarketIfTouchedOrderReject($inner) => $body,
39            Transaction::TakeProfitOrder($inner) => $body,
40            Transaction::TakeProfitOrderReject($inner) => $body,
41            Transaction::StopLossOrder($inner) => $body,
42            Transaction::StopLossOrderReject($inner) => $body,
43            Transaction::TrailingStopLossOrder($inner) => $body,
44            Transaction::TrailingStopLossOrderReject($inner) => $body,
45            Transaction::OrderFill($inner) => $body,
46            Transaction::OrderCancel($inner) => $body,
47            Transaction::OrderCancelReject($inner) => $body,
48            Transaction::OrderClientExtensionsModify($inner) => $body,
49            Transaction::OrderClientExtensionsModifyReject($inner) => $body,
50            Transaction::TradeClientExtensionsModify($inner) => $body,
51            Transaction::TradeClientExtensionsModifyReject($inner) => $body,
52            Transaction::MarginCallEnter($inner) => $body,
53            Transaction::MarginCallExtend($inner) => $body,
54            Transaction::MarginCallExit($inner) => $body,
55            Transaction::DelayedTradeClosure($inner) => $body,
56            Transaction::DailyFinancing($inner) => $body,
57            Transaction::ResetResettablePL($inner) => $body,
58            Transaction::DividendAdjustment($inner) => $body,
59            Transaction::Unknown(_) => $unknown,
60        }
61    };
62}
63
64/// A single change to an account's state, discriminated by its `type`
65/// field.
66///
67/// Transactions of a type unknown to this SDK deserialize into
68/// [`Transaction::Unknown`] with the raw JSON preserved, so new
69/// transaction types on OANDA's side never break deserialization.
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71#[serde(tag = "type")]
72#[non_exhaustive]
73#[allow(clippy::large_enum_variant)] // variants mirror the wire format; boxing would hurt ergonomics
74pub enum Transaction {
75    /// A `CREATE` transaction.
76    #[serde(rename = "CREATE")]
77    Create(CreateTransaction),
78    /// A `CLOSE` transaction.
79    #[serde(rename = "CLOSE")]
80    Close(CloseTransaction),
81    /// A `REOPEN` transaction.
82    #[serde(rename = "REOPEN")]
83    Reopen(ReopenTransaction),
84    /// A `CLIENT_CONFIGURE` transaction.
85    #[serde(rename = "CLIENT_CONFIGURE")]
86    ClientConfigure(ClientConfigureTransaction),
87    /// A `CLIENT_CONFIGURE_REJECT` transaction.
88    #[serde(rename = "CLIENT_CONFIGURE_REJECT")]
89    ClientConfigureReject(ClientConfigureRejectTransaction),
90    /// A `TRANSFER_FUNDS` transaction.
91    #[serde(rename = "TRANSFER_FUNDS")]
92    TransferFunds(TransferFundsTransaction),
93    /// A `TRANSFER_FUNDS_REJECT` transaction.
94    #[serde(rename = "TRANSFER_FUNDS_REJECT")]
95    TransferFundsReject(TransferFundsRejectTransaction),
96    /// A `MARKET_ORDER` transaction.
97    #[serde(rename = "MARKET_ORDER")]
98    MarketOrder(MarketOrderTransaction),
99    /// A `MARKET_ORDER_REJECT` transaction.
100    #[serde(rename = "MARKET_ORDER_REJECT")]
101    MarketOrderReject(MarketOrderRejectTransaction),
102    /// A `FIXED_PRICE_ORDER` transaction.
103    #[serde(rename = "FIXED_PRICE_ORDER")]
104    FixedPriceOrder(FixedPriceOrderTransaction),
105    /// A `LIMIT_ORDER` transaction.
106    #[serde(rename = "LIMIT_ORDER")]
107    LimitOrder(LimitOrderTransaction),
108    /// A `LIMIT_ORDER_REJECT` transaction.
109    #[serde(rename = "LIMIT_ORDER_REJECT")]
110    LimitOrderReject(LimitOrderRejectTransaction),
111    /// A `STOP_ORDER` transaction.
112    #[serde(rename = "STOP_ORDER")]
113    StopOrder(StopOrderTransaction),
114    /// A `STOP_ORDER_REJECT` transaction.
115    #[serde(rename = "STOP_ORDER_REJECT")]
116    StopOrderReject(StopOrderRejectTransaction),
117    /// A `MARKET_IF_TOUCHED_ORDER` transaction.
118    #[serde(rename = "MARKET_IF_TOUCHED_ORDER")]
119    MarketIfTouchedOrder(MarketIfTouchedOrderTransaction),
120    /// A `MARKET_IF_TOUCHED_ORDER_REJECT` transaction.
121    #[serde(rename = "MARKET_IF_TOUCHED_ORDER_REJECT")]
122    MarketIfTouchedOrderReject(MarketIfTouchedOrderRejectTransaction),
123    /// A `TAKE_PROFIT_ORDER` transaction.
124    #[serde(rename = "TAKE_PROFIT_ORDER")]
125    TakeProfitOrder(TakeProfitOrderTransaction),
126    /// A `TAKE_PROFIT_ORDER_REJECT` transaction.
127    #[serde(rename = "TAKE_PROFIT_ORDER_REJECT")]
128    TakeProfitOrderReject(TakeProfitOrderRejectTransaction),
129    /// A `STOP_LOSS_ORDER` transaction.
130    #[serde(rename = "STOP_LOSS_ORDER")]
131    StopLossOrder(StopLossOrderTransaction),
132    /// A `STOP_LOSS_ORDER_REJECT` transaction.
133    #[serde(rename = "STOP_LOSS_ORDER_REJECT")]
134    StopLossOrderReject(StopLossOrderRejectTransaction),
135    /// A `TRAILING_STOP_LOSS_ORDER` transaction.
136    #[serde(rename = "TRAILING_STOP_LOSS_ORDER")]
137    TrailingStopLossOrder(TrailingStopLossOrderTransaction),
138    /// A `TRAILING_STOP_LOSS_ORDER_REJECT` transaction.
139    #[serde(rename = "TRAILING_STOP_LOSS_ORDER_REJECT")]
140    TrailingStopLossOrderReject(TrailingStopLossOrderRejectTransaction),
141    /// A `ORDER_FILL` transaction.
142    #[serde(rename = "ORDER_FILL")]
143    OrderFill(OrderFillTransaction),
144    /// A `ORDER_CANCEL` transaction.
145    #[serde(rename = "ORDER_CANCEL")]
146    OrderCancel(OrderCancelTransaction),
147    /// A `ORDER_CANCEL_REJECT` transaction.
148    #[serde(rename = "ORDER_CANCEL_REJECT")]
149    OrderCancelReject(OrderCancelRejectTransaction),
150    /// A `ORDER_CLIENT_EXTENSIONS_MODIFY` transaction.
151    #[serde(rename = "ORDER_CLIENT_EXTENSIONS_MODIFY")]
152    OrderClientExtensionsModify(OrderClientExtensionsModifyTransaction),
153    /// A `ORDER_CLIENT_EXTENSIONS_MODIFY_REJECT` transaction.
154    #[serde(rename = "ORDER_CLIENT_EXTENSIONS_MODIFY_REJECT")]
155    OrderClientExtensionsModifyReject(OrderClientExtensionsModifyRejectTransaction),
156    /// A `TRADE_CLIENT_EXTENSIONS_MODIFY` transaction.
157    #[serde(rename = "TRADE_CLIENT_EXTENSIONS_MODIFY")]
158    TradeClientExtensionsModify(TradeClientExtensionsModifyTransaction),
159    /// A `TRADE_CLIENT_EXTENSIONS_MODIFY_REJECT` transaction.
160    #[serde(rename = "TRADE_CLIENT_EXTENSIONS_MODIFY_REJECT")]
161    TradeClientExtensionsModifyReject(TradeClientExtensionsModifyRejectTransaction),
162    /// A `MARGIN_CALL_ENTER` transaction.
163    #[serde(rename = "MARGIN_CALL_ENTER")]
164    MarginCallEnter(MarginCallEnterTransaction),
165    /// A `MARGIN_CALL_EXTEND` transaction.
166    #[serde(rename = "MARGIN_CALL_EXTEND")]
167    MarginCallExtend(MarginCallExtendTransaction),
168    /// A `MARGIN_CALL_EXIT` transaction.
169    #[serde(rename = "MARGIN_CALL_EXIT")]
170    MarginCallExit(MarginCallExitTransaction),
171    /// A `DELAYED_TRADE_CLOSURE` transaction.
172    #[serde(rename = "DELAYED_TRADE_CLOSURE")]
173    DelayedTradeClosure(DelayedTradeClosureTransaction),
174    /// A `DAILY_FINANCING` transaction.
175    #[serde(rename = "DAILY_FINANCING")]
176    DailyFinancing(DailyFinancingTransaction),
177    /// A `RESET_RESETTABLE_PL` transaction.
178    #[serde(rename = "RESET_RESETTABLE_PL")]
179    ResetResettablePL(ResetResettablePLTransaction),
180    /// A `DIVIDEND_ADJUSTMENT` transaction.
181    #[serde(rename = "DIVIDEND_ADJUSTMENT")]
182    DividendAdjustment(DividendAdjustmentTransaction),
183    /// A transaction of a type not (yet) known to this SDK; the raw JSON
184    /// object is preserved.
185    #[serde(untagged)]
186    Unknown(serde_json::Value),
187}
188
189impl Transaction {
190    /// The transaction's `id`, when known.
191    pub fn id(&self) -> Option<&TransactionId> {
192        for_each_transaction!(self, t => t.id.as_ref(), None)
193    }
194
195    /// The transaction's `time`, when known.
196    pub fn time(&self) -> Option<&DateTime> {
197        for_each_transaction!(self, t => t.time.as_ref(), None)
198    }
199
200    /// The transaction's `account_id`, when known.
201    pub fn account_id(&self) -> Option<&AccountId> {
202        for_each_transaction!(self, t => t.account_id.as_ref(), None)
203    }
204
205    /// The transaction's `batch_id`, when known.
206    pub fn batch_id(&self) -> Option<&TransactionId> {
207        for_each_transaction!(self, t => t.batch_id.as_ref(), None)
208    }
209
210    /// The transaction's `request_id`, when known.
211    pub fn request_id(&self) -> Option<&RequestId> {
212        for_each_transaction!(self, t => t.request_id.as_ref(), None)
213    }
214
215    /// The ID of the user that initiated the transaction, when known.
216    pub fn user_id(&self) -> Option<i64> {
217        for_each_transaction!(self, t => t.user_id, None)
218    }
219
220    /// The wire name of the transaction's type (e.g. `ORDER_FILL`), or
221    /// the raw `type` value for unknown transactions.
222    pub fn type_name(&self) -> Option<&str> {
223        match self {
224            Transaction::Create(_) => Some("CREATE"),
225            Transaction::Close(_) => Some("CLOSE"),
226            Transaction::Reopen(_) => Some("REOPEN"),
227            Transaction::ClientConfigure(_) => Some("CLIENT_CONFIGURE"),
228            Transaction::ClientConfigureReject(_) => Some("CLIENT_CONFIGURE_REJECT"),
229            Transaction::TransferFunds(_) => Some("TRANSFER_FUNDS"),
230            Transaction::TransferFundsReject(_) => Some("TRANSFER_FUNDS_REJECT"),
231            Transaction::MarketOrder(_) => Some("MARKET_ORDER"),
232            Transaction::MarketOrderReject(_) => Some("MARKET_ORDER_REJECT"),
233            Transaction::FixedPriceOrder(_) => Some("FIXED_PRICE_ORDER"),
234            Transaction::LimitOrder(_) => Some("LIMIT_ORDER"),
235            Transaction::LimitOrderReject(_) => Some("LIMIT_ORDER_REJECT"),
236            Transaction::StopOrder(_) => Some("STOP_ORDER"),
237            Transaction::StopOrderReject(_) => Some("STOP_ORDER_REJECT"),
238            Transaction::MarketIfTouchedOrder(_) => Some("MARKET_IF_TOUCHED_ORDER"),
239            Transaction::MarketIfTouchedOrderReject(_) => Some("MARKET_IF_TOUCHED_ORDER_REJECT"),
240            Transaction::TakeProfitOrder(_) => Some("TAKE_PROFIT_ORDER"),
241            Transaction::TakeProfitOrderReject(_) => Some("TAKE_PROFIT_ORDER_REJECT"),
242            Transaction::StopLossOrder(_) => Some("STOP_LOSS_ORDER"),
243            Transaction::StopLossOrderReject(_) => Some("STOP_LOSS_ORDER_REJECT"),
244            Transaction::TrailingStopLossOrder(_) => Some("TRAILING_STOP_LOSS_ORDER"),
245            Transaction::TrailingStopLossOrderReject(_) => Some("TRAILING_STOP_LOSS_ORDER_REJECT"),
246            Transaction::OrderFill(_) => Some("ORDER_FILL"),
247            Transaction::OrderCancel(_) => Some("ORDER_CANCEL"),
248            Transaction::OrderCancelReject(_) => Some("ORDER_CANCEL_REJECT"),
249            Transaction::OrderClientExtensionsModify(_) => Some("ORDER_CLIENT_EXTENSIONS_MODIFY"),
250            Transaction::OrderClientExtensionsModifyReject(_) => {
251                Some("ORDER_CLIENT_EXTENSIONS_MODIFY_REJECT")
252            }
253            Transaction::TradeClientExtensionsModify(_) => Some("TRADE_CLIENT_EXTENSIONS_MODIFY"),
254            Transaction::TradeClientExtensionsModifyReject(_) => {
255                Some("TRADE_CLIENT_EXTENSIONS_MODIFY_REJECT")
256            }
257            Transaction::MarginCallEnter(_) => Some("MARGIN_CALL_ENTER"),
258            Transaction::MarginCallExtend(_) => Some("MARGIN_CALL_EXTEND"),
259            Transaction::MarginCallExit(_) => Some("MARGIN_CALL_EXIT"),
260            Transaction::DelayedTradeClosure(_) => Some("DELAYED_TRADE_CLOSURE"),
261            Transaction::DailyFinancing(_) => Some("DAILY_FINANCING"),
262            Transaction::ResetResettablePL(_) => Some("RESET_RESETTABLE_PL"),
263            Transaction::DividendAdjustment(_) => Some("DIVIDEND_ADJUSTMENT"),
264            Transaction::Unknown(value) => value.get("type").and_then(serde_json::Value::as_str),
265        }
266    }
267}
268/// A single line of the transaction stream: either a full transaction or a
269/// heartbeat (sent every 5 seconds to keep the connection alive).
270#[derive(Debug, Clone, PartialEq)]
271#[non_exhaustive]
272#[allow(clippy::large_enum_variant)] // variants mirror the wire format; boxing would hurt ergonomics
273pub enum TransactionStreamItem {
274    /// A transaction applied to the account.
275    Transaction(Transaction),
276    /// A keep-alive heartbeat (`"type": "HEARTBEAT"`).
277    Heartbeat(TransactionHeartbeat),
278}
279
280impl serde::Serialize for TransactionStreamItem {
281    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
282        match self {
283            TransactionStreamItem::Transaction(tx) => tx.serialize(serializer),
284            TransactionStreamItem::Heartbeat(heartbeat) => heartbeat.serialize(serializer),
285        }
286    }
287}
288
289impl<'de> serde::Deserialize<'de> for TransactionStreamItem {
290    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
291        let value = serde_json::Value::deserialize(deserializer)?;
292        let is_heartbeat =
293            value.get("type").and_then(serde_json::Value::as_str) == Some("HEARTBEAT");
294        if is_heartbeat {
295            serde_json::from_value(value)
296                .map(TransactionStreamItem::Heartbeat)
297                .map_err(serde::de::Error::custom)
298        } else {
299            serde_json::from_value(value)
300                .map(TransactionStreamItem::Transaction)
301                .map_err(serde::de::Error::custom)
302        }
303    }
304}