Skip to main content

data_preprocess/
display.rs

1use crate::models::{Bar, ImportResult, StatRow, Tick};
2
3/// Print stats table to stdout.
4pub fn print_stats(rows: &[StatRow], db_size: Option<u64>) {
5    if rows.is_empty() {
6        println!("No data found.");
7        return;
8    }
9
10    // Column widths
11    let w_ex = rows
12        .iter()
13        .map(|r| r.exchange.len())
14        .max()
15        .unwrap_or(8)
16        .max(8);
17    let w_sym = rows
18        .iter()
19        .map(|r| r.symbol.len())
20        .max()
21        .unwrap_or(6)
22        .max(6);
23    let w_type = rows
24        .iter()
25        .map(|r| r.data_type.len())
26        .max()
27        .unwrap_or(4)
28        .max(4);
29    let w_count = 12;
30    let w_ts = 19;
31
32    let header = format!(
33        " {:w_ex$} │ {:w_sym$} │ {:w_type$} │ {:>w_count$} │ {:w_ts$} │ {:w_ts$}",
34        "Exchange", "Symbol", "Type", "Count", "From", "To",
35    );
36    let sep = format!(
37        "─{:─>w_ex$}─┼─{:─>w_sym$}─┼─{:─>w_type$}─┼─{:─>w_count$}─┼─{:─>w_ts$}─┼─{:─>w_ts$}─",
38        "", "", "", "", "", "",
39    );
40
41    println!();
42    println!("{header}");
43    println!("{sep}");
44
45    for row in rows {
46        let ts_min = row.ts_min.format("%Y-%m-%d %H:%M:%S").to_string();
47        let ts_max = row.ts_max.format("%Y-%m-%d %H:%M:%S").to_string();
48        println!(
49            " {:w_ex$} │ {:w_sym$} │ {:w_type$} │ {:>w_count$} │ {:w_ts$} │ {:w_ts$}",
50            row.exchange,
51            row.symbol,
52            row.data_type,
53            format_count(row.count),
54            ts_min,
55            ts_max,
56        );
57    }
58
59    // Summary footer
60    let exchanges: std::collections::HashSet<&str> =
61        rows.iter().map(|r| r.exchange.as_str()).collect();
62    let symbols: std::collections::HashSet<&str> = rows.iter().map(|r| r.symbol.as_str()).collect();
63    println!();
64    print!(
65        " Total: {} dataset(s), {} exchange(s), {} symbol(s)",
66        rows.len(),
67        exchanges.len(),
68        symbols.len(),
69    );
70    if let Some(bytes) = db_size {
71        print!("  │  Database size: {}", format_bytes(bytes));
72    }
73    println!();
74}
75
76/// Print tick query results to stdout.
77pub fn print_ticks(exchange: &str, symbol: &str, ticks: &[Tick], total_count: u64) {
78    println!(
79        "\nExchange: {} │ Symbol: {} │ Ticks │ Showing {} of {}\n",
80        exchange,
81        symbol,
82        ticks.len(),
83        format_count(total_count),
84    );
85
86    if ticks.is_empty() {
87        println!("  (no data)");
88        return;
89    }
90
91    println!(
92        " {:26} │ {:>12} │ {:>12} │ {:>12} │ {:>10} │ {:>5}",
93        "Timestamp (UTC)", "Bid", "Ask", "Last", "Volume", "Flags",
94    );
95    println!(
96        "─{:─>26}─┼─{:─>12}─┼─{:─>12}─┼─{:─>12}─┼─{:─>10}─┼─{:─>5}─",
97        "", "", "", "", "", "",
98    );
99
100    for tick in ticks {
101        let ts = tick.ts.format("%Y-%m-%d %H:%M:%S%.3f").to_string();
102        println!(
103            " {:26} │ {:>12} │ {:>12} │ {:>12} │ {:>10} │ {:>5}",
104            ts,
105            fmt_opt_f64(tick.bid),
106            fmt_opt_f64(tick.ask),
107            fmt_opt_f64(tick.last),
108            fmt_opt_f64(tick.volume),
109            fmt_opt_i32(tick.flags),
110        );
111    }
112}
113
114/// Print bar query results to stdout.
115pub fn print_bars(exchange: &str, symbol: &str, tf: &str, bars: &[Bar], total_count: u64) {
116    println!(
117        "\nExchange: {} │ Symbol: {} │ Bars ({}) │ Showing {} of {}\n",
118        exchange,
119        symbol,
120        tf,
121        bars.len(),
122        format_count(total_count),
123    );
124
125    if bars.is_empty() {
126        println!("  (no data)");
127        return;
128    }
129
130    println!(
131        " {:19} │ {:>12} │ {:>12} │ {:>12} │ {:>12} │ {:>8} │ {:>8} │ {:>6}",
132        "Timestamp (UTC)", "Open", "High", "Low", "Close", "TickVol", "Vol", "Spread",
133    );
134    println!(
135        "─{:─>19}─┼─{:─>12}─┼─{:─>12}─┼─{:─>12}─┼─{:─>12}─┼─{:─>8}─┼─{:─>8}─┼─{:─>6}─",
136        "", "", "", "", "", "", "", "",
137    );
138
139    for bar in bars {
140        let ts = bar.ts.format("%Y-%m-%d %H:%M:%S").to_string();
141        println!(
142            " {:19} │ {:>12.2} │ {:>12.2} │ {:>12.2} │ {:>12.2} │ {:>8} │ {:>8} │ {:>6}",
143            ts, bar.open, bar.high, bar.low, bar.close, bar.tick_vol, bar.volume, bar.spread,
144        );
145    }
146}
147
148/// Print import result summary.
149pub fn print_import_result(result: &ImportResult) {
150    let elapsed = if result.elapsed.as_secs() >= 1 {
151        format!("{:.1}s", result.elapsed.as_secs_f64())
152    } else {
153        format!("{}ms", result.elapsed.as_millis())
154    };
155
156    println!("  Imported {}", result.file);
157    println!(
158        "  Exchange: {} │ Symbol: {}",
159        result.exchange, result.symbol,
160    );
161    println!(
162        "  Parsed: {} │ Inserted: {} │ Skipped (dup): {}",
163        format_count(result.rows_parsed as u64),
164        format_count(result.rows_inserted as u64),
165        format_count(result.rows_skipped as u64),
166    );
167    println!("  Elapsed: {elapsed}");
168}
169
170/// Print delete result.
171pub fn print_delete_result(data_type: &str, exchange: &str, detail: &str, count: usize) {
172    println!(
173        "Removed {} {} row(s) for {}/{}",
174        format_count(count as u64),
175        data_type,
176        exchange,
177        detail,
178    );
179}
180
181/// Format a byte count as human-readable (e.g. "42.3 MB").
182fn format_bytes(bytes: u64) -> String {
183    const KB: u64 = 1024;
184    const MB: u64 = 1024 * KB;
185    const GB: u64 = 1024 * MB;
186    if bytes >= GB {
187        format!("{:.1} GB", bytes as f64 / GB as f64)
188    } else if bytes >= MB {
189        format!("{:.1} MB", bytes as f64 / MB as f64)
190    } else if bytes >= KB {
191        format!("{:.1} KB", bytes as f64 / KB as f64)
192    } else {
193        format!("{} B", bytes)
194    }
195}
196
197/// Format a count with thousands separators.
198fn format_count(n: u64) -> String {
199    let s = n.to_string();
200    let mut result = String::with_capacity(s.len() + s.len() / 3);
201    for (i, c) in s.chars().rev().enumerate() {
202        if i > 0 && i % 3 == 0 {
203            result.push(',');
204        }
205        result.push(c);
206    }
207    result.chars().rev().collect()
208}
209
210fn fmt_opt_f64(v: Option<f64>) -> String {
211    v.map_or(String::new(), |f| format!("{:.2}", f))
212}
213
214fn fmt_opt_i32(v: Option<i32>) -> String {
215    v.map_or(String::new(), |i| i.to_string())
216}