Skip to content

feat(tui): the strategy console and the research readers (#390 C4) - #404

Merged
eaitbrahim merged 3 commits into
mainfrom
feat/390-strategy-console
Aug 19, 2026
Merged

feat(tui): the strategy console and the research readers (#390 C4)#404
eaitbrahim merged 3 commits into
mainfrom
feat/390-strategy-console

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

The console's fourth slice: the Rules menu becomes the full try/evaluate/retry loop of PRD O11, and the Research menu lands the O5 evidence readers. Everything dispatches to the C1 service layer; no sizing, gating or reporting math is born in the TUI.

The tried-vs-used ledger's sourcing (O11.2)

Every rules-table row rendered with its lifecycle status AND the machine's recorded reason it sits there:

  • can_promote's own reasons — the exact lines rules promote prints on a refusal: which floor failed (n_trades N < min_trades 100, expectancy/rr/win_rate), and the G4/PBO axis honestly NOT RUN — computed from a fresh backtest over the repo's cached candles at the config's own fee (_backtest_fee + _describe_fee provenance).
  • the insights service's promotion-gate distance for paper rules (build_rule_track_record/track_record, the trades_remaining wording).
  • the rows' own promoted_at/demoted_at stamps for live/disabled rows.
  • A rule with no cached candles renders "no backtest on record" — the honest absent case, never a TUI-authored narrative.

The ledger computes once per entry (every row's backtest is real work) and is held while open. Its detail view renders every param through describe_params.

The simulate UX decision (O11.1)

An ARMED view showing the target report path first — Enter IS the confirm step — then run_simulation on the active profile's config/db with the CLI's own defaults (5y, $500/month, allowlist products, history fetched when the cache does not cover the window, mirroring the CLI). The run blocks the loop exactly like f fetch does — the CLI's own UX, mirrored honestly. The progress lines the CLI streams are collected and shown at the head of the results; the verdict/report render verbatim under a pinned verdict+path footer (pinned_frame); closing re-arms. The report lands in the directory the Research/promotion-reports reader lists, newest-first.

The extraction done first (C1 style, byte-compatible)

The validation and write logic of rules add/backtest/promote/demote/disable/enable moved out of the click bodies into keel.commands.rules services (add_rule_row, run_rule_backtest, attempt_promotion, apply_rule_*) that echo through injected stdout/stderr sinks. The CLI wrappers dispatch and are byte-identical — verified against origin/main across 17 command scenarios (diffed outputs, exit codes), and by the untouched test_cli.py/test_rules_add.py suites. The G4 loader stays injectable (load_pbo) so the _load_pbo test seam keeps working unchanged. --force on the gated path loads config only where the old body did.

O3 held: promote asks an explicit y/N in-console; --force runs only behind the CLI's OWN _require_interactive_confirmation typed gate (clis_typed_promote_force_gate, exact phrase, never pre-filled, failing closed); no auto-promotion anywhere.

describe_params design

One introspection helper in the service layer deriving {param: doc, default, type, declared choices, quotable} from the class itself:

  • doc from per-parameter PARAM_DOCS dictionaries added AT THE CLASS — all four rule kinds (turtle_breakout, pullback_continuation, rsi_meanrev, dca; Dca's docstring Params section moved into its table), two pinned verbatim by test.
  • default/type from the constructor's signature and resolved annotations; choices from the param's own Literal; quotable from agent.coerced_param_keys (the coercion boundary itself).

The add form prompts one field per param with its doc and default (empty keeps the default, exactly as omitting the --params key); the ledger's detail view renders from the same source. No hand-maintained table anywhere.

The research readers (O5)

The experiments / research-docs / promotion-reports corpora (directories single-sourced from the writers — the trials ledger's own parent, run_simulation's report directory — resolved at call time) plus the trials ledger view (read_trials/trial_counts/verify_chain, read-only, the chain verdict fail-loud). Documents read through a bounded reader (1 MiB, loud truncation note) cached per (path, mtime) — the compliance console's scout-browser lessons, kept.

Gates

  • uv run pytest -q — 3458 passed, 3 skipped
  • uv run ruff check keel tests packages — clean
  • uv run mypy — clean

Fixes #390

The console's fourth slice: the Rules menu becomes the full try/evaluate/
retry loop of PRD O11, and the Research menu lands the O5 evidence readers.
Everything dispatches to the C1 service layer; no sizing, gating or
reporting math is born in the TUI.

The tried-vs-used ledger's SOURCING (O11.2): every rules-table row rendered
with its lifecycle status AND the machine's recorded reason it sits there --
`can_promote`'s own `reasons` (the exact lines `rules promote` prints on a
refusal: which floor failed, and the G4/PBO axis honestly NOT RUN) from a
fresh backtest over the repo's cached candles at the config's own fee; the
`insights` service's promotion-gate distance for `paper` rules; the rows'
own `promoted_at`/`demoted_at` stamps for live/disabled. A rule with no
cached candles renders "no backtest on record" -- the honest absent case,
never a TUI-authored narrative. The ledger computes ONCE per entry (every
row's backtest is real work) and is held while open.

The simulate UX DECISION (O11.1): an ARMED view showing the TARGET REPORT
PATH first -- Enter IS the confirm step -- then `run_simulation` on the
active profile's config/db with the CLI's own defaults (5y, $500/month,
allowlist products, history fetched when the cache does not cover the
window). The run BLOCKS the loop exactly like `f` fetch does, mirroring the
CLI's own UX honestly (the mission's note-then-render pattern); the
progress lines the CLI streams are collected and shown at the head of the
results, the verdict/report render VERBATIM under a pinned verdict+path
footer, and closing re-arms. The report lands in the directory the
Research/promotion-reports reader lists, newest-first.

The EXTRACTION done first (C1 style, byte-compatible): the validation and
write logic of `rules add`/`backtest`/`promote`/`demote`/`disable`/`enable`
moved out of the click bodies into `keel.commands.rules` services
(`add_rule_row`, `run_rule_backtest`, `attempt_promotion`,
`apply_rule_*`) that echo through injected stdout/stderr sinks; the CLI
wrappers dispatch and are byte-identical (verified against origin/main
across 17 command scenarios, and by the untouched `test_cli.py`/
`test_rules_add.py` suites). The G4 loader stays injectable so the
`_load_pbo` test seam keeps working unchanged. O3 held: promote asks an
explicit y/N in-console, `--force` runs only behind the CLI's OWN
`_require_interactive_confirmation` typed gate (exact phrase, never
pre-filled, failing closed), and there is no auto-promotion anywhere.

`describe_params` DESIGN: one introspection helper in the service layer
deriving {param: doc, default, type, declared choices, quotable} from the
class itself -- the doc from per-parameter `PARAM_DOCS` dictionaries ADDED
AT THE CLASS (all four rule kinds; Dca's docstring Params section moved
into its table), the default/type from the constructor signature and
annotations, the choices from the param's own `Literal`, and the
"quoted is correct" answer from `agent.coerced_param_keys` (the coercion
boundary). The add form prompts ONE FIELD PER PARAM with its doc and
default (empty keeps the default, exactly as omitting the --params key);
the ledger's detail view renders every param through the same source. No
hand-maintained table anywhere.

The research readers (O5): the experiments/research-docs/promotion-reports
corpora (directories single-sourced from the writers -- the trials ledger's
own parent, `run_simulation`'s report directory -- resolved at call time)
plus the trials ledger view (`read_trials`/`trial_counts`/`verify_chain`,
read-only). Documents read through a BOUNDED reader (1 MiB, loud truncation
note) cached per (path, mtime) -- the compliance console's scout-browser
lessons, kept.

Fixes #390
CI caught what local paths hid: the 80-col wrap can break any
multi-word phrase across lines (a long runner tmp path shifts every
wrap point), so phrase-level asserts read whitespace-collapsed text.
'truncated', 'no documents', 'chain intact', 'no trials' normalized
alongside 'newest first'; single-token asserts stay as-is.
@eaitbrahim
eaitbrahim merged commit 68fcc5c into main Aug 19, 2026
5 checks passed
@eaitbrahim
eaitbrahim deleted the feat/390-strategy-console branch August 19, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

C4: rules menu + research readers (experiments, docs, trials ledger)

1 participant