Skip to content

Persistence

Strix's persistence is a pluggable port (Transport). Two implementations ship:

Backend When to use Survives process exit?
InMemoryTransport (default) Tests, short scripts, anything ephemeral No
LocalTransport (configured with data_dir) Long-running algos, anything you want to recover Yes

A future CloudTransport will compose LocalTransport as a write-ahead log + async push to a hosted dashboard. It's deferred to post-v1; the port shape is designed to accept it without a breaking change.

Choosing a backend

# Ephemeral — state lives until process exit.
strix.init()
# equivalent to:
strix.init(transport=strix.InMemoryTransport())

# Persistent — survives crashes and restarts.
transport = strix.LocalTransport(data_dir="./strix_data")
strix.init(transport=transport, risk=strix.RiskSettings(max_qty_per_order=1000))

If you hold a reference to the same InMemoryTransport across multiple init calls within one process, carry-forward still works — the backend keeps the event log in memory across sessions. You just lose everything when the process exits.

mem = strix.InMemoryTransport()
strix.init(transport=mem)
# ... trade ...
strix.init(transport=mem)   # closes prior session, carries forward open orders + positions

LocalTransport on disk

<data_dir>/
  .strix-storage              # marker: {"format_version": 1}
  strix.lock                  # OS file lock; auto-released on process exit
  active_session              # one line: <session_id> of currently-open session (empty if none)
  sessions/
    <session_id>/             # UUIDv7 dirname → sorts chronologically
      events.jsonl            # append-only event log

Properties worth knowing:

  • data_dir is required, no default. Algo users have opinions about where state lives (mounted volumes, dedicated drives, network shares). Strix won't pick for you.
  • Marker file (.strix-storage). Strix refuses to write into a non-empty directory without a marker, so you can't accidentally point it at your project root. Empty directories get a marker created on first use.
  • One process per data_dir. A strix.lock file is acquired with an OS-level file lock (POSIX flock on Unix, LockFile on Windows). A second process opening the same data_dir raises StorageLockedError. Within one process you can run as many sequential sessions as you want.
  • fsync on every event. Each append() writes one JSON line, flushes, and fsyncs. ~1–5 ms per event, acceptable for non-HFT algo trading. The "durable on return" contract is what lets a CloudTransport layer a WAL on top later without changing semantics.
  • Closed sessions persist forever. Audit-trail value beats disk cost for the trading use case. Enumerate with strix.list_sessions(data_dir=...); remove with rm -rf sessions/<id>/ if you need to reclaim space (archive_session / delete_session helpers are deferred).

Event schema (JSONL)

Each line in events.jsonl is a JSON object describing one event. The line is self-describing — there's no per-session header.

Every event has:

Field Type Notes
type string One of the event types listed below.
session_id string UUIDv7.
seq int Monotonic per session, starts at 0.
ts string (ISO 8601) Client wall clock; informational.
schema_version int Currently 1.
...payload Event-type-specific fields.

(session_id, seq) is the dedupe key. A separate event_id UUID was considered and rejected — (session_id, seq) is already globally unique.

Decimal-valued fields are serialized as JSON strings (e.g. "100.50", not 100.50) to preserve precision across the JSON boundary. Parse them back through decimal.Decimal on the consumer side. Timestamps are ISO 8601 strings (datetime.fromisoformat-compatible). Nullable fields use JSON null.

Event types emitted in v1:

Category type values
Session lifecycle SessionStarted, SessionEnded
Orders OrderCreated, OrderStatusChanged, CancelAttemptFailed
Executions ExecutionApplied, ExecutionAnomalyDetected
Risk config RiskSettingsChanged, RiskBreach
P&L PnLSnapshot
Stop-loss StopLossTriggered, StopLossRecovered, StopLossManuallyOverridden
Broker reconcile PositionAdjusted, ReconciliationCompleted

Per-event payload fields

Beyond the common envelope (type, session_id, seq, ts, schema_version), each event carries:

SessionStarted - reason: str — always "explicit-init" in v1 (every session starts via strix.init) - seeded_positions: list[Position] — positions carried forward or seeded via InitialState - seeded_open_orders: list[Order] — open orders carried forward - risk: RiskSettings — initial risk policy (serialized as a dict; see source for field list) - config: SessionConfig — initial behavior policy

SessionEnded - reason: str"new-session-implicit-close", "explicit", etc.

OrderCreated - order: Order — full Order snapshot at creation

OrderStatusChanged - order_id: str - status: OrderStatus — string enum value - reject_reason: str | null — populated when transitioning to REJECTED

ExecutionApplied - execution: Execution — the applied fill, with order_id, symbol, side, qty, price (Decimal-strings), timestamp, execution_id

ExecutionAnomalyDetected - execution: Execution - category: str"missing-order", "symbol-mismatch", "side-mismatch", "terminal-order", or "overfill" - detail: str — human-readable description - order_id_ref: str | null — the order_id Strix tried to match

CancelAttemptFailed - order_id: str - prior_status: OrderStatus — the status the order was reverted to - reason: str — exception message from the user's cancel block

RiskSettingsChanged - risk: RiskSettings — full new policy (replaces prior)

RiskBreach - order_id: str — the order that breached the limit (still yielded — emitted only under on_breach="warn", the default; under "raise" the order is rejected with StrixRiskError and no event is written) - symbol: str - reason: str — which limit was breached and how

PnLSnapshot - realized: str (Decimal) — cumulative session realized P&L - unrealized: str (Decimal) — sum of by-symbol unrealized - by_symbol: dict[str, dict] — one entry per non-zero position with keys: - qty: str (Decimal) - avg_price: str | null (Decimal) - mark: str | null (Decimal) — None if no mark_to_market has reported a price for this symbol - unrealized: str (Decimal) — per-symbol contribution to the total

StopLossTriggered / StopLossRecovered - scope: str"session" or "position" - subject: str | null — symbol for position scope; null for session scope - threshold: str (Decimal) — the threshold that fired (or recovery_threshold on Recovered) - observed_pnl: str (Decimal) — the P&L observed at the trigger

StopLossManuallyOverridden - cleared_halts: list[[scope, subject]] — halts that were active at override time - reason: str — caller-supplied reason

PositionAdjusted - symbol: str - prev_qty: str (Decimal), new_qty: str (Decimal) - prev_avg_price: str | null, new_avg_price: str | null (Decimals) - reason: str — typically "broker_reconciliation"

ReconciliationCompleted - marker: str | null — adapter's next_marker for the next reconcile call - executions_ingested: int, executions_skipped_duplicate: int - position_mismatches: list[PositionMismatch], order_mismatches: list[OrderMismatch]

For a typed reader, use strix.replay(data_dir=..., session_id=...) — it parses each line into the corresponding Python class and yields SessionEvent instances you can isinstance-check.

You can also cat sessions/<id>/events.jsonl | jq to inspect any session. Don't write to these files yourself.

Durability and the cloud-forward contract

The port contract makes one strong guarantee: append() is durable on return. When append() returns without raising, the event is on disk (or, in a future cloud impl, in the local WAL with the cloud push queued).

This is the constraint that lets the same LocalTransport serve as the v1 backend and the future cloud-WAL — no throwaway code, no migration story between the two.

What the contract does not guarantee:

  • That every Execution is durable independent of the next event. (Strix appends one event per public mutation, but you don't get a transactional batch of "order + execution" — they're separate events.)
  • That fsync survives unfriendly hardware (disks lying about write-through, etc.). Standard fsync caveats apply.
  • That replay() reflects events written by another process holding the lock. Strix is single-writer per data_dir.

Closing the transport cleanly

LocalTransport holds two OS resources: the lockfile and the currently-open event log file. They auto-release when the process exits, but you can release them eagerly via transport.close():

transport = strix.LocalTransport(data_dir="./strix_data")
try:
    strix.init(transport=transport)
    # ...
finally:
    transport.close()

In long-lived algos this matters mostly so a sibling process can pick up the data directory cleanly. In short scripts the auto-cleanup on process exit is fine.

transport.flush() is also exposed and forces an fsync of the currently-open log file. Useful before a known-risky operation, but append() already fsyncs, so flush is rarely necessary.

If you need to delete or move a data_dir (test reset, archival, etc.), call transport.close() first. On Windows the lockfile is held open until close — shutil.rmtree and os.replace will fail with a permission error otherwise. POSIX is more forgiving but the cleanest pattern is still close-before-delete:

transport = strix.LocalTransport(data_dir="./_test_data")
try:
    # ... run test ...
finally:
    transport.close()
shutil.rmtree("./_test_data")  # safe now

Errors you can see

Error Why it's raised
NoActiveSessionError strix.resume(transport=...) called when no open session exists.
StorageLockedError Another process holds the data_dir lock.
StorageVersionError .strix-storage marker has a format_version this SDK doesn't support.
StorageCorruptError Internal inconsistency: bad pointer, malformed line, unknown event type, seq mismatch on append, etc.

All inherit from StrixStorageError, which inherits from StrixError. Catch the base if you want a single handler; catch specifics if you want to react differently. (Names shown are the Python SDK's; other SDKs expose equivalent types — see the API reference.)

What's deferred to post-v1

  • CloudTransport implementation (port shape is locked, ready to receive it).
  • Compression (events.jsonl.zst), session metadata sidecars, analytics cache files. The on-disk layout reserves room.
  • strix.archive_session(id), strix.delete_session(id). (strix.list_sessions(data_dir=...) shipped — read-only enumeration of sessions in a data_dir.)
  • Backpressure / buffering policy for the cloud pusher.
  • Encryption at rest (TLS handles transport; rest is a server-side concern in the cloud impl).

See DESIGN.md §6–§8 for the full architecture rationale.