Skip to content

Positions

Strix maintains a position book per session: one Position per symbol you've traded. Positions update automatically whenever you call strix.ingest_execution(...).

The model

A Position has four fields. The SDK exposes them as an immutable value type — Python uses a frozen dataclass, other SDKs use the equivalent immutable record type.

Field Type Notes
symbol string Trading symbol.
qty decimal Signed: positive long, negative short, zero flat.
avg_price decimal, optional Average entry price of the currently held size. Unset when qty == 0.
last_price decimal, optional Price of the most recent execution on this symbol. Unset when flat or when the latest execution arrived without a price.

Sign convention:

  • qty > 0 — long.
  • qty < 0 — short.
  • qty == 0 — flat.

avg_price is the average entry price of the currently held size. It is unset whenever qty == 0 (no position, no entry price), and after any execution that has no price field set.

last_price is the price of the most recent execution that updated this symbol. It complements avg_price (VWAP across fills): when an order builds up across several partials, last_price tells you where the latest print landed while avg_price reports the cost basis. Reset rules are stricter than avg_price:

  • qty == 0 (flat) → None (matches avg_price).
  • Execution arrived without a price field → None (Strix logs a WARNING; brokers should always report a fill price).
  • Trust-mode reconcile (PositionAdjusted): preserves the prior last_price on a qty change (the most recent observed print is unchanged), resets to None only when the new qty is flat.

Reading positions

positions = strix.positions()
# -> list[Position], one entry per symbol that has ever traded in the session

The returned list includes zero-quantity entries for symbols you previously held and are now flat. This is intentional — a flat position is information ("I closed AAPL today"), not absence of information. Filter if you only want non-zero:

open_positions = [p for p in strix.positions() if p.qty != 0]

How avg_price updates

The math is "average across the current direction, reset on flip". Specifically, for an execution with signed delta delta = +qty (BUY) or -qty (SELL):

new_qty = prev.qty + delta

Then avg_price evolves according to this matrix:

Situation New avg_price
new_qty == 0 (now flat) None
Execution has no price field unchanged from prev.avg_price
Was flat (prev.qty == 0) or no prior price execution price
Direction flipped (prev long → new short, or vice versa) execution price (reset, the residual is new entry)
Adding to existing direction (long+BUY or short+SELL) weighted average over both legs
Reducing existing direction (long+SELL or short+BUY, no flip) unchanged (the entry price of what you still hold is unchanged)

Examples in code:

# Long 100 @ 150, then buy 100 more at 160 -> avg = 155
strix.ingest_execution(Execution(order_id="o1", symbol="AAPL", side=Side.BUY, qty=100, price="150"))
strix.ingest_execution(Execution(order_id="o2", symbol="AAPL", side=Side.BUY, qty=100, price="160"))
# positions() -> Position(symbol="AAPL", qty=Decimal("200"), avg_price=Decimal("155"))

# Reduce — sell half at 200; avg_price unchanged (still represents the 100 you still own at 155)
strix.ingest_execution(Execution(order_id="o3", symbol="AAPL", side=Side.SELL, qty=100, price="200"))
# positions() -> Position(symbol="AAPL", qty=Decimal("100"), avg_price=Decimal("155"))

# Flip — sell another 150, now short 50 at 200 (the new entry, not the long's avg)
strix.ingest_execution(Execution(order_id="o4", symbol="AAPL", side=Side.SELL, qty=150, price="200"))
# positions() -> Position(symbol="AAPL", qty=Decimal("-50"), avg_price=Decimal("200"))

The reduce-leg rule is the one to internalize: selling part of a long position does not change the avg_price of the remaining long. That price represents your cost basis on what you still hold, not a running VWAP across all your trades.

Realized P&L

Not computed in v1. The position book deliberately doesn't track realized P&L counters; that's analytics work that lands when the dashboard does. If you need it today, derive it yourself from the event log or from your broker's P&L feed.

What about unpriced executions?

Execution.price is optional. A common reason to omit it: your broker reports the fill in two stages and the price arrives later, or the venue settled an opening cross at a price you don't have yet.

When you ingest an unpriced execution:

  • qty updates as normal.
  • avg_price is left unchanged from the previous state.
  • last_price is reset to None (Strix has no observation to record), and a WARNING is logged via the strix logger. Brokers should always report a fill price — investigate the integration if this recurs.

If you later get the price, you cannot retroactively fix the avg — execution events are immutable. The pragmatic options are (a) wait for the price before ingesting, or (b) accept slight avg_price drift and rely on broker-side P&L for accounting. Strix is the trader's view, not the books-of-record.

Position limits and projected positions

The position_limits risk check compares against the projected position after a full fill:

projected = abs(current_qty + signed_delta_of_this_order)

So a long 400 with a limit=500 allows a further buy of 100 (projected 500, OK) but not 101 (projected 501, blocked). It also allows a sell of 900 — projected abs(400 - 900) = 500, OK, because the rule is a size limit, not a direction limit. See Risk controls for the full story.