finance_solution/stocks/ta/
cci.rs1use crate::stocks::ta::common::{opt_cell, require_hlc};
64use crate::stocks::ta::ring::RingF64;
65use crate::util::error::{require_finite, FinanceError, FinanceResult};
66use crate::util::primitives::PeriodLength;
67use crate::{columns_with_strings, print_table_locale_opt};
68
69#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
71pub struct CciParams {
72 pub period: usize,
73}
74
75impl CciParams {
76 pub const fn new(period: usize) -> Self {
77 Self { period }
78 }
79
80 pub const fn period_20() -> Self {
81 Self { period: 20 }
82 }
83
84 pub const fn period_14() -> Self {
85 Self { period: 14 }
86 }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
90pub struct ValidatedCci {
91 params: CciParams,
92}
93
94impl ValidatedCci {
95 pub fn new(params: CciParams) -> FinanceResult<Self> {
96 PeriodLength::new(params.period)?;
97 Ok(Self { params })
98 }
99
100 pub fn params(self) -> CciParams {
101 self.params
102 }
103
104 pub fn compute(self, high: &[f64], low: &[f64], close: &[f64]) -> FinanceResult<CciSeries> {
105 cci_validated(high, low, close, self)
106 }
107}
108
109#[derive(Clone, Debug, PartialEq)]
110pub struct CciSeries {
111 pub cci: Vec<Option<f64>>,
112 pub params: CciParams,
113}
114
115impl CciSeries {
116 pub fn last(&self) -> Option<f64> {
117 self.cci.iter().rev().find_map(|x| *x)
118 }
119}
120
121#[derive(Clone, Debug)]
123pub struct CciState {
124 params: CciParams,
125 tp: RingF64,
126 scratch: Vec<f64>,
127 last: Option<f64>,
128}
129
130impl CciState {
131 pub fn new(params: CciParams) -> FinanceResult<Self> {
132 let _ = ValidatedCci::new(params)?;
133 Ok(Self {
134 params,
135 tp: RingF64::with_capacity(params.period),
136 scratch: Vec::with_capacity(params.period),
137 last: None,
138 })
139 }
140
141 pub fn from_history(
142 params: CciParams,
143 high: &[f64],
144 low: &[f64],
145 close: &[f64],
146 ) -> FinanceResult<Self> {
147 let mut s = Self::new(params)?;
148 let _ = s.push_bars(high, low, close)?;
149 Ok(s)
150 }
151
152 pub fn params(&self) -> CciParams {
153 self.params
154 }
155
156 pub fn reset(&mut self) {
157 self.tp.clear();
158 self.scratch.clear();
159 self.last = None;
160 }
161
162 pub fn push(&mut self, high: f64, low: f64, close: f64) -> FinanceResult<Option<f64>> {
163 require_finite("high", high)?;
164 require_finite("low", low)?;
165 require_finite("close", close)?;
166 if high < low {
167 return Err(FinanceError::InvalidCashflow {
168 message: "high must be >= low for each bar",
169 });
170 }
171 let tp = (high + low + close) / 3.0;
172 let _ = self.tp.push(tp);
173 if !self.tp.is_full() {
174 self.last = None;
175 return Ok(None);
176 }
177 self.tp.copy_ordered(&mut self.scratch);
178 let n = self.scratch.len() as f64;
179 let mean = self.tp.sum() / n;
180 let mut md = 0.0;
181 for &x in &self.scratch {
182 md += (x - mean).abs();
183 }
184 md /= n;
185 let out = if md <= 0.0 {
186 None
187 } else {
188 Some((tp - mean) / (0.015 * md))
189 };
190 self.last = out;
191 Ok(out)
192 }
193
194 pub fn push_bars(
195 &mut self,
196 high: &[f64],
197 low: &[f64],
198 close: &[f64],
199 ) -> FinanceResult<Vec<Option<f64>>> {
200 require_hlc(high, low, close)?;
201 let mut out = Vec::with_capacity(close.len());
202 for i in 0..close.len() {
203 out.push(self.push(high[i], low[i], close[i])?);
204 }
205 Ok(out)
206 }
207
208 pub fn last(&self) -> Option<f64> {
209 self.last
210 }
211}
212
213pub fn cci(
214 high: &[f64],
215 low: &[f64],
216 close: &[f64],
217 params: CciParams,
218) -> FinanceResult<CciSeries> {
219 ValidatedCci::new(params)?.compute(high, low, close)
220}
221
222fn cci_validated(
223 high: &[f64],
224 low: &[f64],
225 close: &[f64],
226 eng: ValidatedCci,
227) -> FinanceResult<CciSeries> {
228 let mut st = CciState::new(eng.params)?;
229 let cci = st.push_bars(high, low, close)?;
230 Ok(CciSeries {
231 cci,
232 params: eng.params,
233 })
234}
235
236#[derive(Clone, Debug)]
237pub struct CciSolution {
238 series: CciSeries,
239 close: Vec<f64>,
240 formula: String,
241}
242
243impl CciSolution {
244 pub fn series(&self) -> &CciSeries {
245 &self.series
246 }
247 pub fn formula(&self) -> &str {
248 &self.formula
249 }
250
251 pub fn print_table(&self) {
252 self.print_table_locale_opt(None, None);
253 }
254
255 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
256 self.print_table_locale_opt(Some(locale), Some(precision));
257 }
258
259 fn print_table_locale_opt(
260 &self,
261 locale: Option<&num_format::Locale>,
262 precision: Option<usize>,
263 ) {
264 let columns = columns_with_strings(&[
265 ("period", "i", true),
266 ("close", "f", true),
267 ("cci", "f", true),
268 ]);
269 let data = self
270 .close
271 .iter()
272 .enumerate()
273 .map(|(i, c)| vec![i.to_string(), c.to_string(), opt_cell(self.series.cci[i])])
274 .collect();
275 print_table_locale_opt(&columns, data, locale, precision);
276 }
277}
278
279pub fn cci_solution(
290 high: &[f64],
291 low: &[f64],
292 close: &[f64],
293 params: CciParams,
294) -> FinanceResult<CciSolution> {
295 let series = cci(high, low, close, params)?;
296 Ok(CciSolution {
297 series,
298 close: close.to_vec(),
299 formula: format!(
300 "CCI = (TP - SMA(TP,{})) / (0.015 * MD); TP=(H+L+C)/3",
301 params.period
302 ),
303 })
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 #[test]
311 fn constant_is_none() {
312 let x = vec![100.0; 25];
313 let s = cci(&x, &x, &x, CciParams::period_20()).unwrap();
314 assert!(s.cci[19].is_none());
315 assert!(s.cci[24].is_none());
316 }
317
318 #[test]
319 fn rising_path_positive() {
320 let n = 40usize;
321 let h: Vec<_> = (0..n).map(|i| 101.0 + i as f64).collect();
322 let l: Vec<_> = (0..n).map(|i| 99.0 + i as f64).collect();
323 let c: Vec<_> = (0..n).map(|i| 100.0 + i as f64).collect();
324 let s = cci(&h, &l, &c, CciParams::period_20()).unwrap();
325 assert!(s.cci[39].unwrap() > 0.0);
326 }
327
328 #[test]
329 fn state_parity() {
330 let n = 50usize;
331 let h: Vec<_> = (0..n).map(|i| 12.0 + (i as f64) * 0.05).collect();
332 let l: Vec<_> = (0..n).map(|i| 10.0 + (i as f64) * 0.05).collect();
333 let c: Vec<_> = (0..n).map(|i| 11.0 + (i as f64) * 0.05).collect();
334 let p = CciParams::period_14();
335 let batch = cci(&h, &l, &c, p).unwrap();
336 let mut st = CciState::new(p).unwrap();
337 for i in 0..n {
338 let o = st.push(h[i], l[i], c[i]).unwrap();
339 match (o, batch.cci[i]) {
340 (None, None) => {}
341 (Some(a), Some(b)) => assert!((a - b).abs() < 1e-9, "{a} vs {b}"),
342 other => panic!("{other:?}"),
343 }
344 }
345 }
346}