trader.brokers.base module

The broker interfaces the Core Service Layer depends on.

Defining these as Protocols keeps the Core free of any alpaca-py import, so a second broker (or a fake, in tests) can be substituted freely.

Two Protocols rather than one, deliberately. BrokerAdapter is the read-only surface Slice 1 shipped; OrderPlacingBroker adds order placement. A collaborator that only needs to read asks for the first and is then structurally unable to place an order — which is worth more here than the convenience of a single interface.

class trader.brokers.base.BrokerAdapter(*args, **kwargs)[source]

Bases: Protocol

Read-only broker operations.

get_account()[source]

Return the current account summary.

Return type:

Account

get_positions()[source]

Return all open positions.

Return type:

list[Position]

get_clock()[source]

Return the exchange’s open state and its next transitions.

On the read Protocol deliberately: the daemon needs it on every wake-up, and keeping it here means the retrying read wrapper can cover it without ever holding an order-placing method.

Return type:

MarketClock

get_asset(symbol)[source]

What the broker knows about symbol, or None if it does not.

A read, so it lives here rather than on OrderPlacingBroker — which is also what lets the retrying wrapper cover it.

Parameters:

symbol (str)

Return type:

TradableAsset | None

class trader.brokers.base.OrderPlacingBroker(*args, **kwargs)[source]

Bases: BrokerAdapter, Protocol

Read operations plus order placement.

Note that runtime_checkable checks method names only, never signatures, so isinstance against this Protocol proves less than it appears to. The signatures below are enforced by the tests that exercise real callers, not by the runtime.

submit_limit_buy(symbol, quantity, limit_price)[source]

Submit a DAY limit buy. Raises OrderError on failure.

Parameters:
  • symbol (str)

  • quantity (int)

  • limit_price (Decimal)

Return type:

SubmittedOrder

submit_limit_buy_with_stop(symbol, quantity, limit_price, stop_price)[source]

Submit a GTC limit buy with a protective stop attached atomically.

One OTO request rather than an entry followed by a separate stop, so the broker holds protection from the instant of fill and nothing depends on this app running again to place a second order. Requirements §8.

Returns both orders (ProtectedEntry), not just the parent. One request creates two orders and the caller has to record both: the leg’s trades row is what makes a stop-driven exit — this app’s dominant exit path — attributable at all. Returning a bare SubmittedOrder here is what discarded the leg the first time.

GTC, not DAY: measured 2026-08-10, an OTO’s protective leg inherits the parent’s time-in-force exactly, so a DAY parent produces a DAY stop that expires at the close and leaves the position naked overnight. The DAY intent for the entry itself is enforced by the app instead — see OrderExecutor.cancel_stale_entries.

Parameters:
  • symbol (str)

  • quantity (int)

  • limit_price (Decimal)

  • stop_price (Decimal)

Return type:

ProtectedEntry

replace_stop_price(order_id, stop_price)[source]

Change an open stop order’s trigger price with a single request.

Alpaca cannot attach a trailing stop to an OTO leg — StopLossRequest accepts only stop_price/limit_price — so the fixed stop this app places is ratcheted upward over time by replacing it in place. This app issues exactly one request and never itself cancels the old order and then places a new one, so no gap is introduced by code on this side. Whether Alpaca’s replace is observably gapless on the broker’s side is unverified: measured 2026-08-10, TradingClient.replace_order_by_id is documented as a cancel-and-resubmit under the hood that returns a distinct new order id (the old one transitions to status replaced), which is evidence against gaplessness, not for it. Confirming either way needs a live protective order to be replaced and watched — the operator’s call, not a claim to assert from the docs.

Parameters:
  • order_id (str)

  • stop_price (Decimal)

Return type:

SubmittedOrder

submit_market_sell(symbol, quantity)[source]

Submit a market sell to close a long.

Parameters:
  • symbol (str)

  • quantity (int)

Return type:

SubmittedOrder

submit_trailing_stop_sell(symbol, quantity, trail_percent)[source]

Submit a standalone GTC trailing-stop sell.

Parameters:
  • symbol (str)

  • quantity (int)

  • trail_percent (Decimal)

Return type:

SubmittedOrder

cancel_order(order_id)[source]

Cancel an open order by broker id.

Parameters:

order_id (str)

Return type:

None

get_open_orders(symbol=None)[source]

Open orders, optionally filtered to one symbol.

Parameters:

symbol (str | None)

Return type:

list[SubmittedOrder]

get_filled_orders(since=None)[source]

Recently filled or closed orders. A read; not gated.

Parameters:

since (datetime | None)

Return type:

list[SubmittedOrder]

get_order_by_id(order_id)[source]

One order’s current state at the broker, or None if it has none.

A read, and not gated — but on this Protocol rather than BrokerAdapter, next to the two order-listing reads that already live here. Two reasons, and the second is load-bearing. An order id only exists because an order was placed, so nothing that merely reads the account has any use for this. And RetryingBroker satisfies BrokerAdapter and deliberately holds no submit_* method: putting a method on the read Protocol obliges that wrapper to grow it, which is why get_open_orders and get_filled_orders are here too.

None means the broker denies knowing this order id — a 404, not a failure to ask. Anything that prevented the question from being answered raises OrderError instead, because the caller (issue #1’s settle_terminal_orders) settles a row only on positive evidence of a terminal, non-filling outcome, and “I could not tell” must never be able to masquerade as “it never filled”. Absence from get_open_orders does not mean an order never filled — it usually means it did — which is precisely why this per-id read exists rather than a diff against the open-orders list.

Parameters:

order_id (str)

Return type:

SubmittedOrder | None