grand-exchange-live: the stepped market engine, and the turn budget binds
Arena's first interactive environment, as its own wheel beside grand-exchange so the one-shot scores stay comparable rather than conflated. One basket of orders becomes a loop: quote, see fills and the next tick, re-quote, with inventory, open orders and cash carrying between turns. The engine and its measurement only. No taskset, no config, no probe row — the spec's constants were derived from a throwaway engine and did not reconcile (impatient computed to 0.612 against a reported 0.623), so every constant here is chosen from a ladder re-measured in the code that ships. House rule 4 asks whether the turn budget binds. It does: exhaustive 0.631 vs oracle 1.000, gap 0.369, stdev 0.054 over five blocks, worst block 0.316, and 0.354-0.405 at four further seed bases. That is 18x the proposed margin and it clears the 0.25 quantum of the binary gate outright, so the comparison is not a cliff. The saturation worry does not fire: per-basket clipping is not clipping the mean, so exhaustive averages 0.849 on profit_ratio rather than 1.000. The first draft's gap was FAKE — a clean-looking 0.152 produced by an under-tuned denominator whose every-look sibling earned 22% more gp while scoring below it. It was caught only by sweeping the reference's own family. Nothing that scores below the oracle may earn more than it; that check belongs in every interactive environment that follows this one. freeze is derived from market.seed inside build_market so viability and scoring see one value, roc is on peak rather than average capital employed, and the reference is cached per seed so scoring never re-simulates the played episode. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,540 @@
|
||||
"""Measure the policy ladder in the SHIPPED engine, before any constant is locked.
|
||||
|
||||
The spec this environment came from locked `horizon`, `FREEZE_RANGE`, `TARGET_SHARE` and
|
||||
`TURNS` against arithmetic from a throwaway engine that no longer exists, and its ladder did
|
||||
not reconcile with its own reward — impatient computes to 0.612 where 0.623 was reported.
|
||||
Nothing here is taken from that document. Every row below is produced by `live.play`, the
|
||||
engine the taskset will actually run, over the baskets a run will actually serve.
|
||||
|
||||
Two questions this file exists to answer, in this order:
|
||||
|
||||
1. What does each policy score? Nine of them, from doing nothing to the reference itself,
|
||||
each differing from its neighbour by one judgement.
|
||||
2. **Does the turn budget bind?** Every reward in Arena saturates — `profit_ratio` and
|
||||
`efficiency` clip at 1.0 and `clean` is a boolean band — so an exhaustive policy that
|
||||
clears the band scores 1.000 and house rule 4 is structurally unreachable. That is
|
||||
risk #2 in the programme's register, it is answered as a NUMBER, and if the number is
|
||||
zero that is a finding about the reward shape rather than a failure of this lane.
|
||||
|
||||
Read block-wise, never as one mean: realised P&L over sixty ticks is the noisiest thing in
|
||||
the repository, and `probe.py` already documents this generator swinging 0.48 against 0.18
|
||||
between blocks of twenty-four. A ladder read off one block is a ladder read off the seed.
|
||||
|
||||
uv run --no-project python measure_ladder.py
|
||||
uv run --no-project python measure_ladder.py --sweep-target
|
||||
uv run --no-project python measure_ladder.py --sweep-requote
|
||||
uv run --no-project python measure_ladder.py --sweep-reference
|
||||
uv run --no-project python measure_ladder.py --sweep-freeze
|
||||
uv run --no-project python measure_ladder.py --sweep-shape
|
||||
|
||||
--- what it measured, 2026-08-21, amd-server, 120 baskets from SEED_BASE ------------------
|
||||
|
||||
TURNS=8, TICKS_PER_TURN=7, TAIL_TICKS=4, HORIZON=60, FREEZE_RANGE=(1,4), TARGET_SHARE=0.90.
|
||||
|
||||
policy total profit effcy discip clean gp looks
|
||||
inaction 0.000 0.000 0.000 0.000 0.000 0 0.00
|
||||
staller 0.000 0.000 0.000 0.000 0.000 0 0.00
|
||||
crude (market orders, one look) 0.126 0.207 0.186 0.095 0.017 -2,684 1.00
|
||||
spammer 0.158 0.262 0.219 0.114 0.025 13,938 7.89
|
||||
restate (re-quote the same book) 0.219 0.375 0.362 0.168 0.000 25,244 7.90
|
||||
plausible (mean anchor, no filter) 0.406 0.563 0.524 0.391 0.142 37,084 1.00
|
||||
churn (re-place at full size) 0.447 0.684 0.550 0.430 0.042 49,926 8.00
|
||||
impatient (one-shot ref, one look) 0.538 0.739 0.722 0.560 0.150 50,172 1.00
|
||||
hindsight (whole-window anchor) 0.598 0.792 0.763 0.630 0.208 54,599 1.00
|
||||
exhaustive (re-quote every look) 0.631 0.849 0.749 0.669 0.192 63,802 7.97
|
||||
oracle (the live reference) 1.000 1.000 1.000 1.000 1.000 76,744 3.99
|
||||
|
||||
Every row reconciles with 0.45*profit + 0.30*discipline + 0.25*clean to three decimals,
|
||||
which the spec's ladder did not.
|
||||
|
||||
**The budget binds. Oracle - exhaustive = 0.369, stdev 0.054 over five blocks of 24, worst
|
||||
block 0.316.** That is fifteen times the 0.02 margin B2 proposed and it clears the 0.25
|
||||
quantum of the binary `gate` term outright, so the comparison is not a cliff. It is also
|
||||
not an artifact of the denominator: the reference earns 76,744 gp against exhaustive's
|
||||
63,802 and out-returns it on peak capital, 0.302 against 0.219.
|
||||
|
||||
`hindsight` is the row that says the stepped form measures something the one-shot form
|
||||
cannot. It quotes off the mean of the WHOLE window, held-out ticks included, and still
|
||||
scores 0.598 — perfect price foresight in a single plan is worth less than playing the
|
||||
window with none.
|
||||
|
||||
Three things this measurement overturned:
|
||||
|
||||
the first engine's rule-4 gap was fake. With a reference that never redeployed sale
|
||||
proceeds, exhaustive earned 22% MORE gp than the reference and scored 0.848: the gap was
|
||||
the denominator being under-tuned, exactly the imitation-score defect `book.py` documents
|
||||
paying for once already. Fixed by giving the reference the idle-cash trigger.
|
||||
|
||||
the freeze alone is not what binds. At freeze 0 the gap is still 0.180 — the queue reset
|
||||
and the capital churn carry it. The freeze sets the slope: 0.238 at 1 tick, 0.505 at 4,
|
||||
0.902 at 8. FREEZE_RANGE=(1,4) is kept because 8 turns a look into a catastrophe rather
|
||||
than a cost, and because flat-per-look is the settled decision.
|
||||
|
||||
more turns bind harder, monotonically: 4x14+4 gives 0.155, 8x7+4 gives 0.369, 12x4+12
|
||||
gives 0.517. Eight is chosen for the token cost of a rollout, not because the gap needs
|
||||
it. If the gap ever needs widening, this is the lever, and it is measured.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
import statistics
|
||||
from dataclasses import replace
|
||||
import sys
|
||||
import time
|
||||
import types
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
_shim = types.ModuleType("grand_exchange_live")
|
||||
_shim.__path__ = [str(HERE / "grand_exchange_live")]
|
||||
sys.modules["grand_exchange_live"] = _shim
|
||||
|
||||
from grand_exchange_live.book import ( # noqa: E402
|
||||
BUY_BAND,
|
||||
FILL_SHARE,
|
||||
MAX_ITEM_SHARE,
|
||||
MIN_CROSSINGS,
|
||||
SELL_BAND,
|
||||
TAX,
|
||||
Order,
|
||||
crossings,
|
||||
reference_orders,
|
||||
)
|
||||
from grand_exchange_live.live import ( # noqa: E402
|
||||
HORIZON,
|
||||
IDLE_SHARE,
|
||||
LIVE_VIABILITY_TRIES,
|
||||
REQUOTE_BAND,
|
||||
TAIL_TICKS,
|
||||
TICKS_PER_TURN,
|
||||
TURNS,
|
||||
Leg,
|
||||
Reference,
|
||||
desired_orders,
|
||||
live_reference,
|
||||
play,
|
||||
viable_live_market,
|
||||
)
|
||||
from grand_exchange_live.market import SEED_BASE, build_market # noqa: E402
|
||||
|
||||
HEADER = "requote / idle"
|
||||
BLOCK = 24
|
||||
BLOCKS = 5
|
||||
TARGET_SHARE = 0.90
|
||||
"""What counts as a full score, as a share of the LIVE reference's realised profit and of
|
||||
its return on peak capital. Chosen by `--sweep-target`, not carried over from the spec,
|
||||
which proposed 0.85 against an engine that no longer exists.
|
||||
|
||||
The two things it trades off are both measured over 120 baskets. Raising it widens the
|
||||
rule-4 gap (exhaustive scores 0.723 at 0.80 and 0.574 at 1.00) and narrows the ceiling
|
||||
plateau — and the plateau is what stops the reward being an imitation score for the
|
||||
reference's own constants. At 0.90 the best-earning member of the reference's family that
|
||||
is NOT the reference earns 1.1% more gp and still scores 0.976, far above the 0.90 bar
|
||||
`probe.py` holds the one-shot form to, and the gap to exhaustive is 0.372. It is also the
|
||||
one-shot environment's value, which is one fewer constant that differs between two forms of
|
||||
the same market for no measured reason."""
|
||||
|
||||
|
||||
# --- the baskets a run actually serves --------------------------------------------------
|
||||
def baskets(count: int, base: int = SEED_BASE):
|
||||
"""Skip-aware, exactly as `Taskset.load` will be: `viable_live_market` may pass over a
|
||||
seed the LIVE reference loses money in, and the next task starts after the seed it
|
||||
landed on. Grading a contiguous `range` instead would grade a different set of baskets
|
||||
from the one a model is served."""
|
||||
out, seed = [], base
|
||||
for _ in range(count):
|
||||
market = viable_live_market(seed, 5, 56, HORIZON)
|
||||
seed = market.seed + 1
|
||||
out.append(market)
|
||||
return out
|
||||
|
||||
|
||||
# --- scoring ----------------------------------------------------------------------------
|
||||
def score(result, reference, target_share: float = TARGET_SHARE) -> dict[str, float]:
|
||||
"""The reward, in one place, so no two rows can disagree about the same episode."""
|
||||
target = target_share * reference.realised
|
||||
profit = 0.0 if target <= 0 else min(1.0, max(0.0, result.realised / target))
|
||||
efficiency = 0.0 if reference.roc <= 0 else min(1.0, max(0.0, result.roc / reference.roc))
|
||||
discipline = profit * efficiency
|
||||
clean = float(result.realised >= target_share * reference.realised
|
||||
and result.roc >= target_share * reference.roc)
|
||||
return {
|
||||
"total": 0.45 * profit + 0.30 * discipline + 0.25 * clean,
|
||||
"profit_ratio": profit,
|
||||
"efficiency": efficiency,
|
||||
"discipline": discipline,
|
||||
"clean": clean,
|
||||
}
|
||||
|
||||
|
||||
# --- the ladder, as policies rather than as adjectives ----------------------------------
|
||||
def inaction(view):
|
||||
"""Never submits a sheet. House rule 3's floor."""
|
||||
return []
|
||||
|
||||
|
||||
def staller(view):
|
||||
"""Takes every look and does nothing with any of them. Scores what inaction scores —
|
||||
the point of the row is that BURNING the budget is not itself worth anything, which is
|
||||
the degenerate strategy a time-averaged reward would pay for."""
|
||||
return []
|
||||
|
||||
|
||||
def spammer(seed: int):
|
||||
"""Random legs on every look. Acts constantly, reads nothing."""
|
||||
rng = random.Random(seed)
|
||||
|
||||
def policy(view):
|
||||
items = view.market.items
|
||||
return [
|
||||
Leg(i.name, rng.randint(1, i.buy_limit),
|
||||
round(i.prices[view.seen - 1] * rng.uniform(0.85, 1.15)),
|
||||
round(i.prices[view.seen - 1] * rng.uniform(0.85, 1.15)))
|
||||
for i in items if rng.random() < 0.6
|
||||
]
|
||||
|
||||
return policy
|
||||
|
||||
|
||||
def one_look(plan_of):
|
||||
"""Submit a plan on turn 1, never look again. The one-shot form, played live."""
|
||||
def make(market):
|
||||
state = {"done": False}
|
||||
orders = plan_of(market)
|
||||
|
||||
def policy(view):
|
||||
if state["done"]:
|
||||
return []
|
||||
state["done"] = True
|
||||
return [Leg(o.item, o.quantity, o.buy, o.sell) for o in orders]
|
||||
|
||||
return policy
|
||||
|
||||
return make
|
||||
|
||||
|
||||
def band(market, items, anchor, sizer, filtered=False, ranked=False):
|
||||
"""`probe.py`'s strategy family, unchanged, so the live ladder's crude and plausible
|
||||
rungs are the same strategies the one-shot ladder grades."""
|
||||
plans, out, purse = [], [], float(market.capital)
|
||||
for item in items:
|
||||
prices = item.visible_prices(market.visible)
|
||||
volumes = item.visible_volumes(market.visible)
|
||||
if filtered and crossings(prices) < MIN_CROSSINGS:
|
||||
continue
|
||||
a = anchor(prices)
|
||||
buy = max(1, round(a * (1.0 - BUY_BAND)))
|
||||
sell = max(buy + 1, round(a * (1.0 + SELL_BAND)))
|
||||
lows = [p for p in prices if p <= buy]
|
||||
highs = [p for p in prices if p >= sell]
|
||||
qty = sizer(item, market, buy, prices, volumes)
|
||||
if qty <= 0:
|
||||
continue
|
||||
s = 0.0
|
||||
if ranked:
|
||||
if not lows or not highs:
|
||||
continue
|
||||
entry, exit_ = statistics.fmean(lows), statistics.fmean(highs)
|
||||
s = (exit_ * (1.0 - TAX) - entry) / entry
|
||||
if s <= 0:
|
||||
continue
|
||||
plans.append((s, item.name, qty, buy, sell))
|
||||
plans.sort(key=lambda p: -p[0])
|
||||
for _, name, qty, buy, sell in plans:
|
||||
qty = min(qty, int(purse // buy))
|
||||
if qty <= 0:
|
||||
continue
|
||||
purse -= qty * buy
|
||||
out.append(Order(name, qty, buy, sell))
|
||||
return out
|
||||
|
||||
|
||||
def by_volume(item, market, buy, prices, volumes):
|
||||
lows = [p for p in prices if p <= buy]
|
||||
reach = int(FILL_SHARE * statistics.median(volumes) * (len(lows) / len(prices))
|
||||
* market.held_out)
|
||||
return min(item.buy_limit, reach, int(MAX_ITEM_SHARE * market.capital // buy))
|
||||
|
||||
|
||||
def market_orders(market):
|
||||
"""Buy ten percent over the last price and sell ten under: every offer fills and every
|
||||
round trip pays the tax. The crude maximiser."""
|
||||
return [
|
||||
Order(i.name, i.buy_limit, round(i.prices[market.visible - 1] * 1.10),
|
||||
round(i.prices[market.visible - 1] * 0.90))
|
||||
for i in market.items
|
||||
]
|
||||
|
||||
|
||||
def churn(market):
|
||||
"""Re-place the ORIGINAL plan at full size on every look.
|
||||
|
||||
Not the same policy as re-stating the book: the units already bought release their
|
||||
reservation and sale proceeds have returned to the purse, so asking for the full size
|
||||
again spends them. It earns roughly what a single look earns off materially more peak
|
||||
capital, which is precisely the shape `efficiency` exists to catch.
|
||||
"""
|
||||
orders = reference_orders(market)
|
||||
return lambda view: [Leg(o.item, o.quantity, o.buy, o.sell) for o in orders]
|
||||
|
||||
|
||||
def restate(market):
|
||||
"""Place the one-shot reference on turn 1, then re-submit the book exactly as it stands
|
||||
on every look after it.
|
||||
|
||||
The pure cost of looking, isolated: same names, same prices, same units still sought,
|
||||
nothing learned, one freeze and one queue reset per turn. Its gap below `impatient` — an
|
||||
identical plan that simply stops looking — is the price of a look with no information in
|
||||
it, in reward units.
|
||||
"""
|
||||
orders = reference_orders(market)
|
||||
|
||||
def policy(view):
|
||||
if view.turn == 1:
|
||||
return [Leg(o.item, o.quantity, o.buy, o.sell) for o in orders]
|
||||
return [Leg(o.item, o.quantity, o.buy, o.sell) for o in view.open_orders.values()]
|
||||
|
||||
return policy
|
||||
|
||||
|
||||
def exhaustive(market):
|
||||
"""Spend the entire budget: re-quote on EVERY look, off everything observed so far.
|
||||
|
||||
This is the rule-4 policy. It makes every judgement the reference makes and one more
|
||||
look than the reference thinks is worth paying for, so if it scores at the oracle the
|
||||
budget is free and the environment measures nothing the one-shot form did not.
|
||||
"""
|
||||
inner = Reference(requote_band=0.0)
|
||||
return inner
|
||||
|
||||
|
||||
def perfect_hindsight(market):
|
||||
"""Not shipped, not a rung: a sanity ceiling. Quotes off the mean of the WHOLE window,
|
||||
held-out ticks included. If the oracle were close to this, the visible half would not be
|
||||
the thing being read."""
|
||||
prices = {i.name: list(i.prices) for i in market.items}
|
||||
volumes = {i.name: list(i.volumes) for i in market.items}
|
||||
orders = desired_orders(market.items, prices, volumes, float(market.capital),
|
||||
market.held_out, {i.name: i.buy_limit for i in market.items})
|
||||
return one_look(lambda m: orders)(market)
|
||||
|
||||
|
||||
LADDER = [
|
||||
("inaction", lambda m: inaction),
|
||||
("staller", lambda m: staller),
|
||||
("spammer", lambda m: spammer(m.seed)),
|
||||
("crude (market orders, one look)", one_look(market_orders)),
|
||||
("plausible (mean anchor, no filter)",
|
||||
one_look(lambda m: band(m, m.items, statistics.fmean, by_volume))),
|
||||
("impatient (one-shot reference, one look)", one_look(reference_orders)),
|
||||
("restate (re-quote the same book every look)", restate),
|
||||
("churn (re-place at full size every look)", churn),
|
||||
("exhaustive (re-quote every look)", exhaustive),
|
||||
("oracle (the live reference)", lambda m: Reference()),
|
||||
("hindsight (not a rung: whole-window anchor)", perfect_hindsight),
|
||||
]
|
||||
|
||||
|
||||
def ladder(markets, target_share: float = TARGET_SHARE) -> dict[str, dict[str, float]]:
|
||||
rows: dict[str, dict[str, float]] = {}
|
||||
for name, make in LADDER:
|
||||
totals: dict[str, float] = {}
|
||||
gp = looks = frozen = 0.0
|
||||
peak = 0.0
|
||||
for market in markets:
|
||||
result = play(market, make(market))
|
||||
for key, value in score(result, live_reference(market), target_share).items():
|
||||
totals[key] = totals.get(key, 0.0) + value
|
||||
gp += result.realised
|
||||
looks += result.looks
|
||||
frozen += result.frozen_ticks
|
||||
peak += result.peak_employed
|
||||
n = len(markets)
|
||||
rows[name] = {k: v / n for k, v in totals.items()}
|
||||
rows[name].update(gp=gp / n, looks=looks / n, frozen=frozen / n, peak=peak / n)
|
||||
return rows
|
||||
|
||||
|
||||
def table(markets, target_share: float = TARGET_SHARE) -> None:
|
||||
rows = ladder(markets, target_share)
|
||||
print(f"{'policy':<46}{'total':>8}{'profit':>8}{'effcy':>8}{'discip':>8}{'clean':>7}"
|
||||
f"{'gp':>12}{'peak gp':>10}{'looks':>7}{'frozen':>7}")
|
||||
for name, row in rows.items():
|
||||
print(f"{name:<46}{row['total']:>8.3f}{row['profit_ratio']:>8.3f}"
|
||||
f"{row['efficiency']:>8.3f}{row['discipline']:>8.3f}{row['clean']:>7.3f}"
|
||||
f"{row['gp']:>12,.0f}{row['peak']:>10,.0f}{row['looks']:>7.2f}"
|
||||
f"{row['frozen']:>7.2f}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
flags = set(sys.argv[1:])
|
||||
started = time.monotonic()
|
||||
seeds = baskets(BLOCK * BLOCKS)
|
||||
build = time.monotonic() - started
|
||||
blocks = [seeds[i:i + BLOCK] for i in range(0, len(seeds), BLOCK)]
|
||||
skipped = sum(1 for i, m in enumerate(seeds)
|
||||
if m.seed != (seeds[i - 1].seed + 1 if i else SEED_BASE))
|
||||
|
||||
print(f"engine TURNS={TURNS} TICKS_PER_TURN={TICKS_PER_TURN} TAIL={TAIL_TICKS} "
|
||||
f"HORIZON={HORIZON} REQUOTE_BAND={REQUOTE_BAND:.4f} TARGET_SHARE={TARGET_SHARE}")
|
||||
print(f"baskets {len(seeds)} over {BLOCKS} blocks of {BLOCK}, "
|
||||
f"{skipped} seeds skipped by viability (cap {LIVE_VIABILITY_TRIES}), "
|
||||
f"built in {build:.2f}s")
|
||||
print(f"freeze {statistics.fmean([m.freeze for m in seeds]):.2f} ticks mean, "
|
||||
f"distribution {sorted({f: [m.freeze for m in seeds].count(f) for f in {m.freeze for m in seeds}}.items())}")
|
||||
print()
|
||||
|
||||
print("=== the ladder, all 120 baskets ===")
|
||||
table(seeds)
|
||||
print()
|
||||
|
||||
print("=== does the budget bind? exhaustive against oracle, block by block ===")
|
||||
print(f"{'block':<22}{'exhaustive':>12}{'oracle':>10}{'gap':>10}"
|
||||
f"{'exh gp':>12}{'ora gp':>12}{'exh looks':>11}{'ora looks':>11}")
|
||||
gaps = []
|
||||
for i, block in enumerate(blocks):
|
||||
rows = ladder(block)
|
||||
e, o = rows["exhaustive (re-quote every look)"], rows["oracle (the live reference)"]
|
||||
gaps.append(o["total"] - e["total"])
|
||||
print(f"{f'{block[0].seed}-{block[-1].seed}':<22}{e['total']:>12.3f}{o['total']:>10.3f}"
|
||||
f"{o['total'] - e['total']:>10.3f}{e['gp']:>12,.0f}{o['gp']:>12,.0f}"
|
||||
f"{e['looks']:>11.2f}{o['looks']:>11.2f}")
|
||||
print(f"{'mean':<22}{'':>12}{'':>10}{statistics.fmean(gaps):>10.3f}")
|
||||
print(f"{'stdev':<22}{'':>12}{'':>10}{statistics.stdev(gaps):>10.3f}")
|
||||
print(f"{'worst block':<22}{'':>12}{'':>10}{min(gaps):>10.3f}")
|
||||
print()
|
||||
|
||||
print("=== every rung, block by block (total only) ===")
|
||||
per_block = [ladder(b) for b in blocks]
|
||||
print(f"{'policy':<46}" + "".join(f"{f'b{i}':>9}" for i in range(len(blocks)))
|
||||
+ f"{'spread':>9}")
|
||||
for name, _ in LADDER:
|
||||
values = [rows[name]["total"] for rows in per_block]
|
||||
print(f"{name:<46}" + "".join(f"{v:>9.3f}" for v in values)
|
||||
+ f"{max(values) - min(values):>9.3f}")
|
||||
print()
|
||||
|
||||
if "--sweep-target" in flags:
|
||||
print("=== TARGET_SHARE sweep (the ceiling band) ===")
|
||||
names = [n for n, _ in LADDER]
|
||||
print(f"{'share':<8}" + "".join(f"{n.split(' ')[0]:>12}" for n in names))
|
||||
for share in (0.75, 0.80, 0.85, 0.90, 0.95, 1.00):
|
||||
rows = ladder(seeds, share)
|
||||
print(f"{share:<8.2f}" + "".join(f"{rows[n]['total']:>12.3f}" for n in names))
|
||||
print()
|
||||
|
||||
if "--sweep-requote" in flags:
|
||||
print("=== REQUOTE_BAND sweep (what the reference should pay a freeze for) ===")
|
||||
print(f"{'band':<8}{'gp':>12}{'roc':>10}{'looks':>8}{'frozen':>8}")
|
||||
for band_value in (0.0, 0.005, 0.01, 0.015, 0.025, 0.04, 0.06, 0.10, 1.0):
|
||||
gp = roc = looks = frozen = 0.0
|
||||
for market in seeds:
|
||||
result = play(market, Reference(requote_band=band_value))
|
||||
gp += result.realised
|
||||
roc += result.roc
|
||||
looks += result.looks
|
||||
frozen += result.frozen_ticks
|
||||
n = len(seeds)
|
||||
print(f"{band_value:<8.3f}{gp / n:>12,.0f}{roc / n:>10.4f}"
|
||||
f"{looks / n:>8.2f}{frozen / n:>8.2f}")
|
||||
print()
|
||||
|
||||
if "--sweep-reference" in flags:
|
||||
# The denominator has to be the profit-maximising point of its OWN family, or some
|
||||
# other member earns more gp and scores less, and the reward's argmax is a pair of
|
||||
# constants rather than a strategy. This is the same sweep `book.py` documents for
|
||||
# the one-shot bands, over the two triggers that make the live reference act.
|
||||
print("=== reference family sweep: gp per basket ===")
|
||||
idles = (0.0, 0.05, 0.10, 0.15, 0.20, 0.30, 0.50, 1.01)
|
||||
print(f"{HEADER:<16}" + "".join(f"{i:>10.2f}" for i in idles))
|
||||
best = (0.0, None)
|
||||
for band_value in (0.0, 0.005, 0.010, 0.015, 0.025, 0.040, 0.060, 1.0):
|
||||
row = []
|
||||
for idle in idles:
|
||||
gp = sum(play(m, Reference(band_value, idle)).realised for m in seeds)
|
||||
row.append(gp / len(seeds))
|
||||
if row[-1] > best[0]:
|
||||
best = (row[-1], (band_value, idle))
|
||||
print(f"{band_value:<16.3f}" + "".join(f"{v:>10,.0f}" for v in row))
|
||||
print(f"argmax {best[1]} at {best[0]:,.0f} gp per basket; shipped "
|
||||
f"({REQUOTE_BAND}, {IDLE_SHARE}) is "
|
||||
f"{sum(play(m, Reference()).realised for m in seeds) / len(seeds):,.0f}")
|
||||
print()
|
||||
|
||||
print("=== the same family, in return on peak capital ===")
|
||||
print(f"{HEADER:<16}" + "".join(f"{i:>10.2f}" for i in idles))
|
||||
for band_value in (0.0, 0.005, 0.010, 0.015, 0.025, 0.040, 0.060, 1.0):
|
||||
row = []
|
||||
for idle in idles:
|
||||
roc = sum(play(m, Reference(band_value, idle)).roc for m in seeds)
|
||||
row.append(roc / len(seeds))
|
||||
print(f"{band_value:<16.3f}" + "".join(f"{v:>10.4f}" for v in row))
|
||||
print()
|
||||
|
||||
print("=== and what each family member SCORES against the shipped reference ===")
|
||||
print(f"{HEADER:<16}" + "".join(f"{i:>10.2f}" for i in idles))
|
||||
for band_value in (0.0, 0.005, 0.010, 0.015, 0.025, 0.040, 0.060, 1.0):
|
||||
row = []
|
||||
for idle in idles:
|
||||
total = sum(score(play(m, Reference(band_value, idle)), live_reference(m))["total"]
|
||||
for m in seeds)
|
||||
row.append(total / len(seeds))
|
||||
print(f"{band_value:<16.3f}" + "".join(f"{v:>10.3f}" for v in row))
|
||||
print()
|
||||
|
||||
if "--sweep-freeze" in flags:
|
||||
# If no freeze makes looking expensive enough for the optimum to be interior, the
|
||||
# freeze is decoration and rule 4 is being carried by the denominator alone.
|
||||
print("=== freeze sweep: what a look has to cost before it is worth declining ===")
|
||||
print(f"{'freeze':<10}{'exhaust gp':>12}{'oracle gp':>12}{'exh score':>11}"
|
||||
f"{'gap':>8}{'ora looks':>11}{'ora frozen':>12}")
|
||||
for forced in (0, 1, 2, 3, 4, 6, 8, 12):
|
||||
shaped = [replace(m, freeze=forced) for m in seeds]
|
||||
e = g = 0.0
|
||||
egp = ogp = looks = frozen = 0.0
|
||||
for m in shaped:
|
||||
ref = play(m, Reference())
|
||||
exh = play(m, Reference(requote_band=0.0, idle_share=0.0))
|
||||
if ref.realised <= 0:
|
||||
continue
|
||||
e += score(exh, ref)["total"]
|
||||
g += 1.0 - score(exh, ref)["total"]
|
||||
egp += exh.realised
|
||||
ogp += ref.realised
|
||||
looks += ref.looks
|
||||
frozen += ref.frozen_ticks
|
||||
n = len(shaped)
|
||||
print(f"{forced:<10}{egp / n:>12,.0f}{ogp / n:>12,.0f}{e / n:>11.3f}"
|
||||
f"{g / n:>8.3f}{looks / n:>11.2f}{frozen / n:>12.2f}")
|
||||
print()
|
||||
|
||||
if "--sweep-shape" in flags:
|
||||
print("=== window shape sweep: does a different budget bind harder? ===")
|
||||
print(f"{'turns x step + tail':<22}{'horizon':>9}{'exhaust':>9}{'oracle':>9}"
|
||||
f"{'gap':>9}{'ora looks':>11}{'held out':>10}")
|
||||
for turns, step, tail in ((4, 14, 4), (6, 9, 6), (8, 7, 4), (8, 5, 20),
|
||||
(10, 5, 10), (12, 4, 12)):
|
||||
horizon = turns * step + tail
|
||||
shaped = [build_market(m.seed, 5, 56, horizon) for m in seeds]
|
||||
e = o = 0.0
|
||||
looks = 0.0
|
||||
for market in shaped:
|
||||
ref = play(market, Reference(), turns=turns, step=step, tail=tail)
|
||||
exh = play(market, Reference(requote_band=0.0), turns=turns, step=step,
|
||||
tail=tail)
|
||||
if ref.realised <= 0:
|
||||
continue
|
||||
e += score(exh, ref)["total"]
|
||||
o += score(ref, ref)["total"]
|
||||
looks += ref.looks
|
||||
n = len(shaped)
|
||||
print(f"{f'{turns} x {step} + {tail}':<22}{horizon:>9}{e / n:>9.3f}{o / n:>9.3f}"
|
||||
f"{(o - e) / n:>9.3f}{looks / n:>11.2f}{step + tail:>10}")
|
||||
print()
|
||||
|
||||
print(f"total wall clock {time.monotonic() - started:.1f}s")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user