Charge real freight in network.py/whatif.py; drop "ROI/day %" terminology
network.py: tools/network.py now computes freight cost from the real fuel model (puga.shipping.round_trip_cost) for direct-jump transfers instead of the flat 9250 AIC placeholder; gateway transfers or an explicit trip_cost in the chain YAML still use that as a fallback. puga/network.py's combine() docstring now flags that its "flows cancel, only freight matters" claim only holds under a symmetric-price basis (uni30, the default) - under --basis real it understates the benefit of physical transfer by the bid-ask spread on the moved quantity. whatif.py: now charges marginal freight too - only the EXTRA round trips an addition needs beyond the base's existing traffic, so a small addition riding along on already-necessary hauls costs ~0, while one that adds meaningfully to shipped tonnage doesn't get a free pass. Terminology: "profit/day ÷ capex" was being reported as "ROI/day X%", which invites reading it as a compoundable rate. It's just 1/payback_days - there's no reinvestment mechanism and every opportunity here is capacity/market-limited, so it can't be annualized like a real yield. Payback in days is now the primary number everywhere (scan.py, simulate.py, network.py, whatif.py); see docs/decisions.md for the reasoning. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,3 +16,8 @@
|
|||||||
4. Big-money recommendations (e.g. BHP go/no-go, roughly 100k AIC): independent second opinion on the numbers.
|
4. 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.
|
- 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.
|
- Routine tool building is not reviewed by a bigger model.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
# Multi-base chain: Nike mines LST, ships it to Deimos, Deimos turns it (+ market AL) into BSE.
|
# Multi-base chain: Nike mines LST, ships it to Deimos, Deimos turns it (+ market AL) into BSE.
|
||||||
# trip_cost/cargo are placeholders (see tools/network.py) until a real fuel model exists.
|
# trip_cost omitted below: tools/network.py computes it from the real fuel model (puga.shipping)
|
||||||
|
# since Nike<->Deimos is a direct-jump route. cargo is still a placeholder (ship hold size).
|
||||||
name: "Nike LST -> Deimos PP2 (BSE)"
|
name: "Nike LST -> Deimos PP2 (BSE)"
|
||||||
bases:
|
bases:
|
||||||
nike: {plan: plans/examples/nike_lst_ext.yaml, cm_free: true} # founded via the faction expansion contract's kit
|
nike: {plan: plans/examples/nike_lst_ext.yaml, cm_free: true} # founded via the faction expansion contract's kit
|
||||||
deimos: plans/examples/deimos_pp2_bse.yaml
|
deimos: plans/examples/deimos_pp2_bse.yaml
|
||||||
transfers:
|
transfers:
|
||||||
- {material: LST, src: nike, dst: deimos, trip_cost: 9250, cargo: 500}
|
- {material: LST, src: nike, dst: deimos, cargo: 500}
|
||||||
|
|||||||
+12
-1
@@ -8,6 +8,15 @@ the transferred quantity (base A "sells" it, base B "buys" it, at the same price
|
|||||||
combined total needs only ONE correction: the real freight cost of physically moving it, which
|
combined total needs only ONE correction: the real freight cost of physically moving it, which
|
||||||
isn't in either base's isolated numbers. Leftover surplus (more produced than a partner needs) is
|
isn't in either base's isolated numbers. Leftover surplus (more produced than a partner needs) is
|
||||||
still validly sold at market by the producing base; leftover deficit is still validly bought.
|
still validly sold at market by the producing base; leftover deficit is still validly bought.
|
||||||
|
|
||||||
|
CAVEAT: the "cancel out" claim only holds when the price basis prices buy and sell sides of a
|
||||||
|
ticker identically (true for `uni30`, the default in tools/network.py). Under `--basis real`
|
||||||
|
(or any basis with a genuine bid/ask spread), each base's own `simulate()` correctly books its
|
||||||
|
flows at the real sell (bid/vwap) or buy (ask) price - but for the `moved` quantity, that's not
|
||||||
|
what actually happens: it moves at no transaction price at all, not at both a real sale AND a
|
||||||
|
real purchase. Summing the two bases' independent numbers and subtracting only freight therefore
|
||||||
|
UNDERSTATES the true combined profit by the bid-ask spread on the moved quantity - a real gap,
|
||||||
|
not symmetric cancellation, under any basis with a spread. Not corrected here.
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from . import fio
|
from . import fio
|
||||||
@@ -33,7 +42,9 @@ class Transfer:
|
|||||||
material: str
|
material: str
|
||||||
src: str # producing base's name (key in the `bases` dict)
|
src: str # producing base's name (key in the `bases` dict)
|
||||||
dst: str # consuming base's name
|
dst: str # consuming base's name
|
||||||
trip_cost: float = 9250.0 # AIC per round trip; PLACEHOLDER until a real route/fuel model exists
|
trip_cost: float = 9250.0 # AIC per round trip; tools/network.py fills this from puga.shipping.round_trip_cost()
|
||||||
|
# when src/dst are a direct-jump route and no explicit value is given in the chain
|
||||||
|
# YAML - this default is the fallback for gateway routes/transfers without one
|
||||||
cargo: float = 500.0 # t or m3 per trip
|
cargo: float = 500.0 # t or m3 per trip
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+29
-9
@@ -9,7 +9,7 @@ import argparse, sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import yaml
|
import yaml
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
from puga import ROOT, config, market, prunplanner as pp
|
from puga import ROOT, config, market, prunplanner as pp, shipping
|
||||||
from puga.network import BaseNode, Transfer, combine
|
from puga.network import BaseNode, Transfer, combine
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
@@ -56,18 +56,35 @@ def main():
|
|||||||
built["CM"] = 1
|
built["CM"] = 1
|
||||||
bases[key] = BaseNode(key, plan, recipes, blds, planet["resources"], planet["fertility"], price, faction, perm, built)
|
bases[key] = BaseNode(key, plan, recipes, blds, planet["resources"], planet["fertility"], price, faction, perm, built)
|
||||||
|
|
||||||
transfers = [Transfer(**t) for t in net.get("transfers", [])]
|
sf_px = (snap.get(("SF", a.cx)) or market.Quote("SF", a.cx)).ask or 0
|
||||||
|
ff_px = (snap.get(("FF", a.cx)) or market.Quote("FF", a.cx)).ask or 0
|
||||||
|
transfers, cost_notes = [], []
|
||||||
|
for raw in net.get("transfers", []):
|
||||||
|
t = dict(raw)
|
||||||
|
if "trip_cost" not in t and t["src"] in bases and t["dst"] in bases:
|
||||||
|
real = shipping.round_trip_cost(bases[t["src"]].plan["planet_natural_id"], bases[t["dst"]].plan["planet_natural_id"],
|
||||||
|
sf_px, ff_px)
|
||||||
|
if real is not None:
|
||||||
|
t["trip_cost"] = real
|
||||||
|
cost_notes.append(f" {t['material']} {t['src']}->{t['dst']}: real round-trip cost {real:,.0f} AIC (puga.shipping)")
|
||||||
|
else:
|
||||||
|
cost_notes.append(f" {t['material']} {t['src']}->{t['dst']}: no direct-jump route (gateway?) - using placeholder {Transfer.__dataclass_fields__['trip_cost'].default:,.0f} AIC, check SFC in-game")
|
||||||
|
transfers.append(Transfer(**t))
|
||||||
r = combine(bases, transfers)
|
r = combine(bases, transfers)
|
||||||
|
|
||||||
print(f"{net.get('name', a.spec)} ({a.basis} prices)\n")
|
print(f"{net.get('name', a.spec)} ({a.basis} prices)\n")
|
||||||
for key, res in r["results"].items():
|
for key, res in r["results"].items():
|
||||||
line = f"[{key}] area {res['area']:.0f} profit/day {res['profit']:,.0f} new capex {res['new_capex']:,.0f}"
|
line = f"[{key}] area {res['area']:.0f} profit/day {res['profit']:,.0f} new capex {res['new_capex']:,.0f}"
|
||||||
if res["new_capex"] > 0:
|
if res["new_capex"] > 0:
|
||||||
line += f" ({100*res['profit']/res['new_capex']:.1f}%/day)"
|
line += f" (payback {res['new_capex']/res['profit']:.1f} d)"
|
||||||
print(line)
|
print(line)
|
||||||
for b in res["buildings"]:
|
for b in res["buildings"]:
|
||||||
print(f" {b['amount']:2} x {b['building']:4} eff {b['efficiency']*100:6.1f}%")
|
print(f" {b['amount']:2} x {b['building']:4} eff {b['efficiency']*100:6.1f}%")
|
||||||
|
|
||||||
|
if cost_notes:
|
||||||
|
print("\nFREIGHT COST")
|
||||||
|
print("\n".join(cost_notes))
|
||||||
|
|
||||||
print("\nTRANSFERS")
|
print("\nTRANSFERS")
|
||||||
for t in r["ledger"]:
|
for t in r["ledger"]:
|
||||||
note = f"SHORTFALL {t['shortfall']:.1f}/d bought at market by {t['dst']}" if t["shortfall"] > 0.01 else \
|
note = f"SHORTFALL {t['shortfall']:.1f}/d bought at market by {t['dst']}" if t["shortfall"] > 0.01 else \
|
||||||
@@ -77,13 +94,16 @@ def main():
|
|||||||
|
|
||||||
total_new_capex = sum(res["new_capex"] for res in r["results"].values())
|
total_new_capex = sum(res["new_capex"] for res in r["results"].values())
|
||||||
print(f"\nCOMBINED profit/day {r['total_profit']:,.0f} new capex {total_new_capex:,.0f}"
|
print(f"\nCOMBINED profit/day {r['total_profit']:,.0f} new capex {total_new_capex:,.0f}"
|
||||||
+ (f" {100*r['total_profit']/total_new_capex:.1f}%/day" if total_new_capex > 0 else ""))
|
+ (f" payback {total_new_capex/r['total_profit']:.1f} d" if total_new_capex > 0 else ""))
|
||||||
print("CAUTION: a base's 'profit/day' here is its WHOLE plan (existing buildings included), not just the new "
|
print("CAUTION: a base's 'profit/day' here is its WHOLE plan (existing buildings included), not just the new "
|
||||||
"addition, so its %/day overstates the marginal return where new_capex is small relative to an existing "
|
"addition, so its payback here understates the marginal payback where new_capex is small relative to an "
|
||||||
"base. For the true marginal ROI, also simulate the base's plan WITHOUT the new building/transfer and "
|
"existing base. For the true marginal payback, also simulate the base's plan WITHOUT the new "
|
||||||
"diff the profit; see docs/library.md.")
|
"building/transfer and diff the profit; see docs/library.md.")
|
||||||
print("NOTE: trip_cost/cargo are placeholders (default 9250 AIC, 500 t/m3, same as the AI1 route) until a real "
|
print("NOTE: trip_cost is computed from the real fuel model (puga.shipping, max sliders) for direct-jump routes "
|
||||||
"inter-base fuel model exists (docs/roadmap.md tools/route.py) -- check SFC in-game for the real cost.")
|
"between two bases in this chain (see FREIGHT COST above); a gateway route, or a transfer with an "
|
||||||
|
"explicit trip_cost in the YAML, uses that value instead (9250 AIC if neither applies). cargo defaults "
|
||||||
|
"to 500 t/m3 (the account's ship hold size, confirmed via FIO) - override per-transfer if a different "
|
||||||
|
"ship is used.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+9
-7
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Depth-aware single-step recipe scan at one exchange (saturation model v1, see docs/saturation-design.md).
|
"""Depth-aware single-step recipe scan at one exchange (saturation model v1, see docs/saturation-design.md).
|
||||||
For each recipe: buildings the market can absorb (N*), and ROI/day at N=1 and N* using a patient sell price
|
For each recipe: buildings the market can absorb (N*), and payback/yield at N=1 and N* using a patient sell
|
||||||
(not top-of-book) and ask-walked inputs. Excludes thin markets by default.
|
price (not top-of-book) and ask-walked inputs. Excludes thin markets by default. yld/d is profit/day over
|
||||||
|
capex as a %, NOT a compoundable rate - it's 1/payback; see docs/decisions.md's "Drop ROI/day %" entry.
|
||||||
|
|
||||||
tools/scan.py # AI1, all tiers, top 30 by ROI at N*
|
tools/scan.py # AI1, all tiers, top 30 by yield at N*
|
||||||
tools/scan.py --cogc METALLURGY --tier S # e.g. a Metallurgy-COGC base, up to settlers
|
tools/scan.py --cogc METALLURGY --tier S # e.g. a Metallurgy-COGC base, up to settlers
|
||||||
tools/scan.py --sort total --min-n 3 # rank by absorbable profit/day
|
tools/scan.py --sort total --min-n 3 # rank by absorbable profit/day
|
||||||
"""
|
"""
|
||||||
@@ -35,7 +36,7 @@ def main():
|
|||||||
ap.add_argument("--deprec", type=float, default=0, help="demolish-later mode: building value decays linearly to 0 over this many days (game: ~60); subtracts capex/deprec per day")
|
ap.add_argument("--deprec", type=float, default=0, help="demolish-later mode: building value decays linearly to 0 over this many days (game: ~60); subtracts capex/deprec per day")
|
||||||
ap.add_argument("--input", help="only recipes consuming this ticker as an input, e.g. --input AL for recipes built on top of your own AL chain")
|
ap.add_argument("--input", help="only recipes consuming this ticker as an input, e.g. --input AL for recipes built on top of your own AL chain")
|
||||||
ap.add_argument("--planet", help="planet natural id: adds extraction (EXT/COL/RIG) from its resources, uses its fertility and active COGC; new base (not in state) => no HQ, permits+1")
|
ap.add_argument("--planet", help="planet natural id: adds extraction (EXT/COL/RIG) from its resources, uses its fertility and active COGC; new base (not in state) => no HQ, permits+1")
|
||||||
ap.add_argument("--own", type=int, default=1, help="how many buildings WE would run; ROI is measured at this size (default 1). --min-n is only a market-size filter")
|
ap.add_argument("--own", type=int, default=1, help="how many buildings WE would run; yield/payback is measured at this size (default 1). --min-n is only a market-size filter")
|
||||||
ap.add_argument("--skip", default="", help="tiers left unstaffed, e.g. technician (no housing/wages; efficiency = staffed headcount share)")
|
ap.add_argument("--skip", default="", help="tiers left unstaffed, e.g. technician (no housing/wages; efficiency = staffed headcount share)")
|
||||||
ap.add_argument("--experts", default="", help="expert counts, e.g. METALLURGY=2 (bonus x1.0306..1.284 for 1..5)")
|
ap.add_argument("--experts", default="", help="expert counts, e.g. METALLURGY=2 (bonus x1.0306..1.284 for 1..5)")
|
||||||
ap.add_argument("--hq", action="store_true", help="corp HQ bonus x1.1")
|
ap.add_argument("--hq", action="store_true", help="corp HQ bonus x1.1")
|
||||||
@@ -256,10 +257,11 @@ def main():
|
|||||||
import json
|
import json
|
||||||
Path(a.json).write_text(json.dumps(rows))
|
Path(a.json).write_text(json.dumps(rows))
|
||||||
print(f"# {a.cx} tier<={a.tier or 'any'} cogc={a.cogc or '-'} faction={faction or '-'} ({pu}/{pt} permits) min-n>={a.min_n} sorted by {a.sort}")
|
print(f"# {a.cx} tier<={a.tier or 'any'} cogc={a.cogc or '-'} faction={faction or '-'} ({pu}/{pt} permits) min-n>={a.min_n} sorted by {a.sort}")
|
||||||
print("# ROI/day is for OUR size (--own buildings, default 1) incl. our own price impact; market = buildings the market could absorb (--min-n filters on it, nothing else). Net of freight. Patient asks near VWAP, inputs ask-walked; all estimates")
|
print("# yld/d is profit/day / capex as a %, for OUR size (--own buildings, default 1) incl. our own price impact; this is NOT a compoundable rate (1/payback, see docs/decisions.md) - pback is the number to actually reason about. market = buildings the market could absorb (--min-n filters on it, nothing else). Net of freight. Patient asks near VWAP, inputs ask-walked; all estimates")
|
||||||
print(f"{'ROI/d':>6} {'own':>3} {'market':>6} {'lim':4} {'profit/d':>11} {'capex':>9} {'eff':>4} {'bld':4} {'staff':5} {'t/d':>5} {'frt/d':>6} {'h':>5} recipe")
|
print(f"{'yld/d':>6} {'pback':>6} {'own':>3} {'market':>6} {'lim':4} {'profit/d':>11} {'capex':>9} {'eff':>4} {'bld':4} {'staff':5} {'t/d':>5} {'frt/d':>6} {'h':>5} recipe")
|
||||||
for r in rows[:a.top]:
|
for r in rows[:a.top]:
|
||||||
print(f"{r['roiN']:6.1f} {r['n']:3d} {r['n_lim']:6.1f} {r['lim']:4} {r['total']:11.0f} {r['capex']:9.0f} {r['eff']*100:4.0f} {r['bld']:4} {r['staff']:5} {r['tons']:5.0f} {r['frt']:6.0f} "
|
pback = f"{100/r['roiN']:6.1f}" if r['roiN'] > 0 else " inf"
|
||||||
|
print(f"{r['roiN']:6.1f} {pback} {r['n']:3d} {r['n_lim']:6.1f} {r['lim']:4} {r['total']:11.0f} {r['capex']:9.0f} {r['eff']*100:4.0f} {r['bld']:4} {r['staff']:5} {r['tons']:5.0f} {r['frt']:6.0f} "
|
||||||
f"{r['h']:5.1f} {r['rec']}{' [thin]' if r['thin'] else ''}")
|
f"{r['h']:5.1f} {r['rec']}{' [thin]' if r['thin'] else ''}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -70,7 +70,7 @@ def main():
|
|||||||
print(f"{plan['plan_name']} {plan['planet_natural_id']} COGC {plan.get('plan_cogc')} prices: {a.cx} {a.basis}")
|
print(f"{plan['plan_name']} {plan['planet_natural_id']} COGC {plan.get('plan_cogc')} prices: {a.cx} {a.basis}")
|
||||||
print(f"Area {r['area']:.0f}/500 Profit/day {r['profit']:,.0f} gross {r['gross']:,.0f} degradation {r['degradation']:,.0f} plan cost {r['plan_cost']:,.0f} ROI {r['roi_days'] and round(r['roi_days'], 2)} d")
|
print(f"Area {r['area']:.0f}/500 Profit/day {r['profit']:,.0f} gross {r['gross']:,.0f} degradation {r['degradation']:,.0f} plan cost {r['plan_cost']:,.0f} ROI {r['roi_days'] and round(r['roi_days'], 2)} d")
|
||||||
nc = r["new_capex"]
|
nc = r["new_capex"]
|
||||||
print(f"NEW CAPEX = planned minus already built ({built or 'nothing built'}): {nc:,.0f} -> payback {nc / r['profit']:.2f} d = {100 * r['profit'] / nc:.1f}%/day" if r["profit"] > 0 and nc > 0 else ("NEW CAPEX: nothing to build" if nc <= 0 else "NEW CAPEX: profit <= 0"))
|
print(f"NEW CAPEX = planned minus already built ({built or 'nothing built'}): {nc:,.0f} -> payback {nc / r['profit']:.2f} d" if r["profit"] > 0 and nc > 0 else ("NEW CAPEX: nothing to build" if nc <= 0 else "NEW CAPEX: profit <= 0"))
|
||||||
print("\nWORKFORCE need supply open eff%")
|
print("\nWORKFORCE need supply open eff%")
|
||||||
for t, w in r["workforce"].items():
|
for t, w in r["workforce"].items():
|
||||||
if w["need"] or w["supply"]:
|
if w["need"] or w["supply"]:
|
||||||
|
|||||||
+44
-3
@@ -14,7 +14,7 @@ from collections import Counter
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import yaml
|
import yaml
|
||||||
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, market, prunplanner as pp
|
from puga import config, contracts, fio, market, prunplanner as pp, shipping
|
||||||
from puga.simulate import simulate
|
from puga.simulate import simulate
|
||||||
|
|
||||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||||
@@ -88,6 +88,40 @@ def reclaim_value(ticker: str, qty: int, built_count: int, cx: str):
|
|||||||
return {"value": value, "items": items}
|
return {"value": value, "items": items}
|
||||||
|
|
||||||
|
|
||||||
|
def trip_tonnage(flows: dict, mats: dict):
|
||||||
|
"""Total t/m3 the base needs to haul IN (net importers) and ship OUT (net exporters) per day -
|
||||||
|
same accounting scan.py already uses for its own trip-cost estimate (max of the four, since
|
||||||
|
imports and exports share the same round trips)."""
|
||||||
|
w_in = w_out = v_in = v_out = 0.0
|
||||||
|
for t, f in flows.items():
|
||||||
|
net = f["inp"] - f["out"]
|
||||||
|
m = mats.get(t)
|
||||||
|
if not m or abs(net) < 1e-6:
|
||||||
|
continue
|
||||||
|
if net > 0:
|
||||||
|
w_in += net * m["Weight"]; v_in += net * m["Volume"]
|
||||||
|
else:
|
||||||
|
w_out += -net * m["Weight"]; v_out += -net * m["Volume"]
|
||||||
|
return w_in, w_out, v_in, v_out
|
||||||
|
|
||||||
|
|
||||||
|
def marginal_freight(r_b, r_a, planet_id, cx, sf_price, ff_price, mats):
|
||||||
|
"""AIC/day freight for the EXTRA trips this addition requires - not the base's whole freight
|
||||||
|
bill. If the addition's materials fit in slack the base's existing traffic already has (the
|
||||||
|
common case for a small addition), this comes out to ~0: real freight cost only kicks in once
|
||||||
|
net t/m3 crosses into needing another round trip. Returns (freight_aic_per_day, note)."""
|
||||||
|
origin = shipping.cx_system(cx)
|
||||||
|
if origin is None:
|
||||||
|
return 0.0, f"no station found for CX {cx!r} - freight not modelled"
|
||||||
|
rt = shipping.round_trip_cost(origin, planet_id, sf_price, ff_price)
|
||||||
|
if rt is None:
|
||||||
|
return 0.0, "no priceable route (gateway without a measured LOCK fee?) - freight not modelled"
|
||||||
|
trips_b = max(trip_tonnage(r_b["flows"], mats)) / contracts.SHIP_CARGO
|
||||||
|
trips_a = max(trip_tonnage(r_a["flows"], mats)) / contracts.SHIP_CARGO
|
||||||
|
extra_trips = trips_a - trips_b
|
||||||
|
return extra_trips * rt, f"{extra_trips:+.3f} extra round trips/day x {rt:,.0f} AIC/trip"
|
||||||
|
|
||||||
|
|
||||||
def run(spec, recipes, blds, price, faction, perm, built):
|
def run(spec, recipes, blds, price, faction, perm, built):
|
||||||
plan = plan_push.build_payload(spec, recipes, {b["building_ticker"] for b in blds})
|
plan = plan_push.build_payload(spec, recipes, {b["building_ticker"] for b in blds})
|
||||||
planet = pp._g(f"/data/planet/{plan['planet_natural_id']}/", 3600)
|
planet = pp._g(f"/data/planet/{plan['planet_natural_id']}/", 3600)
|
||||||
@@ -151,6 +185,12 @@ def main():
|
|||||||
d_profit = r_a["profit"] - r_b["profit"]
|
d_profit = r_a["profit"] - r_b["profit"]
|
||||||
d_capex = r_a["new_capex"] - r_b["new_capex"]
|
d_capex = r_a["new_capex"] - r_b["new_capex"]
|
||||||
|
|
||||||
|
mats = {m["Ticker"]: m for m in fio.materials()}
|
||||||
|
sf_px = (snap.get(("SF", a.cx)) or market.Quote("SF", a.cx)).ask or 0
|
||||||
|
ff_px = (snap.get(("FF", a.cx)) or market.Quote("FF", a.cx)).ask or 0
|
||||||
|
freight, freight_note = marginal_freight(r_b, r_a, planet_id, a.cx, sf_px, ff_px, mats)
|
||||||
|
d_profit -= freight
|
||||||
|
|
||||||
before_counts = Counter(b["building"] for b in before_spec.get("buildings", []) for _ in range(b["amount"]))
|
before_counts = Counter(b["building"] for b in before_spec.get("buildings", []) for _ in range(b["amount"]))
|
||||||
after_counts = Counter(b["building"] for b in after_spec.get("buildings", []) for _ in range(b["amount"]))
|
after_counts = Counter(b["building"] for b in after_spec.get("buildings", []) for _ in range(b["amount"]))
|
||||||
reclaim_total = 0.0
|
reclaim_total = 0.0
|
||||||
@@ -166,8 +206,9 @@ def main():
|
|||||||
|
|
||||||
print(f"BEFORE profit/day {r_b['profit']:,.0f} new_capex {r_b['new_capex']:,.0f}")
|
print(f"BEFORE profit/day {r_b['profit']:,.0f} new_capex {r_b['new_capex']:,.0f}")
|
||||||
print(f"AFTER profit/day {r_a['profit']:,.0f} new_capex {r_a['new_capex']:,.0f}")
|
print(f"AFTER profit/day {r_a['profit']:,.0f} new_capex {r_a['new_capex']:,.0f}")
|
||||||
|
print(f"FREIGHT {freight:+,.0f} AIC/day marginal ({freight_note})")
|
||||||
if reclaim_lines:
|
if reclaim_lines:
|
||||||
print(f"\nRECLAIMED MATERIALS (demolishing what's removed, sell value, real FIO data):")
|
print("\nRECLAIMED MATERIALS (demolishing what's removed, sell value, real FIO data):")
|
||||||
for line in reclaim_lines:
|
for line in reclaim_lines:
|
||||||
print(line)
|
print(line)
|
||||||
print(f" total: {reclaim_total:,.0f} AIC")
|
print(f" total: {reclaim_total:,.0f} AIC")
|
||||||
@@ -175,7 +216,7 @@ def main():
|
|||||||
net_label = f" net cash GAINED {-net_capex:,.0f}" if net_capex < 0 and reclaim_total else (f" net new capex {net_capex:,.0f}" if reclaim_total else "")
|
net_label = f" net cash GAINED {-net_capex:,.0f}" if net_capex < 0 and reclaim_total else (f" net new capex {net_capex:,.0f}" if reclaim_total else "")
|
||||||
print(f"\nMARGINAL profit/day {d_profit:,.0f} new_capex {d_capex:,.0f}" + net_label, end="")
|
print(f"\nMARGINAL profit/day {d_profit:,.0f} new_capex {d_capex:,.0f}" + net_label, end="")
|
||||||
if net_capex > 0 and d_profit > 0:
|
if net_capex > 0 and d_profit > 0:
|
||||||
print(f" ROI/day {100*d_profit/net_capex:.2f}% payback {net_capex/d_profit:.1f} d")
|
print(f" payback {net_capex/d_profit:.1f} d (simple, non-compounding - a one-time capex paying itself off; not a reinvestable rate, see docs/decisions.md)")
|
||||||
elif net_capex <= 0 and d_profit > 0:
|
elif net_capex <= 0 and d_profit > 0:
|
||||||
print(" (no net new capex after reclaim)" if reclaim_total else " (no new capex)")
|
print(" (no net new capex after reclaim)" if reclaim_total else " (no new capex)")
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user