s-ttp commited on
Commit
efc4a07
·
verified ·
1 Parent(s): 8911b14

quality round 3: adaptive name cap, REIT/FFO + GBp fixes, context-fit candidates, lens retry, percent-safe narrative, yield units

Browse files
Files changed (1) hide show
  1. src/orchestrator.py +46 -6
src/orchestrator.py CHANGED
@@ -89,7 +89,7 @@ def _enrich_candidates(cands: list[dict], k: int = 8) -> None:
89
  try:
90
  d = fetch_holding(c["ticker"])
91
  c["live"] = {kk: d.get(kk) for kk in
92
- ("name", "price", "sma200", "beta", "pe", "rev_growth")}
93
  c["flags"] = M.trap_flags(d) # same guards holdings get (value trap, inorganic EPS…)
94
  price, sma = d.get("price"), d.get("sma200")
95
  c["trend_ok"] = bool(price and (not sma or price >= sma))
@@ -108,6 +108,36 @@ def _enrich_candidates(cands: list[dict], k: int = 8) -> None:
108
  list(ex.map(one, cands[:k]))
109
 
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  _RISK_CAPS = {"conservative": (0.10, 0.25), "balanced": (0.12, 0.30), "aggressive": (0.15, 0.40)}
112
 
113
 
@@ -188,6 +218,14 @@ def run_analysis(rows: list[dict], emit, context: dict | None = None) -> dict:
188
  h["dcf"] = dict(h["dcf"], note="fund — n/a")
189
  elif isinstance(eps, (int, float)) and eps == eps:
190
  h["dcf"] = dict(h["dcf"], note="loss-making")
 
 
 
 
 
 
 
 
191
  continue
192
  A, src = assumptions.get(t), "ai"
193
  if not isinstance(A, dict):
@@ -213,11 +251,12 @@ def run_analysis(rows: list[dict], emit, context: dict | None = None) -> dict:
213
  list(pm["sector_allocation"].items())[:6]) + ". "
214
  f"Market regime: {'risk-on (S&P above 200-day)' if regime else 'risk-off (S&P below 200-day)' if regime is not None else 'unknown'}. "
215
  f"Portfolio beta ~{pm.get('realized_beta') or pm.get('weighted_beta') or 'n/a'}.")
216
- done_chips = []
217
 
218
  def chip_progress(k, ok):
219
- done_chips.append(k)
220
- emit(45 + 5 * len(done_chips), f"Examination: {len(done_chips)}/5 analysts have ruled…",
 
221
  chip=k if ok else None)
222
 
223
  panel = P.run_panel(briefs, panel_ctx, progress=chip_progress)
@@ -238,7 +277,7 @@ def run_analysis(rows: list[dict], emit, context: dict | None = None) -> dict:
238
  for h in holdings],
239
  "metrics": {k: v for k, v in pm.items() if k != "equity_curve"},
240
  "risk_scorecard": cards,
241
- "healthy_bands": {"per_name_cap": M.MAX_POS_W,
242
  "sector_band": [M.SECTOR_CAP_LO, M.SECTOR_CAP_HI]},
243
  "investor_context": context or "not provided — judge the portfolio on its own terms",
244
  })
@@ -248,7 +287,8 @@ def run_analysis(rows: list[dict], emit, context: dict | None = None) -> dict:
248
  gaps = (strat or {}).get("gaps") or []
249
  candidates = _alt_candidates(holdings, gaps if isinstance(gaps, list) else [])
250
  emit(80, "Verifying candidate ideas against live data…")
251
- _enrich_candidates(candidates)
 
252
  if has_llm():
253
  emit(84, "The Chief Investment Officer drafts the verdict…", chip="cio")
254
  cio = P.cio_synthesis({
 
89
  try:
90
  d = fetch_holding(c["ticker"])
91
  c["live"] = {kk: d.get(kk) for kk in
92
+ ("name", "price", "sma200", "beta", "pe", "rev_growth", "div_yield")}
93
  c["flags"] = M.trap_flags(d) # same guards holdings get (value trap, inorganic EPS…)
94
  price, sma = d.get("price"), d.get("sma200")
95
  c["trend_ok"] = bool(price and (not sma or price >= sma))
 
108
  list(ex.map(one, cands[:k]))
109
 
110
 
111
+ def _fit_context(cands: list[dict], context: dict | None) -> list[dict]:
112
+ """Stable re-order: live-verified candidates that FIT the investor's stated needs go
113
+ first (income → real dividend payers; conservative → low beta). Nothing is removed,
114
+ but the switch pairs and the CIO now see profile-appropriate names before the rest —
115
+ an income investor was being told to rotate REITs into zero-yield growth stocks."""
116
+ if not context:
117
+ return cands
118
+ want_income = "income" in str(context.get("objective") or "").lower()
119
+ conservative = str(context.get("risk") or "").lower() == "conservative"
120
+ if not (want_income or conservative):
121
+ return cands
122
+
123
+ def fits(c: dict) -> bool:
124
+ live = c.get("live") or {}
125
+ ok = True
126
+ if want_income:
127
+ dy = live.get("div_yield") # always percent (normalized at fetch)
128
+ ok = ok and isinstance(dy, (int, float)) and dy >= 2.5
129
+ if conservative:
130
+ b = live.get("beta")
131
+ ok = ok and isinstance(b, (int, float)) and b <= 1.1
132
+ return ok
133
+
134
+ fit = [c for c in cands if c.get("live") and fits(c)]
135
+ rest = [c for c in cands if c not in fit]
136
+ for c in fit:
137
+ c["signals"] = c.get("signals", []) + ["fits your stated profile"]
138
+ return fit + rest
139
+
140
+
141
  _RISK_CAPS = {"conservative": (0.10, 0.25), "balanced": (0.12, 0.30), "aggressive": (0.15, 0.40)}
142
 
143
 
 
218
  h["dcf"] = dict(h["dcf"], note="fund — n/a")
219
  elif isinstance(eps, (int, float)) and eps == eps:
220
  h["dcf"] = dict(h["dcf"], note="loss-making")
221
+ else:
222
+ h["dcf"] = dict(h["dcf"], note="no earnings data")
223
+ continue
224
+ if "REIT" in str(d.get("industry") or "").upper():
225
+ # REIT EPS is crushed by depreciation on appreciating property; the industry
226
+ # values on FFO (crucible valuation_eps) — an EPS-DCF would scream "75%
227
+ # overvalued" at every healthy REIT, so it is not run at all
228
+ h["dcf"] = dict(h["dcf"], note="REIT — valued on FFO, not EPS")
229
  continue
230
  A, src = assumptions.get(t), "ai"
231
  if not isinstance(A, dict):
 
251
  list(pm["sector_allocation"].items())[:6]) + ". "
252
  f"Market regime: {'risk-on (S&P above 200-day)' if regime else 'risk-off (S&P below 200-day)' if regime is not None else 'unknown'}. "
253
  f"Portfolio beta ~{pm.get('realized_beta') or pm.get('weighted_beta') or 'n/a'}.")
254
+ done_chips: set = set()
255
 
256
  def chip_progress(k, ok):
257
+ done_chips.add(k) # a retried lens reports twice — count it once
258
+ n_done = min(len(done_chips), 5)
259
+ emit(45 + 5 * n_done, f"Examination: {n_done}/5 analysts have ruled…",
260
  chip=k if ok else None)
261
 
262
  panel = P.run_panel(briefs, panel_ctx, progress=chip_progress)
 
277
  for h in holdings],
278
  "metrics": {k: v for k, v in pm.items() if k != "equity_curve"},
279
  "risk_scorecard": cards,
280
+ "healthy_bands": {"per_name_cap": M.effective_name_cap(len(holdings)),
281
  "sector_band": [M.SECTOR_CAP_LO, M.SECTOR_CAP_HI]},
282
  "investor_context": context or "not provided — judge the portfolio on its own terms",
283
  })
 
287
  gaps = (strat or {}).get("gaps") or []
288
  candidates = _alt_candidates(holdings, gaps if isinstance(gaps, list) else [])
289
  emit(80, "Verifying candidate ideas against live data…")
290
+ _enrich_candidates(candidates, k=12 if context else 8)
291
+ candidates = _fit_context(candidates, context)
292
  if has_llm():
293
  emit(84, "The Chief Investment Officer drafts the verdict…", chip="cio")
294
  cio = P.cio_synthesis({