The stepped engine becomes a runnable taskset. protocol.py is both the render/parse boundary and the security boundary: TurnView carries the live Market, so view.market.items[i].prices is the whole held-out series and a foresight policy reading it earned +9.4% on the oracle. The renderer emits only observed ticks, the trace carries plain scalars, and a sentinel scan of every rendered turn fails if either reopens. House rule 1 had no other enforcement. Replies are a delta sheet and parsing never raises — NaN, Infinity, 1e309 and 20k-deep nesting all resolve to a hold, because the one-shot form has already shown what a real model sends when it gives up. LiveExchangeEnv.run drives the engine host-side with the harness left null. Two things learned the hard way and worth keeping: a terminated Segment mid-window holds the remaining looks instead of raising, and rewards are recorded INSIDE the interaction, before close. Recording after close writes the traces correctly and leaves every eval.log line reading reward=0.000 — the canonical verifiers reference does it after the block; do not copy it there. The reference-family gate is widened from four named rungs to the whole 8x8 (requote_band, idle_share) family: no member may out-earn the shipped reference by more than 1.02x, and any that does must still score >= 0.90. That is the check that caught the first draft's fake 0.152 gap, now shipped rather than remembered. Also guards `clean` on the reference being profitable, here and in the one-shot form. Against a reference that lost money the bar is negative, so doing nothing cleared it and inaction collected a quarter of the reward. Unreachable while viable_market admits only profitable references; one relaxed filter from reachable, and exactly the floor house rule 3 forbids. Probe values unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
531 lines
24 KiB
Python
531 lines
24 KiB
Python
"""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
|
|
sys.path.insert(0, str(HERE))
|
|
try:
|
|
# The installed package, when there is one — `taskset.py` reads `__all__` off whatever
|
|
# `sys.modules` holds, and a namespace shim has none, so an unconditional shim here
|
|
# would break every taskset lookup in any process that also imports this file.
|
|
import grand_exchange_live # noqa: F401
|
|
except ImportError:
|
|
# Nothing installed: register the package as a namespace pointing at the source and let
|
|
# the leaf modules import under it, so `__init__` — and with it `verifiers` — never runs.
|
|
# This is the path `probe.py` takes, and it is the reason this file can be graded with
|
|
# the training stack absent.
|
|
_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
|
|
import grand_exchange_live.reward as reward # noqa: E402
|
|
|
|
HEADER = "requote / idle"
|
|
BLOCK = 24
|
|
BLOCKS = 5
|
|
TARGET_SHARE = reward.TARGET_SHARE
|
|
"""The ceiling band, imported rather than restated. It was defined here while it was being
|
|
measured — `--sweep-target` is what chose 0.90 — and it moved into the shipped package the
|
|
day the taskset landed: a reward the probe computes and a reward the run computes are two
|
|
rewards, and the day they drift the probe is gating something the model is not paid for.
|
|
`reward.TARGET_SHARE` carries the measurement that chose it."""
|
|
|
|
score = reward.score
|
|
"""Likewise. One definition, three callers: the run, this ladder, and `probe.py` through it."""
|
|
|
|
|
|
# --- the baskets a run actually serves --------------------------------------------------
|
|
def baskets(count: int, base: int = SEED_BASE):
|
|
"""Skip-aware, exactly as `taskset.LiveExchangeTaskset.load` is: `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
|
|
|
|
|
|
# --- 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()
|