@ebowwa/quant-rust 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +161 -0
  2. package/bun-ffi.d.ts +54 -0
  3. package/dist/index.js +576 -0
  4. package/dist/src/index.d.ts +324 -0
  5. package/dist/src/index.d.ts.map +1 -0
  6. package/dist/types/index.d.ts +403 -0
  7. package/dist/types/index.d.ts.map +1 -0
  8. package/native/README.md +62 -0
  9. package/native/darwin-arm64/libquant_rust.dylib +0 -0
  10. package/package.json +70 -0
  11. package/scripts/postinstall.cjs +85 -0
  12. package/src/ffi.rs +496 -0
  13. package/src/index.ts +1073 -0
  14. package/src/indicators/ma.rs +222 -0
  15. package/src/indicators/mod.rs +18 -0
  16. package/src/indicators/momentum.rs +353 -0
  17. package/src/indicators/sr.rs +195 -0
  18. package/src/indicators/trend.rs +351 -0
  19. package/src/indicators/volatility.rs +270 -0
  20. package/src/indicators/volume.rs +213 -0
  21. package/src/lib.rs +130 -0
  22. package/src/patterns/breakout.rs +431 -0
  23. package/src/patterns/chart.rs +772 -0
  24. package/src/patterns/mod.rs +394 -0
  25. package/src/patterns/sr.rs +423 -0
  26. package/src/prediction/amm.rs +338 -0
  27. package/src/prediction/arbitrage.rs +230 -0
  28. package/src/prediction/calibration.rs +317 -0
  29. package/src/prediction/kelly.rs +232 -0
  30. package/src/prediction/lmsr.rs +194 -0
  31. package/src/prediction/mod.rs +59 -0
  32. package/src/prediction/odds.rs +229 -0
  33. package/src/prediction/pnl.rs +254 -0
  34. package/src/prediction/risk.rs +228 -0
  35. package/src/risk/beta.rs +257 -0
  36. package/src/risk/drawdown.rs +256 -0
  37. package/src/risk/leverage.rs +201 -0
  38. package/src/risk/mod.rs +388 -0
  39. package/src/risk/portfolio.rs +287 -0
  40. package/src/risk/ratios.rs +290 -0
  41. package/src/risk/sizing.rs +194 -0
  42. package/src/risk/var.rs +222 -0
  43. package/src/stats/cdf.rs +257 -0
  44. package/src/stats/correlation.rs +225 -0
  45. package/src/stats/distribution.rs +194 -0
  46. package/src/stats/hypothesis.rs +177 -0
  47. package/src/stats/matrix.rs +346 -0
  48. package/src/stats/mod.rs +257 -0
  49. package/src/stats/regression.rs +239 -0
  50. package/src/stats/rolling.rs +193 -0
  51. package/src/stats/timeseries.rs +263 -0
  52. package/src/types.rs +224 -0
  53. package/src/utils/mod.rs +215 -0
  54. package/src/utils/normalize.rs +192 -0
  55. package/src/utils/price.rs +167 -0
  56. package/src/utils/quantiles.rs +177 -0
  57. package/src/utils/returns.rs +158 -0
  58. package/src/utils/rolling.rs +97 -0
  59. package/src/utils/stats.rs +154 -0
  60. package/types/index.ts +513 -0
@@ -0,0 +1,772 @@
1
+ //! Chart Pattern Detection
2
+ //!
3
+ //! Functions for detecting chart patterns like double tops/bottoms,
4
+ //! head and shoulders, triangles, gaps, and channels.
5
+
6
+ use serde::{Deserialize, Serialize};
7
+
8
+ use crate::utils::{max, min};
9
+
10
+ /// Type of chart pattern
11
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12
+ pub enum PatternType {
13
+ DoubleTop,
14
+ DoubleBottom,
15
+ HeadAndShoulders,
16
+ InverseHeadAndShoulders,
17
+ TriangleAscending,
18
+ TriangleDescending,
19
+ TriangleSymmetrical,
20
+ FlagBullish,
21
+ FlagBearish,
22
+ WedgeRising,
23
+ WedgeFalling,
24
+ ChannelUp,
25
+ ChannelDown,
26
+ CupAndHandle,
27
+ RoundingBottom,
28
+ GapUp,
29
+ GapDown,
30
+ }
31
+
32
+ impl PatternType {
33
+ /// Check if this is a bullish pattern
34
+ pub fn is_bullish(&self) -> bool {
35
+ matches!(
36
+ self,
37
+ PatternType::DoubleBottom
38
+ | PatternType::InverseHeadAndShoulders
39
+ | PatternType::TriangleAscending
40
+ | PatternType::FlagBullish
41
+ | PatternType::ChannelUp
42
+ | PatternType::CupAndHandle
43
+ | PatternType::RoundingBottom
44
+ | PatternType::GapUp
45
+ )
46
+ }
47
+
48
+ /// Check if this is a bearish pattern
49
+ pub fn is_bearish(&self) -> bool {
50
+ matches!(
51
+ self,
52
+ PatternType::DoubleTop
53
+ | PatternType::HeadAndShoulders
54
+ | PatternType::TriangleDescending
55
+ | PatternType::FlagBearish
56
+ | PatternType::WedgeRising
57
+ | PatternType::ChannelDown
58
+ | PatternType::GapDown
59
+ )
60
+ }
61
+ }
62
+
63
+ /// Pattern direction (bullish/bearish/neutral)
64
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65
+ pub enum PatternDirection {
66
+ Bullish,
67
+ Bearish,
68
+ Neutral,
69
+ }
70
+
71
+ impl PatternDirection {
72
+ /// Get direction from pattern type
73
+ pub fn from_pattern(pattern_type: &PatternType) -> Self {
74
+ if pattern_type.is_bullish() {
75
+ PatternDirection::Bullish
76
+ } else if pattern_type.is_bearish() {
77
+ PatternDirection::Bearish
78
+ } else {
79
+ PatternDirection::Neutral
80
+ }
81
+ }
82
+ }
83
+
84
+ /// A detected chart pattern
85
+ #[derive(Debug, Clone, Serialize, Deserialize)]
86
+ pub struct PatternMatch {
87
+ /// Type of pattern
88
+ pub pattern_type: PatternType,
89
+ /// Direction (bullish/bearish/neutral)
90
+ pub direction: PatternDirection,
91
+ /// Index where pattern starts
92
+ pub start_index: usize,
93
+ /// Index where pattern ends
94
+ pub end_index: usize,
95
+ /// Confidence level (0-1)
96
+ pub confidence: f64,
97
+ /// Target price (optional)
98
+ pub target_price: Option<f64>,
99
+ /// Stop loss price (optional)
100
+ pub stop_loss: Option<f64>,
101
+ /// Key price points in the pattern
102
+ pub points: Vec<f64>,
103
+ }
104
+
105
+ impl PatternMatch {
106
+ /// Create a new pattern match
107
+ pub fn new(
108
+ pattern_type: PatternType,
109
+ start_index: usize,
110
+ end_index: usize,
111
+ confidence: f64,
112
+ ) -> Self {
113
+ let direction = PatternDirection::from_pattern(&pattern_type);
114
+ Self {
115
+ pattern_type,
116
+ direction,
117
+ start_index,
118
+ end_index,
119
+ confidence: confidence.clamp(0.0, 1.0),
120
+ target_price: None,
121
+ stop_loss: None,
122
+ points: Vec::new(),
123
+ }
124
+ }
125
+
126
+ /// Add target and stop loss
127
+ pub fn with_targets(mut self, target: f64, stop: f64) -> Self {
128
+ self.target_price = Some(target);
129
+ self.stop_loss = Some(stop);
130
+ self
131
+ }
132
+
133
+ /// Add key points
134
+ pub fn with_points(mut self, points: Vec<f64>) -> Self {
135
+ self.points = points;
136
+ self
137
+ }
138
+ }
139
+
140
+ /// A swing point (peak or trough)
141
+ #[derive(Debug, Clone, Copy)]
142
+ struct SwingPoint {
143
+ index: usize,
144
+ value: f64,
145
+ }
146
+
147
+ /// Detect double top pattern
148
+ ///
149
+ /// A double top is a bearish reversal pattern with two peaks
150
+ /// at approximately the same price level.
151
+ pub fn detect_double_top(high: &[f64], low: &[f64], tolerance: f64) -> Vec<PatternMatch> {
152
+ let mut patterns = Vec::new();
153
+ let peaks = find_swing_points_high(high, 3);
154
+
155
+ for i in 0..peaks.len().saturating_sub(1) {
156
+ let p1 = &peaks[i];
157
+ let p2 = &peaks[i + 1];
158
+
159
+ // Peaks should be at similar height
160
+ let peak_diff = (p1.value - p2.value).abs() / p1.value;
161
+ if peak_diff < tolerance {
162
+ // Find neckline (lowest point between peaks)
163
+ let neckline_start = p1.index;
164
+ let neckline_end = p2.index;
165
+
166
+ if neckline_end > neckline_start {
167
+ let neckline_slice = &low[neckline_start..neckline_end];
168
+ let neckline_idx = neckline_start + find_min_index(neckline_slice);
169
+ let neckline = low[neckline_idx];
170
+
171
+ let confidence = 1.0 - peak_diff;
172
+ let target = neckline - (p1.value.max(p2.value) - neckline);
173
+ let stop = p1.value.max(p2.value) * 1.01;
174
+
175
+ patterns.push(
176
+ PatternMatch::new(PatternType::DoubleTop, p1.index, p2.index, confidence)
177
+ .with_targets(target, stop)
178
+ .with_points(vec![p1.value, p2.value, neckline]),
179
+ );
180
+ }
181
+ }
182
+ }
183
+
184
+ patterns
185
+ }
186
+
187
+ /// Detect double bottom pattern
188
+ ///
189
+ /// A double bottom is a bullish reversal pattern with two troughs
190
+ /// at approximately the same price level.
191
+ pub fn detect_double_bottom(high: &[f64], low: &[f64], tolerance: f64) -> Vec<PatternMatch> {
192
+ let mut patterns = Vec::new();
193
+ let troughs = find_swing_points_low(low, 3);
194
+
195
+ for i in 0..troughs.len().saturating_sub(1) {
196
+ let t1 = &troughs[i];
197
+ let t2 = &troughs[i + 1];
198
+
199
+ // Troughs should be at similar level
200
+ let trough_diff = (t1.value - t2.value).abs() / t1.value;
201
+ if trough_diff < tolerance {
202
+ // Find neckline (highest point between troughs)
203
+ let neckline_start = t1.index;
204
+ let neckline_end = t2.index;
205
+
206
+ if neckline_end > neckline_start {
207
+ let neckline_slice = &high[neckline_start..neckline_end];
208
+ let neckline_idx = neckline_start + find_max_index(neckline_slice);
209
+ let neckline = high[neckline_idx];
210
+
211
+ let confidence = 1.0 - trough_diff;
212
+ let target = neckline + (neckline - t1.value.min(t2.value));
213
+ let stop = t1.value.min(t2.value) * 0.99;
214
+
215
+ patterns.push(
216
+ PatternMatch::new(PatternType::DoubleBottom, t1.index, t2.index, confidence)
217
+ .with_targets(target, stop)
218
+ .with_points(vec![t1.value, t2.value, neckline]),
219
+ );
220
+ }
221
+ }
222
+ }
223
+
224
+ patterns
225
+ }
226
+
227
+ /// Detect head and shoulders pattern
228
+ ///
229
+ /// A bearish reversal pattern with three peaks, where the middle peak (head)
230
+ /// is higher than the two shoulders.
231
+ pub fn detect_head_and_shoulders(high: &[f64], low: &[f64], tolerance: f64) -> Vec<PatternMatch> {
232
+ let mut patterns = Vec::new();
233
+ let peaks = find_swing_points_high(high, 4);
234
+
235
+ for i in 0..peaks.len().saturating_sub(2) {
236
+ let left_shoulder = &peaks[i];
237
+ let head = &peaks[i + 1];
238
+ let right_shoulder = &peaks[i + 2];
239
+
240
+ // Head should be higher than shoulders
241
+ if head.value > left_shoulder.value && head.value > right_shoulder.value {
242
+ // Shoulders should be at similar height
243
+ let shoulder_diff =
244
+ (left_shoulder.value - right_shoulder.value).abs() / left_shoulder.value;
245
+
246
+ if shoulder_diff < tolerance {
247
+ // Find neckline points
248
+ let neckline1_start = left_shoulder.index;
249
+ let neckline1_end = head.index;
250
+ let neckline2_start = head.index;
251
+ let neckline2_end = right_shoulder.index;
252
+
253
+ if neckline1_end > neckline1_start && neckline2_end > neckline2_start {
254
+ let nl1_slice = &low[neckline1_start..neckline1_end];
255
+ let nl1_idx = neckline1_start + find_min_index(nl1_slice);
256
+ let nl1 = low[nl1_idx];
257
+
258
+ let nl2_slice = &low[neckline2_start..neckline2_end];
259
+ let nl2_idx = neckline2_start + find_min_index(nl2_slice);
260
+ let nl2 = low[nl2_idx];
261
+
262
+ let neckline = (nl1 + nl2) / 2.0;
263
+ let confidence = (1.0 - shoulder_diff) * (head.value / left_shoulder.value - 1.0);
264
+ let target = neckline - (head.value - neckline);
265
+ let stop = head.value * 1.02;
266
+
267
+ patterns.push(
268
+ PatternMatch::new(
269
+ PatternType::HeadAndShoulders,
270
+ left_shoulder.index,
271
+ right_shoulder.index,
272
+ confidence,
273
+ )
274
+ .with_targets(target, stop)
275
+ .with_points(vec![left_shoulder.value, head.value, right_shoulder.value, neckline]),
276
+ );
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ patterns
283
+ }
284
+
285
+ /// Detect inverse head and shoulders pattern
286
+ ///
287
+ /// A bullish reversal pattern with three troughs, where the middle trough (head)
288
+ /// is lower than the two shoulders.
289
+ pub fn detect_inverse_head_and_shoulders(
290
+ high: &[f64],
291
+ low: &[f64],
292
+ tolerance: f64,
293
+ ) -> Vec<PatternMatch> {
294
+ let mut patterns = Vec::new();
295
+ let troughs = find_swing_points_low(low, 4);
296
+
297
+ for i in 0..troughs.len().saturating_sub(2) {
298
+ let left_shoulder = &troughs[i];
299
+ let head = &troughs[i + 1];
300
+ let right_shoulder = &troughs[i + 2];
301
+
302
+ // Head should be lower than shoulders
303
+ if head.value < left_shoulder.value && head.value < right_shoulder.value {
304
+ let shoulder_diff =
305
+ (left_shoulder.value - right_shoulder.value).abs() / left_shoulder.value;
306
+
307
+ if shoulder_diff < tolerance {
308
+ // Find neckline points
309
+ let neckline1_start = left_shoulder.index;
310
+ let neckline1_end = head.index;
311
+ let neckline2_start = head.index;
312
+ let neckline2_end = right_shoulder.index;
313
+
314
+ if neckline1_end > neckline1_start && neckline2_end > neckline2_start {
315
+ let nl1_slice = &high[neckline1_start..neckline1_end];
316
+ let nl1_idx = neckline1_start + find_max_index(nl1_slice);
317
+ let nl1 = high[nl1_idx];
318
+
319
+ let nl2_slice = &high[neckline2_start..neckline2_end];
320
+ let nl2_idx = neckline2_start + find_max_index(nl2_slice);
321
+ let nl2 = high[nl2_idx];
322
+
323
+ let neckline = (nl1 + nl2) / 2.0;
324
+ let confidence =
325
+ (1.0 - shoulder_diff) * (1.0 - head.value / left_shoulder.value);
326
+ let target = neckline + (neckline - head.value);
327
+ let stop = head.value * 0.98;
328
+
329
+ patterns.push(
330
+ PatternMatch::new(
331
+ PatternType::InverseHeadAndShoulders,
332
+ left_shoulder.index,
333
+ right_shoulder.index,
334
+ confidence,
335
+ )
336
+ .with_targets(target, stop)
337
+ .with_points(vec![left_shoulder.value, head.value, right_shoulder.value, neckline]),
338
+ );
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ patterns
345
+ }
346
+
347
+ /// Detect triangle patterns (ascending, descending, symmetrical)
348
+ ///
349
+ /// Triangles are continuation patterns formed by converging trendlines.
350
+ pub fn detect_triangles(high: &[f64], low: &[f64], min_periods: usize) -> Vec<PatternMatch> {
351
+ let mut patterns = Vec::new();
352
+
353
+ if high.len() < min_periods {
354
+ return patterns;
355
+ }
356
+
357
+ for i in min_periods..high.len() {
358
+ let window_high = &high[(i - min_periods)..i];
359
+ let window_low = &low[(i - min_periods)..i];
360
+
361
+ let high_peaks = find_swing_points_high(window_high, 2);
362
+ let low_troughs = find_swing_points_low(window_low, 2);
363
+
364
+ if high_peaks.len() >= 2 && low_troughs.len() >= 2 {
365
+ let high_line = fit_line(&high_peaks);
366
+ let low_line = fit_line(&low_troughs);
367
+
368
+ let window_high_max = max(window_high);
369
+ let window_low_min = min(window_low);
370
+
371
+ // Ascending triangle: flat top, rising bottom
372
+ if high_line.slope.abs() < 0.001 && low_line.slope > 0.001 {
373
+ let confidence = (low_line.slope.abs() * 100.0).min(1.0);
374
+
375
+ patterns.push(
376
+ PatternMatch::new(PatternType::TriangleAscending, i - min_periods, i, confidence)
377
+ .with_points(vec![window_high_max, window_low_min]),
378
+ );
379
+ }
380
+
381
+ // Descending triangle: falling top, flat bottom
382
+ if high_line.slope < -0.001 && low_line.slope.abs() < 0.001 {
383
+ let confidence = (high_line.slope.abs() * 100.0).min(1.0);
384
+
385
+ patterns.push(
386
+ PatternMatch::new(PatternType::TriangleDescending, i - min_periods, i, confidence)
387
+ .with_points(vec![window_high_max, window_low_min]),
388
+ );
389
+ }
390
+
391
+ // Symmetrical triangle: converging lines
392
+ if high_line.slope < -0.001 && low_line.slope > 0.001 {
393
+ let confidence =
394
+ ((high_line.slope.abs() + low_line.slope.abs()) * 50.0).min(1.0);
395
+
396
+ let direction = PatternDirection::Neutral;
397
+
398
+ let mut pattern = PatternMatch::new(
399
+ PatternType::TriangleSymmetrical,
400
+ i - min_periods,
401
+ i,
402
+ confidence,
403
+ );
404
+ pattern.direction = direction;
405
+ pattern.points = vec![window_high_max, window_low_min];
406
+
407
+ patterns.push(pattern);
408
+ }
409
+ }
410
+ }
411
+
412
+ patterns
413
+ }
414
+
415
+ /// Detect price gaps
416
+ ///
417
+ /// A gap occurs when the opening price is significantly different
418
+ /// from the previous closing range.
419
+ pub fn detect_gaps(open: &[f64], high: &[f64], low: &[f64], threshold: f64) -> Vec<PatternMatch> {
420
+ let mut patterns = Vec::new();
421
+
422
+ if open.len() < 2 {
423
+ return patterns;
424
+ }
425
+
426
+ for i in 1..open.len() {
427
+ let prev_high = high[i - 1];
428
+ let prev_low = low[i - 1];
429
+ let curr_open = open[i];
430
+
431
+ // Gap up
432
+ if curr_open > prev_high * (1.0 + threshold) {
433
+ let gap_size = (curr_open - prev_high) / prev_high;
434
+ let confidence = (gap_size / threshold).min(1.0);
435
+
436
+ patterns.push(
437
+ PatternMatch::new(PatternType::GapUp, i - 1, i, confidence)
438
+ .with_points(vec![prev_high, curr_open]),
439
+ );
440
+ }
441
+
442
+ // Gap down
443
+ if curr_open < prev_low * (1.0 - threshold) {
444
+ let gap_size = (prev_low - curr_open) / prev_low;
445
+ let confidence = (gap_size / threshold).min(1.0);
446
+
447
+ patterns.push(
448
+ PatternMatch::new(PatternType::GapDown, i - 1, i, confidence)
449
+ .with_points(vec![prev_low, curr_open]),
450
+ );
451
+ }
452
+ }
453
+
454
+ patterns
455
+ }
456
+
457
+ /// Detect price channels
458
+ ///
459
+ /// A channel is formed when price moves between two parallel trend lines.
460
+ pub fn detect_channels(
461
+ high: &[f64],
462
+ low: &[f64],
463
+ _close: &[f64],
464
+ min_periods: usize,
465
+ ) -> Vec<PatternMatch> {
466
+ let mut patterns = Vec::new();
467
+
468
+ if high.len() < min_periods {
469
+ return patterns;
470
+ }
471
+
472
+ let mut i = min_periods;
473
+ while i <= high.len() {
474
+ let window_high = &high[(i - min_periods)..i];
475
+ let window_low = &low[(i - min_periods)..i];
476
+
477
+ // Create points for fitting (index, value)
478
+ let high_points: Vec<(usize, f64)> = window_high
479
+ .iter()
480
+ .enumerate()
481
+ .map(|(idx, &v)| (idx, v))
482
+ .collect();
483
+ let low_points: Vec<(usize, f64)> = window_low
484
+ .iter()
485
+ .enumerate()
486
+ .map(|(idx, &v)| (idx, v))
487
+ .collect();
488
+
489
+ let high_line = fit_line_from_tuples(&high_points);
490
+ let low_line = fit_line_from_tuples(&low_points);
491
+
492
+ // Channel should have parallel lines
493
+ let slope_diff = (high_line.slope - low_line.slope).abs();
494
+ let avg_slope = (high_line.slope + low_line.slope) / 2.0;
495
+
496
+ if slope_diff < 0.001 && high_line.r2 > 0.7 && low_line.r2 > 0.7 {
497
+ let pattern_type = if avg_slope > 0.0 {
498
+ PatternType::ChannelUp
499
+ } else {
500
+ PatternType::ChannelDown
501
+ };
502
+
503
+ let confidence = (high_line.r2 + low_line.r2) / 2.0;
504
+ let window_high_max = max(window_high);
505
+ let window_low_min = min(window_low);
506
+
507
+ patterns.push(
508
+ PatternMatch::new(pattern_type, i - min_periods, i, confidence)
509
+ .with_points(vec![window_high_max, window_low_min]),
510
+ );
511
+ }
512
+
513
+ i += min_periods;
514
+ }
515
+
516
+ patterns
517
+ }
518
+
519
+ /// Detect all chart patterns
520
+ ///
521
+ /// Runs all pattern detectors and returns combined results sorted by confidence.
522
+ pub fn detect_all_patterns(high: &[f64], low: &[f64], _close: &[f64]) -> Vec<PatternMatch> {
523
+ let mut patterns = Vec::new();
524
+
525
+ patterns.extend(detect_double_top(high, low, 0.02));
526
+ patterns.extend(detect_double_bottom(high, low, 0.02));
527
+ patterns.extend(detect_head_and_shoulders(high, low, 0.03));
528
+ patterns.extend(detect_inverse_head_and_shoulders(high, low, 0.03));
529
+ patterns.extend(detect_triangles(high, low, 10));
530
+
531
+ // Sort by confidence (highest first)
532
+ patterns.sort_by(|a, b| {
533
+ b.confidence
534
+ .partial_cmp(&a.confidence)
535
+ .unwrap_or(std::cmp::Ordering::Equal)
536
+ });
537
+
538
+ patterns
539
+ }
540
+
541
+ // Helper functions
542
+
543
+ fn find_swing_points_high(data: &[f64], lookback: usize) -> Vec<SwingPoint> {
544
+ let mut points = Vec::new();
545
+ let n = data.len();
546
+
547
+ for i in lookback..(n.saturating_sub(lookback)) {
548
+ let left = &data[(i - lookback)..i];
549
+ let right = &data[(i + 1)..=(i + lookback).min(n - 1)];
550
+
551
+ let left_max = max(left);
552
+ let right_max = max(right);
553
+
554
+ if data[i] >= left_max && data[i] >= right_max {
555
+ points.push(SwingPoint {
556
+ index: i,
557
+ value: data[i],
558
+ });
559
+ }
560
+ }
561
+
562
+ points
563
+ }
564
+
565
+ fn find_swing_points_low(data: &[f64], lookback: usize) -> Vec<SwingPoint> {
566
+ let mut points = Vec::new();
567
+ let n = data.len();
568
+
569
+ for i in lookback..(n.saturating_sub(lookback)) {
570
+ let left = &data[(i - lookback)..i];
571
+ let right = &data[(i + 1)..=(i + lookback).min(n - 1)];
572
+
573
+ let left_min = min(left);
574
+ let right_min = min(right);
575
+
576
+ if data[i] <= left_min && data[i] <= right_min {
577
+ points.push(SwingPoint {
578
+ index: i,
579
+ value: data[i],
580
+ });
581
+ }
582
+ }
583
+
584
+ points
585
+ }
586
+
587
+ fn find_min_index(data: &[f64]) -> usize {
588
+ data.iter()
589
+ .enumerate()
590
+ .min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
591
+ .map(|(i, _)| i)
592
+ .unwrap_or(0)
593
+ }
594
+
595
+ fn find_max_index(data: &[f64]) -> usize {
596
+ data.iter()
597
+ .enumerate()
598
+ .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
599
+ .map(|(i, _)| i)
600
+ .unwrap_or(0)
601
+ }
602
+
603
+ /// Result of linear regression
604
+ #[allow(dead_code)]
605
+ struct LineFitResult {
606
+ slope: f64,
607
+ intercept: f64,
608
+ r2: f64,
609
+ }
610
+
611
+ fn fit_line(points: &[SwingPoint]) -> LineFitResult {
612
+ let n = points.len();
613
+ if n < 2 {
614
+ return LineFitResult {
615
+ slope: 0.0,
616
+ intercept: 0.0,
617
+ r2: 0.0,
618
+ };
619
+ }
620
+
621
+ let mut sum_x = 0.0_f64;
622
+ let mut sum_y = 0.0_f64;
623
+ let mut sum_xy = 0.0_f64;
624
+ let mut sum_x2 = 0.0_f64;
625
+
626
+ for p in points {
627
+ let x = p.index as f64;
628
+ let y = p.value;
629
+ sum_x += x;
630
+ sum_y += y;
631
+ sum_xy += x * y;
632
+ sum_x2 += x * x;
633
+ }
634
+
635
+ let denom = n as f64 * sum_x2 - sum_x * sum_x;
636
+ if denom.abs() < 1e-10 {
637
+ return LineFitResult {
638
+ slope: 0.0,
639
+ intercept: sum_y / n as f64,
640
+ r2: 0.0,
641
+ };
642
+ }
643
+
644
+ let slope = (n as f64 * sum_xy - sum_x * sum_y) / denom;
645
+ let intercept = (sum_y - slope * sum_x) / n as f64;
646
+
647
+ // R-squared
648
+ let y_mean = sum_y / n as f64;
649
+ let ss_tot: f64 = points.iter().map(|p| (p.value - y_mean).powi(2)).sum();
650
+ let ss_res: f64 = points
651
+ .iter()
652
+ .map(|p| {
653
+ let predicted = slope * p.index as f64 + intercept;
654
+ (p.value - predicted).powi(2)
655
+ })
656
+ .sum();
657
+
658
+ let r2 = if ss_tot > 1e-10 {
659
+ 1.0 - ss_res / ss_tot
660
+ } else {
661
+ 0.0
662
+ };
663
+
664
+ LineFitResult {
665
+ slope,
666
+ intercept,
667
+ r2: r2.max(0.0),
668
+ }
669
+ }
670
+
671
+ fn fit_line_from_tuples(points: &[(usize, f64)]) -> LineFitResult {
672
+ let n = points.len();
673
+ if n < 2 {
674
+ return LineFitResult {
675
+ slope: 0.0,
676
+ intercept: 0.0,
677
+ r2: 0.0,
678
+ };
679
+ }
680
+
681
+ let mut sum_x = 0.0_f64;
682
+ let mut sum_y = 0.0_f64;
683
+ let mut sum_xy = 0.0_f64;
684
+ let mut sum_x2 = 0.0_f64;
685
+
686
+ for (x, y) in points {
687
+ let x = *x as f64;
688
+ sum_x += x;
689
+ sum_y += y;
690
+ sum_xy += x * y;
691
+ sum_x2 += x * x;
692
+ }
693
+
694
+ let denom = n as f64 * sum_x2 - sum_x * sum_x;
695
+ if denom.abs() < 1e-10 {
696
+ return LineFitResult {
697
+ slope: 0.0,
698
+ intercept: sum_y / n as f64,
699
+ r2: 0.0,
700
+ };
701
+ }
702
+
703
+ let slope = (n as f64 * sum_xy - sum_x * sum_y) / denom;
704
+ let intercept = (sum_y - slope * sum_x) / n as f64;
705
+
706
+ // R-squared
707
+ let y_mean = sum_y / n as f64;
708
+ let ss_tot: f64 = points.iter().map(|(_, y)| (y - y_mean).powi(2)).sum();
709
+ let ss_res: f64 = points
710
+ .iter()
711
+ .map(|(x, y)| {
712
+ let predicted = slope * *x as f64 + intercept;
713
+ (y - predicted).powi(2)
714
+ })
715
+ .sum();
716
+
717
+ let r2 = if ss_tot > 1e-10 {
718
+ 1.0 - ss_res / ss_tot
719
+ } else {
720
+ 0.0
721
+ };
722
+
723
+ LineFitResult {
724
+ slope,
725
+ intercept,
726
+ r2: r2.max(0.0),
727
+ }
728
+ }
729
+
730
+ #[cfg(test)]
731
+ mod tests {
732
+ use super::*;
733
+
734
+ #[test]
735
+ fn test_pattern_type_direction() {
736
+ assert!(PatternType::DoubleTop.is_bearish());
737
+ assert!(PatternType::DoubleBottom.is_bullish());
738
+ assert!(PatternType::TriangleSymmetrical.is_bullish() == false);
739
+ assert!(PatternType::TriangleSymmetrical.is_bearish() == false);
740
+ }
741
+
742
+ #[test]
743
+ fn test_detect_gaps() {
744
+ let open = vec![100.0, 105.0, 102.0, 95.0]; // Gap up at 1, gap down at 3
745
+ let high = vec![102.0, 107.0, 104.0, 97.0];
746
+ let low = vec![99.0, 104.0, 101.0, 94.0];
747
+
748
+ let gaps = detect_gaps(&open, &high, &low, 0.02);
749
+
750
+ assert!(gaps.len() >= 1); // Should detect at least one gap
751
+ }
752
+
753
+ #[test]
754
+ fn test_detect_double_top() {
755
+ // Create a double top pattern
756
+ let high = vec![
757
+ 100.0, 105.0, 110.0, 105.0, 100.0, // Up to 110, down
758
+ 95.0, 100.0, 105.0, 110.0, 105.0, // Up to 110 again, down
759
+ 100.0,
760
+ ];
761
+ let low = vec![
762
+ 98.0, 103.0, 108.0, 102.0, 95.0,
763
+ 92.0, 97.0, 102.0, 108.0, 100.0,
764
+ 95.0,
765
+ ];
766
+
767
+ let patterns = detect_double_top(&high, &low, 0.02);
768
+
769
+ // May or may not detect depending on swing points
770
+ // This is more of a smoke test
771
+ }
772
+ }