1#[derive(Debug, Clone)]
15pub struct MarketEvent {
16 pub event_id: String,
18 pub event_date: u64,
20 pub event_type: String,
22 pub description: String,
24}
25
26#[derive(Debug, Clone, Copy)]
28pub struct EventWindow {
29 pub pre_days: i32,
31 pub post_days: i32,
33}
34
35#[derive(Debug, Clone)]
37pub struct AbnormalReturn {
38 pub day: i32,
40 pub raw_return: f64,
42 pub expected_return: f64,
44 pub abnormal_return: f64,
46 pub car: f64,
48}
49
50#[derive(Debug, Clone)]
52pub struct EventResult {
53 pub event: MarketEvent,
55 pub car_pre: f64,
57 pub car_post: f64,
59 pub peak_day: i32,
61 pub trough_day: i32,
63 pub abnormal_returns: Vec<AbnormalReturn>,
65}
66
67pub struct EventStudy;
69
70impl EventStudy {
71 pub fn compute(
82 event: &MarketEvent,
83 price_series: &[(u64, f64)],
84 benchmark: &[(u64, f64)],
85 window: EventWindow,
86 ) -> EventResult {
87 let sec_returns = daily_log_returns(price_series, event.event_date);
89 let bmk_returns = daily_log_returns(benchmark, event.event_date);
90
91 let mut abnormal_returns: Vec<AbnormalReturn> = Vec::new();
92 let mut cumulative = 0.0f64;
93
94 let day_start = window.pre_days;
95 let day_end = window.post_days;
96
97 for d in day_start..=day_end {
98 let raw = sec_returns.get(&d).copied().unwrap_or(0.0);
99 let exp = bmk_returns.get(&d).copied().unwrap_or(0.0);
100 let ar = raw - exp;
101 cumulative += ar;
102 abnormal_returns.push(AbnormalReturn {
103 day: d,
104 raw_return: raw,
105 expected_return: exp,
106 abnormal_return: ar,
107 car: cumulative,
108 });
109 }
110
111 let car_pre: f64 = abnormal_returns
113 .iter()
114 .filter(|ar| ar.day >= window.pre_days && ar.day < 0)
115 .map(|ar| ar.abnormal_return)
116 .sum();
117
118 let car_post: f64 = abnormal_returns
120 .iter()
121 .filter(|ar| ar.day >= 1 && ar.day <= window.post_days)
122 .map(|ar| ar.abnormal_return)
123 .sum();
124
125 let (peak_day, trough_day) = abnormal_returns.iter().fold(
127 (0i32, 0i32),
128 |(peak_d, trough_d), ar| {
129 let peak_car = abnormal_returns.iter().find(|x| x.day == peak_d).map(|x| x.car).unwrap_or(0.0);
130 let trough_car = abnormal_returns.iter().find(|x| x.day == trough_d).map(|x| x.car).unwrap_or(0.0);
131 let new_peak = if ar.car > peak_car { ar.day } else { peak_d };
132 let new_trough = if ar.car < trough_car { ar.day } else { trough_d };
133 (new_peak, new_trough)
134 },
135 );
136
137 EventResult {
138 event: event.clone(),
139 car_pre,
140 car_post,
141 peak_day,
142 trough_day,
143 abnormal_returns,
144 }
145 }
146
147 pub fn significance(results: &[EventResult]) -> f64 {
154 let n = results.len();
155 if n < 2 {
156 return 0.0;
157 }
158 let cars: Vec<f64> = results
159 .iter()
160 .map(|r| r.car_pre + r.car_post)
161 .collect();
162 let mean = cars.iter().sum::<f64>() / n as f64;
163 let variance = cars.iter().map(|c| (c - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
164 let std_dev = variance.sqrt();
165 if std_dev < 1e-15 {
166 return 0.0;
167 }
168 mean / (std_dev / (n as f64).sqrt())
169 }
170}
171
172fn daily_log_returns(series: &[(u64, f64)], event_date: u64) -> std::collections::HashMap<i32, f64> {
177 use std::collections::HashMap;
178
179 if series.len() < 2 {
180 return HashMap::new();
181 }
182
183 let event_idx = series.partition_point(|&(ts, _)| ts < event_date);
185 let event_idx = event_idx.min(series.len() - 1);
187
188 let mut map = HashMap::new();
189
190 for i in 1..series.len() {
191 let p_prev = series[i - 1].1;
192 let p_curr = series[i].1;
193 let log_ret = if p_prev > 0.0 && p_curr > 0.0 {
194 (p_curr / p_prev).ln()
195 } else {
196 0.0
197 };
198 let day = i as i32 - event_idx as i32;
201 map.insert(day, log_ret);
202 }
203 map
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 fn synthetic_prices(n: usize, event_idx: usize, drift: f64, vol: f64) -> Vec<(u64, f64)> {
213 let mut prices = Vec::with_capacity(n);
214 let mut p = 100.0f64;
215 let base_ts: u64 = 1_000_000;
216 let day_secs: u64 = 86_400;
217 for i in 0..n {
219 if i > 0 {
220 let sign = if i % 2 == 0 { 1.0 } else { -1.0 };
222 p *= (drift + sign * vol).exp();
223 }
224 prices.push((base_ts + i as u64 * day_secs, p));
225 }
226 let _ = event_idx; prices
228 }
229
230 fn make_event(date: u64) -> MarketEvent {
231 MarketEvent {
232 event_id: "EVT001".into(),
233 event_date: date,
234 event_type: "earnings".into(),
235 description: "Q3 earnings release".into(),
236 }
237 }
238
239 #[test]
240 fn test_compute_returns_correct_window_length() {
241 let prices = synthetic_prices(30, 15, 0.001, 0.005);
242 let bench = synthetic_prices(30, 15, 0.0005, 0.003);
243 let event_ts = prices[15].0;
244 let event = make_event(event_ts);
245 let window = EventWindow { pre_days: -5, post_days: 5 };
246 let result = EventStudy::compute(&event, &prices, &bench, window);
247 assert_eq!(result.abnormal_returns.len(), 11); }
249
250 #[test]
251 fn test_car_accumulates_correctly() {
252 let prices = synthetic_prices(20, 10, 0.001, 0.003);
253 let bench = synthetic_prices(20, 10, 0.001, 0.003); let event_ts = prices[10].0;
255 let event = make_event(event_ts);
256 let window = EventWindow { pre_days: -3, post_days: 3 };
257 let result = EventStudy::compute(&event, &prices, &bench, window);
258 for ar in &result.abnormal_returns {
260 assert!(ar.car.abs() < 1e-10, "CAR should be ~0 when security==benchmark");
261 }
262 }
263
264 #[test]
265 fn test_abnormal_return_equals_raw_minus_expected() {
266 let prices: Vec<(u64, f64)> = (0..20u64).map(|i| (1_000_000 + i * 86_400, 100.0 + i as f64)).collect();
267 let bench: Vec<(u64, f64)> = (0..20u64).map(|i| (1_000_000 + i * 86_400, 100.0 + i as f64 * 0.5)).collect();
268 let event_ts = 1_000_000 + 10 * 86_400;
269 let event = make_event(event_ts);
270 let window = EventWindow { pre_days: -2, post_days: 2 };
271 let result = EventStudy::compute(&event, &prices, &bench, window);
272 for ar in &result.abnormal_returns {
273 let diff = (ar.raw_return - ar.expected_return - ar.abnormal_return).abs();
274 assert!(diff < 1e-12, "AR identity failed on day {}", ar.day);
275 }
276 }
277
278 #[test]
279 fn test_car_monotone_with_window_start() {
280 let prices = synthetic_prices(25, 12, 0.002, 0.004);
281 let bench = synthetic_prices(25, 12, 0.001, 0.002);
282 let event_ts = prices[12].0;
283 let event = make_event(event_ts);
284 let window = EventWindow { pre_days: -5, post_days: 5 };
285 let result = EventStudy::compute(&event, &prices, &bench, window);
286 let mut running = 0.0f64;
288 for ar in &result.abnormal_returns {
289 running += ar.abnormal_return;
290 assert!((ar.car - running).abs() < 1e-12, "CAR mismatch at day {}", ar.day);
291 }
292 }
293
294 #[test]
295 fn test_car_pre_and_post_split() {
296 let prices = synthetic_prices(25, 12, 0.001, 0.002);
297 let bench = synthetic_prices(25, 12, 0.0005, 0.001);
298 let event_ts = prices[12].0;
299 let event = make_event(event_ts);
300 let window = EventWindow { pre_days: -5, post_days: 5 };
301 let result = EventStudy::compute(&event, &prices, &bench, window);
302 let manual_pre: f64 = result.abnormal_returns.iter()
303 .filter(|ar| ar.day >= -5 && ar.day < 0)
304 .map(|ar| ar.abnormal_return)
305 .sum();
306 let manual_post: f64 = result.abnormal_returns.iter()
307 .filter(|ar| ar.day >= 1 && ar.day <= 5)
308 .map(|ar| ar.abnormal_return)
309 .sum();
310 assert!((result.car_pre - manual_pre).abs() < 1e-12);
311 assert!((result.car_post - manual_post).abs() < 1e-12);
312 }
313
314 #[test]
315 fn test_peak_day_is_highest_car() {
316 let prices = synthetic_prices(25, 12, 0.003, 0.001);
317 let bench = synthetic_prices(25, 12, 0.001, 0.001);
318 let event_ts = prices[12].0;
319 let event = make_event(event_ts);
320 let window = EventWindow { pre_days: -5, post_days: 5 };
321 let result = EventStudy::compute(&event, &prices, &bench, window);
322 let max_car = result.abnormal_returns.iter().map(|ar| ar.car).fold(f64::NEG_INFINITY, f64::max);
323 let peak_car = result.abnormal_returns.iter().find(|ar| ar.day == result.peak_day).map(|ar| ar.car).unwrap_or(0.0);
324 assert!((peak_car - max_car).abs() < 1e-12);
325 }
326
327 #[test]
328 fn test_trough_day_is_lowest_car() {
329 let prices = synthetic_prices(25, 12, -0.001, 0.003);
330 let bench = synthetic_prices(25, 12, 0.001, 0.001);
331 let event_ts = prices[12].0;
332 let event = make_event(event_ts);
333 let window = EventWindow { pre_days: -5, post_days: 5 };
334 let result = EventStudy::compute(&event, &prices, &bench, window);
335 let min_car = result.abnormal_returns.iter().map(|ar| ar.car).fold(f64::INFINITY, f64::min);
336 let trough_car = result.abnormal_returns.iter().find(|ar| ar.day == result.trough_day).map(|ar| ar.car).unwrap_or(0.0);
337 assert!((trough_car - min_car).abs() < 1e-12);
338 }
339
340 #[test]
341 fn test_significance_zero_for_less_than_two() {
342 let event = make_event(1_000_000);
343 let prices = synthetic_prices(20, 10, 0.001, 0.002);
344 let bench = synthetic_prices(20, 10, 0.001, 0.002);
345 let window = EventWindow { pre_days: -3, post_days: 3 };
346 let result = EventStudy::compute(&event, &prices, &bench, window);
347 assert_eq!(EventStudy::significance(&[result]), 0.0);
348 assert_eq!(EventStudy::significance(&[]), 0.0);
349 }
350
351 #[test]
352 fn test_significance_positive_when_cars_positive() {
353 let mut results = Vec::new();
354 for i in 0..5 {
355 let prices: Vec<(u64, f64)> = (0..20u64)
357 .map(|j| (1_000_000 + i * 1_000_000 + j * 86_400, 100.0 * (1.01f64).powi(j as i32)))
358 .collect();
359 let bench: Vec<(u64, f64)> = (0..20u64)
360 .map(|j| (1_000_000 + i * 1_000_000 + j * 86_400, 100.0 * (1.005f64).powi(j as i32)))
361 .collect();
362 let event_ts = 1_000_000 + i * 1_000_000 + 10 * 86_400;
363 let event = make_event(event_ts);
364 let window = EventWindow { pre_days: -3, post_days: 3 };
365 results.push(EventStudy::compute(&event, &prices, &bench, window));
366 }
367 let t = EventStudy::significance(&results);
368 assert!(t > 0.0, "t-statistic should be positive when CAR is consistently positive");
369 }
370
371 #[test]
372 fn test_significance_negative_when_cars_negative() {
373 let mut results = Vec::new();
374 for i in 0..5 {
375 let prices: Vec<(u64, f64)> = (0..20u64)
377 .map(|j| (1_000_000 + i * 1_000_000 + j * 86_400, 100.0 * (0.99f64).powi(j as i32)))
378 .collect();
379 let bench: Vec<(u64, f64)> = (0..20u64)
380 .map(|j| (1_000_000 + i * 1_000_000 + j * 86_400, 100.0 * (1.005f64).powi(j as i32)))
381 .collect();
382 let event_ts = 1_000_000 + i * 1_000_000 + 10 * 86_400;
383 let event = make_event(event_ts);
384 let window = EventWindow { pre_days: -3, post_days: 3 };
385 results.push(EventStudy::compute(&event, &prices, &bench, window));
386 }
387 let t = EventStudy::significance(&results);
388 assert!(t < 0.0, "t-statistic should be negative when CAR is consistently negative");
389 }
390
391 #[test]
392 fn test_day_range_in_window() {
393 let prices = synthetic_prices(30, 15, 0.001, 0.002);
394 let bench = synthetic_prices(30, 15, 0.001, 0.002);
395 let event_ts = prices[15].0;
396 let event = make_event(event_ts);
397 let window = EventWindow { pre_days: -10, post_days: 10 };
398 let result = EventStudy::compute(&event, &prices, &bench, window);
399 let days: Vec<i32> = result.abnormal_returns.iter().map(|ar| ar.day).collect();
400 assert!(days.contains(&-10));
401 assert!(days.contains(&0));
402 assert!(days.contains(&10));
403 }
404
405 #[test]
406 fn test_event_fields_preserved() {
407 let prices = synthetic_prices(20, 10, 0.001, 0.002);
408 let bench = synthetic_prices(20, 10, 0.001, 0.002);
409 let event_ts = prices[10].0;
410 let event = make_event(event_ts);
411 let window = EventWindow { pre_days: -2, post_days: 2 };
412 let result = EventStudy::compute(&event, &prices, &bench, window);
413 assert_eq!(result.event.event_id, "EVT001");
414 assert_eq!(result.event.event_type, "earnings");
415 }
416
417 #[test]
418 fn test_significance_t_stat_formula() {
419 fn dummy_result(car: f64) -> EventResult {
421 EventResult {
422 event: make_event(1_000_000),
423 car_pre: car / 2.0,
424 car_post: car / 2.0,
425 peak_day: 1,
426 trough_day: -1,
427 abnormal_returns: vec![],
428 }
429 }
430 let cars = [0.02, 0.03, 0.025, 0.018, 0.022];
431 let results: Vec<EventResult> = cars.iter().map(|&c| dummy_result(c)).collect();
432 let t = EventStudy::significance(&results);
433 assert!(t > 1.0, "t-stat = {t}, expected > 1");
435 }
436
437 #[test]
438 fn test_zero_price_series_gives_zero_returns() {
439 let prices: Vec<(u64, f64)> = vec![(1_000_000, 0.0), (1_086_400, 0.0)];
441 let bench: Vec<(u64, f64)> = vec![(1_000_000, 100.0), (1_086_400, 101.0)];
442 let event = make_event(1_000_000);
443 let window = EventWindow { pre_days: -1, post_days: 1 };
444 let _ = EventStudy::compute(&event, &prices, &bench, window);
446 }
447
448 #[test]
449 fn test_asymmetric_window() {
450 let prices = synthetic_prices(30, 15, 0.001, 0.002);
451 let bench = synthetic_prices(30, 15, 0.001, 0.002);
452 let event_ts = prices[15].0;
453 let event = make_event(event_ts);
454 let window = EventWindow { pre_days: -2, post_days: 8 };
455 let result = EventStudy::compute(&event, &prices, &bench, window);
456 assert_eq!(result.abnormal_returns.len(), 11); }
458
459 #[test]
460 fn test_significance_all_identical_cars_returns_zero() {
461 fn dummy_result(car: f64) -> EventResult {
462 EventResult {
463 event: make_event(1_000_000),
464 car_pre: car,
465 car_post: 0.0,
466 peak_day: 0,
467 trough_day: 0,
468 abnormal_returns: vec![],
469 }
470 }
471 let results: Vec<EventResult> = [0.01, 0.01, 0.01].iter().map(|&c| dummy_result(c)).collect();
473 assert_eq!(EventStudy::significance(&results), 0.0);
474 }
475}