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(matchesavg_price).- Execution arrived without a
pricefield →None(Strix logs a WARNING; brokers should always report a fill price). - Trust-mode reconcile (
PositionAdjusted): preserves the priorlast_priceon a qty change (the most recent observed print is unchanged), resets toNoneonly 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:
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):
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:
qtyupdates as normal.avg_priceis left unchanged from the previous state.last_priceis reset toNone(Strix has no observation to record), and aWARNINGis logged via thestrixlogger. 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:
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.