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 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 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 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!(Trade::find_many(executor, true, None).count().await, 0);
81
82 trade.save(executor).await.expect("Failed to save trade");
84
85 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 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 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(), 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(), 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(), 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(), 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 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 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 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 #[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}