use serde::{Deserialize, Serialize};
use wickra_core::{
Candle as CoreCandle, CrossSection as CoreCrossSection, DerivativesTick as CoreDerivativesTick,
Level as CoreLevel, Member as CoreMember, OrderBook as CoreOrderBook, Side as CoreSide,
Trade as CoreTrade,
};
use crate::error::{BacktestError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Candle {
pub time: i64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
#[serde(default)]
pub volume: f64,
}
impl Candle {
pub fn to_core(self) -> Result<CoreCandle> {
CoreCandle::new(
self.open,
self.high,
self.low,
self.close,
self.volume,
self.time,
)
.map_err(|e| BacktestError::InvalidData(e.to_string()))
}
#[must_use]
pub fn hlc3(self) -> f64 {
(self.high + self.low + self.close) / 3.0
}
#[must_use]
pub fn ohlc4(self) -> f64 {
(self.open + self.high + self.low + self.close) / 4.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TradeSide {
Buy,
Sell,
}
impl TradeSide {
fn to_core(self) -> CoreSide {
match self {
TradeSide::Buy => CoreSide::Buy,
TradeSide::Sell => CoreSide::Sell,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct TradePrint {
pub price: f64,
pub size: f64,
pub side: TradeSide,
#[serde(default)]
pub timestamp: i64,
}
impl TradePrint {
pub fn to_core(self) -> Result<CoreTrade> {
CoreTrade::new(self.price, self.size, self.side.to_core(), self.timestamp)
.map_err(|e| BacktestError::InvalidData(e.to_string()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Level {
pub price: f64,
pub size: f64,
}
impl Level {
fn to_core(self) -> Result<CoreLevel> {
CoreLevel::new(self.price, self.size).map_err(|e| BacktestError::InvalidData(e.to_string()))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OrderBook {
pub bids: Vec<Level>,
pub asks: Vec<Level>,
}
impl OrderBook {
pub fn to_core(&self) -> Result<CoreOrderBook> {
let bids = self
.bids
.iter()
.map(|l| l.to_core())
.collect::<Result<Vec<_>>>()?;
let asks = self
.asks
.iter()
.map(|l| l.to_core())
.collect::<Result<Vec<_>>>()?;
CoreOrderBook::new(bids, asks).map_err(|e| BacktestError::InvalidData(e.to_string()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct DerivativesTick {
pub funding_rate: f64,
pub mark_price: f64,
pub index_price: f64,
pub futures_price: f64,
pub open_interest: f64,
pub long_size: f64,
pub short_size: f64,
pub taker_buy_volume: f64,
pub taker_sell_volume: f64,
pub long_liquidation: f64,
pub short_liquidation: f64,
#[serde(default)]
pub timestamp: i64,
}
impl DerivativesTick {
pub fn to_core(self) -> Result<CoreDerivativesTick> {
CoreDerivativesTick::new(
self.funding_rate,
self.mark_price,
self.index_price,
self.futures_price,
self.open_interest,
self.long_size,
self.short_size,
self.taker_buy_volume,
self.taker_sell_volume,
self.long_liquidation,
self.short_liquidation,
self.timestamp,
)
.map_err(|e| BacktestError::InvalidData(e.to_string()))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct CrossSectionMember {
pub change: f64,
pub volume: f64,
#[serde(default)]
pub new_high: bool,
#[serde(default)]
pub new_low: bool,
}
impl CrossSectionMember {
fn to_core(self) -> CoreMember {
CoreMember::new(self.change, self.volume, self.new_high, self.new_low)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CrossSection {
pub members: Vec<CrossSectionMember>,
#[serde(default)]
pub timestamp: i64,
}
impl CrossSection {
pub fn to_core(&self) -> Result<CoreCrossSection> {
let members: Vec<CoreMember> = self.members.iter().map(|m| m.to_core()).collect();
CoreCrossSection::new(members, self.timestamp)
.map_err(|e| BacktestError::InvalidData(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn converts_to_core() {
let c = Candle {
time: 1,
open: 10.0,
high: 12.0,
low: 9.0,
close: 11.0,
volume: 100.0,
};
assert!(c.to_core().is_ok());
}
#[test]
fn rejects_non_finite() {
let c = Candle {
time: 1,
open: f64::NAN,
high: 1.0,
low: 1.0,
close: 1.0,
volume: 0.0,
};
assert!(c.to_core().is_err());
}
#[test]
fn derived_prices() {
let c = Candle {
time: 0,
open: 4.0,
high: 6.0,
low: 2.0,
close: 4.0,
volume: 0.0,
};
assert!((c.hlc3() - 4.0).abs() < 1e-12);
assert!((c.ohlc4() - 4.0).abs() < 1e-12);
}
#[test]
fn volume_defaults_to_zero() {
let c: Candle =
serde_json::from_str(r#"{"time":0,"open":1,"high":1,"low":1,"close":1}"#).unwrap();
assert!(c.volume.abs() < f64::EPSILON);
}
#[test]
fn trade_converts_and_validates() {
let t = TradePrint {
price: 100.0,
size: 1.5,
side: TradeSide::Buy,
timestamp: 7,
};
assert!(t.to_core().is_ok());
let bad = TradePrint { price: -1.0, ..t };
assert!(bad.to_core().is_err());
}
#[test]
fn trade_deserializes_side() {
let t: TradePrint =
serde_json::from_str(r#"{"price":100,"size":1,"side":"sell"}"#).unwrap();
assert_eq!(t.side, TradeSide::Sell);
assert_eq!(t.timestamp, 0); }
#[test]
fn order_book_converts_and_rejects_crossed() {
let ob = OrderBook {
bids: vec![Level {
price: 100.0,
size: 2.0,
}],
asks: vec![Level {
price: 101.0,
size: 3.0,
}],
};
assert!(ob.to_core().is_ok());
let crossed = OrderBook {
bids: vec![Level {
price: 102.0,
size: 1.0,
}],
asks: vec![Level {
price: 101.0,
size: 1.0,
}],
};
assert!(crossed.to_core().is_err());
}
#[test]
fn derivatives_tick_converts() {
let d = DerivativesTick {
funding_rate: 0.0001,
mark_price: 100.0,
index_price: 99.9,
futures_price: 100.5,
open_interest: 1000.0,
long_size: 600.0,
short_size: 400.0,
taker_buy_volume: 50.0,
taker_sell_volume: 40.0,
long_liquidation: 1.0,
short_liquidation: 2.0,
timestamp: 1,
};
assert!(d.to_core().is_ok());
}
}