Skip to main content

A lightweight Python engine for simulating trading account balances, order states, and exchange-style constraints.

Project description

trading-state

trading-state is a small, focused Python library that models the dynamic state of a trading account: balances, positions, open orders, and PnL — under realistic exchange-like constraints.

It is passive: it never schedules, polls, or talks to an exchange. Every state change is a caller-driven write. It is synchronous: all reads return immediately. It separates state from strategy — the library owns the truth about what is held, ordered, filled, settled, and unsettled, while the caller owns the question "what should we do next?".

Highlights:

  • All internal arithmetic uses Decimal.
  • OrderTicket value objects are frozen; filters produce normalized copies via dataclasses.replace.
  • Exchange / local async is reconciled by an internal ReconciliationManager; callers query state.exposure(asset, include_unsettled_inflow=..., include_unsettled_outflow=...) and state.unsettled(asset) to decide what they want to count.
  • State never raises a business-level exception. Stale updates (status / time / filled regression) are silently dropped and emit a diagnostic STALE_UPDATE event. Protocol-side errors surface through ValueOrException returns from the trading_state.binance.* adapters.

Install

$ pip install trading-state

Usage

1) Initialize state and market data

from datetime import datetime
from decimal import Decimal

from trading_state import (
    Balance,
    Symbol,
    TradingConfig,
    TradingState,
)

config = TradingConfig(
    account_currency='USDT',
    alt_account_currencies=('USDC',),
    benchmark_assets=('BTC',),
)
state = TradingState(config)

state.set_symbol(Symbol('BTCUSDT', 'BTC', 'USDT'))
state.set_price('BTCUSDT', Decimal('30000'))
state.set_notional_limit('BTC', Decimal('100000'))

state.set_balances([
    Balance('USDT', Decimal('10000'), Decimal('0'), datetime.now()),
])

Balance.time is required: it drives the reconciliation between order fills (from the order channel) and balance snapshots (from the balance channel).

2) Build a ticket, register it, and drive the lifecycle

from trading_state import (
    LimitOrderTicket,
    OrderSide,
    OrderStatus,
    TimeInForce,
)

btcusdt = state.get_symbol('BTCUSDT')   # the Symbol registered above

ticket = LimitOrderTicket(
    symbol=btcusdt,
    side=OrderSide.BUY,
    quantity=Decimal('0.2'),
    price=Decimal('30000'),
    time_in_force=TimeInForce.GTC,
)

# add_order returns (exc, Order). On filter rejection you get the
# exception back via the value — state is never raised at.
exc, order = state.add_order(ticket, data={'strategy': 'momentum'})
assert exc is None

# The caller drives the state machine explicitly. update_order requires
# every keyword (pass None for fields you aren't touching).
state.update_order(
    order,
    status=OrderStatus.SUBMITTING,
    updated_at=None,
    id=None,
    filled_quantity=None,
    quote_quantity=None,
    commission_asset=None,
    commission_quantity=None,
)

state.update_order(
    order,
    status=OrderStatus.CREATED,
    updated_at=datetime.now(),
    id='order-1',
    filled_quantity=Decimal('0.1'),
    quote_quantity=Decimal('3000'),
    commission_asset=None,
    commission_quantity=None,
)

state.set_balances([
    Balance('BTC', Decimal('0.1'), Decimal('0'), datetime.now()),
])

state.update_order(
    order,
    status=OrderStatus.FILLED,
    updated_at=datetime.now(),
    id=None,
    filled_quantity=Decimal('0.1'),
    quote_quantity=Decimal('3000'),
    commission_asset=None,
    commission_quantity=None,
)

3) Query exposure and unsettled flow

exc, exposure_now = state.exposure(
    'BTC',
    include_unsettled_inflow=True,    # count fills the exchange has confirmed but balance has not yet caught up to
    include_unsettled_outflow=False,  # do not deduct unsettled outflows here
)

exc, flow = state.unsettled('BTC')    # diagnostic only — do not drive trading decisions from this

Each include_unsettled_* flag is required: callers must state at every call site which components of the holding they want.

4) Best-effort allocation across alt account currencies

state.set_alt_currency_weights((
    (Decimal('0.5'),),   # BUY weights against `alt_account_currencies`
    (Decimal('0'),),     # SELL weights
))

# allocate splits a canonical ticket across the configured account
# currencies and returns filter-applied sub-tickets. When it can't
# split (weights unset, no eligible bucket, unsupported ticket kind,
# etc.) it returns [ticket] so the caller has nothing to special-case.
sub_tickets = state.allocate(ticket)
for t in sub_tickets:
    exc, order = state.add_order(t)

5) Subscribe to events and diagnostics

from trading_state import StaleUpdate, TradingStateEvent

state.on(
    TradingStateEvent.PERFORMANCE_SNAPSHOT_RECORDED,
    lambda snapshot: ...,
)

state.on(
    TradingStateEvent.STALE_UPDATE,
    lambda event: print('dropped stale update', event.kind, event),
)

6) Record snapshots and analyze performance

from trading_state import CashFlow

state.set_cash_flow(
    CashFlow('USDT', Decimal('1000'), datetime.now()),
)

snapshot = state.record()
from trading_state import TradingStateEvent
from trading_state.analyzer import AnalyzerType, PerformanceAnalyzer

analyzer = PerformanceAnalyzer([
    AnalyzerType.TOTAL_RETURN,
    AnalyzerType.SHARPE_RATIO.params(trading_days=365),
    AnalyzerType.MAX_DRAWDOWN,
])

state.on(
    TradingStateEvent.PERFORMANCE_SNAPSHOT_RECORDED,
    analyzer.add_snapshots,
)

results = analyzer.analyze()
total_return = results[AnalyzerType.TOTAL_RETURN].value

7) Bridging to Binance (or any exchange) via decoders

from trading_state import InvalidExchangeData
from trading_state.binance import (
    decode_order_update_event,
)

exc, decoded = decode_order_update_event(payload)
if exc is not None:
    log.error('bad executionReport', err=exc)   # caller decides:
    return                                       # raise / log / retry
client_id, updates = decoded

order = state.get_order_by_id(client_id)
if order is None:
    return                                       # unknown order
state.update_order(order, **updates)             # state silently
                                                 # drops stale data

All decoders in trading_state.binance return ValueOrException[T]; validation is embedded so callers cannot accidentally feed malformed data into state.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

trading_state-2.0.0.tar.gz (81.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

trading_state-2.0.0-py3-none-any.whl (65.1 kB view details)

Uploaded Python 3

File details

Details for the file trading_state-2.0.0.tar.gz.

File metadata

  • Download URL: trading_state-2.0.0.tar.gz
  • Upload date:
  • Size: 81.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for trading_state-2.0.0.tar.gz
Algorithm Hash digest
SHA256 f35a6e25e59d8a548121bd86d51a4248e0d72003ff2d078092d4d78e99ff3b24
MD5 2c3fbf9d622ad14dd6ed01bd8254b98a
BLAKE2b-256 d62243f6eec902c1d57558897c930500689904ce326b475a275d5c078dc88e25

See more details on using hashes here.

File details

Details for the file trading_state-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: trading_state-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 65.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.0

File hashes

Hashes for trading_state-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bb0357d0634110b0e6d433ad294775f9da6d6b83b883bab106ad9683d448cff5
MD5 c784626fcbfa362a63e932cab19c0c74
BLAKE2b-256 9504a4568da5b327c3b821cb13adb09c45a2cf1a7072b0a6c7043394de260bf4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page