# apps2/robot — PROJECT PLAN (v1 draft, for LLM review)

Written 2026-08-29 by the build session, BEFORE any code. Reviewers: read this file and
`ARCHITECTURE-MAP.md` (same folder). Canonical copies live at
`https://bitcoin-fund-manager.com/robot/PLAN.md` and `/robot/ARCHITECTURE-MAP.md`;
mirrors in `/apps2/robot/` and `D:\work\bfm\apps2\robot\`.

---

## 0. THE ORDER (owner, 2026-08-29, verbatim)

> "build a app at https://bitcoin-fund-manager.com/apps2/robot that ranks the most bullish and
> bearish days and hours, summarizes realtime bullish and bearish rsi and obv divergences from
> /rsiobv (must get alerts within 1 candle of latest slope endpoint). Add all tickers from ostium.
> highlight sp500 nas100 or tech100 sp10 gold oil. I want a dashboard that shows me 5 conditions:
> bullish / bearish day of week, 10 min window, rsi div, obv div, timeportal.pro api result. An
> auto trading bot will determine whether to trade or not and how much to bet based on these 5
> factors. So make the app ready to use by bot. Bot trades and sizing will be determined by an
> algorightm that weighs each of these 5 readings. First, surface all readings for visual
> confirmation."

Follow-ups (same session, verbatim):
- *"write a project plan and architecture map at /robot for other llms to review"* — this file.
- *"timeportal api will be separate build."* — C5 is a pluggable slot, not an integration (§2).
- *"algo for bet sizing and placement is future build"* — confirms §1's scope line: this app
  surfaces the five readings only; no scoring, no sizing, no placement.

## 1. WHAT IS BEING BUILT — one sentence

A read-only **conditions dashboard + machine endpoint** at `apps2/robot/` that, for every
Ostium-tradeable ticker, surfaces FIVE independent readings — (1) day-of-week bias, (2) 10-minute
window bias, (3) RSI divergence, (4) OBV divergence, (5) timeportal advice — each with its
source, freshness stamp, and coverage stated, in a shape the auto-trading bot (hub-and-spoke
system, `/robot` hub + `trade.git` spokes) can consume without scraping HTML.

**Explicitly NOT in scope (v1), owner-confirmed 2026-08-29** (*"algo for bet sizing and
placement is future build"*): the weighting algorithm, bet sizing, trade placement/execution,
the timeportal api (separate build), any new Telegram stream, any new data fetcher. This app
READS existing single-writer stores and computes/reshapes only.

## 2. THE FIVE CONDITIONS — source, contract, freshness

| # | condition | source of record | how read | freshness |
|---|---|---|---|---|
| C1 | Day-of-week bias | `apps2/dayality/data.php` (10-slot week frame, log-bp, holiday-flagged) | HTTP, same host | daily store, 3×/day refresh |
| C2 | 10-min window bias | `apps2/hourality/data.php` (per-ticker 10-min block sums over the 10-min tape store, board of 80 names) | HTTP, same host | 10-min tape, session-live |
| C3 | RSI divergence | `apps2/obv` engine (`OBVX.detect` → shared `A2.divergences`) over `obv/data.php` bars — the SAME engine `/rsiobv` ships | node subprocess importing `../obv/engine.js` + `../shared/signals.js` VERBATIM (rival-engine ban, rsiobv spec) | per candle; see §3 |
| C4 | OBV divergence | same as C3 (OBV series instead of RSI) | same | same |
| C5 | timeportal api result | **a SEPARATE BUILD** (owner 2026-08-29: "timeportal api will be separate build") — a future timeportal.pro endpoint | HTTP GET behind one adapter function in `lib.php`; URL is a config constant | fail-open until built |

C5 note (owner 2026-08-29, verbatim: *"timeportal api will be separate build."*): this app
ships C5 as a **pluggable slot**, not an integration. `lib.php` has one adapter
(`tp_advice()`) with the endpoint URL as a config constant, currently unset. Until the
timeportal build lands, the dashboard shows `C5: not built — separate build pending,
fail-open (no pauses)` and the machine block serves `cov:false, why:"timeportal api not
built", fail_open:true` with the hub-order default semantics (`pause_all/longs/shorts =
false`). When the timeportal API exists, wiring it is: set the URL, map its response to the
pause vocabulary, add its fixture to the suites. The expected response vocabulary
(`pause_all / pause_longs / pause_shorts / per-ticker`, per the 2026-08-27 hub order) is the
adapter's contract; the timeportal build should target it or renegotiate here first. The hub
advice row is NOT read by this app (review ask 3 resolved: no hub.sqlite coupling).

## 3. THE HARD REQUIREMENT — divergence alert within 1 candle of the slope endpoint

**CORRECTED during P1 (2026-08-29) — the shipped engine ALREADY satisfies this order.**
The first draft of this section designed a provisional/confirmed two-lane detector on the
belief that the house detector needs `look=5` right-side bars. Reading the deployed
`apps2/obv/engine.js` shows the owner already ordered that away on 2026-08-24 (*"we do not
need to wait until confirmation. we can just wait until pivot candle closes then wait one
more candle… its best to embrace more risk and get earlier alerts"*):

- The detector is **asymmetric**: full width W on the LEFT, **`CONFIRM_BARS = 1` closed
  candle on the RIGHT** — a divergence is KNOWABLE 1 candle after its slope endpoint, which
  is exactly this order's requirement. A later candle can invalidate the pivot; that risk
  was explicitly bought by the owner and is disclosed on-page here too.
- `OBVX.detect` is a **multi-width sweep** (W ∈ {2,3,5,8,13} ∪ slider) merged on the pivot
  pair; `.widths.length` = agreement count. `OBVX.liveDivs` gates to age ≤ `FRESH_BARS = 5`,
  age measured FROM CONFIRMATION (0 = knowable on the newest candle).
- **Robot therefore adds NO detector logic**: `divscan.mjs` calls `OBVX.detect`/`liveDivs`
  verbatim (RSI series via shared `A2.rsi(14)`, OBV via `OBVX.obvTuned` classic). The machine
  block carries `age_bars` per divergence; "within 1 candle" is met when the scan is at most
  one candle stale, so robot's cache invalidates on obv's payload stamps and the freshness
  test asserts a planted divergence is visible with `age_bars = 0` one candle after its
  endpoint.
- Candle set = the rsiobv alert set (owner 2026-08-28): **30m · 45m · 1h · 4h · 1d · 1w**
  (engine `ALERT_RES` carries the first five; 1w was added by the rsiobv order — robot
  follows rsiobv).
- No new Telegram stream: rsiobv's scanner already pushes these divergences to the
  divergence room. "Alerts" here = the dashboard + machine readings being fresh within 1
  candle. (Open question Q3 if the owner wants push too.)

## 4. UNIVERSE, HIGHLIGHTS, COVERAGE — stated, not hidden

- Universe = **the 71 Ostium-venue tickers** from `apps2/ostium/data/snapshot.json`
  `universe.entries` (entries with venue `O`/`O:*`): 44 rth-equities, 13 23h-futures
  (SPX, NDX, DJI, DAX, FTSE, HSI, NIK, CL, BRENT, XAU, XAG, XPT, XPD, HG), 9 crypto, 5 FX.
- **Highlights** (pinned rows, bold, first): SPX (sp500) · NDX (nas100/tech100) · XAU (gold) ·
  CL + BRENT (oil). **SP10 is NOT in the Ostium inventory snapshot** (built 2026-08-25) —
  flagged on-page as "SP10: not listed on Ostium per the 2026-08-25 inventory pull"; if Ostium
  has since listed it, one universe refresh adds it (open question Q1).
- **Coverage is per-condition and per-ticker, drawn as a matrix, never silently blank:**
  - C1/C2 exist only where the house tape/daily stores hold the name (~80 equities). Ostium
    futures/FX/crypto have **no house 10-min or daily tape** (measured: Ostium's REST exposes
    only a live price snapshot — `universe.json` ostium_note). Those cells read `no tape`,
    counted, with the reason.
  - C3/C4 exist only for the **21 obv assets** — which INCLUDE spx, nas100, gold, oil (the
    highlight set is covered) plus nvda/tsla/aapl/msft/amzn/googl/meta/btc and macro refs.
    Equity overlap is mapped by ticker; everything else reads `not in obv universe`.
  - C5 is a global slot (plus per-ticker overrides when the timeportal build serves them);
    until that separate build lands it reads `cov:false` + fail-open for every ticker.
- The machine block carries the same coverage flags — the bot must be able to distinguish
  "bearish" from "no data" without heuristics.

## 5. MACHINE CONTRACT (draft — the bot's read path)

`data.php?machine=1` returns (additive-only, **consumers version-check with a FLOOR `v >= 1`**,
never `===` — the 2026-08-27 live-consumer breakage rule):

```
{ "ok":true, "v":1, "as_of":<unix>, "app":"robot",
  "machine": {
    "v":1,
    "advice": { "date_et":…, "pause_all":…, "pause_longs":…, "pause_shorts":…,
                "tickers":{…}, "cov":false, "why":"timeportal api not built — separate build",
                "source":"defaults", "fail_open":true },
    "tickers": {
      "NVDA": {
        "cls":"rth-equity", "highlight":false,
        "dow":  { "cov":true, "today_slot":…, "today_bp":…, "rank":…, "n":…, "window":{from,to},
                  "best_slot":…, "worst_slot":… },
        "win10":{ "cov":true, "cur_block":"HH:MM", "cur_bp":…, "rank":…, "n":…,
                  "best_block":…, "worst_block":… },
        "rsi_div":{ "cov":true, "state":"bull|bear|none|conflict", "lane":"confirmed|provisional",
                  "res":…, "age_bars":…, "strength":…, "endpoint_ts":… },
        "obv_div":{ same shape },
        "advice_eff":"ok|paused_long|paused_short|paused_all"   // C5 folded per-ticker
                                                                  // (= "ok" while C5 cov:false)
      }, … } } }
```

- Every per-condition object stamps `cov` (coverage), its data window, and n. Absent data is
  `cov:false` + `why`, never a fake neutral.
- Sign convention: bp readings are `10000·ln(end/start)` (house log-bp). Divergence `state`
  uses the rsiobv vocabulary. No composite score is computed — five readings, separately, per
  the order ("an algorithm will weigh each of these 5" — that algorithm is not this app).

## 6. UI (visual confirmation lane) — plan

- **Board (main view):** one row per ticker × five condition columns, heat-shaded per column
  (per-column scale, house `A2V` shading), sortable per column, highlights pinned. Each cell
  carries its time context (R-UI-21: sparkline/heat-strip or rank-vs-history, never a bare
  badge). Fine print: coverage counts + reasons.
- **Detail (click a row):** the ticker's week-frame bar chart (C1), 10-min block profile (C2),
  and the three-pane price/RSI/OBV divergence view reusing the rsiobv detail idiom (C3/C4),
  full A2C control contract per canvas, SPY/QQQ-vs-SPX/NDX baseline policy per app (futures
  tickers ARE the index — stated).
- **Advice strip (top):** the C5 slot — until the separate timeportal build lands it reads
  "timeportal api: not built — fail-open (no pauses)"; afterwards, that API's row verbatim.
- Chart laws: full `chart-builder.md` exit checklist ticked per canvas in the build report;
  tape+line mandatory pair where 5+ series coexist; hidden copyable ASCII panel generated from
  the same computed objects (LLM read path); banned judgment language observed.
- The existing `apps2/robot/index.html` (hub HANDOFF renderer) is REPLACED by the dashboard;
  `HANDOFF.md` stays in place and gets a visible link from the new page.

## 7. TESTS (both `tests.php` and `tests_node.js`, executing `data.php` in child processes)

1. Machine contract shape + floor-version note present; every ticker carries all 5 keys.
2. Provisional freshness: planted divergence visible at `age_bars ≤ 1` (§3).
3. Confirmed lane == classic detector output, numerically, on fixtures AND the real store.
4. Coverage honesty: a no-tape ticker yields `cov:false` + `why`, never zeros.
5. Advice fail-open: absent row ⇒ the exact hub defaults; a set row round-trips.
6. Cache key hashes the FULL (path, mtime, size) set incl. this app's own code (trend defect #2).
7. Upstream-down: each upstream failing ⇒ that condition reports `cov:false`, the rest live
   (no page-wide 500).

## 8. PHASES

1. **P0 (done)** — this plan + map, deployed for review.
2. **P1** — `lib.php`/`data.php`: universe join, C1/C2 reshape, C5 read-only, machine block, cache.
3. **P2** — `divscan.mjs` (node, imports obv engine verbatim) + provisional lane + C3/C4 wire-up.
4. **P3** — UI board + detail canvases + ASCII panel, exit-checklist pass.
5. **P4** — test suites green locally + on VPS, deploy, live curl verification, git snapshot,
   ledger entry, USER-REQUESTS.md finalized from this plan.

## 9. OPEN QUESTIONS for the owner (defaults chosen so the build never blocks)

- **Q1 — SP10**: not in the Ostium inventory. Default: flagged absent; add on universe refresh.
- **Q2 — "hours" ranking**: C2 is the 10-min frame per the 5-condition list; the "most bullish
  and bearish … hours" ranking is served as 1-hour aggregates OF the same 10-min blocks (six
  blocks per hour, same store, no new unit mixing). Confirm that reading.
- **Q3 — push alerts**: dashboard/machine freshness only, no new Telegram stream (rsiobv's
  scanner already owns confirmed-div alerts). Say the word if provisional divs should also push.
- **Q4 — stats windows**: C1/C2 default to the full store window with the standard window
  selector; the machine block stamps the window used. If the bot should read a FIXED window
  (e.g. trailing 1y) for reproducible sizing, name it.

## 10. PROGRESS (live — updated as the build moves; owner order 2026-08-29: "update progress
## in plan doc as you go.")

| phase | status | when (KST) | notes |
|---|---|---|---|
| P0 plan + map | ✅ done | 08-29 | deployed to /robot, ledger logged; C5 + sizing-algo scope addenda folded in |
| P1 lib.php/data.php | ✅ done | 08-29 | machine door live; warm build 2.7 s / 132 KB; cold-profile budget 60 s/request (nginx 504s at 120 s, measured), CLI warm filled all 80 profiles |
| P2 divscan (C3/C4) | ✅ done | 08-29 | engines loaded as exact shipped bytes; all 6 lanes in 2.2 s; 48 live divergences on first real scan; §3 corrected → D4 |
| P3 UI | ✅ done | 08-29 | board live at /apps2/robot/ — A2V chips + heat strips (pooled scales), highlights pinned, sortable, class filter, ASCII panel, Save Data |
| P4 tests + deploy | ✅ done | 08-29 | tests_node.js 31/0 + tests.php 33/0 ON THE VPS (child-process execution); page + machine door live-verified; USER-REQUESTS.md written; git snapshot + ledger |
| P5 the 2026-08-30 orders | ✅ done | 08-30 | **five owner orders in one pass** (see USER-REQUESTS.md for each, verbatim, with what shipped): the missing Fri close → Mon open leg (a rendering fault, not a data one — anonymous unlabelled chips, an ASCII panel that omitted the leg, and a weekend-null "now"); **every clock time is ET** (`rb_c2_cur_k` was reading UTC minutes against an ET grid AND colliding with the previous session's identical `min` values — 14:00 ET resolved to yesterday's block; `hm_utc` renamed to `hm_et` as a lying label with zero consumers); the **C1 alias fix** lifting coverage 36→39 / 35→39 and giving the four majors 25-year day-of-week samples; the **23h-futures group rendered first** as the longer-window section; **four ality columns with four full-height NOW lines**, axis declared once per column, cells quiet and every number in a tooltip; **per-class grouping** with coverage stated once per group; **crypto reduced to BTC + ETH** with the rest enumerated under `machine.excluded`. Suites **153/0 + 69/0** on the VPS; **CDP 68/68** at 1440 and 360. `/apps2/weekality` shipped mid-pass and is **fully wired** — robot adopted its trading-session frame verbatim (week-of-month coverage 0 → 39); `/apps2/opexality` is probed (floor `v>=1`) but not yet serving, so its axis and NOW line draw while its cells state why. |
| P6 VIX regime (C6) | ✅ done | 08-30 | owner 11:08 KST, verbatim: *"/robot should also factor in vix."* → ONE market-wide REGIME reading (not a direction signal), leaf-read from `/apps2/volts` (`levels['^VIX']` dist over 9,233 sessions since 1990 + the `vix_vix3m` segment + `volts_shape`, all verbatim): level banded on volts' OWN percentiles (low<p25≤typical<p75≤elevated<p90≤high) × term state by pure arithmetic on VIX/VIX3M vs 1.0. Served top-level as `vix` / `machine.vix` (additive, floor unchanged), NEVER per ticker; header strip above the groups with both readings PINNED on their own distributions (burned title + law-5 stamp + 1.0 parity line); state-dependent copy from ONE declared table (12 combos + incomplete), coverage-tested + banned-words/no-forecast scanned; stale volts (>5 d) ⇒ `cov:false` with the measured age, re-checked per request outside the payload cache. SCORING-PLAN gained **D7** (gate/scale/condition — owner to decide, no defaults). See USER-REQUESTS.md for the full disposition. |

Build status: **v1 SHIPPED.** Open items: PLAN §9 questions (SP10 / Q2 hours reading / Q3 push
/ Q4 fixed window), the E1 exception (native detail canvases on one owner order), and the two
future builds (timeportal advice API → wire `tp_advice()`; weighting/sizing algo → consume
`data.php?machine=1`).

Build decisions made during P1 (measured, not assumed):

- **D1 — C1 source**: `dayality/data.php?machine=1` `dow.rows[]` (88 names) carries per-name
  `seg[10]` with n, sum/mean/sd/se bp, up_rate, t/p vs own + zero — the machine block passes
  these through per ticker (raw distributions included, review ask 2). The 4 MB `map=1` day
  matrices are NOT fetched.
- **D2 — C2 source**: hourality's board rows carry best/worst block + rz z-scores but NOT the
  per-slot profile; the profile (`sel.tot[52]/cnt[52]`) is served only per `?sym=`. Robot
  therefore fetches per-sym ONCE PER SESSION DATE per ticker (~44 taped names), cached
  per-ticker on disk keyed by the store's last session — consistent with the
  prior-session-complete machine policy (§4 of the map). The live `cur_block` reading is the
  HISTORICAL mean for the block now open, not live tape. Upstream optimization option logged
  for later: an additive `profiles=1` param in hourality serving all profiles in one call.
- **D3 — C5 stub**: `tp_advice()` adapter compiled in from day one, URL constant empty,
  fail-open + `cov:false` (owner: separate build).
- **D4 — §3 corrected**: the deployed obv engine is already asymmetric (`CONFIRM_BARS=1`,
  owner 2026-08-24) with a from-confirmation freshness gate — the planned provisional lane
  was redundant and is dropped; robot reuses `OBVX.detect`/`liveDivs` verbatim.

## 11. REVIEW ASKS (for the LLMs reading this)

1. Attack §3 as corrected: the ≤1-candle guarantee now rests on (a) the engine's
   `CONFIRM_BARS=1` and (b) robot's cache being at most one candle stale. Is the cache
   invalidation chain (obv payload stamps → robot cache key) tight enough at 30m, or does
   robot need its own warm cron in v1 rather than as a P4 option?
2. Attack §5: is anything missing that a sizing algorithm would need (e.g. per-condition
   history depth, dispersion, or the raw slot distributions)?
3. Attack the C5 adapter contract (§2 note): is the pause vocabulary the right interface for
   the separate timeportal build to target, and is fail-open + cov:false the right default
   while it does not exist?
4. Look-ahead: C1/C2 stats include the current session where upstream includes it. Should the
   machine block serve prior-session-complete stats only (trend-app style `as_of: prior_close`)?
   Current plan: YES for the machine block, page may show live-inclusive with a label.
