lemon language
lemon is the small text language you write trading strategies in. A strategy such as
close > sma(close, 2)is lowered (compiled) into a JSON Expr tree — the serializable strategy
AST — which the yuzu backtest engine walks against price/fundamental data to
produce a position matrix. The same JSON runs identically in the browser/Worker
(WASM) and in the native batch runner, so what you write here is exactly what
gets backtested.
lemon does no math itself. It is a surface syntax over the Expr AST: the
parser turns text into JSON, and the engine (documented in
backtest-engine.md) supplies all the semantics.
The crate is lemon-lang, imported as lemon. Its public API is tiny:
let tree: serde_json::Value = lemon::parse("close > sma(close, 2)").unwrap();let text: String = lemon::format(&tree); // JSON Expr → canonical DSL textlemon::parse(src: &str) -> Result<serde_json::Value, lemon::ParseError>— DSL text to the JSONExprtree.lemon::format(&serde_json::Value) -> String— a tree back to canonical, re-indented DSL text (a “gofmt for lemon”). Note:formatcannot reconstructletbindings — seeletis parse-time inlining.
A ParseError carries a 1-based line and col and a message, and prints
as line:col: message.
1. Lexical elements
Section titled “1. Lexical elements”Numbers
Section titled “Numbers”- Plain integers and decimals:
2,0.5,500000000. - Underscore digit separators anywhere in the digits:
1_000_000is1000000. Underscores are simply stripped. - Scientific notation with
e/Eand an optional sign:5e8,5E8,1.5e-3,2e+6. (The exponent must be followed by a digit, or a sign then a digit, or thee/Eis not treated as part of the number.)
Integral values (no fractional part) are emitted as JSON integers so they
deserialize into the engine’s usize window fields; non-integral values stay
floats.
Strings
Section titled “Strings”Double-quoted: "ME", "tech". Strings are used only for a few enum-like
fields (freq, agg, industry_rank categories).
There are no escape sequences. A backslash is a literal backslash, and there
is no way to embed a " inside a string — the first closing quote ends it. An
unterminated string is a parse error.
Comments
Section titled “Comments”# starts a line comment that runs to end-of-line:
close > sma(close, 20) # golden-cross-ish entryIdentifiers
Section titled “Identifiers”ASCII only: [A-Za-z_][A-Za-z0-9_]* — a letter or underscore, then letters,
digits, or underscores. Examples: close, market_cap, revenue_growth,
_tmp. There is no Unicode in identifiers.
Booleans
Section titled “Booleans”true and false are lexed as ordinary identifiers and only recognized as
boolean literals by the parser. They are valid only where a boolean is expected
(the true/false keyword arguments like ascending, pct, add_const); a
boolean cannot stand alone as an expression.
Keywords and punctuation
Section titled “Keywords and punctuation”The only reserved word is let. The logical operators and / or / not
are also words but are lexed as identifiers and recognized positionally. Punctuation:
( ) [ ] , = and the operator characters below.
Any other character ($, @, !, &, |, %, ;, {, }, . outside a
number, etc.) is a lexer error: unexpected character.
2. Operators
Section titled “2. Operators”All operators are infix and left-associative, except unary minus which is
prefix. Comparisons and logical ops produce 1/0 panels (see the engine’s
boolean convention). Precedence, lowest to highest:
| Precedence | Operators | Meaning | Assoc. |
|---|---|---|---|
| 1 (lowest) | or |
logical OR | left |
| 2 | and |
logical AND | left |
| 2.5 | not |
logical NOT (prefix) | — |
| 3 | > < >= <= |
comparisons → 1/0 |
left |
| 4 | + - |
add / subtract | left |
| 5 | * / |
multiply / divide | left |
| 6 (highest) | unary - |
negation (prefix) | — |
So a and b or c parses as (a and b) or c, and 2 * x + y parses as
(2 * x) + y. not binds looser than comparisons and tighter than and:
not a > b is not (a > b), and not a and b is (not a) and b. NaN is
falsy, so not of NaN is 1 (matching and/or).
Operators that do NOT exist
Section titled “Operators that do NOT exist”There is no ==, no !=, no &, no |, and no !. Logical AND / OR /
NOT are the words and / or / not. Equality is deliberately absent —
you compare with > / < / >= / <=. Typing ==, &, |, or ! is a
parse or lex error, so a strategy that “looks right” from another language will
fail loudly rather than silently misbehave.
3. Grammar and expression forms
Section titled “3. Grammar and expression forms”Program shape
Section titled “Program shape”A program is:
- Zero or more
let NAME = EXPRbindings, followed by - exactly one top-level expression.
That final expression is your strategy. It must be an op node — a function
call or an operator expression — not a bare number or string. 42 on its own is
rejected (a strategy must be an expression, not a bare constant); wrap it in
something that produces a signal, e.g. close > 42.
A rough grammar sketch:
program := let_binding* expr EOFlet_binding := "let" IDENT "=" exprexpr := or_expror_expr := and_expr ("or" and_expr)*and_expr := cmp_expr ("and" cmp_expr)*cmp_expr := add_expr ( ("<"|">"|"<="|">=") add_expr )*add_expr := mul_expr ( ("+"|"-") mul_expr )*mul_expr := unary ( ("*"|"/") unary )*unary := "-" unary | primaryprimary := NUMBER | STRING | "true" | "false" | "(" expr ")" | IDENT "(" args ")" # function call | IDENT # let-bound name OR a Data seriesargs := (arg ("," arg)*)?arg := IDENT "=" arg_value # keyword arg | arg_value # positional argarg_value := "[" (expr ("," expr)*)? "]" # list literal (call args only) | exprlet is parse-time inlining
Section titled “let is parse-time inlining”let does not create a runtime variable. At parse time the parser
substitutes the bound subtree at every use site. Given
let ma = sma(close, 20)hold_until(entry = close > ma, exit = close < ma, nstocks_limit = 5)the resulting tree contains two independent copies of sma(close, 20) — one
in entry, one in exit. let is purely for readability and de-duplication in
the source; it changes nothing about what the engine sees.
Consequences:
- Re-binding a name is an error.
let a = closethenlet a = pefails with`a` is already defined. formatcannot reconstructlets. Because bindings are inlined before the tree exists,lemon::formatre-emits the fully expanded form. Round-tripping source through the formatter drops yourlets and inlines them.
Function calls: positional first, then keyword
Section titled “Function calls: positional first, then keyword”Arguments are given positionally first, then by keyword (name = value),
exactly like Python:
sma(close, 20) # both positionalrank(close, ascending=false) # positional `of`, keyword `ascending`rebalance(x, freq="ME") # positional `of`, keyword `freq`A positional argument after a keyword argument is an error
(positional argument after keyword argument). Each op has a fixed field order
(see the reference below); positional args fill fields left to right, and any
remaining fields can be supplied by name. Unknown keyword names and too many
positional args are errors.
List literals — call arguments only
Section titled “List literals — call arguments only”[ ... ] list syntax exists only inside a call’s argument list, for the ops
that take a list field (neutralize(..., by=[pe, market_cap]),
industry_rank(..., categories=["tech", "fin"])). There is no general list
value and no list operator elsewhere in the language.
No subscript / indexing
Section titled “No subscript / indexing”There is no [] indexing and no . member access. x[0] and x.field are
not valid syntax.
Unknown identifiers become Data series — silently
Section titled “Unknown identifiers become Data series — silently”A bare identifier that is neither a let name nor immediately followed by (
becomes a Data series reference ({"op":"Data","name":"..."}). This is how
you reference inputs: close, pe, market_cap, revenue_growth.
There is no parse-time check that the series exists. A typo like clsoe
parses happily as Data("clsoe") and only fails later, at engine evaluation
time, when no such series is found. The set of valid series names is the
engine’s, not lemon’s — lemon will accept any identifier. Proof-read your
series names.
4. Built-in op reference
Section titled “4. Built-in op reference”Every function-style op below is a call: name(args...). Arguments are
listed in positional order; ? marks an optional argument with its default.
sma/average are two names for the same op — the first name is canonical (the
one the formatter emits).
Unless noted, of (and high/low/close/volume) arguments are
expressions — usually a Data series like close, but any sub-expression
works. n-style arguments are plain numbers, not expressions.
Moving averages, momentum & rolling stats
Section titled “Moving averages, momentum & rolling stats”| Call | Arguments | Meaning |
|---|---|---|
sma / average |
of, n |
Simple moving average of of over n days. |
ema |
of, n |
Exponential moving average over n days. |
std |
of, n |
Rolling standard deviation over n days. |
rsi |
of, n |
Relative Strength Index over n days. |
pct_change |
of, n |
Percentage change of of over n days. |
rise |
of, n |
1 where of is above its value n days ago, else 0. |
fall |
of, n |
1 where of is below its value n days ago, else 0. |
shift |
of, n |
of lagged forward by n days. |
rolling_max |
of, n |
Rolling maximum over n days. |
rolling_min |
of, n |
Rolling minimum over n days. |
Bands & channels (single-series composites)
Section titled “Bands & channels (single-series composites)”These take one series (of, usually close) and are composed from the moving
averages / rolling stats above. Bollinger bands warm up with their std term
(NaN until row n-1), the mid line with sma (min_periods = n/2); MACD lines
warm up once the slow EMA seeds; Donchian bands at row n-1.
| Call | Arguments | Meaning |
|---|---|---|
bollinger_mid |
of, n |
Mid band: sma(of, n). |
bollinger_upper |
of, n, k?=2 |
Upper band: sma(of, n) + k * std(of, n). |
bollinger_lower |
of, n, k?=2 |
Lower band: sma(of, n) - k * std(of, n). |
macd |
of, fast?=12, slow?=26 |
MACD line: ema(of, fast) - ema(of, slow). |
macd_signal |
of, fast?=12, slow?=26, signal?=9 |
signal-day EMA of the MACD line. |
macd_hist |
of, fast?=12, slow?=26, signal?=9 |
MACD line minus its signal line. |
donchian_high |
of, n |
Upper channel: rolling n-day high (rolling_max). |
donchian_low |
of, n |
Lower channel: rolling n-day low (rolling_min). |
donchian_mid |
of, n |
Channel mid: (rolling_max + rolling_min) / 2. |
OHLCV technical indicators
Section titled “OHLCV technical indicators”These take price/volume series explicitly (so you decide which series feed them).
| Call | Arguments | Meaning |
|---|---|---|
atr |
high, low, close, n |
Average True Range over n days. |
natr |
high, low, close, n |
Normalized ATR (percent) over n days. |
willr |
high, low, close, n |
Williams %R over n days. |
cci |
high, low, close, n |
Commodity Channel Index over n days. |
stoch_k |
high, low, close, n |
Stochastic %K over n days. |
stoch_d |
high, low, close, n, d?=3 |
Stochastic %D: d-day average of %K. |
aroon_up |
high, n |
Aroon Up over n days (from high). |
aroon_down |
low, n |
Aroon Down over n days (from low). |
adx |
high, low, close, n |
Average Directional Index over n days. |
plus_di |
high, low, close, n |
+DI over n days. |
minus_di |
high, low, close, n |
−DI over n days. |
obv |
close, volume |
On-Balance Volume. |
mfi |
high, low, close, volume, n |
Money Flow Index over n days. |
vwap |
high, low, close, volume, n |
Volume-Weighted Average Price over n days. |
Cross-section & selection (per-row, across symbols)
Section titled “Cross-section & selection (per-row, across symbols)”| Call | Arguments | Meaning |
|---|---|---|
is_largest |
of, n |
1 for the n highest values in each row, else 0. |
is_smallest |
of, n |
1 for the n lowest values in each row, else 0. |
rank |
of, pct?=true, ascending?=true |
Cross-sectional rank per row. pct=true → 0..1 percentile; ascending=true → smallest ranks lowest. |
quantile_row |
of, c |
Per-row quantile of of across symbols at level c (e.g. 0.5 = median). Result is one column named quantile. |
winsorize |
of, lower, upper |
Per-row clip to empirical quantiles lower/upper in 0..1 (linear interp). NaN preserved. |
zscore |
of |
Per-row (x − mean) / std (population std). Constant rows → 0; empty → NaN. |
bucket |
of, n |
Per-row quantile buckets labeled 1..=n (average ranks for ties). Empty/n=0 → NaN. |
demean |
of |
Per-row subtract cross-sectional mean of non-NaN cells. |
mask |
of, by |
Keep of only where by is true; drop (NaN) elsewhere. |
normalize_row |
of |
Scale each row so gross weight (Σ|w|) is 1 — explicit portfolio weights. NaN preserved; zero rows unchanged. |
vol_target |
of, prices, target?=0.1, n?=63 |
Scale each row of the weight panel of toward annualized portfolio-vol target over a trailing n-return window of prices; deleverage only (scale ≤ 1). Warmup / n<2 / target≤0 pass through. |
industry_rank |
of, categories? |
Rank of within each industry; optionally restrict to categories (list of strings). |
cap_industry |
of, max_weight?=0.3 |
Cap each industry’s gross weight (Σ|w|) in the weight panel of at max_weight, scaling that industry’s names down proportionally (sign-preserving); residual left as cash. |
groupby_category |
of, agg |
Aggregate of within each industry using agg (e.g. "mean"); agg is a required string. |
Streaks, edges & stateful rotation
Section titled “Streaks, edges & stateful rotation”| Call | Arguments | Meaning |
|---|---|---|
sustain |
of, nwindow, nsatisfy? |
1 where of was true at least nsatisfy times within the last nwindow rows. |
is_entry |
of |
1 on the row where of turns false→true (rising edge). |
is_exit |
of |
1 on the row where of turns true→false (falling edge). |
exit_when |
entry, exit |
Hold true from an entry edge of entry until an exit edge of entry or exit is true (simpler than hold_until; no rotation cap). |
hold_until |
entry, exit, nstocks_limit?, rank? |
Stateful rotation: enter on entry, exit on exit, hold up to nstocks_limit names prioritized by rank. Price stops (stop-loss / take-profit / trailing) are not here — they live in the backtest config (BacktestConfig::stops), applied by the NAV loop. rank is an expression. |
rebalance |
of, freq?, on? |
Hold of, refreshing on calendar freq ("W"/"ME"/"QE"/"YE") or on rows where the on expression is true. |
Neutralization
Section titled “Neutralization”| Call | Arguments | Meaning |
|---|---|---|
neutralize |
of, by (list), add_const?=true |
Cross-sectionally regress of against the by factors and take residuals; add_const=true adds an intercept. by is a list: by=[pe, market_cap]. |
neutralize_industry |
of, add_const?=true |
Neutralize of within each industry/sector. |
in_sector |
of, name (string) |
Boolean mask: 1 where the symbol’s industry exactly equals name (case-sensitive); shape follows of. Symbols missing from the industry map are 0. Use with mask. |
Scalar / element-wise unary
Section titled “Scalar / element-wise unary”| Call | Arguments | Meaning |
|---|---|---|
ceil |
of |
Ceiling of of. |
(Negation is the prefix operator -, not a call.)
Operator ops and leaves
Section titled “Operator ops and leaves”These are not written as calls but are still nodes in the tree:
| Surface form | Op tag | Meaning |
|---|---|---|
a > b a < b a >= b a <= b |
Gt Lt Ge Le |
Comparisons → 1/0. |
a and b / a or b |
And / Or |
Logical AND / OR. |
a + b a - b a * b a / b |
Add Sub Mul Div |
Element-wise arithmetic. |
-a |
Neg |
Negation (prefix). |
close, pe, … |
Data |
A raw input series by name (bare identifier). |
42, 0.5, 5e8 |
Const |
A constant scalar, broadcast across the panel. A bare number used as an operand is auto-promoted to a Const. |
That is the complete surface: 71 op tags total in the engine — the leaves
Data and Const, the 10 operator ops above, the prefix ops Neg and not,
and the 57 function-style calls in the tables.
5. Worked examples
Section titled “5. Worked examples”Each snippet parses; you can check any of them with lemon fmt (see
Validating).
5.1 A simple filter
Section titled “5.1 A simple filter”close > sma(close, 2)Buy signal where today’s close is above the 2-day simple moving average. The >
yields a 1/0 panel.
5.2 Combining conditions
Section titled “5.2 Combining conditions”close > sma(close, 50) and rsi(close, 14) < 30Uptrend (close above its 50-day average) and oversold (14-day RSI below
30). and binds looser than the comparisons, so this is
(close > sma(...)) and (rsi(...) < 30) — no parentheses needed.
5.3 Ranking and top-N selection
Section titled “5.3 Ranking and top-N selection”is_largest(rank(-pe), 30)rank(-pe) ranks stocks by negated P/E (so cheaper = higher), then
is_largest(..., 30) keeps the top 30 each day. Note rank defaults to
pct=true, ascending=true; here the - flips the ordering instead of passing
ascending=false.
5.4 let for readability
Section titled “5.4 let for readability”let ma = sma(close, 20)hold_until(entry = close > ma, exit = close < ma, nstocks_limit = 5)Enter when price crosses above its 20-day average, exit when it crosses below,
holding at most 5 names. ma is inlined into both entry and exit — the tree
contains two copies of sma(close, 20).
5.5 Mask by liquidity, then rank
Section titled “5.5 Mask by liquidity, then rank”mask(rank(revenue_growth), market_cap > 500000000)Rank stocks by revenue_growth, but only keep those with market cap above
$500M (mask drops everything where by is false). Equivalently the threshold
could be written 5e8.
5.6 Factor neutralization
Section titled “5.6 Factor neutralization”neutralize(rank(-pe), by=[pe, market_cap])Take the value signal rank(-pe) and regress out the pe and market_cap
factors cross-sectionally, keeping the residual — a “value signal, controlling
for size and raw cheapness.”
6. Sharp edges & gotchas
Section titled “6. Sharp edges & gotchas”- No equality operator. There is no
==or!=. Compare with> < >= <=. Logical AND/OR/NOT are the wordsand/or/not— not&/|/!, which are lexer errors. - Typos become silent
Dataleaves. Any unknown bare identifier is treated as a series reference with no parse-time validation.clsoe > 2parses fine and fails only at engine eval. The valid series set is the engine’s, not lemon’s. letis inlined, not a variable. Each use site gets its own copy of the subtree. Re-binding the same name is an error, andformatcannot recoverlets (it emits the expanded tree).- Strings have no escapes. You cannot put a
"inside a string; there are no\n,\t,\"sequences. Strings are only for enum-ish fields (freq,agg,categories). - Default values worth knowing:
rank(x)defaults topct=true(percentile,0..1) andascending=true(smallest value gets the lowest rank).stoch_d(...)’sddefaults to3.neutralize(...)andneutralize_industry(...)defaultadd_const=true(an intercept is added to the regression).
hold_untilis selection only.entry,exit, andrankare expressions;nstocks_limitis a number. Price stops are not part of the op — setstop_loss/take_profit/trail_stopon the backtest config (they apply to any strategy, not justhold_until).- A strategy must be an op node. A bare
42or"ME"at the top level is rejected — the top-level expression has to produce a signal. n-style window arguments are numbers, not expressions.sma(close, x)wherexis a series is a type error (`n` must be a number).
Validating a snippet
Section titled “Validating a snippet”The lemon-cli crate ships the lemon binary — a formatter that parses first,
so it doubles as a syntax checker. Pipe source on stdin:
printf '%s' 'close > sma(close, 2)' | cargo run -q -p lemon-cli -- fmtIt prints the canonical (re-indented) form on success, or line:col: message on
a parse error with a non-zero exit code. lemon fmt -w file.lemon formats files
in place.
Linting
Section titled “Linting”lemon lint reports the two mistakes the parser cannot catch — unknown
series names (a typo like clsoe parses fine as a Data leaf and only fails
at engine eval) and unused let bindings (inlined at parse time, so they
vanish silently):
printf '%s' 'clsoe > 1' | cargo run -q -p lemon-cli -- lint --series close,pe# <stdin>:1:1: warning: unknown series `clsoe` — did you mean `close`?Pass the valid series set with --series a,b,c or --series-file path (one
name per line, # comments); without it only the unused-let check runs.
Exit code is non-zero when there are warnings. The same checks are exposed to
the web editor via lemon-wasm’s lint(src, series_json) export and to Rust
callers as lemon::lint(src, known_series).
Scaffolding a new strategy
Section titled “Scaffolding a new strategy”lemon new writes a starter .lemon — commented front-matter showing every
directive, plus a runnable momentum example — so you don’t assemble the
boilerplate by hand:
lemon new strategy.lemon # write the scaffold (refuses to overwrite)lemon new # or print to stdout: lemon new > strategy.lemonThe scaffold checks clean out of the box (lemon check strategy.lemon); edit
the universe / symbols and the expression, then run it.
Running a strategy
Section titled “Running a strategy”lemon run lowers a .lemon file and backtests it over a local
data-layout tree in one step — and naming a strategy file
as the first argument is short for it:
lemon strategy.lemon # short for `lemon run strategy.lemon`lemon run strategy.lemon --data ~/qdata --from 20180101 --fee-ratio 0.001By default it prints a human-readable summary — headline metrics, the day count, and final equity:
Strategy: strategy.lemonDays simulated: 1749 (20180101 → 20241231)Final equity (base 1.0): 2.4137
Headline metrics total return : 141.37% CAGR : 13.42% Sharpe : 0.98 max drawdown : -24.10% win rate : 54.30% # trades : 312Pass --json to print the full Report JSON (equity curve, every trade, the
complete metrics block) to stdout instead, and
--out report.json to write that JSON to a file (the machine artifact — with
--out the summary still prints to stdout unless you also pass --json). Other
flags mirror yuzu-cli run: --slippage-ratio, --price-key open|high|low|close,
--benchmark SPY. With no file it reads stdin, like the other subcommands.
#! front-matter — a self-contained strategy file
Section titled “#! front-matter — a self-contained strategy file”Because # already starts a comment, lines beginning #! at the top of a
file are invisible to the parser and read by the runner instead. They make
a .lemon file carry its own run parameters, so sharing the file reproduces
the run:
#! name: Momentum v2#! universe: 20180101..20241231#! config: { "fee_ratio": 0.001, "stop_loss": 0.08 }#! price-key: close
close > sma(close, 20) and rsi(close, 14) < 70universeis aFROM..TOdate window (either side may be omitted:20180101..).symbolsis a comma-separated explicit universe (#! symbols: AAPL, MSFT, NVDA/--symbols AAPL,MSFT): only these names are loaded, so cross-sectional ops (rank,is_largest,zscore, …) see exactly this universe. Every listed symbol must exist in the data tree — a missing one is an error, never a silent drop. Survivorship caveat: a list frozen today implies survivorship bias in a historical run.indexis a named point-in-time universe (#! index: sp500/ envelopeuniverse.symbols_hint), resolved against the tree’sin_sp500membership panel. The run is scoped to the window’s ever-members, and the strategy is wrapped so you hold only each day’s members and flatten a name the day it leaves the index. Cross-sectional ops rank across the ever-members; for a per-day-exact cross-section, mask the ranking input (is_largest(rank(mask(-pe, in_sp500)), 30)).indexandsymbolsare mutually exclusive; a missing membership panel is an actionable error. See data-layout.md §8 for the hold-vs-mask subtlety.configis the engine config object, with the same flat knob names as theyuzu-serverrequest (fee_ratio,slippage_ratio,initial_capital,delist_after,stop_loss,take_profit,trail_stop,stop_fill,benchmark,bootstrap_samples, …) — the full knob set, not just the flag subset. Unknown knobs are rejected: a typo likefee_rationfails loudly instead of silently running fee-free.- Precedence is flag > front-matter > environment > default. The data
tree is machine-specific, so it never goes in the file: pass
--dataor set$CITRUS_DATA. - Malformed or misplaced directives (unknown keys, duplicates, a
#!line after the source has started) are line-labelled errors —lemon check strategy.lemonvalidates front-matter and syntax without running anything.
lemon run strategy.json runs a strategy envelope
the same way, taking config and the universe window from the document.
The runner loads what the strategy references: the Data series named in the
tree (pe, high, volume, …) are resolved against the data layout —
combined panels/ files first, then per-symbol prices or fundamentals/
columns — and the tracked/ industry map feeds the industry ops. A name not
in the tree still fails at engine evaluation with unknown series (the
classic typo case lemon lint catches earlier).
Declaring data needs — #! require and --sync
Section titled “Declaring data needs — #! require and --sync”Two more front-matter keys make a strategy file state its data needs:
#! symbols: AAPL, MSFT, NVDA#! require: close, pe#! data-source: fmprequirenames the series the strategy needs. It doubles as the known-series list forlemon lint(typos are caught without passing--series), and tells--syncwhether fundamentals must be fetched.data-sourcedeclares which vendor may fill gaps (fmpfor now). It is a capability hint, never an instruction: a strategy file can never trigger network activity by itself.
On a machine whose data tree is missing some of the declared symbols,
lemon run stops with an error listing exactly what’s missing. Passing
--sync — an explicit opt-in, with your own key in $FMP_API_KEY — fetches
only the missing names through the declared vendor and then runs:
export FMP_API_KEY=...lemon strategy.lemon --syncWhen the tree is already complete, --sync is a no-op (zero requests).
Existing files are never rewritten. Offline builds
(--no-default-features) refuse --sync with a build hint.
See also
Section titled “See also”backtest-engine.md— engine semantics: thePaneldata model, NaN handling, alignment rules, and per-op numerical behavior.- Source of truth for this reference:
crates/lemon-lang/src/dsl/{lex,parse,ops,print}.rsandcrates/lemon-lang/src/spec.rs.