use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi(object))]
pub struct IntradayCandle {
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: i64,
pub average: Option<f64>,
pub date: String,
}
impl IntradayCandle {
pub fn is_bullish(&self) -> bool {
self.close > self.open
}
pub fn is_bearish(&self) -> bool {
self.close < self.open
}
pub fn body(&self) -> f64 {
(self.close - self.open).abs()
}
pub fn range(&self) -> f64 {
self.high - self.low
}
pub fn upper_wick(&self) -> f64 {
self.high - self.close.max(self.open)
}
pub fn lower_wick(&self) -> f64 {
self.close.min(self.open) - self.low
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi(object))]
pub struct IntradayCandlesResponse {
pub date: String,
#[serde(rename = "type")]
pub data_type: Option<String>,
pub exchange: Option<String>,
pub market: Option<String>,
pub symbol: String,
pub timeframe: Option<String>,
#[serde(default)]
pub data: Vec<IntradayCandle>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi(object))]
pub struct HistoricalCandle {
pub date: String,
pub open: f64,
pub high: f64,
pub low: f64,
pub close: f64,
pub volume: i64,
pub turnover: Option<f64>,
pub change: Option<f64>,
}
impl HistoricalCandle {
pub fn is_bullish(&self) -> bool {
self.close > self.open
}
pub fn is_bearish(&self) -> bool {
self.close < self.open
}
pub fn body(&self) -> f64 {
(self.close - self.open).abs()
}
pub fn range(&self) -> f64 {
self.high - self.low
}
pub fn change_percent(&self, prev_close: f64) -> f64 {
if prev_close == 0.0 {
return 0.0;
}
(self.close - prev_close) / prev_close * 100.0
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "python", pyo3::prelude::pyclass)]
#[cfg_attr(feature = "js", napi_derive::napi(object))]
pub struct HistoricalCandlesResponse {
pub symbol: String,
#[serde(rename = "type")]
pub data_type: Option<String>,
pub exchange: Option<String>,
pub market: Option<String>,
pub timeframe: Option<String>,
pub adjusted: Option<bool>,
#[serde(default)]
pub data: Vec<HistoricalCandle>,
}
impl HistoricalCandlesResponse {
pub fn highest_high(&self) -> Option<f64> {
self.data.iter().map(|c| c.high).fold(None, |acc, h| {
Some(acc.map_or(h, |a: f64| a.max(h)))
})
}
pub fn lowest_low(&self) -> Option<f64> {
self.data.iter().map(|c| c.low).fold(None, |acc, l| {
Some(acc.map_or(l, |a: f64| a.min(l)))
})
}
pub fn total_volume(&self) -> i64 {
self.data.iter().map(|c| c.volume).sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_intraday_candle_deserialization() {
let json = r#"{
"open": 580.0,
"high": 585.0,
"low": 578.0,
"close": 583.0,
"volume": 10000,
"average": 581.5,
"date": "2024-01-15T09:00:00.000+08:00"
}"#;
let candle: IntradayCandle = serde_json::from_str(json).unwrap();
assert_eq!(candle.open, 580.0);
assert_eq!(candle.close, 583.0);
assert!(candle.is_bullish());
assert_eq!(candle.range(), 7.0);
}
#[test]
fn test_intraday_candles_response() {
let json = r#"{
"date": "2024-01-15",
"type": "EQUITY",
"exchange": "TWSE",
"market": "TSE",
"symbol": "2330",
"timeframe": "5",
"data": [
{"open": 580.0, "high": 582.0, "low": 579.0, "close": 581.0, "volume": 5000, "date": "2024-01-15T09:00:00.000+08:00"},
{"open": 581.0, "high": 585.0, "low": 580.0, "close": 584.0, "volume": 8000, "date": "2024-01-15T09:05:00.000+08:00"}
]
}"#;
let response: IntradayCandlesResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.symbol, "2330");
assert_eq!(response.timeframe.as_deref(), Some("5"));
assert_eq!(response.data.len(), 2);
}
#[test]
fn test_historical_candle_deserialization() {
let json = r#"{
"date": "2024-01-15",
"open": 580.0,
"high": 590.0,
"low": 575.0,
"close": 588.0,
"volume": 50000000,
"turnover": 29000000000,
"change": 8.0
}"#;
let candle: HistoricalCandle = serde_json::from_str(json).unwrap();
assert_eq!(candle.date, "2024-01-15");
assert_eq!(candle.close, 588.0);
assert!(candle.is_bullish());
}
#[test]
fn test_historical_candles_response() {
let json = r#"{
"symbol": "2330",
"type": "EQUITY",
"timeframe": "D",
"adjusted": true,
"data": [
{"date": "2024-01-12", "open": 570.0, "high": 580.0, "low": 568.0, "close": 578.0, "volume": 40000000},
{"date": "2024-01-15", "open": 580.0, "high": 590.0, "low": 575.0, "close": 588.0, "volume": 50000000}
]
}"#;
let response: HistoricalCandlesResponse = serde_json::from_str(json).unwrap();
assert_eq!(response.symbol, "2330");
assert_eq!(response.adjusted, Some(true));
assert_eq!(response.highest_high(), Some(590.0));
assert_eq!(response.lowest_low(), Some(568.0));
assert_eq!(response.total_volume(), 90000000);
}
#[test]
fn test_candle_patterns() {
let bullish = IntradayCandle {
open: 100.0,
high: 105.0,
low: 99.0,
close: 104.0,
volume: 1000,
average: None,
date: String::new(),
};
assert!(bullish.is_bullish());
assert_eq!(bullish.body(), 4.0);
assert_eq!(bullish.upper_wick(), 1.0); assert_eq!(bullish.lower_wick(), 1.0);
let bearish = IntradayCandle {
open: 104.0,
high: 105.0,
low: 99.0,
close: 100.0,
volume: 1000,
average: None,
date: String::new(),
};
assert!(bearish.is_bearish());
assert_eq!(bearish.body(), 4.0);
assert_eq!(bearish.upper_wick(), 1.0); assert_eq!(bearish.lower_wick(), 1.0); }
}