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 Trade::insert_many(executor, &trades)
264 .await
265 .expect("Coult not insert 5 trade");
266
267 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 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 #[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}