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>
14 KiB
Decisions log
2026-09-18 Project plan and working agreements
- Goal: agent-first toolkit; the player asks, the agent answers by running tools against live data. Not an end-user app.
- PRUNplanner source (
ref/) is the source of truth for mechanics; conflicts with the old handoff resolved in its favour (docs/mechanics.md). - Scope: whole universe, default region Antares (CX AI1). Python in
.venv, secrets in.env, git branchmaster. No commits unless the player asks. - Every reported opportunity must include market saturation (buildings the market can absorb, fill-price ROI, absorbable profit/day). Reason: PRUNplanner's ROI Overview ranks recipes with no depth awareness (e.g. 0.25 day ROI on recipes 2 buildings saturate).
- PRUNplanner integration: API key auth (
Authorization: Api-Key <key>); plans can be created/updated via/planning/plan/. Writes only create/update/delete plans prefixed[PuGa](enforced intools/plan_push.py); never touch the player's other plans. Deleting is allowed only on request (or for throwaway test plans the tool itself made). When the player explicitly asks for a plan, creating it directly is fine; show the dry run in the reply.
Model and review policy
- Default: Sonnet 5 does the building (API plumbing, CLIs, ports, tests).
- Bigger model (Agent tool
model: opusorfable, fresh context, brief with files and question) is allowed and expected at these points:- Saturation model design, before building it, and again on real output.
- Econ formula port (efficiency stack, workforce, hab LP, extraction) checked against PRUNplanner numbers.
plan_pushpayload handling and the own-plans-only guard.- Big-money recommendations (e.g. BHP go/no-go, roughly 100k AIC): independent second opinion on the numbers.
- 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 <= s0rateis 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 ksigmasqrt(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.58sigmasqrt(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 bytools/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) andpuga/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'sS_SHARE=0.25capped 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 aspuga.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 outn_limand 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'smarket/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 --deprecassumed 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 (FIOBuildingCostsat 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 --deprecnow amortizescapex * (1 - --reclaim-frac)(the UNRECLAIMED portion) over the holding period, not full capex.--reclaim-fracdefaults 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.
- Applies to
tools/whatif.py,tools/simulate.py,tools/scan.py,tools/persistence.py,tools/history.py, and anything built after this that reports a return on a one-time building cost.