@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,431 @@
|
|
|
1
|
+
//! Breakout Detection
|
|
2
|
+
//!
|
|
3
|
+
//! Functions for detecting breakouts from support/resistance levels.
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
|
|
7
|
+
use super::sr::SupportResistanceLevel;
|
|
8
|
+
|
|
9
|
+
/// Direction of a breakout
|
|
10
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
11
|
+
pub enum BreakoutDirection {
|
|
12
|
+
Up,
|
|
13
|
+
Down,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// A detected breakout
|
|
17
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
18
|
+
pub struct Breakout {
|
|
19
|
+
/// The price level that was broken
|
|
20
|
+
pub level: f64,
|
|
21
|
+
/// Direction of the breakout
|
|
22
|
+
pub direction: BreakoutDirection,
|
|
23
|
+
/// Volume ratio compared to average
|
|
24
|
+
pub volume_ratio: f64,
|
|
25
|
+
/// Whether the breakout has been confirmed (follow-through)
|
|
26
|
+
pub confirmed: bool,
|
|
27
|
+
/// Index where breakout occurred
|
|
28
|
+
pub index: usize,
|
|
29
|
+
/// The support/resistance level that was broken
|
|
30
|
+
pub sr_level: Option<SupportResistanceLevel>,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl Breakout {
|
|
34
|
+
/// Create a new breakout
|
|
35
|
+
pub fn new(level: f64, direction: BreakoutDirection, index: usize) -> Self {
|
|
36
|
+
Self {
|
|
37
|
+
level,
|
|
38
|
+
direction,
|
|
39
|
+
volume_ratio: 1.0,
|
|
40
|
+
confirmed: false,
|
|
41
|
+
index,
|
|
42
|
+
sr_level: None,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Add volume ratio
|
|
47
|
+
pub fn with_volume(mut self, ratio: f64) -> Self {
|
|
48
|
+
self.volume_ratio = ratio;
|
|
49
|
+
self
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// Set confirmation status
|
|
53
|
+
pub fn with_confirmation(mut self, confirmed: bool) -> Self {
|
|
54
|
+
self.confirmed = confirmed;
|
|
55
|
+
self
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Attach the original support/resistance level
|
|
59
|
+
pub fn with_sr_level(mut self, level: SupportResistanceLevel) -> Self {
|
|
60
|
+
self.sr_level = Some(level);
|
|
61
|
+
self
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Detect breakouts from support/resistance levels
|
|
66
|
+
///
|
|
67
|
+
/// # Arguments
|
|
68
|
+
/// * `close` - Array of close prices
|
|
69
|
+
/// * `levels` - Support/resistance levels to check for breakouts
|
|
70
|
+
/// * `volume` - Array of volume data
|
|
71
|
+
/// * `volume_threshold` - Multiple of average volume required for valid breakout
|
|
72
|
+
///
|
|
73
|
+
/// # Returns
|
|
74
|
+
/// Vector of detected breakouts
|
|
75
|
+
pub fn detect_breakouts(
|
|
76
|
+
close: &[f64],
|
|
77
|
+
levels: &[SupportResistanceLevel],
|
|
78
|
+
volume: &[f64],
|
|
79
|
+
volume_threshold: f64,
|
|
80
|
+
) -> Vec<Breakout> {
|
|
81
|
+
let mut breakouts = Vec::new();
|
|
82
|
+
|
|
83
|
+
if close.len() < 2 || volume.is_empty() {
|
|
84
|
+
return breakouts;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Calculate average volume
|
|
88
|
+
let avg_volume = volume.iter().sum::<f64>() / volume.len() as f64;
|
|
89
|
+
if avg_volume <= 0.0 {
|
|
90
|
+
return breakouts;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for i in 1..close.len() {
|
|
94
|
+
for level in levels {
|
|
95
|
+
let prev_close = close[i - 1];
|
|
96
|
+
let curr_close = close[i];
|
|
97
|
+
|
|
98
|
+
// Bullish breakout (resistance broken)
|
|
99
|
+
if prev_close < level.price && curr_close > level.price {
|
|
100
|
+
let vol_idx = i.min(volume.len() - 1);
|
|
101
|
+
let vol_ratio = volume[vol_idx] / avg_volume;
|
|
102
|
+
|
|
103
|
+
if vol_ratio >= volume_threshold {
|
|
104
|
+
breakouts.push(
|
|
105
|
+
Breakout::new(level.price, BreakoutDirection::Up, i)
|
|
106
|
+
.with_volume(vol_ratio)
|
|
107
|
+
.with_sr_level(level.clone()),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Bearish breakout (support broken)
|
|
113
|
+
if prev_close > level.price && curr_close < level.price {
|
|
114
|
+
let vol_idx = i.min(volume.len() - 1);
|
|
115
|
+
let vol_ratio = volume[vol_idx] / avg_volume;
|
|
116
|
+
|
|
117
|
+
if vol_ratio >= volume_threshold {
|
|
118
|
+
breakouts.push(
|
|
119
|
+
Breakout::new(level.price, BreakoutDirection::Down, i)
|
|
120
|
+
.with_volume(vol_ratio)
|
|
121
|
+
.with_sr_level(level.clone()),
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check for confirmation (follow-through)
|
|
129
|
+
let close_len = close.len();
|
|
130
|
+
for breakout in &mut breakouts {
|
|
131
|
+
let look_ahead = (breakout.index + 3).min(close_len - 1);
|
|
132
|
+
if look_ahead > breakout.index {
|
|
133
|
+
match breakout.direction {
|
|
134
|
+
BreakoutDirection::Up => {
|
|
135
|
+
breakout.confirmed = close[look_ahead] > breakout.level;
|
|
136
|
+
}
|
|
137
|
+
BreakoutDirection::Down => {
|
|
138
|
+
breakout.confirmed = close[look_ahead] < breakout.level;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
breakouts
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Detect breakouts from simple price levels
|
|
148
|
+
///
|
|
149
|
+
/// Simplified version that detects breakouts from a list of price levels
|
|
150
|
+
/// without requiring SupportResistanceLevel structs.
|
|
151
|
+
///
|
|
152
|
+
/// # Arguments
|
|
153
|
+
/// * `close` - Array of close prices
|
|
154
|
+
/// * `levels` - Price levels to check for breakouts
|
|
155
|
+
/// * `volume` - Array of volume data
|
|
156
|
+
/// * `volume_threshold` - Multiple of average volume required for valid breakout
|
|
157
|
+
///
|
|
158
|
+
/// # Returns
|
|
159
|
+
/// Vector of detected breakouts (without sr_level populated)
|
|
160
|
+
pub fn detect_breakouts_from_levels(
|
|
161
|
+
close: &[f64],
|
|
162
|
+
levels: &[f64],
|
|
163
|
+
volume: &[f64],
|
|
164
|
+
volume_threshold: f64,
|
|
165
|
+
) -> Vec<Breakout> {
|
|
166
|
+
let mut breakouts = Vec::new();
|
|
167
|
+
|
|
168
|
+
if close.len() < 2 || volume.is_empty() {
|
|
169
|
+
return breakouts;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Calculate average volume
|
|
173
|
+
let avg_volume = volume.iter().sum::<f64>() / volume.len() as f64;
|
|
174
|
+
if avg_volume <= 0.0 {
|
|
175
|
+
return breakouts;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for i in 1..close.len() {
|
|
179
|
+
for &level in levels {
|
|
180
|
+
let prev_close = close[i - 1];
|
|
181
|
+
let curr_close = close[i];
|
|
182
|
+
|
|
183
|
+
// Bullish breakout (resistance broken)
|
|
184
|
+
if prev_close < level && curr_close > level {
|
|
185
|
+
let vol_idx = i.min(volume.len() - 1);
|
|
186
|
+
let vol_ratio = volume[vol_idx] / avg_volume;
|
|
187
|
+
|
|
188
|
+
if vol_ratio >= volume_threshold {
|
|
189
|
+
breakouts.push(
|
|
190
|
+
Breakout::new(level, BreakoutDirection::Up, i).with_volume(vol_ratio),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Bearish breakout (support broken)
|
|
196
|
+
if prev_close > level && curr_close < level {
|
|
197
|
+
let vol_idx = i.min(volume.len() - 1);
|
|
198
|
+
let vol_ratio = volume[vol_idx] / avg_volume;
|
|
199
|
+
|
|
200
|
+
if vol_ratio >= volume_threshold {
|
|
201
|
+
breakouts.push(
|
|
202
|
+
Breakout::new(level, BreakoutDirection::Down, i).with_volume(vol_ratio),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check for confirmation (follow-through)
|
|
210
|
+
let close_len = close.len();
|
|
211
|
+
for breakout in &mut breakouts {
|
|
212
|
+
let look_ahead = (breakout.index + 3).min(close_len - 1);
|
|
213
|
+
if look_ahead > breakout.index {
|
|
214
|
+
match breakout.direction {
|
|
215
|
+
BreakoutDirection::Up => {
|
|
216
|
+
breakout.confirmed = close[look_ahead] > breakout.level;
|
|
217
|
+
}
|
|
218
|
+
BreakoutDirection::Down => {
|
|
219
|
+
breakout.confirmed = close[look_ahead] < breakout.level;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
breakouts
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// Detect all-time high breakouts
|
|
229
|
+
///
|
|
230
|
+
/// Detects when price breaks above all previous highs.
|
|
231
|
+
///
|
|
232
|
+
/// # Arguments
|
|
233
|
+
/// * `high` - Array of high prices
|
|
234
|
+
/// * `close` - Array of close prices
|
|
235
|
+
///
|
|
236
|
+
/// # Returns
|
|
237
|
+
/// Vector of detected breakouts (direction always Up)
|
|
238
|
+
pub fn detect_all_time_high_breakouts(high: &[f64], close: &[f64]) -> Vec<Breakout> {
|
|
239
|
+
let mut breakouts = Vec::new();
|
|
240
|
+
|
|
241
|
+
if high.len() < 2 || close.len() < 2 {
|
|
242
|
+
return breakouts;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let mut running_high = high[0];
|
|
246
|
+
|
|
247
|
+
for i in 1..high.len() {
|
|
248
|
+
if high[i] > running_high {
|
|
249
|
+
// New all-time high
|
|
250
|
+
breakouts.push(Breakout::new(running_high, BreakoutDirection::Up, i));
|
|
251
|
+
running_high = high[i];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
breakouts
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// Detect all-time low breakouts (breakdown)
|
|
259
|
+
///
|
|
260
|
+
/// Detects when price breaks below all previous lows.
|
|
261
|
+
///
|
|
262
|
+
/// # Arguments
|
|
263
|
+
/// * `low` - Array of low prices
|
|
264
|
+
/// * `close` - Array of close prices
|
|
265
|
+
///
|
|
266
|
+
/// # Returns
|
|
267
|
+
/// Vector of detected breakouts (direction always Down)
|
|
268
|
+
pub fn detect_all_time_low_breakouts(low: &[f64], close: &[f64]) -> Vec<Breakout> {
|
|
269
|
+
let mut breakouts = Vec::new();
|
|
270
|
+
|
|
271
|
+
if low.len() < 2 || close.len() < 2 {
|
|
272
|
+
return breakouts;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let mut running_low = low[0];
|
|
276
|
+
|
|
277
|
+
for i in 1..low.len() {
|
|
278
|
+
if low[i] < running_low {
|
|
279
|
+
// New all-time low
|
|
280
|
+
breakouts.push(Breakout::new(running_low, BreakoutDirection::Down, i));
|
|
281
|
+
running_low = low[i];
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
breakouts
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// Detect consolidation breakouts
|
|
289
|
+
///
|
|
290
|
+
/// Detects breakouts from a period of low volatility (consolidation).
|
|
291
|
+
///
|
|
292
|
+
/// # Arguments
|
|
293
|
+
/// * `high` - Array of high prices
|
|
294
|
+
/// * `low` - Array of low prices
|
|
295
|
+
/// * `close` - Array of close prices
|
|
296
|
+
/// * `consolidation_periods` - Minimum number of periods for consolidation
|
|
297
|
+
/// * `range_threshold` - Maximum range (as % of price) to consider consolidation
|
|
298
|
+
///
|
|
299
|
+
/// # Returns
|
|
300
|
+
/// Vector of detected breakouts
|
|
301
|
+
pub fn detect_consolidation_breakouts(
|
|
302
|
+
high: &[f64],
|
|
303
|
+
low: &[f64],
|
|
304
|
+
close: &[f64],
|
|
305
|
+
consolidation_periods: usize,
|
|
306
|
+
range_threshold: f64,
|
|
307
|
+
) -> Vec<Breakout> {
|
|
308
|
+
let mut breakouts = Vec::new();
|
|
309
|
+
|
|
310
|
+
if high.len() < consolidation_periods + 1 {
|
|
311
|
+
return breakouts;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
for i in consolidation_periods..high.len() {
|
|
315
|
+
// Check if the previous periods were consolidating
|
|
316
|
+
let consolidation_start = i - consolidation_periods;
|
|
317
|
+
let window_high = &high[consolidation_start..i];
|
|
318
|
+
let window_low = &low[consolidation_start..i];
|
|
319
|
+
|
|
320
|
+
let highest = window_high.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
|
321
|
+
let lowest = window_low.iter().cloned().fold(f64::INFINITY, f64::min);
|
|
322
|
+
|
|
323
|
+
let mid_price = (highest + lowest) / 2.0;
|
|
324
|
+
let range_pct = (highest - lowest) / mid_price;
|
|
325
|
+
|
|
326
|
+
// Check if we were in consolidation
|
|
327
|
+
if range_pct < range_threshold {
|
|
328
|
+
// Check for breakout above consolidation
|
|
329
|
+
if close[i] > highest {
|
|
330
|
+
breakouts.push(
|
|
331
|
+
Breakout::new(highest, BreakoutDirection::Up, i).with_confirmation(true),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
// Check for breakdown below consolidation
|
|
335
|
+
else if close[i] < lowest {
|
|
336
|
+
breakouts.push(
|
|
337
|
+
Breakout::new(lowest, BreakoutDirection::Down, i).with_confirmation(true),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
breakouts
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#[cfg(test)]
|
|
347
|
+
mod tests {
|
|
348
|
+
use super::*;
|
|
349
|
+
use super::super::sr::LevelType;
|
|
350
|
+
|
|
351
|
+
fn create_test_sr_level(price: f64, level_type: LevelType) -> SupportResistanceLevel {
|
|
352
|
+
SupportResistanceLevel {
|
|
353
|
+
price,
|
|
354
|
+
strength: 1,
|
|
355
|
+
touches: 1,
|
|
356
|
+
first_touch: 0,
|
|
357
|
+
last_touch: 0,
|
|
358
|
+
level_type,
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#[test]
|
|
363
|
+
fn test_detect_breakouts_basic() {
|
|
364
|
+
let close = vec![95.0, 98.0, 99.0, 102.0, 105.0]; // Crosses above 100 at index 3
|
|
365
|
+
let volume = vec![100.0, 110.0, 120.0, 150.0, 140.0];
|
|
366
|
+
let levels = vec![create_test_sr_level(100.0, LevelType::Resistance)];
|
|
367
|
+
|
|
368
|
+
let breakouts = detect_breakouts(&close, &levels, &volume, 1.0);
|
|
369
|
+
|
|
370
|
+
assert!(!breakouts.is_empty());
|
|
371
|
+
assert_eq!(breakouts[0].direction, BreakoutDirection::Up);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#[test]
|
|
375
|
+
fn test_detect_breakouts_from_levels() {
|
|
376
|
+
let close = vec![95.0, 98.0, 99.0, 102.0, 105.0]; // Crosses above 100 at index 3
|
|
377
|
+
let volume = vec![100.0, 110.0, 120.0, 150.0, 140.0];
|
|
378
|
+
let levels = vec![100.0, 105.0];
|
|
379
|
+
|
|
380
|
+
let breakouts = detect_breakouts_from_levels(&close, &levels, &volume, 1.0);
|
|
381
|
+
|
|
382
|
+
assert!(!breakouts.is_empty());
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#[test]
|
|
386
|
+
fn test_detect_all_time_high_breakouts() {
|
|
387
|
+
let high = vec![100.0, 102.0, 101.0, 105.0, 103.0, 108.0];
|
|
388
|
+
let close = vec![99.0, 101.0, 100.0, 104.0, 102.0, 107.0];
|
|
389
|
+
|
|
390
|
+
let breakouts = detect_all_time_high_breakouts(&high, &close);
|
|
391
|
+
|
|
392
|
+
assert!(!breakouts.is_empty());
|
|
393
|
+
// Should detect breakouts at indices where new highs are made
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[test]
|
|
397
|
+
fn test_detect_consolidation_breakouts() {
|
|
398
|
+
// Consolidation then breakout
|
|
399
|
+
let high = vec![
|
|
400
|
+
100.0, 101.0, 100.5, 101.0, // Consolidation (range ~1%)
|
|
401
|
+
105.0, // Breakout
|
|
402
|
+
];
|
|
403
|
+
let low = vec![
|
|
404
|
+
99.0, 99.5, 99.5, 99.8,
|
|
405
|
+
102.0,
|
|
406
|
+
];
|
|
407
|
+
let close = vec![
|
|
408
|
+
99.5, 100.5, 100.0, 100.5,
|
|
409
|
+
104.0, // Closes above consolidation
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
let breakouts = detect_consolidation_breakouts(&high, &low, &close, 4, 0.03);
|
|
413
|
+
|
|
414
|
+
assert!(!breakouts.is_empty());
|
|
415
|
+
assert_eq!(breakouts[0].direction, BreakoutDirection::Up);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
#[test]
|
|
419
|
+
fn test_breakout_confirmation() {
|
|
420
|
+
let close = vec![95.0, 100.0, 102.0, 101.0, 103.0]; // Stays above 100
|
|
421
|
+
let volume = vec![100.0, 150.0, 120.0, 110.0, 130.0];
|
|
422
|
+
let levels = vec![create_test_sr_level(100.0, LevelType::Resistance)];
|
|
423
|
+
|
|
424
|
+
let breakouts = detect_breakouts(&close, &levels, &volume, 1.0);
|
|
425
|
+
|
|
426
|
+
// Breakout should be confirmed since price stays above level
|
|
427
|
+
if !breakouts.is_empty() {
|
|
428
|
+
assert!(breakouts[0].confirmed);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|