use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum CandleError {
#[error("high ({high}) must be >= low ({low})")]
HighBelowLow { high: Decimal, low: Decimal },
#[error("aggregation received empty OHLC data")]
EmptyAggregation,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VolumeSource {
TradeSize,
TickCount,
ExchangeReported,
#[default]
Unknown,
}
impl VolumeSource {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::TradeSize => "trade_size",
Self::TickCount => "tick_count",
Self::ExchangeReported => "exchange_reported",
Self::Unknown => "unknown",
}
}
#[must_use]
pub fn from_str_value(s: &str) -> Self {
match s {
"trade_size" => Self::TradeSize,
"tick_count" => Self::TickCount,
"exchange_reported" => Self::ExchangeReported,
_ => Self::Unknown,
}
}
}
impl fmt::Display for VolumeSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum CandleQuality {
#[default]
Complete,
Partial { reason: String },
}
impl CandleQuality {
#[must_use]
pub fn as_str(&self) -> &str {
match self {
Self::Complete => "complete",
Self::Partial { .. } => "partial",
}
}
#[must_use]
pub fn from_str_value(s: &str) -> Self {
match s {
"complete" => Self::Complete,
"partial" => Self::Partial {
reason: String::new(),
},
_ => Self::Complete,
}
}
}
impl fmt::Display for CandleQuality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Candle {
open: Decimal,
high: Decimal,
low: Decimal,
close: Decimal,
volume: Decimal,
timestamp: DateTime<Utc>,
#[serde(default)]
volume_source: VolumeSource,
#[serde(default)]
quality: CandleQuality,
}
impl Candle {
pub fn new(
open: Decimal,
high: Decimal,
low: Decimal,
close: Decimal,
volume: Decimal,
timestamp: DateTime<Utc>,
) -> Result<Self, CandleError> {
if high < low {
return Err(CandleError::HighBelowLow { high, low });
}
Ok(Self {
open,
high,
low,
close,
volume,
timestamp,
volume_source: VolumeSource::Unknown,
quality: CandleQuality::Complete,
})
}
#[must_use]
pub fn with_volume_source(mut self, source: VolumeSource) -> Self {
self.volume_source = source;
self
}
#[must_use]
pub fn with_quality(mut self, quality: CandleQuality) -> Self {
self.quality = quality;
self
}
#[must_use]
pub fn open(&self) -> Decimal {
self.open
}
#[must_use]
pub fn high(&self) -> Decimal {
self.high
}
#[must_use]
pub fn low(&self) -> Decimal {
self.low
}
#[must_use]
pub fn close(&self) -> Decimal {
self.close
}
#[must_use]
pub fn volume(&self) -> Decimal {
self.volume
}
#[must_use]
pub fn timestamp(&self) -> DateTime<Utc> {
self.timestamp
}
#[must_use]
pub fn volume_source(&self) -> VolumeSource {
self.volume_source
}
#[must_use]
pub fn quality(&self) -> &CandleQuality {
&self.quality
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CandleRange {
pub earliest: DateTime<Utc>,
pub latest: DateTime<Utc>,
}
impl CandleRange {
#[must_use]
pub fn from_candles(candles: &[Candle]) -> Option<Self> {
let earliest = candles.first()?.timestamp;
let latest = candles.last()?.timestamp;
Some(Self { earliest, latest })
}
}
#[cfg(test)]
#[path = "candle_tests.rs"]
mod tests;