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.
| State | Meaning |
|---|---|
open | BUY has been executed, position is active. No new BUY is permitted until this closes. |
closed | SELL 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:
- The strategy calls
HP.buy(500)— buy $500 worth. - The runner captures the trigger snapshot (mark price, condition rows) synchronously.
- The broker checks: is there an existing open position for this project + symbol? If yes, the BUY is rejected silently.
- 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).
- A
project_positionsrow is inserted withstatus: 'open',entry_price,qty, andentry_time. - A
project_tradesrow is inserted for the BUY side. - TRADE_TRIGGER and TRADE_EXECUTED logs are written and linked to the trade row.
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:
| Function | What 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:
- Strategy calls
HP.sell(100)— sell 100% of the position. - Trigger snapshot is captured (mark price, condition rows).
- Broker checks: is there an open position? If not, the SELL is rejected silently.
- 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.
- If
pct = 100(or the resulting remaining quantity rounds to zero), the position row is updated tostatus: 'closed'withclose_priceandclosed_at. - A
project_tradesrow is inserted for the SELL side. - TRADE_TRIGGER and TRADE_EXECUTED logs are written.
PnL Calculation
Realized PnL for a closed position is computed as:
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.
| Metric | Source |
|---|---|
| Entry price | Actual fill price of the BUY order (mark price in paper, exchange fill in live) |
| Close price | Actual fill price of the SELL order |
| Quantity | Units of base asset held |
| Fees | From 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.
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:
| Column | Description |
|---|---|
| Side | BUY or SELL |
| Symbol | Trading pair (e.g. BTCUSDT) |
| Size | USD value of the order |
| Entry / Exit Price | Fill price for the trade |
| PnL | Realized PnL for SELL trades (green = profit, red = loss) |
| Time | Timestamp of the trade |
| Status | FILLED, 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:
| Table | Created on | Key fields |
|---|---|---|
project_positions | BUY execution | project_id, symbol, status (open/closed), entry_price, qty, entry_time, close_price, closed_at |
project_trades | Every BUY or SELL | project_id, symbol, side, fill_price, qty, fee, trigger_log_id, executed_log_id |
project_logs | Every 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.