1use chrono::NaiveDateTime;
4use polars::prelude::*;
5
6use crate::error::Result;
7use crate::models::{Bar, PriceBar, StoredTick, Tick, Timeframe};
8
9pub 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(), ×tamps)
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
39pub 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
127pub 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(), ×tamps)
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
163pub 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(), ×tamps)
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
307fn 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
316pub fn ndt_to_date_string(ndt: &NaiveDateTime) -> String {
318 ndt.format("%Y-%m-%d").to_string()
319}