trader.domain module

Typed value objects shared across the Core Service Layer.

Adapters convert third-party SDK responses into these objects so no alpaca-py or pandas type ever reaches the CLI or persistence layers. Money is always Decimal, never float.

class trader.domain.Account(account_id, cash, equity, buying_power, portfolio_value, is_paper, last_equity=Decimal('0'))[source]

Bases: object

A broker account snapshot at a point in time.

Parameters:
  • account_id (str)

  • cash (Decimal)

  • equity (Decimal)

  • buying_power (Decimal)

  • portfolio_value (Decimal)

  • is_paper (bool)

  • last_equity (Decimal)

account_id: str
cash: Decimal
equity: Decimal
buying_power: Decimal
portfolio_value: Decimal
is_paper: bool
last_equity: Decimal
class trader.domain.Bar(symbol, timestamp, open, high, low, close, volume)[source]

Bases: object

A single OHLCV candle.

Parameters:
  • symbol (str)

  • timestamp (datetime)

  • open (Decimal)

  • high (Decimal)

  • low (Decimal)

  • close (Decimal)

  • volume (int)

symbol: str
timestamp: datetime
open: Decimal
high: Decimal
low: Decimal
close: Decimal
volume: int
class trader.domain.MarketClock(is_open, next_open, next_close)[source]

Bases: object

Whether the exchange is open, and when that next changes.

The daemon sleeps on these values, so they carry the holiday and half-day schedule the broker knows about and a local 09:30–16:00 weekdays rule does not. Both timestamps are tz-aware UTC; the adapter converts and refuses naive input rather than letting a naive value reach an arithmetic comparison against an aware now.

next_open is meaningful even while the market is open — it is the following session — so the daemon must branch on is_open rather than inferring the state from the timestamps.

Parameters:
  • is_open (bool)

  • next_open (datetime)

  • next_close (datetime)

is_open: bool
next_open: datetime
next_close: datetime
class trader.domain.OrderSide(*values)[source]

Bases: StrEnum

Long-only order directions.

BUY = 'buy'
SELL = 'sell'
class trader.domain.Position(symbol, quantity, avg_entry_price, current_price, market_value, unrealized_pl)[source]

Bases: object

A single open position.

Parameters:
  • symbol (str)

  • quantity (Decimal)

  • avg_entry_price (Decimal)

  • current_price (Decimal)

  • market_value (Decimal)

  • unrealized_pl (Decimal)

symbol: str
quantity: Decimal
avg_entry_price: Decimal
current_price: Decimal
market_value: Decimal
unrealized_pl: Decimal
class trader.domain.ProtectedEntry(entry, protective_leg)[source]

Bases: object

An atomic OTO entry, together with the protective leg attached to it.

Two orders come back from one submit_limit_buy_with_stop request, and both have to reach trades. The parent’s row is what makes the entry fill attributable; the leg’s row is what makes the exit fill attributable, and a stop-driven exit is this app’s dominant exit path. Returning only the parent — which is what this type replaced — meant raw.legs was discarded at the adapter boundary and the leg was never recorded, so reconcile_fills saw its eventual fill as “not ours”, the round trip stayed open forever, total_realized_pl() reported 0 against a real loss, and the symbol then wedged on “a round trip is already open”. That is why this exists at all.

A small result object rather than a protective_leg field on SubmittedOrder, for two reasons. SubmittedOrder is the shape of one broker order and is persisted as one row; a nested order inside it would be meaningless for every other order type and silently dropped by every repository that writes one. And a distinct return type makes the caller name the leg to ignore it, so the next caller cannot repeat the original mistake by accident. Same reasoning, and same shape, as RatchetedStop.

protective_leg is None only when the broker’s response carried no leg at all. That is not a normal outcome — it means the entry may be unprotected — so the adapter logs it loudly rather than treating it as an ordinary absence, and the end-of-cycle §8 check is what catches the resulting position.

Parameters:
entry: SubmittedOrder
protective_leg: SubmittedOrder | None
class trader.domain.Quote(symbol, price, as_of)[source]

Bases: object

The most recent observed price for a symbol.

Parameters:
  • symbol (str)

  • price (Decimal)

  • as_of (datetime)

symbol: str
price: Decimal
as_of: datetime
class trader.domain.SubmittedOrder(order_id, symbol, side, quantity, order_type, status, submitted_at, limit_price=None, trail_percent=None, stop_price=None, filled_qty=None, filled_avg_price=None, filled_at=None)[source]

Bases: object

An order the broker accepted, as a domain object.

quantity is Decimal to match the rest of the money surface even though this app submits whole shares only: the broker reports fractional quantities for positions it acquired other ways, and truncating here would misreport what actually exists at the broker.

Parameters:
  • order_id (str)

  • symbol (str)

  • side (OrderSide)

  • quantity (Decimal)

  • order_type (str)

  • status (str)

  • submitted_at (datetime)

  • limit_price (Decimal | None)

  • trail_percent (Decimal | None)

  • stop_price (Decimal | None)

  • filled_qty (Decimal | None)

  • filled_avg_price (Decimal | None)

  • filled_at (datetime | None)

order_id: str
symbol: str
side: OrderSide
quantity: Decimal
order_type: str
status: str
submitted_at: datetime
limit_price: Decimal | None
trail_percent: Decimal | None
stop_price: Decimal | None
filled_qty: Decimal | None
filled_avg_price: Decimal | None
filled_at: datetime | None
class trader.domain.TradableAsset(symbol, tradable, asset_class, exchange, name=None)[source]

Bases: object

What the broker knows about a symbol.

tradable=False and “the broker has never heard of it” are different answers and discovery treats them the same way — but only because both mean “do not order this”. Keeping them distinct here means a future caller can tell an OTC halt from a typo.

Parameters:
  • symbol (str)

  • tradable (bool)

  • asset_class (str)

  • exchange (str)

  • name (str | None)

symbol: str
tradable: bool
asset_class: str
exchange: str
name: str | None

The issuer’s name as the broker states it — “Apple Inc. Common Stock”. Optional because a broker may omit it and because nothing depends on it being present: trader.news.aliases derives news-matching aliases from it when it is there, and falls back to bare-ticker matching when it is not. It reached this object only because Alpaca already sends it and the mapper was discarding it (issue #6) — no extra call was added for it.