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; [`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}