Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ applies the confirm/autonomy gate, then places and logs. There is deliberately n
quote-balance checks, venue **subscription/withdrawal attestations** — rail 17 encodes
§65.4 *qabd*: an asset that cannot be withdrawn may not have been validly possessed, so
withdrawal capability is attested and enforced, not assumed. A rail veto names itself and
the command that clears it.
the command that clears it. Beside the rails sits one routing-time check that needs the
venue's own book, which a broker-less rail cannot see: the **max-spread entry gate** (#350)
refuses a live BUY whose previewed `(best_ask − best_bid) / mid` is at or beyond
`execution.max_entry_spread_pct` (default 50bp) — BUY-only, live-only, and fail-closed on
an unreadable book.
- **Screening** (`keel/compliance/screen.py`) — allowlist admission is split by what is
knowable: market facts are computed; Shariah classifications are **attested, never
inferred**, and an absent attestation is a rejection, not a default pass.
Expand Down
12 changes: 12 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,15 @@ logging:
research:
pbo_max: 0.05
slope_floor: -0.5

# Live-execution routing-time guardrails (#350). max_entry_spread_pct is the threshold of the
# entry spread gate: a live BUY whose previewed book shows (best_ask - best_bid) / mid at or
# beyond it is REFUSED before placement, so a thin book cannot be entered when its spread
# alone makes the fill materially more expensive than the cost model assumes. BUY entries
# only (SELLs/exits are never gated) and the live path only -- paper fills are synthetic and
# see no book, so the paper profiles accrue no evidence about this gate. 0.005 = 50bp,
# anchored to the backtest's worst-case per-leg slippage assumption (#334's slippage cap):
# a spread AT that line has already consumed the model's entire cost estimate. Valid range
# (0, 0.10] -- a fraction of price, not basis points.
execution:
max_entry_spread_pct: 0.005
10 changes: 10 additions & 0 deletions docs/fiqh-basis.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,16 @@ religious claim:
| 12 | stale-feed + kill-switch, fails closed | operational safety |
| 13, 14 | spend only the settled quote currency; monthly allowance cap | operational safety |

Beside the rails — not among them, and not numbered — sits one routing-time check with the
same prudential character: the **max-spread entry gate** (#350, `keel/execution/executor.py`)
refuses a live BUY whose previewed book shows `(best_ask − best_bid) / mid` at or beyond
`execution.max_entry_spread_pct` (default 50bp). It is not a `guards.check` rail because the
rails are broker-less by design and the book exists only in the venue's preview response.
BUY-only (exits must execute), live-only (paper fills are synthetic and see no book), and
fails closed on an unreadable book — the same fail-closed family as rails 12/13/17, justified
by trading evidence (the backtest's worst-case per-leg cost assumption), carrying no
religious claim.

Rail 7 carries a correction this repository records prominently: §65.6 holds that
"speculation per se, which means sale/purchase keeping in mind possible change in prices in
the future, is not prohibited" — what makes speculation *maisir* is non-ownership,
Expand Down
27 changes: 27 additions & 0 deletions docs/operator-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,33 @@ spread gate. The 8 incumbents keep their relative shape rescaled to 78% total (r
untouched, so their evidence stays comparable across the expansion). Paperforward — the daily
profile — deliberately stays at 8 so its evidence remains a like-for-like 8-asset series.

### The spread guardrail: a sizing half and a live-path half

Thin books cost more to trade than the cost model assumes, and the corpus's thin tail is
exactly where the expansion above added exposure. The guardrail has two halves, each doing the
half it can:

- **Sizing (#358):** every Tier-2 addition sits at a flat 2% target weight, so a thin book can
only ever be a 2% position.
- **Live path (#350):** a **routing-time maximum-spread gate** refuses a live BUY when the
venue's own previewed book shows `(best_ask − best_bid) / mid` at or beyond
`execution.max_entry_spread_pct` — default **0.005 (50bp)**, anchored to the backtest's
worst-case per-leg slippage assumption (#334's `SLIPPAGE_CAP_PCT`): if the spread ALONE
consumes the model's entire cost estimate, the fill economics are materially worse than
anything the rule was measured on, and the entry waits for the book to tighten.

The gate is BUY-only (exits must execute — the same principle that makes rail 17 halt entries,
not exits), **fails closed** (a live BUY whose preview carries no readable bid/ask is refused
with a distinct `book_unreadable` reason, never guessed past), and lives beside the eighteen
rails rather than among them: `guards.check` is broker-less by design, and the book exists only
in the preview the executor just fetched.

**Paper accrues no evidence about it.** Paper fills are synthetic and see no book, so neither
paper profile ever exercises the gate — a reason it ships before any live resumption (the gate
must already be in force when live BUYs resume) rather than being validated on paper first.
A refusal is visible in the cycle log as `executor.entry_spread_refused` (with the measured
spread and the threshold) or `executor.entry_book_unreadable`.

**The rows differ from every other turtle row by one param.** `params.granularity: "ONE_HOUR"`
— `TurtleBreakout`'s declared trading timeframe, persisted the way `RsiMeanReversion.timeframe`
is and coerced back by `keel/agent.py`'s registry. A row with no `granularity` key (every row
Expand Down
3 changes: 2 additions & 1 deletion keel/commands/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,8 @@ def summarise_cycle(cycle_id: str | None, events: Sequence[ActivityEvent]) -> Ac
counting `engine.setup_detected` for a cycle whose `signals_evaluated` records fell outside
the read window.
* `blocked` -- entries that did NOT become an order: an `agent.enter_evaluated` with
`placed=false` (rails vetoed it, or the confirm gate declined), plus an
`placed=false` (rails vetoed it, the routing-time entry-spread gate refused the live
BUY's book, or the confirm gate declined), plus an
`agent.entry_bar_not_ready` (withheld before it was ever evaluated). One per entry, never
one per *reason* -- the PAXG cycle of 2026-08-08 trips two guards on a single signal, and
reporting `blocked=2` there would imply two setups where there was one.
Expand Down
Loading