Skip to main content

data_preprocess/
convert.rs

1//! Bidirectional conversion between Polars DataFrames and domain types (Tick, Bar).
2
3use chrono::NaiveDateTime;
4use polars::prelude::*;
5
6use crate::error::Result;
7use crate::models::{Bar, PriceBar, StoredTick, Tick, Timeframe};
8
9/// Convert a slice of Ticks into a Polars DataFrame.
10pub fn ticks_to_dataframe(ticks: &[Tick]) -> Result<DataFrame> {
11    let exchanges: Vec<&str> = ticks.iter().map(|t| t.exchange.as_str()).collect();
12    let symbols: Vec<&str> = ticks.iter().map(|t| t.symbol.as_str()).collect();
13    let timestamps: Vec<i64> = ticks
14        .iter()
15        .map(|t| t.ts.and_utc().timestamp_micros())
16        .collect();
17    let bids: Vec<Option<f64>> = ticks.iter().map(|t| t.bid).collect();
18    let asks: Vec<Option<f64>> = ticks.iter().map(|t| t.ask).collect();
19    let lasts: Vec<Option<f64>> = ticks.iter().map(|t| t.last).collect();
20    let volumes: Vec<Option<f64>> = ticks.iter().map(|t| t.volume).collect();
21    let flags: Vec<Option<i32>> = ticks.iter().map(|t| t.flags).collect();
22
23    let df = DataFrame::new(vec![
24        Column::new("exchange".into(), &exchanges),
25        Column::new("symbol".into(), &symbols),
26        Column::new("ts".into(), &timestamps)
27            .cast(&DataType::Datetime(TimeUnit::Microseconds, None))
28            .map_err(crate::error::DataError::Polars)?,
29        Column::new("bid".into(), &bids),
30        Column::new("ask".into(), &asks),
31        Column::new("last".into(), &lasts),
32        Column::new("volume".into(), &volumes),
33        Column::new("flags".into(), &flags),
34    ])?;
35
36    Ok(df)
37}
38
39/// Convert a Polars DataFrame back into `Vec<Tick>`.
40pub fn dataframe_to_ticks(df: &DataFrame) -> Result<Vec<Tick>> {
41    let exchanges = df.column("exchange")?.str()?;
42    let symbols = df.column("symbol")?.str()?;
43    let ts_col = df.column("ts")?.datetime()?;
44    let bids = df.column("bid")?.f64()?;
45    let asks = df.column("ask")?.f64()?;
46    let lasts = df.column("last")?.f64()?;
47    let volumes = df.column("volume")?.f64()?;
48    let flags = df.column("flags")?.i32()?;
49
50    let mut ticks = Vec::with_capacity(df.height());
51    for i in 0..df.height() {
52        let ts_micros = ts_col.get(i).ok_or_else(|| {
53            crate::error::DataError::InvalidTimestamp("null timestamp in dataframe".into())
54        })?;
55        let ts = micros_to_ndt(ts_micros);
56
57        ticks.push(Tick {
58            exchange: exchanges.get(i).unwrap_or("").to_string(),
59            symbol: symbols.get(i).unwrap_or("").to_string(),
60            ts,
61            bid: bids.get(i),
62            ask: asks.get(i),
63            last: lasts.get(i),
64            volume: volumes.get(i),
65            flags: flags.get(i),
66        });
67    }
68    Ok(ticks)
69}
70
71pub fn stored_ticks_to_dataframe(ticks: &[StoredTick]) -> Result<DataFrame> {
72    for tick in ticks {
73        tick.validate()?;
74    }
75    let plain = ticks
76        .iter()
77        .map(|value| value.tick.clone())
78        .collect::<Vec<_>>();
79    let mut frame = ticks_to_dataframe(&plain)?;
80    frame.with_column(Column::new(
81        "source_ordinal".into(),
82        ticks
83            .iter()
84            .map(|value| value.source_ordinal)
85            .collect::<Vec<_>>(),
86    ))?;
87    frame.with_column(Column::new(
88        "source_identity".into(),
89        ticks
90            .iter()
91            .map(|value| value.source_identity.as_deref())
92            .collect::<Vec<_>>(),
93    ))?;
94    frame.with_column(Column::new(
95        "provider_sequence".into(),
96        ticks
97            .iter()
98            .map(|value| value.provider_sequence)
99            .collect::<Vec<_>>(),
100    ))?;
101    Ok(frame)
102}
103
104pub fn dataframe_to_stored_ticks(df: &DataFrame) -> Result<Vec<StoredTick>> {
105    let ticks = dataframe_to_ticks(df)?;
106    let ordinals = df.column("source_ordinal")?.u64()?;
107    let identities = df.column("source_identity")?.str()?;
108    let sequences = df.column("provider_sequence")?.u64()?;
109    ticks
110        .into_iter()
111        .enumerate()
112        .map(|(index, tick)| {
113            let value = StoredTick {
114                tick,
115                source_ordinal: ordinals
116                    .get(index)
117                    .ok_or_else(|| crate::DataError::Other("stored tick ordinal is null".into()))?,
118                source_identity: identities.get(index).map(str::to_owned),
119                provider_sequence: sequences.get(index),
120            };
121            value.validate()?;
122            Ok(value)
123        })
124        .collect()
125}
126
127/// Convert a slice of Bars into a Polars DataFrame.
128pub fn bars_to_dataframe(bars: &[Bar]) -> Result<DataFrame> {
129    let exchanges: Vec<&str> = bars.iter().map(|b| b.exchange.as_str()).collect();
130    let symbols: Vec<&str> = bars.iter().map(|b| b.symbol.as_str()).collect();
131    let timeframes: Vec<&str> = bars.iter().map(|b| b.timeframe.as_str()).collect();
132    let timestamps: Vec<i64> = bars
133        .iter()
134        .map(|b| b.ts.and_utc().timestamp_micros())
135        .collect();
136    let opens: Vec<f64> = bars.iter().map(|b| b.open).collect();
137    let highs: Vec<f64> = bars.iter().map(|b| b.high).collect();
138    let lows: Vec<f64> = bars.iter().map(|b| b.low).collect();
139    let closes: Vec<f64> = bars.iter().map(|b| b.close).collect();
140    let tick_vols: Vec<i64> = bars.iter().map(|b| b.tick_vol).collect();
141    let volumes: Vec<i64> = bars.iter().map(|b| b.volume).collect();
142    let spreads: Vec<i32> = bars.iter().map(|b| b.spread).collect();
143
144    let df = DataFrame::new(vec![
145        Column::new("exchange".into(), &exchanges),
146        Column::new("symbol".into(), &symbols),
147        Column::new("timeframe".into(), &timeframes),
148        Column::new("ts".into(), &timestamps)
149            .cast(&DataType::Datetime(TimeUnit::Microseconds, None))
150            .map_err(crate::error::DataError::Polars)?,
151        Column::new("open".into(), &opens),
152        Column::new("high".into(), &highs),
153        Column::new("low".into(), &lows),
154        Column::new("close".into(), &closes),
155        Column::new("tick_vol".into(), &tick_vols),
156        Column::new("volume".into(), &volumes),
157        Column::new("spread".into(), &spreads),
158    ])?;
159
160    Ok(df)
161}
162
163/// Convert a Polars DataFrame back into `Vec<Bar>`.
164pub fn dataframe_to_bars(df: &DataFrame) -> Result<Vec<Bar>> {
165    let exchanges = df.column("exchange")?.str()?;
166    let symbols = df.column("symbol")?.str()?;
167    let timeframes = df.column("timeframe")?.str()?;
168    let ts_col = df.column("ts")?.datetime()?;
169    let opens = df.column("open")?.f64()?;
170    let highs = df.column("high")?.f64()?;
171    let lows = df.column("low")?.f64()?;
172    let closes = df.column("close")?.f64()?;
173    let tick_vols = df.column("tick_vol")?.i64()?;
174    let volumes = df.column("volume")?.i64()?;
175    let spreads = df.column("spread")?.i32()?;
176
177    let mut bars = Vec::with_capacity(df.height());
178    for i in 0..df.height() {
179        let ts_micros = ts_col.get(i).ok_or_else(|| {
180            crate::error::DataError::InvalidTimestamp("null timestamp in dataframe".into())
181        })?;
182        let ts = micros_to_ndt(ts_micros);
183        let tf_str = timeframes.get(i).unwrap_or("1m");
184        let timeframe = Timeframe::parse(tf_str).unwrap_or(Timeframe::M1);
185
186        bars.push(Bar {
187            exchange: exchanges.get(i).unwrap_or("").to_string(),
188            symbol: symbols.get(i).unwrap_or("").to_string(),
189            timeframe,
190            ts,
191            open: opens.get(i).unwrap_or(0.0),
192            high: highs.get(i).unwrap_or(0.0),
193            low: lows.get(i).unwrap_or(0.0),
194            close: closes.get(i).unwrap_or(0.0),
195            tick_vol: tick_vols.get(i).unwrap_or(0),
196            volume: volumes.get(i).unwrap_or(0),
197            spread: spreads.get(i).unwrap_or(0),
198        });
199    }
200    Ok(bars)
201}
202
203pub fn price_bars_to_dataframe(bars: &[PriceBar]) -> Result<DataFrame> {
204    for bar in bars {
205        bar.validate()?;
206    }
207    let timestamps = bars
208        .iter()
209        .map(|bar| bar.ts.and_utc().timestamp_micros())
210        .collect::<Vec<_>>();
211    let available = bars
212        .iter()
213        .map(|bar| bar.available_at.and_utc().timestamp_micros())
214        .collect::<Vec<_>>();
215    Ok(DataFrame::new(vec![
216        Column::new(
217            "exchange".into(),
218            bars.iter()
219                .map(|bar| bar.exchange.as_str())
220                .collect::<Vec<_>>(),
221        ),
222        Column::new(
223            "symbol".into(),
224            bars.iter()
225                .map(|bar| bar.symbol.as_str())
226                .collect::<Vec<_>>(),
227        ),
228        Column::new(
229            "timeframe".into(),
230            bars.iter()
231                .map(|bar| bar.timeframe.as_str())
232                .collect::<Vec<_>>(),
233        ),
234        Column::new("ts".into(), &timestamps)
235            .cast(&DataType::Datetime(TimeUnit::Microseconds, None))?,
236        Column::new("available_at".into(), &available)
237            .cast(&DataType::Datetime(TimeUnit::Microseconds, None))?,
238        Column::new(
239            "open".into(),
240            bars.iter().map(|bar| bar.open).collect::<Vec<_>>(),
241        ),
242        Column::new(
243            "high".into(),
244            bars.iter().map(|bar| bar.high).collect::<Vec<_>>(),
245        ),
246        Column::new(
247            "low".into(),
248            bars.iter().map(|bar| bar.low).collect::<Vec<_>>(),
249        ),
250        Column::new(
251            "close".into(),
252            bars.iter().map(|bar| bar.close).collect::<Vec<_>>(),
253        ),
254        Column::new(
255            "tick_count".into(),
256            bars.iter().map(|bar| bar.tick_count).collect::<Vec<_>>(),
257        ),
258        Column::new(
259            "spread".into(),
260            bars.iter().map(|bar| bar.spread).collect::<Vec<_>>(),
261        ),
262    ])?)
263}
264
265pub fn dataframe_to_price_bars(df: &DataFrame) -> Result<Vec<PriceBar>> {
266    if df.column("tick_vol").is_ok() {
267        return Err(crate::DataError::Other(
268            "price-only frame conflicts with legacy tick_vol".into(),
269        ));
270    }
271    let exchanges = df.column("exchange")?.str()?;
272    let symbols = df.column("symbol")?.str()?;
273    let timeframes = df.column("timeframe")?.str()?;
274    let timestamps = df.column("ts")?.datetime()?;
275    let available = df.column("available_at")?.datetime()?;
276    let opens = df.column("open")?.f64()?;
277    let highs = df.column("high")?.f64()?;
278    let lows = df.column("low")?.f64()?;
279    let closes = df.column("close")?.f64()?;
280    let counts = df.column("tick_count")?.u64()?;
281    let spreads = df.column("spread")?.i32()?;
282    (0..df.height())
283        .map(|index| {
284            let value = PriceBar {
285                exchange: exchanges.get(index).unwrap_or("").into(),
286                symbol: symbols.get(index).unwrap_or("").into(),
287                timeframe: Timeframe::parse(timeframes.get(index).unwrap_or("1m"))?,
288                ts: micros_to_ndt(timestamps.get(index).ok_or_else(|| {
289                    crate::DataError::InvalidTimestamp("null price bar timestamp".into())
290                })?),
291                available_at: micros_to_ndt(available.get(index).ok_or_else(|| {
292                    crate::DataError::InvalidTimestamp("null availability timestamp".into())
293                })?),
294                open: opens.get(index).unwrap_or(0.0),
295                high: highs.get(index).unwrap_or(0.0),
296                low: lows.get(index).unwrap_or(0.0),
297                close: closes.get(index).unwrap_or(0.0),
298                tick_count: counts.get(index),
299                spread: spreads.get(index),
300            };
301            value.validate()?;
302            Ok(value)
303        })
304        .collect()
305}
306
307/// Convert microsecond epoch to NaiveDateTime.
308fn micros_to_ndt(micros: i64) -> NaiveDateTime {
309    let secs = micros / 1_000_000;
310    let nsecs = ((micros % 1_000_000) * 1_000) as u32;
311    chrono::DateTime::from_timestamp(secs, nsecs)
312        .map(|dt| dt.naive_utc())
313        .unwrap_or_default()
314}
315
316/// Extract the date portion from a NaiveDateTime as a formatted string.
317pub fn ndt_to_date_string(ndt: &NaiveDateTime) -> String {
318    ndt.format("%Y-%m-%d").to_string()
319}