s-ttp commited on
Commit
352f18e
·
verified ·
1 Parent(s): e937493

Stock-picker plans never hold an index fund unless the profile is cautious or a loss tolerance is stated

Browse files
Files changed (2) hide show
  1. README.md +4 -0
  2. src/orchestrator.py +6 -2
README.md CHANGED
@@ -214,6 +214,10 @@ Recommendation discipline for every book with a usable DCF range:
214
  model applies, P/E vs own history). One-year performance is a ±3 tie-breaker only; a breached
215
  loss tolerance costs 8; the weakest pillar caps the total (one broken pillar cannot be
216
  outvoted by strong prospects).
 
 
 
 
217
  - **Stock-picker books get stock ideas** — a book of 12+ single names is treated as a
218
  stock-picker's: the CIO sees single names first, at least two alternatives are single stocks
219
  and at most one is an instrument; smaller books or de-risking needs still get instruments.
 
214
  model applies, P/E vs own history). One-year performance is a ±3 tie-breaker only; a breached
215
  loss tolerance costs 8; the weakest pillar caps the total (one broken pillar cannot be
216
  outvoted by strong prospects).
217
+ - **A stock-picker's plan never holds an index fund** — for a book of 12+ single names with no
218
+ cautious profile or stated loss tolerance, no ETF enters the candidate pool at all: sector
219
+ gaps, ballast and diversification are expressed through single names. A conservative profile,
220
+ a short horizon or a loss tolerance brings defensive instruments back.
221
  - **Stock-picker books get stock ideas** — a book of 12+ single names is treated as a
222
  stock-picker's: the CIO sees single names first, at least two alternatives are single stocks
223
  and at most one is an instrument; smaller books or de-risking needs still get instruments.
src/orchestrator.py CHANGED
@@ -413,6 +413,8 @@ def run_analysis(rows: list[dict], emit, context: dict | None = None, budget_s:
413
  pass
414
  cards += _risk_cards(risk, holdings, context)
415
  needs = M.diagnose_needs(pm, risk, holdings, context) # what THIS book needs — drives everything below
 
 
416
  _bg = cf.ThreadPoolExecutor(max_workers=2)
417
  strat_fut = _bg.submit(P.strategist_review, {
418
  "holdings": [{"ticker": h["symbol"], "sector": h["data"].get("sector"),
@@ -1080,8 +1082,10 @@ def _instrument_candidates(needs: dict, holdings: list[dict], risk: dict) -> lis
1080
  if needs.get("_cautious"): # a retiree's gap-filler is a defensive sector, never a tech fund
1081
  gaps = [g for g in gaps if g in ("Utilities", "Consumer Defensive", "Healthcare")]
1082
  fund_syms_local = {h["symbol"] for h in holdings if str(h["data"].get("quote_type") or "").upper() in M.FUND_TYPES}
1083
- if _stock_picker(holdings, fund_syms_local) and not needs.get("_cautious"):
1084
- gaps = [] # a stock-picker fills a sector gap with a stock, not a sector fund
 
 
1085
  for i in inst:
1086
  if i.get("asset_class") == "sector_etf" and i.get("sector") in gaps[:4] and i["ticker"] not in held:
1087
  out.append({"ticker": i["ticker"], "sector": i["sector"], "instrument": True, "need_target": "sector_gaps",
 
413
  pass
414
  cards += _risk_cards(risk, holdings, context)
415
  needs = M.diagnose_needs(pm, risk, holdings, context) # what THIS book needs — drives everything below
416
+ if M._tol_from_context(context) is not None:
417
+ needs["_loss_tol"] = {"weight": 0.0, "label": None} # marker: instruments allowed as ballast
418
  _bg = cf.ThreadPoolExecutor(max_workers=2)
419
  strat_fut = _bg.submit(P.strategist_review, {
420
  "holdings": [{"ticker": h["symbol"], "sector": h["data"].get("sector"),
 
1082
  if needs.get("_cautious"): # a retiree's gap-filler is a defensive sector, never a tech fund
1083
  gaps = [g for g in gaps if g in ("Utilities", "Consumer Defensive", "Healthcare")]
1084
  fund_syms_local = {h["symbol"] for h in holdings if str(h["data"].get("quote_type") or "").upper() in M.FUND_TYPES}
1085
+ if _stock_picker(holdings, fund_syms_local) and not needs.get("_cautious") and not needs.get("_loss_tol"):
1086
+ return [] # a stock-picker's plan never holds an index fund: gaps, ballast and
1087
+ # diversification are all expressed through single names (user rule);
1088
+ # only a cautious profile or a stated loss tolerance brings instruments back
1089
  for i in inst:
1090
  if i.get("asset_class") == "sector_etf" and i.get("sector") in gaps[:4] and i["ticker"] not in held:
1091
  out.append({"ticker": i["ticker"], "sector": i["sector"], "instrument": True, "need_target": "sector_gaps",