Skip to content

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 a LocalTransport data 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 CloudTransport lands 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 = strix.init()
log.info("strix session %s", session.session_id)

Session lifetime events

Two first-class events frame every session in the log:

  • SessionStarted — emitted by init. Carries the seeded positions, seeded open orders, and risk config.
  • SessionEnded — emitted by the next init (which closes the prior session). Carries a reason string.

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 at max(seq) + 1.
  • strix.init(transport=...) closes the still-open session (appending SessionEnded with reason="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=...) raises NoActiveSessionError. 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 init once at market open. Crashes during the day → resume until close, then init again the next morning. Each session = one trading day in the dashboard.
  • Per-strategy-run: call init at 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 init per process boot, never a resume. 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_id reachable.
  • Sessions are not analytics buckets yet. The seam exists (SessionEnded is where analytics hooks fire); the computation lands post-v1.