1use serde::{Deserialize, Serialize};
10use wickra_core::{
11 Candle as CoreCandle, CrossSection as CoreCrossSection, DerivativesTick as CoreDerivativesTick,
12 Level as CoreLevel, Member as CoreMember, OrderBook as CoreOrderBook, Side as CoreSide,
13 Trade as CoreTrade,
14};
15
16use crate::error::{BacktestError, Result};
17
18#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
21pub struct Candle {
22 pub time: i64,
24 pub open: f64,
26 pub high: f64,
28 pub low: f64,
30 pub close: f64,
32 #[serde(default)]
34 pub volume: f64,
35}
36
37impl Candle {
38 pub fn to_core(self) -> Result<CoreCandle> {
41 CoreCandle::new(
42 self.open,
43 self.high,
44 self.low,
45 self.close,
46 self.volume,
47 self.time,
48 )
49 .map_err(|e| BacktestError::InvalidData(e.to_string()))
50 }
51
52 #[must_use]
54 pub fn hlc3(self) -> f64 {
55 (self.high + self.low + self.close) / 3.0
56 }
57
58 #[must_use]
60 pub fn ohlc4(self) -> f64 {
61 (self.open + self.high + self.low + self.close) / 4.0
62 }
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case")]
68pub enum TradeSide {
69 Buy,
71 Sell,
73}
74
75impl TradeSide {
76 fn to_core(self) -> CoreSide {
77 match self {
78 TradeSide::Buy => CoreSide::Buy,
79 TradeSide::Sell => CoreSide::Sell,
80 }
81 }
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
86pub struct TradePrint {
87 pub price: f64,
89 pub size: f64,
91 pub side: TradeSide,
93 #[serde(default)]
95 pub timestamp: i64,
96}
97
98impl TradePrint {
99 pub fn to_core(self) -> Result<CoreTrade> {
101 CoreTrade::new(self.price, self.size, self.side.to_core(), self.timestamp)
102 .map_err(|e| BacktestError::InvalidData(e.to_string()))
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
108pub struct Level {
109 pub price: f64,
111 pub size: f64,
113}
114
115impl Level {
116 fn to_core(self) -> Result<CoreLevel> {
117 CoreLevel::new(self.price, self.size).map_err(|e| BacktestError::InvalidData(e.to_string()))
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124pub struct OrderBook {
125 pub bids: Vec<Level>,
127 pub asks: Vec<Level>,
129}
130
131impl OrderBook {
132 pub fn to_core(&self) -> Result<CoreOrderBook> {
135 let bids = self
136 .bids
137 .iter()
138 .map(|l| l.to_core())
139 .collect::<Result<Vec<_>>>()?;
140 let asks = self
141 .asks
142 .iter()
143 .map(|l| l.to_core())
144 .collect::<Result<Vec<_>>>()?;
145 CoreOrderBook::new(bids, asks).map_err(|e| BacktestError::InvalidData(e.to_string()))
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
151pub struct DerivativesTick {
152 pub funding_rate: f64,
154 pub mark_price: f64,
156 pub index_price: f64,
158 pub futures_price: f64,
160 pub open_interest: f64,
162 pub long_size: f64,
164 pub short_size: f64,
166 pub taker_buy_volume: f64,
168 pub taker_sell_volume: f64,
170 pub long_liquidation: f64,
172 pub short_liquidation: f64,
174 #[serde(default)]
176 pub timestamp: i64,
177}
178
179impl DerivativesTick {
180 pub fn to_core(self) -> Result<CoreDerivativesTick> {
182 CoreDerivativesTick::new(
183 self.funding_rate,
184 self.mark_price,
185 self.index_price,
186 self.futures_price,
187 self.open_interest,
188 self.long_size,
189 self.short_size,
190 self.taker_buy_volume,
191 self.taker_sell_volume,
192 self.long_liquidation,
193 self.short_liquidation,
194 self.timestamp,
195 )
196 .map_err(|e| BacktestError::InvalidData(e.to_string()))
197 }
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
202pub struct CrossSectionMember {
203 pub change: f64,
205 pub volume: f64,
207 #[serde(default)]
209 pub new_high: bool,
210 #[serde(default)]
212 pub new_low: bool,
213}
214
215impl CrossSectionMember {
216 fn to_core(self) -> CoreMember {
217 CoreMember::new(self.change, self.volume, self.new_high, self.new_low)
220 }
221}
222
223#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226pub struct CrossSection {
227 pub members: Vec<CrossSectionMember>,
229 #[serde(default)]
231 pub timestamp: i64,
232}
233
234impl CrossSection {
235 pub fn to_core(&self) -> Result<CoreCrossSection> {
238 let members: Vec<CoreMember> = self.members.iter().map(|m| m.to_core()).collect();
239 CoreCrossSection::new(members, self.timestamp)
240 .map_err(|e| BacktestError::InvalidData(e.to_string()))
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn converts_to_core() {
250 let c = Candle {
251 time: 1,
252 open: 10.0,
253 high: 12.0,
254 low: 9.0,
255 close: 11.0,
256 volume: 100.0,
257 };
258 assert!(c.to_core().is_ok());
259 }
260
261 #[test]
262 fn rejects_non_finite() {
263 let c = Candle {
264 time: 1,
265 open: f64::NAN,
266 high: 1.0,
267 low: 1.0,
268 close: 1.0,
269 volume: 0.0,
270 };
271 assert!(c.to_core().is_err());
272 }
273
274 #[test]
275 fn derived_prices() {
276 let c = Candle {
277 time: 0,
278 open: 4.0,
279 high: 6.0,
280 low: 2.0,
281 close: 4.0,
282 volume: 0.0,
283 };
284 assert!((c.hlc3() - 4.0).abs() < 1e-12);
285 assert!((c.ohlc4() - 4.0).abs() < 1e-12);
286 }
287
288 #[test]
289 fn volume_defaults_to_zero() {
290 let c: Candle =
291 serde_json::from_str(r#"{"time":0,"open":1,"high":1,"low":1,"close":1}"#).unwrap();
292 assert!(c.volume.abs() < f64::EPSILON);
293 }
294
295 #[test]
296 fn trade_converts_and_validates() {
297 let t = TradePrint {
298 price: 100.0,
299 size: 1.5,
300 side: TradeSide::Buy,
301 timestamp: 7,
302 };
303 assert!(t.to_core().is_ok());
304 let bad = TradePrint { price: -1.0, ..t };
305 assert!(bad.to_core().is_err());
306 }
307
308 #[test]
309 fn trade_deserializes_side() {
310 let t: TradePrint =
311 serde_json::from_str(r#"{"price":100,"size":1,"side":"sell"}"#).unwrap();
312 assert_eq!(t.side, TradeSide::Sell);
313 assert_eq!(t.timestamp, 0); }
315
316 #[test]
317 fn order_book_converts_and_rejects_crossed() {
318 let ob = OrderBook {
319 bids: vec![Level {
320 price: 100.0,
321 size: 2.0,
322 }],
323 asks: vec![Level {
324 price: 101.0,
325 size: 3.0,
326 }],
327 };
328 assert!(ob.to_core().is_ok());
329 let crossed = OrderBook {
330 bids: vec![Level {
331 price: 102.0,
332 size: 1.0,
333 }],
334 asks: vec![Level {
335 price: 101.0,
336 size: 1.0,
337 }],
338 };
339 assert!(crossed.to_core().is_err());
340 }
341
342 #[test]
343 fn derivatives_tick_converts() {
344 let d = DerivativesTick {
345 funding_rate: 0.0001,
346 mark_price: 100.0,
347 index_price: 99.9,
348 futures_price: 100.5,
349 open_interest: 1000.0,
350 long_size: 600.0,
351 short_size: 400.0,
352 taker_buy_volume: 50.0,
353 taker_sell_volume: 40.0,
354 long_liquidation: 1.0,
355 short_liquidation: 2.0,
356 timestamp: 1,
357 };
358 assert!(d.to_core().is_ok());
359 }
360}