trader.risk.guardrails module

Risk guardrails (requirements §8).

Checked here and never inside a strategy or evaluator. The app enforces limits on the thing being limited, rather than asking it to limit itself — the same principle as the Slice 1 safety gate.

class trader.risk.guardrails.ExposureCounter(committed)[source]

Bases: object

A mutable running total of capital committed this cycle.

Seeded before the symbol loop from open positions plus already-pending entry orders, then advanced by commit() every time an entry is actually submitted — so the next symbol in the same cycle sees the reduced headroom.

Mutable on purpose. A total computed once and never updated is blind to what the cycle itself is spending, which is precisely how twelve limit buys left one cycle on 2026-08-04, and the same mistake the discovered-position cap made before it.

Parameters:

committed (Decimal)

committed: Decimal
commit(amount)[source]

Record capital just committed by a submitted entry.

Parameters:

amount (Decimal)

Return type:

None

class trader.risk.guardrails.Guardrails(config)[source]

Bases: object

Position-size and daily-loss limits.

Parameters:

config (PipelineConfig)

daily_loss_breached(account)[source]

Whether today’s drawdown has reached the configured limit.

Uses the broker’s own equity and last_equity rather than reconstructing realized P/L from trade history, which would have to model partial fills and corporate actions to arrive at the same number.

Parameters:

account (Account)

Return type:

bool

check_entry(account, symbol, proposed_quantity, price, *, is_discovered=False, discovered_positions_held=0, max_discovered_positions=0, exposure=None)[source]

Approve, shrink, or reject a proposed entry.

The discovery arguments are keyword-only with defaults so that every pre-Slice-3d caller behaves exactly as before: a fixed ticker is never subject to the discovered cap.

Parameters:
  • account (Account)

  • symbol (str)

  • proposed_quantity (int)

  • price (Decimal)

  • is_discovered (bool)

  • discovered_positions_held (int)

  • max_discovered_positions (int)

  • exposure (ExposureCounter | None)

Return type:

RiskDecision

class trader.risk.guardrails.RiskDecision(allowed, reason, approved_quantity, code=None)[source]

Bases: object

The outcome of a guardrail check, and why.

A rejection is not an error — it is a recorded Decision explaining why the trade did not happen. reason is therefore populated on every path, including approval.

code is one of the module’s REASON_* constants on a refusal (allowed=False), and None on every approval path — including a shrink, which is not a refusal. decisions.rejection_reason (issue #21) is written from this, never from reason’s free text.

Parameters:
  • allowed (bool)

  • reason (str)

  • approved_quantity (int)

  • code (str | None)

allowed: bool
reason: str
approved_quantity: int
code: str | None