Sessions¶
A session is a sequential, non-overlapping analytics window. Each session has a unique session_id (a UUIDv7) and an event log that captures every order, status change, and execution that happened inside it.
You decide what a session means:
- A trading day.
- A backtest run.
- A single strategy invocation.
- A run between two restarts.
Strix doesn't impose semantics; you pick the boundary by choosing when to call init(). The dashboard (post-v1) will compute analytics per session.
Two functions, two intents¶
# Start a NEW session. initial_state and risk apply only here.
strix.init(
*,
transport=None,
initial_state=None, # InitialState(positions=..., open_orders=...)
risk=None, # RiskSettings(max_qty_per_order=..., ...)
market_data=None, # live MarketDataAdapter; not persisted
) -> Session
# CONTINUE the existing open session. State is replayed from the event log;
# market_data is a live source and must be re-supplied.
strix.resume(*, transport, market_data=None) -> Session
Note the asymmetry: initial_state and risk live only on init. resume rebuilds session state (including risk policy) from the on-disk log, so passing them would be ambiguous — the log already contains what was configured. market_data lives on both: it's a live source, not persisted, so callers re-supply it on every resume.
These map to two distinct user intents:
| Function | Intent | Behavior |
|---|---|---|
init |
"new trading day / new strategy run / new backtest" | Closes any prior open session, carries forward open orders + positions, opens a new session. |
resume |
"I crashed, get me back to where I was" | Replays the event log of the currently-open session and continues it. Raises NoActiveSessionError if none. |
The split is deliberate. A single function with a resume=True flag would hide intent in an argument. An implicit auto-resume on init would be surprising — a user who called init expecting fresh state and got yesterday's resumed state has a hard-to-debug problem.
resume cannot resume a closed session. Once a session is closed (via the next init, or future explicit close), it's archived. A new session must be started.
Carry-forward on init¶
When init finds an open session in storage, it closes that session and seeds the new one with state that physically still exists:
| State | Carries to new session? | Reason |
|---|---|---|
Open orders (PENDING_NEW, NEW, PARTIALLY_FILLED, PENDING_CANCEL) |
Yes | Broker still has them. |
| Positions (non-zero quantity) | Yes | You still own them. |
| Realized P&L, counters, dashboard tallies | No | Belong to the closed session. |
Terminal orders (FILLED, REJECTED) |
No (live view), yes (event log audit) | Removed from active view, preserved in history. |
With persistent storage configured, this carry-forward is automatic. Strix replays the prior session's events to compute the seed state — you don't pass anything.
Overriding carry-forward with initial_state¶
When you need to take seed state from somewhere other than the prior session, pass initial_state=InitialState(...). The override is atomic: both positions and open orders come from the value you pass. There is no partial override — if you want to keep one and replace the other, you have to compose both lists yourself.
from strix import InitialState, Position
from decimal import Decimal
# First run against a brand-new data_dir, seeding from an external source.
strix.init(
transport=strix.LocalTransport(data_dir="./strix_data"),
initial_state=InitialState(
positions=[Position(symbol="AAPL", qty=Decimal("100"), avg_price=Decimal("140"))],
),
)
# Start a paper/backtest run that ignores prior live state entirely.
strix.init(
transport=strix.LocalTransport(data_dir="./strix_data"),
initial_state=InitialState(), # empty positions AND empty open orders
)
# Default — no initial_state: carry forward from the prior session.
strix.init(transport=strix.LocalTransport(data_dir="./strix_data"))
| Call | Positions seeded from | Open orders seeded from |
|---|---|---|
init() |
prior session (carry-forward) | prior session (carry-forward) |
init(initial_state=InitialState()) |
empty | empty |
init(initial_state=InitialState(positions=[...])) |
the list you passed | empty |
init(initial_state=InitialState(open_orders=[...])) |
empty | the list you passed |
Seeded open orders are taken at face value — Strix does not verify them against the broker. Use this path knowing the broker holds (or doesn't hold) what you claim.
Session IDs¶
session_id is a UUIDv7, client-generated at session start.
- Time-ordered:
ls -1 sessions/on aLocalTransportdata directory gives chronological history with no separate timestamp file. - Globally unique: same guarantees as UUIDv4.
- Cloud-friendly: better B-tree locality when indexed server-side, so the design carries forward when
CloudTransportlands post-v1.
You can read it from the session handle's session_id accessor if you need to log it or correlate to an external system.
Session lifetime events¶
Two first-class events frame every session in the log:
SessionStarted— emitted byinit. Carries the seeded positions, seeded open orders, and risk config.SessionEnded— emitted by the nextinit(which closes the prior session). Carries areasonstring.
SessionEnded.reason values:
| Value | When |
|---|---|
"explicit" |
Reserved for a future strix.close() call. |
"new-session-implicit-close" |
The most common: a fresh init closed the prior session. |
"shutdown" |
Reserved for clean-shutdown hooks. |
Treat these as first-class signal, not metadata. Any consumer (in-process now, server-side later) can derive session boundaries by scanning the event stream.
What happens after a crash¶
Two crash scenarios produce different recovery paths:
Crash mid-session¶
No SessionEnded event was written. The storage still points at the active session.
strix.resume(transport=...)replays the log and continues. The session_id is unchanged. The next event picks up atmax(seq) + 1.strix.init(transport=...)closes the still-open session (appendingSessionEndedwithreason="new-session-implicit-close") and starts a new one, carrying forward open orders and positions.
Crash between SessionEnded and the next SessionStarted¶
Prior session has a terminal event; no new session yet.
strix.resume(transport=...)raisesNoActiveSessionError. Closed sessions are not resumable.strix.init(transport=...)starts a fresh session, seeded from the closed session's final state.
In both cases, the on-disk log is the source of truth and is self-correcting on the next open.
Choosing session boundaries¶
Some common shapes:
- Per-day: call
initonce at market open. Crashes during the day →resumeuntil close, theninitagain the next morning. Each session = one trading day in the dashboard. - Per-strategy-run: call
initat the start of each strategy invocation. Useful when you compose several strategies and want per-strategy attribution. - Per-backtest: each historical run gets its own
init. The completed sessions accumulate in storage as a backtest history. - Per-process: simplest — one
initper process boot, never aresume. Crashes lose attribution boundaries but the model still works (open positions/orders carry forward into the next process's session).
There's no "right" answer; the dashboard's session granularity follows yours.
What you should not assume¶
- Sessions are not threads. The Strix SDK holds a single active session per process; concurrent multi-session use in one process is not supported in v1.
- Sessions are not order containers. An order placed in session A can be filled in session B — the execution applies to whichever session owns the order at the time. Carry-forward keeps the order's
order_idreachable. - Sessions are not analytics buckets yet. The seam exists (
SessionEndedis where analytics hooks fire); the computation lands post-v1.