use std::collections::HashMap;
use serde_json::json;
use crate::core::{
ExchangeResult, ExchangeError,
};
pub struct DhanWebSocket {
access_token: String,
}
impl DhanWebSocket {
pub fn new(access_token: String) -> Self {
Self { access_token }
}
pub fn build_url(&self) -> String {
format!("wss://api-feed.dhan.co?token={}&version=2", self.access_token)
}
pub fn build_subscription(&self, request_code: u8, instruments: Vec<(u8, &str)>) -> String {
let instrument_list: Vec<_> = instruments
.iter()
.map(|(segment, security_id)| {
json!({
"ExchangeSegment": segment,
"SecurityId": security_id
})
})
.collect();
json!({
"RequestCode": request_code,
"InstrumentCount": instruments.len(),
"InstrumentList": instrument_list
})
.to_string()
}
pub fn parse_ticker_packet(&self, data: &[u8]) -> ExchangeResult<HashMap<String, f64>> {
if data.len() < 52 {
return Err(ExchangeError::Parse(format!(
"Invalid ticker packet size: {} (expected 52)",
data.len()
)));
}
use byteorder::{LittleEndian, ByteOrder};
let mut result = HashMap::new();
let exchange_segment = LittleEndian::read_u16(&data[0..2]);
result.insert("exchange_segment".to_string(), exchange_segment as f64);
let security_id = LittleEndian::read_u32(&data[2..6]);
result.insert("security_id".to_string(), security_id as f64);
let ltp = LittleEndian::read_f32(&data[6..10]);
result.insert("ltp".to_string(), ltp as f64);
let volume = LittleEndian::read_i32(&data[10..14]);
result.insert("volume".to_string(), volume as f64);
let open_interest = LittleEndian::read_i32(&data[14..18]);
result.insert("open_interest".to_string(), open_interest as f64);
Ok(result)
}
pub fn parse_quote_packet(&self, data: &[u8]) -> ExchangeResult<HashMap<String, f64>> {
if data.len() < 56 {
return Err(ExchangeError::Parse(format!(
"Invalid quote packet size: {} (expected at least 56)",
data.len()
)));
}
use byteorder::{LittleEndian, ByteOrder};
let mut result = HashMap::new();
let packet_type = LittleEndian::read_u16(&data[0..2]);
result.insert("packet_type".to_string(), packet_type as f64);
let exchange_segment = LittleEndian::read_u16(&data[2..4]);
result.insert("exchange_segment".to_string(), exchange_segment as f64);
let security_id = LittleEndian::read_u32(&data[4..8]);
result.insert("security_id".to_string(), security_id as f64);
let ltp = LittleEndian::read_f64(&data[8..16]);
result.insert("ltp".to_string(), ltp);
let high = LittleEndian::read_f64(&data[16..24]);
result.insert("high".to_string(), high);
let low = LittleEndian::read_f64(&data[24..32]);
result.insert("low".to_string(), low);
let open = LittleEndian::read_f64(&data[32..40]);
result.insert("open".to_string(), open);
let close = LittleEndian::read_f64(&data[40..48]);
result.insert("close".to_string(), close);
let volume = LittleEndian::read_f64(&data[48..56]);
result.insert("volume".to_string(), volume);
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_build_url() {
let ws = DhanWebSocket::new("test_token".to_string());
let url = ws.build_url();
assert!(url.contains("wss://api-feed.dhan.co"));
assert!(url.contains("token=test_token"));
}
#[test]
fn test_build_subscription() {
let ws = DhanWebSocket::new("test_token".to_string());
let sub = ws.build_subscription(15, vec![(0, "1333")]);
assert!(sub.contains("RequestCode"));
assert!(sub.contains("InstrumentCount"));
assert!(sub.contains("1333"));
}
#[test]
fn test_parse_ticker_packet_invalid_size() {
let ws = DhanWebSocket::new("test_token".to_string());
let data = vec![0u8; 10];
let result = ws.parse_ticker_packet(&data);
assert!(result.is_err());
}
}