Running
Execution & Data
Last updated March 1, 2026
Understanding how the Runner executes strategies and how market data flows through the system helps you write more reliable strategies and debug unexpected behavior.
The Tick Cycle
The Runner daemon runs in a tight loop. Every 2 seconds it checks for projects that are due for execution. A project is "due" based on its Interval setting (default: 5 minutes). When claimed, the Runner:
- Clears the kline cache so stale data from any prior error can't affect the current run.
- Checks settings: trade hours, weekend guard, and per-day trade limit are enforced before any code runs. If any guard fails, the run is logged as "skipped" and no strategy code runs.
- Loads fresh kline data into the in-memory cache for all required timeframes and symbols.
- For each symbol: creates a fresh set of indicator functions, runs the compiled JS strategy in a sandboxed VM with a 5-second timeout.
- Logs results and updates the project's
last_run_status.
Per-Symbol Isolation
Each symbol is fully isolated. If your project has BTCUSDT and ETHUSDT, the strategy runs twice per tick, once for BTC, once for ETH. Positions, trades, and indicator state are all independent between symbols.
Kline Data
The Runner uses a KlineManager that periodically refreshes historical candle data from Binance into a Supabase database. From the database it's loaded into an in-memory cache before each tick. The default history window is 30 days of candles.
1m (1-minute) timeframe by default. The timeframe is embedded in the compiled JS and extracted by the Runner to know which klines to preload.The mark price (used for paper trade execution) is the latest close available in the cache. The Runner checks timeframes in priority order: 1m โ 5m โ 15m โ 1h โ 4h, using the first one that has data.
Paper Broker
The PaperBroker simulates order fills without real money. It's designed to reflect realistic position management: entries, exits, partial closes, and PnL tracking.
Buy Execution
When your strategy triggers a BUY with a USD amount:
- The mark price is fetched from the kline cache.
- Quantity = USD รท mark price.
- If no position exists: a new position row is created with entry price = mark price.
- If a position already exists: the new fill is averaged in. New entry price = (old_qty ร old_entry + new_qty ร mark_price) / (old_qty + new_qty). This lets you scale into positions.
Sell Execution
When your strategy triggers a SELL with a percentage:
- The open position is fetched from the database.
- If no open position: the sell is skipped and a warning is logged. No phantom trade is recorded.
- Close qty = position.qty ร (pct รท 100).
- Realized PnL = (mark_price โ entry_price) ร close_qty.
- If pct = 100 (or remaining qty โ 0): position is marked "closed."
- If partial: position qty is reduced; the position stays open.
Positions & Trade History
The Positions tab shows both open positions and a paginated history of all filled orders.
Open Positions
Each open position shows: symbol, side (always Long in the current version), entry price, current mark price, quantity, unrealized PnL, and time open.
Filled Orders
Every buy and sell creates a trade record. Fields include: symbol, side, quantity, price, realized PnL (for sells), fee, and timestamp. Realized PnL is only non-zero on sell fills.
Equity Curve
The Live tab shows a cumulative realized PnL chart. It sums all sell-side realized PnL values in chronological order. The chart can be filtered by time range: 1D, 1W, 1M, or ALL. Only sells contribute to the equity curve, open position unrealized gains are not included.