feat(alloc): per-sleeve crypto allocation (promote xsfunding/unlock; bybit_4edge->reference)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-07-16 07:47:26 +02:00
parent c1d1cb80fb
commit 98dda4e835
4 changed files with 122 additions and 42 deletions

View File

@@ -6,14 +6,20 @@ PURE + network-free: `store` is read-only (panel reads via the existing edge run
Everything is composed from the existing engines — no return math is reimplemented here:
* `sleeve_weights_and_contributions` (per-symbol weights) + `honest_sleeve_returns` (capacity + cost) build
each sleeve's honest daily series at a given AUM;
* `risk_parity_weights` + `book_returns` aggregate the crypto BOOK for the `bybit_4edge` deploy strategy;
* `risk_parity_weights` + `book_returns` aggregate the crypto BOOK for the honest-reference REPORT (see
`_book_series` below) — no longer used by the allocation path (see the 2026-07-16 spec §4d);
* `sharpe_of` + `capacity_ceiling_from_curve` turn an AUM→Sharpe curve into the honest capacity ceiling.
The deploy→honest mapping (crypto-only today):
* "positioning" → its OWN sleeve series at the AUM; ceiling from its capacity curve.
* "bybit_4edge" → the risk-parity BOOK of its available crypto sleeves (`_BOOK_SLEEVES`, tstrend EXCLUDED
— it has no standalone store series); empty sleeves (e.g. unlock with no events) are
dropped; ceiling from the book's capacity curve.
The deploy→honest mapping (crypto-only today; per-sleeve since the 2026-07-16 spec, which re-points the
crypto allocation at the individual sleeves instead of the double-counting `bybit_4edge` book):
* "positioning" / "xsfunding" / "unlock" (`_CRYPTO_DEPLOY_SIDS`) → that sleeve's OWN series at the AUM
(via `_sleeve_series`); ceiling from its capacity curve. Each sleeve enters the deploy
set independently (gated by its own reconciliation gate / `promote_on_pass` in the
registry, not by this module).
* "bybit_4edge" → NO LONGER an allocation target (demoted to `tier: research`, a tracked reference
aggregate). It falls through to the "any OTHER sid" branch below like any non-crypto
sid; `_book_series` / `_BOOK_SLEEVES` are KEPT for the honest-reference REPORT (a
separate consumer), just no longer dispatched to from the allocation path.
* any OTHER sid → the equity default ceiling, NO series (the caller falls back to its own reference for
equity strategies; we do NOT fabricate a crypto series for equity).
@@ -22,8 +28,9 @@ but the downstream consumers (`compute_allocation` / `full_history_vol` / `deplo
`record_allocation`) key on ISO DATE STRINGS (e.g. "2026-06-01") and cross-align strategies BY KEY — and the
nav fallback in `record_allocation` keys on those SAME ISO date strings. So every series is returned with the
epoch-day int converted to the SAME ISO date string via `_iso` (the canonical `date(1970,1,1)+timedelta(days)`
conversion) for every strategy, so `deploy_book_returns` aligns positioning, bybit_4edge, AND any nav-fallback
series on identical calendar-day keys (a test asserts positioning & bybit_4edge share a common key)."""
conversion) for every strategy, so `deploy_book_returns` aligns positioning, xsfunding, unlock, AND any
nav-fallback series on identical calendar-day keys (a test asserts positioning & xsfunding share a common
key)."""
from __future__ import annotations
import datetime as _dt
@@ -39,9 +46,11 @@ from fxhnt.application.honest_reference import honest_sleeve_returns
# when its event calendar is absent (empty series). Ordered; the book aggregation is order-independent.
_BOOK_SLEEVES = ("xsfunding", "unlock", "positioning")
# The deploy strategies with a crypto honest-reference series. Any sid NOT here gets the equity default
# ceiling and no series (the caller supplies the equity reference).
_CRYPTO_DEPLOY_SIDS = frozenset({"positioning", "bybit_4edge"})
# The deploy strategies with a crypto honest-reference series — the individual sleeves, NOT the `bybit_4edge`
# book (demoted to a research-tier reference aggregate 2026-07-16 to resolve the positioning/book
# double-count; see the module docstring + spec §4d). Any sid NOT here gets the equity default ceiling and
# no series (the caller supplies the equity reference).
_CRYPTO_DEPLOY_SIDS = frozenset({"positioning", "xsfunding", "unlock"})
_EPOCH = _dt.date(1970, 1, 1)
@@ -88,14 +97,12 @@ def _deploy_series(store: Any, sid: str, spread_bps: dict[str, float],
unlock_events: list[dict[str, Any]] | None, *, aum: float,
participation_cap: float, impact_k: float) -> dict[int, float]:
"""The honest-reference `dict[int, float]` series for a crypto deploy `sid` at ONE `aum` (empty for a
non-crypto sid). `sid` is passed explicitly (not captured from a loop variable) so there is no
lambda-in-loop closure bug — the branch computes the series directly."""
if sid == "positioning":
return _sleeve_series(store, "positioning", spread_bps, unlock_events, aum=aum,
non-crypto sid, INCLUDING `bybit_4edge` — no longer an allocation target, see `_CRYPTO_DEPLOY_SIDS`).
`sid` is passed explicitly (not captured from a loop variable) so there is no lambda-in-loop closure
bug — the branch computes the series directly."""
if sid in _CRYPTO_DEPLOY_SIDS:
return _sleeve_series(store, sid, spread_bps, unlock_events, aum=aum,
participation_cap=participation_cap, impact_k=impact_k)
if sid == "bybit_4edge":
return _book_series(store, spread_bps, unlock_events, aum=aum,
participation_cap=participation_cap, impact_k=impact_k)
return {}

View File

@@ -62,6 +62,10 @@ STRATEGY_REGISTRY: dict[str, dict[str, Any]] = {
"display_name": "Crypto funding harvest (cross-sectional, delta-neutral)", "sleeve": "crypto-funding",
"venue": "bybit-perp",
"tier": "research", "execution": "paper",
# AUTO-PROMOTE research->deploy once the reconciliation gate PASSes — per-sleeve allocation
# (2026-07-16 spec §4d) re-points the crypto allocation at the individual sleeves instead of the
# `bybit_4edge` book, so this sleeve enters the allocation on gate PASS, exactly as `multistrat` does.
"promote_on_pass": True,
"gate_spec": {"gate_type": "reconciliation", "min_forward_days": 14},
# cost_bps mirrors Settings.bybit_cost_bps; a cost change requires a definition_version bump
# to re-inception (else the hashed audit silently diverges from the cost actually charged).
@@ -82,6 +86,9 @@ STRATEGY_REGISTRY: dict[str, dict[str, Any]] = {
"display_name": "Token-unlock dilution shorts (market-neutral)", "sleeve": "crypto-event",
"venue": "bybit-perp",
"tier": "research", "execution": "paper",
# AUTO-PROMOTE research->deploy once the reconciliation gate PASSes — same per-sleeve rationale as
# xsfunding above (2026-07-16 spec §4d).
"promote_on_pass": True,
"gate_spec": {"gate_type": "reconciliation", "min_forward_days": 14},
# cost_bps mirrors Settings.bybit_cost_bps; a cost change requires a definition_version bump
# to re-inception (else the hashed audit silently diverges from the cost actually charged).
@@ -161,15 +168,21 @@ STRATEGY_REGISTRY: dict[str, dict[str, Any]] = {
"definition": {"params": {"source": "ibkr-xsp-fills", "models": "vrp"},
"version": 1, "record_mode": "observed", "backtest": {"kind": "model-ref"}},
},
# Live FORWARD PAPER TRACK for the 4-edge Bybit book — the OUT-OF-SAMPLE record on the venue we'll
# actually trade. Records the NAIVE equal-weight 4-sleeve book (tstrend/unlock/xsfunding/positioning) on
# `bybit_features` — the honest deployable number (the live risk overlay over-levers on Bybit; that is a
# separate re-tune, deliberately NOT this track). RECONCILIATION gate: WAIT ~2 weeks, then PASS iff the
# forward track reconciles with the backtest (does not diverge/contradict it) and executed cleanly.
# Forward PAPER TRACK for the 4-edge Bybit book — the OUT-OF-SAMPLE record on the venue we'll actually
# trade. Records the NAIVE equal-weight 4-sleeve book (tstrend/unlock/xsfunding/positioning) on
# `bybit_features` (the live risk overlay over-levers on Bybit; that is a separate re-tune, deliberately
# NOT this track). RECONCILIATION gate: WAIT ~2 weeks, then PASS iff the forward track reconciles with
# the backtest (does not diverge/contradict it) and executed cleanly.
# DEMOTED to `tier: research` 2026-07-16 (spec `2026-07-16-deploy-fund-config-knobs-design.md` §4d): the
# book double-counted `positioning` (booked both standalone AND as a book constituent), so the crypto
# allocation now runs PER-SLEEVE (`positioning` + any of `xsfunding`/`unlock` whose own gate PASSes —
# see `promote_on_pass` above) instead of over this aggregate. This track stays a tracked REFERENCE
# aggregate alongside its `bybit_4edge_exec` / `bybit_4edge_levered` research twins — no longer an
# allocation target (gate_spec/definition/inception_t0 unchanged; only `tier` moved).
"bybit_4edge": {
"display_name": "Bybit 4-edge book (naive eq-wt, forward)", "sleeve": "crypto-bybit",
"venue": "bybit-perp",
"tier": "deploy", "execution": "paper",
"tier": "research", "execution": "paper",
# min_forward_days 21 (not 14): this book's gains arrive in irregular clusters (median ~17 days between
# big +days), so a lumpy edge needs a slightly longer window to be representative before the gate rules.
"gate_spec": {"gate_type": "reconciliation", "min_forward_days": 21},

View File

@@ -1,16 +1,21 @@
"""Task 4 — `honest_allocation_inputs`: the deploy-strategy -> honest-reference series + capacity ceiling
provider. Uses an in-memory sqlite `TimescaleFeatureStore` seeded like the fidelity test's fixture (flat
cross-sectional funding for xsfunding + a per-coin long_ratio/price wiggle for positioning), so BOTH crypto
sleeves are computable WITHOUT an unlock calendar. Asserts the crypto-only deploy mapping, the int->str
key seam, and that the positioning and bybit_4edge series share a common string key (so `deploy_book_returns`
aligns them)."""
sleeves are computable WITHOUT an unlock calendar. Asserts the crypto-sleeve-only deploy mapping — now the
THREE individual sleeves (positioning/xsfunding/unlock), NOT the `bybit_4edge` book, which per the
2026-07-16 spec (`docs/superpowers/specs/2026-07-16-deploy-fund-config-knobs-design.md` §4d) moved to a
research-tier reference aggregate and is no longer an allocation target — the int->str key seam, and that
positioning and xsfunding series share a common string key (so `deploy_book_returns` can align them)."""
from __future__ import annotations
import datetime as dt
import math
from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore
from fxhnt.application.allocation_honest_inputs import honest_allocation_inputs
from fxhnt.application.allocation_honest_inputs import (
_CRYPTO_DEPLOY_SIDS,
honest_allocation_inputs,
)
_DAY = 86_400
# Base the fixture at a REAL calendar date (2026-01-01) so the epoch-day keys convert to ISO dates in the
@@ -45,20 +50,43 @@ def _seed(store: TimescaleFeatureStore, *, days: int = 120) -> None:
store.write_features(sym, rows)
def _seed_with_unlock(store: TimescaleFeatureStore, *, days: int = 120) -> list[dict]:
"""As `_seed`, plus a BTCUSDT hedge instrument (flat/neutral: zero funding, flat basis, so it barely
perturbs the xsfunding cross-section) so the `unlock` sleeve is ALSO computable — `UnlockShortRunner`
needs a BTCUSDT hedge leg (`market="BTCUSDT"`, the default) — and one supply-shock-eligible unlock-cliff
event on AAA (mirrors the shape used by `tests/integration/test_bybit_paper_reconciliation.py`). Returns
the `unlock_events` list the caller passes to `honest_allocation_inputs`."""
_seed(store, days=days)
price = 50_000.0
rows = []
for d in range(days):
price *= 1.0 + 0.0005 * math.sin(0.1 * d)
rows.append(((_BASE_DAY + d) * _DAY, {
"close": price, "spot_close": price, "funding": 0.0,
"high": price, "low": price, "turnover": 1.0e12,
}))
store.write_features("BTCUSDT", rows)
# Cliff near the end of the sample so the 10-day pre-window is inside it; n large enough that the
# supply-shock (n*price / warehouse-qvol-fallback 1e7) clears the runner's min_shock (0.1).
cliff = _BASE_DAY + days - 5
return [{"sym": "AAA", "cliff_day": cliff, "n": 5_000_000.0, "cat": "insiders"}]
def test_honest_allocation_inputs_crypto_only_mapping_and_key_seam() -> None:
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
_seed(store)
returns, ceilings = honest_allocation_inputs(
store, spread_bps={}, unlock_events=[],
deploy_sids={"positioning", "bybit_4edge", "multistrat"},
deploy_sids={"positioning", "xsfunding", "bybit_4edge", "multistrat"},
target_aum=35_000,
aums=[35_000, 100_000, 350_000, 1_000_000, 10_000_000],
)
store.close()
# 1. crypto-only series: multistrat (equity) has NO crypto series.
assert set(returns) == {"positioning", "bybit_4edge"}
# 1. crypto-sleeve-only series: `bybit_4edge` (research-tier reference aggregate, no longer a crypto
# deploy sid) and `multistrat` (equity) get NO series in the allocation path.
assert set(returns) == {"positioning", "xsfunding"}
# 2. every series is non-empty and ISO-DATE-STRING-keyed (the epoch-day int -> ISO date seam): each key
# contains "-", parses via date.fromisoformat, and falls in the 2020-2027 window (proves the epoch base
@@ -70,18 +98,45 @@ def test_honest_allocation_inputs_crypto_only_mapping_and_key_seam() -> None:
parsed = dt.date.fromisoformat(k)
assert dt.date(2020, 1, 1) <= parsed <= dt.date(2027, 12, 31), f"{sid} key out of window: {k}"
# 3. the equity default ceiling for the non-crypto deploy sid.
assert "multistrat" in ceilings and ceilings["multistrat"] == 10_000_000.0
# 3. non-crypto sids AND the demoted book both get the equity default ceiling (no honest series to size
# a real ceiling from).
for sid in ("multistrat", "bybit_4edge"):
assert ceilings[sid] == 10_000_000.0
# 4. each crypto sid has a ceiling (a numeric AUM-or-None; the ceiling inherits the AUM key's type, so
# with the int aums passed here it is an int — with non-binding capacity the honest Sharpe stays above
# min_sharpe at every AUM, so the ceiling is the top of the range).
for sid in ("positioning", "bybit_4edge"):
for sid in ("positioning", "xsfunding"):
assert sid in ceilings
assert ceilings[sid] is None or isinstance(ceilings[sid], (int, float))
# 5. KEY ALIGNMENT: positioning and bybit_4edge share >= 1 common string key, so deploy_book_returns
# will cross-align them (the consistent str(epoch_day) conversion across strategies).
common = set(returns["positioning"]) & set(returns["bybit_4edge"])
assert common, "positioning and bybit_4edge share no common key -> deploy_book_returns cannot align them"
# 5. KEY ALIGNMENT: positioning and xsfunding share >= 1 common string key, so deploy_book_returns
# will cross-align them (the consistent epoch-day -> ISO conversion across strategies).
common = set(returns["positioning"]) & set(returns["xsfunding"])
assert common, "positioning and xsfunding share no common key -> deploy_book_returns cannot align them"
assert all(isinstance(k, str) and dt.date.fromisoformat(k) for k in common)
def test_crypto_deploy_sids_are_the_three_sleeves() -> None:
assert _CRYPTO_DEPLOY_SIDS == frozenset({"positioning", "xsfunding", "unlock"})
def test_bybit_4edge_is_not_a_crypto_deploy_sid() -> None:
assert "bybit_4edge" not in _CRYPTO_DEPLOY_SIDS
def test_xsfunding_and_unlock_get_a_series_not_equity_default() -> None:
store = TimescaleFeatureStore("sqlite://", table="bybit_features")
events = _seed_with_unlock(store)
returns, ceilings = honest_allocation_inputs(
store, spread_bps={}, unlock_events=events,
deploy_sids={"xsfunding", "unlock"}, target_aum=35_000.0, aums=[35_000.0, 350_000.0],
)
store.close()
assert "xsfunding" in returns and returns["xsfunding"] # sleeve-sized, ISO-keyed
assert "unlock" in returns and returns["unlock"]
# ceilings present (not the equity default 10M sentinel for these crypto sleeves)
assert ceilings["xsfunding"] != 10_000_000.0
assert ceilings["unlock"] != 10_000_000.0

View File

@@ -1,11 +1,16 @@
"""The registry is the cockpit SSOT: every entry declares a `tier` (deploy|research) and `execution`
(paper|live). The Bybit deploy book + the positioning sleeve are the only `deploy` entries; everything else
is `research`. All trackers are `paper` today (no live execution yet)."""
(paper|live). `positioning` is the only `deploy` entry; everything else is `research`. All trackers are
`paper` today (no live execution yet).
2026-07-16 (spec `2026-07-16-deploy-fund-config-knobs-design.md` §4d, Task 4): `bybit_4edge` moved
deploy->research (a tracked reference aggregate, no longer an allocation target — it double-counted
`positioning`). The crypto allocation now runs per-sleeve: `positioning` + any of `xsfunding`/`unlock` whose
own reconciliation gate PASSes (`promote_on_pass`)."""
from __future__ import annotations
from fxhnt.registry import STRATEGY_REGISTRY
_DEPLOY = {"bybit_4edge", "positioning"}
_DEPLOY = {"positioning"}
def test_every_entry_has_tier_and_execution() -> None:
@@ -14,7 +19,7 @@ def test_every_entry_has_tier_and_execution() -> None:
assert e.get("execution") in {"paper", "live"}, f"{sid} has no valid execution"
def test_deploy_tier_is_exactly_the_bybit_book_and_positioning() -> None:
def test_deploy_tier_is_exactly_positioning() -> None:
deploy = {sid for sid, e in STRATEGY_REGISTRY.items() if e["tier"] == "deploy"}
assert deploy == _DEPLOY