tank_tests/
trade.rs

1#![allow(unused_imports)]
2use rust_decimal::Decimal;
3use std::{collections::BTreeMap, pin::pin, str::FromStr, sync::LazyLock};
4use tank::{
5    AsValue, Driver, Entity, Executor, FixedDecimal, Passive, Query, QueryResult, RowsAffected,
6    SqlWriter, Value,
7    stream::{StreamExt, TryStreamExt},
8};
9use time::macros::datetime;
10use tokio::sync::Mutex;
11use uuid::Uuid;
12
13#[derive(Entity, Debug, PartialEq)]
14#[tank(schema = "trading", name = "trade_execution", primary_key = ("trade_id", "execution_time"))]
15pub struct Trade {
16    #[tank(name = "trade_id")]
17    pub trade: u64,
18    #[tank(name = "order_id", default = Uuid::from_str("241d362d-797e-4769-b3f6-412440c8cf68").unwrap().as_value())]
19    pub order: Uuid,
20    /// Ticker symbol
21    pub symbol: String,
22    #[cfg(not(feature = "disable-arrays"))]
23    pub isin: [char; 12],
24    pub price: FixedDecimal<18, 4>,
25    pub quantity: u32,
26    pub execution_time: Passive<time::PrimitiveDateTime>,
27    pub currency: Option<String>,
28    pub is_internalized: bool,
29    /// Exchange
30    pub venue: Option<String>,
31    #[cfg(not(feature = "disable-lists"))]
32    pub child_trade_ids: Option<Vec<i64>>,
33    pub metadata: Option<Box<[u8]>>,
34    #[cfg(not(feature = "disable-maps"))]
35    pub tags: Option<BTreeMap<String, String>>,
36}
37static MUTEX: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
38
39pub async fn trade_simple<E: Executor>(executor: &mut E) {
40    let _lock = MUTEX.lock().await;
41
42    // Setup
43    Trade::drop_table(executor, true, false)
44        .await
45        .expect("Failed to drop Trade table");
46    Trade::create_table(executor, false, true)
47        .await
48        .expect("Failed to create Trade table");
49
50    // Trade object
51    let trade = Trade {
52        trade: 46923,
53        order: Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap(),
54        symbol: "RIVN".to_string(),
55        #[cfg(not(feature = "disable-arrays"))]
56        isin: std::array::from_fn(|i| "US76954A1034".chars().nth(i).unwrap()),
57        price: Decimal::new(1226, 2).into(), // 12.26
58        quantity: 500,
59        execution_time: datetime!(2025-06-07 14:32:00).into(),
60        currency: Some("USD".into()),
61        is_internalized: true,
62        venue: Some("NASDAQ".into()),
63        #[cfg(not(feature = "disable-lists"))]
64        child_trade_ids: vec![36209, 85320].into(),
65        metadata: b"Metadata Bytes".to_vec().into_boxed_slice().into(),
66        #[cfg(not(feature = "disable-maps"))]
67        tags: BTreeMap::from_iter([
68            ("source".into(), "internal".into()),
69            ("strategy".into(), "scalping".into()),
70        ])
71        .into(),
72    };
73
74    // Expect to find no trades
75    let result = Trade::find_pk(executor, &trade.primary_key())
76        .await
77        .expect("Failed to find trade by primary key");
78    assert!(result.is_none(), "Expected no trades at this time");
79    assert_eq!(Trade::find_many(executor, &true, None).count().await, 0);
80
81    // Save a trade
82    trade.save(executor).await.expect("Failed to save trade");
83
84    // Expect to find the only trade
85    let result = Trade::find_pk(executor, &trade.primary_key())
86        .await
87        .expect("Failed to find trade");
88    assert!(
89        result.is_some(),
90        "Expected Trade::find_pk to return some result",
91    );
92    let result = result.unwrap();
93    assert_eq!(result.trade, 46923);
94    assert_eq!(
95        result.order,
96        Uuid::from_str("550e8400-e29b-41d4-a716-446655440000").unwrap()
97    );
98    assert_eq!(result.symbol, "RIVN");
99    #[cfg(not(feature = "disable-arrays"))]
100    assert_eq!(
101        result
102            .isin
103            .iter()
104            .map(|v| v.to_string())
105            .collect::<Vec<_>>()
106            .join(""),
107        "US76954A1034"
108    );
109    assert_eq!(result.price, Decimal::new(1226, 2).into());
110    assert_eq!(result.quantity, 500);
111    assert_eq!(
112        result.execution_time,
113        Passive::Set(datetime!(2025-06-07 14:32:00))
114    );
115    assert_eq!(result.currency, Some("USD".into()));
116    assert_eq!(result.is_internalized, true);
117    assert_eq!(result.venue, Some("NASDAQ".into()));
118    #[cfg(not(feature = "disable-lists"))]
119    assert_eq!(result.child_trade_ids, Some(vec![36209, 85320]));
120    assert_eq!(
121        result.metadata,
122        Some(b"Metadata Bytes".to_vec().into_boxed_slice())
123    );
124    #[cfg(not(feature = "disable-maps"))]
125    let Some(tags) = result.tags else {
126        unreachable!("Tag is expected");
127    };
128    #[cfg(not(feature = "disable-maps"))]
129    assert_eq!(tags.len(), 2);
130    #[cfg(not(feature = "disable-maps"))]
131    assert_eq!(
132        tags,
133        BTreeMap::from_iter([
134            ("source".into(), "internal".into()),
135            ("strategy".into(), "scalping".into())
136        ])
137    );
138
139    assert_eq!(Trade::find_many(executor, &true, None).count().await, 1);
140}
141
142pub async fn trade_multiple<E: Executor>(executor: &mut E) {
143    let _lock = MUTEX.lock().await;
144
145    // Setup
146    Trade::drop_table(executor, false, false)
147        .await
148        .expect("Failed to drop Trade table");
149    Trade::create_table(executor, false, true)
150        .await
151        .expect("Failed to create Trade table");
152
153    // Trade objects
154    let trades = vec![
155        Trade {
156            trade: 10001,
157            order: Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap(),
158            symbol: "AAPL".to_string(),
159            #[cfg(not(feature = "disable-arrays"))]
160            isin: std::array::from_fn(|i| "US0378331005".chars().nth(i).unwrap()),
161            price: Decimal::new(15000, 2).into(),
162            quantity: 10,
163            execution_time: datetime!(2025-06-01 09:00:00).into(),
164            currency: Some("USD".into()),
165            is_internalized: false,
166            venue: Some("NASDAQ".into()),
167            #[cfg(not(feature = "disable-lists"))]
168            child_trade_ids: Some(vec![101, 102]),
169            metadata: Some(b"First execution".to_vec().into_boxed_slice()),
170            #[cfg(not(feature = "disable-maps"))]
171            tags: Some(BTreeMap::from_iter([
172                ("source".into(), "algo".into()),
173                ("strategy".into(), "momentum".into()),
174            ])),
175        },
176        Trade {
177            trade: 10002,
178            order: Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap(),
179            symbol: "GOOG".to_string(),
180            #[cfg(not(feature = "disable-arrays"))]
181            isin: std::array::from_fn(|i| "US02079K3059".chars().nth(i).unwrap()),
182            price: Decimal::new(280000, 3).into(), // 280.000
183            quantity: 5,
184            execution_time: datetime!(2025-06-02 10:15:30).into(),
185            currency: Some("USD".into()),
186            is_internalized: true,
187            venue: Some("NYSE".into()),
188            #[cfg(not(feature = "disable-lists"))]
189            child_trade_ids: None,
190            metadata: Some(b"Second execution".to_vec().into_boxed_slice()),
191            #[cfg(not(feature = "disable-maps"))]
192            tags: Some(BTreeMap::from_iter([
193                ("source".into(), "internal".into()),
194                ("strategy".into(), "mean_reversion".into()),
195            ])),
196        },
197        Trade {
198            trade: 10003,
199            order: Uuid::parse_str("33333333-3333-3333-3333-333333333333").unwrap(),
200            symbol: "MSFT".to_string(),
201            #[cfg(not(feature = "disable-arrays"))]
202            isin: std::array::from_fn(|i| "US5949181045".chars().nth(i).unwrap()),
203            price: Decimal::new(32567, 2).into(), // 325.67
204            quantity: 20,
205            execution_time: datetime!(2025-06-03 11:45:00).into(),
206            currency: Some("USD".into()),
207            is_internalized: false,
208            venue: Some("BATS".into()),
209            #[cfg(not(feature = "disable-lists"))]
210            child_trade_ids: Some(vec![301]),
211            metadata: Some(b"Third execution".to_vec().into_boxed_slice()),
212            #[cfg(not(feature = "disable-maps"))]
213            tags: Some(BTreeMap::from_iter([
214                ("sourcev".into(), "external".into()),
215                ("strategy".into(), "arbitrage".into()),
216            ])),
217        },
218        Trade {
219            trade: 10004,
220            order: Uuid::parse_str("44444444-4444-4444-4444-444444444444").unwrap(),
221            symbol: "TSLA".to_string(),
222            #[cfg(not(feature = "disable-arrays"))]
223            isin: std::array::from_fn(|i| "US88160R1014".chars().nth(i).unwrap()),
224            price: Decimal::new(62000, 2).into(), // 620.00
225            quantity: 15,
226            execution_time: datetime!(2025-06-04 14:00:00).into(),
227            currency: Some("USD".into()),
228            is_internalized: true,
229            venue: Some("CBOE".into()),
230            #[cfg(not(feature = "disable-lists"))]
231            child_trade_ids: None,
232            metadata: None,
233            #[cfg(not(feature = "disable-maps"))]
234            tags: Some(BTreeMap::from_iter([
235                ("source".into(), "manual".into()),
236                ("strategy".into(), "news_event".into()),
237            ])),
238        },
239        Trade {
240            trade: 10005,
241            order: Uuid::parse_str("55555555-5555-5555-5555-555555555555").unwrap(),
242            symbol: "AMZN".to_string(),
243            #[cfg(not(feature = "disable-arrays"))]
244            isin: std::array::from_fn(|i| "US0231351067".chars().nth(i).unwrap()),
245            price: Decimal::new(134899, 3).into(), // 1348.99
246            quantity: 8,
247            execution_time: datetime!(2025-06-05 16:30:00).into(),
248            currency: Some("USD".into()),
249            is_internalized: false,
250            venue: Some("NASDAQ".into()),
251            #[cfg(not(feature = "disable-lists"))]
252            child_trade_ids: Some(vec![501, 502, 503]),
253            metadata: Some(b"Fifth execution".to_vec().into_boxed_slice()),
254            #[cfg(not(feature = "disable-maps"))]
255            tags: Some(BTreeMap::from_iter([
256                ("source".into(), "internal".into()),
257                ("strategy".into(), "scalping".into()),
258            ])),
259        },
260    ];
261
262    // Insert 5 trades
263    Trade::insert_many(executor, &trades)
264        .await
265        .expect("Coult not insert 5 trade");
266
267    // Find 5 trades
268    let data = Trade::find_many(executor, &true, None)
269        .try_collect::<Vec<_>>()
270        .await
271        .expect("Failed to query threads");
272    assert_eq!(data.len(), 5, "Expect to find 5 trades");
273
274    // Verify data integrity
275    for (i, expected) in trades.iter().enumerate() {
276        let actual_a = &trades[i];
277        let actual_b = Trade::find_pk(executor, &expected.primary_key())
278            .await
279            .expect(&format!("Failed to find trade {} by pk", data[i].symbol));
280        let Some(actual_b) = actual_b else {
281            panic!("Trade {} not found", expected.trade);
282        };
283
284        assert_eq!(actual_a.trade, expected.trade);
285        assert_eq!(actual_b.trade, expected.trade);
286
287        assert_eq!(actual_a.order, expected.order);
288        assert_eq!(actual_b.order, expected.order);
289
290        assert_eq!(actual_a.symbol, expected.symbol);
291        assert_eq!(actual_b.symbol, expected.symbol);
292
293        assert_eq!(actual_a.price, expected.price);
294        assert_eq!(actual_b.price, expected.price);
295
296        assert_eq!(actual_a.quantity, expected.quantity);
297        assert_eq!(actual_b.quantity, expected.quantity);
298
299        assert_eq!(actual_a.execution_time, expected.execution_time);
300        assert_eq!(actual_b.execution_time, expected.execution_time);
301
302        assert_eq!(actual_a.currency, expected.currency);
303        assert_eq!(actual_b.currency, expected.currency);
304
305        assert_eq!(actual_a.is_internalized, expected.is_internalized);
306        assert_eq!(actual_b.is_internalized, expected.is_internalized);
307
308        assert_eq!(actual_a.venue, expected.venue);
309        assert_eq!(actual_b.venue, expected.venue);
310
311        #[cfg(not(feature = "disable-lists"))]
312        assert_eq!(actual_a.child_trade_ids, expected.child_trade_ids);
313        #[cfg(not(feature = "disable-lists"))]
314        assert_eq!(actual_b.child_trade_ids, expected.child_trade_ids);
315
316        assert_eq!(actual_a.metadata, expected.metadata);
317        assert_eq!(actual_b.metadata, expected.metadata);
318
319        #[cfg(not(feature = "disable-maps"))]
320        assert_eq!(actual_a.tags, expected.tags);
321        #[cfg(not(feature = "disable-maps"))]
322        assert_eq!(actual_b.tags, expected.tags);
323    }
324
325    // Multiple statements
326    #[cfg(not(feature = "disable-multiple-statements"))]
327    {
328        let writer = executor.driver().sql_writer();
329        let mut query = String::new();
330        writer.write_delete::<Trade>(&mut query, &true);
331        writer.write_insert(
332            &mut query,
333            &[Trade {
334                trade: 10002,
335                order: Uuid::parse_str("895dc048-be92-4a55-afbf-38a60936e844").unwrap(),
336                symbol: "RIVN".to_string(),
337                #[cfg(not(feature = "disable-arrays"))]
338                isin: std::array::from_fn(|i| "US76954A1034".chars().nth(i).unwrap()),
339                price: Decimal::new(1345, 2).into(),
340                quantity: 3200,
341                execution_time: datetime!(2025-06-01 10:15:30).into(),
342                currency: Some("USD".into()),
343                is_internalized: true,
344                venue: Some("NASDAQ".into()),
345                #[cfg(not(feature = "disable-lists"))]
346                child_trade_ids: Some(vec![201]),
347                metadata: Some(
348                    b"desc: \"Crossed with internal liquidity\", id:'\\X696E7465726E616C'"
349                        .to_vec()
350                        .into_boxed_slice(),
351                ),
352                #[cfg(not(feature = "disable-maps"))]
353                tags: Some(BTreeMap::from_iter([
354                    ("source".into(), "internal".into()),
355                    ("strategy".into(), "arbitrage".into()),
356                    ("risk_limit".into(), "high".into()),
357                ])),
358            }],
359            false,
360        );
361        writer.write_select(&mut query, Trade::columns(), Trade::table(), &true, None);
362        let mut stream = pin!(executor.run(query));
363        let Some(Ok(QueryResult::Affected(RowsAffected { rows_affected, .. }))) =
364            stream.next().await
365        else {
366            panic!("Could not get the result of the first query");
367        };
368        if let Some(rows_affected) = rows_affected {
369            assert_eq!(rows_affected, 5);
370        }
371        let Some(Ok(QueryResult::Affected(RowsAffected { rows_affected, .. }))) =
372            stream.next().await
373        else {
374            panic!("Could not get the result of the first statement");
375        };
376        if let Some(rows_affected) = rows_affected {
377            assert_eq!(rows_affected, 1);
378        }
379        let Some(Ok(QueryResult::Row(row))) = stream.next().await else {
380            panic!("Could not get the result of the second statement");
381        };
382        let trade = Trade::from_row(row).expect("Could not decode the Trade from row");
383        assert_eq!(
384            trade,
385            Trade {
386                trade: 10002,
387                order: Uuid::parse_str("895dc048-be92-4a55-afbf-38a60936e844").unwrap(),
388                symbol: "RIVN".to_string(),
389                #[cfg(not(feature = "disable-arrays"))]
390                isin: std::array::from_fn(|i| "US76954A1034".chars().nth(i).unwrap()),
391                price: Decimal::new(1345, 2).into(),
392                quantity: 3200,
393                execution_time: datetime!(2025-06-01 10:15:30).into(),
394                currency: Some("USD".into()),
395                is_internalized: true,
396                venue: Some("NASDAQ".into()),
397                #[cfg(not(feature = "disable-lists"))]
398                child_trade_ids: Some(vec![201]),
399                metadata: Some(
400                    b"desc: \"Crossed with internal liquidity\", id:'\\X696E7465726E616C'"
401                        .to_vec()
402                        .into_boxed_slice(),
403                ),
404                #[cfg(not(feature = "disable-maps"))]
405                tags: Some(BTreeMap::from_iter([
406                    ("source".into(), "internal".into()),
407                    ("strategy".into(), "arbitrage".into()),
408                    ("risk_limit".into(), "high".into()),
409                ])),
410            }
411        );
412    }
413}