@alphacharts/script 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +161 -2
- package/dist/builtins-BXxWK58s.js +812 -0
- package/dist/builtins-BXxWK58s.js.map +1 -0
- package/dist/editor.js +211 -0
- package/dist/editor.js.map +1 -0
- package/dist/index.js +342 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox-p2HPP8yl.js +2416 -0
- package/dist/sandbox-p2HPP8yl.js.map +1 -0
- package/dist/sandbox-worker.js +8 -0
- package/dist/sandbox-worker.js.map +1 -0
- package/dist/types/ast.d.ts +156 -0
- package/dist/types/builtins.d.ts +67 -0
- package/dist/types/check.d.ts +78 -0
- package/dist/types/codegen.d.ts +14 -0
- package/dist/types/compile.d.ts +52 -0
- package/dist/types/diag.d.ts +45 -0
- package/dist/types/editor/index.d.ts +23 -0
- package/dist/types/feeds.d.ts +33 -0
- package/dist/types/index.d.ts +17 -0
- package/dist/types/lexer.d.ts +14 -0
- package/dist/types/limits.d.ts +25 -0
- package/dist/types/parser.d.ts +3 -0
- package/dist/types/run.d.ts +69 -0
- package/dist/types/runner.d.ts +54 -0
- package/dist/types/sandbox-worker.d.ts +1 -0
- package/dist/types/sandbox.d.ts +78 -0
- package/dist/types/ui-adapter.d.ts +21 -0
- package/package.json +75 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alphabench
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,162 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @alphacharts/script
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A small Pine-like indicator language for alphacharts. A script compiles to a study definition for
|
|
4
|
+
`@alphacharts/indicators`, so its numbers come from the same incremental engine and kernels as the built-in
|
|
5
|
+
studies (a script that is `ta.sma(close, 20)` is bit-identical to the `sma` study). User code runs only in a
|
|
6
|
+
dedicated Web Worker.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { compile, runScript, ScriptRunner, restoreScript } from "@alphacharts/script";
|
|
10
|
+
import { createScriptEditorAdapter } from "@alphacharts/script/editor"; // CodeMirror 6, this subpath only
|
|
11
|
+
|
|
12
|
+
const r = compile(source); // { ok: true, study, inputs, colorInputs, ... } | { ok: false, diagnostics }
|
|
13
|
+
const h = runScript(chart, candles, source, { len: 14 }, { volume, workerFactory }); // sandboxed; returns a handle
|
|
14
|
+
h.setInputs({ len: 21 }); h.study.setVisible(false); h.remove();
|
|
15
|
+
h.toJSON(); // { source, inputs, series ids, visible } -> restoreScript(chart, saved)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`compile(...).plan` is the checked program as data for the host app (`{ overlay, scale, needs, dependencies, plots: [{kind, title, expr}], body }`; `needs` is `"ohlcv"`
|
|
19
|
+
when the script reads volume; `dependencies` lists every `outside(...)` call, see below; plot kinds include `barcolor` and `bgcolor`). Its nodes are the syntax tree with names resolved (`id.sym` is one object per variable, `ty` on every expression);
|
|
20
|
+
it is read-only and shared with the compiled study.
|
|
21
|
+
|
|
22
|
+
`compile` never executes anything: it lexes, parses, checks and builds closures from the syntax tree. `runScript`
|
|
23
|
+
throws `ScriptDiagnosticsError` (with `.diagnostics`) when the script does not compile, and runs the computation
|
|
24
|
+
(batch and every tick) in the worker (`@alphacharts/script/worker`; pass `workerFactory`, `() => new Worker(new URL("@alphacharts/script/worker", import.meta.url), { type: "module" })`, or a shared `runner`: the package never guesses where its worker lives). A run-time failure (budget, runtime error, killed worker) goes to
|
|
25
|
+
`onError(diagnostic)` and `handle.error`; the study stops following the source until inputs change.
|
|
26
|
+
`compile(...).study` can also be given to `addStudy` directly (trusted code only: it then runs on the calling thread).
|
|
27
|
+
|
|
28
|
+
## The language
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
indicator("Bands", overlay=true)
|
|
32
|
+
len = input.int(20, "Length", minval=2, maxval=200)
|
|
33
|
+
mult = input.float(2.0, "Mult", 0.5, 5, 0.5)
|
|
34
|
+
[mid, up, lo] = ta.bb(close, len, mult)
|
|
35
|
+
p1 = plot(up, "Upper", color.blue)
|
|
36
|
+
p2 = plot(lo, "Lower", color.blue)
|
|
37
|
+
fill(p1, p2, color.new(color.blue, 90))
|
|
38
|
+
plot(mid, "Basis", color.orange)
|
|
39
|
+
plotshape(ta.crossover(close, up), "Break", shape.triangleup, location.belowbar, color.green)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- **Blocks** by indentation (spaces; tabs are an error). Lines inside `()` / `[]` continue. `//` comments.
|
|
43
|
+
- **Types**: `int float bool string color`, each with a qualifier: *const* (literal), *input* (known when the study is
|
|
44
|
+
built) and *series* (varies per bar). Declarations infer; `float x = na` declares an `na` of a type. A number written with a dot or an exponent (`0.0`, `1.`, `1e3`) is a float, whatever its value; there is no `int()`. `ta.highestbars/lowestbars`, `bar_index` and `input.int` are int. Nothing coerces:
|
|
45
|
+
a number is not a condition, a bool is not a number (use `?:`), `int` does not silently take a float.
|
|
46
|
+
- **Series and history**: every name is a series with implicit bar iteration; `x[n]` is `n` bars back (`na` before the
|
|
47
|
+
first). `n` may be an expression; it must be an integer in 0..1,000,000 (checked at run time when dynamic).
|
|
48
|
+
- **na semantics**: arithmetic with `na` is `na`; every comparison with `na` is false (including `!=`); `x / 0` and `x % 0` are
|
|
49
|
+
`na`; `math.*` results that are not finite are `na`; `nz(x, r)` replaces `na`, `na(x)` tests it.
|
|
50
|
+
- **State**: `var x = init` initialises on the first bar and persists; `x := e`, `+= -= *= /= %=` assign. A variable assigned
|
|
51
|
+
with `:=` (or declared `var`) is *state*; `if / else if / else` and bounded `for i = a to b [by s]` (inclusive, at most 10,000
|
|
52
|
+
iterations) change state. Block variables are local to their block. `var` is top-level only.
|
|
53
|
+
- **Pure vs state**: everything that does not depend on state is evaluated as whole columns by the engine's kernels. `ta.*`
|
|
54
|
+
calls therefore take only arguments known before the bar is evaluated (no `var`/`:=` variables, block variables or loop counters);
|
|
55
|
+
the compiler says so. `x[n]`, `ta.change` and `ta.crossover/under/cross` accept a top-level state variable, not an expression of one.
|
|
56
|
+
- **Inputs**: `name = input.int|float|bool|string|source|color(defval, title, ...)` as the whole right-hand side of a top-level
|
|
57
|
+
declaration; the variable name is the input key. `string` needs `options=[...]` (a select). `color` inputs are not part of the study
|
|
58
|
+
input schema (the engine has no colour type): they are reported in `compile().colorInputs` and applied as the colour of the plots that use them.
|
|
59
|
+
- **Output**: `plot(series, title, color, linewidth, style, offset, color_down)` (`plot.style_line|histogram|area`), `hline(price, title, color, linestyle)`,
|
|
60
|
+
`fill(plotA, plotB, color)` (both are handles: `p = plot(...)` and `h = hline(...)`; a fill may join two lines, a line and a level, or two levels, and a level it joins is a column, constant on every bar, named `<hline key>~level`), `plotshape(cond, title, style, location, color, offset, last, within)` (`offset` is a literal or a whole-number input expression, `offset=-right`, worked out when the study is built, -5000 to 5000; `last=N` keeps the N newest marks, `within=M` only marks on the final M bars; integer literals, markers only), `indicator(title, shorttitle, overlay, scale)` (`scale="left"` draws on the
|
|
61
|
+
chart's left axis), `barcolor(cond, color)` (colours the chart's candles or bars), `bgcolor(cond, color)` (shades the background of the pane the script draws in).
|
|
62
|
+
Colours are constants (`color.red`, `#rrggbb[aa]`, `color.new(c, transp)`) or an `input.color`; a line plot's colour may depend on the bar,
|
|
63
|
+
`plot(x, color = cond ? colorA : colorB)` (constant colours, bool series condition). Output statements are top-level only.
|
|
64
|
+
- **Outside series**: `outside(symbol, field="close", timeframe=na)` is a number series of another instrument and/or timeframe: `symbol` and `timeframe`
|
|
65
|
+
are string literals (`na` or left out: the chart's own), `field` one of open, high, low, close, volume. The package names nothing and fetches nothing:
|
|
66
|
+
`compile().plan.dependencies` lists each `{symbol, field, timeframe}` without running anything, `feedsOf(...)` groups them by series, and the host
|
|
67
|
+
loads each and passes it with `handle.setSeries(key, bars)` (bars in the series' own time) and `handle.pushSeries(key, bar)` for live bars.
|
|
68
|
+
`RunScriptOptions.intervalMs` (the chart's bar length) is required when the script reads any. `describeJoin(chartTimes, feedTimes, { align, spanMs })` (same as `alignFor` returns) says why a feed is blank on a chart: how many chart bars see a bar of it and, when none, the reason, e.g. NSE hourly bars (09:15) read from an MCX hourly chart (09:00) never share a time (`reason: "different-clock"`, `offsetMs: 900000`, with a sentence for the user). The rules, which are the safety property:
|
|
69
|
+
a same-length series joins on exactly equal bar time and is `na` where the other has no bar (nothing carried over); a longer one joins on the latest
|
|
70
|
+
bar that has finished (open + length not after the chart bar's start), so history never holds a value from after its bar and the live bar shows the last
|
|
71
|
+
finished one; a shorter one is refused. A series with a timeframe is computed on its own bars (`ta.rsi(outside(timeframe="1h"), 14)` is hourly RSI),
|
|
72
|
+
one without on the chart's; mixing it with other series puts each on the chart's bars first. `indicators/src/engine/feed.ts` has the join.
|
|
73
|
+
- **Built-in series**: `open high low close volume time hl2 hlc3 ohlc4 hlcc4 bar_index bars_per_year`, `ta.tr` (na on bar 0, Pine's). `bars_per_year` is the chart's bars in a year read from its own bars (a causal annualisation: the median bars per UTC day over the last 60 finished days times 252 for intraday bars; 252 for daily bars without weekend bars, else 365 days over the median spacing), constant within a day, na before the first finished day, never a value from after the bar. `math.sqrt(bars_per_year)` annualises; `ta.bars_per_year(source, utcOffsetMinutes)` cuts the day elsewhere or counts an outside series' bars.
|
|
74
|
+
- **Functions**: `math.abs sqrt pow log log10 exp sin cos tan asin acos atan sinh cosh tanh ceil floor trunc round sign avg max min toradians todegrees`,
|
|
75
|
+
`math.pi math.e`, `nz`, `na`, `color.new`, and `ta.*` below. There are no user functions, strings operations, arrays, `while`, `eval` or any
|
|
76
|
+
way to name a host object: such words are refused with a diagnostic.
|
|
77
|
+
|
|
78
|
+
### `ta.*`
|
|
79
|
+
|
|
80
|
+
- Kernels (the graph's own): `sma ema wma rma highest lowest highestbars lowestbars sum linreg` `(source, length)`, `stdev` and `variance` `(source, length, sample=false)` (population, divisor n; `sample=true` divides by n - 1 and needs length 2 or more), `atr(length)` (Wilder's: bar 0's true range is high - low, first value at bar length - 1), `ta.tr`,
|
|
81
|
+
`change(source, length=1)`, `crossover / crossunder / cross(a, b)`, `bb(source, length, mult) -> [basis, upper, lower]`.
|
|
82
|
+
`highestbars/lowestbars` are bars since the extreme (0 = this bar), as the built-in Aroon uses them (TradingView's are negated).
|
|
83
|
+
- **Every other study of `@alphacharts/indicators`** (all 200+ non-math ones, TA-Lib's catalogue and the TradingView set, candlestick patterns
|
|
84
|
+
included) is `ta.<id>` with `-` written `_`: `ta.rsi(source, length)`, `ta.macd(source, fast, slow, signal) -> [macd, signal, histogram]`, `ta.bbands`,
|
|
85
|
+
`ta.stoch()`, `ta.supertrend(atrLength, factor)`, `ta.cdl_doji()`, `ta.ht_dcperiod(source)`. A study with a `source` input takes it first (a series);
|
|
86
|
+
the others follow in the study's own order, by position or by name, all optional with the study's defaults. A study with several columns returns a
|
|
87
|
+
tuple: destructure it. The call builds that study's own nodes on the script's graph with the source series substituted, so there is one implementation.
|
|
88
|
+
|
|
89
|
+
### Grammar
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
program = { statement } ;
|
|
93
|
+
statement = declaration | tuple | assignment | if | for | call NEWLINE ;
|
|
94
|
+
declaration = [ "var" ] [ type ] IDENT "=" expr NEWLINE ;
|
|
95
|
+
tuple = "[" IDENT { "," IDENT } "]" "=" expr NEWLINE ;
|
|
96
|
+
assignment = IDENT ( ":=" | "+=" | "-=" | "*=" | "/=" | "%=" ) expr NEWLINE ;
|
|
97
|
+
if = "if" expr block [ "else" ( if | block ) ] ;
|
|
98
|
+
for = "for" IDENT "=" expr "to" expr [ "by" expr ] block ;
|
|
99
|
+
block = NEWLINE INDENT statement { statement } DEDENT ;
|
|
100
|
+
expr = or [ "?" expr ":" expr ] ;
|
|
101
|
+
or = and { "or" and } ; and = not { "and" not } ; not = "not" not | cmp ;
|
|
102
|
+
cmp = add [ ( "==" | "!=" | "<" | "<=" | ">" | ">=" ) add ] ; (no chaining)
|
|
103
|
+
add = mul { ( "+" | "-" ) mul } ; mul = unary { ( "*" | "/" | "%" ) unary } ;
|
|
104
|
+
unary = ( "-" | "+" ) unary | postfix ;
|
|
105
|
+
postfix = primary { "(" [ arg { "," arg } ] ")" | "[" expr "]" | "." IDENT } ;
|
|
106
|
+
arg = [ IDENT "=" ] expr ;
|
|
107
|
+
primary = NUMBER | STRING | COLOR | "true" | "false" | IDENT | "(" expr ")" | "[" [ expr { "," expr } ] "]" ;
|
|
108
|
+
```
|
|
109
|
+
(array literals exist only for `options=[...]`.) Operators bind: `?:` < `or` < `and` < `not` < comparison < `+ -` < `* / %` < unary < postfix.
|
|
110
|
+
|
|
111
|
+
## Pipeline
|
|
112
|
+
|
|
113
|
+
`lexer.ts` (tokens with offsets, INDENT/DEDENT) -> `parser.ts` (recursive descent, recovers at the next line/block so one run reports every
|
|
114
|
+
independent error, at most 50) -> `check.ts` (types and qualifiers, signature table `builtins.ts`, plan: inputs, plots, program) -> `codegen.ts`
|
|
115
|
+
(a `StudyDef`). Diagnostics carry `line/column/endLine/endColumn` (1-based) and `from/to` offsets.
|
|
116
|
+
|
|
117
|
+
### How a script runs
|
|
118
|
+
|
|
119
|
+
Codegen emits **closures built from the syntax tree, never source text**: no identifier or string of a script is ever concatenated into code, so
|
|
120
|
+
injection is not possible by construction (test/injection.test.ts replaces `eval` and `Function` with throwing stubs and runs the whole corpus).
|
|
121
|
+
Pure statements become graph nodes (the same calls the built-in studies make, in the same arithmetic order: that is why results are bit-identical).
|
|
122
|
+
The state part is one per-bar node: a `var` lives in its own column, and bar *i* starts from bar *i-1*'s committed value, so appending a bar,
|
|
123
|
+
revising the last bar (a tick) and a batch run give identical columns, O(1) per new bar plus the work of the statements in it.
|
|
124
|
+
|
|
125
|
+
## Sandbox
|
|
126
|
+
|
|
127
|
+
`runScript` computes in a Web Worker (batch through a `StudyRunner`, ticks over a one-bar message; the main thread only mirrors the columns, with nodes
|
|
128
|
+
that compute nothing, and draws through the indicators plugin).
|
|
129
|
+
|
|
130
|
+
- **No host access from the language**: it cannot name `fetch`, `XMLHttpRequest`, `WebSocket`, `importScripts`, `EventSource`, `indexedDB`, `caches`, the DOM,
|
|
131
|
+
`self/globalThis`, `Function`, `eval`, prototypes (`__proto__`, `constructor`, `prototype` are reserved words; name tables have no prototype), `Date`, `Math.random`.
|
|
132
|
+
Output is deterministic: no clock or randomness exists in the language.
|
|
133
|
+
- **`lockdown(scope)`** (first thing in the worker): shadows those capabilities plus `Worker`, `SharedWorker`, `BroadcastChannel`, `Request`, `navigator`, `location`, ... with
|
|
134
|
+
read-only, non-configurable `undefined`, makes `eval`, `Math.random` and `Date.now` throw, freezes the intrinsic prototypes. Reports what it could not shadow.
|
|
135
|
+
- **Outside series** reach the worker as transferred columns only; the language still names nothing, the lockdown is unchanged. They count against the limits:
|
|
136
|
+
at most `maxFeeds` (8) series, each at most `maxBars` bars, and their bytes (a series is a graph of its own: its 6 columns and what is computed on them, 3 x, plus the
|
|
137
|
+
page's copy of the bars) are added to the script's. A live bar of a series is answered like a tick (`tickMs` watchdog), with `idle` when nothing the chart's last bar sees changed.
|
|
138
|
+
- **Budgets** (`DEFAULT_LIMITS`, overridable per runner): loop iterations per loop (10,000) and per bar (2,000,000 steps) -> `execution budget exceeded`; columns (512), bytes
|
|
139
|
+
(512 MiB = 3 x columns x bars x 8: the worker keeps a copy and a column grows by half on the first appended bar; every column, not only the plotted ones, is also mirrored on the main thread), bars (10M), period (5,000), history offset (1,000,000), source size (200,000 chars), expression nesting (100), plots (64). Each breach is a diagnostic naming the limit.
|
|
140
|
+
- **Watchdog**: the main thread terminates a worker that does not answer a batch within 8 s or a tick within 1 s, rejects with the budget error and starts a fresh worker for the next request.
|
|
141
|
+
- There are no arrays or other heap values in the language, so "huge arrays" reduce to huge periods, offsets, bars and column counts, which the limits above cover.
|
|
142
|
+
|
|
143
|
+
## Editor (`@alphacharts/script/editor`)
|
|
144
|
+
|
|
145
|
+
`createScriptEditorAdapter(): ScriptEditorAdapter` — `mount(el, { value, onChange, onRun, diagnostics }) -> { setDiagnostics, destroy }`. CodeMirror 6 (pinned,
|
|
146
|
+
dependencies of this subpath only): highlighting, inline lint markers from the diagnostics you pass, autocomplete of `ta.*`, `math.*`, `input.*`, constants,
|
|
147
|
+
built-in series and the names declared in the text, Ctrl/Cmd-Enter calls `onRun`, Tab inserts four spaces. Colours come from CSS variables (`--ac-script-*`).
|
|
148
|
+
|
|
149
|
+
## Verification
|
|
150
|
+
|
|
151
|
+
`pnpm -C packages/script test`: grammar corpus (`test/corpus.ts`, 113 invalid programs with their exact diagnostics in `corpus-invalid.json`, 85 valid programs run end to end),
|
|
152
|
+
type/semantics tests, 45 scripts reproducing built-in studies with exact equality (`equality.test.ts`), incremental == batch with random revisions
|
|
153
|
+
(`incremental.test.ts`), sandbox / limits / lockdown / runner watchdog (`sandbox.test.ts`), injection (`injection.test.ts`). Browser: `bench/tests/script.spec.ts`
|
|
154
|
+
(playground `?ex=script`) on Chrome, WebKit and Firefox.
|
|
155
|
+
|
|
156
|
+
## Known limits
|
|
157
|
+
|
|
158
|
+
- `ta.*` cannot take state-dependent arguments (see above); `x[n]` / `crossover` need a plain series or a top-level variable.
|
|
159
|
+
- No user functions, arrays, strings operations, `break`, drawings (labels/lines/boxes), alerts. A per-bar colour is for line plots (not histogram, area or markers).
|
|
160
|
+
- A same-timeframe outside series with holes gives windows that reach a hole `na` (a missing bar is never filled); a state variable (`var`, `:=`) runs on the chart's bars only.
|
|
161
|
+
- `input.color` and `input.string` are outside the study input schema's types (`InputType` has no colour/free-text): colours are handled through `colorInputs`, a string needs `options`.
|
|
162
|
+
- A chart layout saved with `chart.saveState()` holds the script's study id; restore scripts with `handle.toJSON()` / `restoreScript`, not by restoring the layout alone.
|