BSL (BitValue Strategy Language) is an embedded JavaScript (ES5.1+) DSL.
A strategy is a .js file executed bar-by-bar by Athena's goja runtime, then fed
into the existing backtest matching engine and golden-compare. This page is the canonical definition that a
strategy's // @bsl bitvalue.io/schema/bsl/v1 header dereferences to.
$id: https://bitvalue.io/schema/bsl/v1) describes the same host API surface for tools and AI agents.
It is a surface descriptor, not a JSON Schema for validating data.
Every BSL strategy starts with a magic header comment declaring the language version. The runtime parses it and validates it against its supported set; a missing or unknown URI is a hard error — it is never silently treated as v1.
// @bsl bitvalue.io/schema/bsl/v1
A strategy calls the host-provided strategy({...}) once to register itself.
// @bsl bitvalue.io/schema/bsl/v1 strategy({ name: "eth15m", // must equal manifest identity name params: { er_period: 14, er_threshold_long: 60 }, // defaults; manifest overrides setup(p) { // runs once: build indicators + persistent state this.er = ind.ER(p.er_period); this.psar = ind.CustomATRPSAR(p.psar_start, p.psar_inc, p.atr_len); this.retestCount = 0; // persistent state = plain this.* field }, onBar(c, pos, p) { // runs once per closed bar if (pos.flat && this.psar.trendDir === 1 && this.er.value >= p.er_threshold_long) { enterLong("TREND_LONG", { tp: [[.05,.30],[.10,.15]], sl: .03, slMode: "strategy_managed" }); } if (pos.long && this.psar.flipped()) close("PSAR_EXIT"); plot("Custom PSAR", this.psar.value, { style: "cross", pane: "main" }); }, });
ind.*Constructed in setup(). Backed by strategy-lib/indicators (the same types the native Go
engine uses, so factor values are identical bar-for-bar). The runtime advances every indicator each closed bar;
onBar only reads them.
| Constructor | Defaults | Reads |
|---|---|---|
ind.ER(period) | 14 | .value |
ind.ATR(period) | 14 | .value |
ind.ATRRank(atrLength, window) | 14, 300 | .value |
ind.VolRatio(fastLength, slowLength) | 14, 300 | .value (=fastATR/slowATR) |
ind.CustomATRPSAR(start, increment, atrLength, longMax, shortMax, minDistMult) | 0.01, 0.03, 14, 0.06, 0.06, 5.0 | PSAR fields ↓ |
ind.PSARDirectional(start, longIncrement, shortIncrement, atrLength, longMax, shortMax, minDistMult) | 0.01, 0.03, 0.06, 14, 10, 12, 5.0 | PSAR fields ↓ |
| Field | Meaning |
|---|---|
.value / .psar | SAR level at the current bar |
.trendDir | +1 long, −1 short |
.trendBars | bars held in the current trend direction |
.ep / .af | extreme point / acceleration factor |
.flipped() | true on the bar the trend direction changed (initialized trends only) |
c, pos, p| Object | Fields |
|---|---|
c (candle) | time (bar open, epoch s), closeTime (epoch s), open, high, low, close |
pos (position) | inPosition, flat, long, short, direction ("long"|"short"|""), avgPrice, entryPrice, qty, barsSinceEntry |
p (params) | resolved parameters (strategy params defaults overridden by the manifest) |
onBar may emit at most one position action.
The first wins; a second, different action in the same bar is a hard runtime error — guard with
pos.flat/long/short. An identical repeat is a no-op. plot() is not a position action.| Verb | Signature |
|---|---|
enterLong | enterLong(signalType, { tp, sl, slMode }) |
enterShort | enterShort(signalType, { tp, sl, slMode }) |
close | close(reason) |
flipLong | flipLong(signalType = "TREND_FLIP", { tp, sl, slMode }) |
flipShort | flipShort(signalType = "TREND_FLIP", { tp, sl, slMode }) |
| Key | Type | Meaning |
|---|---|---|
tp | [[offset, qty], …] | take-profit ladder; offset = price fraction from entry, qty = fraction of position closed at that rung |
sl | number | stop-loss as a price fraction from entry (e.g. 0.03 = 3%) |
slMode | "enforced" | "strategy_managed" | enforced = exchange-side SL (default, fail-safe); strategy_managed = named exemption, the strategy's own PSAR/CLOSE owns the exit (hermes AGENTS §9) |
Signal-type / exit-reason strings must align with the canonical values in signal.go so golden-compare can map them.
plot(name, value, { style, pane, color }) emits one named per-bar series → a column in
indicators.parquet. value = null means sparse/skipped for that bar (e.g. only plot a
Long SL while long). Align name with TradingView golden column names for indicator-level golden compare.
{ style, pane, color } (style = line|cross|circles|histogram, pane = main|sub, color) is a planned
chart-spec hint for the frontend renderer. The v1 runtime currently records only
(name, value) and accepts but does not yet parse this argument. It never affects signals.Backtests must be reproducible: same .js + same bars + same params ⇒ same result.
Date is removed and Math.random is disabled — use c.time for time.require; a fresh VM per run.Two independent layers:
bitvalue.io/schema/bsl/vN). A breaking
language/semantic change bumps to a new URI (…/v2); backward-compatible host API additions do not.family@version (e.g. eth15m@2026q3),
governed by the strategy manifest.