1use crate::history::DatedBar;
27use crate::indicators::{rolling_std, sma, OHLCV};
28use crate::sd2::features::FeatureFrame;
29use crate::sd2::primitives::{highest, squash};
30use chrono::{Datelike, NaiveDate};
31
32pub const GROUPS: &[(&str, &[&str])] = &[
34 ("sd", &["sd_blend", "sd_agree", "sd_fire_blend", "sd_comp", "sd_energy", "sd_stretch", "sd_semi"]),
35 ("mom", &["r5", "r21", "r63", "r126", "mom12_1"]),
36 ("trend", &["d50", "d200", "dd252"]),
37 ("vol", &["vol_ratio", "vol_level"]),
38 ("rel", &["rel63"]),
39 ("mkt", &["spy_trend200", "spy_tsmom", "spy_volratio", "spy_r5", "spy_r21"]),
40 ("vix", &["vix_rel", "vix_level", "vix_vrp"]),
41 ("season", &["halloween", "month_sin", "month_cos"]),
42 ("sdw", &["w_blend", "w_agree", "w_fire_blend", "w_comp", "w_energy", "w_stretch"]),
43 ("sdm", &["m_blend", "m_agree", "m_fire_blend", "m_comp", "m_energy", "m_stretch"]),
44];
45
46pub fn n_features() -> usize {
47 GROUPS.iter().map(|g| g.1.len()).sum()
48}
49
50pub fn group_range(name: &str) -> std::ops::Range<usize> {
52 let mut start = 0;
53 for (g, cols) in GROUPS {
54 if *g == name {
55 return start..start + cols.len();
56 }
57 start += cols.len();
58 }
59 panic!("unknown feature group {name}");
60}
61
62fn log_returns(close: &[f64]) -> Vec<f64> {
63 (0..close.len()).map(|i| if i == 0 { 0.0 } else { (close[i] / close[i - 1]).ln() }).collect()
64}
65
66fn norm_ret(close: &[f64], v126: &[f64], i: usize, from: usize, h: usize) -> f64 {
68 if i < from + h || !v126[i].is_finite() || v126[i] <= 0.0 {
69 return f64::NAN;
70 }
71 squash((close[i - from] / close[i - from - h]).ln() / (v126[i] * (h as f64).sqrt()), 2.0)
72}
73
74pub struct MarketWeather {
76 pub dates: Vec<NaiveDate>,
77 pub rows: Vec<[f64; 8]>,
78 pub close: Vec<f64>,
79}
80
81impl MarketWeather {
82 pub fn new(spy: &[DatedBar], vix: &[DatedBar]) -> Self {
84 let close: Vec<f64> = spy.iter().map(|b| b.bar.close).collect();
85 let n = close.len();
86 let lr = log_returns(&close);
87 let v20 = rolling_std(&lr, 20);
88 let v126 = rolling_std(&lr, 126);
89 let s200 = sma(&close, 200);
90 let vix_dates: Vec<NaiveDate> = vix.iter().map(|b| b.date).collect();
91 let vix_close: Vec<f64> = vix.iter().map(|b| b.bar.close).collect();
92 let vix_mean = sma(&vix_close, 252);
93 let rows = (0..n)
94 .map(|i| {
95 if i < 252 {
96 return [f64::NAN; 8];
97 }
98 let j = match vix_dates.binary_search(&spy[i].date) {
99 Ok(j) => Some(j),
100 Err(0) => None,
101 Err(j) => Some(j - 1),
102 };
103 let (vrel, vlvl, vrp) = match j {
104 Some(j) if j >= 252 && vix_close[j] > 0.0 => {
105 let v = vix_close[j];
106 (
107 squash((v / vix_mean[j]).ln(), 0.5),
108 squash((v / 20.0).ln(), 0.5),
109 squash((v / 100.0 / (v20[i] * 252f64.sqrt())).ln(), 0.5),
110 )
111 }
112 _ => (0.0, 0.0, 0.0),
113 };
114 [
115 squash((close[i] / s200[i]).ln() / (v126[i] * 200f64.sqrt() * 0.5), 1.0),
116 squash((close[i] / close[i - 126]).ln() / (v126[i] * 126f64.sqrt()), 1.0),
117 squash(v20[i] / v126[i] - 1.0, 0.5),
118 norm_ret(&close, &v126, i, 0, 5),
119 norm_ret(&close, &v126, i, 0, 21),
120 vrel,
121 vlvl,
122 vrp,
123 ]
124 })
125 .collect();
126 Self { dates: spy.iter().map(|b| b.date).collect(), rows, close }
127 }
128
129 pub fn at(&self, d: NaiveDate) -> Option<(usize, [f64; 8])> {
131 let i = match self.dates.binary_search(&d) {
132 Ok(i) => i,
133 Err(0) => return None,
134 Err(i) => i - 1,
135 };
136 Some((i, self.rows[i]))
137 }
138}
139
140pub fn slow_sd(bars: &[OHLCV], dates: &[NaiveDate], monthly: bool) -> Vec<[f64; 6]> {
146 let key = |d: &NaiveDate| if monthly { d.year() * 12 + d.month() as i32 } else { d.iso_week().year() * 53 + d.iso_week().week() as i32 };
147 let mut agg: Vec<OHLCV> = Vec::new();
148 let mut period_of_day = Vec::with_capacity(bars.len());
149 let mut last_key = None;
150 for (b, d) in bars.iter().zip(dates) {
151 let k = key(d);
152 if last_key != Some(k) {
153 agg.push(b.clone());
154 last_key = Some(k);
155 } else {
156 let a = agg.last_mut().unwrap();
157 a.high = a.high.max(b.high);
158 a.low = a.low.min(b.low);
159 a.close = b.close;
160 a.volume += b.volume;
161 }
162 period_of_day.push(agg.len() - 1);
163 }
164 let f = FeatureFrame::compute(&agg, crate::sd2::features::FeatureConfig::default());
165 let clean = |v: f64| if v.is_finite() { v } else { 0.0 };
166 period_of_day
167 .into_iter()
168 .map(|p| {
169 if p == 0 || p - 1 < 30 {
170 return [0.0; 6];
171 }
172 let j = p - 1;
173 let alive = if f.fire_alive(j) { 1.0 } else { 0.0 };
174 [
175 clean(f.blend[j]),
176 clean(f.agree[j]),
177 clean(alive * f.blend[j] * f.decay[j]),
178 f.comp[j] as f64 / 3.0,
179 clean(squash(f.energy[j], 8.0)),
180 clean(f.stretch[j]),
181 ]
182 })
183 .collect()
184}
185
186pub fn direction_frame(bars: &[OHLCV], dates: &[NaiveDate], f: &FeatureFrame, mkt: &MarketWeather) -> Vec<Vec<f64>> {
188 let close: Vec<f64> = bars.iter().map(|b| b.close).collect();
189 let n = close.len();
190 let lr = log_returns(&close);
191 let v20 = rolling_std(&lr, 20);
192 let v126 = rolling_std(&lr, 126);
193 let s50 = sma(&close, 50);
194 let s200 = sma(&close, 200);
195 let hi252 = highest(&close, 252);
196 let weekly = slow_sd(bars, dates, false);
197 let monthly = slow_sd(bars, dates, true);
198 (0..n)
199 .map(|i| {
200 if i < 260 || !v126[i].is_finite() || v126[i] <= 0.0 {
201 return vec![f64::NAN; n_features()];
202 }
203 let alive = if f.fire_alive(i) { 1.0 } else { 0.0 };
204 let mut r = vec![
205 f.blend[i],
206 f.agree[i],
207 alive * f.blend[i] * f.decay[i],
208 f.comp[i] as f64 / 3.0,
209 squash(f.energy[i], 8.0),
210 f.stretch[i],
211 f.semi[i],
212 ];
213 r.extend([
214 norm_ret(&close, &v126, i, 0, 5),
215 norm_ret(&close, &v126, i, 0, 21),
216 norm_ret(&close, &v126, i, 0, 63),
217 norm_ret(&close, &v126, i, 0, 126),
218 norm_ret(&close, &v126, i, 21, 231),
219 ]);
220 r.extend([
221 squash((close[i] / s50[i]).ln() / (v126[i] * 25f64.sqrt()), 2.0),
222 squash((close[i] / s200[i]).ln() / (v126[i] * 100f64.sqrt()), 2.0),
223 squash((close[i] / hi252[i]).ln() / (v126[i] * 252f64.sqrt()), 1.0),
224 ]);
225 r.extend([squash((v20[i] / v126[i]).ln(), 0.5), squash((v126[i] * 252f64.sqrt() / 0.25).ln(), 1.0)]);
226 let (mi, m) = match mkt.at(dates[i]) {
227 Some(x) => x,
228 None => return vec![f64::NAN; n_features()],
229 };
230 let spy63 = if mi >= 63 { (mkt.close[mi] / mkt.close[mi - 63]).ln() } else { f64::NAN };
231 r.push(squash(((close[i] / close[i - 63]).ln() - spy63) / (v126[i] * 63f64.sqrt()), 2.0));
232 r.extend(&m[0..5]);
233 r.extend(&m[5..8]);
234 let month = dates[i].month() as f64;
235 let angle = std::f64::consts::TAU * (month - 0.5) / 12.0;
236 r.extend([if (5..=10).contains(&dates[i].month()) { 0.0 } else { 1.0 }, angle.sin(), angle.cos()]);
237 r.extend(weekly[i]);
238 r.extend(monthly[i]);
239 r
240 })
241 .collect()
242}
243
244#[derive(Debug, Clone)]
252pub struct Gbm {
253 pub base: f64,
254 pub edges: Vec<Vec<f64>>,
255 pub trees: Vec<Vec<GbmNode>>,
256}
257
258#[derive(Debug, Clone)]
259pub enum GbmNode {
260 Split { feature: usize, bin: u8, left: usize, right: usize },
262 Leaf(f64),
263}
264
265pub struct GbmParams {
266 pub rounds: usize,
267 pub learning_rate: f64,
268 pub depth: usize,
269 pub min_leaf: f64,
271}
272
273impl Default for GbmParams {
274 fn default() -> Self {
275 Self { rounds: 150, learning_rate: 0.05, depth: 3, min_leaf: 0.01 }
276 }
277}
278
279const BINS: usize = 32;
280
281fn bin_of(edges: &[f64], v: f64) -> u8 {
282 edges.partition_point(|&e| e < v) as u8
283}
284
285impl Gbm {
286 pub fn fit(x: &[Vec<f64>], y: &[f64], p: &GbmParams) -> Self {
287 let n = x.len();
288 let d = x[0].len();
289 let step = (n / 20_000).max(1);
291 let edges: Vec<Vec<f64>> = (0..d)
292 .map(|j| {
293 let mut v: Vec<f64> = x.iter().step_by(step).map(|r| r[j]).collect();
294 v.sort_by(|a, b| a.partial_cmp(b).unwrap());
295 let mut e: Vec<f64> = (1..BINS).map(|k| v[k * (v.len() - 1) / BINS]).collect();
296 e.dedup();
297 e
298 })
299 .collect();
300 let binned: Vec<Vec<u8>> = x.iter().map(|r| r.iter().enumerate().map(|(j, &v)| bin_of(&edges[j], v)).collect()).collect();
301 let mean = (y.iter().sum::<f64>() / n as f64).clamp(1e-6, 1.0 - 1e-6);
302 let base = (mean / (1.0 - mean)).ln();
303 let mut f = vec![base; n];
304 let min_leaf = ((n as f64 * p.min_leaf) as usize).max(50);
305 let mut trees = Vec::with_capacity(p.rounds);
306 for _ in 0..p.rounds {
307 let prob: Vec<f64> = f.iter().map(|&v| 1.0 / (1.0 + (-v).exp())).collect();
308 let g: Vec<f64> = (0..n).map(|i| y[i] - prob[i]).collect();
309 let h: Vec<f64> = prob.iter().map(|&q| (q * (1.0 - q)).max(1e-6)).collect();
310 let mut nodes = Vec::new();
311 let idx: Vec<u32> = (0..n as u32).collect();
312 grow(&binned, &g, &h, idx, 0, p, min_leaf, d, &mut nodes);
313 for i in 0..n {
314 f[i] += leaf_value(&nodes, &binned[i]);
315 }
316 trees.push(nodes);
317 }
318 Self { base, edges, trees }
319 }
320
321 pub fn predict(&self, r: &[f64]) -> f64 {
322 let b: Vec<u8> = r.iter().enumerate().map(|(j, &v)| bin_of(&self.edges[j], v)).collect();
323 let z = self.base + self.trees.iter().map(|t| leaf_value(t, &b)).sum::<f64>();
324 1.0 / (1.0 + (-z).exp())
325 }
326}
327
328fn leaf_value(nodes: &[GbmNode], b: &[u8]) -> f64 {
329 let mut k = 0;
330 loop {
331 match nodes[k] {
332 GbmNode::Leaf(v) => return v,
333 GbmNode::Split { feature, bin, left, right } => k = if b[feature] <= bin { left } else { right },
334 }
335 }
336}
337
338#[allow(clippy::too_many_arguments)]
339fn grow(
340 x: &[Vec<u8>],
341 g: &[f64],
342 h: &[f64],
343 idx: Vec<u32>,
344 depth: usize,
345 p: &GbmParams,
346 min_leaf: usize,
347 d: usize,
348 nodes: &mut Vec<GbmNode>,
349) -> usize {
350 const L2: f64 = 1.0;
351 let me = nodes.len();
352 let (gs, hs) = idx.iter().fold((0.0, 0.0), |(a, b), &i| (a + g[i as usize], b + h[i as usize]));
353 nodes.push(GbmNode::Leaf(p.learning_rate * gs / (hs + L2)));
354 if depth >= p.depth || idx.len() < 2 * min_leaf {
355 return me;
356 }
357 let parent = gs * gs / (hs + L2);
358 let mut best: Option<(f64, usize, u8)> = None;
359 for j in 0..d {
360 let mut hist = [(0.0f64, 0.0f64, 0usize); BINS];
361 for &i in &idx {
362 let e = &mut hist[x[i as usize][j] as usize];
363 e.0 += g[i as usize];
364 e.1 += h[i as usize];
365 e.2 += 1;
366 }
367 let (mut gl, mut hl, mut nl) = (0.0, 0.0, 0usize);
368 for (b, e) in hist.iter().enumerate().take(BINS - 1) {
369 gl += e.0;
370 hl += e.1;
371 nl += e.2;
372 let nr = idx.len() - nl;
373 if nl < min_leaf || nr < min_leaf {
374 continue;
375 }
376 let (gr, hr) = (gs - gl, hs - hl);
377 let gain = gl * gl / (hl + L2) + gr * gr / (hr + L2) - parent;
378 if best.is_none_or(|(bg, _, _)| gain > bg) {
379 best = Some((gain, j, b as u8));
380 }
381 }
382 }
383 let Some((gain, feature, bin)) = best else { return me };
384 if gain <= 0.0 {
385 return me;
386 }
387 let (li, ri): (Vec<u32>, Vec<u32>) = idx.into_iter().partition(|&i| x[i as usize][feature] <= bin);
388 let left = grow(x, g, h, li, depth + 1, p, min_leaf, d, nodes);
389 let right = grow(x, g, h, ri, depth + 1, p, min_leaf, d, nodes);
390 nodes[me] = GbmNode::Split { feature, bin, left, right };
391 me
392}