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 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 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 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 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(), 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 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 trade.save(executor).await.expect("Failed to save trade");
83
84 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 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 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(), 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(), 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(), 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(), 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 let affected = Trade::insert_many(executor, &trades)
264 .await
265 .expect("Coult not insert 5 trade");
266 if let Some(affected) = affected.rows_affected {
267 assert_eq!(affected, 5);
268 }
269
270 let data = Trade::find_many(executor, &true, None)
272 .try_collect::<Vec<_>>()
273 .await
274 .expect("Failed to query threads");
275 assert_eq!(data.len(), 5, "Expect to find 5 trades");
276
277 for (i, expected) in trades.iter().enumerate() {
279 let actual_a = &trades[i];
280 let actual_b = Trade::find_pk(executor, &expected.primary_key())
281 .await
282 .expect(&format!("Failed to find trade {} by pk", data[i].symbol));
283 let Some(actual_b) = actual_b else {
284 panic!("Trade {} not found", expected.trade);
285 };
286
287 assert_eq!(actual_a.trade, expected.trade);
288 assert_eq!(actual_b.trade, expected.trade);
289
290 assert_eq!(actual_a.order, expected.order);
291 assert_eq!(actual_b.order, expected.order);
292
293 assert_eq!(actual_a.symbol, expected.symbol);
294 assert_eq!(actual_b.symbol, expected.symbol);
295
296 assert_eq!(actual_a.price, expected.price);
297 assert_eq!(actual_b.price, expected.price);
298
299 assert_eq!(actual_a.quantity, expected.quantity);
300 assert_eq!(actual_b.quantity, expected.quantity);
301
302 assert_eq!(actual_a.execution_time, expected.execution_time);
303 assert_eq!(actual_b.execution_time, expected.execution_time);
304
305 assert_eq!(actual_a.currency, expected.currency);
306 assert_eq!(actual_b.currency, expected.currency);
307
308 assert_eq!(actual_a.is_internalized, expected.is_internalized);
309 assert_eq!(actual_b.is_internalized, expected.is_internalized);
310
311 assert_eq!(actual_a.venue, expected.venue);
312 assert_eq!(actual_b.venue, expected.venue);
313
314 #[cfg(not(feature = "disable-lists"))]
315 assert_eq!(actual_a.child_trade_ids, expected.child_trade_ids);
316 #[cfg(not(feature = "disable-lists"))]
317 assert_eq!(actual_b.child_trade_ids, expected.child_trade_ids);
318
319 assert_eq!(actual_a.metadata, expected.metadata);
320 assert_eq!(actual_b.metadata, expected.metadata);
321
322 #[cfg(not(feature = "disable-maps"))]
323 assert_eq!(actual_a.tags, expected.tags);
324 #[cfg(not(feature = "disable-maps"))]
325 assert_eq!(actual_b.tags, expected.tags);
326 }
327
328 #[cfg(not(feature = "disable-multiple-statements"))]
330 {
331 let writer = executor.driver().sql_writer();
332 let mut query = String::new();
333 writer.write_delete::<Trade>(&mut query, &true);
334 writer.write_insert(
335 &mut query,
336 &[Trade {
337 trade: 10002,
338 order: Uuid::parse_str("895dc048-be92-4a55-afbf-38a60936e844").unwrap(),
339 symbol: "RIVN".to_string(),
340 #[cfg(not(feature = "disable-arrays"))]
341 isin: std::array::from_fn(|i| "US76954A1034".chars().nth(i).unwrap()),
342 price: Decimal::new(1345, 2).into(),
343 quantity: 3200,
344 execution_time: datetime!(2025-06-01 10:15:30).into(),
345 currency: Some("USD".into()),
346 is_internalized: true,
347 venue: Some("NASDAQ".into()),
348 #[cfg(not(feature = "disable-lists"))]
349 child_trade_ids: Some(vec![201]),
350 metadata: Some(
351 b"desc: \"Crossed with internal liquidity\", id:'\\X696E7465726E616C'"
352 .to_vec()
353 .into_boxed_slice(),
354 ),
355 #[cfg(not(feature = "disable-maps"))]
356 tags: Some(BTreeMap::from_iter([
357 ("source".into(), "internal".into()),
358 ("strategy".into(), "arbitrage".into()),
359 ("risk_limit".into(), "high".into()),
360 ])),
361 }],
362 false,
363 );
364 writer.write_select(&mut query, Trade::columns(), Trade::table(), &true, None);
365 let mut stream = pin!(executor.run(query));
366 let Some(Ok(QueryResult::Affected(RowsAffected { rows_affected, .. }))) =
367 stream.next().await
368 else {
369 panic!("Could not get the result of the first query");
370 };
371 if let Some(rows_affected) = rows_affected {
372 assert_eq!(rows_affected, 5);
373 }
374 let Some(Ok(QueryResult::Affected(RowsAffected { rows_affected, .. }))) =
375 stream.next().await
376 else {
377 panic!("Could not get the result of the first statement");
378 };
379 if let Some(rows_affected) = rows_affected {
380 assert_eq!(rows_affected, 1);
381 }
382 let Some(Ok(QueryResult::Row(row))) = stream.next().await else {
383 panic!("Could not get the result of the second statement");
384 };
385 let trade = Trade::from_row(row).expect("Could not decode the Trade from row");
386 assert_eq!(
387 trade,
388 Trade {
389 trade: 10002,
390 order: Uuid::parse_str("895dc048-be92-4a55-afbf-38a60936e844").unwrap(),
391 symbol: "RIVN".to_string(),
392 #[cfg(not(feature = "disable-arrays"))]
393 isin: std::array::from_fn(|i| "US76954A1034".chars().nth(i).unwrap()),
394 price: Decimal::new(1345, 2).into(),
395 quantity: 3200,
396 execution_time: datetime!(2025-06-01 10:15:30).into(),
397 currency: Some("USD".into()),
398 is_internalized: true,
399 venue: Some("NASDAQ".into()),
400 #[cfg(not(feature = "disable-lists"))]
401 child_trade_ids: Some(vec![201]),
402 metadata: Some(
403 b"desc: \"Crossed with internal liquidity\", id:'\\X696E7465726E616C'"
404 .to_vec()
405 .into_boxed_slice(),
406 ),
407 #[cfg(not(feature = "disable-maps"))]
408 tags: Some(BTreeMap::from_iter([
409 ("source".into(), "internal".into()),
410 ("strategy".into(), "arbitrage".into()),
411 ("risk_limit".into(), "high".into()),
412 ])),
413 }
414 );
415 }
416}