{"owner":"chrisleekr","repo":"binance-trading-bot","hasSkills":true,"totalSkillsCount":4,"totalTokensCount":7114,"categories":["anthropic-skill","root-instruction","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":[".claude/rules/principles.md",".claude/skills/feature-walkthrough/SKILL.md","AGENTS.md","CLAUDE.md"],"skills":{".claude/rules/principles.md":"# Engineering principles\n\nGeneric engineering rules for this repo. Project-specific invariants live in `CLAUDE.md`.\n\n## Think before coding\n\nDon't assume. Don't hide confusion. Surface tradeoffs.\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n## Simplicity first\n\nMinimum code that solves the problem. Nothing speculative.\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n- Ask: \"Would a senior engineer say this is overcomplicated?\" If yes, simplify.\n\n## Goal-driven execution\n\nDefine success criteria. Loop until verified.\n\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\nFor multi-step tasks, state a brief plan:\n\n1. [Step] → verify: [check]\n2. [Step] → verify: [check]\n",".claude/skills/feature-walkthrough/SKILL.md":"---\nname: feature-walkthrough\ndescription: Autonomously test the running binance-trading-bot app in a real browser. The agent drives the browser itself via the Playwright MCP - logs in, looks at each screen, and works through every feature end-to-end like a real operator, finding and fixing bugs. Use when asked to test the app, smoke-test or walk through the UI, verify the frontend end-to-end, or check that features actually work in a browser.\n---\n\n# Feature walkthrough\n\nAutonomously exercise the real app in a real browser. **You** are the test driver: open the browser, look at the screen, decide what to click, observe the result, decide the next step. This is agentic, vision-driven testing - not a pre-written script.\n\nThe goal is \"the feature works when a person uses it\". Type-checks and unit tests verify code correctness, not feature correctness.\n\n**Test as a non-expert operator.** Walk the app as a user who is _not_ a financial or trading expert. The product must be **easy but accurate**: precise and correct under the hood, yet usable by someone who does not already know the strategy. Unexplained jargon, a setting with no inline help, a number with no units or context, a flow that only a strategy-literate user can complete - each is a real UI/UX finding, not cosmetic.\n\n**This is a greenfield project - refactor freely.** Nothing is deployed; there is no legacy to protect and no shipped behaviour to preserve. When a finding exposes mis-architecture, a performance problem, or a UI/UX flow that is wrong at the structural level, the sanctioned fix is a **large refactor**, not a surgical patch. Do not paper over a bad design to keep the diff small - the bar is the correct architecture, correct code, tested and performant. Surface the scope of a big refactor, confirm, then do it properly.\n\nThis skill is stateless and self-contained - no sibling tracker files. Discover features fresh each run; fix what you can this run; file the rest as GitLab issues. Deterministic regression tests do NOT belong here - they live in `e2e/tests/` as Playwright specs (`manual-order-roundtrip.spec.ts`, `profile-controls.spec.ts`, ...). This skill is the exploratory layer that adapts to whatever the UI actually shows.\n\n## 1. Preflight\n\n1. **Browser tools.** Needs the Playwright MCP `browser_*` tools (`browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_select_option`, `browser_press_key`, `browser_wait_for`, `browser_resize`, `browser_console_messages`, `browser_network_requests`, `browser_take_screenshot`). If unavailable, stop and tell the operator to enable the Playwright MCP - the skill cannot run without it.\n2. **Stack.** Postgres + Redis must be up - `docker ps` for `binance-trading-bot-postgres-1` and `-redis-1`; if absent, `docker compose -f deploy/compose/docker-compose.yml up -d db redis`. Apply migrations: `bun run db:migrate`.\n3. **Dev server.** If not up: `bun run dev > /tmp/btb-dev.log 2>&1 &`. Ports: api `:3000`, web `:5173`, worker `:9101`. **`bun --watch` does NOT reload workspace `packages/*`** - after editing anything under `packages/`, or if the server was started before a recent merge, restart `bun run dev` or it serves stale code and a correct fix looks broken. Vite HMR (`apps/web`) reloads reliably.\n\n## 2. Log in and orient\n\n1. `browser_navigate` to `http://localhost:5173/login`.\n2. `browser_snapshot` the login form. Type the dev credentials - request the executor credentials - into the real fields and click submit. (If login reports no account, create the master account at `/onboarding`.)\n3. Log in **once** - the API rate-limits `/api/auth/sign-in/*` (60s window). Reuse the same session for the whole run; a wrong-password test also counts against the window, so run it after the real login.\n4. `browser_snapshot` the dashboard. This is your map: the nav, the profile list, the action surfaces. Cross-reference `apps/web/src/routes/` for routes not linked from the dashboard.\n\n## 3. Discover - fresh, every run\n\nBuild the feature set live each run; there is no coverage file to read:\n\n1. From the dashboard snapshot, walk every nav target and action surface (`browser_snapshot` each screen).\n2. Enumerate routes from `apps/web/src/routes/` - anything not reachable by clicking is still a feature.\n3. Treat each distinct end-to-end thing an operator can _do_ (place an order, switch strategy, save a notifier, run the wizard, ...) as one feature.\n\n## 4. Pick a batch\n\nFix a **substantial batch per run - ~5–8 items**, sized by reviewable diff, shipped as one MR(Gitlab) or PR(GitHub). CI and senior-review cost the same per MR whether it carries one fix or many, so a thin batch wastes the cycle. Priority order, highest first:\n\n1. Defects already seen this run.\n2. Features closest to the money path: orders > config > notifiers > cosmetic.\n3. Sub-bar UX/DevX small enough to fold in.\n\nRules: a genuinely large or risky item (whole-feature work, refactor, \"multiple valid approaches\" territory) ships **solo** in its own MR - never padded with or batched against small fixes. A batch may span files and themes; keep commit history legible (one commit per item or theme). Each batched item is still walked and verified per §5.\n\n## 5. Walk it - run this loop yourself, do not pre-script it\n\nRun this loop for **each item in the batch**. Items are independent; do them in turn.\n\n1. **Navigate** to the feature's entry screen.\n2. **Observe** - `browser_snapshot` (accessibility tree, gives clickable refs); `browser_take_screenshot` when layout matters. Refs are snapshot-scoped: re-snapshot after every navigation or re-render or a stale ref clicks the wrong thing.\n3. **Decide** the next interaction from what you see - the real button, input, selector.\n4. **Act** - `browser_click` / `browser_type` / `browser_select_option` / `browser_press_key`. Real interactions only. Destructive actions (kill-switch, strategy switch) open a confirm modal - snapshot the dialog and click its Confirm; the action is not done until you do.\n   - If `browser_click` reports success but nothing happens (no events reach the page), the long session's click coordinates have desynced - a tool artifact, not an app bug: `browser_close` then re-`browser_navigate` for a fresh context, and the click works again.\n5. **Verify** - snapshot again. Assert the concrete outcome: a banner, a badge, a redirected URL, a new row, a value that survives a reload.\n6. **Loop** to step 2 until the journey is complete or it visibly breaks.\n7. **Check silent failure** - `browser_console_messages` and `browser_network_requests`: unexpected console errors or API responses ≥ 400 are findings even when the screen looked fine.\n8. **Restore** every mutation through the UI - release kill-switches, revert config edits, delete test profiles. Leave the dev DB as you found it.\n\n**UX/DevX is a first-class finding, not cosmetic.** This is an operator's trading tool. At every Verify step, judge the screen as a user: is the layout clean and aligned, the hierarchy clear, the primary action obvious, are loading/empty/error states handled, is copy precise, the flow short and unsurprising? Read the pixels with `browser_take_screenshot`, not only the accessibility tree.\n\n**Non-expert lens.** Assume the user does not know trading. Jargon without explanation, a field with no inline help, a value with no units/context, or a flow that only a strategy-literate user can finish is a defect - fix or file it. \"Easy but accurate\": friendly defaults, plain-language labels, inline help, actionable errors, without losing correctness.\n\n**DevX counts the same.** Every setting needs a proper form UI with typed inputs, inline help, and per-field validation - a raw JSON editor is itself a UX defect: flag and replace it. Validation errors must say exactly what is wrong and where; defaults must be sane; setup/recovery must not require reading source. A cryptic error, a silent rejection, or a flow only a code-reader can complete is a DevX finding.\n\nCross-cutting checks while you are in there:\n\n- Wrong-password login → friendly error, not a crash. Do this **after** the real login or it burns the rate-limit window.\n- Resize to 375×667 (`browser_resize`) on every screen you visit → no horizontal overflow, and the mobile layout must be genuinely usable, not merely non-overflowing (mobile-first is a core invariant).\n\n**Dev-env boundary:** there is no live Binance connection in dev, and manual orders enqueue but may not fill. Verify what dev _can_ prove (enqueue, UI state, DB row) and state the boundary - do not file expected env state as a bug.\n\n## 6. Triage - one verdict per observation\n\n- **Fix it this run** - a small, contained defect, UI/UX, or DevX finding. Fix the root cause (§7).\n- **File a GitLab issue** - a real finding too large/ambiguous for this batch, or sub-bar-but-not-broken. Title + location + severity + repro steps. This is the durable backlog; there is no local tracker file.\n- **Leave it** - Cite the doc; do not act.\n- **Re-observe** - your own misread: wrong screen, stale dev server, a moved control, a desynced click (§5). Do not touch the app.\n- **Not a bug** - expected dev-env state: empty data, \"Never\" last-tick, 502s from Binance, 404s for unconfigured notifiers.\n\nA bug is never parked as a \"known issue\" to work around - it is fixed this run or filed as an issue to fix later.\n\n## 7. Fix - root cause, not symptom\n\nFix every batched item at the root. This is a greenfield project - no legacy to protect, no shipped behaviour to preserve - so **large refactoring is sanctioned and expected** wherever a finding warrants it. Mis-architecture (wrong abstraction, a pattern that should not exist, a missing layer), a performance problem, or a structurally-wrong UI/UX flow are all valid triggers for a big rewrite, not a patch. Do not paper over a bad design with a surgical fix that leaves the bad design in place. The bar is correct architecture, correct code, tested, performant - not minimal diff. A change that large is \"multiple valid approaches\" territory: surface the scope and design, confirm, then build. Keep `bun run typecheck`, `bun run lint`, and `bun run test` clean, and re-walk each affected feature in the browser. Loop until every batched item works.\n\nWhen a bug is worth locking down, add a deterministic spec to `e2e/tests/` (follow `profile-controls.spec.ts`) - that is the regression layer; this skill is not.\n","AGENTS.md":"# Engineering charter\n\n> Charter for any coding agent in this repo (Claude Code, Cursor, Codex, …). Keep it short — architecture, config, and ops live in `docs/` (built by `mkdocs`). Generic engineering principles (think-first, simplicity, goal-driven): `.claude/rules/principles.md`.\n\n## Project\n\n`binance-trading-bot`: single-operator, multi-account, multi-profile, reliability-first, mobile-first trading platform on Binance. One operator owns N first-class Binance accounts (each = one API key pair + one environment + one user-data stream); each account runs N strategy **profiles** against its shared wallet. Strategy is a **plugin** — trailing-trade is the first; source of truth is `packages/strategy/trailing-trade/`, contract in `packages/strategy/core/` + `docs/architecture/extensibility.md`.\n\n## Architecture principles\n\nCorrectness first. Before coding, confirm the design is sound: surface tradeoffs, pick the simplest option that holds the invariants below, cite authoritative sources for any version/API claim, and push back rather than guess. **Greenfield, not yet deployed** — no back-compat debt, so prefer the correct extensible design over the minimal diff and restructure freely; the invariants still bind, and you name a large refactor before starting it.\n\n## Core invariants\n\n1. **Extensibility.** Strategies are plugins behind `packages/strategy-core`; notifier providers behind `@app/notify`'s `./providers/*` subpath exports. `apps/api`/`apps/worker` MUST NOT import a specific `strategy-*` package or an `@app/notify/providers/*` provider outside their registry bootstrap; `apps/web` MAY import strategy packages for typed event payloads. Adding a strategy = new package + registry entry; adding a notifier = new provider module + `buildNotifyRegistry` entry — never an edit to `apps/api`/`apps/worker`.\n2. **Reliability.** Crash-only. Idempotent jobs. Per-account Binance-rate isolation. No silent failures.\n3. **Approachable & responsive.** Solo operator, not a finance pro: plain language, gloss a trading term the first time it appears, never assume off-screen knowledge. Mobile-first — every view usable at 375×667 — then scale up.\n4. **Account-scoped by row, not convention.**\n   - **Model:** one operator (`users`) → many `accounts` → many `profiles`. `accounts.owner_id → users.id`; each account holds one Binance key pair, one `binance_mode`, one user-data stream. `profiles.account_id` binds each profile to its account, sharing that account's key/wallet/stream.\n   - **Branded two-tier scope:** `AccountScope{db,operatorId,accountId}` from `scopeAccount`, `ProfileScope{db,operatorId,accountId,profileId}` from `scopeProfile` — proves the ownership chain in one query. `UserId` and `AccountId` are distinct branded types (misplacing one won't compile).\n   - **Access rule:** app code reaches account-scoped data ONLY through `profileRepo(...)` / `profileRepoFromScope(scope)` — or `accountRepo(...)` / `accountRepoFromScope` for account-level surfaces like api-keys — from `@app/db`. No manual `(db, operatorId, …)` threading; every account-scoped repo fn takes an `AccountScope`/`ProfileScope` first, minted only by `scopeAccount`/`scopeProfile` — ownership proven exactly once at compile time. Flat `repo.<x>` stays only for operator/global fns (`repo.users.*`, `repo.accounts.create`/`listForOwner`, retention sweeps).\n   - **Routes:** API under `/api/accounts/:accountId/...`, web under `/accounts/$accountId/...`.\n   - **Enforcement:** `packages/db/__tests__/repo/ast-check.test.ts` + cross-account integration tests. No DB-side RLS. See `docs/architecture/{account-isolation,database}.md`.\n   - **Strategy state** is per-(profile, symbol) in storage and in `tick()` — one slice per symbol per call. Cross-symbol state goes through the per-profile KV store: emit `set-kv`/`delete-kv` under a strategy-owned namespaced key and read the merged snapshot back via `TickInput.profileKv` (opt in with `capabilities.needsProfileKv`). A KV write lands for sibling symbols on later ticks, never the same tick.\n\n## Stack\n\nVersions are pinned in `package.json`/lockfiles (the source of truth); names only here.\n\n- Runtime **Bun** · monorepo **Turborepo** · CI: GitHub Actions + GitLab both call `scripts/ci/*.sh`\n- Backend **Hono** · Frontend **Vite + React + TanStack Router/Query + Tailwind + shadcn/ui** (PWA)\n- Charts **lightweight-charts** (financial), **Recharts** (non-financial)\n- DB **Postgres + TimescaleDB** · ORM **Drizzle** · migrations = hand-authored SQL via a checksum-tracked runner\n- Cache/queue **Redis + BullMQ** (`ioredis`, `maxRetriesPerRequest: null`)\n- Auth **Better Auth** — no email/SMTP, no 2FA (single master account). See `docs/architecture/auth.md`.\n- **Encryption-at-rest: none.** Binance keys + notifier secrets stored plaintext; operator IP-allowlists Binance keys. See `docs/architecture/auth.md` \"Threat model\".\n- **Money = decimal.js.** Every price/quantity/amount/balance/pnl is `Decimal` end-to-end; `number` only for counters/indices/ms-timestamps. Snapshots serialise `Decimal` as strings; strategies revive at the boundary.\n- Notifications: pluggable `@app/notify` providers (Slack first).\n- Technicals: in-process `packages/indicators/rating` (vendored MIT); worker `technicals-compute` cron computes ratings locally, no external scanner. See `docs/architecture/technicals.md`.\n- Containers: one multi-stage image (`apps/server/Dockerfile`); `ROLE=all|api|worker|study` selects behaviour. `apps/web` is build-only (api serves the SPA same-origin). Scale split via `docker-compose.scale.yml`.\n- Worker: **single replica today** (`WORKER_REPLICAS=1`), no distributed locks; single-execution = BullMQ `jobId` coalescing + in-process `chainByKey` + idempotent `clientOrderId` + version-aware `symbol_states` CAS. Elastic-pool scale-out plumbing is merged but **dormant**. See `docs/architecture/worker-pipeline.md`.\n\n## Repo layout\n\n```text\napps/         api/ web/ worker/ server/       # server = ROLE-selectable entrypoint (api + worker)\npackages/\n  strategy/    core/ trailing-trade/          # core = contract + Executor; TT = first plugin\n  indicators/                                 # pure, Decimal-typed candle/window math\n  notify/                                     # @app/notify: contract + registry + providers/{slack,telegram,webhook}\n  binance/ db/ contracts/ config/\n  core/                                       # shared runtime utils; subpath exports (./env, …)\ndeploy/compose/  scripts/ci/\n```\n\nDirectory grouping (`packages/strategy/*`) is filesystem-only; **npm names stay flat** (`@app/strategy-trailing-trade`). `@app/notify` is one package; providers are subpath exports.\n\n## Required commands\n\n| Command | Purpose |\n| --- | --- |\n| `bun install` | Install workspace deps. |\n| `bun run dev` | Boot api+web+worker via turbo. |\n| `bun run lint` | oxlint + invariant gates + prettier. Must be clean. |\n| `bun run typecheck` | `tsc --noEmit -b` across all packages. Must be clean. |\n| `bun run test` | Vitest. Per-workspace thresholds are enforced in the complete-suite coverage lane assigned by `COVERAGE_POLICY`. |\n| `bun run test:e2e` | Playwright e2e. |\n| `bun run test:worker-integration` | `apps/worker` integration lane on a laptop. Provisions throwaway Postgres + Redis under `TESTCONTAINERS=1`, so it needs a reachable Docker daemon and prints a notice instead of running when there is none. |\n| `bun run db:migrate` | Apply hand-authored SQL migrations (checksum-tracked runner). |\n| `bun run db:generate` | Not used — repo hand-authors `NNNN_*.sql`. |\n| `bun run setup` | First-time bootstrap: `.env` from `.env.example` + migrate. |\n| `bun run docs:install` | `uv sync --group docs`. Once, before any other `docs:*` command. |\n| `bun run docs:build` | `docs:gen --check` + `uv run mkdocs build --strict`. The docs gate. |\n| `docker compose up` | Local stack: postgres, redis, one `ROLE=all` app. |\n\nDocs build in a **uv-managed Python env** (`pyproject.toml`, `[dependency-groups] docs`), never against whatever `mkdocs` happens to be on `PATH`. Run a bare `mkdocs build --strict` and it picks up an unrelated interpreter and dies on `The \"<plugin>\" plugin is not installed` — that message means the wrong environment, not a broken config, so installing the named plugin by hand is the wrong repair. Go through `bun run docs:build` / `docs:serve`, which invoke mkdocs under `uv run`; `bun run docs:install` (`uv sync --group docs`) creates that env first.\n\n## Linting\n\n**oxlint** is the single lint path (`.oxlintrc.json`, one whole-repo pass so `import/no-cycle` sees the full graph): `correctness` + `typescript`/`import`/`oxc`/`react` plugins, plus repo invariants via `overrides` — strategy/indicator **purity** (`no-restricted-globals`/`-imports`) and the **decimal.js boundary**. `react` is on for `react/no-unstable-nested-components` (a component declared inside another's render body remounts its subtree every render, which clamps `scrollTop` on WebKit); enabling the plugin also arms every `react` **correctness** rule, so `react/exhaustive-deps` is explicitly `off` pending triage of its pre-existing violations. Invariants oxlint can't express are `scripts/ci/*.sh` gates run from `lint.sh`: `no-plugin-leak` (invariant 1), `no-arbitrary-color-token`, `no-phantom-env-var`, `no-invalid-mermaid`, `no-undeclared-workspace-import`, `no-wider-metrics-sink` (only `apps/worker/src/metrics/catalog.ts` may declare the metrics sink; a second one typed `name: string` type-checks and escapes the catalogue), `no-unreviewed-tofixed` (every direct `.toFixed(` and fixed-two `maximumFractionDigits` site under `apps/web/src/**` is registered in `scripts/ci/tofixed-inventory.json` with its pattern identity and a factual value-kind reason; a hard round is how a sub-cent quote balance renders as `0.00`), `no-dropped-lint-rule` (asserts the resolved oxlint config still arms the rules carrying an invariant), `no-backfilled-migration` (a new `NNNN_*.sql` takes the next unused number: the runner applies files in name order, so one inserted below the high-water mark runs LAST on every already-migrated database), `no-mutated-applied-migration` (a shipped `NNNN_*.sql` is immutable; every suite migrates a fresh DB, so only a pinned-digest manifest can see the drift that wedges a deployed one), `no-bun-version-skew` (nine Bun pin sites must agree; Renovate does not reliably cover them, and the gate's reading counts are exact, so a site that stops matching fails loudly instead of silently narrowing the check), `no-blind-walk` (every gate that reads its verdict off a tree walk takes that walk from `scripts/ci/lib/walk.mjs`, whose `collectOrExit` refuses a root declaring no anchor, and carries the `GUARD_ROOT` override seam its self-test needs: a walk that merely narrowed still returns hundreds of files, so the count a gate prints is evidence only once it is proven to still reach the module the rule protects and its stops have been watched fire). See `docs/contributing/coding-rules.md`.\n\n## Quality gates (CI must enforce)\n\n1. Lint clean.\n2. Typecheck clean.\n3. Docs updated when behaviour changes — every claim traces to a source, not memory. Machine-enforced: `bun run docs:build` (`docs:gen --check` for config tables + env-var reference, then `uv run mkdocs build --strict`), `no-invalid-mermaid.sh`, `no-stale-migration-doc.sh`; narrative accuracy stays a review gate. See `docs/contributing/coding-rules.md#documentation-accuracy`.\n4. Per-workspace complete-suite coverage thresholds met (`packages/config/vitest/coverage-policy.js`).\n5. Strategy golden-fixture replay diff = 0.\n6. Playwright lanes green. `browser-bootstrap` proves the four configured browser projects start and reports every skipped app execution with its reason; it does not claim critical-path app coverage. `app-e2e` boots a hermetic ROLE=all stack against a loopback Binance fixture and runs the P0 journey in all four projects; it fails on any skip and on any Binance traffic the fixture does not answer.\n7. `bun run compose:build` succeeds for every app.\n8. Migrations: hand-authored `NNNN_*.sql` applied in order by the checksum-tracked runner (`packages/db/src/migrate.ts`, tracked in `_app_migrations`); the `packages/db` suite runs `migrate()` against real Postgres. A shipped migration is **immutable**, and the two ways to break that fail differently. **Editing** one (a comment counts) moves its digest and wedges every already-migrated database at that file, which no later migration can repair. **Renaming or deleting** one is invisible to the runner, which keys on name: the file re-applies as new and orphans its old ledger row, silently. Change a shipped migration with a new migration; `no-mutated-applied-migration` enforces this because a fresh-DB suite cannot see either case, and `migrate-immutability.test.ts` pins both behaviours. Numbering is the other half of the same contract: a new migration takes the next unused number, never a hole, a letter suffix, or anything below `0001`, because name order is apply order — `no-backfilled-migration` enforces it from the directory listing, the only oracle that can tell a backfill from a fresh tree. See `docs/contributing/coding-rules.md` \"A migration number is never reused and never backfilled\".\n9. JSDoc quality is a review gate, not a lint gate. Every exported or non-trivial function carries a block with one `@param` line per parameter and a `@returns` line when it returns a value. The prose above the tags carries the WHY; each tag glosses what that value _means_ — no name-restatements (`@param symbol - The symbol`), no blank blocks. A name that is a misnomer or an easy misread makes its `@param` line the only place a reader is told, so it is required there even when the type looks self-evident.\n\n   **Do not hard-wrap comments.** One paragraph is one line, and one `@param` / `@returns` tag is one line, however long it runs — in JSDoc blocks and in `//` comments alike. This is the `proseWrap: 'never'` rule `prettier.config.mjs` already applies to Markdown, for the same reason: hard-wrapped prose turns every later edit into a multi-line reflow, so the diff hides the sentence that actually changed. Editors soft-wrap; diffs do not. Nothing enforces this — `printWidth` governs code only, and prettier never reflows a comment body — so it is a review gate, and existing wrapped comments are left alone until the code around them is touched. Line breaks are for meaning, not width: a bulleted list, a `- x:` table, or an ASCII diagram keeps its breaks.\n\n## Anti-patterns to refuse\n\n- **NotifyProvider single source of truth.** The shape lives in `@app/notify`; `apps/api` consumes `notifyProviders.describeAll()`, never its own provider interface.\n- **No distributed locks.** No `redlock`, `intents:` Redis sets, soft balance reservation, or equivalents (`scripts/ci/no-locks.sh` fails, also scanning `packages/binance`). Permitted shared-Redis primitives are rate-limit / visibility, not locks (no owner, no release/refund): the consume-and-decay request-weight bucket and the self-expiring `SET NX PX` notifier-gap throttle.\n- **No plaintext API keys on disk** outside the Postgres exception, and only under the `docs/architecture/auth.md` mitigations (single-tenant, operator-side Binance IP-allowlist). Any new storage surface (backups, replicas, exports, multi-tenant) requires encryption-at-rest first.\n- **No secret in a `LIVE_DEMO` response.** The public demo injects the sole operator id, so every route not behind `requireNotDemo` is reachable anonymously. A credential-equivalent value — Binance API key, notifier **webhook URL**, bot token, auth header, AI-provider key — MUST NOT serialise into any response body. Strip it at the projection boundary (api-key → `last4`, ai-provider → `has*` booleans, notifiers declare secrets in `secretFields`) so a leak is impossible by construction. `requireNotDemo` is a blocklist / defence-in-depth: a new secret-reading or credential-writing route is exposed by default — add it to the deny-list AND to the completeness assertion in `apps/api/__tests__/routes/live-demo-guard.test.ts`.\n- **No speculative abstraction.** Feature flags, configurability, or \"for the future\" indirection require a tracked issue with a concrete consumer.\n- **Money math = `Decimal`.** `number` (IEEE-754) is banned downstream of `packages/{strategy-*,indicators,contracts}`; the decimal.js import boundary is lint-enforced (oxlint `no-restricted-imports`).\n- **`Decision` union stays generic.** `noop` / `place-order` / `cancel-order` / `replace-order` / `emit-event` / `set-kv` / `delete-kv` (`packages/strategy/core/src/decision.ts`). Generic exchange-request shapes such as `replace-order` belong in the union; strategy-specific side effects ride `emit-event` with namespaced keys, not new strategy-specific variants; cross-symbol coupling rides `set-kv` / `delete-kv`. A removed variant takes its worker-side port with it — no idle seams.\n- **Strategy & indicators are pure.** No `ioredis` / `pg` / `drizzle-orm` / `fs` / `net` / `http` / `crypto` / `Date` / `Math.random` inside `packages/strategy-*/src/**` or `packages/indicators/src/**`; no I/O inside `tick()` (the worker injects `Clock` and `RNG`). `decimal.js` is allowed.\n- **Comments say why, not what.** No Spec / Issue / Phase / `@see` refs in code; commits and PRs own that. CI rejects.\n- **Tests live in `<pkg>/__tests__/`,** never beside `src/`. CI rejects.\n- **New shared runtime utilities go into `@app/core` via a new subpath, not a new package** — unless the utility has a clear domain boundary (strategy, notifier, binance, db, contracts).\n","CLAUDE.md":"AGENTS.md"},"files":{".claude/rules/principles.md":"# Engineering principles\n\nGeneric engineering rules for this repo. Project-specific invariants live in `CLAUDE.md`.\n\n## Think before coding\n\nDon't assume. Don't hide confusion. Surface tradeoffs.\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n## Simplicity first\n\nMinimum code that solves the problem. Nothing speculative.\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n- Ask: \"Would a senior engineer say this is overcomplicated?\" If yes, simplify.\n\n## Goal-driven execution\n\nDefine success criteria. Loop until verified.\n\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\nFor multi-step tasks, state a brief plan:\n\n1. [Step] → verify: [check]\n2. [Step] → verify: [check]\n",".claude/skills/feature-walkthrough/SKILL.md":"---\nname: feature-walkthrough\ndescription: Autonomously test the running binance-trading-bot app in a real browser. The agent drives the browser itself via the Playwright MCP - logs in, looks at each screen, and works through every feature end-to-end like a real operator, finding and fixing bugs. Use when asked to test the app, smoke-test or walk through the UI, verify the frontend end-to-end, or check that features actually work in a browser.\n---\n\n# Feature walkthrough\n\nAutonomously exercise the real app in a real browser. **You** are the test driver: open the browser, look at the screen, decide what to click, observe the result, decide the next step. This is agentic, vision-driven testing - not a pre-written script.\n\nThe goal is \"the feature works when a person uses it\". Type-checks and unit tests verify code correctness, not feature correctness.\n\n**Test as a non-expert operator.** Walk the app as a user who is _not_ a financial or trading expert. The product must be **easy but accurate**: precise and correct under the hood, yet usable by someone who does not already know the strategy. Unexplained jargon, a setting with no inline help, a number with no units or context, a flow that only a strategy-literate user can complete - each is a real UI/UX finding, not cosmetic.\n\n**This is a greenfield project - refactor freely.** Nothing is deployed; there is no legacy to protect and no shipped behaviour to preserve. When a finding exposes mis-architecture, a performance problem, or a UI/UX flow that is wrong at the structural level, the sanctioned fix is a **large refactor**, not a surgical patch. Do not paper over a bad design to keep the diff small - the bar is the correct architecture, correct code, tested and performant. Surface the scope of a big refactor, confirm, then do it properly.\n\nThis skill is stateless and self-contained - no sibling tracker files. Discover features fresh each run; fix what you can this run; file the rest as GitLab issues. Deterministic regression tests do NOT belong here - they live in `e2e/tests/` as Playwright specs (`manual-order-roundtrip.spec.ts`, `profile-controls.spec.ts`, ...). This skill is the exploratory layer that adapts to whatever the UI actually shows.\n\n## 1. Preflight\n\n1. **Browser tools.** Needs the Playwright MCP `browser_*` tools (`browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_select_option`, `browser_press_key`, `browser_wait_for`, `browser_resize`, `browser_console_messages`, `browser_network_requests`, `browser_take_screenshot`). If unavailable, stop and tell the operator to enable the Playwright MCP - the skill cannot run without it.\n2. **Stack.** Postgres + Redis must be up - `docker ps` for `binance-trading-bot-postgres-1` and `-redis-1`; if absent, `docker compose -f deploy/compose/docker-compose.yml up -d db redis`. Apply migrations: `bun run db:migrate`.\n3. **Dev server.** If not up: `bun run dev > /tmp/btb-dev.log 2>&1 &`. Ports: api `:3000`, web `:5173`, worker `:9101`. **`bun --watch` does NOT reload workspace `packages/*`** - after editing anything under `packages/`, or if the server was started before a recent merge, restart `bun run dev` or it serves stale code and a correct fix looks broken. Vite HMR (`apps/web`) reloads reliably.\n\n## 2. Log in and orient\n\n1. `browser_navigate` to `http://localhost:5173/login`.\n2. `browser_snapshot` the login form. Type the dev credentials - request the executor credentials - into the real fields and click submit. (If login reports no account, create the master account at `/onboarding`.)\n3. Log in **once** - the API rate-limits `/api/auth/sign-in/*` (60s window). Reuse the same session for the whole run; a wrong-password test also counts against the window, so run it after the real login.\n4. `browser_snapshot` the dashboard. This is your map: the nav, the profile list, the action surfaces. Cross-reference `apps/web/src/routes/` for routes not linked from the dashboard.\n\n## 3. Discover - fresh, every run\n\nBuild the feature set live each run; there is no coverage file to read:\n\n1. From the dashboard snapshot, walk every nav target and action surface (`browser_snapshot` each screen).\n2. Enumerate routes from `apps/web/src/routes/` - anything not reachable by clicking is still a feature.\n3. Treat each distinct end-to-end thing an operator can _do_ (place an order, switch strategy, save a notifier, run the wizard, ...) as one feature.\n\n## 4. Pick a batch\n\nFix a **substantial batch per run - ~5–8 items**, sized by reviewable diff, shipped as one MR(Gitlab) or PR(GitHub). CI and senior-review cost the same per MR whether it carries one fix or many, so a thin batch wastes the cycle. Priority order, highest first:\n\n1. Defects already seen this run.\n2. Features closest to the money path: orders > config > notifiers > cosmetic.\n3. Sub-bar UX/DevX small enough to fold in.\n\nRules: a genuinely large or risky item (whole-feature work, refactor, \"multiple valid approaches\" territory) ships **solo** in its own MR - never padded with or batched against small fixes. A batch may span files and themes; keep commit history legible (one commit per item or theme). Each batched item is still walked and verified per §5.\n\n## 5. Walk it - run this loop yourself, do not pre-script it\n\nRun this loop for **each item in the batch**. Items are independent; do them in turn.\n\n1. **Navigate** to the feature's entry screen.\n2. **Observe** - `browser_snapshot` (accessibility tree, gives clickable refs); `browser_take_screenshot` when layout matters. Refs are snapshot-scoped: re-snapshot after every navigation or re-render or a stale ref clicks the wrong thing.\n3. **Decide** the next interaction from what you see - the real button, input, selector.\n4. **Act** - `browser_click` / `browser_type` / `browser_select_option` / `browser_press_key`. Real interactions only. Destructive actions (kill-switch, strategy switch) open a confirm modal - snapshot the dialog and click its Confirm; the action is not done until you do.\n   - If `browser_click` reports success but nothing happens (no events reach the page), the long session's click coordinates have desynced - a tool artifact, not an app bug: `browser_close` then re-`browser_navigate` for a fresh context, and the click works again.\n5. **Verify** - snapshot again. Assert the concrete outcome: a banner, a badge, a redirected URL, a new row, a value that survives a reload.\n6. **Loop** to step 2 until the journey is complete or it visibly breaks.\n7. **Check silent failure** - `browser_console_messages` and `browser_network_requests`: unexpected console errors or API responses ≥ 400 are findings even when the screen looked fine.\n8. **Restore** every mutation through the UI - release kill-switches, revert config edits, delete test profiles. Leave the dev DB as you found it.\n\n**UX/DevX is a first-class finding, not cosmetic.** This is an operator's trading tool. At every Verify step, judge the screen as a user: is the layout clean and aligned, the hierarchy clear, the primary action obvious, are loading/empty/error states handled, is copy precise, the flow short and unsurprising? Read the pixels with `browser_take_screenshot`, not only the accessibility tree.\n\n**Non-expert lens.** Assume the user does not know trading. Jargon without explanation, a field with no inline help, a value with no units/context, or a flow that only a strategy-literate user can finish is a defect - fix or file it. \"Easy but accurate\": friendly defaults, plain-language labels, inline help, actionable errors, without losing correctness.\n\n**DevX counts the same.** Every setting needs a proper form UI with typed inputs, inline help, and per-field validation - a raw JSON editor is itself a UX defect: flag and replace it. Validation errors must say exactly what is wrong and where; defaults must be sane; setup/recovery must not require reading source. A cryptic error, a silent rejection, or a flow only a code-reader can complete is a DevX finding.\n\nCross-cutting checks while you are in there:\n\n- Wrong-password login → friendly error, not a crash. Do this **after** the real login or it burns the rate-limit window.\n- Resize to 375×667 (`browser_resize`) on every screen you visit → no horizontal overflow, and the mobile layout must be genuinely usable, not merely non-overflowing (mobile-first is a core invariant).\n\n**Dev-env boundary:** there is no live Binance connection in dev, and manual orders enqueue but may not fill. Verify what dev _can_ prove (enqueue, UI state, DB row) and state the boundary - do not file expected env state as a bug.\n\n## 6. Triage - one verdict per observation\n\n- **Fix it this run** - a small, contained defect, UI/UX, or DevX finding. Fix the root cause (§7).\n- **File a GitLab issue** - a real finding too large/ambiguous for this batch, or sub-bar-but-not-broken. Title + location + severity + repro steps. This is the durable backlog; there is no local tracker file.\n- **Leave it** - Cite the doc; do not act.\n- **Re-observe** - your own misread: wrong screen, stale dev server, a moved control, a desynced click (§5). Do not touch the app.\n- **Not a bug** - expected dev-env state: empty data, \"Never\" last-tick, 502s from Binance, 404s for unconfigured notifiers.\n\nA bug is never parked as a \"known issue\" to work around - it is fixed this run or filed as an issue to fix later.\n\n## 7. Fix - root cause, not symptom\n\nFix every batched item at the root. This is a greenfield project - no legacy to protect, no shipped behaviour to preserve - so **large refactoring is sanctioned and expected** wherever a finding warrants it. Mis-architecture (wrong abstraction, a pattern that should not exist, a missing layer), a performance problem, or a structurally-wrong UI/UX flow are all valid triggers for a big rewrite, not a patch. Do not paper over a bad design with a surgical fix that leaves the bad design in place. The bar is correct architecture, correct code, tested, performant - not minimal diff. A change that large is \"multiple valid approaches\" territory: surface the scope and design, confirm, then build. Keep `bun run typecheck`, `bun run lint`, and `bun run test` clean, and re-walk each affected feature in the browser. Loop until every batched item works.\n\nWhen a bug is worth locking down, add a deterministic spec to `e2e/tests/` (follow `profile-controls.spec.ts`) - that is the regression layer; this skill is not.\n","AGENTS.md":"# Engineering charter\n\n> Charter for any coding agent in this repo (Claude Code, Cursor, Codex, …). Keep it short — architecture, config, and ops live in `docs/` (built by `mkdocs`). Generic engineering principles (think-first, simplicity, goal-driven): `.claude/rules/principles.md`.\n\n## Project\n\n`binance-trading-bot`: single-operator, multi-account, multi-profile, reliability-first, mobile-first trading platform on Binance. One operator owns N first-class Binance accounts (each = one API key pair + one environment + one user-data stream); each account runs N strategy **profiles** against its shared wallet. Strategy is a **plugin** — trailing-trade is the first; source of truth is `packages/strategy/trailing-trade/`, contract in `packages/strategy/core/` + `docs/architecture/extensibility.md`.\n\n## Architecture principles\n\nCorrectness first. Before coding, confirm the design is sound: surface tradeoffs, pick the simplest option that holds the invariants below, cite authoritative sources for any version/API claim, and push back rather than guess. **Greenfield, not yet deployed** — no back-compat debt, so prefer the correct extensible design over the minimal diff and restructure freely; the invariants still bind, and you name a large refactor before starting it.\n\n## Core invariants\n\n1. **Extensibility.** Strategies are plugins behind `packages/strategy-core`; notifier providers behind `@app/notify`'s `./providers/*` subpath exports. `apps/api`/`apps/worker` MUST NOT import a specific `strategy-*` package or an `@app/notify/providers/*` provider outside their registry bootstrap; `apps/web` MAY import strategy packages for typed event payloads. Adding a strategy = new package + registry entry; adding a notifier = new provider module + `buildNotifyRegistry` entry — never an edit to `apps/api`/`apps/worker`.\n2. **Reliability.** Crash-only. Idempotent jobs. Per-account Binance-rate isolation. No silent failures.\n3. **Approachable & responsive.** Solo operator, not a finance pro: plain language, gloss a trading term the first time it appears, never assume off-screen knowledge. Mobile-first — every view usable at 375×667 — then scale up.\n4. **Account-scoped by row, not convention.**\n   - **Model:** one operator (`users`) → many `accounts` → many `profiles`. `accounts.owner_id → users.id`; each account holds one Binance key pair, one `binance_mode`, one user-data stream. `profiles.account_id` binds each profile to its account, sharing that account's key/wallet/stream.\n   - **Branded two-tier scope:** `AccountScope{db,operatorId,accountId}` from `scopeAccount`, `ProfileScope{db,operatorId,accountId,profileId}` from `scopeProfile` — proves the ownership chain in one query. `UserId` and `AccountId` are distinct branded types (misplacing one won't compile).\n   - **Access rule:** app code reaches account-scoped data ONLY through `profileRepo(...)` / `profileRepoFromScope(scope)` — or `accountRepo(...)` / `accountRepoFromScope` for account-level surfaces like api-keys — from `@app/db`. No manual `(db, operatorId, …)` threading; every account-scoped repo fn takes an `AccountScope`/`ProfileScope` first, minted only by `scopeAccount`/`scopeProfile` — ownership proven exactly once at compile time. Flat `repo.<x>` stays only for operator/global fns (`repo.users.*`, `repo.accounts.create`/`listForOwner`, retention sweeps).\n   - **Routes:** API under `/api/accounts/:accountId/...`, web under `/accounts/$accountId/...`.\n   - **Enforcement:** `packages/db/__tests__/repo/ast-check.test.ts` + cross-account integration tests. No DB-side RLS. See `docs/architecture/{account-isolation,database}.md`.\n   - **Strategy state** is per-(profile, symbol) in storage and in `tick()` — one slice per symbol per call. Cross-symbol state goes through the per-profile KV store: emit `set-kv`/`delete-kv` under a strategy-owned namespaced key and read the merged snapshot back via `TickInput.profileKv` (opt in with `capabilities.needsProfileKv`). A KV write lands for sibling symbols on later ticks, never the same tick.\n\n## Stack\n\nVersions are pinned in `package.json`/lockfiles (the source of truth); names only here.\n\n- Runtime **Bun** · monorepo **Turborepo** · CI: GitHub Actions + GitLab both call `scripts/ci/*.sh`\n- Backend **Hono** · Frontend **Vite + React + TanStack Router/Query + Tailwind + shadcn/ui** (PWA)\n- Charts **lightweight-charts** (financial), **Recharts** (non-financial)\n- DB **Postgres + TimescaleDB** · ORM **Drizzle** · migrations = hand-authored SQL via a checksum-tracked runner\n- Cache/queue **Redis + BullMQ** (`ioredis`, `maxRetriesPerRequest: null`)\n- Auth **Better Auth** — no email/SMTP, no 2FA (single master account). See `docs/architecture/auth.md`.\n- **Encryption-at-rest: none.** Binance keys + notifier secrets stored plaintext; operator IP-allowlists Binance keys. See `docs/architecture/auth.md` \"Threat model\".\n- **Money = decimal.js.** Every price/quantity/amount/balance/pnl is `Decimal` end-to-end; `number` only for counters/indices/ms-timestamps. Snapshots serialise `Decimal` as strings; strategies revive at the boundary.\n- Notifications: pluggable `@app/notify` providers (Slack first).\n- Technicals: in-process `packages/indicators/rating` (vendored MIT); worker `technicals-compute` cron computes ratings locally, no external scanner. See `docs/architecture/technicals.md`.\n- Containers: one multi-stage image (`apps/server/Dockerfile`); `ROLE=all|api|worker|study` selects behaviour. `apps/web` is build-only (api serves the SPA same-origin). Scale split via `docker-compose.scale.yml`.\n- Worker: **single replica today** (`WORKER_REPLICAS=1`), no distributed locks; single-execution = BullMQ `jobId` coalescing + in-process `chainByKey` + idempotent `clientOrderId` + version-aware `symbol_states` CAS. Elastic-pool scale-out plumbing is merged but **dormant**. See `docs/architecture/worker-pipeline.md`.\n\n## Repo layout\n\n```text\napps/         api/ web/ worker/ server/       # server = ROLE-selectable entrypoint (api + worker)\npackages/\n  strategy/    core/ trailing-trade/          # core = contract + Executor; TT = first plugin\n  indicators/                                 # pure, Decimal-typed candle/window math\n  notify/                                     # @app/notify: contract + registry + providers/{slack,telegram,webhook}\n  binance/ db/ contracts/ config/\n  core/                                       # shared runtime utils; subpath exports (./env, …)\ndeploy/compose/  scripts/ci/\n```\n\nDirectory grouping (`packages/strategy/*`) is filesystem-only; **npm names stay flat** (`@app/strategy-trailing-trade`). `@app/notify` is one package; providers are subpath exports.\n\n## Required commands\n\n| Command | Purpose |\n| --- | --- |\n| `bun install` | Install workspace deps. |\n| `bun run dev` | Boot api+web+worker via turbo. |\n| `bun run lint` | oxlint + invariant gates + prettier. Must be clean. |\n| `bun run typecheck` | `tsc --noEmit -b` across all packages. Must be clean. |\n| `bun run test` | Vitest. Per-workspace thresholds are enforced in the complete-suite coverage lane assigned by `COVERAGE_POLICY`. |\n| `bun run test:e2e` | Playwright e2e. |\n| `bun run test:worker-integration` | `apps/worker` integration lane on a laptop. Provisions throwaway Postgres + Redis under `TESTCONTAINERS=1`, so it needs a reachable Docker daemon and prints a notice instead of running when there is none. |\n| `bun run db:migrate` | Apply hand-authored SQL migrations (checksum-tracked runner). |\n| `bun run db:generate` | Not used — repo hand-authors `NNNN_*.sql`. |\n| `bun run setup` | First-time bootstrap: `.env` from `.env.example` + migrate. |\n| `bun run docs:install` | `uv sync --group docs`. Once, before any other `docs:*` command. |\n| `bun run docs:build` | `docs:gen --check` + `uv run mkdocs build --strict`. The docs gate. |\n| `docker compose up` | Local stack: postgres, redis, one `ROLE=all` app. |\n\nDocs build in a **uv-managed Python env** (`pyproject.toml`, `[dependency-groups] docs`), never against whatever `mkdocs` happens to be on `PATH`. Run a bare `mkdocs build --strict` and it picks up an unrelated interpreter and dies on `The \"<plugin>\" plugin is not installed` — that message means the wrong environment, not a broken config, so installing the named plugin by hand is the wrong repair. Go through `bun run docs:build` / `docs:serve`, which invoke mkdocs under `uv run`; `bun run docs:install` (`uv sync --group docs`) creates that env first.\n\n## Linting\n\n**oxlint** is the single lint path (`.oxlintrc.json`, one whole-repo pass so `import/no-cycle` sees the full graph): `correctness` + `typescript`/`import`/`oxc`/`react` plugins, plus repo invariants via `overrides` — strategy/indicator **purity** (`no-restricted-globals`/`-imports`) and the **decimal.js boundary**. `react` is on for `react/no-unstable-nested-components` (a component declared inside another's render body remounts its subtree every render, which clamps `scrollTop` on WebKit); enabling the plugin also arms every `react` **correctness** rule, so `react/exhaustive-deps` is explicitly `off` pending triage of its pre-existing violations. Invariants oxlint can't express are `scripts/ci/*.sh` gates run from `lint.sh`: `no-plugin-leak` (invariant 1), `no-arbitrary-color-token`, `no-phantom-env-var`, `no-invalid-mermaid`, `no-undeclared-workspace-import`, `no-wider-metrics-sink` (only `apps/worker/src/metrics/catalog.ts` may declare the metrics sink; a second one typed `name: string` type-checks and escapes the catalogue), `no-unreviewed-tofixed` (every direct `.toFixed(` and fixed-two `maximumFractionDigits` site under `apps/web/src/**` is registered in `scripts/ci/tofixed-inventory.json` with its pattern identity and a factual value-kind reason; a hard round is how a sub-cent quote balance renders as `0.00`), `no-dropped-lint-rule` (asserts the resolved oxlint config still arms the rules carrying an invariant), `no-backfilled-migration` (a new `NNNN_*.sql` takes the next unused number: the runner applies files in name order, so one inserted below the high-water mark runs LAST on every already-migrated database), `no-mutated-applied-migration` (a shipped `NNNN_*.sql` is immutable; every suite migrates a fresh DB, so only a pinned-digest manifest can see the drift that wedges a deployed one), `no-bun-version-skew` (nine Bun pin sites must agree; Renovate does not reliably cover them, and the gate's reading counts are exact, so a site that stops matching fails loudly instead of silently narrowing the check), `no-blind-walk` (every gate that reads its verdict off a tree walk takes that walk from `scripts/ci/lib/walk.mjs`, whose `collectOrExit` refuses a root declaring no anchor, and carries the `GUARD_ROOT` override seam its self-test needs: a walk that merely narrowed still returns hundreds of files, so the count a gate prints is evidence only once it is proven to still reach the module the rule protects and its stops have been watched fire). See `docs/contributing/coding-rules.md`.\n\n## Quality gates (CI must enforce)\n\n1. Lint clean.\n2. Typecheck clean.\n3. Docs updated when behaviour changes — every claim traces to a source, not memory. Machine-enforced: `bun run docs:build` (`docs:gen --check` for config tables + env-var reference, then `uv run mkdocs build --strict`), `no-invalid-mermaid.sh`, `no-stale-migration-doc.sh`; narrative accuracy stays a review gate. See `docs/contributing/coding-rules.md#documentation-accuracy`.\n4. Per-workspace complete-suite coverage thresholds met (`packages/config/vitest/coverage-policy.js`).\n5. Strategy golden-fixture replay diff = 0.\n6. Playwright lanes green. `browser-bootstrap` proves the four configured browser projects start and reports every skipped app execution with its reason; it does not claim critical-path app coverage. `app-e2e` boots a hermetic ROLE=all stack against a loopback Binance fixture and runs the P0 journey in all four projects; it fails on any skip and on any Binance traffic the fixture does not answer.\n7. `bun run compose:build` succeeds for every app.\n8. Migrations: hand-authored `NNNN_*.sql` applied in order by the checksum-tracked runner (`packages/db/src/migrate.ts`, tracked in `_app_migrations`); the `packages/db` suite runs `migrate()` against real Postgres. A shipped migration is **immutable**, and the two ways to break that fail differently. **Editing** one (a comment counts) moves its digest and wedges every already-migrated database at that file, which no later migration can repair. **Renaming or deleting** one is invisible to the runner, which keys on name: the file re-applies as new and orphans its old ledger row, silently. Change a shipped migration with a new migration; `no-mutated-applied-migration` enforces this because a fresh-DB suite cannot see either case, and `migrate-immutability.test.ts` pins both behaviours. Numbering is the other half of the same contract: a new migration takes the next unused number, never a hole, a letter suffix, or anything below `0001`, because name order is apply order — `no-backfilled-migration` enforces it from the directory listing, the only oracle that can tell a backfill from a fresh tree. See `docs/contributing/coding-rules.md` \"A migration number is never reused and never backfilled\".\n9. JSDoc quality is a review gate, not a lint gate. Every exported or non-trivial function carries a block with one `@param` line per parameter and a `@returns` line when it returns a value. The prose above the tags carries the WHY; each tag glosses what that value _means_ — no name-restatements (`@param symbol - The symbol`), no blank blocks. A name that is a misnomer or an easy misread makes its `@param` line the only place a reader is told, so it is required there even when the type looks self-evident.\n\n   **Do not hard-wrap comments.** One paragraph is one line, and one `@param` / `@returns` tag is one line, however long it runs — in JSDoc blocks and in `//` comments alike. This is the `proseWrap: 'never'` rule `prettier.config.mjs` already applies to Markdown, for the same reason: hard-wrapped prose turns every later edit into a multi-line reflow, so the diff hides the sentence that actually changed. Editors soft-wrap; diffs do not. Nothing enforces this — `printWidth` governs code only, and prettier never reflows a comment body — so it is a review gate, and existing wrapped comments are left alone until the code around them is touched. Line breaks are for meaning, not width: a bulleted list, a `- x:` table, or an ASCII diagram keeps its breaks.\n\n## Anti-patterns to refuse\n\n- **NotifyProvider single source of truth.** The shape lives in `@app/notify`; `apps/api` consumes `notifyProviders.describeAll()`, never its own provider interface.\n- **No distributed locks.** No `redlock`, `intents:` Redis sets, soft balance reservation, or equivalents (`scripts/ci/no-locks.sh` fails, also scanning `packages/binance`). Permitted shared-Redis primitives are rate-limit / visibility, not locks (no owner, no release/refund): the consume-and-decay request-weight bucket and the self-expiring `SET NX PX` notifier-gap throttle.\n- **No plaintext API keys on disk** outside the Postgres exception, and only under the `docs/architecture/auth.md` mitigations (single-tenant, operator-side Binance IP-allowlist). Any new storage surface (backups, replicas, exports, multi-tenant) requires encryption-at-rest first.\n- **No secret in a `LIVE_DEMO` response.** The public demo injects the sole operator id, so every route not behind `requireNotDemo` is reachable anonymously. A credential-equivalent value — Binance API key, notifier **webhook URL**, bot token, auth header, AI-provider key — MUST NOT serialise into any response body. Strip it at the projection boundary (api-key → `last4`, ai-provider → `has*` booleans, notifiers declare secrets in `secretFields`) so a leak is impossible by construction. `requireNotDemo` is a blocklist / defence-in-depth: a new secret-reading or credential-writing route is exposed by default — add it to the deny-list AND to the completeness assertion in `apps/api/__tests__/routes/live-demo-guard.test.ts`.\n- **No speculative abstraction.** Feature flags, configurability, or \"for the future\" indirection require a tracked issue with a concrete consumer.\n- **Money math = `Decimal`.** `number` (IEEE-754) is banned downstream of `packages/{strategy-*,indicators,contracts}`; the decimal.js import boundary is lint-enforced (oxlint `no-restricted-imports`).\n- **`Decision` union stays generic.** `noop` / `place-order` / `cancel-order` / `replace-order` / `emit-event` / `set-kv` / `delete-kv` (`packages/strategy/core/src/decision.ts`). Generic exchange-request shapes such as `replace-order` belong in the union; strategy-specific side effects ride `emit-event` with namespaced keys, not new strategy-specific variants; cross-symbol coupling rides `set-kv` / `delete-kv`. A removed variant takes its worker-side port with it — no idle seams.\n- **Strategy & indicators are pure.** No `ioredis` / `pg` / `drizzle-orm` / `fs` / `net` / `http` / `crypto` / `Date` / `Math.random` inside `packages/strategy-*/src/**` or `packages/indicators/src/**`; no I/O inside `tick()` (the worker injects `Clock` and `RNG`). `decimal.js` is allowed.\n- **Comments say why, not what.** No Spec / Issue / Phase / `@see` refs in code; commits and PRs own that. CI rejects.\n- **Tests live in `<pkg>/__tests__/`,** never beside `src/`. CI rejects.\n- **New shared runtime utilities go into `@app/core` via a new subpath, not a new package** — unless the utility has a clear domain boundary (strategy, notifier, binance, db, contracts).\n","CLAUDE.md":"AGENTS.md"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/chrisleekr/binance-trading-bot/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# Engineering charter\n\n> Charter for any coding agent in this repo (Claude Code, Cursor, Codex, …). Keep it short — architecture, config, and ops live in `docs/` (built by `mkdocs`). Generic engineering principles (think-first, simplicity, goal-driven): `.claude/rules/principles.md`.\n\n## Project\n\n`binance-trading-bot`: single-operator, multi-account, multi-profile, reliability-first, mobile-first trading platform on Binance. One operator owns N first-class Binance accounts (each = one API key pair + one environment + one user-data stream); each account runs N strategy **profiles** against its shared wallet. Strategy is a **plugin** — trailing-trade is the first; source of truth is `packages/strategy/trailing-trade/`, contract in `packages/strategy/core/` + `docs/architecture/extensibility.md`.\n\n## Architecture principles\n\nCorrectness first. Before coding, confirm the design is sound: surface tradeoffs, pick the simplest option that holds the invariants below, cite authoritative sources for any version/API claim, and push back rather than guess. **Greenfield, not yet deployed** — no back-compat debt, so prefer the correct extensible design over the minimal diff and restructure freely; the invariants still bind, and you name a large refactor before starting it.\n\n## Core invariants\n\n1. **Extensibility.** Strategies are plugins behind `packages/strategy-core`; notifier providers behind `@app/notify`'s `./providers/*` subpath exports. `apps/api`/`apps/worker` MUST NOT import a specific `strategy-*` package or an `@app/notify/providers/*` provider outside their registry bootstrap; `apps/web` MAY import strategy packages for typed event payloads. Adding a strategy = new package + registry entry; adding a notifier = new provider module + `buildNotifyRegistry` entry — never an edit to `apps/api`/`apps/worker`.\n2. **Reliability.** Crash-only. Idempotent jobs. Per-account Binance-rate isolation. No silent failures.\n3. **Approachable & responsive.** Solo operator, not a finance pro: plain language, gloss a trading term the first time it appears, never assume off-screen knowledge. Mobile-first — every view usable at 375×667 — then scale up.\n4. **Account-scoped by row, not convention.**\n   - **Model:** one operator (`users`) → many `accounts` → many `profiles`. `accounts.owner_id → users.id`; each account holds one Binance key pair, one `binance_mode`, one user-data stream. `profiles.account_id` binds each profile to its account, sharing that account's key/wallet/stream.\n   - **Branded two-tier scope:** `AccountScope{db,operatorId,accountId}` from `scopeAccount`, `ProfileScope{db,operatorId,accountId,profileId}` from `scopeProfile` — proves the ownership chain in one query. `UserId` and `AccountId` are distinct branded types (misplacing one won't compile).\n   - **Access rule:** app code reaches account-scoped data ONLY through `profileRepo(...)` / `profileRepoFromScope(scope)` — or `accountRepo(...)` / `accountRepoFromScope` for account-level surfaces like api-keys — from `@app/db`. No manual `(db, operatorId, …)` threading; every account-scoped repo fn takes an `AccountScope`/`ProfileScope` first, minted only by `scopeAccount`/`scopeProfile` — ownership proven exactly once at compile time. Flat `repo.<x>` stays only for operator/global fns (`repo.users.*`, `repo.accounts.create`/`listForOwner`, retention sweeps).\n   - **Routes:** API under `/api/accounts/:accountId/...`, web under `/accounts/$accountId/...`.\n   - **Enforcement:** `packages/db/__tests__/repo/ast-check.test.ts` + cross-account integration tests. No DB-side RLS. See `docs/architecture/{account-isolation,database}.md`.\n   - **Strategy state** is per-(profile, symbol) in storage and in `tick()` — one slice per symbol per call. Cross-symbol state goes through the per-profile KV store: emit `set-kv`/`delete-kv` under a strategy-owned namespaced key and read the merged snapshot back via `TickInput.profileKv` (opt in with `capabilities.needsProfileKv`). A KV write lands for sibling symbols on later ticks, never the same tick.\n\n## Stack\n\nVersions are pinned in `package.json`/lockfiles (the source of truth); names only here.\n\n- Runtime **Bun** · monorepo **Turborepo** · CI: GitHub Actions + GitLab both call `scripts/ci/*.sh`\n- Backend **Hono** · Frontend **Vite + React + TanStack Router/Query + Tailwind + shadcn/ui** (PWA)\n- Charts **lightweight-charts** (financial), **Recharts** (non-financial)\n- DB **Postgres + TimescaleDB** · ORM **Drizzle** · migrations = hand-authored SQL via a checksum-tracked runner\n- Cache/queue **Redis + BullMQ** (`ioredis`, `maxRetriesPerRequest: null`)\n- Auth **Better Auth** — no email/SMTP, no 2FA (single master account). See `docs/architecture/auth.md`.\n- **Encryption-at-rest: none.** Binance keys + notifier secrets stored plaintext; operator IP-allowlists Binance keys. See `docs/architecture/auth.md` \"Threat model\".\n- **Money = decimal.js.** Every price/quantity/amount/balance/pnl is `Decimal` end-to-end; `number` only for counters/indices/ms-timestamps. Snapshots serialise `Decimal` as strings; strategies revive at the boundary.\n- Notifications: pluggable `@app/notify` providers (Slack first).\n- Technicals: in-process `packages/indicators/rating` (vendored MIT); worker `technicals-compute` cron computes ratings locally, no external scanner. See `docs/architecture/technicals.md`.\n- Containers: one multi-stage image (`apps/server/Dockerfile`); `ROLE=all|api|worker|study` selects behaviour. `apps/web` is build-only (api serves the SPA same-origin). Scale split via `docker-compose.scale.yml`.\n- Worker: **single replica today** (`WORKER_REPLICAS=1`), no distributed locks; single-execution = BullMQ `jobId` coalescing + in-process `chainByKey` + idempotent `clientOrderId` + version-aware `symbol_states` CAS. Elastic-pool scale-out plumbing is merged but **dormant**. See `docs/architecture/worker-pipeline.md`.\n\n## Repo layout\n\n```text\napps/         api/ web/ worker/ server/       # server = ROLE-selectable entrypoint (api + worker)\npackages/\n  strategy/    core/ trailing-trade/          # core = contract + Executor; TT = first plugin\n  indicators/                                 # pure, Decimal-typed candle/window math\n  notify/                                     # @app/notify: contract + registry + providers/{slack,telegram,webhook}\n  binance/ db/ contracts/ config/\n  core/                                       # shared runtime utils; subpath exports (./env, …)\ndeploy/compose/  scripts/ci/\n```\n\nDirectory grouping (`packages/strategy/*`) is filesystem-only; **npm names stay flat** (`@app/strategy-trailing-trade`). `@app/notify` is one package; providers are subpath exports.\n\n## Required commands\n\n| Command | Purpose |\n| --- | --- |\n| `bun install` | Install workspace deps. |\n| `bun run dev` | Boot api+web+worker via turbo. |\n| `bun run lint` | oxlint + invariant gates + prettier. Must be clean. |\n| `bun run typecheck` | `tsc --noEmit -b` across all packages. Must be clean. |\n| `bun run test` | Vitest. Per-workspace thresholds are enforced in the complete-suite coverage lane assigned by `COVERAGE_POLICY`. |\n| `bun run test:e2e` | Playwright e2e. |\n| `bun run test:worker-integration` | `apps/worker` integration lane on a laptop. Provisions throwaway Postgres + Redis under `TESTCONTAINERS=1`, so it needs a reachable Docker daemon and prints a notice instead of running when there is none. |\n| `bun run db:migrate` | Apply hand-authored SQL migrations (checksum-tracked runner). |\n| `bun run db:generate` | Not used — repo hand-authors `NNNN_*.sql`. |\n| `bun run setup` | First-time bootstrap: `.env` from `.env.example` + migrate. |\n| `bun run docs:install` | `uv sync --group docs`. Once, before any other `docs:*` command. |\n| `bun run docs:build` | `docs:gen --check` + `uv run mkdocs build --strict`. The docs gate. |\n| `docker compose up` | Local stack: postgres, redis, one `ROLE=all` app. |\n\nDocs build in a **uv-managed Python env** (`pyproject.toml`, `[dependency-groups] docs`), never against whatever `mkdocs` happens to be on `PATH`. Run a bare `mkdocs build --strict` and it picks up an unrelated interpreter and dies on `The \"<plugin>\" plugin is not installed` — that message means the wrong environment, not a broken config, so installing the named plugin by hand is the wrong repair. Go through `bun run docs:build` / `docs:serve`, which invoke mkdocs under `uv run`; `bun run docs:install` (`uv sync --group docs`) creates that env first.\n\n## Linting\n\n**oxlint** is the single lint path (`.oxlintrc.json`, one whole-repo pass so `import/no-cycle` sees the full graph): `correctness` + `typescript`/`import`/`oxc`/`react` plugins, plus repo invariants via `overrides` — strategy/indicator **purity** (`no-restricted-globals`/`-imports`) and the **decimal.js boundary**. `react` is on for `react/no-unstable-nested-components` (a component declared inside another's render body remounts its subtree every render, which clamps `scrollTop` on WebKit); enabling the plugin also arms every `react` **correctness** rule, so `react/exhaustive-deps` is explicitly `off` pending triage of its pre-existing violations. Invariants oxlint can't express are `scripts/ci/*.sh` gates run from `lint.sh`: `no-plugin-leak` (invariant 1), `no-arbitrary-color-token`, `no-phantom-env-var`, `no-invalid-mermaid`, `no-undeclared-workspace-import`, `no-wider-metrics-sink` (only `apps/worker/src/metrics/catalog.ts` may declare the metrics sink; a second one typed `name: string` type-checks and escapes the catalogue), `no-unreviewed-tofixed` (every direct `.toFixed(` and fixed-two `maximumFractionDigits` site under `apps/web/src/**` is registered in `scripts/ci/tofixed-inventory.json` with its pattern identity and a factual value-kind reason; a hard round is how a sub-cent quote balance renders as `0.00`), `no-dropped-lint-rule` (asserts the resolved oxlint config still arms the rules carrying an invariant), `no-backfilled-migration` (a new `NNNN_*.sql` takes the next unused number: the runner applies files in name order, so one inserted below the high-water mark runs LAST on every already-migrated database), `no-mutated-applied-migration` (a shipped `NNNN_*.sql` is immutable; every suite migrates a fresh DB, so only a pinned-digest manifest can see the drift that wedges a deployed one), `no-bun-version-skew` (nine Bun pin sites must agree; Renovate does not reliably cover them, and the gate's reading counts are exact, so a site that stops matching fails loudly instead of silently narrowing the check), `no-blind-walk` (every gate that reads its verdict off a tree walk takes that walk from `scripts/ci/lib/walk.mjs`, whose `collectOrExit` refuses a root declaring no anchor, and carries the `GUARD_ROOT` override seam its self-test needs: a walk that merely narrowed still returns hundreds of files, so the count a gate prints is evidence only once it is proven to still reach the module the rule protects and its stops have been watched fire). See `docs/contributing/coding-rules.md`.\n\n## Quality gates (CI must enforce)\n\n1. Lint clean.\n2. Typecheck clean.\n3. Docs updated when behaviour changes — every claim traces to a source, not memory. Machine-enforced: `bun run docs:build` (`docs:gen --check` for config tables + env-var reference, then `uv run mkdocs build --strict`), `no-invalid-mermaid.sh`, `no-stale-migration-doc.sh`; narrative accuracy stays a review gate. See `docs/contributing/coding-rules.md#documentation-accuracy`.\n4. Per-workspace complete-suite coverage thresholds met (`packages/config/vitest/coverage-policy.js`).\n5. Strategy golden-fixture replay diff = 0.\n6. Playwright lanes green. `browser-bootstrap` proves the four configured browser projects start and reports every skipped app execution with its reason; it does not claim critical-path app coverage. `app-e2e` boots a hermetic ROLE=all stack against a loopback Binance fixture and runs the P0 journey in all four projects; it fails on any skip and on any Binance traffic the fixture does not answer.\n7. `bun run compose:build` succeeds for every app.\n8. Migrations: hand-authored `NNNN_*.sql` applied in order by the checksum-tracked runner (`packages/db/src/migrate.ts`, tracked in `_app_migrations`); the `packages/db` suite runs `migrate()` against real Postgres. A shipped migration is **immutable**, and the two ways to break that fail differently. **Editing** one (a comment counts) moves its digest and wedges every already-migrated database at that file, which no later migration can repair. **Renaming or deleting** one is invisible to the runner, which keys on name: the file re-applies as new and orphans its old ledger row, silently. Change a shipped migration with a new migration; `no-mutated-applied-migration` enforces this because a fresh-DB suite cannot see either case, and `migrate-immutability.test.ts` pins both behaviours. Numbering is the other half of the same contract: a new migration takes the next unused number, never a hole, a letter suffix, or anything below `0001`, because name order is apply order — `no-backfilled-migration` enforces it from the directory listing, the only oracle that can tell a backfill from a fresh tree. See `docs/contributing/coding-rules.md` \"A migration number is never reused and never backfilled\".\n9. JSDoc quality is a review gate, not a lint gate. Every exported or non-trivial function carries a block with one `@param` line per parameter and a `@returns` line when it returns a value. The prose above the tags carries the WHY; each tag glosses what that value _means_ — no name-restatements (`@param symbol - The symbol`), no blank blocks. A name that is a misnomer or an easy misread makes its `@param` line the only place a reader is told, so it is required there even when the type looks self-evident.\n\n   **Do not hard-wrap comments.** One paragraph is one line, and one `@param` / `@returns` tag is one line, however long it runs — in JSDoc blocks and in `//` comments alike. This is the `proseWrap: 'never'` rule `prettier.config.mjs` already applies to Markdown, for the same reason: hard-wrapped prose turns every later edit into a multi-line reflow, so the diff hides the sentence that actually changed. Editors soft-wrap; diffs do not. Nothing enforces this — `printWidth` governs code only, and prettier never reflows a comment body — so it is a review gate, and existing wrapped comments are left alone until the code around them is touched. Line breaks are for meaning, not width: a bulleted list, a `- x:` table, or an ASCII diagram keeps its breaks.\n\n## Anti-patterns to refuse\n\n- **NotifyProvider single source of truth.** The shape lives in `@app/notify`; `apps/api` consumes `notifyProviders.describeAll()`, never its own provider interface.\n- **No distributed locks.** No `redlock`, `intents:` Redis sets, soft balance reservation, or equivalents (`scripts/ci/no-locks.sh` fails, also scanning `packages/binance`). Permitted shared-Redis primitives are rate-limit / visibility, not locks (no owner, no release/refund): the consume-and-decay request-weight bucket and the self-expiring `SET NX PX` notifier-gap throttle.\n- **No plaintext API keys on disk** outside the Postgres exception, and only under the `docs/architecture/auth.md` mitigations (single-tenant, operator-side Binance IP-allowlist). Any new storage surface (backups, replicas, exports, multi-tenant) requires encryption-at-rest first.\n- **No secret in a `LIVE_DEMO` response.** The public demo injects the sole operator id, so every route not behind `requireNotDemo` is reachable anonymously. A credential-equivalent value — Binance API key, notifier **webhook URL**, bot token, auth header, AI-provider key — MUST NOT serialise into any response body. Strip it at the projection boundary (api-key → `last4`, ai-provider → `has*` booleans, notifiers declare secrets in `secretFields`) so a leak is impossible by construction. `requireNotDemo` is a blocklist / defence-in-depth: a new secret-reading or credential-writing route is exposed by default — add it to the deny-list AND to the completeness assertion in `apps/api/__tests__/routes/live-demo-guard.test.ts`.\n- **No speculative abstraction.** Feature flags, configurability, or \"for the future\" indirection require a tracked issue with a concrete consumer.\n- **Money math = `Decimal`.** `number` (IEEE-754) is banned downstream of `packages/{strategy-*,indicators,contracts}`; the decimal.js import boundary is lint-enforced (oxlint `no-restricted-imports`).\n- **`Decision` union stays generic.** `noop` / `place-order` / `cancel-order` / `replace-order` / `emit-event` / `set-kv` / `delete-kv` (`packages/strategy/core/src/decision.ts`). Generic exchange-request shapes such as `replace-order` belong in the union; strategy-specific side effects ride `emit-event` with namespaced keys, not new strategy-specific variants; cross-symbol coupling rides `set-kv` / `delete-kv`. A removed variant takes its worker-side port with it — no idle seams.\n- **Strategy & indicators are pure.** No `ioredis` / `pg` / `drizzle-orm` / `fs` / `net` / `http` / `crypto` / `Date` / `Math.random` inside `packages/strategy-*/src/**` or `packages/indicators/src/**`; no I/O inside `tick()` (the worker injects `Clock` and `RNG`). `decimal.js` is allowed.\n- **Comments say why, not what.** No Spec / Issue / Phase / `@see` refs in code; commits and PRs own that. CI rejects.\n- **Tests live in `<pkg>/__tests__/`,** never beside `src/`. CI rejects.\n- **New shared runtime utilities go into `@app/core` via a new subpath, not a new package** — unless the utility has a clear domain boundary (strategy, notifier, binance, db, contracts).\n","isInternal":false,"tokens":4361,"sizeBytes":17701},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/chrisleekr/binance-trading-bot/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"AGENTS.md","isInternal":false,"tokens":3,"sizeBytes":9},{"name":"principles.md","path":".claude/rules/principles.md","rawUrl":"https://raw.githubusercontent.com/chrisleekr/binance-trading-bot/HEAD/.claude/rules/principles.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Engineering principles\n\nGeneric engineering rules for this repo. Project-specific invariants live in `CLAUDE.md`.\n\n## Think before coding\n\nDon't assume. Don't hide confusion. Surface tradeoffs.\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n## Simplicity first\n\nMinimum code that solves the problem. Nothing speculative.\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n- Ask: \"Would a senior engineer say this is overcomplicated?\" If yes, simplify.\n\n## Goal-driven execution\n\nDefine success criteria. Loop until verified.\n\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\nFor multi-step tasks, state a brief plan:\n\n1. [Step] → verify: [check]\n2. [Step] → verify: [check]\n","isInternal":false,"tokens":289,"sizeBytes":1238},{"name":"SKILL.md","path":".claude/skills/feature-walkthrough/SKILL.md","rawUrl":"https://raw.githubusercontent.com/chrisleekr/binance-trading-bot/HEAD/.claude/skills/feature-walkthrough/SKILL.md","title":"feature-walkthrough","category":"anthropic-skill","format":"markdown","content":"---\nname: feature-walkthrough\ndescription: Autonomously test the running binance-trading-bot app in a real browser. The agent drives the browser itself via the Playwright MCP - logs in, looks at each screen, and works through every feature end-to-end like a real operator, finding and fixing bugs. Use when asked to test the app, smoke-test or walk through the UI, verify the frontend end-to-end, or check that features actually work in a browser.\n---\n\n# Feature walkthrough\n\nAutonomously exercise the real app in a real browser. **You** are the test driver: open the browser, look at the screen, decide what to click, observe the result, decide the next step. This is agentic, vision-driven testing - not a pre-written script.\n\nThe goal is \"the feature works when a person uses it\". Type-checks and unit tests verify code correctness, not feature correctness.\n\n**Test as a non-expert operator.** Walk the app as a user who is _not_ a financial or trading expert. The product must be **easy but accurate**: precise and correct under the hood, yet usable by someone who does not already know the strategy. Unexplained jargon, a setting with no inline help, a number with no units or context, a flow that only a strategy-literate user can complete - each is a real UI/UX finding, not cosmetic.\n\n**This is a greenfield project - refactor freely.** Nothing is deployed; there is no legacy to protect and no shipped behaviour to preserve. When a finding exposes mis-architecture, a performance problem, or a UI/UX flow that is wrong at the structural level, the sanctioned fix is a **large refactor**, not a surgical patch. Do not paper over a bad design to keep the diff small - the bar is the correct architecture, correct code, tested and performant. Surface the scope of a big refactor, confirm, then do it properly.\n\nThis skill is stateless and self-contained - no sibling tracker files. Discover features fresh each run; fix what you can this run; file the rest as GitLab issues. Deterministic regression tests do NOT belong here - they live in `e2e/tests/` as Playwright specs (`manual-order-roundtrip.spec.ts`, `profile-controls.spec.ts`, ...). This skill is the exploratory layer that adapts to whatever the UI actually shows.\n\n## 1. Preflight\n\n1. **Browser tools.** Needs the Playwright MCP `browser_*` tools (`browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_select_option`, `browser_press_key`, `browser_wait_for`, `browser_resize`, `browser_console_messages`, `browser_network_requests`, `browser_take_screenshot`). If unavailable, stop and tell the operator to enable the Playwright MCP - the skill cannot run without it.\n2. **Stack.** Postgres + Redis must be up - `docker ps` for `binance-trading-bot-postgres-1` and `-redis-1`; if absent, `docker compose -f deploy/compose/docker-compose.yml up -d db redis`. Apply migrations: `bun run db:migrate`.\n3. **Dev server.** If not up: `bun run dev > /tmp/btb-dev.log 2>&1 &`. Ports: api `:3000`, web `:5173`, worker `:9101`. **`bun --watch` does NOT reload workspace `packages/*`** - after editing anything under `packages/`, or if the server was started before a recent merge, restart `bun run dev` or it serves stale code and a correct fix looks broken. Vite HMR (`apps/web`) reloads reliably.\n\n## 2. Log in and orient\n\n1. `browser_navigate` to `http://localhost:5173/login`.\n2. `browser_snapshot` the login form. Type the dev credentials - request the executor credentials - into the real fields and click submit. (If login reports no account, create the master account at `/onboarding`.)\n3. Log in **once** - the API rate-limits `/api/auth/sign-in/*` (60s window). Reuse the same session for the whole run; a wrong-password test also counts against the window, so run it after the real login.\n4. `browser_snapshot` the dashboard. This is your map: the nav, the profile list, the action surfaces. Cross-reference `apps/web/src/routes/` for routes not linked from the dashboard.\n\n## 3. Discover - fresh, every run\n\nBuild the feature set live each run; there is no coverage file to read:\n\n1. From the dashboard snapshot, walk every nav target and action surface (`browser_snapshot` each screen).\n2. Enumerate routes from `apps/web/src/routes/` - anything not reachable by clicking is still a feature.\n3. Treat each distinct end-to-end thing an operator can _do_ (place an order, switch strategy, save a notifier, run the wizard, ...) as one feature.\n\n## 4. Pick a batch\n\nFix a **substantial batch per run - ~5–8 items**, sized by reviewable diff, shipped as one MR(Gitlab) or PR(GitHub). CI and senior-review cost the same per MR whether it carries one fix or many, so a thin batch wastes the cycle. Priority order, highest first:\n\n1. Defects already seen this run.\n2. Features closest to the money path: orders > config > notifiers > cosmetic.\n3. Sub-bar UX/DevX small enough to fold in.\n\nRules: a genuinely large or risky item (whole-feature work, refactor, \"multiple valid approaches\" territory) ships **solo** in its own MR - never padded with or batched against small fixes. A batch may span files and themes; keep commit history legible (one commit per item or theme). Each batched item is still walked and verified per §5.\n\n## 5. Walk it - run this loop yourself, do not pre-script it\n\nRun this loop for **each item in the batch**. Items are independent; do them in turn.\n\n1. **Navigate** to the feature's entry screen.\n2. **Observe** - `browser_snapshot` (accessibility tree, gives clickable refs); `browser_take_screenshot` when layout matters. Refs are snapshot-scoped: re-snapshot after every navigation or re-render or a stale ref clicks the wrong thing.\n3. **Decide** the next interaction from what you see - the real button, input, selector.\n4. **Act** - `browser_click` / `browser_type` / `browser_select_option` / `browser_press_key`. Real interactions only. Destructive actions (kill-switch, strategy switch) open a confirm modal - snapshot the dialog and click its Confirm; the action is not done until you do.\n   - If `browser_click` reports success but nothing happens (no events reach the page), the long session's click coordinates have desynced - a tool artifact, not an app bug: `browser_close` then re-`browser_navigate` for a fresh context, and the click works again.\n5. **Verify** - snapshot again. Assert the concrete outcome: a banner, a badge, a redirected URL, a new row, a value that survives a reload.\n6. **Loop** to step 2 until the journey is complete or it visibly breaks.\n7. **Check silent failure** - `browser_console_messages` and `browser_network_requests`: unexpected console errors or API responses ≥ 400 are findings even when the screen looked fine.\n8. **Restore** every mutation through the UI - release kill-switches, revert config edits, delete test profiles. Leave the dev DB as you found it.\n\n**UX/DevX is a first-class finding, not cosmetic.** This is an operator's trading tool. At every Verify step, judge the screen as a user: is the layout clean and aligned, the hierarchy clear, the primary action obvious, are loading/empty/error states handled, is copy precise, the flow short and unsurprising? Read the pixels with `browser_take_screenshot`, not only the accessibility tree.\n\n**Non-expert lens.** Assume the user does not know trading. Jargon without explanation, a field with no inline help, a value with no units/context, or a flow that only a strategy-literate user can finish is a defect - fix or file it. \"Easy but accurate\": friendly defaults, plain-language labels, inline help, actionable errors, without losing correctness.\n\n**DevX counts the same.** Every setting needs a proper form UI with typed inputs, inline help, and per-field validation - a raw JSON editor is itself a UX defect: flag and replace it. Validation errors must say exactly what is wrong and where; defaults must be sane; setup/recovery must not require reading source. A cryptic error, a silent rejection, or a flow only a code-reader can complete is a DevX finding.\n\nCross-cutting checks while you are in there:\n\n- Wrong-password login → friendly error, not a crash. Do this **after** the real login or it burns the rate-limit window.\n- Resize to 375×667 (`browser_resize`) on every screen you visit → no horizontal overflow, and the mobile layout must be genuinely usable, not merely non-overflowing (mobile-first is a core invariant).\n\n**Dev-env boundary:** there is no live Binance connection in dev, and manual orders enqueue but may not fill. Verify what dev _can_ prove (enqueue, UI state, DB row) and state the boundary - do not file expected env state as a bug.\n\n## 6. Triage - one verdict per observation\n\n- **Fix it this run** - a small, contained defect, UI/UX, or DevX finding. Fix the root cause (§7).\n- **File a GitLab issue** - a real finding too large/ambiguous for this batch, or sub-bar-but-not-broken. Title + location + severity + repro steps. This is the durable backlog; there is no local tracker file.\n- **Leave it** - Cite the doc; do not act.\n- **Re-observe** - your own misread: wrong screen, stale dev server, a moved control, a desynced click (§5). Do not touch the app.\n- **Not a bug** - expected dev-env state: empty data, \"Never\" last-tick, 502s from Binance, 404s for unconfigured notifiers.\n\nA bug is never parked as a \"known issue\" to work around - it is fixed this run or filed as an issue to fix later.\n\n## 7. Fix - root cause, not symptom\n\nFix every batched item at the root. This is a greenfield project - no legacy to protect, no shipped behaviour to preserve - so **large refactoring is sanctioned and expected** wherever a finding warrants it. Mis-architecture (wrong abstraction, a pattern that should not exist, a missing layer), a performance problem, or a structurally-wrong UI/UX flow are all valid triggers for a big rewrite, not a patch. Do not paper over a bad design with a surgical fix that leaves the bad design in place. The bar is correct architecture, correct code, tested, performant - not minimal diff. A change that large is \"multiple valid approaches\" territory: surface the scope and design, confirm, then build. Keep `bun run typecheck`, `bun run lint`, and `bun run test` clean, and re-walk each affected feature in the browser. Loop until every batched item works.\n\nWhen a bug is worth locking down, add a deterministic spec to `e2e/tests/` (follow `profile-controls.spec.ts`) - that is the regression layer; this skill is not.\n","frontmatter":{"name":"feature-walkthrough","description":"Autonomously test the running binance-trading-bot app in a real browser. The agent drives the browser itself via the Playwright MCP - logs in, looks at each screen, and works through every feature end-to-end like a real operator, finding and fixing bugs. Use when asked to test the app, smoke-test or walk through the UI, verify the frontend end-to-end, or check that features actually work in a browser."},"isInternal":false,"tokens":2461,"sizeBytes":10431}],"systemPromptSnippet":"<yakaai_skills repo=\"chrisleekr/binance-trading-bot\">\n<!-- File: AGENTS.md (Tokens: ~4361 | Category: root-instruction) -->\n# Engineering charter\n\n> Charter for any coding agent in this repo (Claude Code, Cursor, Codex, …). Keep it short — architecture, config, and ops live in `docs/` (built by `mkdocs`). Generic engineering principles (think-first, simplicity, goal-driven): `.claude/rules/principles.md`.\n\n## Project\n\n`binance-trading-bot`: single-operator, multi-account, multi-profile, reliability-first, mobile-first trading platform on Binance. One operator owns N first-class Binance accounts (each = one API key pair + one environment + one user-data stream); each account runs N strategy **profiles** against its shared wallet. Strategy is a **plugin** — trailing-trade is the first; source of truth is `packages/strategy/trailing-trade/`, contract in `packages/strategy/core/` + `docs/architecture/extensibility.md`.\n\n## Architecture principles\n\nCorrectness first. Before coding, confirm the design is sound: surface tradeoffs, pick the simplest option that holds the invariants below, cite authoritative sources for any version/API claim, and push back rather than guess. **Greenfield, not yet deployed** — no back-compat debt, so prefer the correct extensible design over the minimal diff and restructure freely; the invariants still bind, and you name a large refactor before starting it.\n\n## Core invariants\n\n1. **Extensibility.** Strategies are plugins behind `packages/strategy-core`; notifier providers behind `@app/notify`'s `./providers/*` subpath exports. `apps/api`/`apps/worker` MUST NOT import a specific `strategy-*` package or an `@app/notify/providers/*` provider outside their registry bootstrap; `apps/web` MAY import strategy packages for typed event payloads. Adding a strategy = new package + registry entry; adding a notifier = new provider module + `buildNotifyRegistry` entry — never an edit to `apps/api`/`apps/worker`.\n2. **Reliability.** Crash-only. Idempotent jobs. Per-account Binance-rate isolation. No silent failures.\n3. **Approachable & responsive.** Solo operator, not a finance pro: plain language, gloss a trading term the first time it appears, never assume off-screen knowledge. Mobile-first — every view usable at 375×667 — then scale up.\n4. **Account-scoped by row, not convention.**\n   - **Model:** one operator (`users`) → many `accounts` → many `profiles`. `accounts.owner_id → users.id`; each account holds one Binance key pair, one `binance_mode`, one user-data stream. `profiles.account_id` binds each profile to its account, sharing that account's key/wallet/stream.\n   - **Branded two-tier scope:** `AccountScope{db,operatorId,accountId}` from `scopeAccount`, `ProfileScope{db,operatorId,accountId,profileId}` from `scopeProfile` — proves the ownership chain in one query. `UserId` and `AccountId` are distinct branded types (misplacing one won't compile).\n   - **Access rule:** app code reaches account-scoped data ONLY through `profileRepo(...)` / `profileRepoFromScope(scope)` — or `accountRepo(...)` / `accountRepoFromScope` for account-level surfaces like api-keys — from `@app/db`. No manual `(db, operatorId, …)` threading; every account-scoped repo fn takes an `AccountScope`/`ProfileScope` first, minted only by `scopeAccount`/`scopeProfile` — ownership proven exactly once at compile time. Flat `repo.<x>` stays only for operator/global fns (`repo.users.*`, `repo.accounts.create`/`listForOwner`, retention sweeps).\n   - **Routes:** API under `/api/accounts/:accountId/...`, web under `/accounts/$accountId/...`.\n   - **Enforcement:** `packages/db/__tests__/repo/ast-check.test.ts` + cross-account integration tests. No DB-side RLS. See `docs/architecture/{account-isolation,database}.md`.\n   - **Strategy state** is per-(profile, symbol) in storage and in `tick()` — one slice per symbol per call. Cross-symbol state goes through the per-profile KV store: emit `set-kv`/`delete-kv` under a strategy-owned namespaced key and read the merged snapshot back via `TickInput.profileKv` (opt in with `capabilities.needsProfileKv`). A KV write lands for sibling symbols on later ticks, never the same tick.\n\n## Stack\n\nVersions are pinned in `package.json`/lockfiles (the source of truth); names only here.\n\n- Runtime **Bun** · monorepo **Turborepo** · CI: GitHub Actions + GitLab both call `scripts/ci/*.sh`\n- Backend **Hono** · Frontend **Vite + React + TanStack Router/Query + Tailwind + shadcn/ui** (PWA)\n- Charts **lightweight-charts** (financial), **Recharts** (non-financial)\n- DB **Postgres + TimescaleDB** · ORM **Drizzle** · migrations = hand-authored SQL via a checksum-tracked runner\n- Cache/queue **Redis + BullMQ** (`ioredis`, `maxRetriesPerRequest: null`)\n- Auth **Better Auth** — no email/SMTP, no 2FA (single master account). See `docs/architecture/auth.md`.\n- **Encryption-at-rest: none.** Binance keys + notifier secrets stored plaintext; operator IP-allowlists Binance keys. See `docs/architecture/auth.md` \"Threat model\".\n- **Money = decimal.js.** Every price/quantity/amount/balance/pnl is `Decimal` end-to-end; `number` only for counters/indices/ms-timestamps. Snapshots serialise `Decimal` as strings; strategies revive at the boundary.\n- Notifications: pluggable `@app/notify` providers (Slack first).\n- Technicals: in-process `packages/indicators/rating` (vendored MIT); worker `technicals-compute` cron computes ratings locally, no external scanner. See `docs/architecture/technicals.md`.\n- Containers: one multi-stage image (`apps/server/Dockerfile`); `ROLE=all|api|worker|study` selects behaviour. `apps/web` is build-only (api serves the SPA same-origin). Scale split via `docker-compose.scale.yml`.\n- Worker: **single replica today** (`WORKER_REPLICAS=1`), no distributed locks; single-execution = BullMQ `jobId` coalescing + in-process `chainByKey` + idempotent `clientOrderId` + version-aware `symbol_states` CAS. Elastic-pool scale-out plumbing is merged but **dormant**. See `docs/architecture/worker-pipeline.md`.\n\n## Repo layout\n\n```text\napps/         api/ web/ worker/ server/       # server = ROLE-selectable entrypoint (api + worker)\npackages/\n  strategy/    core/ trailing-trade/          # core = contract + Executor; TT = first plugin\n  indicators/                                 # pure, Decimal-typed candle/window math\n  notify/                                     # @app/notify: contract + registry + providers/{slack,telegram,webhook}\n  binance/ db/ contracts/ config/\n  core/                                       # shared runtime utils; subpath exports (./env, …)\ndeploy/compose/  scripts/ci/\n```\n\nDirectory grouping (`packages/strategy/*`) is filesystem-only; **npm names stay flat** (`@app/strategy-trailing-trade`). `@app/notify` is one package; providers are subpath exports.\n\n## Required commands\n\n| Command | Purpose |\n| --- | --- |\n| `bun install` | Install workspace deps. |\n| `bun run dev` | Boot api+web+worker via turbo. |\n| `bun run lint` | oxlint + invariant gates + prettier. Must be clean. |\n| `bun run typecheck` | `tsc --noEmit -b` across all packages. Must be clean. |\n| `bun run test` | Vitest. Per-workspace thresholds are enforced in the complete-suite coverage lane assigned by `COVERAGE_POLICY`. |\n| `bun run test:e2e` | Playwright e2e. |\n| `bun run test:worker-integration` | `apps/worker` integration lane on a laptop. Provisions throwaway Postgres + Redis under `TESTCONTAINERS=1`, so it needs a reachable Docker daemon and prints a notice instead of running when there is none. |\n| `bun run db:migrate` | Apply hand-authored SQL migrations (checksum-tracked runner). |\n| `bun run db:generate` | Not used — repo hand-authors `NNNN_*.sql`. |\n| `bun run setup` | First-time bootstrap: `.env` from `.env.example` + migrate. |\n| `bun run docs:install` | `uv sync --group docs`. Once, before any other `docs:*` command. |\n| `bun run docs:build` | `docs:gen --check` + `uv run mkdocs build --strict`. The docs gate. |\n| `docker compose up` | Local stack: postgres, redis, one `ROLE=all` app. |\n\nDocs build in a **uv-managed Python env** (`pyproject.toml`, `[dependency-groups] docs`), never against whatever `mkdocs` happens to be on `PATH`. Run a bare `mkdocs build --strict` and it picks up an unrelated interpreter and dies on `The \"<plugin>\" plugin is not installed` — that message means the wrong environment, not a broken config, so installing the named plugin by hand is the wrong repair. Go through `bun run docs:build` / `docs:serve`, which invoke mkdocs under `uv run`; `bun run docs:install` (`uv sync --group docs`) creates that env first.\n\n## Linting\n\n**oxlint** is the single lint path (`.oxlintrc.json`, one whole-repo pass so `import/no-cycle` sees the full graph): `correctness` + `typescript`/`import`/`oxc`/`react` plugins, plus repo invariants via `overrides` — strategy/indicator **purity** (`no-restricted-globals`/`-imports`) and the **decimal.js boundary**. `react` is on for `react/no-unstable-nested-components` (a component declared inside another's render body remounts its subtree every render, which clamps `scrollTop` on WebKit); enabling the plugin also arms every `react` **correctness** rule, so `react/exhaustive-deps` is explicitly `off` pending triage of its pre-existing violations. Invariants oxlint can't express are `scripts/ci/*.sh` gates run from `lint.sh`: `no-plugin-leak` (invariant 1), `no-arbitrary-color-token`, `no-phantom-env-var`, `no-invalid-mermaid`, `no-undeclared-workspace-import`, `no-wider-metrics-sink` (only `apps/worker/src/metrics/catalog.ts` may declare the metrics sink; a second one typed `name: string` type-checks and escapes the catalogue), `no-unreviewed-tofixed` (every direct `.toFixed(` and fixed-two `maximumFractionDigits` site under `apps/web/src/**` is registered in `scripts/ci/tofixed-inventory.json` with its pattern identity and a factual value-kind reason; a hard round is how a sub-cent quote balance renders as `0.00`), `no-dropped-lint-rule` (asserts the resolved oxlint config still arms the rules carrying an invariant), `no-backfilled-migration` (a new `NNNN_*.sql` takes the next unused number: the runner applies files in name order, so one inserted below the high-water mark runs LAST on every already-migrated database), `no-mutated-applied-migration` (a shipped `NNNN_*.sql` is immutable; every suite migrates a fresh DB, so only a pinned-digest manifest can see the drift that wedges a deployed one), `no-bun-version-skew` (nine Bun pin sites must agree; Renovate does not reliably cover them, and the gate's reading counts are exact, so a site that stops matching fails loudly instead of silently narrowing the check), `no-blind-walk` (every gate that reads its verdict off a tree walk takes that walk from `scripts/ci/lib/walk.mjs`, whose `collectOrExit` refuses a root declaring no anchor, and carries the `GUARD_ROOT` override seam its self-test needs: a walk that merely narrowed still returns hundreds of files, so the count a gate prints is evidence only once it is proven to still reach the module the rule protects and its stops have been watched fire). See `docs/contributing/coding-rules.md`.\n\n## Quality gates (CI must enforce)\n\n1. Lint clean.\n2. Typecheck clean.\n3. Docs updated when behaviour changes — every claim traces to a source, not memory. Machine-enforced: `bun run docs:build` (`docs:gen --check` for config tables + env-var reference, then `uv run mkdocs build --strict`), `no-invalid-mermaid.sh`, `no-stale-migration-doc.sh`; narrative accuracy stays a review gate. See `docs/contributing/coding-rules.md#documentation-accuracy`.\n4. Per-workspace complete-suite coverage thresholds met (`packages/config/vitest/coverage-policy.js`).\n5. Strategy golden-fixture replay diff = 0.\n6. Playwright lanes green. `browser-bootstrap` proves the four configured browser projects start and reports every skipped app execution with its reason; it does not claim critical-path app coverage. `app-e2e` boots a hermetic ROLE=all stack against a loopback Binance fixture and runs the P0 journey in all four projects; it fails on any skip and on any Binance traffic the fixture does not answer.\n7. `bun run compose:build` succeeds for every app.\n8. Migrations: hand-authored `NNNN_*.sql` applied in order by the checksum-tracked runner (`packages/db/src/migrate.ts`, tracked in `_app_migrations`); the `packages/db` suite runs `migrate()` against real Postgres. A shipped migration is **immutable**, and the two ways to break that fail differently. **Editing** one (a comment counts) moves its digest and wedges every already-migrated database at that file, which no later migration can repair. **Renaming or deleting** one is invisible to the runner, which keys on name: the file re-applies as new and orphans its old ledger row, silently. Change a shipped migration with a new migration; `no-mutated-applied-migration` enforces this because a fresh-DB suite cannot see either case, and `migrate-immutability.test.ts` pins both behaviours. Numbering is the other half of the same contract: a new migration takes the next unused number, never a hole, a letter suffix, or anything below `0001`, because name order is apply order — `no-backfilled-migration` enforces it from the directory listing, the only oracle that can tell a backfill from a fresh tree. See `docs/contributing/coding-rules.md` \"A migration number is never reused and never backfilled\".\n9. JSDoc quality is a review gate, not a lint gate. Every exported or non-trivial function carries a block with one `@param` line per parameter and a `@returns` line when it returns a value. The prose above the tags carries the WHY; each tag glosses what that value _means_ — no name-restatements (`@param symbol - The symbol`), no blank blocks. A name that is a misnomer or an easy misread makes its `@param` line the only place a reader is told, so it is required there even when the type looks self-evident.\n\n   **Do not hard-wrap comments.** One paragraph is one line, and one `@param` / `@returns` tag is one line, however long it runs — in JSDoc blocks and in `//` comments alike. This is the `proseWrap: 'never'` rule `prettier.config.mjs` already applies to Markdown, for the same reason: hard-wrapped prose turns every later edit into a multi-line reflow, so the diff hides the sentence that actually changed. Editors soft-wrap; diffs do not. Nothing enforces this — `printWidth` governs code only, and prettier never reflows a comment body — so it is a review gate, and existing wrapped comments are left alone until the code around them is touched. Line breaks are for meaning, not width: a bulleted list, a `- x:` table, or an ASCII diagram keeps its breaks.\n\n## Anti-patterns to refuse\n\n- **NotifyProvider single source of truth.** The shape lives in `@app/notify`; `apps/api` consumes `notifyProviders.describeAll()`, never its own provider interface.\n- **No distributed locks.** No `redlock`, `intents:` Redis sets, soft balance reservation, or equivalents (`scripts/ci/no-locks.sh` fails, also scanning `packages/binance`). Permitted shared-Redis primitives are rate-limit / visibility, not locks (no owner, no release/refund): the consume-and-decay request-weight bucket and the self-expiring `SET NX PX` notifier-gap throttle.\n- **No plaintext API keys on disk** outside the Postgres exception, and only under the `docs/architecture/auth.md` mitigations (single-tenant, operator-side Binance IP-allowlist). Any new storage surface (backups, replicas, exports, multi-tenant) requires encryption-at-rest first.\n- **No secret in a `LIVE_DEMO` response.** The public demo injects the sole operator id, so every route not behind `requireNotDemo` is reachable anonymously. A credential-equivalent value — Binance API key, notifier **webhook URL**, bot token, auth header, AI-provider key — MUST NOT serialise into any response body. Strip it at the projection boundary (api-key → `last4`, ai-provider → `has*` booleans, notifiers declare secrets in `secretFields`) so a leak is impossible by construction. `requireNotDemo` is a blocklist / defence-in-depth: a new secret-reading or credential-writing route is exposed by default — add it to the deny-list AND to the completeness assertion in `apps/api/__tests__/routes/live-demo-guard.test.ts`.\n- **No speculative abstraction.** Feature flags, configurability, or \"for the future\" indirection require a tracked issue with a concrete consumer.\n- **Money math = `Decimal`.** `number` (IEEE-754) is banned downstream of `packages/{strategy-*,indicators,contracts}`; the decimal.js import boundary is lint-enforced (oxlint `no-restricted-imports`).\n- **`Decision` union stays generic.** `noop` / `place-order` / `cancel-order` / `replace-order` / `emit-event` / `set-kv` / `delete-kv` (`packages/strategy/core/src/decision.ts`). Generic exchange-request shapes such as `replace-order` belong in the union; strategy-specific side effects ride `emit-event` with namespaced keys, not new strategy-specific variants; cross-symbol coupling rides `set-kv` / `delete-kv`. A removed variant takes its worker-side port with it — no idle seams.\n- **Strategy & indicators are pure.** No `ioredis` / `pg` / `drizzle-orm` / `fs` / `net` / `http` / `crypto` / `Date` / `Math.random` inside `packages/strategy-*/src/**` or `packages/indicators/src/**`; no I/O inside `tick()` (the worker injects `Clock` and `RNG`). `decimal.js` is allowed.\n- **Comments say why, not what.** No Spec / Issue / Phase / `@see` refs in code; commits and PRs own that. CI rejects.\n- **Tests live in `<pkg>/__tests__/`,** never beside `src/`. CI rejects.\n- **New shared runtime utilities go into `@app/core` via a new subpath, not a new package** — unless the utility has a clear domain boundary (strategy, notifier, binance, db, contracts).\n\n<!-- File: CLAUDE.md (Tokens: ~3 | Category: claude-rule) -->\nAGENTS.md\n\n<!-- File: .claude/rules/principles.md (Tokens: ~289 | Category: anthropic-skill) -->\n# Engineering principles\n\nGeneric engineering rules for this repo. Project-specific invariants live in `CLAUDE.md`.\n\n## Think before coding\n\nDon't assume. Don't hide confusion. Surface tradeoffs.\n\n- State assumptions explicitly. If uncertain, ask.\n- If multiple interpretations exist, present them — don't pick silently.\n- If a simpler approach exists, say so. Push back when warranted.\n- If something is unclear, stop. Name what's confusing. Ask.\n\n## Simplicity first\n\nMinimum code that solves the problem. Nothing speculative.\n\n- No features beyond what was asked.\n- No abstractions for single-use code.\n- No \"flexibility\" or \"configurability\" that wasn't requested.\n- No error handling for impossible scenarios.\n- If you write 200 lines and it could be 50, rewrite it.\n- Ask: \"Would a senior engineer say this is overcomplicated?\" If yes, simplify.\n\n## Goal-driven execution\n\nDefine success criteria. Loop until verified.\n\n- \"Add validation\" → \"Write tests for invalid inputs, then make them pass\"\n- \"Fix the bug\" → \"Write a test that reproduces it, then make it pass\"\n- \"Refactor X\" → \"Ensure tests pass before and after\"\n\nFor multi-step tasks, state a brief plan:\n\n1. [Step] → verify: [check]\n2. [Step] → verify: [check]\n\n<!-- File: .claude/skills/feature-walkthrough/SKILL.md (Tokens: ~2461 | Category: anthropic-skill) -->\n---\nname: feature-walkthrough\ndescription: Autonomously test the running binance-trading-bot app in a real browser. The agent drives the browser itself via the Playwright MCP - logs in, looks at each screen, and works through every feature end-to-end like a real operator, finding and fixing bugs. Use when asked to test the app, smoke-test or walk through the UI, verify the frontend end-to-end, or check that features actually work in a browser.\n---\n\n# Feature walkthrough\n\nAutonomously exercise the real app in a real browser. **You** are the test driver: open the browser, look at the screen, decide what to click, observe the result, decide the next step. This is agentic, vision-driven testing - not a pre-written script.\n\nThe goal is \"the feature works when a person uses it\". Type-checks and unit tests verify code correctness, not feature correctness.\n\n**Test as a non-expert operator.** Walk the app as a user who is _not_ a financial or trading expert. The product must be **easy but accurate**: precise and correct under the hood, yet usable by someone who does not already know the strategy. Unexplained jargon, a setting with no inline help, a number with no units or context, a flow that only a strategy-literate user can complete - each is a real UI/UX finding, not cosmetic.\n\n**This is a greenfield project - refactor freely.** Nothing is deployed; there is no legacy to protect and no shipped behaviour to preserve. When a finding exposes mis-architecture, a performance problem, or a UI/UX flow that is wrong at the structural level, the sanctioned fix is a **large refactor**, not a surgical patch. Do not paper over a bad design to keep the diff small - the bar is the correct architecture, correct code, tested and performant. Surface the scope of a big refactor, confirm, then do it properly.\n\nThis skill is stateless and self-contained - no sibling tracker files. Discover features fresh each run; fix what you can this run; file the rest as GitLab issues. Deterministic regression tests do NOT belong here - they live in `e2e/tests/` as Playwright specs (`manual-order-roundtrip.spec.ts`, `profile-controls.spec.ts`, ...). This skill is the exploratory layer that adapts to whatever the UI actually shows.\n\n## 1. Preflight\n\n1. **Browser tools.** Needs the Playwright MCP `browser_*` tools (`browser_navigate`, `browser_snapshot`, `browser_click`, `browser_type`, `browser_select_option`, `browser_press_key`, `browser_wait_for`, `browser_resize`, `browser_console_messages`, `browser_network_requests`, `browser_take_screenshot`). If unavailable, stop and tell the operator to enable the Playwright MCP - the skill cannot run without it.\n2. **Stack.** Postgres + Redis must be up - `docker ps` for `binance-trading-bot-postgres-1` and `-redis-1`; if absent, `docker compose -f deploy/compose/docker-compose.yml up -d db redis`. Apply migrations: `bun run db:migrate`.\n3. **Dev server.** If not up: `bun run dev > /tmp/btb-dev.log 2>&1 &`. Ports: api `:3000`, web `:5173`, worker `:9101`. **`bun --watch` does NOT reload workspace `packages/*`** - after editing anything under `packages/`, or if the server was started before a recent merge, restart `bun run dev` or it serves stale code and a correct fix looks broken. Vite HMR (`apps/web`) reloads reliably.\n\n## 2. Log in and orient\n\n1. `browser_navigate` to `http://localhost:5173/login`.\n2. `browser_snapshot` the login form. Type the dev credentials - request the executor credentials - into the real fields and click submit. (If login reports no account, create the master account at `/onboarding`.)\n3. Log in **once** - the API rate-limits `/api/auth/sign-in/*` (60s window). Reuse the same session for the whole run; a wrong-password test also counts against the window, so run it after the real login.\n4. `browser_snapshot` the dashboard. This is your map: the nav, the profile list, the action surfaces. Cross-reference `apps/web/src/routes/` for routes not linked from the dashboard.\n\n## 3. Discover - fresh, every run\n\nBuild the feature set live each run; there is no coverage file to read:\n\n1. From the dashboard snapshot, walk every nav target and action surface (`browser_snapshot` each screen).\n2. Enumerate routes from `apps/web/src/routes/` - anything not reachable by clicking is still a feature.\n3. Treat each distinct end-to-end thing an operator can _do_ (place an order, switch strategy, save a notifier, run the wizard, ...) as one feature.\n\n## 4. Pick a batch\n\nFix a **substantial batch per run - ~5–8 items**, sized by reviewable diff, shipped as one MR(Gitlab) or PR(GitHub). CI and senior-review cost the same per MR whether it carries one fix or many, so a thin batch wastes the cycle. Priority order, highest first:\n\n1. Defects already seen this run.\n2. Features closest to the money path: orders > config > notifiers > cosmetic.\n3. Sub-bar UX/DevX small enough to fold in.\n\nRules: a genuinely large or risky item (whole-feature work, refactor, \"multiple valid approaches\" territory) ships **solo** in its own MR - never padded with or batched against small fixes. A batch may span files and themes; keep commit history legible (one commit per item or theme). Each batched item is still walked and verified per §5.\n\n## 5. Walk it - run this loop yourself, do not pre-script it\n\nRun this loop for **each item in the batch**. Items are independent; do them in turn.\n\n1. **Navigate** to the feature's entry screen.\n2. **Observe** - `browser_snapshot` (accessibility tree, gives clickable refs); `browser_take_screenshot` when layout matters. Refs are snapshot-scoped: re-snapshot after every navigation or re-render or a stale ref clicks the wrong thing.\n3. **Decide** the next interaction from what you see - the real button, input, selector.\n4. **Act** - `browser_click` / `browser_type` / `browser_select_option` / `browser_press_key`. Real interactions only. Destructive actions (kill-switch, strategy switch) open a confirm modal - snapshot the dialog and click its Confirm; the action is not done until you do.\n   - If `browser_click` reports success but nothing happens (no events reach the page), the long session's click coordinates have desynced - a tool artifact, not an app bug: `browser_close` then re-`browser_navigate` for a fresh context, and the click works again.\n5. **Verify** - snapshot again. Assert the concrete outcome: a banner, a badge, a redirected URL, a new row, a value that survives a reload.\n6. **Loop** to step 2 until the journey is complete or it visibly breaks.\n7. **Check silent failure** - `browser_console_messages` and `browser_network_requests`: unexpected console errors or API responses ≥ 400 are findings even when the screen looked fine.\n8. **Restore** every mutation through the UI - release kill-switches, revert config edits, delete test profiles. Leave the dev DB as you found it.\n\n**UX/DevX is a first-class finding, not cosmetic.** This is an operator's trading tool. At every Verify step, judge the screen as a user: is the layout clean and aligned, the hierarchy clear, the primary action obvious, are loading/empty/error states handled, is copy precise, the flow short and unsurprising? Read the pixels with `browser_take_screenshot`, not only the accessibility tree.\n\n**Non-expert lens.** Assume the user does not know trading. Jargon without explanation, a field with no inline help, a value with no units/context, or a flow that only a strategy-literate user can finish is a defect - fix or file it. \"Easy but accurate\": friendly defaults, plain-language labels, inline help, actionable errors, without losing correctness.\n\n**DevX counts the same.** Every setting needs a proper form UI with typed inputs, inline help, and per-field validation - a raw JSON editor is itself a UX defect: flag and replace it. Validation errors must say exactly what is wrong and where; defaults must be sane; setup/recovery must not require reading source. A cryptic error, a silent rejection, or a flow only a code-reader can complete is a DevX finding.\n\nCross-cutting checks while you are in there:\n\n- Wrong-password login → friendly error, not a crash. Do this **after** the real login or it burns the rate-limit window.\n- Resize to 375×667 (`browser_resize`) on every screen you visit → no horizontal overflow, and the mobile layout must be genuinely usable, not merely non-overflowing (mobile-first is a core invariant).\n\n**Dev-env boundary:** there is no live Binance connection in dev, and manual orders enqueue but may not fill. Verify what dev _can_ prove (enqueue, UI state, DB row) and state the boundary - do not file expected env state as a bug.\n\n## 6. Triage - one verdict per observation\n\n- **Fix it this run** - a small, contained defect, UI/UX, or DevX finding. Fix the root cause (§7).\n- **File a GitLab issue** - a real finding too large/ambiguous for this batch, or sub-bar-but-not-broken. Title + location + severity + repro steps. This is the durable backlog; there is no local tracker file.\n- **Leave it** - Cite the doc; do not act.\n- **Re-observe** - your own misread: wrong screen, stale dev server, a moved control, a desynced click (§5). Do not touch the app.\n- **Not a bug** - expected dev-env state: empty data, \"Never\" last-tick, 502s from Binance, 404s for unconfigured notifiers.\n\nA bug is never parked as a \"known issue\" to work around - it is fixed this run or filed as an issue to fix later.\n\n## 7. Fix - root cause, not symptom\n\nFix every batched item at the root. This is a greenfield project - no legacy to protect, no shipped behaviour to preserve - so **large refactoring is sanctioned and expected** wherever a finding warrants it. Mis-architecture (wrong abstraction, a pattern that should not exist, a missing layer), a performance problem, or a structurally-wrong UI/UX flow are all valid triggers for a big rewrite, not a patch. Do not paper over a bad design with a surgical fix that leaves the bad design in place. The bar is correct architecture, correct code, tested, performant - not minimal diff. A change that large is \"multiple valid approaches\" territory: surface the scope and design, confirm, then build. Keep `bun run typecheck`, `bun run lint`, and `bun run test` clean, and re-walk each affected feature in the browser. Loop until every batched item works.\n\nWhen a bug is worth locking down, add a deterministic spec to `e2e/tests/` (follow `profile-controls.spec.ts`) - that is the regression layer; this skill is not.\n</yakaai_skills>"}