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; the engine's evaluator 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
15fn default_macd_fast() -> usize {
16 12
17}
18
19fn default_macd_slow() -> usize {
20 26
21}
22
23fn default_macd_signal() -> usize {
24 9
25}
26
27fn default_bollinger_k() -> f64 {
28 2.0
29}
30
31#[derive(Debug, Deserialize)]
32#[serde(tag = "op")]
33pub enum Expr {
34 /// A raw input series by name (e.g. close, pe, revenue_growth).
35 Data { name: String },
36 /// A constant scalar value, broadcast across the panel.
37 Const { value: f64 },
38 /// Simple moving average of `of` over `n` days.
39 Average { of: Box<Expr>, n: usize },
40 /// Exponential moving average of `of` over `n` days.
41 Ema { of: Box<Expr>, n: usize },
42 /// Rolling standard deviation of `of` over `n` days.
43 Std { of: Box<Expr>, n: usize },
44 /// Relative Strength Index of `of` over `n` days.
45 Rsi { of: Box<Expr>, n: usize },
46 /// Percentage change of `of` over `n` days.
47 PctChange { of: Box<Expr>, n: usize },
48 /// 1 where `of` rose for `n` consecutive days, else 0.
49 Rise { of: Box<Expr>, n: usize },
50 /// `of` lagged forward by `n` days.
51 Shift { of: Box<Expr>, n: usize },
52 /// Rolling maximum of `of` over `n` days.
53 RollingMax { of: Box<Expr>, n: usize },
54 /// Rolling minimum of `of` over `n` days.
55 RollingMin { of: Box<Expr>, n: usize },
56 /// Bollinger mid band: the `n`-day simple moving average of `of`.
57 BollingerMid { of: Box<Expr>, n: usize },
58 /// Bollinger upper band: `sma(of, n) + k * std(of, n)` (`k` defaults to 2).
59 BollingerUpper {
60 of: Box<Expr>,
61 n: usize,
62 #[serde(default = "default_bollinger_k")]
63 k: f64,
64 },
65 /// Bollinger lower band: `sma(of, n) - k * std(of, n)` (`k` defaults to 2).
66 BollingerLower {
67 of: Box<Expr>,
68 n: usize,
69 #[serde(default = "default_bollinger_k")]
70 k: f64,
71 },
72 /// MACD line: `ema(of, fast) - ema(of, slow)` (defaults 12/26).
73 Macd {
74 of: Box<Expr>,
75 #[serde(default = "default_macd_fast")]
76 fast: usize,
77 #[serde(default = "default_macd_slow")]
78 slow: usize,
79 },
80 /// MACD signal line: `signal`-day EMA of the MACD line (defaults 12/26/9).
81 MacdSignal {
82 of: Box<Expr>,
83 #[serde(default = "default_macd_fast")]
84 fast: usize,
85 #[serde(default = "default_macd_slow")]
86 slow: usize,
87 #[serde(default = "default_macd_signal")]
88 signal: usize,
89 },
90 /// MACD histogram: MACD line minus its signal line (defaults 12/26/9).
91 MacdHist {
92 of: Box<Expr>,
93 #[serde(default = "default_macd_fast")]
94 fast: usize,
95 #[serde(default = "default_macd_slow")]
96 slow: usize,
97 #[serde(default = "default_macd_signal")]
98 signal: usize,
99 },
100 /// Donchian channel upper band: rolling `n`-day high of `of`.
101 DonchianHigh { of: Box<Expr>, n: usize },
102 /// Donchian channel lower band: rolling `n`-day low of `of`.
103 DonchianLow { of: Box<Expr>, n: usize },
104 /// Donchian channel mid-line: `(rolling_max + rolling_min) / 2` over `n` days.
105 DonchianMid { of: Box<Expr>, n: usize },
106 /// Average True Range over `n` days from high/low/close.
107 Atr {
108 high: Box<Expr>,
109 low: Box<Expr>,
110 close: Box<Expr>,
111 n: usize,
112 },
113 /// Normalized ATR (percent) over `n` days.
114 Natr {
115 high: Box<Expr>,
116 low: Box<Expr>,
117 close: Box<Expr>,
118 n: usize,
119 },
120 /// Williams %R over `n` days.
121 WillR {
122 high: Box<Expr>,
123 low: Box<Expr>,
124 close: Box<Expr>,
125 n: usize,
126 },
127 /// Commodity Channel Index over `n` days.
128 Cci {
129 high: Box<Expr>,
130 low: Box<Expr>,
131 close: Box<Expr>,
132 n: usize,
133 },
134 /// Stochastic %K over `n` days.
135 StochK {
136 high: Box<Expr>,
137 low: Box<Expr>,
138 close: Box<Expr>,
139 n: usize,
140 },
141 /// Stochastic %D: `d`-day average of %K over `n` days.
142 StochD {
143 high: Box<Expr>,
144 low: Box<Expr>,
145 close: Box<Expr>,
146 n: usize,
147 #[serde(default = "default_stoch_d")]
148 d: usize,
149 },
150 /// Aroon Up over `n` days from high.
151 AroonUp { high: Box<Expr>, n: usize },
152 /// Aroon Down over `n` days from low.
153 AroonDown { low: Box<Expr>, n: usize },
154 /// Average Directional Index over `n` days.
155 Adx {
156 high: Box<Expr>,
157 low: Box<Expr>,
158 close: Box<Expr>,
159 n: usize,
160 },
161 /// Plus Directional Indicator (+DI) over `n` days.
162 PlusDi {
163 high: Box<Expr>,
164 low: Box<Expr>,
165 close: Box<Expr>,
166 n: usize,
167 },
168 /// Minus Directional Indicator (−DI) over `n` days.
169 MinusDi {
170 high: Box<Expr>,
171 low: Box<Expr>,
172 close: Box<Expr>,
173 n: usize,
174 },
175 /// On-Balance Volume from close and volume.
176 Obv { close: Box<Expr>, volume: Box<Expr> },
177 /// Money Flow Index over `n` days.
178 Mfi {
179 high: Box<Expr>,
180 low: Box<Expr>,
181 close: Box<Expr>,
182 volume: Box<Expr>,
183 n: usize,
184 },
185 /// Volume-Weighted Average Price over `n` days from high/low/close/volume.
186 Vwap {
187 high: Box<Expr>,
188 low: Box<Expr>,
189 close: Box<Expr>,
190 volume: Box<Expr>,
191 n: usize,
192 },
193 /// 1 where `of` fell for `n` consecutive days, else 0.
194 Fall { of: Box<Expr>, n: usize },
195 /// 1 for the `n` highest values per row (cross-section), else 0.
196 IsLargest { of: Box<Expr>, n: usize },
197 /// 1 for the `n` lowest values per row (cross-section), else 0.
198 IsSmallest { of: Box<Expr>, n: usize },
199 /// 1 where `of` held true at least `nsatisfy` times within the last `nwindow` rows.
200 Sustain {
201 of: Box<Expr>,
202 nwindow: usize,
203 nsatisfy: Option<usize>,
204 },
205 /// 1 on the row where `of` turns false→true (rising edge).
206 IsEntry { of: Box<Expr> },
207 /// 1 on the row where `of` turns true→false (falling edge).
208 IsExit { of: Box<Expr> },
209 /// Hold true from an entry edge until an exit edge (or the `exit` panel is true).
210 ExitWhen { entry: Box<Expr>, exit: Box<Expr> },
211 /// Per-row quantile of `of` across symbols; collapses to one column (`quantile`).
212 QuantileRow { of: Box<Expr>, c: f64 },
213 /// Per-row winsorize: clip to empirical quantiles `lower`/`upper` in `[0, 1]`.
214 Winsorize {
215 of: Box<Expr>,
216 lower: f64,
217 upper: f64,
218 },
219 /// Per-row z-score `(x − mean) / std` (population std); constant rows → 0.
220 Zscore { of: Box<Expr> },
221 /// Per-row quantile buckets labeled 1..=`n` (NaN preserved).
222 Bucket { of: Box<Expr>, n: usize },
223 /// Per-row demean: subtract the cross-sectional mean of non-NaN cells.
224 Demean { of: Box<Expr> },
225 /// 1 where `l` is greater than `r`, else 0.
226 Gt { l: Box<Expr>, r: Box<Expr> },
227 /// 1 where `l` is less than `r`, else 0.
228 Lt { l: Box<Expr>, r: Box<Expr> },
229 /// 1 where `l` is greater than or equal to `r`, else 0.
230 Ge { l: Box<Expr>, r: Box<Expr> },
231 /// 1 where `l` is less than or equal to `r`, else 0.
232 Le { l: Box<Expr>, r: Box<Expr> },
233 /// Logical AND of two boolean panels.
234 And { l: Box<Expr>, r: Box<Expr> },
235 /// Logical OR of two boolean panels.
236 Or { l: Box<Expr>, r: Box<Expr> },
237 /// Logical NOT of a boolean panel (NaN is falsy, so `not` of NaN is 1).
238 Not { of: Box<Expr> },
239 /// Element-wise `l` + `r`.
240 Add { l: Box<Expr>, r: Box<Expr> },
241 /// Element-wise `l` − `r`.
242 Sub { l: Box<Expr>, r: Box<Expr> },
243 /// Element-wise `l` × `r`.
244 Mul { l: Box<Expr>, r: Box<Expr> },
245 /// Element-wise `l` ÷ `r`.
246 Div { l: Box<Expr>, r: Box<Expr> },
247 /// Negation (−`of`).
248 Neg { of: Box<Expr> },
249 /// Ceiling of `of`.
250 Ceil { of: Box<Expr> },
251 /// Cross-sectional rank of `of` per row; `pct` for 0..1 percentile, `ascending` sets direction.
252 Rank {
253 of: Box<Expr>,
254 #[serde(default = "default_true")]
255 pct: bool,
256 #[serde(default = "default_true")]
257 ascending: bool,
258 },
259 /// `of` kept only where `by` is true; elsewhere dropped.
260 Mask { of: Box<Expr>, by: Box<Expr> },
261 /// Scale each row so gross weight (Σ|w|) is 1; NaN preserved, zero rows unchanged.
262 NormalizeRow { of: Box<Expr> },
263 /// 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.
264 HoldUntil {
265 entry: Box<Expr>,
266 exit: Box<Expr>,
267 nstocks_limit: Option<usize>,
268 rank: Option<Box<Expr>>,
269 #[serde(default)]
270 stop_loss: Option<f64>,
271 #[serde(default)]
272 take_profit: Option<f64>,
273 #[serde(default)]
274 trail_stop: Option<f64>,
275 #[serde(default)]
276 trail_stop_activation: Option<f64>,
277 },
278 /// Hold `of`, refreshing on calendar `freq` (W/ME/QE) or on dates where `on` is true.
279 Rebalance {
280 of: Box<Expr>,
281 #[serde(default)]
282 freq: Option<String>,
283 #[serde(default)]
284 on: Option<Box<Expr>>,
285 },
286 /// Cross-sectionally regress `of` against the `by` factors, optionally adding a constant.
287 Neutralize {
288 of: Box<Expr>,
289 by: Vec<Expr>,
290 #[serde(default = "default_true")]
291 add_const: bool,
292 },
293 /// Neutralize `of` within each industry/sector.
294 NeutralizeIndustry {
295 of: Box<Expr>,
296 #[serde(default = "default_true")]
297 add_const: bool,
298 },
299 /// Rank `of` within each industry, optionally limited to `categories`.
300 IndustryRank {
301 of: Box<Expr>,
302 #[serde(default)]
303 categories: Option<Vec<String>>,
304 },
305 /// Aggregate `of` within each industry using `agg` (e.g. mean).
306 GroupbyCategory { of: Box<Expr>, agg: String },
307 /// Boolean mask: `1` where the symbol's industry equals `name` (exact, case-sensitive).
308 /// Shape follows `of`; symbols missing from the industry map are `0`.
309 InSector { of: Box<Expr>, name: String },
310}