Skip to main content

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}