Docs: register today's fixes, prune completed roadmap items

CLAUDE.md/docs/library.md updated to match the sell.py/parkcash.py/
scan.py changes in the preceding commits. docs/decisions.md gets the
full reasoning trail: the Black-Scholes closed form marked superseded,
the actual backtest entry (BHP/AL touch-probability table) that both
sell.py and library.md already cited but was never written, the
discretization-bias caveat on that backtest, the price-impact-cap and
demolition-value decisions. docs/roadmap.md pruned back to open items
only - finished work is discoverable from CLAUDE.md/library.md/
decisions.md instead of staying listed as a done backlog entry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 23:07:02 +02:00
co-authored by Claude Sonnet 5
parent 3ee26e5a85
commit 12d26c5d68
4 changed files with 210 additions and 34 deletions
+138
View File
@@ -17,6 +17,144 @@
- If a bigger-model spawn is refused, tell the player and ask them to run the review.
- Routine tool building is not reviewed by a bigger model.
## 2026-10-10 Optimal ask price: first-passage-time (Black-Scholes), not order-book depth
**SUPERSEDED same day - see "Black-Scholes model backtested wrong" below. The closed form here
(`daily_log_vol()`, a driftless-random-walk assumption) is no longer what `tools/sell.py --tiers`
uses; kept for the reasoning trail, not as current guidance.**
- The naive model ("units ahead of me in the book ÷ historical daily volume = days to clear")
is WRONG: it assumes the book is static (no one else reprices, cancels, or adds orders while
you wait) and that historical trade volume at normal prices still applies at a resting ask far
outside the recently-traded range. Tried first, broke immediately: fed a fat-finger 1,000,000
AIC ask the same reachability as the real book, "confirming" it as optimal.
- Correct model: whether the market ever pays price P is governed by price DIFFUSION, not queue
depth. Model ln(price) as a driftless random walk with the ticker's own measured daily
log-return volatility sigma (deliberately no drift - same "don't model trend" rule as
`puga/parkcash.py`), and ask for the discounted value of the first time it diffuses up to P.
Closed form (standard Brownian-motion hitting-time Laplace transform):
y = ln(P/s0), rho = ln(1 + rate), D(P) = P * exp(-(y/sigma) * sqrt(2*rho)) for P > s0
D(P) = P for P <= s0
`rate` is the player's own REALIZED capital growth rate (opportunity cost of the delay - see
below), not a theoretical marginal-build ROI.
- ln(D) is LINEAR in y, so there is no interior optimum: D is strictly decreasing past the
current price (sell near top of book / at the last traded price wins) whenever
`sigma < sqrt(2*rho)`, and rises without bound (a degenerate corner case, not real free money)
otherwise. Checked against this account's real tickers (AL 2.5%/day, BHP 8.1%/day, DEC
10.8%/day vol, all against a 5%/day rate -> threshold ~31%/day): every one is comfortably in
the "sell now dominates" regime. Treat a result where climbing pays as a signal to double
check sigma/rate inputs, not as license to post a speculative high ask.
- Implementation: `puga.history.daily_log_vol()` (pooled variance estimator, scales each
consecutive trading-day gap's squared log-return by its elapsed days so sparse tickers don't
understate how far price can move between trades) + `tools/sell.py --tiers --rate R`.
- This also resolved the earlier "what discount rate" question: use the player's REALIZED equity
growth rate (read off their own in-game equity-growth chart, ~5%/day as of 2026-10-10), not the
scan/build tools' marginal per-building ROI figures (14-29%/day) - those are a ceiling on one
marginal building at current scale (capped by permits, workforce-arrival delay, and market-
depth saturation the moment you scale), not what a freed-up dollar actually compounds at. The
realized rate already nets out all of that friction, which is exactly what a discount rate for
"should I wait for a better price" needs to reflect.
## 2026-10-10 Black-Scholes closed form backtested wrong - empirical model instead
- Tested the driftless-random-walk assumption above against this account's real exchange history
(BHP: 802 trading days, sigma 7.39%/day; AL: 995 days, sigma 4.42%/day). Model predicts
P(touch level k*sigma*sqrt(T)) = 2*(1-Phi(k)), independent of T - the scale-invariance signature
of a true random walk. Measured the real empirical touch frequency for the same (k, T) pairs:
| k | model pred | BHP empirical (T=7/14/30/60d) | AL empirical (T=7/14/30/60d) |
|-----|-----------:|----------------------------------|-----------------------------------|
| 0.5 | 61.7% | 16.6% / 18.0% / 17.5% / 20.1% | 32.6% / 33.9% / 32.4% / 40.1% |
| 1.0 | 31.7% | 8.8% / 8.0% / 8.2% / 7.9% | 9.2% / 10.2% / 10.8% / 16.7% |
| 1.5 | 13.4% | 4.1% / 3.8% / 0.1% / 0.0% | 3.9% / 3.9% / 4.4% / 2.7% |
| 2.0 | 4.6% | 2.0% / 1.2% / 0.0% / 0.0% | 1.9% / 1.5% / 2.0% / 0.0% |
| 3.0 | 0.3% | 0.5% / 0.0% / 0.0% / 0.0% | 0.7% / 0.5% / 0.0% / 0.0% |
Model overstates touch probability by roughly 2-4x at low-moderate k, consistently across both
tickers. Scale-invariance partially holds at low k (flat across T, locally random-walk-like
short-term) but breaks down at higher k/longer T (empirical rate COLLAPSES faster than the model
past k=1.5, instead of staying flat) - the signature of mean reversion: real prices are anchored
to production cost/supply-demand equilibrium, not free-floating, so they wander less far over
longer horizons than a pure random walk would.
- The 2-4x figure overstates how much of this is mean reversion specifically: touches were
measured on daily VWAPs, a discrete sample of a continuous path, and the model's prediction
assumes continuous monitoring. Discrete sampling can only miss touches (an intraday spike that
reverts before the next VWAP looks like "no touch"), biasing the measured gap upward especially
at short horizons (k=0.5, T=7 - standard discrete-barrier corrections put this at roughly
0.58*sigma*sqrt(step) per sampling interval, shrinking as the horizon grows). The k>=1.5/T>=30
COLLAPSE is not explained by discretization (a roughly constant/shrinking effect, not a
progressive one) - that part is real mean reversion.
- Consequence: the model's absolute numbers are upper bounds, not honest estimates - directionally
conservative (it already said "don't climb the book," and the true probability of ever
profitably climbing is even lower than it claimed) but not quantitatively trustworthy.
- Replaced with `puga.history.empirical_touch()`: a non-parametric estimator measuring first-
passage directly from the ticker's own full multi-year history (every historical start day,
first time the price actually moved by the target amount, right-censored samples contribute 0
rather than being excluded) - no assumed distribution, so whatever mean-reversion/fat-tails/etc.
the real data has shows up directly. Inherits the same daily-VWAP discretization conservatism as
the backtest above (a resting ask can fill on an intraday high the VWAP series never shows) - so
it also leans a little more toward "sell now" than fully warranted, not corrected for. Used by
`tools/sell.py --tiers` (combined with queue-depth as a deterministic shift of the hitting time,
not a max - ranking by the resulting hit_factor, not a point estimate of median days) and
`puga/parkcash.py` (two sequential legs: buy-dip,
sell-recovery).
- Refined further: the unconditional version above still averages over EVERY historical start day,
which answers "how often does this ticker move by y from an arbitrary day" - but mean reversion
specifically means the answer depends on where price is NOW relative to its own recent trend.
Conditioned on state instead (`empirical_touch(..., condition_on_state=True)`, the default):
only start days whose z-score against a trailing 60-day mean is within +/-0.5 of TODAY's are
used, falls back to every start day if there's not enough history to compute today's z-score or
the match band is empty. Stays non-parametric (no fitted mean-reversion speed/strength). Real
effect confirmed on DEC (`tools/parkcash.py`): round-trip wait dropped from 19 days
(unconditional) to 3 days once conditioned on DEC's actual current state - a big, real
difference, not a rounding adjustment.
## 2026-10-10 Price-impact cap: real book depth, not a flat 25%-of-flow guess
- `puga/saturation.py`'s `S_SHARE=0.25` capped planned output at a flat 25% of a ticker's traded
flow, regardless of how deep or thin that specific market's standing bid book actually is -
never estimated how much price actually falls as supply rises, flagged by review.
- Tried first: a day-over-day regression of d(log price) on d(log volume) per ticker, the
textbook first-order elasticity estimate. Real data killed it: BHP -0.009, AL +0.004,
DEC +0.038, KV +0.007 - near zero and inconsistently signed. Cause: daily volume spikes in this
market are mostly DEMAND-driven (more buyers -> more volume AND flat/higher price), which
swamps and often reverses the sign of the SUPPLY-side effect we actually want. Shipping this
would have been worse than the flat guess (could tell you adding supply raises price).
- Fix: `puga.saturation.max_units_at_impact()` walks the LIVE bid book (same mechanics as
`puga.market.walk()`) and returns how many units you could sell before the realized price
degrades more than `--max-impact`%% (default 10) below vwap7/bid - a real, ticker-specific
measure, no regression, no confounding.
- Caveat, caught immediately when first wired as a hard `min()` cap alongside the flow-based
check: a live order book is a SINGLE SNAPSHOT - standing bids get added/withdrawn constantly,
same class of problem already found and fixed twice this session (`sell.py`'s queue model,
`parkcash.py`'s driftless walk), and we have no historical book-snapshot log to tell a
structurally-thin book from an unlucky instant. Using it as a hard cap let one noisy snapshot
silently zero out `n_lim` and hide a recipe entirely (happened to KV: flow-based n_lim 10.6 ->
0.0 purely from the snapshot). The 30-day flow figure (`tref_precise`) is the more reliable
signal precisely because it aggregates many real days, not one instant.
- Final design: `n_lim`/filtering stays flow-only (unchanged, reliable). `max_units_at_impact()`
surfaces as an `[exit-thin]` WARNING flag instead (can the live book absorb an instant dump of 1
day of 1 building's output right now, if cash were needed immediately?) - a panic-exit liquidity
check, NOT a production price-impact measure (that's `market`/`lim`, which price via patient
asks, the real selling mechanism) - visible, not silently filtering. Caught real signal on
several recipes (KV, SC, HER, TRN, NL, RG all flagged) without hiding any of them.
## 2026-10-10 Demolition value: flat reclaim fraction, not a time-decay curve
- `tools/scan.py --deprec` assumed building value decays LINEARLY to 0 over ~60 days (a guess,
never checked against real data). Checked it: pulled this account's live FIO reclaimables
(`/rain/userplanetbuildingreclaimables/{user}`) for all 8 owned building types, normalized by
built-count, and compared to real construction cost (FIO `BuildingCosts` at live ask prices).
- Result: reclaim fraction is 50-86% (CHP 68.7, EXT 86.5, HB1 58.3, HB2 50.9, HWP 73.1, POL 78.0,
SME 74.6; CM a low outlier at 7.4%, likely a core-module-specific rule) - but EVERY one of these
buildings is at ~99.7-99.9% live condition (this account's base is young). If reclaim really
decayed from ~100% toward 0 over 60 days, a near-perfect-condition building should reclaim close
to 100%, not 50-86%. This contradicts the decay-curve premise and is more consistent with a
roughly FLAT, material-dependent reclaim fraction that doesn't move much with condition in the
normal operating range.
- Can't fully settle the shape - no aged/low-condition building exists in this account to check
the other end of the curve, so this is suggestive, not conclusive (single base, narrow condition
range, small sample).
- Fix: `scan.py --deprec` now amortizes `capex * (1 - --reclaim-frac)` (the UNRECLAIMED portion)
over the holding period, not full capex. `--reclaim-frac` defaults to 0.65 (the mean of the 7
non-CM observations) - still a rough placeholder, not a calibrated curve; re-check once an aged
building exists to compare against.
## 2026-10-09 Drop "ROI/day %" terminology
- `profit/day ÷ new_capex` (what tools were calling "ROI/day X%") is just 1/payback_days rewritten as a percentage. It is NOT a compoundable rate of return - there's no reinvestment mechanism, and every opportunity here is capacity/market-limited (saturation), so it can't be extrapolated or annualized like an interest rate.
- Report **payback in days** as the primary number everywhere. A "%/day" figure, if shown at all, must be labelled as simple/non-compounding and never compared to a real yield/interest rate.