use serde::{Deserialize, Serialize};
use crate::error::{ExchangeError, Result};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Candle {
pub time: i64,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: f64,
}
impl Candle {
pub fn from_raw(arr: &[serde_json::Value]) -> Result<Self> {
if arr.len() < 6 {
return Err(ExchangeError::InsufficientData(format!(
"candle array too short: expected ≥6 elements, got {}",
arr.len()
)));
}
let time = arr[0].as_i64().ok_or_else(|| {
ExchangeError::InsufficientData(format!(
"candle[0] (timestamp) is not an integer: {:?}",
arr[0]
))
})?;
let num = |i: usize, field: &str| -> Result<f64> {
let v = &arr[i];
if let Some(s) = v.as_str() {
s.parse::<f64>().map_err(|_| {
ExchangeError::InsufficientData(format!(
"candle[{i}] ({field}) could not be parsed as f64: {s:?}"
))
})
} else {
v.as_f64().ok_or_else(|| {
ExchangeError::InsufficientData(format!(
"candle[{i}] ({field}) is not a number: {v:?}"
))
})
}
};
Ok(Self {
time,
open: num(1, "open")?,
high: num(2, "high")?,
low: num(3, "low")?,
close: num(4, "close")?,
volume: num(5, "volume")?,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Side {
Buy,
Sell,
}
impl Side {
pub const fn as_str(self) -> &'static str {
match self {
Self::Buy => "buy",
Self::Sell => "sell",
}
}
#[must_use]
pub const fn flip(self) -> Self {
match self {
Self::Buy => Self::Sell,
Self::Sell => Self::Buy,
}
}
}
impl std::fmt::Display for Side {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OrderType {
Market,
Limit,
}
impl OrderType {
pub const fn as_str(self) -> &'static str {
match self {
Self::Market => "market",
Self::Limit => "limit",
}
}
}
impl std::fmt::Display for OrderType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum TimeInForce {
#[default]
GTC,
GTT,
IOC,
FOK,
}
impl TimeInForce {
pub const fn as_str(self) -> &'static str {
match self {
Self::GTC => "GTC",
Self::GTT => "GTT",
Self::IOC => "IOC",
Self::FOK => "FOK",
}
}
}
impl std::fmt::Display for TimeInForce {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum STP {
DC,
CO,
CN,
CB,
}
impl STP {
pub const fn as_str(self) -> &'static str {
match self {
Self::DC => "DC",
Self::CO => "CO",
Self::CN => "CN",
Self::CB => "CB",
}
}
}
impl std::fmt::Display for STP {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn candle_parse_string_fields() {
let arr = serde_json::json!([
1_713_000_000_000_i64,
"86000.0",
"87000.0",
"85000.0",
"86500.0",
"1234.5"
]);
let row = arr.as_array().unwrap();
let c = Candle::from_raw(row).expect("should parse");
assert_eq!(c.time, 1_713_000_000_000);
assert!((c.open - 86000.0).abs() < 1e-9);
assert!((c.close - 86500.0).abs() < 1e-9);
}
#[test]
fn candle_too_short_returns_err() {
let arr = serde_json::json!([1_713_000_000_000_i64, "86000.0"]);
let row = arr.as_array().unwrap();
let err = Candle::from_raw(row).unwrap_err();
assert!(matches!(err, ExchangeError::InsufficientData(_)));
assert!(err.to_string().contains("too short"));
}
#[test]
fn candle_bad_field_returns_err_with_context() {
let arr = serde_json::json!([
1_713_000_000_000_i64,
"not-a-number",
"87000.0",
"85000.0",
"86500.0",
"1234.5"
]);
let row = arr.as_array().unwrap();
let err = Candle::from_raw(row).unwrap_err();
assert!(matches!(err, ExchangeError::InsufficientData(_)));
assert!(err.to_string().contains("open"));
}
#[test]
fn side_flip() {
assert_eq!(Side::Buy.flip(), Side::Sell);
assert_eq!(Side::Sell.flip(), Side::Buy);
}
}