feat(fund-config): nightly allocation reads fund_config for equity + policy (target_vol/kelly)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-07-16 07:30:34 +02:00
parent d2353ced52
commit c1d1cb80fb
3 changed files with 44 additions and 11 deletions

View File

@@ -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'})")

View File

@@ -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

View File

@@ -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