use crate::export::{DataExporter, ExportResult};
use crate::types::{OHLC, Tick};
use csv::{Writer, WriterBuilder};
use std::fs::File;
use std::io::Write;
use std::path::Path;
#[derive(Debug, Clone)]
#[derive(Default)]
pub struct CsvExporter {
options: CsvOptions,
}
#[derive(Debug, Clone)]
pub struct CsvOptions {
pub include_headers: bool,
pub delimiter: u8,
pub quote_style: QuoteStyle,
pub buffer_capacity: usize,
}
#[derive(Debug, Clone, Copy)]
pub enum QuoteStyle {
Necessary,
Always,
Never,
}
impl Default for CsvOptions {
fn default() -> Self {
Self {
include_headers: true,
delimiter: b',',
quote_style: QuoteStyle::Necessary,
buffer_capacity: 8192,
}
}
}
impl CsvExporter {
pub fn new() -> Self {
Self::default()
}
pub fn with_options(options: CsvOptions) -> Self {
Self { options }
}
pub fn include_headers(mut self, include: bool) -> Self {
self.options.include_headers = include;
self
}
pub fn delimiter(mut self, delimiter: u8) -> Self {
self.options.delimiter = delimiter;
self
}
pub fn quote_style(mut self, style: QuoteStyle) -> Self {
self.options.quote_style = style;
self
}
fn create_writer<W: Write>(&self, writer: W) -> Writer<W> {
let mut builder = WriterBuilder::new();
builder
.delimiter(self.options.delimiter)
.has_headers(self.options.include_headers)
.buffer_capacity(self.options.buffer_capacity);
match self.options.quote_style {
QuoteStyle::Always => builder.quote_style(csv::QuoteStyle::Always),
QuoteStyle::Never => builder.quote_style(csv::QuoteStyle::Never),
QuoteStyle::Necessary => builder.quote_style(csv::QuoteStyle::Necessary),
};
builder.from_writer(writer)
}
pub fn write_ohlc<W: Write>(&self, data: &[OHLC], writer: W) -> ExportResult<()> {
let mut csv_writer = self.create_writer(writer);
if self.options.include_headers {
csv_writer.write_record([
"timestamp",
"open",
"high",
"low",
"close",
"volume",
])?;
}
for ohlc in data {
csv_writer.write_record(&[
ohlc.timestamp.to_string(),
ohlc.open.to_string(),
ohlc.high.to_string(),
ohlc.low.to_string(),
ohlc.close.to_string(),
ohlc.volume.value().to_string(),
])?;
}
csv_writer.flush()?;
Ok(())
}
pub fn write_ticks<W: Write>(&self, data: &[Tick], writer: W) -> ExportResult<()> {
let mut csv_writer = self.create_writer(writer);
if self.options.include_headers {
csv_writer.write_record([
"timestamp",
"price",
"volume",
"bid",
"ask",
])?;
}
for tick in data {
csv_writer.write_record(&[
tick.timestamp.to_string(),
tick.price.to_string(),
tick.volume.value().to_string(),
tick.bid.map_or(String::new(), |b| b.to_string()),
tick.ask.map_or(String::new(), |a| a.to_string()),
])?;
}
csv_writer.flush()?;
Ok(())
}
pub fn stream_ohlc<P: AsRef<Path>, I>(&self, data: I, path: P) -> ExportResult<usize>
where
I: IntoIterator<Item = OHLC>,
{
let file = File::create(path)?;
let mut csv_writer = self.create_writer(file);
let mut count = 0;
if self.options.include_headers {
csv_writer.write_record([
"timestamp",
"open",
"high",
"low",
"close",
"volume",
])?;
}
for ohlc in data {
csv_writer.write_record(&[
ohlc.timestamp.to_string(),
ohlc.open.to_string(),
ohlc.high.to_string(),
ohlc.low.to_string(),
ohlc.close.to_string(),
ohlc.volume.value().to_string(),
])?;
count += 1;
if count % 1000 == 0 {
csv_writer.flush()?;
}
}
csv_writer.flush()?;
Ok(count)
}
pub fn stream_ticks<P: AsRef<Path>, I>(&self, data: I, path: P) -> ExportResult<usize>
where
I: IntoIterator<Item = Tick>,
{
let file = File::create(path)?;
let mut csv_writer = self.create_writer(file);
let mut count = 0;
if self.options.include_headers {
csv_writer.write_record([
"timestamp",
"price",
"volume",
"bid",
"ask",
])?;
}
for tick in data {
csv_writer.write_record(&[
tick.timestamp.to_string(),
tick.price.to_string(),
tick.volume.value().to_string(),
tick.bid.map_or(String::new(), |b| b.to_string()),
tick.ask.map_or(String::new(), |a| a.to_string()),
])?;
count += 1;
if count % 1000 == 0 {
csv_writer.flush()?;
}
}
csv_writer.flush()?;
Ok(count)
}
}
impl DataExporter for CsvExporter {
fn export_ohlc<P: AsRef<Path>>(&self, data: &[OHLC], path: P) -> ExportResult<()> {
let file = File::create(path)?;
self.write_ohlc(data, file)
}
fn export_ticks<P: AsRef<Path>>(&self, data: &[Tick], path: P) -> ExportResult<()> {
let file = File::create(path)?;
self.write_ticks(data, file)
}
fn export_ohlc_to_writer<W: Write>(&self, data: &[OHLC], writer: W) -> ExportResult<()> {
self.write_ohlc(data, writer)
}
fn export_ticks_to_writer<W: Write>(&self, data: &[Tick], writer: W) -> ExportResult<()> {
self.write_ticks(data, writer)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
use rust_decimal::Decimal;
use crate::types::{OHLC, Tick};
use std::str::FromStr;
#[test]
fn test_csv_exporter_creation() {
let exporter = CsvExporter::new();
assert!(exporter.options.include_headers);
assert_eq!(exporter.options.delimiter, b',');
}
#[test]
fn test_csv_options_builder() {
let exporter = CsvExporter::new()
.include_headers(false)
.delimiter(b';')
.quote_style(QuoteStyle::Always);
assert!(!exporter.options.include_headers);
assert_eq!(exporter.options.delimiter, b';');
}
#[test]
fn test_write_ohlc_to_buffer() {
let exporter = CsvExporter::new();
let data = vec![
OHLC::new(Decimal::from(100), Decimal::from(105), Decimal::from(99), Decimal::from(103), 1000, 1234567890000),
OHLC::new(Decimal::from(103), Decimal::from(104), Decimal::from(101), Decimal::from(102), 1500, 1234567891000),
];
let mut buffer = Cursor::new(Vec::new());
exporter.write_ohlc(&data, &mut buffer).unwrap();
let csv_content = String::from_utf8(buffer.into_inner()).unwrap();
assert!(csv_content.contains("timestamp,open,high,low,close,volume"));
assert!(csv_content.contains("1234567890000,100,105,99,103,1000"));
assert!(csv_content.contains("1234567891000,103,104,101,102,1500"));
}
#[test]
fn test_write_ticks_to_buffer() {
let exporter = CsvExporter::new();
let data = vec![
Tick::new(Decimal::from_str("100.5").unwrap(), 500, 1234567890000),
Tick::with_spread(Decimal::from(101), 750, 1234567891000, Decimal::from_str("100.9").unwrap(), Decimal::from_str("101.1").unwrap()),
];
let mut buffer = Cursor::new(Vec::new());
exporter.write_ticks(&data, &mut buffer).unwrap();
let csv_content = String::from_utf8(buffer.into_inner()).unwrap();
assert!(csv_content.contains("timestamp,price,volume,bid,ask"));
assert!(csv_content.contains("1234567890000,100.5,500,,"));
assert!(csv_content.contains("1234567891000,101,750,100.9,101.1"));
}
#[test]
fn test_no_headers() {
let exporter = CsvExporter::new().include_headers(false);
let data = vec![OHLC::new(Decimal::from(100), Decimal::from(105), Decimal::from(99), Decimal::from(103), 1000, 1234567890000)];
let mut buffer = Cursor::new(Vec::new());
exporter.write_ohlc(&data, &mut buffer).unwrap();
let csv_content = String::from_utf8(buffer.into_inner()).unwrap();
assert!(!csv_content.contains("timestamp,open,high,low,close,volume"));
assert!(csv_content.contains("1234567890000,100,105,99,103,1000"));
}
#[test]
fn test_custom_delimiter() {
let exporter = CsvExporter::new().delimiter(b';');
let data = vec![OHLC::new(Decimal::from(100), Decimal::from(105), Decimal::from(99), Decimal::from(103), 1000, 1234567890000)];
let mut buffer = Cursor::new(Vec::new());
exporter.write_ohlc(&data, &mut buffer).unwrap();
let csv_content = String::from_utf8(buffer.into_inner()).unwrap();
assert!(csv_content.contains("timestamp;open;high;low;close;volume"));
assert!(csv_content.contains("1234567890000;100;105;99;103;1000"));
}
}