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
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,64 @@ If the deployment runs on a schedule (LaunchAgents, cron), a new build takes eff
cycle with nothing to restart — each cycle is a fresh process. A **long-running** process is the
exception: a `keel tui` left open keeps the build it started with until you quit and relaunch it.

## Paper vs. live

A deployment such as `~/keel` runs **two of them side by side**, and they share nothing: separate
configs, separate databases, separate allowlists, separate caps, separate schedules, separate
histories. **A figure from one says nothing about the other.** Checking a paper position size
against live account equity — or a live cap against paper cash — yields a confident wrong answer,
and has already produced one. Establish which account a number came from before reasoning about it.

| | paper | live |
| --- | --- | --- |
| config | `config.paperforward.yaml` | `config.live-sandbox.yaml` |
| database | `keel.db` (the `--db` default) | `keel-live.db` (must be passed) |
| `auto_trade.mode` | `paper` | `confirm` |
| allowlist | BTC, ETH, PAXG, SOL, XLM, LTC, ADA, LINK (8) | BTC, ETH, PAXG, ADA, XLM (5) |
| `caps.max_exposure_usd` | 5000 | 200 |
| money spent | synthetic `paper_cash_usdc` | the real broker balance |
| sizing basis | the paper account's own equity | `caps.max_exposure_usd`, as a proxy |
| rail 14 allowance | $500/month (Basic tier) | $200/month |
| `equity_state_mode` | `paper` | `live` |
| launchd job | `com.keel.paperforward` | `com.keel.live` |

**Which one am I looking at.** On any dashboard (`keel status`, `keel insights`, `keel tui`) the
`equity_state_mode` line names the account the equity, high-water mark and drawdown figures
describe, and `paper_cash_usdc` is printed in paper mode only. On the command line it is the
`--config`/`--db` pair — and `--db` is the one that bites, because `keel.db` is its default, so a
live command that omits it silently reads the **paper** database and answers about the wrong
account. Live commands always carry both:

```bash
keel --config config.live-sandbox.yaml --db keel-live.db status
```

**Placing an order is gated differently.** Paper places freely against synthetic cash — nothing is
asked and nothing real moves, which is the point. Live runs `mode: confirm`: each order is
previewed and waits for a typed `y` at a terminal, so a headless live cycle **fails closed** and
places nothing — *unless autonomy is armed*, which is exactly what makes an unattended live cycle
place. Autonomy changes who is asked, never what is allowed (see **Confirm vs. autonomy** above);
check the flag before assuming a live cycle is supervised, rather than inferring it from `confirm`.

**Both fire hourly; both run once a day.** Each launchd job has a list of hourly triggers plus
`RunAtLoad`, and each runner is day-stamped: the first eligible trigger that finds no stamp for
today runs the cycle and writes the stamp, and every later trigger that day is a no-op. The
trigger count is **catch-up breadth, not cadence** — launchd re-runs a calendar interval missed
while asleep but *not* one that passed while the machine was off, so the extra triggers are what
stop a shutdown over the scheduled hour from losing the day outright. A cycle that **fails** leaves
no stamp, so the next hour retries it, which also covers waking with no network. The two jobs
differ only in anchor: paper fires 09:00–20:00 local and stamps the local date; live fires hourly
at :20 and gates and stamps on the **UTC** date, because a daily bar is not visible until the
00:00–01:00 UTC hourly candle has closed. On live the stamp is a correctness mechanism, not tidiness
— nothing on that path dedupes an entry, so two cycles in one UTC day means two entries off one
daily bar (`tests/test_schedule.py` pins it).

**Sizing is a different calculation on each.** Paper sizes off its own synthetic equity, passed to
`_build_intent` as `equity_override`; the live path has no equity reading there and falls back to
`caps.max_exposure_usd` as a proxy (`keel/execution/executor.py`). The same rule, the same setup and
the same day therefore produce different quantities on the two accounts, and neither is an estimate
of the other. The settings behind those numbers are covered under **How much money moves** above.

## Before trading live

Read `docs/operator-runbook.md`. It lists the compliance obligations **no rail can enforce** — chiefly
Expand Down
10 changes: 10 additions & 0 deletions keel/commands/tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,16 @@ def _note(text: str) -> None:
_note(" reads the engine log, offline -- see 'Activity overlay' below")
_note(" opens scoped to TODAY; press t inside it to widen")
lines.append(_blank())
_row("Which account is this?")
_note(" paper and live are SEPARATE deployments -- separate config, database, allowlist,")
_note(" caps and history -- and no figure on this screen describes the other one. Read")
_note(" `equity_state_mode` to tell which is on screen; paper_cash_usdc appears in paper")
_note(" mode only.")
_note(" There is no in-app switch: the account is fixed by the --config/--db pair this")
_note(" process started with, and --db DEFAULTS to keel.db, so omitting it shows PAPER.")
_note(" Quit and relaunch against the other pair to change it, e.g.")
_note(" keel --config config.live-sandbox.yaml --db keel-live.db tui")
lines.append(_blank())
_row("Live balance")
_note(" 'live account' shows the REAL account's spendable quote balance (e.g. USDC),")
_note(" refreshed every ~30s and immediately on 'r' or 'f' -- so a deposit or sell shows up.")
Expand Down
27 changes: 27 additions & 0 deletions tests/commands/test_tui.py
Original file line number Diff line number Diff line change
Expand Up @@ -1520,6 +1520,33 @@ def test_build_help_screen_documents_discover_network_gating_and_attest_is_cli_o
assert "keel assets attest" in text


def test_help_says_which_account_is_on_screen_and_how_to_switch() -> None:
"""paper and live render in an IDENTICAL layout, so nothing on the dashboard itself says which
account a number belongs to -- and reading a paper figure as a live one is the most expensive
confusion this project has. The help must name the field that disambiguates, and the
`--config`/`--db` pair that changes it, including that `--db` defaults to the PAPER database:
an operator who omits it gets paper numbers under a live-looking command. Scoped to the
section, like `test_help_says_the_live_balance_line_is_itself_a_venue_call`, so a word three
sections away cannot satisfy it."""
section: list[str] = []
lines = build_help_screen()
start = next(
i for i, line in enumerate(lines) if line.text.strip() == "Which account is this?"
)
for line in lines[start:]:
if not line.text.strip():
break
section.append(line.text.lower())
text = " ".join(section)
assert "separate" in text
assert "equity_state_mode" in text
assert "--config" in text
assert "--db" in text
assert "keel-live.db" in text
# The default is the trap, not a detail: omitting --db reads keel.db and reports on paper.
assert "defaults to keel.db" in text


#: Every operator-facing place that counts this dashboard's network touches. The count is a
#: SAFETY claim -- an operator deciding whether a keypress can reach the venue reads it and stops
#: looking -- so an undercount is a bug, not a typo, and it must be pinned wherever it is stated.
Expand Down
Loading