@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,423 @@
1
+ //! Support and Resistance Detection
2
+ //!
3
+ //! Functions for finding support/resistance levels and trend lines
4
+ //! using swing point analysis.
5
+
6
+ use serde::{Deserialize, Serialize};
7
+
8
+ use crate::utils::{max, min};
9
+
10
+ /// Type of support/resistance level
11
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12
+ pub enum LevelType {
13
+ Support,
14
+ Resistance,
15
+ }
16
+
17
+ /// A support or resistance level
18
+ #[derive(Debug, Clone, Serialize, Deserialize)]
19
+ pub struct SupportResistanceLevel {
20
+ /// Price level
21
+ pub price: f64,
22
+ /// Strength score (number of touches * weight)
23
+ pub strength: usize,
24
+ /// Number of times price has touched this level
25
+ pub touches: usize,
26
+ /// Index of first touch
27
+ pub first_touch: usize,
28
+ /// Index of last touch
29
+ pub last_touch: usize,
30
+ /// Whether this is support or resistance
31
+ pub level_type: LevelType,
32
+ }
33
+
34
+ impl SupportResistanceLevel {
35
+ /// Create a new support/resistance level
36
+ pub fn new(price: f64, level_type: LevelType, index: usize) -> Self {
37
+ Self {
38
+ price,
39
+ strength: 1,
40
+ touches: 1,
41
+ first_touch: index,
42
+ last_touch: index,
43
+ level_type,
44
+ }
45
+ }
46
+
47
+ /// Merge another level into this one
48
+ pub fn merge(&mut self, other: &SupportResistanceLevel) {
49
+ let total_touches = self.touches + other.touches;
50
+ self.price = (self.price * self.touches as f64 + other.price * other.touches as f64)
51
+ / total_touches as f64;
52
+ self.touches = total_touches;
53
+ self.strength += other.strength;
54
+ self.last_touch = self.last_touch.max(other.last_touch);
55
+ }
56
+ }
57
+
58
+ /// A trend line
59
+ #[derive(Debug, Clone, Serialize, Deserialize)]
60
+ pub struct TrendLine {
61
+ /// Slope of the line (price change per bar)
62
+ pub slope: f64,
63
+ /// Y-intercept (price at index 0)
64
+ pub intercept: f64,
65
+ /// Start index of the trend line
66
+ pub start_index: usize,
67
+ /// End index of the trend line
68
+ pub end_index: usize,
69
+ /// R-squared value (goodness of fit)
70
+ pub r2: f64,
71
+ /// Whether this is a support or resistance trend line
72
+ pub trend_type: LevelType,
73
+ }
74
+
75
+ impl TrendLine {
76
+ /// Get the price at a given index
77
+ pub fn price_at(&self, index: usize) -> f64 {
78
+ self.slope * index as f64 + self.intercept
79
+ }
80
+ }
81
+
82
+ /// A swing point (peak or trough)
83
+ #[derive(Debug, Clone, Copy)]
84
+ struct SwingPoint {
85
+ index: usize,
86
+ value: f64,
87
+ }
88
+
89
+ /// Find support and resistance levels using swing points
90
+ ///
91
+ /// # Arguments
92
+ /// * `high` - Array of high prices
93
+ /// * `low` - Array of low prices
94
+ /// * `lookback` - Number of bars on each side to define a swing point
95
+ /// * `threshold` - Price threshold for merging nearby levels (as decimal, e.g., 0.02 = 2%)
96
+ ///
97
+ /// # Returns
98
+ /// Vector of support/resistance levels sorted by strength (most touches first)
99
+ pub fn find_support_resistance(
100
+ high: &[f64],
101
+ low: &[f64],
102
+ lookback: usize,
103
+ threshold: f64,
104
+ ) -> Vec<SupportResistanceLevel> {
105
+ if high.len() != low.len() || high.is_empty() {
106
+ return vec![];
107
+ }
108
+
109
+ let n = high.len();
110
+ let mut levels: Vec<SupportResistanceLevel> = Vec::new();
111
+
112
+ // Find swing highs (resistance)
113
+ for i in lookback..(n - lookback) {
114
+ let left_start = i - lookback;
115
+ let left_highs = &high[left_start..i];
116
+ let right_highs = &high[(i + 1)..=(i + lookback).min(n - 1)];
117
+
118
+ let left_max = max(left_highs);
119
+ let right_max = max(right_highs);
120
+
121
+ if high[i] >= left_max && high[i] >= right_max {
122
+ levels.push(SupportResistanceLevel::new(high[i], LevelType::Resistance, i));
123
+ }
124
+ }
125
+
126
+ // Find swing lows (support)
127
+ for i in lookback..(n - lookback) {
128
+ let left_start = i - lookback;
129
+ let left_lows = &low[left_start..i];
130
+ let right_lows = &low[(i + 1)..=(i + lookback).min(n - 1)];
131
+
132
+ let left_min = min(left_lows);
133
+ let right_min = min(right_lows);
134
+
135
+ if low[i] <= left_min && low[i] <= right_min {
136
+ levels.push(SupportResistanceLevel::new(low[i], LevelType::Support, i));
137
+ }
138
+ }
139
+
140
+ // Merge nearby levels
141
+ merge_levels(&mut levels, threshold);
142
+
143
+ // Sort by strength (most touches first)
144
+ levels.sort_by(|a, b| b.touches.cmp(&a.touches));
145
+
146
+ levels
147
+ }
148
+
149
+ /// Merge levels that are close together
150
+ fn merge_levels(levels: &mut Vec<SupportResistanceLevel>, threshold: f64) {
151
+ if levels.is_empty() {
152
+ return;
153
+ }
154
+
155
+ // Sort by price
156
+ levels.sort_by(|a, b| {
157
+ a.price
158
+ .partial_cmp(&b.price)
159
+ .unwrap_or(std::cmp::Ordering::Equal)
160
+ });
161
+
162
+ let mut merged: Vec<SupportResistanceLevel> = Vec::with_capacity(levels.len());
163
+ merged.push(levels[0].clone());
164
+
165
+ for current in levels.iter().skip(1) {
166
+ let last = merged.last_mut().unwrap();
167
+
168
+ // Check if levels should be merged
169
+ let price_diff = (current.price - last.price).abs() / last.price;
170
+ if price_diff < threshold {
171
+ last.merge(current);
172
+ } else {
173
+ merged.push(current.clone());
174
+ }
175
+ }
176
+
177
+ *levels = merged;
178
+ }
179
+
180
+ /// Find trend lines by connecting swing points
181
+ ///
182
+ /// # Arguments
183
+ /// * `high` - Array of high prices
184
+ /// * `low` - Array of low prices
185
+ /// * `min_points` - Minimum number of points to form a trend line
186
+ /// * `tolerance` - Price tolerance for point alignment (as decimal)
187
+ ///
188
+ /// # Returns
189
+ /// Vector of trend lines
190
+ pub fn find_trend_lines(
191
+ high: &[f64],
192
+ low: &[f64],
193
+ min_points: usize,
194
+ tolerance: f64,
195
+ ) -> Vec<TrendLine> {
196
+ let mut lines = Vec::new();
197
+
198
+ // Find support trend lines (connecting lows)
199
+ let low_points = find_swing_points(low, SwingType::Low, 3);
200
+ lines.extend(fit_trend_lines(
201
+ &low_points,
202
+ high.len(),
203
+ LevelType::Support,
204
+ min_points,
205
+ tolerance,
206
+ ));
207
+
208
+ // Find resistance trend lines (connecting highs)
209
+ let high_points = find_swing_points(high, SwingType::High, 3);
210
+ lines.extend(fit_trend_lines(
211
+ &high_points,
212
+ high.len(),
213
+ LevelType::Resistance,
214
+ min_points,
215
+ tolerance,
216
+ ));
217
+
218
+ lines
219
+ }
220
+
221
+ /// Type of swing point
222
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
223
+ enum SwingType {
224
+ High,
225
+ Low,
226
+ }
227
+
228
+ /// Find swing points (peaks or troughs)
229
+ fn find_swing_points(data: &[f64], swing_type: SwingType, lookback: usize) -> Vec<SwingPoint> {
230
+ let mut points = Vec::new();
231
+
232
+ for i in lookback..(data.len() - lookback) {
233
+ let left = &data[(i - lookback)..i];
234
+ let right = &data[(i + 1)..=(i + lookback).min(data.len() - 1)];
235
+
236
+ let is_swing = match swing_type {
237
+ SwingType::High => {
238
+ data[i] >= max(left) && data[i] >= max(right)
239
+ }
240
+ SwingType::Low => {
241
+ data[i] <= min(left) && data[i] <= min(right)
242
+ }
243
+ };
244
+
245
+ if is_swing {
246
+ points.push(SwingPoint { index: i, value: data[i] });
247
+ }
248
+ }
249
+
250
+ points
251
+ }
252
+
253
+ /// Fit trend lines to a set of swing points
254
+ fn fit_trend_lines(
255
+ points: &[SwingPoint],
256
+ _data_length: usize,
257
+ trend_type: LevelType,
258
+ min_points: usize,
259
+ tolerance: f64,
260
+ ) -> Vec<TrendLine> {
261
+ let mut lines = Vec::new();
262
+
263
+ if points.len() < min_points {
264
+ return lines;
265
+ }
266
+
267
+ // Try all combinations of points
268
+ for i in 0..=(points.len() - min_points) {
269
+ for j in ((i + min_points - 1)..points.len()).rev() {
270
+ let subset = &points[i..=j];
271
+ let line = fit_line(subset);
272
+
273
+ if line.r2 > 0.8 {
274
+ // Check if all points are within tolerance
275
+ let all_points_fit = subset.iter().all(|p| {
276
+ let predicted = line.slope * p.index as f64 + line.intercept;
277
+ let diff = (predicted - p.value).abs() / p.value;
278
+ diff < tolerance
279
+ });
280
+
281
+ if all_points_fit {
282
+ lines.push(TrendLine {
283
+ slope: line.slope,
284
+ intercept: line.intercept,
285
+ start_index: subset.first().unwrap().index,
286
+ end_index: subset.last().unwrap().index,
287
+ r2: line.r2,
288
+ trend_type,
289
+ });
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ lines
296
+ }
297
+
298
+ /// Result of linear regression
299
+ struct LineFit {
300
+ slope: f64,
301
+ intercept: f64,
302
+ r2: f64,
303
+ }
304
+
305
+ /// Perform linear regression on swing points
306
+ fn fit_line(points: &[SwingPoint]) -> LineFit {
307
+ let n = points.len();
308
+ if n < 2 {
309
+ return LineFit {
310
+ slope: 0.0,
311
+ intercept: 0.0,
312
+ r2: 0.0,
313
+ };
314
+ }
315
+
316
+ let mut sum_x = 0.0_f64;
317
+ let mut sum_y = 0.0_f64;
318
+ let mut sum_xy = 0.0_f64;
319
+ let mut sum_x2 = 0.0_f64;
320
+ let mut sum_y2 = 0.0_f64;
321
+
322
+ for p in points {
323
+ let x = p.index as f64;
324
+ let y = p.value;
325
+ sum_x += x;
326
+ sum_y += y;
327
+ sum_xy += x * y;
328
+ sum_x2 += x * x;
329
+ sum_y2 += y * y;
330
+ }
331
+
332
+ let denom = n as f64 * sum_x2 - sum_x * sum_x;
333
+ if denom.abs() < 1e-10 {
334
+ return LineFit {
335
+ slope: 0.0,
336
+ intercept: sum_y / n as f64,
337
+ r2: 0.0,
338
+ };
339
+ }
340
+
341
+ let slope = (n as f64 * sum_xy - sum_x * sum_y) / denom;
342
+ let intercept = (sum_y - slope * sum_x) / n as f64;
343
+
344
+ // R-squared calculation
345
+ let ss_tot = sum_y2 - sum_y * sum_y / n as f64;
346
+
347
+ let ss_res: f64 = points
348
+ .iter()
349
+ .map(|p| {
350
+ let predicted = slope * p.index as f64 + intercept;
351
+ let residual = p.value - predicted;
352
+ residual * residual
353
+ })
354
+ .sum();
355
+
356
+ let r2 = if ss_tot > 1e-10 {
357
+ 1.0 - ss_res / ss_tot
358
+ } else {
359
+ 0.0
360
+ };
361
+
362
+ LineFit {
363
+ slope,
364
+ intercept,
365
+ r2: r2.max(0.0),
366
+ }
367
+ }
368
+
369
+ #[cfg(test)]
370
+ mod tests {
371
+ use super::*;
372
+
373
+ #[test]
374
+ fn test_find_support_resistance() {
375
+ // Create a simple price series with obvious levels
376
+ let high = vec![
377
+ 100.0, 102.0, 105.0, 103.0, 101.0, // Down
378
+ 98.0, 100.0, 103.0, 105.0, 103.0, // Up and down
379
+ 101.0, 98.0, 100.0, 103.0, 105.0, // Up again
380
+ ];
381
+ let low = vec![
382
+ 98.0, 100.0, 103.0, 100.0, 98.0,
383
+ 95.0, 97.0, 100.0, 102.0, 100.0,
384
+ 98.0, 95.0, 97.0, 100.0, 102.0,
385
+ ];
386
+
387
+ let levels = find_support_resistance(&high, &low, 2, 0.02);
388
+
389
+ // Should find at least one level
390
+ assert!(!levels.is_empty());
391
+ }
392
+
393
+ #[test]
394
+ fn test_trend_line_price_at() {
395
+ let line = TrendLine {
396
+ slope: 1.0,
397
+ intercept: 100.0,
398
+ start_index: 0,
399
+ end_index: 10,
400
+ r2: 0.95,
401
+ trend_type: LevelType::Support,
402
+ };
403
+
404
+ assert!((line.price_at(0) - 100.0).abs() < 1e-10);
405
+ assert!((line.price_at(5) - 105.0).abs() < 1e-10);
406
+ assert!((line.price_at(10) - 110.0).abs() < 1e-10);
407
+ }
408
+
409
+ #[test]
410
+ fn test_fit_line() {
411
+ let points = vec![
412
+ SwingPoint { index: 0, value: 100.0 },
413
+ SwingPoint { index: 5, value: 105.0 },
414
+ SwingPoint { index: 10, value: 110.0 },
415
+ ];
416
+
417
+ let line = fit_line(&points);
418
+
419
+ assert!((line.slope - 1.0).abs() < 1e-10);
420
+ assert!((line.intercept - 100.0).abs() < 1e-10);
421
+ assert!((line.r2 - 1.0).abs() < 1e-10); // Perfect fit
422
+ }
423
+ }