finance_solution/stocks/ta/
stochastic.rs1use crate::stocks::ta::common::opt_cell;
76use crate::util::error::{require_finite, FinanceError, FinanceResult};
77use crate::util::primitives::PeriodLength;
78use crate::{columns_with_strings, print_table_locale_opt};
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
85pub struct StochasticParams {
86 pub k_period: usize,
88 pub k_smooth: usize,
90 pub d_period: usize,
92}
93
94impl StochasticParams {
95 pub const fn fast(k_period: usize, d_period: usize) -> Self {
99 Self {
100 k_period,
101 k_smooth: 1,
102 d_period,
103 }
104 }
105
106 pub const fn full(k_period: usize, k_smooth: usize, d_period: usize) -> Self {
110 Self {
111 k_period,
112 k_smooth,
113 d_period,
114 }
115 }
116
117 pub const fn warm_up_bars(self) -> usize {
119 self.k_period
121 .saturating_add(self.k_smooth.saturating_sub(1))
122 .saturating_add(self.d_period.saturating_sub(1))
123 }
124}
125
126#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
130pub struct ValidatedStochastic {
131 params: StochasticParams,
132}
133
134impl ValidatedStochastic {
135 pub fn new(params: StochasticParams) -> FinanceResult<Self> {
137 PeriodLength::new(params.k_period)?;
138 PeriodLength::new(params.k_smooth)?;
139 PeriodLength::new(params.d_period)?;
140 Ok(Self { params })
141 }
142
143 #[inline]
144 pub fn params(self) -> StochasticParams {
145 self.params
146 }
147
148 pub fn compute(
150 self,
151 high: &[f64],
152 low: &[f64],
153 close: &[f64],
154 ) -> FinanceResult<StochasticSeries> {
155 stochastics_validated(high, low, close, self)
156 }
157}
158
159#[derive(Clone, Debug, PartialEq)]
161pub struct StochasticSeries {
162 pub k: Vec<Option<f64>>,
163 pub d: Vec<Option<f64>>,
164 pub params: StochasticParams,
165}
166
167impl StochasticSeries {
168 pub fn last_kd(&self) -> Option<(f64, f64)> {
170 let k = self.k.iter().rev().find_map(|x| *x)?;
171 let d = self.d.iter().rev().find_map(|x| *x)?;
172 Some((k, d))
173 }
174}
175
176pub fn stochastics(
180 high: &[f64],
181 low: &[f64],
182 close: &[f64],
183 params: StochasticParams,
184) -> FinanceResult<StochasticSeries> {
185 let v = ValidatedStochastic::new(params)?;
186 stochastics_validated(high, low, close, v)
187}
188
189pub fn stochastics_solution(
205 high: &[f64],
206 low: &[f64],
207 close: &[f64],
208 params: StochasticParams,
209) -> FinanceResult<StochasticSolution> {
210 let series = stochastics(high, low, close, params)?;
211 let formula = format!(
212 "%K: stoch(k={}, smooth={}); %D: SMA(%K, {})",
213 params.k_period, params.k_smooth, params.d_period
214 );
215 let symbolic =
216 "raw_%K = 100 * (C - LL) / (HH - LL); %K = SMA(raw_%K, k_smooth); %D = SMA(%K, d)"
217 .to_string();
218 Ok(StochasticSolution {
219 series,
220 close: close.to_vec(),
221 formula,
222 symbolic_formula: symbolic,
223 })
224}
225
226#[derive(Clone, Debug)]
228pub struct StochasticSolution {
229 series: StochasticSeries,
230 close: Vec<f64>,
231 formula: String,
232 symbolic_formula: String,
233}
234
235impl StochasticSolution {
236 pub fn series(&self) -> &StochasticSeries {
237 &self.series
238 }
239 pub fn formula(&self) -> &str {
240 &self.formula
241 }
242 pub fn symbolic_formula(&self) -> &str {
243 &self.symbolic_formula
244 }
245 pub fn params(&self) -> StochasticParams {
246 self.series.params
247 }
248
249 pub fn print_table(&self) {
257 self.print_table_locale_opt(None, None);
258 }
259
260 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
261 self.print_table_locale_opt(Some(locale), Some(precision));
262 }
263
264 fn print_table_locale_opt(
265 &self,
266 locale: Option<&num_format::Locale>,
267 precision: Option<usize>,
268 ) {
269 let columns = columns_with_strings(&[
270 ("period", "i", true),
271 ("close", "f", true),
272 ("k", "f", true),
273 ("d", "f", true),
274 ]);
275 let data = self
276 .close
277 .iter()
278 .enumerate()
279 .map(|(i, c)| {
280 vec![
281 i.to_string(),
282 c.to_string(),
283 opt_cell(self.series.k[i]),
284 opt_cell(self.series.d[i]),
285 ]
286 })
287 .collect();
288 print_table_locale_opt(&columns, data, locale, precision);
289 }
290}
291
292fn stochastics_validated(
293 high: &[f64],
294 low: &[f64],
295 close: &[f64],
296 v: ValidatedStochastic,
297) -> FinanceResult<StochasticSeries> {
298 let p = v.params;
299 check_hlc(high, low, close)?;
300 let n = close.len();
301 let mut raw_k = vec![None; n];
302 let kp = p.k_period;
303 let mut prev_raw: Option<f64> = None;
304
305 for i in 0..n {
306 if i + 1 < kp {
307 continue;
308 }
309 let start = i + 1 - kp;
310 let mut hh = f64::NEG_INFINITY;
311 let mut ll = f64::INFINITY;
312 for j in start..=i {
313 hh = hh.max(high[j]);
314 ll = ll.min(low[j]);
315 }
316 let range = hh - ll;
317 let raw = if range == 0.0 {
319 prev_raw.unwrap_or(50.0)
320 } else {
321 100.0 * (close[i] - ll) / range
322 };
323 prev_raw = Some(raw);
324 raw_k[i] = Some(raw);
325 }
326
327 let smooth_k = sma_option_series(&raw_k, p.k_smooth);
328 let d_line = sma_option_series(&smooth_k, p.d_period);
329
330 Ok(StochasticSeries {
331 k: smooth_k,
332 d: d_line,
333 params: p,
334 })
335}
336
337fn sma_option_series(data: &[Option<f64>], period: usize) -> Vec<Option<f64>> {
339 let n = data.len();
340 let mut out = vec![None; n];
341 if period == 0 || n < period {
342 return out;
343 }
344 for i in (period - 1)..n {
345 let start = i + 1 - period;
346 let mut sum = 0.0;
347 let mut ok = true;
348 for j in start..=i {
349 match data[j] {
350 Some(v) => sum += v,
351 None => {
352 ok = false;
353 break;
354 }
355 }
356 }
357 if ok {
358 out[i] = Some(sum / period as f64);
359 }
360 }
361 out
362}
363
364fn check_hlc(high: &[f64], low: &[f64], close: &[f64]) -> FinanceResult<()> {
365 if high.is_empty() {
366 return Err(FinanceError::EmptyInput { what: "high" });
367 }
368 if high.len() != low.len() || high.len() != close.len() {
369 return Err(FinanceError::LengthMismatch {
370 left: high.len(),
371 right: close.len(),
372 context: "stochastic high/low/close",
373 });
374 }
375 for i in 0..high.len() {
376 require_finite("high", high[i])?;
377 require_finite("low", low[i])?;
378 require_finite("close", close[i])?;
379 if high[i] < low[i] {
380 return Err(FinanceError::InvalidCashflow {
381 message: "high must be >= low for each bar",
382 });
383 }
384 }
385 Ok(())
386}
387
388#[cfg(test)]
389mod tests {
390 use super::*;
391
392 #[test]
393 fn fast_const_and_validate() {
394 let p = StochasticParams::fast(9, 3);
395 assert_eq!(p.k_smooth, 1);
396 let v = ValidatedStochastic::new(p).unwrap();
397 assert_eq!(v.params().k_period, 9);
398 }
399
400 #[test]
401 fn full_presets() {
402 let p = StochasticParams::full(14, 3, 3);
403 assert_eq!(p.warm_up_bars(), 14 + 2 + 2);
404 }
405
406 #[test]
407 fn series_length_and_warmup() {
408 let n = 30;
409 let high: Vec<_> = (0..n).map(|i| 100.0 + i as f64).collect();
410 let low: Vec<_> = (0..n).map(|i| 90.0 + i as f64).collect();
411 let close: Vec<_> = (0..n).map(|i| 95.0 + i as f64).collect();
412 let out = stochastics(&high, &low, &close, StochasticParams::fast(14, 3)).unwrap();
413 assert_eq!(out.k.len(), n);
414 assert!(out.k[12].is_none()); assert!(out.k[13].is_some());
416 assert!(out.d[13 + 2].is_some());
418 }
419
420 #[test]
421 fn zero_period_err() {
422 assert!(ValidatedStochastic::new(StochasticParams {
423 k_period: 0,
424 k_smooth: 1,
425 d_period: 3
426 })
427 .is_err());
428 }
429
430 #[test]
431 fn flat_window_carries_previous_raw() {
432 let high = [10.0, 11.0, 12.0, 12.0, 12.0];
434 let low = [9.0, 10.0, 12.0, 12.0, 12.0];
435 let close = [9.5, 10.5, 12.0, 12.0, 12.0];
436 let p = StochasticParams::fast(3, 1);
437 let s = stochastics(&high, &low, &close, p).unwrap();
438 let k3 = s.k[3].unwrap();
439 assert!((s.k[4].unwrap() - k3).abs() < 1e-12);
441 assert!(s.k[4].is_some());
444 }
445
446 #[test]
447 fn k_in_unit_interval_when_range_positive() {
448 let n = 40;
449 let high: Vec<_> = (0..n).map(|i| 100.0 + (i % 5) as f64).collect();
450 let low: Vec<_> = (0..n).map(|i| 90.0 + (i % 5) as f64).collect();
451 let close: Vec<_> = (0..n).map(|i| 95.0 + (i % 5) as f64 * 0.5).collect();
452 let s = stochastics(&high, &low, &close, StochasticParams::full(14, 3, 3)).unwrap();
453 for k in s.k.iter().flatten() {
454 assert!(*k >= -1e-9 && *k <= 100.0 + 1e-9, "k={k}");
455 }
456 }
457
458 #[test]
459 fn high_lt_low_err() {
460 let h = [10.0, 9.0];
461 let l = [9.0, 10.0];
462 let c = [9.5, 9.5];
463 assert!(stochastics(&h, &l, &c, StochasticParams::fast(2, 1)).is_err());
464 }
465}