use std::fmt;
use rust_decimal::Decimal;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "api-server")]
use utoipa::ToSchema;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "api-server", derive(ToSchema))]
pub struct OHLC {
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
pub close: Decimal,
pub volume: Volume,
pub timestamp: i64,
}
impl OHLC {
pub fn new(open: Decimal, high: Decimal, low: Decimal, close: Decimal, volume: u64, timestamp: i64) -> Self {
Self {
open,
high,
low,
close,
volume: Volume::new(volume),
timestamp,
}
}
pub fn is_valid(&self) -> bool {
let max = self.open.max(self.close);
let min = self.open.min(self.close);
self.high >= max && self.low <= min &&
self.high >= self.low &&
self.open > Decimal::ZERO && self.high > Decimal::ZERO &&
self.low > Decimal::ZERO && self.close > Decimal::ZERO
}
pub fn range(&self) -> Decimal {
self.high - self.low
}
pub fn body(&self) -> Decimal {
(self.close - self.open).abs()
}
pub fn is_bullish(&self) -> bool {
self.close > self.open
}
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "api-server", derive(ToSchema))]
pub struct Tick {
pub price: Decimal,
pub volume: Volume,
pub timestamp: i64,
pub bid: Option<Decimal>,
pub ask: Option<Decimal>,
}
impl Tick {
pub fn new(price: Decimal, volume: u64, timestamp: i64) -> Self {
Self {
price,
volume: Volume::new(volume),
timestamp,
bid: None,
ask: None,
}
}
pub fn with_spread(price: Decimal, volume: u64, timestamp: i64, bid: Decimal, ask: Decimal) -> Self {
Self {
price,
volume: Volume::new(volume),
timestamp,
bid: Some(bid),
ask: Some(ask),
}
}
pub fn spread(&self) -> Option<Decimal> {
match (self.bid, self.ask) {
(Some(bid), Some(ask)) => Some(ask - bid),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[cfg_attr(feature = "api-server", derive(ToSchema))]
pub struct Volume {
pub value: u64,
}
impl Volume {
pub fn new(value: u64) -> Self {
Self { value }
}
pub fn value(&self) -> u64 {
self.value
}
pub fn as_f64(&self) -> f64 {
self.value as f64
}
}
impl fmt::Display for Volume {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "api-server", derive(ToSchema))]
pub enum TimeInterval {
#[cfg_attr(feature = "serde", serde(rename = "1m"))]
OneMinute,
#[cfg_attr(feature = "serde", serde(rename = "5m"))]
FiveMinutes,
#[cfg_attr(feature = "serde", serde(rename = "15m"))]
FifteenMinutes,
#[cfg_attr(feature = "serde", serde(rename = "30m"))]
ThirtyMinutes,
#[cfg_attr(feature = "serde", serde(rename = "1h"))]
OneHour,
#[cfg_attr(feature = "serde", serde(rename = "4h"))]
FourHours,
#[cfg_attr(feature = "serde", serde(rename = "1d"))]
OneDay,
#[cfg_attr(feature = "serde", serde(rename = "custom"))]
Custom(u32),
}
impl TimeInterval {
pub fn seconds(&self) -> u32 {
match self {
TimeInterval::OneMinute => 60,
TimeInterval::FiveMinutes => 300,
TimeInterval::FifteenMinutes => 900,
TimeInterval::ThirtyMinutes => 1800,
TimeInterval::OneHour => 3600,
TimeInterval::FourHours => 14400,
TimeInterval::OneDay => 86400,
TimeInterval::Custom(s) => *s,
}
}
pub fn millis(&self) -> u64 {
self.seconds() as u64 * 1000
}
}
impl fmt::Display for TimeInterval {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TimeInterval::OneMinute => write!(f, "1m"),
TimeInterval::FiveMinutes => write!(f, "5m"),
TimeInterval::FifteenMinutes => write!(f, "15m"),
TimeInterval::ThirtyMinutes => write!(f, "30m"),
TimeInterval::OneHour => write!(f, "1h"),
TimeInterval::FourHours => write!(f, "4h"),
TimeInterval::OneDay => write!(f, "1d"),
TimeInterval::Custom(s) => write!(f, "{s}s"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::prelude::FromPrimitive;
#[test]
fn test_ohlc_creation() {
let ohlc = OHLC::new(
Decimal::from_f64(100.0).unwrap(),
Decimal::from_f64(105.0).unwrap(),
Decimal::from_f64(99.0).unwrap(),
Decimal::from_f64(103.0).unwrap(),
1000, 1234567890
);
assert_eq!(ohlc.open, Decimal::from_f64(100.0).unwrap());
assert_eq!(ohlc.high, Decimal::from_f64(105.0).unwrap());
assert_eq!(ohlc.low, Decimal::from_f64(99.0).unwrap());
assert_eq!(ohlc.close, Decimal::from_f64(103.0).unwrap());
assert_eq!(ohlc.volume.value(), 1000);
assert_eq!(ohlc.timestamp, 1234567890);
}
#[test]
fn test_ohlc_validation() {
let valid_ohlc = OHLC::new(
Decimal::from_f64(100.0).unwrap(),
Decimal::from_f64(105.0).unwrap(),
Decimal::from_f64(99.0).unwrap(),
Decimal::from_f64(103.0).unwrap(),
1000, 1234567890
);
assert!(valid_ohlc.is_valid());
let invalid_ohlc = OHLC::new(
Decimal::from_f64(100.0).unwrap(),
Decimal::from_f64(102.0).unwrap(),
Decimal::from_f64(99.0).unwrap(),
Decimal::from_f64(103.0).unwrap(),
1000, 1234567890
);
assert!(!invalid_ohlc.is_valid());
}
#[test]
fn test_ohlc_calculations() {
let ohlc = OHLC::new(
Decimal::from_f64(100.0).unwrap(),
Decimal::from_f64(105.0).unwrap(),
Decimal::from_f64(99.0).unwrap(),
Decimal::from_f64(103.0).unwrap(),
1000, 1234567890
);
assert_eq!(ohlc.range(), Decimal::from_f64(6.0).unwrap());
assert_eq!(ohlc.body(), Decimal::from_f64(3.0).unwrap());
assert!(ohlc.is_bullish());
let bearish = OHLC::new(
Decimal::from_f64(100.0).unwrap(),
Decimal::from_f64(102.0).unwrap(),
Decimal::from_f64(97.0).unwrap(),
Decimal::from_f64(98.0).unwrap(),
1000, 1234567890
);
assert!(!bearish.is_bullish());
}
#[test]
fn test_tick_creation() {
let tick = Tick::new(
Decimal::from_f64(100.5).unwrap(),
500, 1234567890
);
assert_eq!(tick.price, Decimal::from_f64(100.5).unwrap());
assert_eq!(tick.volume.value(), 500);
assert_eq!(tick.timestamp, 1234567890);
assert!(tick.bid.is_none());
assert!(tick.ask.is_none());
}
#[test]
fn test_tick_with_spread() {
let tick = Tick::with_spread(
Decimal::from_f64(100.5).unwrap(),
500, 1234567890,
Decimal::from_f64(100.4).unwrap(),
Decimal::from_f64(100.6).unwrap()
);
assert_eq!(tick.bid, Some(Decimal::from_f64(100.4).unwrap()));
assert_eq!(tick.ask, Some(Decimal::from_f64(100.6).unwrap()));
let spread = tick.spread().unwrap();
assert_eq!(spread, Decimal::from_f64(0.2).unwrap());
}
#[test]
fn test_time_interval() {
assert_eq!(TimeInterval::OneMinute.seconds(), 60);
assert_eq!(TimeInterval::FiveMinutes.seconds(), 300);
assert_eq!(TimeInterval::OneHour.seconds(), 3600);
assert_eq!(TimeInterval::OneDay.seconds(), 86400);
assert_eq!(TimeInterval::Custom(120).seconds(), 120);
assert_eq!(TimeInterval::OneMinute.millis(), 60000);
}
#[test]
fn test_volume() {
let vol = Volume::new(1500);
assert_eq!(vol.value(), 1500);
assert_eq!(vol.as_f64(), 1500.0);
assert_eq!(format!("{vol}"), "1500");
}
#[cfg(feature = "serde")]
mod serde_tests {
use super::*;
use serde_json;
#[test]
fn test_ohlc_serialization() {
let ohlc = OHLC::new(
Decimal::from_f64(100.0).unwrap(),
Decimal::from_f64(105.0).unwrap(),
Decimal::from_f64(99.0).unwrap(),
Decimal::from_f64(103.0).unwrap(),
1000, 1234567890
);
let json = serde_json::to_string(&ohlc).unwrap();
let deserialized: OHLC = serde_json::from_str(&json).unwrap();
assert_eq!(ohlc, deserialized);
}
#[test]
fn test_tick_serialization() {
let tick = Tick::with_spread(
Decimal::from_f64(100.5).unwrap(),
500, 1234567890,
Decimal::from_f64(100.4).unwrap(),
Decimal::from_f64(100.6).unwrap()
);
let json = serde_json::to_string(&tick).unwrap();
let deserialized: Tick = serde_json::from_str(&json).unwrap();
assert_eq!(tick, deserialized);
}
#[test]
fn test_volume_serialization() {
let volume = Volume::new(1500);
let json = serde_json::to_string(&volume).unwrap();
assert_eq!(json, "1500");
let deserialized: Volume = serde_json::from_str(&json).unwrap();
assert_eq!(volume, deserialized);
}
#[test]
fn test_time_interval_serialization() {
let interval = TimeInterval::OneMinute;
let json = serde_json::to_string(&interval).unwrap();
assert_eq!(json, r#""1m""#);
let deserialized: TimeInterval = serde_json::from_str(&json).unwrap();
assert_eq!(interval, deserialized);
let custom = TimeInterval::Custom(120);
let json = serde_json::to_string(&custom).unwrap();
let deserialized: TimeInterval = serde_json::from_str(&json).unwrap();
assert_eq!(custom, deserialized);
}
}
}