Live: https://bensumm.github.io/The-Ledger/
A self-contained web app for finding and managing OSRS Grand Exchange flips. Vanilla JS, no build step, no framework, no bundler — plain static files served by GitHub Pages. Installable as a PWA (works on desktop and iOS home screen), but primary development is now on desktop.
Every price/margin in the app is after the 2% GE sell tax (floored per item, capped at 5m, none under 50gp). Price convention follows flipper usage: Buy = the instasell price (where you place buy offers), Sell = the instabuy price.
- Finder — ranks flippable items by a budget-aware rating (profit/hr × a
quality dampener blending ROI, liquidity, stability, and turnaround; hover the Risk
grade for the per-factor breakdown). Stability here is a cheap live-price-vs-guide
proxy — the full regime-drift check lives on the Trends page. Sortable by rating,
profit/hr, margin, ROI, or volume. Typing a search query reveals every mapped
match — including cheap items (soul rune ~300gp) the browse-mode price floor normally
hides; those search-only rows show
—for the rating columns and lean on the quote button + star. - Trends — deep per-item analysis. A live "Suggested plan" (instant buy/sell + patient pricing that sizes a wider-margin offer off the recent 2h range), a regime-shift guard that warns when a recent price-level jump makes the hourly-timing stats unreliable, a plain-language guide-divergence readout ("Why this trend?"), 3-month price history, and a collapsible timing & seasonality section gated on a walk-forward backtest (hourly charts only appear when the timing edge is actually proven out-of-sample).
- Watchlist — star items to track their live margins each refresh; the same starred set is the repo-shared watchlist the pipeline scans. (The old Signals tab — live cheap-window buy signals — was removed in 0.57.0; Ben didn't use it.)
- Ledger — per-item grouped open/closed positions with after-tax realized/unrealized P/L, summarized in the "Coffer" header tiles. Item names link to Trends; multi-lot groups expand via a chevron; a "P&L by" Day/Week/Month control on the Closed-flips header drives a period strip whose buckets click to filter the table by sell date; the manual-entry form is a collapsible section and the closed columns are sortable.
- Fill-data pipeline — see
pipeline/(below): captures real GE trades from RuneLite tofills.jsonso the tool can eventually calibrate its predictions against actual fills.
index.html— the app shell (markup only)styles.css— all stylesjs/— app logic as ES modules:state.js(shared mutable state as oneSTATEobject + constants + persistence + diagnostics),money-math.js(the tax/margin/bond MATH — canonicaltax()/netMargin/netMarginQty+ the ONE BOND tax exceptionBOND_ID/isBond/bondFee: a bond is tax-exempt but pays a 10%-of-guide retrade fee, sonetMargin(low,high,{bond,guide})=sell − (buy + fee), tax-free; plus the genericclamp/now),money-format.js(gp/number DISPLAY formatting —fmt/fmtSig/fmtP/fmtTurn/fmtHour/fmtHourRange+ukHourOffset/localTzAbbrev/pad2/parseGp/sgn/grade/gradeCls; split out of the oldformat.jsin the R2 rename.fmtHourRange(startH,endH)labels a diurnal hour-of-day window in BOTH the runner's local zone AND the UK — e.g.01:00–03:00 PDT / 09:00–11:00 UK— so peak/dip window narration can't slip between the Pacific-local hours the tools emit and the UK-driven demand basis; the offset is Intl-derived per instant, DST-correct for both zones),charts-static.js(static inline SVG —svgLine/svgBars, fixed-size, no interaction; still used by the Trends hourly seasonality charts + the quote sparkline),charts-interactive.js(CL — the reusable interactive SVG chart:createChart(container,{series,overlay,fillBetween,refs,bands,markers, kind,yFmt,xFmt,spans,span})→{setSpan,destroy}handle, with pointer-drag PAN, wheel/pinch ZOOM about the cursor, a span selector, y-axis auto-rescale to the visible window, a hover tooltip + crosshair, and aDEFAULT_SPANSexport. Max zoom-in is density-floored (~4 sample points,medGap*4) so you can't zoom into empty space; explicit span buttons bypass that floor to hit their exact duration. Degrades to a no-op on a missing container or empty series. An optionaloverlaysecond line +fillBetweenshading draws the Forward-forecast low/high CONE (0.61.0; absent config ⇒ byte-identical single-series). Consumed bytrends.js— the Recent movement (2h), Price history (1/7/30/90d windows), Diurnal timing (7d/28d toggle), and Forward forecast (cone) charts; more surfaces adopt it over time. ADDITIVE —charts-static.jsstays intact),marketfetch.js(shared browser fetch layer — one timeout-guardedjget- one cached
fetchTs/fetch24hstore, A2),market.js(price/guide fetch + scoring; keeps the bond in the catalog — searchable — with a bond-aware Finder margin viabondMarginOpts; AP4desirabilityOfcomputes the Finder's shared rank + Desirability grade offjs/estimators.mjs+js/rating.mjs.vol24Of(id)(0.74.2) is THE ONE HOME for turningSTATE.VOL24into a daily volume — every reader goes through it so the absent-vs-unavailable rule cannot be re-derived per call site: an item ABSENT from a PRESENT/24hmap traded ZERO (measured — 0.0% of absent items traded during the day the map covers), while a null return means the MAP is unusable and the consumer must keep its wide prior. Consumed bydesirabilityOfand bytrends.js's item header),trends.js(archive + seasonal analysis + regime/patient/backtest — renders the Trends view; pure analytics live intrendcore.js; TV — also renders the Diurnal timing section via the sharedwindowread.mjshourProfile/deriveDiurnalRange+ acharts-interactive.jsbar chart + an inform-onlyvalidate.mjsreachValidatornote — the same computation the console prints),trendcore.js(TC1 — pure DOM-free Trends analytics: hourly/seasonal decomposition, the walk-forwardbacktestPlangate,patientTargetsoffer sizing,bestWindow/median; moved out oftrends.jsforpipeline/test/trendcore.test.mjs),quotecore.js(DOM-free quote model + canonical market-table cells —computeQuote/regimeDrift/quoteCells; shared byte-for-byte with the node analysis scripts; also home torecentDirection(DP1),flushSignal(DL2 — the reactive liquid-flush firing read, consumed ONLY bypipeline/commands/watch-positions.mjs --dip, no app import), andnominateDip/reconcileDipPool/pruneDipPool(DL4 — the scan's flush-SUITABILITY nomination + the quality-ranked, self-pruning pool write;selectNominationsis the legacy dedup/cap, retained + tested but no longer on the write path — all consumed ONLY bypipeline/commands/screen-flip-niches.mjs, no app import); also the ONE type-7 quantile/median home (SF-1):quantileSorted(pre-sorted input) +quantileOf/median(sort a copy) —termstructure.mjsre-exports it asquantile,retrojoin.mjsaliasesquantileOf; and home togrossFromNet(PLAN-SALE-LOG-TAX — the EXACT sell-side inverse oftax(): the smallest integergwithg − tax(g) === round(net), found by a ±2 scan aroundround(net/0.98)thennet + TAXCAP(the cap region falls out of tax saturation). NOTbreakEven(that answers ≥ and lands 1gp high where two consecutivegshare one net — the per-item floor makes ~every-50th pair collide, so an ask at an exact-2% point recovers 1gp low: display-only, realised never routes through it; fractional net inverts the rounded value). The FALLBACK gross-recovery for.json-era networthin the reconstruction + the raw-row display sites — since SLT C5 those prefer the plugin's own recorded tax (worth + tax, exact) when the row carries it; bond-blind liketax()— the quarantined-bond caveat applies),forward-reach.mjs(the shared FORWARD-SCORING primitives over the 1h archive —touchedAt(bid side,avgLowPrice),reachedWithin/maxHighWithin(ask side,avgHighPrice),covers,firstIndexAfter, and the END-OF-WINDOW bail pairendLowWithin/endHighWithinoff onelastPrintWithin(series, from, windowH, field)(chunk 4 homed them here;js/reach-surface.mjshad carried a privateendLowWithintwin). The two bails are DIFFERENT POLICIES for a seller whose ask was never reached — cross the spread into a standing bid (avgLow, chunk 1's choice) or rest at the ask level (avgHigh) — andjoin-exit-ev.mjsmeasures that they rank contenders differently, so a consumer must say which it charges rather than inheriting one. Lives injs/as of PLAN-REACH-SURFACE chunk 0 (it waspipeline/lib/market/forward-reach.mjs; that path is now a one-line re-export shim so every pipeline importer resolves byte-identically, thepipeline/lib/signal/estimators.mjspattern). The move is what letsjs/reach-surface.mjsbuild the surface from the same walk the joiners score with —js/never importspipeline/, so a primitive both sides need has to live here.coversis the load-bearing one: an unresolved window is DROPPED, never counted as a miss, because counting it biases every rate DOWN by exactly the truncation at the end of the archive. Side convention is pinned byjs/quotecore.js— a BUY fills againstavgLow, a SELL againstavgHigh. Pure: no fs, no fetch, no clock; the caller supplies thets-ascending series. ⚠ It is shared byjoin-asym-outcomes.mjs+join-reach-outcomes.mjs+js/reach-surface.mjsONLY:join-reach-basis.mjsstill carries its own independentscoreForwardwalk, andpipeline/lib/market/printed-at.mjsa third — so a fix here does NOT reach every reach joiner. Consolidating them is unfinished work, not a claim to repeat;printedAtadditionally has the better tristate (nullwhen no bucket exists, whichreachedWithincollapses tofalse)),reach-surface.mjs(PLAN-REACH-SURFACE chunk 1 — the empirical reach surface p(ask, H) for ONE item, replayed from its own 1h archive.buildReachSurface(series, opts)walks origins everystrideHhours and scores a z-grid of ask levels againstmaxHighWithin, returning the p(z,H) grid plusrefHigh/disp,bailZOnMiss(the miss payoff, in z), the per-horizon refusal, and a reported-never-applied 1h-vs-5mgrainBias.surfaceProb(surface, ask, H)reads a live ask off it;surfaceShapereduces the curve to z50/z20/spread;referenceAsOfexposes the point-in-time reference. It re-derives nothing:refHighiswindowread.mjs'srecentQuant(days,'ask',0.5,3)anddispitsiqr(exported from that module for this — it was module-local), both fed bywindowStats, whosept.timestampshape is bridged from the archive's.tshere rather than inwindowStats, where every other caller already passestimestamp. Everything is@provisional-apiuntil chunk 3'sread-exit-surface.mjsconsumes it — chunk 4 scores the surface before anything prices off it. Four properties are load-bearing and each has a killed mutant inpipeline/test/reach-surface.test.mjs: (1) no look-ahead —refHigh/dispare re-derived at every origin from complete days strictly before it, so a level scored 90 days ago never sees day 91; (2) unresolved windows are DROPPED, never scored as misses, and an origin whose window held no printing bucket is dropped too and COUNTED (noPrintDropped) because the archive cannot tell a quiet hour from an unfetched one — that drop biases p UP; (3) refusal is a WIDTH bound — a cell is thin by its Wilson half-width (Wald reads 0 at p=0 and would price an empty cell as certain), computed onnIndep(origins thinned to non-overlapping windows) rather than the raw count; a HORIZON refuses on its decision cell, the one nearest p=0.5, because "every cell thin" can never fire; (4) the grain bias is reported, never applied — passingfiveMinmust not move a grid cell, andfiveMinCoveragerides beside every delta since a ~0 delta on a thin item means NOT MEASURABLE rather than unbiased. The z axis is(ask - refHigh)/disp, not a % grid: a raw %-above-median grid inverts the trend split. z-monotonicity needs no cleanup — it holds by construction (one origin set, one threshold per origin; measured 0 violations in 22,500 adjacent pairs, so the specified isotonic pass was deleted rather than shipped inert); the H axis DOES invert because its origin set shrinks with H (155 violations over 250 items, max 12.1pp) and keeps a running max. REACH IS NOT FILL — queue position is invisible in bucketed aggregates, so p bounds P(fill) from ABOVE as a working bound, not a theorem: the folded plan's §6.1 RETRACTED the theorem reading — the 1h-average instrument pushes p the other way on liquid items, so the net error's direction is item-dependent there (read it offgrainBias, never assume it) — and every consumer must say so. What chunk 1 MEASURED — including that PLAN-REACH-SURFACE §1.5's taxonomy premise did not survive and its ordering inverted — is §1b of the folded plan (git show bdea911:plans/PLAN-REACH-SURFACE.md, folded into PLAN.md 2026-08-30); don't restate it here),exit-ev.mjs(PLAN-REACH-SURFACE chunk 2 — the inversions that turn a reach surface into a PRICE.evCurve(surface,H,{bailNet,delayCost})scores every level of one horizon atEV = p·net(ask) + (1−p)·(net(bail) − delayCost)(tax frommoney-math.js, the ONE definition);askStaris its argmax,askForHorizonthe highest level clearingpTarget, andhorizonForAskthe SMALLEST horizon clearing it — returned with the full p-by-H row, so the threshold never travels alone; p is read throughreach-surface.mjs'ssurfaceProb, the ONE interpolation, so every row carries itsciHalf. Six properties are load-bearing and each has a killed mutant inpipeline/test/exit-ev.test.mjs: (1) EV has an interior maximum on real curves — the property the co-log scorer lacked, and without it nothing can rank; (2) the miss payoff is PER-CELL (bailZOnMiss) because that is the conditional expectation the decomposition asks for — and its MEASURED DIRECTION is the opposite of the intuition: at a low ask only a catastrophic window misses, so E[bail|miss] is WORST there and rises toward the unconditional value as the ask climbs, which means per-cell prices at or ABOVE the unconditional form, never below (an earlier version of this entry, the module header and the test all asserted the reverse); (3)delayCostis charged to the MISS branch only — on both branches it is a constant at fixed H and cannot move the argmax at all, so the asymmetry is what makes waiting cost anything; (4) a maximum on the last SCORED z is a refusal, not a price (the edge is the scored one, not the declared grid — a dropped cell would otherwise pass as interior); (5)net()is the ONE tax definition on BOTH legs — a tax asymmetry is 2% of price, larger than the whole EV spread being optimized over; (6)horizonForAskreads horizons ascending regardless of grid order. Consumed by chunk 3'sread-exit-surface.mjs, which is why the@provisional-apimarkers are gone from every export butaskForHorizon— that one is now@test-onlyand has no production consumer BY DESIGN, since §1c forbids its ask as a price.askStaris an argmax over a PLATEAU, not a point — adjacent cells sit within a few basis points of refHigh of each other, decided off a p known to a few pp, so a consumer must present a band and never a false point;read-exit-surface.mjsowns that. The chunk-2 stop-or-go gate and what it measured live in the folded plan's §1c — PLAN-REACH-SURFACE folded into PLAN.md 2026-08-30; full textgit show bdea911:plans/PLAN-REACH-SURFACE.md(don't restate its numbers here — they were, once). The one rule to carry out of it:pTargetmust never pick a price. It answers "how long", never "how much". ⚠ And chunk 4 has now measured whataskStaris worth: scored against realized net gp over the archive it LOSES to a deployed incumbent, decisively enough to fire the plan's pre-registered null branch. Everything above is still how the arithmetic works; none of it is a reason to price an offer offaskStar. Seejoin-exit-ev.mjs),windowread.mjs(P2 — pure window-range/reach math:windowReliability(DT4, 2026-08-10 — the split-half hours gate: parity-split the lastWINDOW_RELIABLE_NIGHTS(14) days,hourProfileeach half, Pearson-correlate the de-trended devLow/devHi 24h vectors, gate onmin(rLow,rHi) ≥ WINDOW_RELIABLE_R(0.6, PLACEHOLDER). Returns a TRI-statereliabletrue/false/null — null = not measurable, deliberately distinct from a measured fail. Pins its OWN 14-day window off the RAW series, never the caller'snights. Consumed bydiurnalTimedLap(lap.reliable),formatTimedLap,softBuyRead/softBuyHoursClause, the/scandigest soft-buy cell,read-schedule.mjsandread-window-range --profile; shadow-logged bysuggestlog.mjs. DISPLAY-ONLY — it gates no grade, rank, verdict orscreen.jsonfield. It DOES move displayed prices on the ~0.8% of rows that PASS: DT4b refits those over the gate's window, and hours and levels come from one fit, so the soft-buy floor/diurnal levels shift there — seedisplayFitNights) +softBuyHoursClause(the ONE wording for the three hours states,full+compactstyles) +windowStats/quantLow/quantHigh/touchedDays/reachedDays(EC2 2026-09-08:windowStatsalso returnsforming— today's partial-day{key, low, hi, volLo, volHi}, still excluded fromdaysas always but no longer silently discarded;floorCeilingTrack's monotone-certainty read consumes it) + the RC1recencySplit/recentQuantreach-contamination guard +askExitRead(PLAN-POSITIONS-WINDOW-READ 2026-07-18 — the ONE ask-side "typical exit" assembly: daily-HIGH q50/q75/every-day levels + the scored list-price reach/placement + the ≥FIVE_MIN_MIN_DAYS5m-grain reach; pure over already-computedwindowStatsresults, soread-window-range.mjs's--askblock andquote-items.mjs --positions' auto-surfaced big-ticket↗ windowExitnote render from ONE definition instead of re-sequencing the primitives) +reachMargin(the fade check, 2026-07-20 — folded INTOaskExitRead, so it rides both surfaces: the cushion TRENDfading|stable|extendingover the recentMARGIN_NIGHTSdays atMARGIN_FADE_FRAC, the current-day cushion, and today'space— live-now vs the reaching-day median at this hour-of-day off the in-handhourProfile; symmetric ask/bid, inform-only, placeholders pending F1; the lean summary ridessuggestions.jsonlviawindowExitShadow) +avgBoundRead/formatAvgBound(2026-08-05 — the DEEP-BOOK reach-misread guard:touchedDays/reachedDayscount days the per-day extremum of 1h-bucket AVERAGES crossed a level, a bias that is strict IN PROPORTION TO LIQUIDITY, so a low N/M on a deep book means "below every hourly average", not "never fills". Fires only when the limiting-sidevolDayclearsREACH_RELIEF_MIN_VOL(reused fromjs/estimators/reach.mjs, never forked — passed in as an opt sowindowread.mjsstays a leaf) AND the hit fraction is belowAVG_BOUND_LOW_FRAC; names the averaged basis, the signed gap to the most extreme daily average (read against AC2's ~0.36–0.56% measured smoothing bias), the in-window competing pool, and the bid-side "low placement = deep patient entry" gloss. Rendered byread-window-range.mjson every scored--bid/--ask/--exitAND mirrored into the--json/--outdump asavgBound(the machine path is the point — agents readverify.json). THIN books return null and print byte-identically, an asymmetry pinned by test inpipeline/test/windowread.test.mjs. INFORM-ONLY, n≈0 — gates nothing) + the hour-of-day diurnal profilehourProfile/deriveDiurnalRange(2026-07-09 — de-trended per-hour dip/peak detection, side-specific clustering, and the stale-to-live guard; the peak-timing enginescreen-flip-niches.mjsauto-runs andwindowrange --profileprints) + multi-peak windows (PLAN-MULTI-PEAK-WINDOWS 2026-07-23 —hourProfilegains ADDITIVE prominence-rankedpeaks/dipsarrays (length 1–2);peaks[0]/dips[0]deep-equal the unchangedpeak/dip,peaks[1]/dips[1]is a SECOND window that cleared theSECOND_PROMINENCE_FRACtopographic-prominence gate;diurnalTimedLapmirrors it with index-alignedaskReaches/bidReachesarrays — inform-only, n≈0, rendered as a trailing clause byemit.mjs formatTimedLap) +computeReality/realityClause(PLAN-DIURNAL-RECENCY-GUARD 2026-07-24 — each emittedpeak/dipcarries an additiverealitylevel-check:spikeTop(a recent 1–2 day spike over-generalised into the quoted level) /staleOptimistic(an old high the current regime no longer reaches) + a recency-honesttypicalLevelto quote instead, off the cluster's per-day HIGHS/LOWS with ZERO new fetch;realityClauseis the ONE renderer across nine console call sites as of Chunk 2c (2026-08-13) —--profilewindow headers + its→ BID/ASKline, the non---profilediurnal:summary,formatTimedLap's primary BID/ASK and itsalso ASK/also BIDsecondaries,quote-items'windowExitpeak-level bit, the held-lot thesis-frameexit, and/schedule's Level column. (This cell read "the three console surfaces" through Chunks 2b AND 2c; every hand-enumeration of these sites has been short — treat the number as a floor, and see the plan's §10 for what is still bare.) ConstantsSPIKE_REACH_FRAC/SPIKE_PLACEMENT_PCTILE/SPIKE_MIN_GAP_FRAC/REALITY_TYPICAL_QUANT/REALITY_TYPICAL_RECENTNare PLACEHOLDERS; inform-only, n≈0, never gates — the emittedlevelis unchanged. No longer console-only: Chunk 2c added the WRITE side, sorealitynow reachessuggestions.jsonl(timedLapShadow) andverify.json(result.profile, which already carried it) — that is what finally makes "does this flag predict anything" an answerable question) +hourConcentration/diurnalTimedLap(PLAN-DIURNAL-TIMING DT1 2026-07-23 —hourConcentrationis a per-day argmin/argmax-hour CIRCULAR-concentration classifier (mean resultant length R∈[0,1] of each day's own trough/peak hour,HOURCONC_MIN_DAYS/HOURCONC_MIN_Rplaceholders), distinct fromhourProfile's aggregate cluster width;diurnalTimedLapisderiveDiurnalRange's output EXTENDED withnet/roi(the TIMED trough→peak lap,netMarginfromjs/money-math.js) +instantNet/instantRoi(the SAME-HOUR/churn margin — both surfaced, since a big-ticket item can show a NEGATIVE same-hour margin beside a POSITIVE timed one) +bidReach/askReach(recencySplitscored against the chosen dip/peak levels' ownwindowStatsslice) +lowTrend/hiTrend(projectTrajectory) +dipPool/peakPool+trancheComfort/trancheCeiling(DT_TRANCHE_COMFORT_VOL_PCT=0.5%/DT_TRANCHE_CEILING_VOL_PCT=1% ofvolDay, borrowed fromjs/estimators/reach.mjs's n≈6 reach-relief knee, not validated for diurnal specifically — renderedtranche ~X clean · ~Y price-knee, and read the two disclaimers onemit.mjs's caveat before quoting either number: it is a price-degradation knee rather than a clearing cap, and a ROUND-TRIP bound rather than a per-leg one, both of which have been misread off it in a live session) +hourConcentration'scleanverdict; degrades to{degraded:true, reason}, never a throw. DT2 (2026-07-23) wires this intoscreen-flip-niches.mjsfor EVERY flip-niche survivor (was top-picks-only via rawhourProfile+deriveDiurnalRange), rendered through the ONE sharedpipeline/lib/render/emit.mjsformatTimedLap— see that file's README entry. DT3 (2026-07-23) wires the SAMEdiurnalTimedLap+formatTimedLappair intoquote-items.mjs's bare-quotekind:'diurnal'note (prof/drthemselves stay — they still feedextraEst.diurnal, the window-clear peak window, and the forward E4 inputs), and swapswatch-positions.mjs's two directhourProfile+deriveDiurnalRangecall sites (the shadow-log bid/ask co-log, thediurnalAskcycle-fallback exit) fordiurnalTimedLap— those two are VALUE consumers, not note-render sites, so only the underlying computation moved) +softBuyRead/formatSoftBuy/SOFT_BUY_CUE_TEXT(2026-07-22 — the ADD-while-holding soft-buy timing read off the SAMEhourProfile: the dip-cluster FLOOR level + a live-vs-dip-floor@floor/+X%marker atSOFT_BUY_AT_FLOOR_PCT, ending in a cue, with the diurnal DIP window in a trailing parenthetical labelled attended. DT2 (2026-08-09) made the render LEVEL-FIRST and re-scoped the window: it does NOT time a resting offer (71.2% in-window touch vs 70.5% for a random window of the same width; waiting forfeits ~29% of bid fill-days at an identical price), so a resting bid is placed at the level now and the hours are for attended market-TAKING. The@floorcue is FLOOR-AWARE (the fang under-read fix) —softBuyReadtakes an optionalfc(afloorCeilingTrackresult the caller already computed; NO re-derived slope) and resolvesbuy now(flat/ranging),▲ favorable(rising floor — price-trend-only, never a green-light), or▽ caution — floor breaking ↓(broke/crash-risk — a dump artifact, not a discount);SOFT_BUY_CUE_TEXTmaps the cue to its wording so both surfaces phrase it identically.quote-items.mjsrenders it as the⏳ softBuynote on held lots + bare quotes (threading thefcpushTrajectoryreturns), andscreen-flip-niches.mjs's digest soft-buy column delegates to the SAME helper (ONE implementation); inform-only, n≈0, null profile / no fc ⇒ plain cue) +asymPair(PART II PLAN-GRADE-REACH 2026-07-12 — the day-level deep-bid/high-reach-ask realizable pair + P_ask/P_bid, consumed byjs/estimators.mjsasymEstimatefor the◆ asym fillinform line + theasymsuggestions-ledger shadow field) +clearableAsk(PLAN-DEPTH-EXIT DE1 2026-07-15 — the percentile-DEPTH exit: reconstructs a per-day price→volume distribution from the 1h bucket point masses and answers "what can I actually BOOK at?" for a given lot size; the reach count is its qty→0 limit, and a thin book collapses to a null-with-reason; feeds the--depth"BOOK AT ≤X" line + the LIVE DE3depthExitshadow on watch/quote held lots; theDEPTH_*constants module-internal placeholders.depthDays/clearableBidwere REMOVED 2026-07-22, PLAN-REMOVE-DEPTH-PRESSURE-READS — git-revivable) +demandPressure/reachableBand(PLAN-DEPTH-EXIT Extension A PB1 2026-07-15 — the pressure-driven reachable band:s=ln(medVolHi/medVolLo)sets each side's headroombase ± band·φ(±s)·reliabilityoff the recent central daily level (RC1 reused) - the daily-high/low IQR; a thin-VOLUME book collapses to the smoothed center via the sample-reliability
guard (no peak-cap); the
PRESSURE_*constants are exported n≈0 placeholders and the Soul-rune/sell-heavy reasonableness pins live in the test; its surviving consumers are the co-log shadow (bid/band), theread-window-range.mjs --pressureinspector and reverse-flip'sreverseListBand— the pressure SELL model it once priced was RETIRED 2026-08-30 (join-exit-ev.mjs's criterion; CHANGELOG 0.76.0). The Extension-BhourlyPressure/demandRegimeper-hour demand-cycle classifier was REMOVED 2026-07-22, PLAN-REMOVE-DEPTH-PRESSURE-READS — git-revivable) +trajectoryRead(2026-07-21, the fang under-read fix — the shared multi-day SHAPE read over awindowStats().daysseries: classifies rising/falling/oscillating/based/elevated + the window floor/ceiling (with the day each printed) + where aliveRefsits between them; HEURISTIC/inform-only, never gates. Its rendered⌁ read:line was RETIRED at both emitters by R6 —floorCeilingTrackbelow supersedes it; what survives is the floor/ceiling/livePos fields that note now carries) +floorCeilingTrack/formatFloorCeiling(PLAN-DRIFT-VS-CRASH, 2026-07-22 — the phase-aligned floor+ceiling slope-asymmetry classifier thattrajectoryRead's single min-low/max-high collapse washes out: reads the daily-LOW track and daily-HIGH track SEPARATELY, each a recent-window least-squares slope (windowed, so a short trailing wiggle cannot flip it; a volatile END day can — OLS endpoint leverage) classifiedrising|flat|falling+ a raw-sign trailing micro-runfor duration, plus a discrete floor-break flag (latest completed low vs the prior-lookback floor); combines the two slopes + the break intocrash-risk(break dominates) /healthy-trend/compressing-up/mild-cooldown/cooling/ranging. REQUIREMENT #1 phase-alignment: the forming/incomplete day is DROPPED from the slope/break and surfaced separately as provisional. EC2 (PLAN-ENTRY-CONFIDENCE, 2026-09-08 — the Avernic-hilt entry post-mortem): the live forming read arrives via theformingopt (=windowStats().forming, the partial-day aggregates windowStats always excluded fromdaysand now RETURNS instead of discarding — before this, every productiondayswas pre-stripped, sofc.formingwas structurally null and the "provisional" clause never rendered anywhere);todayKeysurvives as the hand-built-days fallback. On it ridesformingContradiction, the MONOTONE-CERTAINTY read: a forming day's low only falls, soforming.low <a completed-days statistic is decided at any hour —under-trough(forming low < min(prior floor, latest): the discrete break is CERTAIN at day end) orunder-last-low(the floor label is STALE — measured over 21,013 archive item-days at 21:00, the next 3 days print below the current floor 88.5% when it fires vs 53.1% when it doesn't; a "cheaper entry likely" read, NOT a trend-reversal predictor — forward slope is unpredicted either way, and 44.1% of rising labels carried the contradiction).formatFloorCeilingstale-marks a rising-flavored label on under-last-low and WITHHOLDS the label on under-trough;softBuyFloorCuemaps under-trough →cautionand rising+under-last-low → the newstale-uptrendcue. The regime GATE (quotecore.jsregimeDrift),driftExitFrom's slope read andjoin-exit-ev.mjs's replay deliberately pass noforming— gates and replays stay completed-days-only. HEURISTIC/n≈0/inform-only, never gates;formatFloorCeiling(fmt injected — windowread stays dependency-free) is the ONE line-render bothread-window-range.mjs's trajectory block andquote-items.mjs's⇅note use); MOVED here frompipeline/lib/so it is node- AND app-importable likequotecore.js; consumed bypipeline/commands/read-window-range.mjs,pipeline/commands/watch-positions.mjs,pipeline/commands/screen-flip-niches.mjs,js/validate.mjsandjs/forecast.mjs(both now app-imported viajs/trends.js, TV). PF1 (2026-07-10) added additive per-hour dispersion fieldsdevMid/devLowSpread/devHiSpread(IQR of the deviation samples) so the forecast band isn't re-derived; every pre-existing field is byte-identical),forecast.mjs(PF1 2026-07-10 — the pure forward 12h/24h price projection: CONSUMES anhourProfileobject and produces a diurnal+trend forecast —diurnalForecast(profile, ctx)→nextTrough/nextPeak{level, band, etaH, window, confidence, mode}+ the per-hour projectedseries, pluswhenBuyable/whenSellable/fmtEta. The interpretable ADDITIVE modelprojLevel(h) ≈ baselineNow + trendPerHour·Δt + deTrendedHourShape(h); anchor from the live quote, shape/dispersion from up to 14d, trend from the recent slope. DEGRADES LOUDLY to{forecast:null, reason}on a spike/decay phase, a live band violation, a thin/short series, an unreliable quote, or a trend-erased dip (trend-only mode); the band widens with horizon. Claims ONLY "recurring diurnal shape + dumb trend extension" — never an exogenous shock. Imports onlywindowread.mjs(no quotecore —phase/mom/reliablearrive as plain ctx). INFORM-ONLY / console-only / provisional (n≈0, every constant a NAMED PLACEHOLDER pending the PF8 backtest); no consumer wired yet (PF2–PF8) and no app import → no APP_VERSION. Pinned bypipeline/test/forecast.test.mjs. Also homes PLAN-OSCILLATION-CYCLE Chunk 1 (the multi-week oscillator lane):driftAdjustedExit(fc, {ceilingSlope,floorSlope,holdHorizonDays})composes diurnalForecast's next trough/peak with a multi-week drift NUMBER (never a direction label — NO phase/direction field), shifting ONLY by the RESIDUAL horizon past the diurnal eta (diurnalForecast already trend-extrapolates to the eta), andoscillationVsKnife(days)— a detector (REDESIGNED at F-A, 2026-07-22 — the original first-difference flip-fraction metric measured day-to-day NOISINESS, not harvestable oscillation, and mislabeled fang/blowpipe's smooth multi-day runs a false knife; see the header comment above the function for the full finding) that detrends the daily mids (same sharedprojectTrajectoryslope, one-home) and splits the residuals into maximal same-direction LEGS, counting a leg as real only pastOSC_MIN_LEG_DAYS+OSC_AMP_NOISE_MULT× the series' own day-to-day noise floor;oscillatingfires atOSC_MIN_LEGS(≥2 direction reversals) — fewer legs is a monotone linear-fit hump (even a CURVED collapse), never a real cycle. Tells an oscillating-while-drifting shape (fang/blowpipe) from a monotone knife where floorCeilingTrack.oscillating structurally can't. F-H (2026-07-22) addedOSC_DETECTOR_NIGHTS=21(> the amplitude gate'sAMP_NIGHTS=14):renderAmplitudeModefeeds the detector its OWN longer trailingwindowStats(...).dayswindow (off the same in-hand series, NO fetch) so it sees the ≥1.5 cycles / ≥3 legs it needs WITHOUT widening the gate'sAMP_NIGHTSdaily-range/ reach/recency read — a sample-size fix BOUNDED by the ~16-day/timeseries?timestep=1hendpoint, not a calibration. ⚠ That endpoint bound is now load-bearing, not incidental (2026-08-11):OSC_MIN_LEGSis an ABSOLUTE leg count with no length normalisation, so the label is a function of WINDOW LENGTH — measured 59.5% OSC at 14d → 99.9% at 60d on the real archive, and ~66% at 14d → ~100% at 30d on a synthetic DRIFTLESS RANDOM WALK containing no cycle at all. Amplitude cannot matter — every threshold is homogeneous of degree 1 in price, so the criterion is scale-free BY CONSTRUCTION and length is the only free variable (an earlier note offered amplitude-invariance as corroborating measurement; it is an algebraic identity, not evidence). The ~15d cap is the only reason the knife temper still discriminates; feeding the detector a deeperarchive.mjsseries (F-H calls this a noted-not-built follow-up, andrenderAmplitudeModealready has that archive open) would take it to ~100% and silently delete the guard. Normalise the criterion BEFORE widening the window — full note in the function header; measured inRANGE-PERSISTENCE-FINDINGS.md. INFORM-ONLY, wired into NO gate in Chunk 1 (gating is Chunk 3); pinned bypipeline/test/oscillation-cycle.test.mjs. Chunk 2 addsdriftExitFrom(profile, days, ctx, opts)— the ONE slope-sourcing + drift-adjusted-exit COMPOSITION (importsfloorCeilingTrackfrom windowread to pull the ceiling/floor slope off an in-handwindowStats().days, NO fetch; builds the diurnalForecast wrapper; callsdriftAdjustedExit) — the reusable caller pattern the amplitude lane established and Chunk 6 REUSES; PURE/tax-free (the after-tax margin stays the caller's concern). Pinned bypipeline/test/oscillation-shadow.test.mjs. Chunk 3 (THE ONLY GATE) turns that margin intoamplitudeGate'smargin-below-floorreject (amplitudeDriftMargin().margin <= 0, direction-agnostic, the floor already inside the margin) sequenced after trend/knife, computed ONCE at the gate stage inrenderAmplitudeModeand reused for the shadow-log, and TEMPERS the knife guard withoscillationVsKnife(a drift-riding oscillator is not a false knife → falls through to the margin gate). Pinned bypipeline/test/oscillation-gate.test.mjs. Chunk 6 REUSESdriftExitFromper-thesis (band/churn/scalp/value drift-adjusted-exit INFORM notes — seejs/flip-niches.mjs; console-only, no gate). Chunk 5 folds the drift-adjusted exit LEVEL into the SHAREDformatFloorCeilingnote path (an optionaldriftopt — the caller passes a pre-computeddriftAdjustedExit()result off its in-hand prof+days, so windowread keeps its one-way arrow) so it rides beside EVERY price suggestion (quote-items.mjstrajectory note,read-window-range.mjs, and — APP-VISIBLE —js/trends.jsrenderForecast); a projected LEVEL never a direction verdict, display-only, degrade-clean; APP_VERSION-bumped (reachesjs/trends.js). Pinned bypipeline/test/oscillation-render.test.mjs). F-F adds a trough-vs-decay DISPLAY annotation to the amplitude reach cell (reachPhaseNoteinscreen-flip-niches.mjs— recent+full both-leg reach plus a 3-signal phase note offoscillating/floorSlope/margin, direction-agnostic in the knife bucket, console-only; pinned bypipeline/test/oscillation-reachphase.test.mjs),validate.mjs(P2 — the pure VALIDATOR REGISTRY(ctx)→{status:pass|caution|reject,reason,evidence}run on EVERY surface:reachValidatorwraps windowread reach + RC1 into caution/reject WITH the reach evidence;floorValidator(P3, BUY-side) rejects/cautions a buy parked above the durable floor — anddurableFloorRead(vres)(2026-08-06) is the ONE canonical extraction of that verdict ({status, ranges, lookback}) for consumers that need the LEVEL read without re-deriving it: the soft-buyunproven-basecue, the⚠N×floorrow probe, and the bucketed caution footer all compose it rather than fork it (windowread cannot import validate — the arrow is one-way — so the caller hands it down);trajectoryValidator(TV1 2026-07-09, BUY-side) is the SHAPE policy overtermstructure'sclassifyTrajectory— knife/oscillating/based/elevated;valueAmplitudeValidator(TV1) the recent-week amplitude+proximity for value;limitValidator(LM1) the rolling-4h buy-limit;dipPostureValidator(DP1, BUY-side, INFORM-only/NEVER-REJECT) the dip DIRECTION read viaquotecore.js'srecentDirection(a reverting dip → caution "cross or pass"; wired inform on band/churn). All degrade to pass on missing data and never throw.runValidators(ctx,{specs})drives a PER-THESIS plan ({key,mode,window}fromjs/flip-niches.mjs) —gate(verdict stands) vsinform(informFlags: annotate-only, clamped to pass, would-have verdict logged vialeanValidators).worstStatus/flags. Screens DROP reject + FLAG caution + SHOW inform notes; explicit asks/held/watchlist never hidden. App-imported viajs/trends.js(TV) — a behavioral change here needs the smoke test + the APP_VERSION rule),termstructure.mjs(P3 — pure DOM-free multi-day term structure over a daily-mid[{ts,mid}]series: the 1/3/7/14/28dtermStructure(median/low/high/pctInRange per lookback), a durable floor (low quantile of the longest multi-week lookback), a robust ceiling (P5 — the symmetric high quantile q85, so a lone spike can't inflate a range), a typical fluctuation (IQR), and a trajectory SHAPE (TV1 —classifyTrajectory: knife/oscillating/based/rising/elevated/flat, attached asts.trajectory); degrades tohasData:false/unknownon a short series. Plus (DT6, PLAN-DIURNAL-TIMING §6, 2026-07-23)basePosition(ts)— a pure, LIGHT read of an already-computedts: reusests.lookbacks[14].pctInRange(the SAME fieldclassifyTrajectoryalready scores) + a 3-way coarsening ofts.trajectory.shape(+ts.recentTrend.dirto split a falling-drift oscillation as "decaying") ontorange-bound/trending↑/trending↓/decaying, for thescreen-flip-niches.mjsBase position note on band/ churn/amplitude survivors (rendered by <
- one cached
0 comments
log in to comment.