use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bar {
pub time: DateTime<Utc>,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
}
impl Bar {
pub fn new(
time: DateTime<Utc>,
open: f64,
high: f64,
low: f64,
close: f64,
volume: f64,
) -> Self {
Self {
time,
open,
high,
low,
close,
volume,
}
}
#[inline]
pub fn is_bullish(&self) -> bool {
self.close > self.open
}
#[inline]
pub fn is_bearish(&self) -> bool {
self.close < self.open
}
pub fn is_doji(&self, threshold: f64) -> bool {
let range = self.range();
if range == 0.0 {
return true;
}
(self.close - self.open).abs() / range < threshold
}
#[inline]
pub fn body_height(&self) -> f64 {
(self.close - self.open).abs()
}
#[inline]
pub fn range(&self) -> f64 {
self.high - self.low
}
#[inline]
pub fn upper_wick(&self) -> f64 {
self.high - self.open.max(self.close)
}
#[inline]
pub fn lower_wick(&self) -> f64 {
self.open.min(self.close) - self.low
}
#[inline]
pub fn typical_price(&self) -> f64 {
(self.high + self.low + self.close) / 3.0
}
#[inline]
pub fn weighted_close(&self) -> f64 {
(self.high + self.low + self.close * 2.0) / 4.0
}
#[inline]
pub fn midpoint(&self) -> f64 {
(self.high + self.low) / 2.0
}
#[inline]
pub fn avg_price(&self) -> f64 {
(self.open + self.high + self.low + self.close) / 4.0
}
pub fn body_percentage(&self) -> f64 {
let range = self.range();
if range == 0.0 {
return 0.0;
}
self.body_height() / range
}
pub fn wick_ratio(&self) -> f64 {
let lower = self.lower_wick();
if lower == 0.0 {
return f64::INFINITY;
}
self.upper_wick() / lower
}
#[inline]
pub fn change(&self) -> f64 {
self.close - self.open
}
pub fn change_percent(&self) -> f64 {
if self.open == 0.0 {
return 0.0;
}
(self.close - self.open) / self.open * 100.0
}
#[inline]
pub fn body_top(&self) -> f64 {
self.open.max(self.close)
}
#[inline]
pub fn body_bottom(&self) -> f64 {
self.open.min(self.close)
}
}
impl fmt::Display for Bar {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Bar[{}, O:{:.2}, H:{:.2}, L:{:.2}, C:{:.2}, V:{:.2}]",
self.time.format("%Y-%m-%d %H:%M:%S"),
self.open,
self.high,
self.low,
self.close,
self.volume
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_bullish_bar() -> Bar {
Bar::new(Utc::now(), 100.0, 110.0, 95.0, 105.0, 1000.0)
}
fn sample_bearish_bar() -> Bar {
Bar::new(Utc::now(), 105.0, 110.0, 95.0, 100.0, 1000.0)
}
#[test]
fn test_bullish_bearish() {
let bullish = sample_bullish_bar();
let bearish = sample_bearish_bar();
assert!(bullish.is_bullish());
assert!(!bullish.is_bearish());
assert!(bearish.is_bearish());
assert!(!bearish.is_bullish());
}
#[test]
fn test_measurements() {
let bar = sample_bullish_bar();
assert_eq!(bar.body_height(), 5.0); assert_eq!(bar.range(), 15.0); assert_eq!(bar.upper_wick(), 5.0); assert_eq!(bar.lower_wick(), 5.0); }
#[test]
fn test_derived_prices() {
let bar = Bar::new(Utc::now(), 100.0, 120.0, 80.0, 110.0, 1000.0);
assert!((bar.typical_price() - 103.333).abs() < 0.01);
assert_eq!(bar.midpoint(), 100.0);
assert_eq!(bar.avg_price(), 102.5);
}
#[test]
fn test_change_percent() {
let bar = Bar::new(Utc::now(), 100.0, 110.0, 95.0, 105.0, 1000.0);
assert_eq!(bar.change_percent(), 5.0); }
#[test]
fn test_body_positions() {
let bullish = sample_bullish_bar();
assert_eq!(bullish.body_top(), 105.0);
assert_eq!(bullish.body_bottom(), 100.0);
let bearish = sample_bearish_bar();
assert_eq!(bearish.body_top(), 105.0);
assert_eq!(bearish.body_bottom(), 100.0);
}
}