From fb226dd419c2a20ee218a3d723b4fa52050d068f Mon Sep 17 00:00:00 2001 From: Ken Sedgwick Date: Sun, 14 Jun 2026 15:24:52 -0700 Subject: [PATCH] contrib: clboss-xrebalance-survival splits curves by regime sample count Both P(changed|...) curves -- the absolute-gap cap curve and the gap/span frac curve -- now break each bucket's contradiction rate into nN sub-columns: the rate among regimes backed by 2, 3-5, and 6+ observations (n2 / n3-5 / n6+). The horizon (min(cap, frac*span)) extrapolates from a regime's span no matter how many points define it, gated only by min-samples=2. So a flat headline rate can hide thin two-observation regimes -- the ones a longer frac or cap trusts the most -- misbehaving. The sub-columns surface that before lengthening either knob. A trailing ~ marks a sub-rate with fewer than 30 trials. Adds nrec (regime record count) to each replayed trial and a sample_bin helper; the curve aggregator now tracks per-bucket sub-bin trials and contradictions. Other tables are unchanged. --- contrib/clboss-xrebalance-survival | 53 +++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 8 deletions(-) diff --git a/contrib/clboss-xrebalance-survival b/contrib/clboss-xrebalance-survival index 379fa60..9789a1e 100755 --- a/contrib/clboss-xrebalance-survival +++ b/contrib/clboss-xrebalance-survival @@ -28,6 +28,13 @@ Outputs: 4. P(changed | gap/span) -- tests the frac*span rule itself: if this curve is cleaner than (3), frac is the knob that matters; if (3) dominates, the cap is. + Both (3) and (4) split each bucket's rate into nN sub-columns: + the contradiction rate among regimes backed by N observations + (n2 / n3-5 / n6+). The horizon extrapolates from span no matter + how many points define it, so a flat headline rate can still + hide thin 2-observation regimes misbehaving -- the sub-columns + expose that before you lengthen frac or the cap. ('~' marks a + sub-rate with < 30 trials.) 5. policy-fail walls vs liquidity walls (inbound-fee exclusions should survive far longer). 6. visit-frequency terciles for the long-gap buckets (the tail is @@ -103,6 +110,24 @@ REL_BUCKETS = [ (float("inf"), ">8x"), ] +# Sub-bins for the P(changed|...) curves: split each gap bucket's +# rate by how many observations back the regime (and hence its span). +# The horizon extrapolates from span regardless of point count, so a +# flat headline rate can hide thin 2-observation regimes misbehaving. +SAMPLE_BINS = [ + (2, "n2"), + (5, "n3-5"), + (INF, "n6+"), +] +SAMPLE_HDR = tuple(label for _, label in SAMPLE_BINS) + + +def sample_bin(nrec): + for limit, label in SAMPLE_BINS: + if nrec <= limit: + return label + return SAMPLE_BINS[-1][1] + def bucket_of(value, buckets): for limit, label in buckets: @@ -191,6 +216,7 @@ def replay(rows, frac, cap, min_samples): yield { "gap": gap, "span": span, + "nrec": reg["nfail"] + reg["nok"], "contradiction": contradiction, "side": "drain" if isf else "refill", "asserting": asserting, @@ -314,22 +340,32 @@ def main(): agg = {} for t in trial_list: b = keyfn(t) - a = agg.setdefault(b, [0, 0, 0, 0]) - a[0] += 1 + a = agg.setdefault(b, {"tot": [0, 0, 0, 0], "smp": {}}) + a["tot"][0] += 1 if t["contradiction"]: - a[1] += 1 - a[2 if t["side"] == "refill" else 3] += 1 + a["tot"][1] += 1 + a["tot"][2 if t["side"] == "refill" else 3] += 1 + sb = a["smp"].setdefault(sample_bin(t["nrec"]), [0, 0]) + sb[0] += 1 + if t["contradiction"]: + sb[1] += 1 rows = [] for limit, label in buckets: if label not in agg: continue - n, ch, re, dr = agg[label] - rows.append((label, n, ch, rate(ch, n), re, dr)) + n, ch, re, dr = agg[label]["tot"] + smp = agg[label]["smp"] + subs = [] + for _, sbl in SAMPLE_BINS: + stot, sch = smp.get(sbl, [0, 0]) + subs.append(rate(sch, stot)) + rows.append((label, n, ch, rate(ch, n), *subs, re, dr)) return rows print(fmt_table( "P(changed | gap) [the cap-setting curve]", - ("gap", "trials", "changed", "rate", "refill", "drain"), + ("gap", "trials", "changed", "rate") + SAMPLE_HDR + + ("refill", "drain"), curve(curve_trials, ABS_BUCKETS, lambda t: bucket_of(t["gap"], ABS_BUCKETS)))) @@ -339,7 +375,8 @@ def main(): print(fmt_table( "P(changed | gap/span) [tests the frac*span rule;" " %d no-span trials excluded]" % nospan, - ("gap/span", "trials", "changed", "rate", "refill", "drain"), + ("gap/span", "trials", "changed", "rate") + SAMPLE_HDR + + ("refill", "drain"), curve(spanned, REL_BUCKETS, lambda t: bucket_of(t["gap"] / t["span"], REL_BUCKETS))))