use std::{fmt, num::NonZero};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::{Error, Timeframe};
#[derive(Clone, Copy, Debug, Eq, Deserialize, Serialize)]
pub struct Candle {
pub timestamp: OffsetDateTime,
pub timeframe: Timeframe,
pub sources: NonZero<usize>,
pub open: Decimal,
pub high: Decimal,
pub low: Decimal,
pub close: Decimal,
pub volume: Decimal,
}
impl Candle {
#[allow(clippy::missing_panics_doc)]
pub fn merge<'a, I>(candles: I) -> Result<Self, Error>
where
I: IntoIterator<Item = &'a Self>,
{
let mut timestamp = Option::<OffsetDateTime>::None;
let mut timeframe = Option::<Timeframe>::None;
let mut sources = 0;
let mut open = Decimal::ZERO;
let mut high = Decimal::ZERO;
let mut low = Decimal::MAX;
let mut close = Decimal::ZERO;
let mut volume = Decimal::ZERO;
for (index, candle) in candles.into_iter().enumerate() {
if let Some(timestamp) = timestamp {
if timestamp != candle.timestamp {
return Err(Error::MergeTimestamp(index, timestamp, candle.timestamp));
}
} else {
timestamp = Some(candle.timestamp);
}
if let Some(timeframe) = timeframe {
if timeframe != candle.timeframe {
return Err(Error::MergeTimeframe(index, timeframe, candle.timeframe));
}
} else {
timeframe = Some(candle.timeframe);
}
sources += candle.sources.get();
volume += candle.volume;
open += candle.open * candle.volume;
high += candle.high * candle.volume;
low += candle.low * candle.volume;
close += candle.close * candle.volume;
}
let open = open / volume;
let high = high / volume;
let low = low / volume;
let close = close / volume;
match (timestamp, timeframe) {
(Some(timestamp), Some(timeframe)) => Ok(Self {
timestamp,
timeframe,
sources: NonZero::new(sources).unwrap(),
open,
high,
low,
close,
volume,
}),
_ => Err(Error::MergeEmpty),
}
}
#[must_use]
pub fn color(&self) -> Color {
if self.close > self.open {
Color::Green
} else {
Color::Red
}
}
#[must_use]
pub fn body(&self) -> Decimal {
self.close - self.open
}
#[must_use]
pub fn high_wick(&self) -> Decimal {
self.high - self.close.max(self.open)
}
#[must_use]
pub fn low_wick(&self) -> Decimal {
self.open.min(self.close) - self.low
}
#[must_use]
pub fn range(&self) -> Decimal {
self.high - self.low
}
#[must_use]
pub fn upper_shadow(&self) -> Decimal {
self.high - self.close.max(self.open)
}
#[must_use]
pub fn lower_shadow(&self) -> Decimal {
self.open.min(self.close) - self.low
}
}
impl PartialEq for Candle {
fn eq(&self, other: &Self) -> bool {
self.timestamp == other.timestamp && self.timeframe == other.timeframe
}
}
impl PartialOrd for Candle {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
match self.timestamp.cmp(&other.timestamp) {
std::cmp::Ordering::Equal => self.timeframe.partial_cmp(&other.timeframe),
ordering => Some(ordering),
}
}
}
impl Default for Candle {
fn default() -> Self {
Self {
timestamp: OffsetDateTime::UNIX_EPOCH,
timeframe: Timeframe::default(),
sources: NonZero::new(1).unwrap(),
open: Decimal::ZERO,
high: Decimal::ZERO,
low: Decimal::ZERO,
close: Decimal::ZERO,
volume: Decimal::ZERO,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Color {
Green,
Red,
}
impl fmt::Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Green => write!(f, "green"),
Self::Red => write!(f, "red"),
}
}
}