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 {
157 let n = results.len();
158 if n < 2 {
159 return 0.0;
160 }
161 let cars: Vec<f64> = results
162 .iter()
163 .map(|r| r.abnormal_returns.last().map_or(r.car_pre + r.car_post, |a| a.car))
164 .collect();
165 let mean = cars.iter().sum::<f64>() / n as f64;
166 let variance = cars.iter().map(|c| (c - mean).powi(2)).sum::<f64>() / (n - 1) as f64;
167 let std_dev = variance.sqrt();
168 if std_dev < 1e-15 {
169 return 0.0;
170 }
171 mean / (std_dev / (n as f64).sqrt())
172 }
173}
174
175fn daily_log_returns(series: &[(u64, f64)], event_date: u64) -> std::collections::HashMap<i32, f64> {
180 use std::collections::HashMap;
181
182 if series.len() < 2 {
183 return HashMap::new();
184 }
185
186 let event_idx = series.partition_point(|&(ts, _)| ts < event_date);
188 let event_idx = event_idx.min(series.len() - 1);
190
191 let mut map = HashMap::new();
192
193 for i in 1..series.len() {
194 let p_prev = series[i - 1].1;
195 let p_curr = series[i].1;
196 let log_ret = if p_prev > 0.0 && p_curr > 0.0 {
197 (p_curr / p_prev).ln()
198 } else {
199 0.0
200 };
201 let day = i as i32 - event_idx as i32;
204 map.insert(day, log_ret);
205 }
206 map
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 fn synthetic_prices(n: usize, event_idx: usize, drift: f64, vol: f64) -> Vec<(u64, f64)> {
216 let mut prices = Vec::with_capacity(n);
217 let mut p = 100.0f64;
218 let base_ts: u64 = 1_000_000;
219 let day_secs: u64 = 86_400;
220 for i in 0..n {
222 if i > 0 {
223 let sign = if i % 2 == 0 { 1.0 } else { -1.0 };
225 p *= (drift + sign * vol).exp();
226 }
227 prices.push((base_ts + i as u64 * day_secs, p));
228 }
229 let _ = event_idx; prices
231 }
232
233 fn make_event(date: u64) -> MarketEvent {
234 MarketEvent {
235 event_id: "EVT001".into(),
236 event_date: date,
237 event_type: "earnings".into(),
238 description: "Q3 earnings release".into(),
239 }
240 }
241
242 #[test]
243 fn test_compute_returns_correct_window_length() {
244 let prices = synthetic_prices(30, 15, 0.001, 0.005);
245 let bench = synthetic_prices(30, 15, 0.0005, 0.003);
246 let event_ts = prices[15].0;
247 let event = make_event(event_ts);
248 let window = EventWindow { pre_days: -5, post_days: 5 };
249 let result = EventStudy::compute(&event, &prices, &bench, window);
250 assert_eq!(result.abnormal_returns.len(), 11); }
252
253 #[test]
254 fn test_car_accumulates_correctly() {
255 let prices = synthetic_prices(20, 10, 0.001, 0.003);
256 let bench = synthetic_prices(20, 10, 0.001, 0.003); let event_ts = prices[10].0;
258 let event = make_event(event_ts);
259 let window = EventWindow { pre_days: -3, post_days: 3 };
260 let result = EventStudy::compute(&event, &prices, &bench, window);
261 for ar in &result.abnormal_returns {
263 assert!(ar.car.abs() < 1e-10, "CAR should be ~0 when security==benchmark");
264 }
265 }
266
267 #[test]
268 fn test_abnormal_return_equals_raw_minus_expected() {
269 let prices: Vec<(u64, f64)> = (0..20u64).map(|i| (1_000_000 + i * 86_400, 100.0 + i as f64)).collect();
270 let bench: Vec<(u64, f64)> = (0..20u64).map(|i| (1_000_000 + i * 86_400, 100.0 + i as f64 * 0.5)).collect();
271 let event_ts = 1_000_000 + 10 * 86_400;
272 let event = make_event(event_ts);
273 let window = EventWindow { pre_days: -2, post_days: 2 };
274 let result = EventStudy::compute(&event, &prices, &bench, window);
275 for ar in &result.abnormal_returns {
276 let diff = (ar.raw_return - ar.expected_return - ar.abnormal_return).abs();
277 assert!(diff < 1e-12, "AR identity failed on day {}", ar.day);
278 }
279 }
280
281 #[test]
282 fn test_car_monotone_with_window_start() {
283 let prices = synthetic_prices(25, 12, 0.002, 0.004);
284 let bench = synthetic_prices(25, 12, 0.001, 0.002);
285 let event_ts = prices[12].0;
286 let event = make_event(event_ts);
287 let window = EventWindow { pre_days: -5, post_days: 5 };
288 let result = EventStudy::compute(&event, &prices, &bench, window);
289 let mut running = 0.0f64;
291 for ar in &result.abnormal_returns {
292 running += ar.abnormal_return;
293 assert!((ar.car - running).abs() < 1e-12, "CAR mismatch at day {}", ar.day);
294 }
295 }
296
297 #[test]
298 fn test_car_pre_and_post_split() {
299 let prices = synthetic_prices(25, 12, 0.001, 0.002);
300 let bench = synthetic_prices(25, 12, 0.0005, 0.001);
301 let event_ts = prices[12].0;
302 let event = make_event(event_ts);
303 let window = EventWindow { pre_days: -5, post_days: 5 };
304 let result = EventStudy::compute(&event, &prices, &bench, window);
305 let manual_pre: f64 = result.abnormal_returns.iter()
306 .filter(|ar| ar.day >= -5 && ar.day < 0)
307 .map(|ar| ar.abnormal_return)
308 .sum();
309 let manual_post: f64 = result.abnormal_returns.iter()
310 .filter(|ar| ar.day >= 1 && ar.day <= 5)
311 .map(|ar| ar.abnormal_return)
312 .sum();
313 assert!((result.car_pre - manual_pre).abs() < 1e-12);
314 assert!((result.car_post - manual_post).abs() < 1e-12);
315 }
316
317 #[test]
318 fn test_peak_day_is_highest_car() {
319 let prices = synthetic_prices(25, 12, 0.003, 0.001);
320 let bench = synthetic_prices(25, 12, 0.001, 0.001);
321 let event_ts = prices[12].0;
322 let event = make_event(event_ts);
323 let window = EventWindow { pre_days: -5, post_days: 5 };
324 let result = EventStudy::compute(&event, &prices, &bench, window);
325 let max_car = result.abnormal_returns.iter().map(|ar| ar.car).fold(f64::NEG_INFINITY, f64::max);
326 let peak_car = result.abnormal_returns.iter().find(|ar| ar.day == result.peak_day).map(|ar| ar.car).unwrap_or(0.0);
327 assert!((peak_car - max_car).abs() < 1e-12);
328 }
329
330 #[test]
331 fn test_trough_day_is_lowest_car() {
332 let prices = synthetic_prices(25, 12, -0.001, 0.003);
333 let bench = synthetic_prices(25, 12, 0.001, 0.001);
334 let event_ts = prices[12].0;
335 let event = make_event(event_ts);
336 let window = EventWindow { pre_days: -5, post_days: 5 };
337 let result = EventStudy::compute(&event, &prices, &bench, window);
338 let min_car = result.abnormal_returns.iter().map(|ar| ar.car).fold(f64::INFINITY, f64::min);
339 let trough_car = result.abnormal_returns.iter().find(|ar| ar.day == result.trough_day).map(|ar| ar.car).unwrap_or(0.0);
340 assert!((trough_car - min_car).abs() < 1e-12);
341 }
342
343 #[test]
344 fn test_significance_zero_for_less_than_two() {
345 let event = make_event(1_000_000);
346 let prices = synthetic_prices(20, 10, 0.001, 0.002);
347 let bench = synthetic_prices(20, 10, 0.001, 0.002);
348 let window = EventWindow { pre_days: -3, post_days: 3 };
349 let result = EventStudy::compute(&event, &prices, &bench, window);
350 assert_eq!(EventStudy::significance(&[result]), 0.0);
351 assert_eq!(EventStudy::significance(&[]), 0.0);
352 }
353
354 #[test]
355 fn test_significance_positive_when_cars_positive() {
356 let mut results = Vec::new();
357 for i in 0..5 {
358 let prices: Vec<(u64, f64)> = (0..20u64)
361 .map(|j| (1_000_000 + i * 1_000_000 + j * 86_400, 100.0 * (1.01f64 + 0.002 * i as f64).powi(j as i32)))
362 .collect();
363 let bench: Vec<(u64, f64)> = (0..20u64)
364 .map(|j| (1_000_000 + i * 1_000_000 + j * 86_400, 100.0 * (1.005f64).powi(j as i32)))
365 .collect();
366 let event_ts = 1_000_000 + i * 1_000_000 + 10 * 86_400;
367 let event = make_event(event_ts);
368 let window = EventWindow { pre_days: -3, post_days: 3 };
369 results.push(EventStudy::compute(&event, &prices, &bench, window));
370 }
371 let t = EventStudy::significance(&results);
372 assert!(t > 0.0, "t-statistic should be positive when CAR is consistently positive");
373 }
374
375 #[test]
376 fn test_significance_negative_when_cars_negative() {
377 let mut results = Vec::new();
378 for i in 0..5 {
379 let prices: Vec<(u64, f64)> = (0..20u64)
382 .map(|j| (1_000_000 + i * 1_000_000 + j * 86_400, 100.0 * (0.99f64 - 0.002 * i as f64).powi(j as i32)))
383 .collect();
384 let bench: Vec<(u64, f64)> = (0..20u64)
385 .map(|j| (1_000_000 + i * 1_000_000 + j * 86_400, 100.0 * (1.005f64).powi(j as i32)))
386 .collect();
387 let event_ts = 1_000_000 + i * 1_000_000 + 10 * 86_400;
388 let event = make_event(event_ts);
389 let window = EventWindow { pre_days: -3, post_days: 3 };
390 results.push(EventStudy::compute(&event, &prices, &bench, window));
391 }
392 let t = EventStudy::significance(&results);
393 assert!(t < 0.0, "t-statistic should be negative when CAR is consistently negative");
394 }
395
396 #[test]
397 fn test_day_range_in_window() {
398 let prices = synthetic_prices(30, 15, 0.001, 0.002);
399 let bench = synthetic_prices(30, 15, 0.001, 0.002);
400 let event_ts = prices[15].0;
401 let event = make_event(event_ts);
402 let window = EventWindow { pre_days: -10, post_days: 10 };
403 let result = EventStudy::compute(&event, &prices, &bench, window);
404 let days: Vec<i32> = result.abnormal_returns.iter().map(|ar| ar.day).collect();
405 assert!(days.contains(&-10));
406 assert!(days.contains(&0));
407 assert!(days.contains(&10));
408 }
409
410 #[test]
411 fn test_event_fields_preserved() {
412 let prices = synthetic_prices(20, 10, 0.001, 0.002);
413 let bench = synthetic_prices(20, 10, 0.001, 0.002);
414 let event_ts = prices[10].0;
415 let event = make_event(event_ts);
416 let window = EventWindow { pre_days: -2, post_days: 2 };
417 let result = EventStudy::compute(&event, &prices, &bench, window);
418 assert_eq!(result.event.event_id, "EVT001");
419 assert_eq!(result.event.event_type, "earnings");
420 }
421
422 #[test]
423 fn test_significance_t_stat_formula() {
424 fn dummy_result(car: f64) -> EventResult {
426 EventResult {
427 event: make_event(1_000_000),
428 car_pre: car / 2.0,
429 car_post: car / 2.0,
430 peak_day: 1,
431 trough_day: -1,
432 abnormal_returns: vec![],
433 }
434 }
435 let cars = [0.02, 0.03, 0.025, 0.018, 0.022];
436 let results: Vec<EventResult> = cars.iter().map(|&c| dummy_result(c)).collect();
437 let t = EventStudy::significance(&results);
438 assert!(t > 1.0, "t-stat = {t}, expected > 1");
440 }
441
442 #[test]
443 fn test_zero_price_series_gives_zero_returns() {
444 let prices: Vec<(u64, f64)> = vec![(1_000_000, 0.0), (1_086_400, 0.0)];
446 let bench: Vec<(u64, f64)> = vec![(1_000_000, 100.0), (1_086_400, 101.0)];
447 let event = make_event(1_000_000);
448 let window = EventWindow { pre_days: -1, post_days: 1 };
449 let _ = EventStudy::compute(&event, &prices, &bench, window);
451 }
452
453 #[test]
454 fn test_asymmetric_window() {
455 let prices = synthetic_prices(30, 15, 0.001, 0.002);
456 let bench = synthetic_prices(30, 15, 0.001, 0.002);
457 let event_ts = prices[15].0;
458 let event = make_event(event_ts);
459 let window = EventWindow { pre_days: -2, post_days: 8 };
460 let result = EventStudy::compute(&event, &prices, &bench, window);
461 assert_eq!(result.abnormal_returns.len(), 11); }
463
464 #[test]
465 fn test_significance_all_identical_cars_returns_zero() {
466 fn dummy_result(car: f64) -> EventResult {
467 EventResult {
468 event: make_event(1_000_000),
469 car_pre: car,
470 car_post: 0.0,
471 peak_day: 0,
472 trough_day: 0,
473 abnormal_returns: vec![],
474 }
475 }
476 let results: Vec<EventResult> = [0.01, 0.01, 0.01].iter().map(|&c| dummy_result(c)).collect();
478 assert_eq!(EventStudy::significance(&results), 0.0);
479 }
480}