use std::fmt::{self, Display};
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Timeframe {
#[serde(rename = "1s")]
Second1,
#[serde(rename = "5s")]
Second5,
#[serde(rename = "1m")]
Minute1,
#[serde(rename = "5m")]
Minute5,
#[serde(rename = "15m")]
Minute15,
#[serde(rename = "30m")]
Minute30,
#[serde(rename = "1h")]
Hour1,
#[serde(rename = "4h")]
Hour4,
#[serde(rename = "1d")]
Day1,
}
impl Display for Timeframe {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Timeframe::Second1 => write!(f, "1s"),
Timeframe::Second5 => write!(f, "5s"),
Timeframe::Minute1 => write!(f, "1m"),
Timeframe::Minute5 => write!(f, "5m"),
Timeframe::Minute15 => write!(f, "15m"),
Timeframe::Minute30 => write!(f, "30m"),
Timeframe::Hour1 => write!(f, "1h"),
Timeframe::Hour4 => write!(f, "4h"),
Timeframe::Day1 => write!(f, "1d"),
}
}
}
impl FromStr for Timeframe {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"1s" => Ok(Timeframe::Second1),
"5s" => Ok(Timeframe::Second5),
"1m" => Ok(Timeframe::Minute1),
"5m" => Ok(Timeframe::Minute5),
"15m" => Ok(Timeframe::Minute15),
"30m" => Ok(Timeframe::Minute30),
"1h" => Ok(Timeframe::Hour1),
"4h" => Ok(Timeframe::Hour4),
"1d" => Ok(Timeframe::Day1),
_ => Err(format!("Unknown timeframe: {s}")),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiCandle {
pub time: i64,
pub low: f64,
pub high: f64,
pub open: f64,
pub close: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub volume: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub trade_count: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CandleData {
pub candle: ApiCandle,
pub symbol: String,
pub timeframe: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CandlesQueryParams {
pub symbol: String,
pub timeframe: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_time: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub end_time: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
impl CandlesQueryParams {
pub fn new(symbol: impl Into<String>, timeframe: Timeframe) -> Self {
Self {
symbol: symbol.into(),
timeframe: timeframe.to_string(),
..Default::default()
}
}
pub fn with_start_time(mut self, start_time_ms: i64) -> Self {
self.start_time = Some(start_time_ms);
self
}
pub fn with_end_time(mut self, end_time_ms: i64) -> Self {
self.end_time = Some(end_time_ms);
self
}
pub fn with_limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timeframe_display() {
assert_eq!(Timeframe::Minute1.to_string(), "1m");
assert_eq!(Timeframe::Hour4.to_string(), "4h");
assert_eq!(Timeframe::Day1.to_string(), "1d");
}
#[test]
fn test_timeframe_from_str() {
assert_eq!("1m".parse::<Timeframe>().unwrap(), Timeframe::Minute1);
assert_eq!("4h".parse::<Timeframe>().unwrap(), Timeframe::Hour4);
assert_eq!("1d".parse::<Timeframe>().unwrap(), Timeframe::Day1);
assert!("invalid".parse::<Timeframe>().is_err());
}
#[test]
fn test_timeframe_serde() {
let tf = Timeframe::Minute5;
let json = serde_json::to_string(&tf).unwrap();
assert_eq!(json, r#""5m""#);
let parsed: Timeframe = serde_json::from_str(r#""5m""#).unwrap();
assert_eq!(parsed, Timeframe::Minute5);
}
#[test]
fn test_deserialize_candle_data() {
let json = r#"{
"candle": {
"time": 1727181985,
"low": 149.80,
"high": 151.50,
"open": 150.25,
"close": 150.90,
"volume": 1234.56,
"tradeCount": 89
},
"symbol": "SOL",
"timeframe": "1m"
}"#;
let data: CandleData = serde_json::from_str(json).unwrap();
assert_eq!(data.symbol, "SOL");
assert_eq!(data.timeframe, "1m");
assert_eq!(data.candle.time, 1727181985);
assert_eq!(data.candle.open, 150.25);
assert_eq!(data.candle.close, 150.90);
assert_eq!(data.candle.volume, Some(1234.56));
assert_eq!(data.candle.trade_count, Some(89));
}
}