1use std::fs;
2use std::path::Path;
3
4use anyhow::{Context, Result};
5use chrono::{DateTime, Utc};
6use csv::Writer;
7use rust_decimal::Decimal;
8use serde::Serialize;
9use tesser_data::analytics::{
10 analyze_execution, ExecutionAnalysisRequest, ExecutionReport, ExecutionStats,
11};
12
13pub fn run_execution(request: ExecutionAnalysisRequest, export_csv: Option<&Path>) -> Result<()> {
15 let report = analyze_execution(&request)?;
16 if let Some(path) = export_csv {
17 export_report(&report, path)?;
18 }
19 render_report(&report);
20 Ok(())
21}
22
23fn render_report(report: &ExecutionReport) {
24 println!("=== Execution Analysis ===");
25 println!(
26 "Period : {} -> {}",
27 format_period(report.period_start),
28 format_period(report.period_end)
29 );
30 println!(
31 "Orders : {} analyzed ({} skipped)",
32 report.totals.order_count, report.skipped_orders
33 );
34 println!("Fills : {}", report.totals.fill_count);
35 println!(
36 "Arrival Cover : {} ({})",
37 report.totals.orders_with_arrival,
38 arrival_percent(&report.totals)
39 );
40 if report.totals.order_count == 0 {
41 println!("No executions found for the requested window.");
42 return;
43 }
44
45 println!(
46 "Volume : {}",
47 format_decimal(&report.totals.filled_quantity, 4)
48 );
49 println!(
50 "Notional : {}",
51 format_decimal(&report.totals.notional, 2)
52 );
53 println!(
54 "Fees : {}",
55 format_decimal(&report.totals.total_fees, 6)
56 );
57 println!(
58 "Shortfall : {}",
59 format_decimal(&report.totals.implementation_shortfall, 6)
60 );
61 println!(
62 "Avg Slippage : {} bps",
63 format_optional(report.totals.avg_slippage_bps, 2)
64 );
65
66 if report.per_algo.is_empty() {
67 return;
68 }
69
70 println!("\nAlgo breakdown:");
71 println!(
72 "{:<12} {:>8} {:>12} {:>14} {:>12} {:>14} {:>14}",
73 "Algo", "Orders", "Quantity", "Notional", "Fees", "Shortfall", "Slippage(bps)"
74 );
75 for stats in &report.per_algo {
76 println!(
77 "{:<12} {:>8} {:>12} {:>14} {:>12} {:>14} {:>14}",
78 stats.label,
79 stats.order_count,
80 format_decimal(&stats.filled_quantity, 4),
81 format_decimal(&stats.notional, 2),
82 format_decimal(&stats.total_fees, 6),
83 format_decimal(&stats.implementation_shortfall, 6),
84 format_optional(stats.avg_slippage_bps, 2)
85 );
86 }
87}
88
89fn format_period(ts: Option<DateTime<Utc>>) -> String {
90 ts.map(|value| value.to_rfc3339())
91 .unwrap_or_else(|| "-".to_string())
92}
93
94fn format_decimal(value: &Decimal, scale: u32) -> String {
95 if value.is_zero() {
96 return "0".to_string();
97 }
98 value.round_dp(scale).normalize().to_string()
99}
100
101fn format_optional(value: Option<Decimal>, scale: u32) -> String {
102 value
103 .map(|v| format_decimal(&v, scale))
104 .unwrap_or_else(|| "-".to_string())
105}
106
107fn arrival_percent(stats: &ExecutionStats) -> String {
108 if stats.order_count == 0 {
109 return "-".to_string();
110 }
111 let pct = stats.orders_with_arrival as f64 / stats.order_count as f64 * 100.0;
112 format!("{pct:.0}%")
113}
114
115fn decimal_raw(value: &Decimal) -> String {
116 if value.is_zero() {
117 return "0".to_string();
118 }
119 value.normalize().to_string()
120}
121
122fn optional_decimal_raw(value: Option<Decimal>) -> String {
123 value.map(|v| decimal_raw(&v)).unwrap_or_default()
124}
125
126fn export_report(report: &ExecutionReport, output: &Path) -> Result<()> {
127 if let Some(parent) = output.parent() {
128 fs::create_dir_all(parent)
129 .with_context(|| format!("failed to create directory {}", parent.display()))?;
130 }
131 let mut writer = Writer::from_path(output)
132 .with_context(|| format!("failed to create {}", output.display()))?;
133 let rows = std::iter::once(&report.totals).chain(report.per_algo.iter());
134 for stats in rows {
135 let row = ExecutionCsvRow {
136 label: &stats.label,
137 order_count: stats.order_count,
138 fill_count: stats.fill_count,
139 orders_with_arrival: stats.orders_with_arrival,
140 filled_quantity: decimal_raw(&stats.filled_quantity),
141 notional: decimal_raw(&stats.notional),
142 total_fees: decimal_raw(&stats.total_fees),
143 implementation_shortfall: decimal_raw(&stats.implementation_shortfall),
144 avg_slippage_bps: optional_decimal_raw(stats.avg_slippage_bps),
145 };
146 writer.serialize(row)?;
147 }
148 writer.flush()?;
149 Ok(())
150}
151
152#[derive(Serialize)]
153struct ExecutionCsvRow<'a> {
154 label: &'a str,
155 order_count: usize,
156 fill_count: usize,
157 orders_with_arrival: usize,
158 filled_quantity: String,
159 notional: String,
160 total_fees: String,
161 implementation_shortfall: String,
162 avg_slippage_bps: String,
163}