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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user