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:
@@ -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'})")
|
||||
|
||||
@@ -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
|
||||
|
||||
18
tests/unit/test_record_allocation_policy.py
Normal file
18
tests/unit/test_record_allocation_policy.py
Normal 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
|
||||
Reference in New Issue
Block a user