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
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,53 @@ rail — not even autonomy.
time-boxed session: `keel autonomy on --for-hours N`. To stop trading immediately, use
`keel kill`, not `keel autonomy off`.

### How much money moves

Four settings decide position size and how much can be spent. Three live in `config.yaml`; the
fourth does not, which is most of why they drift apart.

- **`paper.starting_equity_usd`** — the synthetic paper account's seed. **It is a ONE-TIME seed,
applied on the FIRST paper run only** (`keel/agent.py`, the `paper_trader.get_cash() is None`
branch). Editing it afterwards does nothing at all: the seeding branch is skipped whenever
`paper_cash_usdc` is already set, so an already-seeded account keeps its balance forever.
Resizing a running paper account means clearing that persisted `paper_cash_usdc` — a key in the
`agent_state` table, with **no command that clears it** (`keel reset-hwm` does not); a fresh
database is the clean way. `0` (the default) means "seed from real mark-to-market equity
instead"; any value above `0` overrides that and seeds at exactly that amount.
- **`paper.monthly_contribution_usd`** — a recurring top-up, applied once per UTC calendar month.
It compounds, and the base is small: a contribution comparable to the seed doubles the account
monthly, and every position size below grows with it.
- **`caps.max_exposure_usd`** — has **two jobs at once**. It is the ceiling on total notional held
at any one moment (rail 4, and rail 6's concentration cap is a percentage of it), *and* it is
the **equity proxy that sizes orders** on the live path
(`keel/execution/executor.py::_build_intent`). So live `risk_pct` is a fraction of THIS number,
not of real account equity — raising the cap raises the real dollars risked per trade. Set above
actual equity it stops binding before available cash does, and the refusal comes later and less
legibly from the funding check (rail 13). In paper mode the proxy is bypassed: sizing uses the
paper account's own equity.
- **rail 14's monthly allowance** — the fee-free monthly BUY volume. It lives in the **database,
not `config.yaml`**: the `broker_subscriptions` row written by `keel subscription attest --venue
coinbase --tier <tier>`, or set directly with `keel subscription set --free-volume-usd N`.
`config.yaml` only supplies the tier catalogue and the unattested fallback
(`subscription.unsubscribed_allowance_usd`). Being in a different place from the caps is exactly
why it drifts out of step with them.

**The interaction is the point.** Position sizing scales with equity (or, on the live path, with
the `max_exposure_usd` proxy); the rail-14 allowance is a fixed dollar figure that scales with
nothing. Let the two drift apart and *every* setup is vetoed — keel looks broken while every
component is doing exactly what it was configured to do.

The real case: at **$11,000** paper equity with `risk_pct: 0.01`, a PAXG setup with a 3.35%-wide
stop sized to **$3,284.67** — exactly 1% of equity ($110) at risk, the correct answer. Rail 14's
allowance was **$500/month**, so it was vetoed, as was every other setup. Not a bug in either
setting; the two were simply on different scales. Reseeding the paper account at $500 sizes the
same setup at **$149.30**, which fits.

Note the counter-intuitive mechanic behind those numbers: **a tighter stop produces a LARGER
position**, because `size = risk ÷ stop-distance` (`keel/execution/sizing.py::size`). That is how
a 1% risk becomes a **30% position** — `risk_pct` bounds what you lose if the stop holds, not what
you spend.

### Halal by construction, and ships inert

Long-only spot only — no leverage, shorting, or derivatives; sizing uses actual cash, so no
Expand Down
24 changes: 24 additions & 0 deletions keel/commands/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,30 @@ def _note(text: str) -> None:
_note(" is a read: it places no orders and changes nothing.")
_note(" In paper mode, paper buys spend paper_cash_usdc instead -- not this balance.")
lines.append(_blank())
_row("Glossary (the field names the dashboard prints verbatim)")
_row(" cycle")
_note(" One pass of the agent loop: poll the feed, evaluate every rule against every")
_note(" allowlisted product, decide. This deployment runs ONE cycle per day. A cycle that")
_note(" happened and found nothing is the NORMAL case, not a fault.")
_row(" signal")
_note(" A rule's setup that passed the engine's gates. `signals=0` means no rule found a")
_note(" setup at all -- which is NOT the same as a setup being found and then vetoed.")
_row(" sig / blk / ent / exi / err")
_note(" The activity overlay's per-cycle columns: signals, blocked (rail vetoes),")
_note(" entered, exited, errors. `sig 1 blk 1` means keel DID find something and a rail")
_note(" stopped it; `sig 0` means it found nothing to stop. Read the two together: they")
_note(" are the difference between 'no setup' and 'setup, declined'.")
_row(" paper_cash_usdc")
_note(" The synthetic cash balance paper buys spend -- seeded once, then tracked in the")
_note(" DB. It is NOT a real broker balance, and it appears only in paper mode.")
_row(" equity_state_mode")
_note(" Whether the equity figures above describe the PAPER account or the LIVE one. The")
_note(" two are separate accounts with separate histories; neither reflects the other.")
_row(" high_water_mark / drawdown / rail11")
_note(" The peak equity the drawdown breaker measures against, how far equity has fallen")
_note(" from that peak now, and whether the breaker is holding trading. The ceilings in")
_note(" parentheses on the drawdown line come from config.")
lines.append(_blank())
_row("Help mode (this screen)")
_row(" up / k scroll up one line")
_row(" down / j scroll down one line")
Expand Down
62 changes: 62 additions & 0 deletions tests/commands/test_tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -1575,6 +1575,68 @@ def test_build_help_screen_is_longer_than_a_small_terminal() -> None:
assert len(lines) > 24


def _help_section(heading_prefix: str) -> str:
"""The lowercased body of one help section -- from the line starting `heading_prefix` up to
the next blank -- so a glossary assertion cannot be satisfied by a word appearing three
sections away. Mirrors `test_help_says_the_live_balance_line_is_itself_a_venue_call`."""
lines = build_help_screen()
start = next(i for i, line in enumerate(lines) if line.text.startswith(heading_prefix))
body: list[str] = []
for line in lines[start:]:
if not line.text.strip():
break
body.append(line.text.lower())
return " ".join(body)


def test_help_screen_glossary_defines_every_field_name_the_dashboard_prints() -> None:
"""`_equity_lines` and the activity overlay print keel's INTERNAL field names verbatim --
`equity_state_mode`, `high_water_mark`, `rail11`, `paper_cash_usdc`, `sig blk ent exi err`.
Nothing on the dashboard explains any of them, so the help must, by name."""
text = _help_section("Glossary")
for term in (
"cycle",
"signal",
"sig / blk / ent / exi / err",
"paper_cash_usdc",
"equity_state_mode",
"high_water_mark",
"drawdown",
"rail11",
):
assert term in text, term


def test_help_screen_glossary_distinguishes_no_setup_from_a_vetoed_setup() -> None:
"""The distinction the whole glossary exists for: `sig 0` (found nothing) and `sig 1 blk 1`
(found something, a rail stopped it) look equally idle on a dashboard of zeroes, and an
operator who conflates them reads a correctly-declining deployment as a dead one."""
text = _help_section("Glossary")
assert "`sig 1 blk 1`" in text
assert "`sig 0`" in text
assert "rail vetoes" in text
# A cycle that finds nothing is the normal case, not a fault -- said in those terms.
assert "one cycle per day" in text
assert "normal case, not a fault" in text


def test_help_screen_glossary_says_paper_cash_is_synthetic_and_paper_only() -> None:
"""`paper_cash_usdc: 11000` is the single most mistakable number on the dashboard: it reads
like a broker balance. It is neither real nor present in live mode."""
text = _help_section("Glossary")
assert "not a real broker balance" in text
assert "only in paper mode" in text
# The two equity accounts are separate histories, not two views of one account.
assert "separate accounts with separate histories" in text


def test_help_screen_glossary_sources_the_drawdown_ceilings_to_config() -> None:
"""The parenthesised ceilings on the `drawdown:` line are config values, not live readings --
an operator who thinks they are measurements has no idea where to change them."""
text = _help_section("Glossary")
assert "come from config" in text


def test_visible_slice_clamps_too_large_offset() -> None:
lines = [ScreenLine(str(i), "normal") for i in range(50)]
result = _visible_slice(lines, offset=1000, height=10)
Expand Down
Loading