Running
Strategy Execution Engine
Last updated April 20, 2026
Norena is not a drag-and-drop toy — it is a compiled, sandboxed execution system. Strategies are translated into real JavaScript, isolated in a secure VM, evaluated against live market data, and run on a precise 2-second scheduler. This page explains exactly how all of that works.
Overview
Every Norena strategy goes through four stages from block to live execution:
You assemble logic visually in the Strategy Builder using blocks.
Blockly generates JavaScript from your block graph. The JS is validated and stored.
The runner claims the project when it is due based on its interval setting.
Strategy code runs in an isolated sandbox with access only to the whitelisted API.
Strategy Compilation
When you click Compile, the Strategy Builder serializes your Blockly workspace into JavaScript using Blockly's JavaScript generator. Each block type has a customforBlock generator function that produces the exact runtime call.
For example, an RSI block with period 14 on the 1h timeframe generates:
((_v) => HP.__cond("RSI(14)", String(+_v.toFixed(2)), "RSI(14) < 30", _v < 30))(RSI({ tf: "1h", period: 14 }))This pattern wraps every boolean comparison in HP.__cond(), which records the condition label, runtime value, rule string, and result into the condition log — without any extra work from you.
The compiled JS also includes a timeframe manifest comment injected at the top:
// @norena-timeframes: 1h,4h
The runner reads this comment to know which kline series to load before executing the strategy. Both the live runner and the backtest engine use this manifest — it is the single source of truth for which timeframes a strategy needs.
What compilation validates
| Check | Behavior on failure |
|---|---|
| Legacy blocks present | Compiled JS includes a throw that surfaces an error in Logs at runtime, identifying which block to replace. |
| No blocks in workspace | An empty workspace produces empty JS. The runner skips the tick with "No code compiled." |
| Multi-timeframe strategies | All referenced timeframes are extracted and added to the manifest automatically. |
The Run Loop
The Norena runner is a Node.js process that runs a continuous 2-second polling loop. On each tick it calls claim_due_projects — a database function that atomically claims up to 5 projects whose next scheduled run time has passed. Claiming is atomic, so multiple runner instances cannot process the same project simultaneously.
while (true) {
const projects = await claimDueProjects(limit=5);
for (const project of projects) {
await runProject(project); // per-project, per-symbol
}
await sleep(2000);
}Per-project execution steps
- Load settings — reads the latest
settings_jsonfrom the database (mode, wallet, trade hours, etc.). - Check guards — trade hours window, disable weekends. Skips and logs if any guard fails.
- Load compiled JS — reads the latest
generated_jsfrom the project row. - Extract timeframes — parses the
@norena-timeframesmanifest. - Per-symbol loop — for each configured symbol:
- Check kline readiness (bootstrapped & fresh within 3 min)
- Check max daily trades limit
- Build the broker (Paper or Live)
- Build the API surface (indicators + HP)
- Run the strategy in a sandbox (5s timeout)
- Update run record — writes status, finish time, and per-symbol error summary.
Sandbox Isolation
User strategy code runs inside Node.js's built-in vm module with a strict null-prototype global. This means strategy code has no access to:
- The filesystem (
fs,path) - The network (
fetch,http,https) - The process (
process,require,import) - Any global not explicitly whitelisted
The sandbox exposes only the strategy API (indicators, HP, position state functions) plus frozen versions of safe builtins: Math, Date, JSON,Array, Object, Number, String, Boolean. Mutable globals like Array and Object are frozen to prevent prototype pollution between sandboxed runs.
vm.Script runs with a synchronous timeout. Additionally, the async IIFE wrapper is raced against a 5-second deadline — so strategies that loop on async calls (e.g. while(true) { await HP.buy(...) }) cannot hang the runner indefinitely.In the backtest engine, a Function() constructor is used instead of Node'svm module (since backtests run in the browser). The same whitelisting logic applies — only the strategy API is injected as named parameters.
Strategy API Surface
Inside the sandbox, strategy code has access to a precisely defined set of named functions. These are the only things a strategy can call:
| Name | Type | Description |
|---|---|---|
EMA, SMA, WMA | Indicator → number | Moving average indicators |
RSI, ATR, MACD, BBANDS, VWAP | Indicator → number/object | Standard technical indicators |
BOLLINGER, VOLATILITY_REGIME | Indicator → object/boolean | Volatility indicators |
PRICE, VOLUME | Market data → number | Raw price/volume series |
HIGHEST, LOWEST, HIGHEST_HIGH, LOWEST_LOW | Lookback → number | Rolling high/low values |
PREV | Lookback → number | Historical value N bars ago |
CANDLE_PATTERN, VOLUME_SPIKE, PRICE_CHANGE_PCT | Market → bool/number | Pattern and volume detection |
BREAKOUT_UP, BREAKOUT_DOWN | Event → boolean | Price breakout detection |
EMA_CROSS_UP/DOWN, SMA_CROSS_UP/DOWN, MACD_CROSS_UP/DOWN | Event → boolean | Built-in cross event shortcuts |
CROSS_UP, CROSS_DOWN | Event → boolean | Generic cross between two series |
IN_POSITION | State → boolean | Whether an open position exists |
BARS_SINCE_ENTRY | State → number | Bars elapsed since position entry |
POSITION_VALUE | State → number | Current position notional USD value |
TAKE_PROFIT, STOP_LOSS | State → boolean | Price vs. entry price thresholds |
COOLDOWN_OK | State → boolean | Minimum bars since last trade |
HP.buy, HP.sell | Action → async void | Place a buy/sell order |
HP.log | Logging → async void | Write a custom info log entry |
HP.__cond, __ctx, __group, __checkpoint, __flipFrom, __negate | Internal | Condition tracking (called by generated code, not manually) |
Condition Tracking System
One of Norena's most distinctive features is that every boolean comparison in your strategy is automatically recorded at the moment a trade is placed. This is not optional logging — it is baked into the compiled code.
The Blockly generator wraps every logic_compare, logic_operation,logic_negate, and controls_if block in calls toHP.__cond, HP.__group, HP.__checkpoint, andHP.__flipFrom. These functions accumulate a ConditionRow[] array that captures what happened — in order — during the current tick.
| Function | Called by | Purpose |
|---|---|---|
HP.__cond(label, value, rule, result) | Every logic_compare | Records a single condition evaluation with label, runtime value, rule string, and boolean result. |
HP.__ctx(label, value) | RHS indicator comparisons | Records a display-only context row (e.g. shows EMA value for reference). Never counted as a pass/fail condition. |
HP.__group(op, startIdx, evaluate) | AND/OR blocks | Runs the compound expression, marks all sub-rows as grouped, appends a group summary row with the actual runtime result. |
HP.__checkpoint() | Each if block entrance | Returns the current length of the condition rows array — used to know where conditions for this branch start. |
HP.__flipFrom(idx) | else/else-if branches | Inverts the rule operator and result of leaf conditions recorded since idx. This makes the log accurately reflect "why this else branch was entered." |
HP.__negate(val) | NOT blocks | Inverts the last condition row's result and prepends "NOT (" to its rule. |
When HP.buy() or HP.sell() is called, it snapshots the entireconditionRows array and resets it to empty. Each discrete trade call gets exactly the conditions accumulated since the last trade call in that tick. The snapshot is immediately written to the TRADE_TRIGGER log.
HP.__cond / __group / __flipFrom logic runs in both the live runner and the browser backtest engine. This ensures condition logs in the Trade Detail Modal match what the backtest analytics compute.Signal Generation
A "signal" in Norena is simply a call to HP.buy() or HP.sell()inside the strategy code. There is no separate signal queue or event bus — the strategy code evaluates conditions inline and calls the trade function directly when conditions are met.
The decision flow per symbol per tick is:
- System conditions are appended to the condition log (trade hours, max daily trades).
- Indicators are computed lazily on first access and cached for the remainder of the tick.
- Strategy code evaluates its
ifbranches in top-to-bottom order. - If a branch's condition is true, the body executes — which may call
HP.buy()orHP.sell(). - The trade action captures a snapshot, logs TRADE_TRIGGER, executes the order, logs TRADE_EXECUTED.
- If no trade call was made, the runner logs "Result: No trade conditions met."
Multiple HP.buy() or HP.sell() calls in a single tick are possible if the strategy has multiple branches. Each call produces its own TRADE_TRIGGER + TRADE_EXECUTED log pair and its own condition snapshot.
Duplicate Signal Prevention
The runner prevents several classes of duplicate or nonsensical signals automatically:
| Scenario | Behavior |
|---|---|
| BUY while already in a position | The broker checks for an existing open position before executing. If one exists, the BUY is silently rejected (status: REJECTED) and logged. The strategy does not error. |
| SELL with no open position | The broker detects no position and rejects the SELL silently. The condition log still records what was evaluated. |
| Cross events on first tick | Cross Up/Down events require two consecutive ticks. The first tick after project start always returns false because no prior state exists yet. |
| Concurrent claims of same project | The claim_due_projects database function is atomic — the same project cannot be claimed twice simultaneously across runner instances. |
| Stale klines | If the latest 1m candle is more than 3 minutes old, the tick is skipped entirely to prevent signals based on stale data. |
Multi-Timeframe Execution
Norena supports strategies that read from multiple timeframes simultaneously. The@norena-timeframes manifest comment in the compiled JS lists every timeframe the strategy references. The runner loads all of them into the kline cache before execution.
The primary timeframe — the first in the manifest — determines:
- Which series drives the bar loop in backtesting
- Which series is used for
BARS_SINCE_ENTRYandCOOLDOWN_OKcalculations - Which series drives the kline readiness check before each live tick
Secondary timeframes are available on the same tick — the indicator cache serves all timeframes simultaneously. A strategy can read a 1h EMA and a 4h RSI on the same bar without any additional configuration.