@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,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
|
+
}
|