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
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 /// Logical NOT of a boolean panel (NaN is falsy, so `not` of NaN is 1).
154 Not { of: Box<Expr> },
155 /// Element-wise `l` + `r`.
156 Add { l: Box<Expr>, r: Box<Expr> },
157 /// Element-wise `l` − `r`.
158 Sub { l: Box<Expr>, r: Box<Expr> },
159 /// Element-wise `l` × `r`.
160 Mul { l: Box<Expr>, r: Box<Expr> },
161 /// Element-wise `l` ÷ `r`.
162 Div { l: Box<Expr>, r: Box<Expr> },
163 /// Negation (−`of`).
164 Neg { of: Box<Expr> },
165 /// Ceiling of `of`.
166 Ceil { of: Box<Expr> },
167 /// Cross-sectional rank of `of` per row; `pct` for 0..1 percentile, `ascending` sets direction.
168 Rank {
169 of: Box<Expr>,
170 #[serde(default = "default_true")]
171 pct: bool,
172 #[serde(default = "default_true")]
173 ascending: bool,
174 },
175 /// `of` kept only where `by` is true; elsewhere dropped.
176 Mask { of: Box<Expr>, by: Box<Expr> },
177 /// Scale each row so gross weight (Σ|w|) is 1; NaN preserved, zero rows unchanged.
178 NormalizeRow { of: Box<Expr> },
179 /// 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.
180 HoldUntil {
181 entry: Box<Expr>,
182 exit: Box<Expr>,
183 nstocks_limit: Option<usize>,
184 rank: Option<Box<Expr>>,
185 #[serde(default)]
186 stop_loss: Option<f64>,
187 #[serde(default)]
188 take_profit: Option<f64>,
189 #[serde(default)]
190 trail_stop: Option<f64>,
191 #[serde(default)]
192 trail_stop_activation: Option<f64>,
193 },
194 /// Hold `of`, refreshing on calendar `freq` (W/ME/QE) or on dates where `on` is true.
195 Rebalance {
196 of: Box<Expr>,
197 #[serde(default)]
198 freq: Option<String>,
199 #[serde(default)]
200 on: Option<Box<Expr>>,
201 },
202 /// Cross-sectionally regress `of` against the `by` factors, optionally adding a constant.
203 Neutralize {
204 of: Box<Expr>,
205 by: Vec<Expr>,
206 #[serde(default = "default_true")]
207 add_const: bool,
208 },
209 /// Neutralize `of` within each industry/sector.
210 NeutralizeIndustry {
211 of: Box<Expr>,
212 #[serde(default = "default_true")]
213 add_const: bool,
214 },
215 /// Rank `of` within each industry, optionally limited to `categories`.
216 IndustryRank {
217 of: Box<Expr>,
218 #[serde(default)]
219 categories: Option<Vec<String>>,
220 },
221 /// Aggregate `of` within each industry using `agg` (e.g. mean).
222 GroupbyCategory { of: Box<Expr>, agg: String },
223}