Files
fxhnt/tests/unit/test_equity_allocation_inputs.py
2026-07-16 12:49:30 +02:00

497 lines
27 KiB
Python

"""Task 3 — `equity_allocation_inputs`: the equity analog of `honest_allocation_inputs`, wiring multistrat's
cost-netted `book_series` + real-ADV capacity curve into the allocation engine.
Step-1 finding (see module docstring in `equity_allocation_inputs.py`): `multistrat.book_series` returns an
ISO-date-string-keyed `list[tuple[str, float]]` (drop-in for `book_ret_by_day`), but `multistrat.target_weights`
returns ONLY the latest day's `{sym: weight}` snapshot — there is no weights HISTORY. `_weights_by_day`
reconstructs one by replaying `target_weights` on closes truncated day-by-day.
Also covers `first_crossing_ceiling` — the non-monotone-safe ceiling rule used for equity capacity (NOT
`allocation_engine.capacity_ceiling_from_curve`'s largest-viable-AUM rule, which would over-state the
ceiling on a curve that dips below `min_sharpe` and then ticks back up at extreme AUM).
TASK 2 (2026-07-16 UCITS re-base plan) additions: `regime="us"|"ucits"` sources each sleeve's ADV + spread
from the ACTUAL traded instrument per regime (see docs/superpowers/plans/2026-07-16-equity-ucits-rebase.md).
The pre-existing tests above are updated to pass `regime="us"` explicitly (they were written against the
US-only plumbing pre-regime; the FakeRepo fixtures they use only seed US-keyed volumes)."""
from __future__ import annotations
import datetime as dt
import math
from types import SimpleNamespace
import pytest
from fxhnt.application.equity_allocation_inputs import (
_tradeable_weight_fraction,
_weights_by_day,
equity_allocation_inputs,
first_crossing_ceiling,
)
from fxhnt.application.equity_honest import dollar_adv, equity_capacity_curve
from fxhnt.application.honest_gates import sharpe_of
from fxhnt.domain.strategies.multistrat import FUND_INSTRUMENTS, book_series
_BASE = dt.date(2026, 1, 1)
class _FakeRepo:
"""Minimal stand-in for `ForwardNavRepo` exposing the PIT reads the provider needs. TASK 2 adds
`ibkr_pit_volumes` as a SEPARATE store from `yahoo_pit_volumes` -- the `volume_source` flag switches
WHICH of the two tables the ucits branch reads, so the fake must expose two independently-seedable
tables (`volumes` for yahoo, `ibkr_volumes` for ibkr), not one shared dict."""
def __init__(self, closes: dict[str, dict[str, float]], volumes: dict[str, dict[str, float]],
ibkr_volumes: dict[str, dict[str, float]] | None = None) -> None:
self._closes = closes
self._volumes = volumes
self._ibkr_volumes = ibkr_volumes or {}
def yahoo_pit_closes(self, symbol: str) -> dict[str, float]:
return dict(self._closes.get(symbol, {}))
def yahoo_pit_volumes(self, symbol: str) -> dict[str, float]:
return dict(self._volumes.get(symbol, {}))
def ibkr_pit_volumes(self, symbol: str) -> dict[str, float]:
return dict(self._ibkr_volumes.get(symbol, {}))
def _synthetic_closes_and_volumes(days: int = 120, *, with_volumes: bool = True
) -> tuple[dict[str, dict[str, float]], dict[str, dict[str, float]]]:
"""Deterministic per-symbol price/volume paths, real enough (positive prices, sine-wave variation) that
multistrat's book_series/target_weights produce non-trivial output, and dollar-ADV huge relative to the
tested AUMs so capacity never binds (isolating the plumbing from capacity saturation). `with_volumes=False`
omits the volume snapshot entirely (simulating an empty/never-backfilled `yahoo_pit_volumes` store)."""
closes: dict[str, dict[str, float]] = {}
volumes: dict[str, dict[str, float]] = {}
for j, sym in enumerate(FUND_INSTRUMENTS):
c: dict[str, float] = {}
v: dict[str, float] = {}
price = 100.0 + 10.0 * j
for d in range(days):
date = (_BASE + dt.timedelta(days=d)).isoformat()
price *= 1.0 + 0.004 * math.sin(0.05 * d + j)
c[date] = price
if with_volumes:
v[date] = 5_000_000.0 + 200_000.0 * math.sin(0.07 * d + j)
closes[sym] = c
volumes[sym] = v
return closes, volumes
def _good_book_closes_and_volumes(days: int = 150, *, drift: float = 0.0004, noise: float = 0.002
) -> tuple[dict[str, dict[str, float]], dict[str, dict[str, float]]]:
"""A synthetic price path with a clear, low-noise positive drift so the resulting book_series/
target_weights book has an ANNUALIZED Sharpe well above the default `capacity_min_sharpe=1.0` (measured
~20 at these parameters) -- proving the ceiling math on a genuinely good book, not just isolating
plumbing from the modeled Sharpe's sign (`min_sharpe=-10` in the other tests here)."""
closes: dict[str, dict[str, float]] = {}
volumes: dict[str, dict[str, float]] = {}
for j, sym in enumerate(FUND_INSTRUMENTS):
c: dict[str, float] = {}
v: dict[str, float] = {}
price = 100.0 + 10.0 * j
for d in range(days):
date = (_BASE + dt.timedelta(days=d)).isoformat()
price *= 1.0 + drift + noise * math.sin(0.31 * d + j)
c[date] = price
v[date] = 5_000_000.0 + 200_000.0 * math.sin(0.07 * d + j)
closes[sym] = c
volumes[sym] = v
return closes, volumes
def test_equity_allocation_inputs_produces_series_and_finite_ceiling():
closes, volumes = _synthetic_closes_and_volumes(days=130)
repo = _FakeRepo(closes, volumes)
returns, ceilings = equity_allocation_inputs(
repo, {"multistrat"}, target_aum=100_000.0,
aums=[100_000, 350_000, 1_000_000, 3_000_000, 10_000_000],
min_sharpe=-10.0, # isolate plumbing from the modeled Sharpe's sign
regime="us", # this test's FakeRepo only seeds US-keyed volumes (written pre-regime)
)
assert "multistrat" in returns
series = returns["multistrat"]
assert len(series) >= 60
for k in series:
assert isinstance(k, str) and dt.date.fromisoformat(k)
assert ceilings["multistrat"] is not None
assert ceilings["multistrat"] == 10_000_000.0 # every grid AUM clears min_sharpe=-10 -> largest AUM
def test_equity_allocation_inputs_sizes_multistrat_under_the_default_min_sharpe():
"""Whole-branch-review fix: the ceiling curve used to be built with `per_period_sharpe` (raw daily
mean/std, NO annualization) and compared against `min_sharpe` on the ANNUALIZED (x sqrt(365)) scale --
~19x too small, so under the DEFAULT policy (`capacity_min_sharpe=1.0`) the curve was ALWAYS < 1.0,
`first_crossing_ceiling` broke on the very first grid AUM, and multistrat was capped to $0 regardless of
how good the book was. This test uses the DEFAULT threshold (not the -10 the other tests here use to
isolate plumbing) with a genuinely good synthetic book (measured annualized Sharpe ~20 well above 1.0)
and full ADV coverage -- proving the fixed units actually let multistrat size > $0."""
closes, volumes = _good_book_closes_and_volumes()
repo = _FakeRepo(closes, volumes)
returns, ceilings = equity_allocation_inputs(
repo, {"multistrat"}, target_aum=100_000.0,
aums=[100_000, 350_000, 1_000_000, 3_000_000, 10_000_000],
min_sharpe=1.0, # the production default (`AllocationPolicy.capacity_min_sharpe`)
regime="us", # this test's FakeRepo only seeds US-keyed volumes (written pre-regime)
)
assert "multistrat" in returns
assert len(returns["multistrat"]) > 0
# a non-empty, non-zero ceiling: either a finite AUM > 0, or None (== unbounded) -- NEVER 0.0, which
# would mean `target_capital` caps multistrat's dollars to $0 (the exact bug this test guards against).
ceiling = ceilings["multistrat"]
assert ceiling is None or ceiling > 0.0
def test_equity_allocation_inputs_insufficient_adv_coverage_falls_back():
"""Whole-branch-review BUG 2: before `backfill-etf-volume` runs (or if the nightly volume snapshot keeps
failing -- swallow-and-warn), `yahoo_pit_volumes` is empty. The provider's pre-fix guard only checked
`book_ret_by_day` (derived from closes, never volumes), so an empty ADV store still emitted a full-length
series where every day is scaled to zero capacity and books `-cost` -- a GARBAGE series that (being
non-empty) would be used INSTEAD of the nav fallback. The fixed provider must instead emit NO series (and
ceiling `None`), so `record_allocation`'s `honest_returns.get(sid) or nav_fallback` falls back to nav.
(Still true under the partial-sizing `_tradeable_weight_fraction` guard: with NO volumes at all, every
sleeve is dead, so tradeable weight is ~0.0 -- well below `_MIN_TRADEABLE_WEIGHT` -- a fully-degenerate
store, not a single-thin-sleeve partial-coverage case.)"""
closes, volumes = _synthetic_closes_and_volumes(days=130, with_volumes=False)
repo = _FakeRepo(closes, volumes)
returns, ceilings = equity_allocation_inputs(
repo, {"multistrat"}, target_aum=100_000.0,
aums=[100_000, 350_000, 1_000_000, 3_000_000, 10_000_000],
min_sharpe=1.0,
regime="us", # this test's FakeRepo only seeds US-keyed volumes (written pre-regime)
)
assert "multistrat" not in returns
assert ceilings["multistrat"] is None
def _ucits_map(*, half_spread_bps: float = 5.0, ticker_suffix: str = "_UCITS.L"
) -> dict[str, SimpleNamespace]:
"""A minimal stand-in for `settings.ucits.map` — only the two attributes
`equity_allocation_inputs` reads (`.yahoo_ticker`, `.half_spread_bps`)."""
return {sym: SimpleNamespace(yahoo_ticker=f"{sym}{ticker_suffix}", half_spread_bps=half_spread_bps)
for sym in FUND_INSTRUMENTS}
def test_equity_allocation_inputs_regime_us_is_byte_identical_to_pre_regime_output():
"""TASK 2 regression (Global Constraint: byte-identity of regime="us"): independently reconstructs the
EXACT pre-Task-2 computation (US-only ADV via `dollar_adv`/`equity_capacity_curve` with `spread_bps=None`
-> the module-constant `ETF_HALF_SPREAD_BPS` fallback -- the only code path that existed before this
task) in THIS test process, then asserts `equity_allocation_inputs(..., regime="us")` reproduces it
EXACTLY (`==`, not `math.isclose`). Deliberately NOT hardcoded literal floats: the cost loop's
`set(cur) | set(prev)` iterates in PYTHONHASHSEED-dependent order, so the exact float bits are only
reproducible WITHIN one process, not across separate interpreter invocations -- comparing two
same-process computations is the correct byte-identity check, a golden literal captured from a
different process is not."""
closes, volumes = _good_book_closes_and_volumes()
repo = _FakeRepo(closes, volumes)
aums = [100_000, 350_000, 1_000_000, 3_000_000, 10_000_000]
us_closes = {sym: repo.yahoo_pit_closes(sym) for sym in FUND_INSTRUMENTS}
us_volumes = {sym: repo.yahoo_pit_volumes(sym) for sym in FUND_INSTRUMENTS}
book_ret_by_day = dict(book_series(us_closes, FUND_INSTRUMENTS, drop_incomplete_today=False))
weights_by_day = _weights_by_day(us_closes, FUND_INSTRUMENTS)
adv = dollar_adv(us_closes, us_volumes)
all_aums = sorted({100_000.0, *(float(a) for a in aums)})
curve = equity_capacity_curve(book_ret_by_day, weights_by_day, adv, aums=all_aums, price_by_day=us_closes)
expected_series = curve[100_000.0]
expected_sharpe = {float(a): sharpe_of(curve[float(a)]) for a in aums}
expected_ceiling = first_crossing_ceiling(expected_sharpe, 1.0)
assert len(expected_series) >= 60 # sanity: this really is the good book, not an empty fixture
returns, ceilings = equity_allocation_inputs(
repo, {"multistrat"}, target_aum=100_000.0, aums=aums, min_sharpe=1.0, regime="us",
)
assert returns["multistrat"] == expected_series
assert ceilings["multistrat"] == expected_ceiling
def test_equity_allocation_inputs_ucits_ceiling_materially_lower_than_us_when_ucits_volume_thin():
"""TASK 2: with a HIGH US volume (deep US-ETF liquidity) but a LOW (thin, not absent) UCITS volume for
every sleeve, the `ucits` regime's capacity ceiling must be MATERIALLY lower than the `us` regime's --
proving the re-base actually re-sources capacity onto the thinner UCITS lines rather than silently
keeping the US ADV. Both regimes read the SAME closes/book -- only the ADV source differs."""
closes, us_volumes = _good_book_closes_and_volumes() # HIGH US volume (~5,000,000 shares/day)
ucits_map = _ucits_map(half_spread_bps=5.0)
# LOW UCITS volume: 1/10th of the US volume for every sleeve, every day -- thin but never absent (every
# day > 0, so the ADV-coverage guard still PASSES; see the thin/absent test below for the guard itself
# tripping). Measured: this alone drops the ceiling from the US grid's max ($10M) to $1M -- an order of
# magnitude, well within the plan's expected $0.5-3M UCITS-real band -- while a 1/500th cut collapses it
# all the way to $0 (still "materially lower" but less illustrative of a partial, non-degenerate ceiling).
repo_volumes = dict(us_volumes)
for sym in FUND_INSTRUMENTS:
repo_volumes[ucits_map[sym].yahoo_ticker] = {d: v / 10.0 for d, v in us_volumes[sym].items()}
repo = _FakeRepo(closes, repo_volumes)
aums = [100_000, 350_000, 1_000_000, 3_000_000, 10_000_000]
_, ceilings_us = equity_allocation_inputs(
repo, {"multistrat"}, target_aum=100_000.0, aums=aums, min_sharpe=1.0, regime="us")
_, ceilings_ucits = equity_allocation_inputs(
repo, {"multistrat"}, target_aum=100_000.0, aums=aums, min_sharpe=1.0,
regime="ucits", ucits_map=ucits_map)
ceiling_us = ceilings_us["multistrat"]
ceiling_ucits = ceilings_ucits["multistrat"]
assert ceiling_us == 10_000_000.0 # deep US liquidity clears min_sharpe at every grid AUM
assert ceiling_ucits is not None
# materially lower -- at least an order of magnitude below the US ceiling (proves the re-base bites,
# not just a marginal wobble from the different spread) -- AND non-zero, a genuine partial ceiling, not
# a full collapse (the coverage-guard test below covers the "no capacity at all" case).
assert 0.0 < ceiling_ucits <= ceiling_us / 10.0
def test_equity_allocation_inputs_ucits_volume_source_yahoo_is_byte_identical_to_direct_yahoo_read():
"""TASK 2 Global Constraint (byte-identity of `volume_source="yahoo"`, the default): independently
reconstructs the ucits-regime computation using `repo.yahoo_pit_volumes` directly (the ONLY code path
that existed before this task), then asserts `equity_allocation_inputs(..., volume_source="yahoo")`
reproduces it EXACTLY (`==`) -- proving the new flag is additive for the default, not a behavior change."""
closes, us_volumes = _good_book_closes_and_volumes()
ucits_map = _ucits_map(half_spread_bps=5.0)
repo_volumes = dict(us_volumes)
for sym in FUND_INSTRUMENTS:
repo_volumes[ucits_map[sym].yahoo_ticker] = {d: v / 10.0 for d, v in us_volumes[sym].items()}
repo = _FakeRepo(closes, repo_volumes)
aums = [100_000, 350_000, 1_000_000, 3_000_000, 10_000_000]
ucits_closes = {sym: repo.yahoo_pit_closes(sym) for sym in FUND_INSTRUMENTS}
book_ret_by_day = dict(book_series(ucits_closes, FUND_INSTRUMENTS, drop_incomplete_today=False))
weights_by_day = _weights_by_day(ucits_closes, FUND_INSTRUMENTS)
ucits_volumes = {sym: repo.yahoo_pit_volumes(ucits_map[sym].yahoo_ticker) for sym in FUND_INSTRUMENTS}
adv = dollar_adv(ucits_closes, ucits_volumes)
spread = {sym: ucits_map[sym].half_spread_bps for sym in FUND_INSTRUMENTS}
all_aums = sorted({100_000.0, *(float(a) for a in aums)})
curve = equity_capacity_curve(book_ret_by_day, weights_by_day, adv, aums=all_aums,
price_by_day=ucits_closes, spread_bps=spread)
expected_series = curve[100_000.0]
expected_sharpe = {float(a): sharpe_of(curve[float(a)]) for a in aums}
expected_ceiling = first_crossing_ceiling(expected_sharpe, 1.0)
assert len(expected_series) >= 60 # sanity: this really is the good book, not an empty fixture
returns, ceilings = equity_allocation_inputs(
repo, {"multistrat"}, target_aum=100_000.0, aums=aums, min_sharpe=1.0,
regime="ucits", ucits_map=ucits_map, volume_source="yahoo",
)
assert returns["multistrat"] == expected_series
assert ceilings["multistrat"] == expected_ceiling
def test_equity_allocation_inputs_ucits_volume_source_ibkr_reads_ibkr_pit_volumes_not_yahoo():
"""TASK 2: `volume_source="ibkr"` must read `repo.ibkr_pit_volumes`, NOT `yahoo_pit_volumes` -- seed the
fake repo with MATERIALLY DIFFERENT yahoo vs ibkr volume for the same UCITS ticker (yahoo 1/10th of US
volume -- thin but a real, non-collapsing ceiling per the test above; ibkr 1/500th -- thin enough to
collapse capacity to $0, per the measured ratios already established in the `us`-vs-`ucits` test above)
and assert the resulting ceiling actually DIFFERS by source, proving the flag switches which table is
read rather than being accepted and silently ignored."""
closes, us_volumes = _good_book_closes_and_volumes()
ucits_map = _ucits_map(half_spread_bps=5.0)
yahoo_volumes = dict(us_volumes)
for sym in FUND_INSTRUMENTS:
yahoo_volumes[ucits_map[sym].yahoo_ticker] = {d: v / 10.0 for d, v in us_volumes[sym].items()}
ibkr_volumes = {ucits_map[sym].yahoo_ticker: {d: v / 500.0 for d, v in us_volumes[sym].items()}
for sym in FUND_INSTRUMENTS}
repo = _FakeRepo(closes, yahoo_volumes, ibkr_volumes=ibkr_volumes)
aums = [100_000, 350_000, 1_000_000, 3_000_000, 10_000_000]
_, ceilings_yahoo = equity_allocation_inputs(
repo, {"multistrat"}, target_aum=100_000.0, aums=aums, min_sharpe=1.0,
regime="ucits", ucits_map=ucits_map, volume_source="yahoo")
_, ceilings_ibkr = equity_allocation_inputs(
repo, {"multistrat"}, target_aum=100_000.0, aums=aums, min_sharpe=1.0,
regime="ucits", ucits_map=ucits_map, volume_source="ibkr")
ceiling_yahoo = ceilings_yahoo["multistrat"]
ceiling_ibkr = ceilings_ibkr["multistrat"]
assert ceiling_yahoo is not None and ceiling_yahoo > 0.0
# ibkr's volume is 50x thinner than yahoo's here -> the ibkr-sourced ceiling must be materially lower
# (None or 0.0 both count as "lower" -- a full capacity collapse is still proof the switch bit).
assert ceiling_ibkr is None or ceiling_ibkr < ceiling_yahoo
def test_equity_allocation_inputs_unknown_volume_source_raises_instead_of_silently_defaulting():
"""`volume_source` is a plain string, so a typo must NOT silently fall into either read path -- fail
loud, mirroring the existing `regime` guard."""
closes, volumes = _synthetic_closes_and_volumes(days=120)
repo = _FakeRepo(closes, volumes)
with pytest.raises(ValueError, match="unknown ucits_volume_source"):
equity_allocation_inputs(repo, {"multistrat"}, target_aum=100_000.0, aums=[100_000],
regime="ucits", volume_source="bogus")
def test_equity_allocation_inputs_ucits_one_dead_sleeve_still_emits_partial_sizing():
"""PARTIAL-SIZING (weight-based guard): an ABSENT UCITS line for exactly ONE sleeve (e.g. DBMF.L -- Yahoo
lacks the new/small UCITS listing, or the LSE volume snapshot hasn't backfilled it yet) must NOT drop the
whole book. `dollar_adv` on an empty volume dict yields an empty ADV series for DBMF -> every traded day
holding DBMF has that ONE sleeve's weight untradeable, but the other four (most of the book's gross
weight) remain tradeable -> `_tradeable_weight_fraction` averages to ~0.8, comfortably ABOVE
`_MIN_TRADEABLE_WEIGHT` (0.5) -> the provider still EMITS a series (the dead DBMF sleeve gets scaled to
~0 by `equity_honest_returns`'s per-sleeve participation cap instead — "use as much as there is"), unlike
the prior all-or-nothing guard which would have zeroed the entire book over one dead sleeve."""
closes, us_volumes = _good_book_closes_and_volumes()
ucits_map = _ucits_map(half_spread_bps=30.0)
repo_volumes = dict(us_volumes)
for sym in FUND_INSTRUMENTS:
# every UCITS line present and liquid EXCEPT DBMF, which Yahoo has no data for at all (empty dict).
repo_volumes[ucits_map[sym].yahoo_ticker] = (
{} if sym == "DBMF" else {d: v / 10.0 for d, v in us_volumes[sym].items()}
)
repo = _FakeRepo(closes, repo_volumes)
returns, ceilings = equity_allocation_inputs(
repo, {"multistrat"}, target_aum=100_000.0,
aums=[100_000, 350_000, 1_000_000, 3_000_000, 10_000_000],
min_sharpe=1.0, regime="ucits", ucits_map=ucits_map,
)
assert "multistrat" in returns
assert len(returns["multistrat"]) > 0
assert ceilings["multistrat"] is None or ceilings["multistrat"] >= 0.0
def test_equity_allocation_inputs_ucits_all_sleeves_dead_falls_back_to_nav():
"""DEGENERATE case preserved: with EVERY UCITS line absent (an empty/pre-backfill volume store), the
tradeable-weight fraction is ~0.0 -- well below `_MIN_TRADEABLE_WEIGHT` -- so the provider must still emit
NO series (ceiling `None`, nav fallback), same as the pre-existing US-side insufficient-coverage test.
This is the genuinely degenerate case the guard exists for, distinct from the one-dead-sleeve partial
case above."""
closes, us_volumes = _good_book_closes_and_volumes()
ucits_map = _ucits_map(half_spread_bps=30.0)
repo_volumes = dict(us_volumes)
for sym in FUND_INSTRUMENTS:
repo_volumes[ucits_map[sym].yahoo_ticker] = {} # every UCITS line absent
repo = _FakeRepo(closes, repo_volumes)
returns, ceilings = equity_allocation_inputs(
repo, {"multistrat"}, target_aum=100_000.0,
aums=[100_000, 350_000, 1_000_000, 3_000_000, 10_000_000],
min_sharpe=1.0, regime="ucits", ucits_map=ucits_map,
)
assert "multistrat" not in returns
assert ceilings["multistrat"] is None
def test_tradeable_weight_fraction_one_dead_sleeve_is_about_point_eight():
"""Direct unit test of the weight-based guard's core metric: 5 equally-weighted (20% each) sleeves held
every day, ONE of which (E) has zero ADV every day it's held -- the tradeable fraction must be the
tradeable GROSS WEIGHT share (4/5 = 0.8), not 0 (the prior all-symbols-covered check would have failed
every day outright)."""
weights_by_day = {
"2026-01-01": {"A": 0.2, "B": 0.2, "C": 0.2, "D": 0.2, "E": 0.2},
"2026-01-02": {"A": 0.2, "B": 0.2, "C": 0.2, "D": 0.2, "E": 0.2},
}
adv = {
"A": {"2026-01-01": 1_000.0, "2026-01-02": 1_000.0},
"B": {"2026-01-01": 1_000.0, "2026-01-02": 1_000.0},
"C": {"2026-01-01": 1_000.0, "2026-01-02": 1_000.0},
"D": {"2026-01-01": 1_000.0, "2026-01-02": 1_000.0},
"E": {"2026-01-01": 0.0, "2026-01-02": 0.0}, # dead sleeve, every day
}
assert _tradeable_weight_fraction(weights_by_day, adv) == pytest.approx(0.8)
def test_tradeable_weight_fraction_skips_empty_weight_rows_and_handles_no_traded_days():
# A day with no weights (pre-warmup) is skipped, not counted as untradeable.
weights_by_day = {"2026-01-01": {}, "2026-01-02": {"A": 0.5, "B": 0.5}}
adv = {"A": {"2026-01-02": 1_000.0}, "B": {"2026-01-02": 1_000.0}}
assert _tradeable_weight_fraction(weights_by_day, adv) == pytest.approx(1.0)
# No traded days at all -> 0.0 (the safe default), not vacuously 1.0.
assert _tradeable_weight_fraction({}, {}) == 0.0
assert _tradeable_weight_fraction({"2026-01-01": {}}, {}) == 0.0
def test_equity_allocation_inputs_empty_deploy_sids_is_noop():
closes, volumes = _synthetic_closes_and_volumes(days=120)
repo = _FakeRepo(closes, volumes)
returns, ceilings = equity_allocation_inputs(repo, set(), target_aum=100_000.0, aums=[100_000, 1_000_000])
assert returns == {}
assert ceilings == {}
def test_equity_allocation_inputs_absent_pit_data_gives_no_series_and_none_ceiling():
repo = _FakeRepo({sym: {} for sym in FUND_INSTRUMENTS}, {sym: {} for sym in FUND_INSTRUMENTS})
returns, ceilings = equity_allocation_inputs(repo, {"multistrat"}, target_aum=100_000.0, aums=[100_000])
assert "multistrat" not in returns
assert ceilings["multistrat"] is None
def test_equity_allocation_inputs_short_pit_history_gives_no_series():
# Only 10 days -- well short of the ~66-day warmup book_series/target_weights need.
closes, volumes = _synthetic_closes_and_volumes(days=10)
repo = _FakeRepo(closes, volumes)
returns, ceilings = equity_allocation_inputs(repo, {"multistrat"}, target_aum=100_000.0, aums=[100_000])
assert "multistrat" not in returns
assert ceilings["multistrat"] is None
def test_equity_allocation_inputs_unknown_regime_raises_instead_of_silently_defaulting_to_us():
"""`regime` is a plain string, so a typo like `"US"` (wrong case) must NOT silently fall into the
aggressive `us` branch (deep US-liquidity capacity model) -- dangerous in the EU retail default. Fail
loud instead."""
closes, volumes = _synthetic_closes_and_volumes(days=120)
repo = _FakeRepo(closes, volumes)
with pytest.raises(ValueError, match="unknown equity_execution_regime"):
equity_allocation_inputs(repo, {"multistrat"}, target_aum=100_000.0, aums=[100_000], regime="US")
@pytest.mark.parametrize("regime", ["ucits", "us"])
def test_equity_allocation_inputs_valid_regimes_are_accepted(regime):
closes, volumes = _synthetic_closes_and_volumes(days=120)
repo = _FakeRepo(closes, volumes)
ucits_map = {
sym: SimpleNamespace(yahoo_ticker=sym, half_spread_bps=10.0) for sym in FUND_INSTRUMENTS
}
# Must not raise -- both real regime values are accepted.
equity_allocation_inputs(repo, {"multistrat"}, target_aum=100_000.0, aums=[100_000],
regime=regime, ucits_map=ucits_map)
def test_first_crossing_ceiling_empty_curve_is_none():
assert first_crossing_ceiling({}, 1.0) is None
def test_first_crossing_ceiling_all_viable_picks_largest():
curve = {100_000.0: 1.5, 350_000.0: 1.3, 1_000_000.0: 1.1}
assert first_crossing_ceiling(curve, 1.0) == 1_000_000.0
def test_first_crossing_ceiling_stops_at_first_dip_not_largest_viable():
"""The critical robustness fix: a curve that dips below min_sharpe and then ticks BACK UP at extreme AUM
(the equity scale_hit approximation's known non-monotone tail) must NOT report the later, spuriously
viable AUM as the ceiling -- it must stop at the AUM just before the first dip."""
curve = {
100_000.0: 1.5,
350_000.0: 1.2,
1_000_000.0: 0.5, # first dip below min_sharpe=1.0
3_000_000.0: 0.3,
10_000_000.0: 1.8, # ticks back up -- capacity_ceiling_from_curve would (wrongly) pick this
}
ceiling = first_crossing_ceiling(curve, 1.0)
assert ceiling == 350_000.0
assert ceiling != 10_000_000.0
def test_first_crossing_ceiling_none_viable_is_zero():
curve = {100_000.0: 0.2, 350_000.0: 0.1}
assert first_crossing_ceiling(curve, 1.0) == 0.0