@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.
- package/README.md +161 -0
- package/bun-ffi.d.ts +54 -0
- package/dist/index.js +576 -0
- package/dist/src/index.d.ts +324 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/types/index.d.ts +403 -0
- package/dist/types/index.d.ts.map +1 -0
- package/native/README.md +62 -0
- package/native/darwin-arm64/libquant_rust.dylib +0 -0
- package/package.json +70 -0
- package/scripts/postinstall.cjs +85 -0
- package/src/ffi.rs +496 -0
- package/src/index.ts +1073 -0
- package/src/indicators/ma.rs +222 -0
- package/src/indicators/mod.rs +18 -0
- package/src/indicators/momentum.rs +353 -0
- package/src/indicators/sr.rs +195 -0
- package/src/indicators/trend.rs +351 -0
- package/src/indicators/volatility.rs +270 -0
- package/src/indicators/volume.rs +213 -0
- package/src/lib.rs +130 -0
- package/src/patterns/breakout.rs +431 -0
- package/src/patterns/chart.rs +772 -0
- package/src/patterns/mod.rs +394 -0
- package/src/patterns/sr.rs +423 -0
- package/src/prediction/amm.rs +338 -0
- package/src/prediction/arbitrage.rs +230 -0
- package/src/prediction/calibration.rs +317 -0
- package/src/prediction/kelly.rs +232 -0
- package/src/prediction/lmsr.rs +194 -0
- package/src/prediction/mod.rs +59 -0
- package/src/prediction/odds.rs +229 -0
- package/src/prediction/pnl.rs +254 -0
- package/src/prediction/risk.rs +228 -0
- package/src/risk/beta.rs +257 -0
- package/src/risk/drawdown.rs +256 -0
- package/src/risk/leverage.rs +201 -0
- package/src/risk/mod.rs +388 -0
- package/src/risk/portfolio.rs +287 -0
- package/src/risk/ratios.rs +290 -0
- package/src/risk/sizing.rs +194 -0
- package/src/risk/var.rs +222 -0
- package/src/stats/cdf.rs +257 -0
- package/src/stats/correlation.rs +225 -0
- package/src/stats/distribution.rs +194 -0
- package/src/stats/hypothesis.rs +177 -0
- package/src/stats/matrix.rs +346 -0
- package/src/stats/mod.rs +257 -0
- package/src/stats/regression.rs +239 -0
- package/src/stats/rolling.rs +193 -0
- package/src/stats/timeseries.rs +263 -0
- package/src/types.rs +224 -0
- package/src/utils/mod.rs +215 -0
- package/src/utils/normalize.rs +192 -0
- package/src/utils/price.rs +167 -0
- package/src/utils/quantiles.rs +177 -0
- package/src/utils/returns.rs +158 -0
- package/src/utils/rolling.rs +97 -0
- package/src/utils/stats.rs +154 -0
- 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
|
+
}
|