# Equity Capacity/Cost UCITS Re-base Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: superpowers:subagent-driven-development. Checkbox (`- [ ]`) steps.

**Goal:** Re-base the equity leg's capacity ceiling + cost model onto the UCITS lines the EU account actually trades (CSPX/IDTM/IGLN/ICOM/DBMF-LU) instead of the US ETFs — with a regime flag so it switches back to the deep US-ETF liquidity once the fund elects MiFID-II professional status (~€500k). Returns (`book_series` on US underlyings) are unchanged — only ADV/spread inputs are re-sourced.

**Architecture:** The equity honest-reference already exists (`equity_honest.py`, `equity_allocation_inputs.py`) sourcing ADV from US-ETF volume + US spreads. This adds: UCITS volume ingestion (Yahoo LSE tickers), per-line UCITS spreads, and a `equity_execution_regime` config (`ucits` default | `us`). `equity_allocation_inputs` sources each US sleeve's ADV + spread from the regime's instrument (US ticker's own data, or its UCITS-mapped ticker's) while `book_series`/`target_weights` stay US.

**Tech Stack:** Python 3.12, SQLAlchemy 2.0, typer, pytest. Spec context: `docs/superpowers/specs/2026-07-16-equity-leg-deployable-design.md` (this refines §4a/§4b for EU/UCITS reality).

## Global Constraints

- The US→UCITS map is `settings.ucits.map` (`config.py`): `SPY→CSPX (IE00B5BMR087)`, `IEF→IDTM (IE00B1FZS798)`, `GLD→IGLN (IE00B4ND3602)`, `PDBC→ICOM (IE00BDFL4P12)`, `DBMF→DBMF (LU2951555585)`.
- **Yahoo LSE tickers** for UCITS volume: `<ticker>.L` (CSPX.L, IDTM.L, IGLN.L, ICOM.L, DBMF.L). Per-symbol fetch failures are isolated (Task-1-of-the-prior-feature pattern) — a missing/thin line (esp. DBMF-UCITS) must NOT break ingestion.
- **UCITS per-line half-spread (bps), documented estimates** (wider than US): `CSPX 1.5, IDTM 2.0, IGLN 2.0, ICOM 8.0, DBMF 30.0` (DBMF-UCITS is new/small/thin). Default 10.0.
- **Regime flag** `equity_execution_regime: "ucits" | "us"`, DEFAULT `"ucits"` (the fund is retail EU today). Switching to `"us"` is a deliberate operator action AFTER electing MiFID-II professional status (~€500k portfolio + trading frequency) — NEVER auto-switched on AUM.
- **Returns unchanged**: `book_series`/`target_weights` always use US closes (the edge proxy holds in both regimes; UCITS track the US underlyings). Only `adv_usd` + `spread_bps` fed to `equity_honest_returns` change by regime.
- **Byte-identity of the `us` regime**: with `equity_execution_regime="us"`, the equity model is byte-identical to today (US ADV + `ETF_HALF_SPREAD_BPS`). The re-base only changes behavior under the (new default) `ucits` regime.
- **ADV-coverage guard (from the prior fix)** still applies — if the UCITS volume is absent/thin (< coverage threshold), the sid emits NO series → nav fallback (a thin DBMF-UCITS must not fabricate capacity).
- Commit trailer: `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>`.

---

### Task 1: UCITS volume ingestion + config (spreads, yahoo ticker, regime flag)

**Files:**
- Modify: `src/fxhnt/config.py` (`UcitsListing.yahoo_ticker` + `.half_spread_bps`; `equity_execution_regime`)
- Modify: `src/fxhnt/cli.py` (`backfill-etf-volume` also ingests the UCITS LSE tickers)
- Modify: `src/fxhnt/adapters/orchestration/assets.py` (nightly snapshots UCITS volume too)
- Test: `tests/unit/test_ucits_volume_config.py`

**Interfaces:**
- Produces: `UcitsListing.yahoo_ticker` (default `f"{ticker}.L"`) + `half_spread_bps`; `settings.<...>.equity_execution_regime`.

- [ ] **Step 1: Failing tests** — the UCITS map exposes a `yahoo_ticker` (`CSPX.L`, `DBMF.L`, ...) and a `half_spread_bps` per line; `equity_execution_regime` defaults to `"ucits"`; an env override sets it to `"us"`.
- [ ] **Step 2: Run — expect FAIL.**
- [ ] **Step 3: Implement.**
  - `config.py` `UcitsListing`: add `yahoo_ticker: str = ""` (post-init/validator: default to `f"{ticker}.L"` when empty) and `half_spread_bps: float`. Populate the map's 5 entries with the Global-Constraints spread values (CSPX 1.5, IDTM 2.0, IGLN 2.0, ICOM 8.0, DBMF 30.0). DBMF's yahoo_ticker may need to be `"DBMF.L"` — set it explicitly; if Yahoo lacks it the ingestion skips gracefully.
  - Add `equity_execution_regime: str = "ucits"` to a settings group (recommend `AllocationSettings`, env `FXHNT_ALLOCATION_EQUITY_EXECUTION_REGIME`) with a comment: `ucits` (retail EU, default) | `us` (after electing MiFID-II professional, ~€500k) — sets which instruments' ADV/spread the equity capacity+cost model uses; returns stay US.
  - `cli.py` `backfill-etf-volume`: after the US `FUND_INSTRUMENTS` loop, ALSO iterate `settings.ucits.map.values()` and `YahooDailyClient(rng="25y").volumes(listing.yahoo_ticker)` → `repo.snapshot_yahoo_volumes(listing.yahoo_ticker, ...)`. Per-symbol try/except (a thin UCITS line that Yahoo lacks is logged + skipped, not fatal). Echo counts.
  - `assets.py` nightly: in the same volume-snapshot loop, also snapshot each UCITS `yahoo_ticker` (guarded like the US volume snapshot).
- [ ] **Step 4: Run — expect PASS.**
- [ ] **Step 5: Commit** (`feat(equity): ingest UCITS LSE volume + regime flag + UCITS spread config`).

---

### Task 2: regime-aware equity ADV/spread sourcing

**Files:**
- Modify: `src/fxhnt/application/equity_allocation_inputs.py`
- Test: `tests/unit/test_equity_allocation_inputs.py` (extend)

**Interfaces:**
- `equity_allocation_inputs(repo, deploy_sids, *, target_aum, aums, min_sharpe=1.0, regime="ucits", ucits_map=None, us_spread_bps=None, ucits_spread_bps=None)` — new `regime` + injectable maps (default from settings). Returns unchanged shape.

- [ ] **Step 1: Failing tests.**
  - regime="us": `adv_usd[sym]` sourced from `yahoo_pit_volumes(sym)` (US) + spread `ETF_HALF_SPREAD_BPS[sym]` — byte-identical to current behavior (regression assertion against the pre-regime output).
  - regime="ucits": `adv_usd[sym]` sourced from the UCITS-mapped ticker's `yahoo_pit_volumes(ucits_ticker)` (remapped to the US sleeve key so `equity_honest_returns`'s US-keyed weights line up) + spread = the per-line UCITS spread. With a seeded LOW UCITS volume and a HIGH US volume, the `ucits` ceiling must be MATERIALLY LOWER than the `us` ceiling (proves the re-base bites).
  - regime="ucits" with a thin/absent UCITS line (e.g. DBMF.L empty) → that sleeve's ADV is absent → coverage guard trips or that sleeve is capacity-scaled — verify it does NOT fabricate capacity (no series if coverage < threshold).
- [ ] **Step 2: Run — expect FAIL.**
- [ ] **Step 3: Implement.**
  - In `equity_allocation_inputs`, build the per-sleeve ADV + spread from the regime:
    - `us`: `adv_source_ticker[sym] = sym`; `spread[sym] = (us_spread_bps or ETF_HALF_SPREAD_BPS).get(sym, default)`.
    - `ucits`: `adv_source_ticker[sym] = ucits_map[sym].yahoo_ticker`; `spread[sym] = ucits_map[sym].half_spread_bps`.
  - Load `volumes_by_source = {sym: repo.yahoo_pit_volumes(adv_source_ticker[sym])}`; `adv = dollar_adv({sym: us_closes[sym]}, {sym: volumes_by_source[sym]})` — **KEY**: `dollar_adv` needs price×volume; use the US close as the price proxy for the USD-denominated UCITS line (the USD UCITS lines track the US underlying's USD price closely; document this — a small basis, acceptable for a capacity estimate; a follow-on could use the real UCITS price). So `adv_usd` is keyed by the US sleeve `sym` but its VOLUME comes from the UCITS line. Spread passed as the regime spread dict.
  - Pass `spread_bps=<regime spread dict>` into `equity_honest_returns`/`equity_capacity_curve` (add a `spread_bps` param there if it currently hardcodes `ETF_HALF_SPREAD_BPS` — check Task-2-of-the-prior-feature `equity_honest.py`; if `_spread_bps(sym)` reads the module constant, thread an optional `spread_bps: dict|None` override so the regime dict wins). Do NOT mutate the module constant.
  - Defaults: `regime = settings...equity_execution_regime`, `ucits_map = settings.ucits.map`, spreads from config — resolved at the call in `record_allocation` (pass them in) OR defaulted inside via `get_settings()`; keep it injectable for tests.
- [ ] **Step 4: `record_allocation`** passes `regime=getattr(pol/settings, ...)` (thread the regime from settings; the equity block already exists). Confirm the `us`-regime path stays byte-identical.
- [ ] **Step 5: Run — expect PASS** (unit + the byte-identity regression + full allocation tests).
- [ ] **Step 6: Commit** (`feat(equity): regime-aware ADV/spread (ucits default | us) for the capacity+cost model`).

---

### Task 3: full-suite green

- [ ] **Step 1:** `.venv/bin/python3 -m pytest tests/unit -q` → all pass.
- [ ] **Step 2:** `.venv/bin/python3 -m pytest tests/integration -k "allocation or equity or yahoo or multistrat" -q` → all pass.
- [ ] **Step 3:** commit any fixes.

## Post-implementation (controller-run, in-cluster)
Deploy (git-sync) + `fxhnt migrate-cockpit` + `fxhnt backfill-etf-volume` (now also fetches the UCITS LSE lines). Then re-run the equity gates UNDER `equity_execution_regime=ucits`: report the UCITS-real ceiling (expect MATERIALLY below the US ≥$10M — likely $0.5–3M, DBMF/ICOM-bound) + which UCITS line binds + the net Sharpe under UCITS spreads (expect below the 1.58 US estimate). Confirm DBMF.L coverage (if Yahoo lacks it, the sleeve falls to the coverage guard). Contrast with `regime=us` to show the professional-status upside. NOTE: run LIGHT (equity provider only — do NOT re-run the 2× crypto-book path that OOM-killed the daemon; keep in-cluster diagnostics to one honest path at a time).

## Self-Review
- Byte-identity: `regime="us"` reproduces today's equity model (regression test).
- Returns unchanged: only adv/spread inputs re-sourced; `book_series`/`target_weights` untouched.
- Thin-line safety: ADV-coverage guard + per-symbol ingestion isolation protect against a missing/thin DBMF-UCITS.
- Documented approximation: US close as the price proxy for the USD UCITS line's dollar-ADV (small basis; follow-on = real UCITS price).
