Stock-picker plans never hold an index fund unless the profile is cautious or a loss tolerance is stated
Browse files- README.md +4 -0
- 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 |
-
|
|
|
|
|
|
|
| 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",
|