#!/usr/bin/env python3 """STATE_VECTOR_CONSTRUCTOR_V1 — spec/formulas/domains/portfolio.yaml. Merges holdings/cash/tax_lots/sector_graph/factor_exposures/macro_regime_probabilities into one state_vector. Missing components stay null -- never backfilled from another component. governance/todo/v8_9_p3_adoption_plan.yaml P3-A. """ from __future__ import annotations import argparse import json from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DEFAULT_CASH_LADDER = ROOT / "Temp" / "cash_ratios_v1.json" DEFAULT_POSITIONS = ROOT / "Temp" / "account_snapshot_v1.json" DEFAULT_SECTOR_GRAPH = ROOT / "Temp" / "sector_exposure_graph_v1.json" DEFAULT_OUT = ROOT / "Temp" / "state_vector_constructor_v1.json" COMPONENT_KEYS = [ "cash_ladder", "positions", "sector_exposure_graph", "factor_exposures", "tax_lots", "risk_bucket_weights", "macro_regime_probabilities", "goal_progress_pct", ] def _load(path: Path) -> dict: if not path.exists(): return {} try: data = json.loads(path.read_text(encoding="utf-8")) return data if isinstance(data, dict) else {} except Exception: return {} def construct_state_vector(components: dict) -> dict: state_vector = {} missing_components = [] for key in COMPONENT_KEYS: value = components.get(key) state_vector[key] = value if value is None: missing_components.append(key) completeness_pct = 100.0 * (len(COMPONENT_KEYS) - len(missing_components)) / len(COMPONENT_KEYS) return { "state_vector": state_vector, "state_vector_completeness_pct": completeness_pct, "missing_components": missing_components, } def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--cash-ladder", default=str(DEFAULT_CASH_LADDER)) ap.add_argument("--positions", default=str(DEFAULT_POSITIONS)) ap.add_argument("--sector-graph", default=str(DEFAULT_SECTOR_GRAPH)) ap.add_argument("--out", default=str(DEFAULT_OUT)) args = ap.parse_args() cash_ladder_doc = _load(Path(args.cash_ladder)) positions_doc = _load(Path(args.positions)) sector_graph_doc = _load(Path(args.sector_graph)) components = { "cash_ladder": cash_ladder_doc or None, "positions": positions_doc.get("positions") if positions_doc else None, "sector_exposure_graph": sector_graph_doc.get("rows") if sector_graph_doc.get("rows") else None, "factor_exposures": None, "tax_lots": positions_doc.get("tax_lots") if positions_doc else None, "risk_bucket_weights": None, "macro_regime_probabilities": None, "goal_progress_pct": positions_doc.get("goal_progress_pct") if positions_doc else None, } result = {"formula_id": "STATE_VECTOR_CONSTRUCTOR_V1", **construct_state_vector(components)} result["source_paths"] = [str(Path(args.cash_ladder)), str(Path(args.positions)), str(Path(args.sector_graph))] out = Path(args.out) out.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") print(json.dumps(result, ensure_ascii=False, indent=2)) return 0 if __name__ == "__main__": raise SystemExit(main())