Skip to main content

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