trader.persistence.trades module

Repository for submitted orders (requirements §10).

A dry-run row is a real row: it records that the pipeline decided to act, and what it would have sent. alpaca_order_id is NULL and status is “dry_run”, so it can never be read as an order that reached a broker.

class trader.persistence.trades.TradeRepository(session_factory)[source]

Bases: object

Writes and reads the trades table.

Parameters:

session_factory (sessionmaker[Session])

record_submission(order, strategy_id, is_paper)[source]

Record an order the broker accepted. Returns the new row id.

submitted_at is the broker’s timestamp, not ours: that is when the order actually reached the market, whereas now() is when this process got around to writing it down.

Parameters:
Return type:

int

record_dry_run(symbol, side, quantity, price, strategy_id, is_paper)[source]

Record an order that was intended but deliberately not submitted.

Parameters:
  • symbol (str)

  • side (OrderSide)

  • quantity (int)

  • price (Decimal)

  • strategy_id (str | None)

  • is_paper (bool)

Return type:

int

mark_settled(order_ids, settled_at)[source]

Stamp settled_at on every trade with one of these broker order ids.

Returns how many rows were stamped. Called once a side of a round trip is committed, so that every contributing fill — not only the first, which is all round_trips.entry_trade_id/exit_trade_id can name — is skipped when the broker replays it. Deliberately not called at fill time: a partial exit that has not yet reached flat must stay unsettled so the next cycle re-accumulates it.

Already-stamped rows keep their original timestamp: settlement happens once, and re-stamping would make the column drift forward the same way Trade.filled_at once did.

Parameters:
  • order_ids (Sequence[str])

  • settled_at (datetime)

Return type:

int

oldest_unsettled_submitted_at()[source]

When the oldest still-outstanding order was submitted.

This is the correct lower bound for get_filled_orders(since=…), and the reasoning is worth keeping next to the query: Alpaca’s GetOrdersRequest(after=…) filters on submission time, not fill time. Bounding on the newest recorded fill instead — the obvious choice — would exclude an order submitted days ago that fills today, and that is precisely what a GTC trailing stop is. Trailing stops are this app’s dominant exit path, so missing one means a round trip that never closes.

Unsettled rather than unfilled: settled_at is stamped when a fill is folded into round-trip state, so a partially-exited position stays in the window until it reaches flat.

Dry-run rows are excluded. They have no alpaca_order_id, so no broker order can ever match them and none will ever settle — including them would pin the window open permanently after a single run-once –dry-run.

Returns None when nothing is outstanding, which is the steady state and the case the bound exists for: the caller then asks for a short recent window instead of the broker’s whole recent history.

Return type:

datetime | None

unsettled_broker_order_ids()[source]

Broker order ids for every still-outstanding row, oldest first.

The same population oldest_unsettled_submitted_at() takes its minimum over — deliberately, because these two must agree about what “still outstanding” means or issue #1’s sweep would chase rows that do not actually hold the window open, and miss the one that does. Dry-run rows are excluded here for the same reason as there: they carry no alpaca_order_id, so no broker order can ever match them and asking about them is meaningless.

Oldest first, because the row with the earliest submitted_at is exactly the one pinning the fill-poll window; a caller that has to bound how many broker lookups it makes in one cycle then spends them where they narrow the window.

Return type:

list[str]

get(trade_id)[source]

One trade by id.

Parameters:

trade_id (int)

Return type:

Trade | None

get_by_order_id(order_id)[source]

The trade recorded for a broker order id, if this app placed it.

Parameters:

order_id (str)

Return type:

Trade | None

exists_for_order(order_id)[source]

Whether this app placed the order with this broker id.

Parameters:

order_id (str)

Return type:

bool

all()[source]

Every trade, oldest first.

Return type:

Sequence[Trade]

recent(limit=20)[source]

The most recently submitted trades, newest first.

For the chat CLI (issue #13), which wants “what did you trade recently” without pulling the whole table through all() and reversing it client-side. Same ordering convention as DecisionRepository.recent and OutcomeRepository.recent: submitted_at descending with id descending as the tiebreak, since two rows can share a timestamp.

Parameters:

limit (int)

Return type:

list[Trade]

count()[source]

How many trades have been recorded.

Return type:

int