/Documentation

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:

1
Build

You assemble logic visually in the Strategy Builder using blocks.

2
Compile

Blockly generates JavaScript from your block graph. The JS is validated and stored.

3
Schedule

The runner claims the project when it is due based on its interval setting.

4
Execute

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:

Generated JavaScript
((_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:

Timeframe manifest (auto-injected)
// @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

CheckBehavior on failure
Legacy blocks presentCompiled JS includes a throw that surfaces an error in Logs at runtime, identifying which block to replace.
No blocks in workspaceAn empty workspace produces empty JS. The runner skips the tick with "No code compiled."
Multi-timeframe strategiesAll 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.

Run loop (simplified)
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

  1. Load settings — reads the latest settings_json from the database (mode, wallet, trade hours, etc.).
  2. Check guards — trade hours window, disable weekends. Skips and logs if any guard fails.
  3. Load compiled JS — reads the latest generated_js from the project row.
  4. Extract timeframes — parses the @norena-timeframes manifest.
  5. 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)
  6. 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.

🔒
Two timeout layers. The 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:

NameTypeDescription
EMA, SMA, WMAIndicator → numberMoving average indicators
RSI, ATR, MACD, BBANDS, VWAPIndicator → number/objectStandard technical indicators
BOLLINGER, VOLATILITY_REGIMEIndicator → object/booleanVolatility indicators
PRICE, VOLUMEMarket data → numberRaw price/volume series
HIGHEST, LOWEST, HIGHEST_HIGH, LOWEST_LOWLookback → numberRolling high/low values
PREVLookback → numberHistorical value N bars ago
CANDLE_PATTERN, VOLUME_SPIKE, PRICE_CHANGE_PCTMarket → bool/numberPattern and volume detection
BREAKOUT_UP, BREAKOUT_DOWNEvent → booleanPrice breakout detection
EMA_CROSS_UP/DOWN, SMA_CROSS_UP/DOWN, MACD_CROSS_UP/DOWNEvent → booleanBuilt-in cross event shortcuts
CROSS_UP, CROSS_DOWNEvent → booleanGeneric cross between two series
IN_POSITIONState → booleanWhether an open position exists
BARS_SINCE_ENTRYState → numberBars elapsed since position entry
POSITION_VALUEState → numberCurrent position notional USD value
TAKE_PROFIT, STOP_LOSSState → booleanPrice vs. entry price thresholds
COOLDOWN_OKState → booleanMinimum bars since last trade
HP.buy, HP.sellAction → async voidPlace a buy/sell order
HP.logLogging → async voidWrite a custom info log entry
HP.__cond, __ctx, __group, __checkpoint, __flipFrom, __negateInternalCondition 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.

FunctionCalled byPurpose
HP.__cond(label, value, rule, result)Every logic_compareRecords a single condition evaluation with label, runtime value, rule string, and boolean result.
HP.__ctx(label, value)RHS indicator comparisonsRecords 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 blocksRuns the compound expression, marks all sub-rows as grouped, appends a group summary row with the actual runtime result.
HP.__checkpoint()Each if block entranceReturns the current length of the condition rows array — used to know where conditions for this branch start.
HP.__flipFrom(idx)else/else-if branchesInverts 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 blocksInverts 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.

ℹ️
The condition tracking system mirrors the backtest engine exactly. The same 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:

  1. System conditions are appended to the condition log (trade hours, max daily trades).
  2. Indicators are computed lazily on first access and cached for the remainder of the tick.
  3. Strategy code evaluates its if branches in top-to-bottom order.
  4. If a branch's condition is true, the body executes — which may call HP.buy() or HP.sell().
  5. The trade action captures a snapshot, logs TRADE_TRIGGER, executes the order, logs TRADE_EXECUTED.
  6. 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:

ScenarioBehavior
BUY while already in a positionThe 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 positionThe broker detects no position and rejects the SELL silently. The condition log still records what was evaluated.
Cross events on first tickCross 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 projectThe claim_due_projects database function is atomic — the same project cannot be claimed twice simultaneously across runner instances.
Stale klinesIf 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_ENTRY and COOLDOWN_OK calculations
  • 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.

⚠️
Set your run interval to match the primary timeframe. If your primary timeframe is 1h, there's no benefit to running every 60 seconds — the hourly close won't update until the next hour. A run interval of 3600s (1h) is appropriate for a 1h strategy.