diff --git a/src/fxhnt/adapters/orchestration/assets.py b/src/fxhnt/adapters/orchestration/assets.py index 7d679e9..9b213c3 100644 --- a/src/fxhnt/adapters/orchestration/assets.py +++ b/src/fxhnt/adapters/orchestration/assets.py @@ -1070,7 +1070,15 @@ def cockpit_forward(context: AssetExecutionContext) -> None: # does and pass them to record_allocation (capacity-aware, stable backtest series). GRACEFUL: any failure # here (warehouse unreachable, missing spread SSOT, etc.) must NEVER crash the nightly — fall back to the # nav_history path (store=None, today's behavior). Store closed in a finally. - equity = get_settings().paper_capital + import dataclasses + + from fxhnt.application.allocation_policy import resolve_allocation_policy + from fxhnt.application.fund_config import read_fund_config + + _cfg = read_fund_config(dsn) + equity = _cfg.capital + _policy = dataclasses.replace(resolve_allocation_policy(get_settings()), target_vol=_cfg.target_vol, + kelly_fraction=_cfg.kelly_fraction) try: from fxhnt.adapters.persistence.paper_repo import PaperRepo from fxhnt.adapters.warehouse.timescale_feature_store import TimescaleFeatureStore @@ -1090,11 +1098,12 @@ def cockpit_forward(context: AssetExecutionContext) -> None: unlock_events = [] spread_bps = PaperRepo(s.operational_dsn).read_real_spread_by_coin() allocated = record_allocation(dsn, equity=equity, store=bounded, - spread_bps=spread_bps, unlock_events=unlock_events) + spread_bps=spread_bps, unlock_events=unlock_events, policy=_policy) finally: raw.close() except Exception as e: # noqa: BLE001 — honest sizing is best-effort; nightly must never crash on it context.log.warning( f"cockpit_forward: honest-reference store unavailable, falling back to nav_history sizing: {e}") - allocated = record_allocation(dsn, equity=equity) # deploy-set is fresh (B2a) — nav_history fallback + # deploy-set is fresh (B2a) — nav_history fallback + allocated = record_allocation(dsn, equity=equity, policy=_policy) context.log.info(f"cockpit_forward: allocated {len(allocated)} strategies (promoted: {promoted or 'none'})") diff --git a/src/fxhnt/application/allocation_ingest.py b/src/fxhnt/application/allocation_ingest.py index a06c4cd..6778ba4 100644 --- a/src/fxhnt/application/allocation_ingest.py +++ b/src/fxhnt/application/allocation_ingest.py @@ -32,8 +32,14 @@ from fxhnt.registry import STRATEGY_REGISTRY _log = logging.getLogger(__name__) +def _effective_policy(policy: object | None): + """`policy=None` resolves to TODAY's exact default (`resolve_allocation_policy(get_settings())`) — the + single choke point `record_allocation` must call through so `policy=None` stays byte-identical.""" + return resolve_allocation_policy(get_settings()) if policy is None else policy + + def record_allocation(dsn: str, equity: float, *, store=None, spread_bps=None, - unlock_events=None) -> dict[str, float]: + unlock_events=None, policy=None) -> dict[str, float]: repo = ForwardNavRepo(dsn) repo.migrate() at = dt.datetime.now(dt.UTC).replace(tzinfo=None) @@ -62,7 +68,7 @@ def record_allocation(dsn: str, equity: float, *, store=None, spread_bps=None, if defunded: _log.warning("record_allocation: de-funded (sustained decay): %s", defunded) - policy = resolve_allocation_policy(get_settings()) + pol = _effective_policy(policy) if store is not None: # Honest-reference sizing: stable, capacity-aware backtest series + per-strategy dollar ceilings for # the crypto deploy set (equity sids get no series -> caller's nav fallback + the equity ceiling). @@ -70,8 +76,8 @@ def record_allocation(dsn: str, equity: float, *, store=None, spread_bps=None, store, spread_bps or {}, unlock_events or [], deploy_sids=funded_sids, target_aum=equity, aums=[35_000, 100_000, 350_000, 1_000_000, 10_000_000], - min_sharpe=getattr(policy, "capacity_min_sharpe", 1.0), - equity_default_ceiling=getattr(policy, "equity_default_ceiling", 10_000_000.0)) + min_sharpe=getattr(pol, "capacity_min_sharpe", 1.0), + equity_default_ceiling=getattr(pol, "equity_default_ceiling", 10_000_000.0)) else: honest_returns, ceilings = {}, {} # honest backtest series where available (stable, capacity-aware), else the forward nav record (fallback) @@ -84,10 +90,10 @@ def record_allocation(dsn: str, equity: float, *, store=None, spread_bps=None, {sid: {r.date: r.ret for r in nav_rows[sid]} for sid in funded_sids} if store is not None else None ) - weights = compute_allocation(returns, policy, killswitch_returns=killswitch_returns) - dollars = target_capital(weights, equity, policy, capacity_ceiling=ceilings) + weights = compute_allocation(returns, pol, killswitch_returns=killswitch_returns) + dollars = target_capital(weights, equity, pol, capacity_ceiling=ceilings) - phash = allocation_policy_hash(policy, ALLOCATION_POLICY_VERSION) + phash = allocation_policy_hash(pol, ALLOCATION_POLICY_VERSION) alloc_rows = [StrategyAllocationRow(strategy_id=sid, target_weight=weights[sid], target_dollars=dollars.get(sid, 0.0), policy_version=ALLOCATION_POLICY_VERSION, policy_hash=phash, @@ -104,5 +110,5 @@ def record_allocation(dsn: str, equity: float, *, store=None, spread_bps=None, _log.info("record_allocation: %d/%d strategies funded+allocated (gross $%.0f, ceiling $%.0f)", len(weights), len(deploy_sids), sum(abs(v) for v in dollars.values()), - policy.capital_ceiling) + pol.capital_ceiling) return dollars diff --git a/tests/unit/test_record_allocation_policy.py b/tests/unit/test_record_allocation_policy.py new file mode 100644 index 0000000..2f07b04 --- /dev/null +++ b/tests/unit/test_record_allocation_policy.py @@ -0,0 +1,18 @@ +import dataclasses + +from fxhnt.application.allocation_policy import resolve_allocation_policy +from fxhnt.config import get_settings + + +def test_policy_none_matches_resolved_default(monkeypatch): + # record_allocation(policy=None) must build the SAME policy resolve_allocation_policy(get_settings()) + # returns today. Assert via the helper the code will use: a None arg resolves to that same call. + from fxhnt.application import allocation_ingest as ai + resolved = ai._effective_policy(None) # helper introduced in Step 3 + assert resolved == resolve_allocation_policy(get_settings()) + + +def test_policy_passthrough(): + from fxhnt.application import allocation_ingest as ai + p = dataclasses.replace(resolve_allocation_policy(get_settings()), target_vol=0.20, kelly_fraction=0.5) + assert ai._effective_policy(p) is p