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
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}