BitValue / schema
bitvalue.io/schema/bsl/v1 stable

BSL Language — v1

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.

Machine-readable: descriptor.json ($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.

1 · Declaring the language version

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

2 · Strategy shape

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" });
  },
});

3 · Indicators — 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.

ConstructorDefaultsReads
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.0PSAR fields ↓
ind.PSARDirectional(start, longIncrement, shortIncrement, atrLength, longMax, shortMax, minDistMult)0.01, 0.03, 0.06, 14, 10, 12, 5.0PSAR fields ↓

PSAR handle fields

FieldMeaning
.value / .psarSAR level at the current bar
.trendDir+1 long, −1 short
.trendBarsbars held in the current trend direction
.ep / .afextreme point / acceleration factor
.flipped()true on the bar the trend direction changed (initialized trends only)

4 · Bar context — c, pos, p

ObjectFields
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)

5 · Position actions

One signal per bar. 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.
VerbSignature
enterLongenterLong(signalType, { tp, sl, slMode })
enterShortenterShort(signalType, { tp, sl, slMode })
closeclose(reason)
flipLongflipLong(signalType = "TREND_FLIP", { tp, sl, slMode })
flipShortflipShort(signalType = "TREND_FLIP", { tp, sl, slMode })

Protective-order spec (2nd arg of enter*/flip*)

KeyTypeMeaning
tp[[offset, qty], …]take-profit ladder; offset = price fraction from entry, qty = fraction of position closed at that rung
slnumberstop-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.

6 · Plot

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.

Reserved / forward-looking: the third argument { 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.

7 · Determinism & sandbox

Backtests must be reproducible: same .js + same bars + same params ⇒ same result.

8 · Versioning

Two independent layers: