lemon/spec.rs
1//! [`Expr`]: the serializable strategy AST. A JSON tree (tagged by `"op"`) that the
2//! website and the batch runner both emit; [`crate::eval`] walks it against a data
3//! context to produce a position matrix.
4
5use serde::Deserialize;
6
7fn default_true() -> bool {
8 true
9}
10
11fn default_stoch_d() -> usize {
12 3
13}
14
15#[derive(Debug, Deserialize)]
16#[serde(tag = "op")]
17pub enum Expr {
18 /// A raw input series by name (e.g. close, pe, revenue_growth).
19 Data { name: String },
20 /// A constant scalar value, broadcast across the panel.
21 Const { value: f64 },
22 /// Simple moving average of `of` over `n` days.
23 Average { of: Box<Expr>, n: usize },
24 /// Exponential moving average of `of` over `n` days.
25 Ema { of: Box<Expr>, n: usize },
26 /// Rolling standard deviation of `of` over `n` days.
27 Std { of: Box<Expr>, n: usize },
28 /// Relative Strength Index of `of` over `n` days.
29 Rsi { of: Box<Expr>, n: usize },
30 /// Percentage change of `of` over `n` days.
31 PctChange { of: Box<Expr>, n: usize },
32 /// 1 where `of` rose for `n` consecutive days, else 0.
33 Rise { of: Box<Expr>, n: usize },
34 /// `of` lagged forward by `n` days.
35 Shift { of: Box<Expr>, n: usize },
36 /// Rolling maximum of `of` over `n` days.
37 RollingMax { of: Box<Expr>, n: usize },
38 /// Average True Range over `n` days from high/low/close.
39 Atr {
40 high: Box<Expr>,
41 low: Box<Expr>,
42 close: Box<Expr>,
43 n: usize,
44 },
45 /// Normalized ATR (percent) over `n` days.
46 Natr {
47 high: Box<Expr>,
48 low: Box<Expr>,
49 close: Box<Expr>,
50 n: usize,
51 },
52 /// Williams %R over `n` days.
53 WillR {
54 high: Box<Expr>,
55 low: Box<Expr>,
56 close: Box<Expr>,
57 n: usize,
58 },
59 /// Commodity Channel Index over `n` days.
60 Cci {
61 high: Box<Expr>,
62 low: Box<Expr>,
63 close: Box<Expr>,
64 n: usize,
65 },
66 /// Stochastic %K over `n` days.
67 StochK {
68 high: Box<Expr>,
69 low: Box<Expr>,
70 close: Box<Expr>,
71 n: usize,
72 },
73 /// Stochastic %D: `d`-day average of %K over `n` days.
74 StochD {
75 high: Box<Expr>,
76 low: Box<Expr>,
77 close: Box<Expr>,
78 n: usize,
79 #[serde(default = "default_stoch_d")]
80 d: usize,
81 },
82 /// Aroon Up over `n` days from high.
83 AroonUp { high: Box<Expr>, n: usize },
84 /// Aroon Down over `n` days from low.
85 AroonDown { low: Box<Expr>, n: usize },
86 /// Average Directional Index over `n` days.
87 Adx {
88 high: Box<Expr>,
89 low: Box<Expr>,
90 close: Box<Expr>,
91 n: usize,
92 },
93 /// Plus Directional Indicator (+DI) over `n` days.
94 PlusDi {
95 high: Box<Expr>,
96 low: Box<Expr>,
97 close: Box<Expr>,
98 n: usize,
99 },
100 /// Minus Directional Indicator (−DI) over `n` days.
101 MinusDi {
102 high: Box<Expr>,
103 low: Box<Expr>,
104 close: Box<Expr>,
105 n: usize,
106 },
107 /// On-Balance Volume from close and volume.
108 Obv { close: Box<Expr>, volume: Box<Expr> },
109 /// Money Flow Index over `n` days.
110 Mfi {
111 high: Box<Expr>,
112 low: Box<Expr>,
113 close: Box<Expr>,
114 volume: Box<Expr>,
115 n: usize,
116 },
117 /// Volume-Weighted Average Price over `n` days from high/low/close/volume.
118 Vwap {
119 high: Box<Expr>,
120 low: Box<Expr>,
121 close: Box<Expr>,
122 volume: Box<Expr>,
123 n: usize,
124 },
125 /// 1 where `of` fell for `n` consecutive days, else 0.
126 Fall { of: Box<Expr>, n: usize },
127 /// 1 for the `n` highest values per row (cross-section), else 0.
128 IsLargest { of: Box<Expr>, n: usize },
129 /// 1 for the `n` lowest values per row (cross-section), else 0.
130 IsSmallest { of: Box<Expr>, n: usize },
131 /// 1 where `of` held true at least `nsatisfy` times within the last `nwindow` rows.
132 Sustain {
133 of: Box<Expr>,
134 nwindow: usize,
135 nsatisfy: Option<usize>,
136 },
137 /// 1 on the row where `of` turns false→true (rising edge).
138 IsEntry { of: Box<Expr> },
139 /// 1 on the row where `of` turns true→false (falling edge).
140 IsExit { of: Box<Expr> },
141 /// 1 where `l` is greater than `r`, else 0.
142 Gt { l: Box<Expr>, r: Box<Expr> },
143 /// 1 where `l` is less than `r`, else 0.
144 Lt { l: Box<Expr>, r: Box<Expr> },
145 /// 1 where `l` is greater than or equal to `r`, else 0.
146 Ge { l: Box<Expr>, r: Box<Expr> },
147 /// 1 where `l` is less than or equal to `r`, else 0.
148 Le { l: Box<Expr>, r: Box<Expr> },
149 /// Logical AND of two boolean panels.
150 And { l: Box<Expr>, r: Box<Expr> },
151 /// Logical OR of two boolean panels.
152 Or { l: Box<Expr>, r: Box<Expr> },
153 /// Element-wise `l` + `r`.
154 Add { l: Box<Expr>, r: Box<Expr> },
155 /// Element-wise `l` − `r`.
156 Sub { l: Box<Expr>, r: Box<Expr> },
157 /// Element-wise `l` × `r`.
158 Mul { l: Box<Expr>, r: Box<Expr> },
159 /// Element-wise `l` ÷ `r`.
160 Div { l: Box<Expr>, r: Box<Expr> },
161 /// Negation (−`of`).
162 Neg { of: Box<Expr> },
163 /// Ceiling of `of`.
164 Ceil { of: Box<Expr> },
165 /// Cross-sectional rank of `of` per row; `pct` for 0..1 percentile, `ascending` sets direction.
166 Rank {
167 of: Box<Expr>,
168 #[serde(default = "default_true")]
169 pct: bool,
170 #[serde(default = "default_true")]
171 ascending: bool,
172 },
173 /// `of` kept only where `by` is true; elsewhere dropped.
174 Mask { of: Box<Expr>, by: Box<Expr> },
175 /// Stateful rotation: enter on `entry`, exit on `exit`, hold up to `nstocks_limit` (prioritised by `rank`), with optional stop_loss/take_profit/trail_stop/trail_stop_activation.
176 HoldUntil {
177 entry: Box<Expr>,
178 exit: Box<Expr>,
179 nstocks_limit: Option<usize>,
180 rank: Option<Box<Expr>>,
181 #[serde(default)]
182 stop_loss: Option<f64>,
183 #[serde(default)]
184 take_profit: Option<f64>,
185 #[serde(default)]
186 trail_stop: Option<f64>,
187 #[serde(default)]
188 trail_stop_activation: Option<f64>,
189 },
190 /// Hold `of`, refreshing on calendar `freq` (W/ME/QE) or on dates where `on` is true.
191 Rebalance {
192 of: Box<Expr>,
193 #[serde(default)]
194 freq: Option<String>,
195 #[serde(default)]
196 on: Option<Box<Expr>>,
197 },
198 /// Cross-sectionally regress `of` against the `by` factors, optionally adding a constant.
199 Neutralize {
200 of: Box<Expr>,
201 by: Vec<Expr>,
202 #[serde(default = "default_true")]
203 add_const: bool,
204 },
205 /// Neutralize `of` within each industry/sector.
206 NeutralizeIndustry {
207 of: Box<Expr>,
208 #[serde(default = "default_true")]
209 add_const: bool,
210 },
211 /// Rank `of` within each industry, optionally limited to `categories`.
212 IndustryRank {
213 of: Box<Expr>,
214 #[serde(default)]
215 categories: Option<Vec<String>>,
216 },
217 /// Aggregate `of` within each industry using `agg` (e.g. mean).
218 GroupbyCategory { of: Box<Expr>, agg: String },
219}