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