/Documentation

Running

Position Lifecycle

Last updated April 20, 2026

A position is the record of capital deployed in a symbol. Every BUY opens one; every full SELL closes one. This page traces the complete lifecycle — from the moment a position is created through how PnL is computed and how everything is stored.

Overview

Norena models positions as a simple spot long: you hold some quantity of an asset, bought at a specific entry price, and you exit at a later price. There are no shorts, no leverage, and no margin. Each project + symbol pair can have at most one open position at a time.

StateMeaning
openBUY has been executed, position is active. No new BUY is permitted until this closes.
closedSELL has been executed for 100% of the position. PnL is finalized.

Opening a Position

A position is opened when HP.buy(usd) is called and the broker accepts the order. The sequence is:

  1. The strategy calls HP.buy(500) — buy $500 worth.
  2. The runner captures the trigger snapshot (mark price, condition rows) synchronously.
  3. The broker checks: is there an existing open position for this project + symbol? If yes, the BUY is rejected silently.
  4. If no position exists:
    • Paper mode: fill price = current mark price. Quantity = USD ÷ fill price. Fee = $0.
    • Live mode: a Binance MARKET order is placed. Fill price = exchange fill. Quantity = exchange fill quantity. Fee = Binance fee (paid in BNB or base asset).
  5. A project_positions row is inserted with status: 'open', entry_price, qty, and entry_time.
  6. A project_trades row is inserted for the BUY side.
  7. TRADE_TRIGGER and TRADE_EXECUTED logs are written and linked to the trade row.
ℹ️
Entry price is the actual fill price, not the mark price at trigger time. In paper mode these are the same. In live mode, the entry price reflects the real Binance fill. The difference between trigger price and fill price is recorded as slippage.

Tracking an Open Position

While a position is open, the runner reads it from the database at the start of each tick via a fresh query to project_positions. The position data is stored in apositionRef reference that is kept current across the tick and refreshed after each trade action.

The following strategy functions read from the active position:

FunctionWhat it reads
IN_POSITION()Whether positionRef.current has a non-null ID
BARS_SINCE_ENTRY()entry_time of the current position, converted to a bar count on the primary timeframe
POSITION_VALUE()qty × current mark price
TAKE_PROFIT({ pct })entry_price — checks if mark price ≥ entry × (1 + pct/100)
STOP_LOSS({ pct })entry_price — checks if mark price ≤ entry × (1 − pct/100)

Unrealized PnL is not persisted between ticks — it is computed on demand in the UI by comparing the current mark price against the stored entry price and quantity.

Closing a Position

A position is fully or partially closed when HP.sell(pct) is called. The sequence mirrors the open flow:

  1. Strategy calls HP.sell(100) — sell 100% of the position.
  2. Trigger snapshot is captured (mark price, condition rows).
  3. Broker checks: is there an open position? If not, the SELL is rejected silently.
  4. If a position exists:
    • Paper mode: fills at current mark price with no fee.
    • Live mode: places a MARKET sell order for qty × (pct/100) units.
  5. If pct = 100 (or the resulting remaining quantity rounds to zero), the position row is updated to status: 'closed' with close_price and closed_at.
  6. A project_trades row is inserted for the SELL side.
  7. TRADE_TRIGGER and TRADE_EXECUTED logs are written.

PnL Calculation

Realized PnL for a closed position is computed as:

PnL formula
pnl_usd = (close_price - entry_price) × qty_sold - fees
pnl_pct = (close_price - entry_price) / entry_price × 100

In paper mode, fees are always zero. In live mode, Binance fees are deducted from the fill result. The project_trades row stores both the gross fill value and the net amount after fees.

MetricSource
Entry priceActual fill price of the BUY order (mark price in paper, exchange fill in live)
Close priceActual fill price of the SELL order
QuantityUnits of base asset held
FeesFrom TRADE_EXECUTED.fees.amount in the log (zero in paper mode)

Partial Sells

Calling HP.sell(50) sells half the position. The position row remainsopen with qty reduced to the remaining amount. A SELL trade row is created for the portion sold.

The position is only marked closed when:

  • A 100% SELL is executed, or
  • A partial SELL results in a remaining quantity that rounds to zero.
⚠️
Partial sells are supported but keep the position open. If your strategy uses partial exits (e.g. HP.sell(50) at TP1 then HP.sell(100)at TP2), make sure your exit logic accounts for the fact that IN_POSITION()remains true after the first partial sell.

The Positions Tab

The Positions tab inside each project shows all trades — both open and closed — sorted by most recent first. Each row displays:

ColumnDescription
SideBUY or SELL
SymbolTrading pair (e.g. BTCUSDT)
SizeUSD value of the order
Entry / Exit PriceFill price for the trade
PnLRealized PnL for SELL trades (green = profit, red = loss)
TimeTimestamp of the trade
StatusFILLED, REJECTED, or PARTIALLY_FILLED

Clicking any row opens the Trade Detail Modal, which shows the full TRADE_TRIGGER condition table and TRADE_EXECUTED fill details for that specific trade.

How Trades Are Stored

Each trade action creates or updates two database records:

TableCreated onKey fields
project_positionsBUY executionproject_id, symbol, status (open/closed), entry_price, qty, entry_time, close_price, closed_at
project_tradesEvery BUY or SELLproject_id, symbol, side, fill_price, qty, fee, trigger_log_id, executed_log_id
project_logsEvery BUY or SELL (×2)TRADE_TRIGGER log (conditions + trigger price) and TRADE_EXECUTED log (fill details + slippage)

The trade row links to both log entries via trigger_log_id andexecuted_log_id. The position row links to the most recent trade via its own foreign key. This structure makes it possible to reconstruct the full context of any trade — conditions, prices, fees, and slippage — from a single trade ID.