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 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 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 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 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(), 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 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 trade.save(executor).await.expect("Failed to save trade");
90
91 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 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 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(), 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(), 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(), 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(), 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 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 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 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 #[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}