1use crate::{Error, Result};
2use bytemuck;
3use serde::{Deserialize, Serialize};
4use sqlx::FromRow;
5use std::{io::Read, u16};
6
7#[cfg(feature = "python")]
8use pyo3::pyclass;
9
10fn write_string(buffer: &mut Vec<u8>, string: &str) {
12 let length = string.len() as u16; buffer.extend(&length.to_le_bytes()); buffer.extend(string.as_bytes()); }
16
17fn read_string<R: Read>(cursor: &mut R) -> Result<String> {
19 let mut len_buf = [0u8; 2]; cursor
21 .read_exact(&mut len_buf)
22 .map_err(|_| Error::CustomError("Failed to read string length".to_string()))?;
23 let len = u16::from_le_bytes(len_buf) as usize; let mut string_buf = vec![0u8; len]; cursor
27 .read_exact(&mut string_buf)
28 .map_err(|_| Error::CustomError("Failed to read string bytes".to_string()))?;
29 String::from_utf8(string_buf)
30 .map_err(|_| Error::CustomError("Invalid UTF-8 string".to_string()))
31}
32
33fn read_fixed<T: Sized + Copy, R: Read>(cursor: &mut R) -> Result<T>
35where
36 T: bytemuck::Pod + bytemuck::Zeroable,
37{
38 let mut buffer = vec![0u8; std::mem::size_of::<T>()];
39 cursor.read_exact(&mut buffer).map_err(|_| {
40 Error::CustomError(format!("Failed to read {} bytes", std::mem::size_of::<T>()))
41 })?;
42 Ok(bytemuck::cast_slice(&buffer)[0])
43}
44
45pub trait Encode {
47 fn encode(&self, buffer: &mut Vec<u8>);
48}
49
50pub trait Decode<R: Read>: Sized {
51 fn decode(cursor: &mut R) -> Result<Self>;
52}
53
54#[repr(C)]
55#[cfg_attr(
56 feature = "python",
57 pyclass(get_all, set_all, dict, module = "mbinary")
58)]
59#[derive(Deserialize, Serialize, FromRow, Debug, Clone, PartialEq)]
60pub struct BacktestData {
61 pub metadata: BacktestMetaData,
62 pub period_timeseries_stats: Vec<TimeseriesStats>,
63 pub daily_timeseries_stats: Vec<TimeseriesStats>,
64 pub trades: Vec<Trades>,
65 pub signals: Vec<Signals>,
66}
67
68#[repr(C)]
69#[cfg_attr(
70 feature = "python",
71 pyclass(get_all, set_all, dict, module = "mbinary")
72)]
73#[derive(Deserialize, Serialize, FromRow, Debug, Clone, PartialEq)]
74pub struct BacktestMetaData {
75 pub backtest_id: u16,
76 pub backtest_name: String,
77 pub parameters: Parameters,
78 pub static_stats: StaticStats,
79}
80
81impl BacktestMetaData {
82 pub fn new(
83 mut backtest_id: Option<u16>,
84 backtest_name: &str,
85 parameters: Parameters,
86 static_stats: StaticStats,
87 ) -> Self {
88 if None == backtest_id {
89 backtest_id = Some(u16::MAX); };
91 Self {
92 backtest_id: backtest_id.unwrap(),
93 backtest_name: backtest_name.to_string(),
94 parameters,
95 static_stats,
96 }
97 }
98}
99
100impl Encode for BacktestMetaData {
101 fn encode(&self, buffer: &mut Vec<u8>) {
102 buffer.extend(&self.backtest_id.to_le_bytes());
103 write_string(buffer, &self.backtest_name);
104 self.parameters.encode(buffer);
105 self.static_stats.encode(buffer);
106 }
107}
108
109impl<R: Read> Decode<R> for BacktestMetaData {
110 fn decode(cursor: &mut R) -> Result<Self> {
111 let backtest_id: u16 = read_fixed(cursor)?;
112 let backtest_name: String = read_string(cursor)?;
113 let parameters: Parameters = Parameters::decode(cursor)?;
114 let static_stats: StaticStats = StaticStats::decode(cursor)?;
115
116 Ok(Self {
117 backtest_id,
118 backtest_name,
119 parameters,
120 static_stats,
121 })
122 }
123}
124
125#[repr(C)]
126#[cfg_attr(
127 feature = "python",
128 pyclass(get_all, set_all, dict, module = "mbinary")
129)]
130#[derive(Deserialize, Serialize, FromRow, Debug, Clone, PartialEq)]
131pub struct Parameters {
132 pub strategy_name: String,
133 pub capital: i64,
134 pub schema: String,
135 pub data_type: String,
136 pub start: i64,
137 pub end: i64,
138 pub tickers: Vec<String>,
139}
140
141impl Encode for Parameters {
142 fn encode(&self, buffer: &mut Vec<u8>) {
143 write_string(buffer, &self.strategy_name);
144 buffer.extend(&self.capital.to_le_bytes());
145 write_string(buffer, &self.schema);
146 write_string(buffer, &self.data_type);
147 buffer.extend(&self.start.to_le_bytes());
148 buffer.extend(&self.end.to_le_bytes());
149
150 let tickers_len = self.tickers.len() as u32;
152 buffer.extend(&tickers_len.to_le_bytes());
153
154 for ticker in &self.tickers {
155 write_string(buffer, ticker);
156 }
157 }
158}
159
160impl<R: Read> Decode<R> for Parameters {
161 fn decode(cursor: &mut R) -> Result<Self> {
162 let strategy_name: String = read_string(cursor)?;
166 let capital: i64 = read_fixed(cursor)?;
167 let schema: String = read_string(cursor)?;
168 let data_type: String = read_string(cursor)?;
169 let start: i64 = read_fixed(cursor)?;
170 let end: i64 = read_fixed(cursor)?;
171
172 let mut tickers_len_buf = [0u8; 4];
174 cursor
175 .read_exact(&mut tickers_len_buf)
176 .map_err(|_| Error::CustomError("Failed to read tickers length".to_string()))?;
177 let tickers_len = u32::from_le_bytes(tickers_len_buf) as usize;
178
179 let mut tickers = Vec::new();
181 for _ in 0..tickers_len {
182 tickers.push(read_string(cursor)?);
183 }
184
185 Ok(Self {
186 strategy_name,
187 capital,
188 schema,
189 data_type,
190 start,
191 end,
192 tickers,
193 })
194 }
195}
196
197#[repr(C)]
198#[cfg_attr(
199 feature = "python",
200 pyclass(get_all, set_all, dict, module = "mbinary")
201)]
202#[derive(Deserialize, Serialize, FromRow, Debug, Clone, PartialEq)]
203pub struct StaticStats {
204 pub total_trades: i32,
205 pub total_winning_trades: i32,
206 pub total_losing_trades: i32,
207 pub avg_profit: i64, pub avg_profit_percent: i64, pub avg_gain: i64, pub avg_gain_percent: i64, pub avg_loss: i64, pub avg_loss_percent: i64, pub profitability_ratio: i64, pub profit_factor: i64, pub profit_and_loss_ratio: i64, pub total_fees: i64, pub net_profit: i64, pub beginning_equity: i64, pub ending_equity: i64, pub total_return: i64, pub annualized_return: i64, pub daily_standard_deviation_percentage: i64, pub annual_standard_deviation_percentage: i64, pub max_drawdown_percentage_period: i64, pub max_drawdown_percentage_daily: i64, pub sharpe_ratio: i64, pub sortino_ratio: i64, }
229
230impl Encode for StaticStats {
231 fn encode(&self, buffer: &mut Vec<u8>) {
232 buffer.extend(unsafe {
233 std::slice::from_raw_parts(
234 (self as *const StaticStats) as *const u8,
235 std::mem::size_of::<StaticStats>(),
236 )
237 });
238 }
239}
240
241impl<R: Read> Decode<R> for StaticStats {
242 fn decode(cursor: &mut R) -> Result<Self> {
243 let mut buffer = vec![0u8; std::mem::size_of::<StaticStats>()];
245 cursor.read_exact(&mut buffer)?;
246
247 let ptr = buffer.as_ptr() as *const StaticStats;
249
250 unsafe { Ok(ptr.read()) }
252 }
253}
254
255#[repr(C)]
256#[cfg_attr(
257 feature = "python",
258 pyclass(get_all, set_all, dict, module = "mbinary")
259)]
260#[derive(Deserialize, Serialize, FromRow, Debug, Clone, PartialEq, Eq)]
261pub struct TimeseriesStats {
262 pub timestamp: i64,
263 pub equity_value: i64, pub percent_drawdown: i64, pub cumulative_return: i64, pub period_return: i64, }
268
269impl Encode for TimeseriesStats {
270 fn encode(&self, buffer: &mut Vec<u8>) {
271 buffer.extend(unsafe {
272 std::slice::from_raw_parts(
274 (self as *const TimeseriesStats) as *const u8,
275 std::mem::size_of::<TimeseriesStats>(),
276 )
277 });
278 }
279}
280
281impl<R: Read> Decode<R> for TimeseriesStats {
282 fn decode(cursor: &mut R) -> Result<Self> {
283 let mut buffer = vec![0u8; std::mem::size_of::<TimeseriesStats>()];
285 cursor.read_exact(&mut buffer)?;
286
287 let ptr = buffer.as_ptr() as *const TimeseriesStats;
289
290 unsafe { Ok(ptr.read()) }
292 }
293}
294
295#[repr(C)]
296#[cfg_attr(
297 feature = "python",
298 pyclass(get_all, set_all, dict, module = "mbinary")
299)]
300#[derive(Deserialize, Serialize, FromRow, Debug, Clone, PartialEq, Eq)]
301pub struct Trades {
302 pub trade_id: i32,
303 pub signal_id: i32,
304 pub timestamp: i64,
305 pub ticker: String,
306 pub quantity: i64, pub avg_price: i64, pub trade_value: i64, pub trade_cost: i64, pub action: String,
311 pub fees: i64, }
313
314impl Encode for Trades {
315 fn encode(&self, buffer: &mut Vec<u8>) {
316 buffer.extend(&self.trade_id.to_le_bytes());
317 buffer.extend(&self.signal_id.to_le_bytes());
318 buffer.extend(&self.timestamp.to_le_bytes());
319 write_string(buffer, &self.ticker);
320 buffer.extend(&self.quantity.to_le_bytes());
321 buffer.extend(&self.avg_price.to_le_bytes());
322 buffer.extend(&self.trade_value.to_le_bytes());
323 buffer.extend(&self.trade_cost.to_le_bytes());
324 write_string(buffer, &self.action);
325 buffer.extend(&self.fees.to_le_bytes());
326 }
327}
328
329impl<R: Read> Decode<R> for Trades {
330 fn decode(cursor: &mut R) -> Result<Self> {
331 let trade_id: i32 = read_fixed(cursor)?;
332 let signal_id: i32 = read_fixed(cursor)?;
333 let timestamp: i64 = read_fixed(cursor)?;
334 let ticker: String = read_string(cursor)?;
335 let quantity: i64 = read_fixed(cursor)?;
336 let avg_price: i64 = read_fixed(cursor)?;
337 let trade_value: i64 = read_fixed(cursor)?;
338 let trade_cost: i64 = read_fixed(cursor)?;
339 let action: String = read_string(cursor)?;
340 let fees: i64 = read_fixed(cursor)?;
341
342 Ok(Self {
343 trade_id,
344 signal_id,
345 timestamp,
346 ticker,
347 quantity,
348 avg_price,
349 trade_value,
350 trade_cost,
351 action,
352 fees,
353 })
354 }
355}
356
357#[repr(C)]
358#[cfg_attr(
359 feature = "python",
360 pyclass(get_all, set_all, dict, module = "mbinary")
361)]
362#[derive(Deserialize, Serialize, FromRow, Debug, Clone, PartialEq, Eq)]
363pub struct Signals {
364 pub timestamp: i64,
365 pub trade_instructions: Vec<SignalInstructions>,
366}
367
368impl Encode for Signals {
369 fn encode(&self, buffer: &mut Vec<u8>) {
370 buffer.extend(&self.timestamp.to_le_bytes());
371
372 let trade_len = self.trade_instructions.len() as u32;
374 buffer.extend(&trade_len.to_le_bytes());
375
376 for t in &self.trade_instructions {
377 t.encode(buffer);
378 }
379 }
380}
381
382impl<R: Read> Decode<R> for Signals {
383 fn decode(cursor: &mut R) -> Result<Self> {
384 let timestamp: i64 = read_fixed(cursor)?;
385
386 let instruction_len: u32 = read_fixed(cursor)?;
388
389 let mut trade_instructions = Vec::new();
391 for _ in 0..instruction_len {
392 trade_instructions.push(SignalInstructions::decode(cursor)?);
393 }
394
395 Ok(Self {
396 timestamp,
397 trade_instructions,
398 })
399 }
400}
401
402#[repr(C)]
403#[cfg_attr(
404 feature = "python",
405 pyclass(get_all, set_all, dict, module = "mbinary")
406)]
407#[derive(Deserialize, Serialize, FromRow, Debug, Clone, PartialEq, Eq)]
408pub struct SignalInstructions {
409 pub ticker: String,
410 pub order_type: String,
411 pub action: String,
412 pub signal_id: i32,
413 pub weight: i64, pub quantity: i32,
415 pub limit_price: String, pub aux_price: String, }
418
419impl Encode for SignalInstructions {
420 fn encode(&self, buffer: &mut Vec<u8>) {
421 write_string(buffer, &self.ticker);
422 write_string(buffer, &self.order_type);
423 write_string(buffer, &self.action);
424 buffer.extend(&self.signal_id.to_le_bytes());
425 buffer.extend(&self.weight.to_le_bytes());
426 buffer.extend(&self.quantity.to_le_bytes());
427 write_string(buffer, &self.limit_price);
428 write_string(buffer, &self.aux_price);
429 }
430}
431impl<R: Read> Decode<R> for SignalInstructions {
432 fn decode(cursor: &mut R) -> Result<Self> {
433 let ticker: String = read_string(cursor)?;
434 let order_type: String = read_string(cursor)?;
435 let action: String = read_string(cursor)?;
436 let signal_id: i32 = read_fixed(cursor)?;
437 let weight: i64 = read_fixed(cursor)?;
438 let quantity: i32 = read_fixed(cursor)?;
439 let limit_price: String = read_string(cursor)?;
440 let aux_price: String = read_string(cursor)?;
441
442 Ok(Self {
443 ticker,
444 order_type,
445 action,
446 signal_id,
447 weight,
448 quantity,
449 limit_price,
450 aux_price,
451 })
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458 use std::io::Cursor;
459
460 #[test]
461 fn test_encode_valid_string() -> anyhow::Result<()> {
462 let text = "testing123";
464 let mut buffer = Vec::new();
465 write_string(&mut buffer, &text);
466
467 let bytes: &[u8] = &buffer;
469 let mut cursor = Cursor::new(bytes);
470 let decoded = read_string(&mut cursor)?;
471
472 assert_eq!(text, &decoded);
474
475 Ok(())
476 }
477
478 #[test]
479 fn test_encode_empty_string() -> anyhow::Result<()> {
480 let text = "";
482 let mut buffer = Vec::new();
483 write_string(&mut buffer, &text);
484
485 let bytes: &[u8] = &buffer;
487 let mut cursor = Cursor::new(bytes);
488 let decoded = read_string(&mut cursor)?;
489
490 assert_eq!(text, &decoded);
492
493 Ok(())
494 }
495
496 #[test]
497 fn parameters_encode_decode() -> anyhow::Result<()> {
498 let params = Parameters {
499 strategy_name: "Testing".to_string(),
500 capital: 10000,
501 schema: "Ohlcv-1s".to_string(),
502 data_type: "BAR".to_string(),
503 start: 1730160814000000000,
504 end: 1730160814000000000,
505 tickers: vec!["HE.n.0".to_string(), "AAPL".to_string()],
506 };
507
508 let mut bytes = Vec::new();
510 params.encode(&mut bytes);
511
512 let mut cursor = Cursor::new(bytes.as_slice());
514 let decoded = Parameters::decode(&mut cursor)?;
515
516 assert_eq!(params, decoded);
518
519 Ok(())
520 }
521
522 #[test]
523 fn staticstats_encode_decode() -> anyhow::Result<()> {
524 let static_stats = StaticStats {
525 total_trades: 100,
526 total_winning_trades: 50,
527 total_losing_trades: 50,
528 avg_profit: 1000000000000,
529 avg_profit_percent: 10383783337737,
530 avg_gain: 23323212233,
531 avg_gain_percent: 24323234,
532 avg_loss: 203982828,
533 avg_loss_percent: 23432134323,
534 profitability_ratio: 130213212323,
535 profit_factor: 12342123431,
536 profit_and_loss_ratio: 1234321343,
537 total_fees: 123453234,
538 net_profit: 1234323,
539 beginning_equity: 12343234323,
540 ending_equity: 12343234,
541 total_return: 234532345,
542 annualized_return: 234532345,
543 daily_standard_deviation_percentage: 23453234,
544 annual_standard_deviation_percentage: 34543443,
545 max_drawdown_percentage_period: 234543234,
546 max_drawdown_percentage_daily: 23432345,
547 sharpe_ratio: 23432343,
548 sortino_ratio: 123453234543,
549 };
550
551 let mut bytes = Vec::new();
553 static_stats.encode(&mut bytes);
554
555 let mut cursor = Cursor::new(bytes.as_slice());
557 let decoded = StaticStats::decode(&mut cursor)?;
558
559 assert_eq!(static_stats, decoded);
561
562 Ok(())
563 }
564
565 #[test]
566 fn timeseriesstats_encode_decode() -> anyhow::Result<()> {
567 let timeseries = TimeseriesStats {
568 timestamp: 123700000000000,
569 equity_value: 9999999,
570 percent_drawdown: 2343234,
571 cumulative_return: 2343234,
572 period_return: 2345432345,
573 };
574
575 let stats: Vec<TimeseriesStats> = vec![timeseries.clone(), timeseries.clone()];
576
577 let mut bytes = Vec::new();
579 for s in &stats {
580 s.encode(&mut bytes);
581 }
582
583 let mut cursor = Cursor::new(bytes.as_slice());
585 let mut decoded = Vec::new();
586
587 while (cursor.position() as usize) < cursor.get_ref().len() {
588 let trade = TimeseriesStats::decode(&mut cursor)?;
589 decoded.push(trade);
590 }
591
592 assert_eq!(stats, decoded);
594
595 Ok(())
596 }
597
598 #[test]
599 fn trades_encode_decode() -> anyhow::Result<()> {
600 let trade = Trades {
601 trade_id: 1,
602 signal_id: 1,
603 timestamp: 1704903000,
604 ticker: "AAPL".to_string(),
605 quantity: 4,
606 avg_price: 13074,
607 trade_value: -52296,
608 trade_cost: -52296,
609 action: "BUY".to_string(),
610 fees: 100,
611 };
612
613 let vec: Vec<Trades> = vec![trade.clone(), trade.clone()];
614
615 let mut buffer = Vec::new();
617 for t in &vec {
618 t.encode(&mut buffer);
619 }
620
621 let mut cursor = Cursor::new(buffer.as_slice());
623 let mut decoded = Vec::new();
624
625 while (cursor.position() as usize) < cursor.get_ref().len() {
626 let trade = Trades::decode(&mut cursor)?;
627 decoded.push(trade);
628 }
629
630 assert_eq!(vec, decoded);
632
633 Ok(())
634 }
635
636 #[test]
637 fn signal_instructions_encode_decode() -> anyhow::Result<()> {
638 let instructions = SignalInstructions {
639 ticker: "AAPL".to_string(),
640 order_type: "MKT".to_string(),
641 action: "BUY".to_string(),
642 signal_id: 1,
643 weight: 13213432,
644 quantity: 2343,
645 limit_price: "12341".to_string(),
646 aux_price: "1233212".to_string(),
647 };
648
649 let vec: Vec<SignalInstructions> = vec![instructions.clone(), instructions.clone()];
650
651 let mut buffer = Vec::new();
653 for s in &vec {
654 s.encode(&mut buffer);
655 }
656
657 let mut cursor = Cursor::new(buffer.as_slice());
659 let mut decoded = Vec::new();
660
661 while (cursor.position() as usize) < cursor.get_ref().len() {
662 let trade = SignalInstructions::decode(&mut cursor)?;
663 decoded.push(trade);
664 }
665
666 assert_eq!(vec, decoded);
668
669 Ok(())
670 }
671
672 #[test]
673 fn signals_encode_decode() -> anyhow::Result<()> {
674 let instructions = SignalInstructions {
675 ticker: "AAPL".to_string(),
676 order_type: "MKT".to_string(),
677 action: "BUY".to_string(),
678 signal_id: 1,
679 weight: 13213432,
680 quantity: 2343,
681 limit_price: "12341".to_string(),
682 aux_price: "1233212".to_string(),
683 };
684
685 let vec: Vec<SignalInstructions> = vec![instructions.clone(), instructions.clone()];
686 let signal = Signals {
687 timestamp: 1234565432345,
688 trade_instructions: vec,
689 };
690
691 let signals: Vec<Signals> = vec![signal.clone(), signal.clone()];
692
693 let mut buffer = Vec::new();
695 for s in &signals {
696 s.encode(&mut buffer);
697 }
698
699 let mut cursor = Cursor::new(buffer.as_slice());
701 let mut decoded = Vec::new();
702
703 while (cursor.position() as usize) < cursor.get_ref().len() {
704 let trade = Signals::decode(&mut cursor)?;
705 decoded.push(trade);
706 }
707
708 assert_eq!(signals, decoded);
710
711 Ok(())
712 }
713
714 #[test]
715 fn backtestmetdata_encode_decode() -> anyhow::Result<()> {
716 let params = Parameters {
717 strategy_name: "Testing".to_string(),
718 capital: 10000,
719 schema: "Ohlcv-1s".to_string(),
720 data_type: "BAR".to_string(),
721 start: 1730160814000000000,
722 end: 1730160814000000000,
723 tickers: vec!["HE.n.0".to_string(), "AAPL".to_string()],
724 };
725
726 let static_stats = StaticStats {
727 total_trades: 100,
728 total_winning_trades: 50,
729 total_losing_trades: 50,
730 avg_profit: 1000000000000,
731 avg_profit_percent: 10383783337737,
732 avg_gain: 23323212233,
733 avg_gain_percent: 24323234,
734 avg_loss: 203982828,
735 avg_loss_percent: 23432134323,
736 profitability_ratio: 130213212323,
737 profit_factor: 12342123431,
738 profit_and_loss_ratio: 1234321343,
739 total_fees: 123453234,
740 net_profit: 1234323,
741 beginning_equity: 12343234323,
742 ending_equity: 12343234,
743 total_return: 234532345,
744 annualized_return: 234532345,
745 daily_standard_deviation_percentage: 23453234,
746 annual_standard_deviation_percentage: 34543443,
747 max_drawdown_percentage_period: 234543234,
748 max_drawdown_percentage_daily: 23432345,
749 sharpe_ratio: 23432343,
750 sortino_ratio: 123453234543,
751 };
752 let bt_metadata = BacktestMetaData::new(None, "testing", params, static_stats);
753
754 let mut buffer = Vec::new();
756 bt_metadata.encode(&mut buffer);
757
758 let mut cursor = Cursor::new(buffer.as_slice());
760 let mut decoded = Vec::new();
761 decoded.extend(BacktestMetaData::decode(&mut cursor));
762
763 assert_eq!(bt_metadata, decoded[0]);
765
766 Ok(())
767 }
768}