use quant_primitives::{Candle, CandleError};
use rust_decimal::Decimal;
use thiserror::Error;
#[derive(Debug, Error, PartialEq, Eq)]
pub enum RatioCandleError {
#[error("ratio candle synthesis requires non-empty series")]
EmptySeries,
#[error("series length mismatch: a={len_a}, b={len_b}")]
LengthMismatch { len_a: usize, len_b: usize },
#[error("timestamp mismatch at index {index}")]
MismatchedTimestamps { index: usize },
#[error("division by zero at index {index} (B candle has zero price)")]
DivisionByZero { index: usize },
#[error("candle construction failed at index {index}: {source}")]
CandleConstruction { index: usize, source: CandleError },
}
pub fn synthesize_ratio_candles(
a: &[Candle],
b: &[Candle],
) -> Result<Vec<Candle>, RatioCandleError> {
if a.is_empty() || b.is_empty() {
return Err(RatioCandleError::EmptySeries);
}
if a.len() != b.len() {
return Err(RatioCandleError::LengthMismatch {
len_a: a.len(),
len_b: b.len(),
});
}
let mut out = Vec::with_capacity(a.len());
for (i, (ca, cb)) in a.iter().zip(b.iter()).enumerate() {
if ca.timestamp() != cb.timestamp() {
return Err(RatioCandleError::MismatchedTimestamps { index: i });
}
if cb.open().is_zero() || cb.high().is_zero() || cb.low().is_zero() || cb.close().is_zero()
{
return Err(RatioCandleError::DivisionByZero { index: i });
}
let open = ca.open() / cb.open();
let close = ca.close() / cb.close();
let high = ca.high() / cb.low();
let low = ca.low() / cb.high();
let volume = ca.volume().min(cb.volume());
let candle = Candle::new(open, high, low, close, volume, ca.timestamp())
.map_err(|source| RatioCandleError::CandleConstruction { index: i, source })?;
out.push(candle);
}
debug_assert!(out.iter().all(|c| c.high() >= c.low()));
Ok(out)
}
pub fn ratio_close_series(
a: &[Candle],
b: &[Candle],
) -> Result<Vec<(chrono::DateTime<chrono::Utc>, Decimal)>, RatioCandleError> {
if a.is_empty() || b.is_empty() {
return Err(RatioCandleError::EmptySeries);
}
if a.len() != b.len() {
return Err(RatioCandleError::LengthMismatch {
len_a: a.len(),
len_b: b.len(),
});
}
let mut out = Vec::with_capacity(a.len());
for (i, (ca, cb)) in a.iter().zip(b.iter()).enumerate() {
if ca.timestamp() != cb.timestamp() {
return Err(RatioCandleError::MismatchedTimestamps { index: i });
}
if cb.close().is_zero() {
return Err(RatioCandleError::DivisionByZero { index: i });
}
out.push((ca.timestamp(), ca.close() / cb.close()));
}
Ok(out)
}
#[cfg(test)]
#[path = "ratio_candles_tests.rs"]
mod tests;