use crate::errors::QlResult;
use crate::require;
use crate::time::date::Date;
use crate::time::timeseries::TimeSeries;
use crate::types::Real;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct IntervalPrice {
open: Real,
high: Real,
low: Real,
close: Real,
}
impl IntervalPrice {
pub fn new(open: Real, high: Real, low: Real, close: Real) -> QlResult<Self> {
require!(
[open, high, low, close]
.iter()
.all(|price| price.is_finite()),
"OHLC prices must be finite"
);
require!(
low <= open && open <= high && low <= close && close <= high,
"OHLC open and close must lie within low and high"
);
Ok(Self {
open,
high,
low,
close,
})
}
pub fn open(self) -> Real {
self.open
}
pub fn high(self) -> Real {
self.high
}
pub fn low(self) -> Real {
self.low
}
pub fn close(self) -> Real {
self.close
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct PriceSeries {
values: TimeSeries<IntervalPrice>,
}
impl PriceSeries {
pub fn from_slices(
dates: &[Date],
opens: &[Real],
highs: &[Real],
lows: &[Real],
closes: &[Real],
) -> QlResult<Self> {
require!(
[opens.len(), highs.len(), lows.len(), closes.len()]
.iter()
.all(|len| *len == dates.len()),
"OHLC slices must have the same length as dates"
);
let mut values = TimeSeries::new();
for ((((&date, &open), &high), &low), &close) in
dates.iter().zip(opens).zip(highs).zip(lows).zip(closes)
{
values.insert(date, IntervalPrice::new(open, high, low, close)?)?;
}
Ok(Self { values })
}
pub fn get(&self, date: Date) -> Option<&IntervalPrice> {
self.values.get(date)
}
pub fn len(&self) -> usize {
self.values.len()
}
pub fn is_empty(&self) -> bool {
self.values.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = (Date, &IntervalPrice)> {
self.values.iter()
}
pub fn opens(&self) -> TimeSeries<Real> {
self.values.map_values(|price| price.open())
}
pub fn highs(&self) -> TimeSeries<Real> {
self.values.map_values(|price| price.high())
}
pub fn lows(&self) -> TimeSeries<Real> {
self.values.map_values(|price| price.low())
}
pub fn closes(&self) -> TimeSeries<Real> {
self.values.map_values(|price| price.close())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn interval_price_validates_finiteness_and_bounds() {
let price = IntervalPrice::new(-10.0, -8.0, -12.0, -9.0).unwrap();
assert_eq!(
(price.open(), price.high(), price.low(), price.close()),
(-10.0, -8.0, -12.0, -9.0)
);
for bad in [
(-13.0, -8.0, -12.0, -9.0),
(-10.0, -11.0, -12.0, -9.0),
(-10.0, -8.0, -9.0, -12.0),
(Real::NAN, -8.0, -12.0, -9.0),
(-10.0, Real::INFINITY, -12.0, -9.0),
(-10.0, -8.0, Real::NEG_INFINITY, -9.0),
] {
assert!(IntervalPrice::new(bad.0, bad.1, bad.2, bad.3).is_err());
}
}
#[test]
fn price_series_sorts_replaces_and_extracts_each_field() {
let early = Date::from_serial(45_000);
let late = Date::from_serial(45_001);
let dates = [late, early, late];
let opens = [10.0, 8.0, 12.0];
let highs = [13.0, 11.0, 15.0];
let lows = [9.0, 7.0, 11.0];
let closes = [11.0, 9.0, 14.0];
let series = PriceSeries::from_slices(&dates, &opens, &highs, &lows, &closes).unwrap();
assert_eq!(dates, [late, early, late]);
assert_eq!(opens, [10.0, 8.0, 12.0]);
assert_eq!(series.len(), 2);
assert_eq!(
series.iter().map(|(date, _)| date).collect::<Vec<_>>(),
[early, late]
);
assert_eq!(series.get(late).map(|price| price.close()), Some(14.0));
assert_eq!(
series.opens().iter().collect::<Vec<_>>(),
[(early, &8.0), (late, &12.0)]
);
assert_eq!(
series.highs().iter().collect::<Vec<_>>(),
[(early, &11.0), (late, &15.0)]
);
assert_eq!(
series.lows().iter().collect::<Vec<_>>(),
[(early, &7.0), (late, &11.0)]
);
assert_eq!(
series.closes().iter().collect::<Vec<_>>(),
[(early, &9.0), (late, &14.0)]
);
}
#[test]
fn empty_and_invalid_price_series_are_safe() {
let empty = PriceSeries::from_slices(&[], &[], &[], &[], &[]).unwrap();
assert!(empty.is_empty());
assert_eq!(empty.get(Date::from_serial(45_000)), None);
assert!(empty.closes().is_empty());
let date = Date::from_serial(45_000);
assert!(PriceSeries::from_slices(&[date], &[1.0], &[], &[0.0], &[1.0]).is_err());
assert!(PriceSeries::from_slices(&[Date::null()], &[1.0], &[2.0], &[0.0], &[1.0]).is_err());
assert!(PriceSeries::from_slices(&[date], &[1.0], &[2.0], &[0.0], &[Real::NAN]).is_err());
}
}