Add tools/undervalued.py: trading strategy, goods priced below production cost

Finds manufactured goods currently selling below their implied
production cost (live input cost vs output price) - candidates to buy
and hold for a price-reversion bounce, the mirror of the "is this
worth building" question saturation.py already answers.

Backtested first (326 output tickers, 287 deduplicated dip-onset
episodes across 112 distinct tickers): +3mo forward return mean
+21.3%/median +8.9%/73.2% hit rate at >=10 units/day volume - real
signal, not noise, but only ~0.10-0.24%/day, far below the 14-29%/day
marginal ROI this session's production builds found. Documented
prominently in the tool's own docstring and CLAUDE.md so it doesn't
get mistaken for a primary strategy - it's for excess idle capital
once better-yielding builds are exhausted, not before.

Promoted daily_prices() out of tools/history.py into puga/history.py
so both tools share it, per the self-improve rule.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 10:28:34 +02:00
co-authored by Claude Sonnet 5
parent affc65cd67
commit c4178fe779
7 changed files with 133 additions and 9 deletions
+1
View File
@@ -50,6 +50,7 @@ Run as `puga <tool> ...` (e.g. `puga scan --min-n 3`, `puga plan pull <uuid>`);
- `tools/network.py <chain.yaml>` models a MULTI-BASE chain (e.g. mine LST at Nike, ship it, make BSE at Deimos): simulates each base, nets a transferred material's producer-surplus against consumer-need, charges real freight only on what's moved (`puga/network.py`). Spec and worked example in `plans/chains/`. `cm_free: true` per base for a new founding covered by a Core Module Kit. profit/day per base is the WHOLE plan, not the marginal addition - diff against the base's plan without the addition for a true marginal ROI. - `tools/network.py <chain.yaml>` models a MULTI-BASE chain (e.g. mine LST at Nike, ship it, make BSE at Deimos): simulates each base, nets a transferred material's producer-surplus against consumer-need, charges real freight only on what's moved (`puga/network.py`). Spec and worked example in `plans/chains/`. `cm_free: true` per base for a new founding covered by a Core Module Kit. profit/day per base is the WHOLE plan, not the marginal addition - diff against the base's plan without the addition for a true marginal ROI.
- `tools/whatif.py <before.yaml> [--add BUILDING:AMOUNT[:RECIPE]] [--infra HB1:+1] [--after after.yaml]` marginal ROI of adding to a base you already run (whole-plan diff, same rule as buy/runway - pass the WHOLE base, not just the addition). `--add` mutates a copy of `before` in-memory (reuses an existing building's recipe if omitted, e.g. `--add POL:1`); `--after` takes a second full spec instead. Flags any workforce tier the addition newly overloads (vs. a pre-existing one, which it won't flag) - this is what caught the PP2/pioneer-housing and 2nd-CHP/pioneer-housing surprises by hand before this tool existed. - `tools/whatif.py <before.yaml> [--add BUILDING:AMOUNT[:RECIPE]] [--infra HB1:+1] [--after after.yaml]` marginal ROI of adding to a base you already run (whole-plan diff, same rule as buy/runway - pass the WHOLE base, not just the addition). `--add` mutates a copy of `before` in-memory (reuses an existing building's recipe if omitted, e.g. `--add POL:1`); `--after` takes a second full spec instead. Flags any workforce tier the addition newly overloads (vs. a pre-existing one, which it won't flag) - this is what caught the PP2/pioneer-housing and 2nd-CHP/pioneer-housing surprises by hand before this tool existed.
- `tools/runway.py <spec.yaml|--uuid U> [--no-inbound]` days of runway per material before a base stalls: real NET consumption via `simulate()` (self-produced inputs excluded), base storage, PLUS (by default) any ship currently flying to that planet, auto-detected from live flight data (`tools/state.py` tags each ship with its flight origin/destination). Pass the WHOLE base plan (see `docs/library.md`). - `tools/runway.py <spec.yaml|--uuid U> [--no-inbound]` days of runway per material before a base stalls: real NET consumption via `simulate()` (self-produced inputs excluded), base storage, PLUS (by default) any ship currently flying to that planet, auto-detected from live flight data (`tools/state.py` tags each ship with its flight origin/destination). Pass the WHOLE base plan (see `docs/library.md`).
- `tools/undervalued.py [--cx AI1] [--min-vol 5] [--top 20]` manufactured goods currently priced below implied production cost - buy-and-hold-for-reversion candidates, not build candidates. **Expected ROI is much lower than production builds** (~0.10-0.24%/day backtested, vs 14-29%/day for the builds this toolkit recommends) - only worth it for excess idle capital once better-yielding builds are exhausted. Library: `puga/undervalued.py`.
- `tools/chain.py TICKER` make-vs-buy cost tree plus sourcing depth of inputs. `--consumers` flips it: what recipes EAT this ticker as an input, with each output's market depth - for "is there a deeper downstream product worth chaining into" questions (e.g. checking BOR/BSE/BBH for further processing). - `tools/chain.py TICKER` make-vs-buy cost tree plus sourcing depth of inputs. `--consumers` flips it: what recipes EAT this ticker as an input, with each output's market depth - for "is there a deeper downstream product worth chaining into" questions (e.g. checking BOR/BSE/BBH for further processing).
- `tools/planet.py RESOURCE [--min-factor X] [--from PLANET] [--top N]` ranks planets for a raw resource by extraction factor, with founding cost straight from FIO's own `BuildRequirements` (real environment-extras materials, not guessed temperature/pressure thresholds - PRUNplanner's own calculations don't model those either) and optional jump distance via `tools/route.py`. Pure-factor ranking alone is misleading - the best-factor planets are often thousands of ly away; always pass `--from` an existing base. Library: `puga/planet.py`. - `tools/planet.py RESOURCE [--min-factor X] [--from PLANET] [--top N]` ranks planets for a raw resource by extraction factor, with founding cost straight from FIO's own `BuildRequirements` (real environment-extras materials, not guessed temperature/pressure thresholds - PRUNplanner's own calculations don't model those either) and optional jump distance via `tools/route.py`. Pure-factor ranking alone is misleading - the best-factor planets are often thousands of ly away; always pass `--from` an existing base. Library: `puga/planet.py`.
- `tools/route.py <from planet/system> <to planet/system>` jump-distance route (not a PRUNplanner feature - built from FIO's `/systemstars` graph, Dijkstra by straight-line ly since that's what FTL fuel/time scale with, jump count reported alongside). Accepts planet natural IDs, system natural IDs, or names. Library: `puga/route.py`. - `tools/route.py <from planet/system> <to planet/system>` jump-distance route (not a PRUNplanner feature - built from FIO's `/systemstars` graph, Dijkstra by straight-line ly since that's what FTL fuel/time scale with, jump count reported alongside). Accepts planet natural IDs, system natural IDs, or names. Library: `puga/route.py`.
+5
View File
@@ -49,6 +49,11 @@ EOF
-> SystemId), `shortest_path(src, dst, graph=None)` (Dijkstra by straight-line ly over FIO's -> SystemId), `shortest_path(src, dst, graph=None)` (Dijkstra by straight-line ly over FIO's
`/systemstars` jump graph; returns jumps, distance_ly, path). Not a PRUNplanner feature, built `/systemstars` jump graph; returns jumps, distance_ly, path). Not a PRUNplanner feature, built
for this project since travel isn't modelled anywhere else. CLI: `tools/route.py`. for this project since travel isn't modelled anywhere else. CLI: `tools/route.py`.
- **`puga.undervalued`** — `manufacturing_candidates()`, `current_margin(ticker, recipe, cx)`,
`scan_current(cx, min_vol=5.0)`: manufactured goods priced below implied production cost, for
a buy-and-hold-for-reversion trade, not a build decision. Backtested expected return is
~0.10-0.24%/day - far below production's 14-29%/day, only worth excess idle capital. CLI:
`tools/undervalued.py`. Shares `puga.history.daily_prices` with `tools/history.py`.
- **`puga.planet`** — `candidates(resource_ticker, min_factor=0.0)`: every planet with that - **`puga.planet`** — `candidates(resource_ticker, min_factor=0.0)`: every planet with that
resource, sorted by extraction factor, each with temperature/pressure/gravity/fertility/faction resource, sorted by extraction factor, each with temperature/pressure/gravity/fertility/faction
and `build_requirements` (FIO's own founding-cost materials for that specific planet — real and `build_requirements` (FIO's own founding-cost materials for that specific planet — real
+1
View File
@@ -23,6 +23,7 @@ Legend: [ ] todo, [x] done. Build order matters; each step is usable on its own.
15. [x] `tools/runway.py`: days-of-runway per material from simulate()'s net consumption, base storage plus auto-detected inbound ship cargo (tools/state.py now tags each ship with its live flight origin/destination, and each STORE with its planet, needed once a 2nd base existed). 15. [x] `tools/runway.py`: days-of-runway per material from simulate()'s net consumption, base storage plus auto-detected inbound ship cargo (tools/state.py now tags each ship with its live flight origin/destination, and each STORE with its planet, needed once a 2nd base existed).
12. [ ] `tools/portfolio.py`: choose the set of 1-building opportunities for one base that maximises total profit given shared whole-building housing (HB1/HB2/...), capex, permits and (for demolish-later) 60-day value decay. Persistence check exists (`tools/persistence.py`); wire it into the ranking. 12. [ ] `tools/portfolio.py`: choose the set of 1-building opportunities for one base that maximises total profit given shared whole-building housing (HB1/HB2/...), capex, permits and (for demolish-later) 60-day value decay. Persistence check exists (`tools/persistence.py`); wire it into the ranking.
14. [x] `tools/undervalued.py`: trading (not building) strategy - manufactured goods priced below implied production cost, candidates for a buy-and-hold-for-reversion bet. Backtested 2026-10-09 (326 output tickers, 287 deduplicated dip-onset episodes, 112 distinct tickers): +3mo forward return mean +21.3%/median +8.9%/73.2% hit rate at >=10 units/day volume - roughly 0.10-0.24%/day, far below the 14-29%/day marginal ROI on production builds found this session. Only worth excess idle capital, not a primary strategy.
## Open questions ## Open questions
- PRUNplanner auth is known (`Authorization: Api-Key <key>`). FIO REST and FIO API are separate services with separate keys (separate services with separate keys; an earlier "one API" reading was wrong). Keys are in `.env`. Verified 2026-09-18: FIO_REST_KEY works on rest.fnar.net as `Authorization: <key>` (own /sites, /storage etc. readable); FIO_API_KEY gives 401 there, host unknown. PRUNplanner key works (plans 0, empires 1, cx prefs 1 on his account). Still unknown: the OpenAPI spec at https://doc.fnar.net/api.json has no securitySchemes. Probe with read-only calls. - PRUNplanner auth is known (`Authorization: Api-Key <key>`). FIO REST and FIO API are separate services with separate keys (separate services with separate keys; an earlier "one API" reading was wrong). Keys are in `.env`. Verified 2026-09-18: FIO_REST_KEY works on rest.fnar.net as `Authorization: <key>` (own /sites, /storage etc. readable); FIO_API_KEY gives 401 there, host unknown. PRUNplanner key works (plans 0, empires 1, cx prefs 1 on his account). Still unknown: the OpenAPI spec at https://doc.fnar.net/api.json has no securitySchemes. Probe with read-only calls.
+13
View File
@@ -0,0 +1,13 @@
"""Daily exchange price/volume series from FIO's cxpc candles, shared by tools/history.py and
tools/undervalued.py."""
import datetime
from . import fio
def daily_prices(tk: str, cx: str) -> dict:
"""date -> (VWAP price, units traded) for days with trades, from FIO cxpc DAY_ONE candles."""
out = {}
for e in fio.cxpc(tk, cx):
if e.get("Interval") == "DAY_ONE" and e.get("Traded"):
out[datetime.datetime.fromtimestamp(e["DateEpochMs"] / 1000, datetime.timezone.utc).date()] = (e["Volume"] / e["Traded"], e["Traded"])
return out
+70
View File
@@ -0,0 +1,70 @@
"""Finds manufactured goods currently selling below their implied production cost (output price
minus input cost, from live daily VWAP candles) - the mirror image of saturation.py's "is this
worth building" question: here we're asking "is this worth buying and holding for a price-
reversion bounce."
Backtested 2026-10-09 across 326 output tickers / 112 distinct tickers that ever showed the
signal, 287 independent dip-onset episodes (deduplicated so a persisting dip only counts once,
at onset, not once per month it continues): at >=10 units/day traded volume, +3 months forward
price return was mean +21.3%, median +8.9%, 73.2% of episodes positive (see empire/docs or the
session that built this for the raw numbers). That is ~0.10-0.24%/day - MUCH lower than this
project's production-building marginal ROI (14-29%/day seen this session). Only worth deploying
*excess* capital into once better-yielding builds are exhausted, not as a primary strategy, and
it's a gross-return backtest with no spread/freight/storage-fee/capital-lockup cost subtracted.
"""
from . import fio
from .history import daily_prices
def manufacturing_candidates():
"""One representative recipe per output ticker, excluding pure-extraction buildings (no
material inputs to compute an implied cost from)."""
seen = {}
for r in fio.recipes():
if r["BuildingTicker"] in ("EXT", "COL", "RIG") or not r["Inputs"]:
continue
for o in r["Outputs"]:
seen.setdefault(o["Ticker"], r)
return seen
def current_margin(ticker: str, recipe: dict, cx: str, window_days: int = 35, recent_days: int = 14):
"""Mean margin%/price/volume over the most recent `recent_days` of overlapping price history
for this recipe's inputs+output, within a `window_days` lookback. None if not enough history."""
ins = {i["Ticker"]: i["Amount"] for i in recipe["Inputs"]}
outs = {o["Ticker"]: o["Amount"] for o in recipe["Outputs"]}
series = {m: daily_prices(m, cx) for m in list(ins) + list(outs)}
if any(not s for s in series.values()):
return None
days = sorted(set().union(*[set(s) for s in series.values()]))[-window_days:]
last = {}
rows = []
for d in days:
for m, s in series.items():
if d in s:
last[m] = s[d][0]
if len(last) == len(series):
rev = sum(last[m] * n for m, n in outs.items())
cost = sum(last[m] * n for m, n in ins.items())
rows.append((rev, cost, last[ticker], series[ticker].get(d, (0, 0))[1]))
recent = rows[-recent_days:]
if len(recent) < recent_days // 2:
return None
rev_m = sum(x[0] for x in recent) / len(recent)
cost_m = sum(x[1] for x in recent) / len(recent)
px_m = sum(x[2] for x in recent) / len(recent)
vol_m = sum(x[3] for x in recent) / len(recent)
margin_pct = 100 * (rev_m - cost_m) / cost_m if cost_m else 0.0
return {"margin_pct": margin_pct, "price": px_m, "volume": vol_m, "building": recipe["BuildingTicker"]}
def scan_current(cx: str, min_vol: float = 5.0):
"""Every manufactured good currently priced below its implied production cost, with enough
volume to actually trade, sorted most-undervalued first."""
out = []
for t, r in manufacturing_candidates().items():
m = current_margin(t, r, cx)
if m and m["margin_pct"] < 0 and m["volume"] >= min_vol:
out.append({"ticker": t, **m})
out.sort(key=lambda x: x["margin_pct"])
return out
+2 -9
View File
@@ -4,18 +4,11 @@ Daily price = value traded / units traded (VWAP), forward-filled over days witho
tools/history.py KV # AI1, the recipe producing KV tools/history.py KV # AI1, the recipe producing KV
tools/history.py BHP --cx AI1 --eff 1.18 --overhead 3200 --capex 94000 --months 12 tools/history.py BHP --cx AI1 --eff 1.18 --overhead 3200 --capex 94000 --months 12
Building/day = batches/day at --eff; profit/day = batches/day * margin_per_batch - overhead (wages etc, --overhead).""" Building/day = batches/day at --eff; profit/day = batches/day * margin_per_batch - overhead (wages etc, --overhead)."""
import argparse, datetime, statistics, sys import argparse, statistics, sys
from pathlib import Path from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from puga import config, fio from puga import config, fio
from puga.history import daily_prices
def daily_prices(tk, cx):
out = {}
for e in fio.cxpc(tk, cx):
if e.get("Interval") == "DAY_ONE" and e.get("Traded"):
out[datetime.datetime.fromtimestamp(e["DateEpochMs"] / 1000, datetime.timezone.utc).date()] = (e["Volume"] / e["Traded"], e["Traded"])
return out
def main(): def main():
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env python3
"""Manufactured goods currently selling below their implied production cost (output price minus
live input cost) - candidates to buy and hold for a price-reversion bounce, not to build.
EXPECTED ROI IS MUCH LOWER THAN THIS PROJECT'S PRODUCTION BUILDS. Backtest (2026-10-09, 326
output tickers, 287 independent dip-onset episodes across 112 distinct tickers, >=10 units/day
volume): +3 months forward price return mean +21.3%, median +8.9%, 73.2% of episodes positive.
That's ~0.10-0.24%/day, or very roughly ~41%/year if you could always immediately redeploy into
a new dip (optimistic) - versus 14-29%/day marginal ROI on the production buildings this toolkit
has been recommending. Gross price return only, no spread/freight/storage-fee/capital-lockup cost
subtracted. Only deploy EXCESS capital here once better-yielding builds are exhausted - this is
not a primary strategy.
tools/undervalued.py # AI1, min 5 units/day traded
tools/undervalued.py --cx AI1 --min-vol 10 --top 10
"""
import argparse, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from puga import config, undervalued
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--cx", default=config.DEFAULT_CX)
ap.add_argument("--min-vol", type=float, default=5.0, help="min units/day traded (noise filter, thin markets won't revert reliably)")
ap.add_argument("--top", type=int, default=20)
a = ap.parse_args()
rows = undervalued.scan_current(a.cx, a.min_vol)
if not rows:
print(f"nothing currently priced below production cost at {a.cx} with volume >= {a.min_vol}/day")
return
print(f"# {a.cx} min-vol>={a.min_vol}/day (gross price return only - see docstring for backtest caveats)")
print(f"{'ticker':6} {'margin%':>8} {'price':>10} {'vol/d':>7} bld")
for r in rows[:a.top]:
print(f"{r['ticker']:6} {r['margin_pct']:8.1f} {r['price']:10.1f} {r['volume']:7.1f} {r['building']}")
if __name__ == "__main__":
main()