Skip to content

feat(book): veto on the ratcheted stop from stops.jsonl - #65

Merged
HaoweiChan merged 2 commits into
mainfrom
task/24
Oct 14, 2026
Merged

HaoweiChan merged 2 commits into
mainfrom
task/24

Conversation

@HaoweiChan

@HaoweiChan HaoweiChan commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Why

The book vetoed only on a rating's own stop_loss, while the actual-portfolio pass exits on the ATR-ratcheted stop in stops.jsonl. A name the owner exited on the ratcheted line stayed in, or came back to, the book the next day. Observed on the owner's 2026-10-13 book: a held name with an EXIT card on 2026-10-12 was still in core at full weight.

Task (verbatim): TASK-24 — book honors the ratcheted stop in stops.jsonl

Description

The book's stop veto (kuroshio/core/book.py, stopped_out and the "below its stop" check in
walk) only knows the rating's stop_loss. The actual-portfolio pass ratchets held names' stops
upward by ATR and appends each move to the ledger's stops.jsonl (ledger.STOPS, rows
{date, market, ticker, old, new, reason}), and fires EXIT on that ratcheted line. The book never
reads it, so a name the owner just exited on the ratcheted stop stays in (or comes back to) the
book the next day. Observed on the owner's 2026-10-13 book: a held name whose stop the ratchet
had raised well above its rating's stop_loss closed under the ratcheted line, got an EXIT card,
and stayed in the book at full weight.

Change: the stop the book vetoes on is the stop in effect on each session:
max(rating stop_loss, newest stops.jsonl new for that ticker and market whose row date is on or after the rating date and strictly before that session). A close at or below the stop in
effect that session is a stop-out → the existing skip "{rating} stopped out {date}; needs a new rating" (the existing phrase stays; it may append the stop value). Today's "below its stop" check
uses the stop in effect today. Rows dated before the rating date are ignored, so a newer rating
re-arms the name exactly as today. A close that was above the stop in effect on its own day is
never a stop-out retroactively, even if the stop was later ratcheted above it.

cmd_book loads ledger.ledger_dir() / ledger.STOPS best-effort, filtered to --market, the
same way _logged_stops does (an unreadable ledger warns on stderr and the book builds as
before); build_book takes the rows as a new optional argument (None → today's behavior).

Not changed: sizing (target_weight still sizes on the rating stop), the invalidation_price the
book writes to holdings.yml, propose, the ratchet itself, stops.jsonl writes, the vendored
kuroshio/agents/engine.

Probe: T=$(mktemp -d); cp -R ~/.kuroshio/ledger $T/ledger; mkdir -p $T/book/2026-10-12; cp ~/.kuroshio/book/2026-10-12/book.json $T/book/2026-10-12/; KUROSHIO_LEDGER_DIR=$T/ledger PYTHONPATH=. <main-checkout>/.venv/bin/python -m kuroshio.cli book --market us --screen ~/.kuroshio/screen/2026-10-13.json --ratings ~/.kuroshio/ledger/ratings.jsonl --scores ~/.kuroshio/ledger/scores.jsonl --ips ~/.kuroshio/ips-us.md --meta ~/.kuroshio/book/meta.json --pm-size ~/.kuroshio/book/pm_size.json --locked ~/.kuroshio/book/locked.json --themes ~/.kuroshio/book/themes.json --provider yfinance --lang zh --out $T/book/2026-10-13; echo rc=$?; grep -o '[A-Z]* *[A-Za-z]* \(below its stop\|stopped out\)[^"]*' $T/book/2026-10-13/alloc.md $T/book/2026-10-13/book.md | head · budget $0, one yfinance fetch of ~80 names, under 90 s · reps 1 — rc=0, and the name exited on its ratcheted stop on 2026-10-12 is skipped (below its stop or stopped out) at the ratcheted level, in neither core nor attack. Real holdings: the PR states the outcome only.

Acceptance Criteria

  • groundwork: adopt pr-loop + portfolio-management diagnosis and plan #1 a test in tests/test_book.py on a fictional ticker: rating stop 80 (rated 2026-09-05), stops rows ratcheting to 110 on 2026-10-02, close 109.5 on 2026-10-12 → the name is skipped stopped out 2026-10-12; needs a new rating; the same fixture without stops rows keeps the name (today's behavior).
  • CLAUDE.md: drop hardcoded local path from the dev-env note #2 tests: a close above the stop in effect on its day but below a later ratcheted stop is not a stop-out; stops rows dated before the rating date are ignored (a newer rating re-arms the name); rows of another market or ticker are ignored.
  • T1 — Accept Hold in the verdict ladder #3 kuroshio book reads stops.jsonl from the ledger dir; an unreadable or missing file warns (or is silent when absent) and the book still builds; pre-existing tests pass unedited.

What changed

  • The stop veto uses the stop in effect on each session: max(rating stop_loss, the newest stops.jsonl row for that ticker/market dated on or after the rating date and before that session). A close at or below it is a stop-out ("needs a new rating"); today's "below its stop" check uses today's level.
  • A later ratchet never turns an earlier close into a stop-out; rows dated before the rating are ignored, so a new rating re-arms the name.
  • kuroshio book reads stops.jsonl from the ledger dir best-effort (filtered to --market); build_book takes it as stops_rows (None = previous behavior).

Not changed: sizing and the invalidation_price in holdings.yml still use the rating's stop; propose; the ratchet; stops.jsonl writes.

Verification

Gate: ruff clean · pytest 494/494 · 32c89c1
Red-first: the 15 test_ratchet_stop_* cases watched red by the implementer (build_book rejected stops_rows), green at 32c89c1
Live: run eFyGPyRts1 ×1 on build 345d919 (same code as 32c89c1; only test fixtures and the task file changed) · $0 — rc=0; the name exited on its ratcheted stop is skipped at the ratcheted level and is in neither core nor attack. The reason reads below its stop instead of the stopped out phrase the probe line names, because the screen-day close was also under the stop and that check runs first; the effect is the same.
Not verified: the cron run (the change is live from the next daily book after merge)

Problems found

  • Test fixtures first used a real holding's ticker and prices → swapped to a fictional ticker and numbers before push → fixed

Follow-ups

none

Reviewer notes

Start here: stop_in_effect / stopped_out in kuroshio/core/book.py
Reproduce: pytest tests/test_book.py -q -k ratchet

🤖 Generated with Claude Code

HaoweiChan and others added 2 commits October 14, 2026 23:15
TASK-24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@HaoweiChan

Copy link
Copy Markdown
Owner Author

pr-loop/verifier
Result: APPROVED · no findings
Gate: ruff clean · pytest 494/494
Probe: run eFyGPyRts1

@HaoweiChan
HaoweiChan merged commit 74573c0 into main Oct 14, 2026
5 checks passed
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.

1 participant