Order plan never adds to hurdle-gated Keeps; sub-1% rule as final pass; overvalued candidates need a strong committee; narrative at medium effort
Browse files- src/orchestrator.py +6 -1
- src/personas.py +12 -9
- src/revival.py +1 -1
src/orchestrator.py
CHANGED
|
@@ -521,6 +521,11 @@ def run_analysis(rows: list[dict], emit, context: dict | None = None, budget_s:
|
|
| 521 |
if isinstance(b, (int, float)) and b > 1.15 and not c.get("instrument"):
|
| 522 |
c["fit"]["cautions"].append(f"beta {b:.2f} β too volatile for a conservative / short-horizon profile")
|
| 523 |
c["fit"]["score"] = round(c["fit"]["score"] - 0.6, 3)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
for c in scored: # trend: buying into a broken trend is disclosed and costs fit β ETFs included
|
| 525 |
if c.get("trend_ok") is False:
|
| 526 |
c["fit"]["cautions"].append("below its 200-day average β would be bought into a weak trend")
|
|
@@ -834,7 +839,7 @@ def run_analysis(rows: list[dict], emit, context: dict | None = None, budget_s:
|
|
| 834 |
dec = decisions[h["symbol"]]
|
| 835 |
flagged = bool((h["data"].get("us") or {}).get("flags"))
|
| 836 |
in_over = bool(over_sec) and str(h["data"].get("sector") or "") == str(over_sec)
|
| 837 |
-
dec["no_add"] = dec["action"] == "Keep" and (dec["conviction"] < 55 or flagged or in_over)
|
| 838 |
dec["protect"] = dec["action"] == "Keep" and str(h["data"].get("sector") or "") != str(over_sec or "")
|
| 839 |
emit_bg(88, "bounds/scenarios start")
|
| 840 |
loss_tol = M._tol_from_context(context)
|
|
|
|
| 521 |
if isinstance(b, (int, float)) and b > 1.15 and not c.get("instrument"):
|
| 522 |
c["fit"]["cautions"].append(f"beta {b:.2f} β too volatile for a conservative / short-horizon profile")
|
| 523 |
c["fit"]["score"] = round(c["fit"]["score"] - 0.6, 3)
|
| 524 |
+
for c in scored: # valuation: a name far above its DCF value needs a strong committee to be an add
|
| 525 |
+
up = c.get("upside")
|
| 526 |
+
if isinstance(up, (int, float)) and up < -0.25 and not c.get("instrument"):
|
| 527 |
+
c["fit"]["cautions"].append(f"price {abs(up) / (1 + up) * 100:.0f}% above DCF value β a diversification case only")
|
| 528 |
+
c["fit"]["score"] = round(c["fit"]["score"] - 0.15, 3)
|
| 529 |
for c in scored: # trend: buying into a broken trend is disclosed and costs fit β ETFs included
|
| 530 |
if c.get("trend_ok") is False:
|
| 531 |
c["fit"]["cautions"].append("below its 200-day average β would be bought into a weak trend")
|
|
|
|
| 839 |
dec = decisions[h["symbol"]]
|
| 840 |
flagged = bool((h["data"].get("us") or {}).get("flags"))
|
| 841 |
in_over = bool(over_sec) and str(h["data"].get("sector") or "") == str(over_sec)
|
| 842 |
+
dec["no_add"] = dec["action"] == "Keep" and (dec["conviction"] < 55 or flagged or in_over or dec.get("gated_add"))
|
| 843 |
dec["protect"] = dec["action"] == "Keep" and str(h["data"].get("sector") or "") != str(over_sec or "")
|
| 844 |
emit_bg(88, "bounds/scenarios start")
|
| 845 |
loss_tol = M._tol_from_context(context)
|
src/personas.py
CHANGED
|
@@ -540,14 +540,6 @@ def decide_actions(panel: dict[str, dict], tickers: list[str],
|
|
| 540 |
action = "Keep"
|
| 541 |
size_note = (f"conviction supports adding, but at {w * 100:.0f}% of the book the "
|
| 542 |
f"position is already past the {cap * 100:.0f}% healthy cap")
|
| 543 |
-
elif action == "Trim" and 0 < w < 0.01 and vals:
|
| 544 |
-
# a partial sale of a sub-1% position is tax friction, not risk management
|
| 545 |
-
if med < 40:
|
| 546 |
-
action = "Sell"
|
| 547 |
-
size_note = f"at {w * 100:.1f}% of the book a partial sale changes nothing β exit cleanly"
|
| 548 |
-
else:
|
| 549 |
-
action = "Keep"
|
| 550 |
-
size_note = f"at {w * 100:.1f}% of the book a trim is not worth the friction β keep or exit, not shave"
|
| 551 |
elif action == "Buy More" and sector_over and t in sector_over:
|
| 552 |
# adding to the diagnosed overweight sector deepens the book's main problem
|
| 553 |
action = "Keep"
|
|
@@ -577,7 +569,18 @@ def decide_actions(panel: dict[str, dict], tickers: list[str],
|
|
| 577 |
low = [k for k, v in scores.items() if v <= 35]
|
| 578 |
size_note = (f"cyclical at a peak β the {' and '.join(low)} lens{'es' if len(low) > 1 else ''} "
|
| 579 |
f"see real damage; trim rather than ride the cycle down")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 580 |
out[t] = {"action": action, "conviction": round(med), "dispersion": round(disp),
|
| 581 |
"votes_low": votes_low, "n_lenses": len(vals), "scores": scores,
|
| 582 |
-
"size_note": size_note}
|
| 583 |
return out
|
|
|
|
| 540 |
action = "Keep"
|
| 541 |
size_note = (f"conviction supports adding, but at {w * 100:.0f}% of the book the "
|
| 542 |
f"position is already past the {cap * 100:.0f}% healthy cap")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
elif action == "Buy More" and sector_over and t in sector_over:
|
| 544 |
# adding to the diagnosed overweight sector deepens the book's main problem
|
| 545 |
action = "Keep"
|
|
|
|
| 569 |
low = [k for k, v in scores.items() if v <= 35]
|
| 570 |
size_note = (f"cyclical at a peak β the {' and '.join(low)} lens{'es' if len(low) > 1 else ''} "
|
| 571 |
f"see real damage; trim rather than ride the cycle down")
|
| 572 |
+
gated_add = action == "Keep" and bool(size_note) and ("hurdle" in size_note or "deepens" in size_note
|
| 573 |
+
or "healthy cap" in size_note)
|
| 574 |
+
if action == "Trim" and 0 < w < 0.01 and vals:
|
| 575 |
+
# FINAL pass: a partial sale of a sub-1% position is tax friction, not risk
|
| 576 |
+
# management β whichever tilt produced the Trim
|
| 577 |
+
if med < 40:
|
| 578 |
+
action = "Sell"
|
| 579 |
+
size_note = f"at {w * 100:.1f}% of the book a partial sale changes nothing β exit cleanly"
|
| 580 |
+
else:
|
| 581 |
+
action = "Keep"
|
| 582 |
+
size_note = f"at {w * 100:.1f}% of the book a trim is not worth the friction β keep or exit, not shave"
|
| 583 |
out[t] = {"action": action, "conviction": round(med), "dispersion": round(disp),
|
| 584 |
"votes_low": votes_low, "n_lenses": len(vals), "scores": scores,
|
| 585 |
+
"size_note": size_note, "gated_add": gated_add}
|
| 586 |
return out
|
src/revival.py
CHANGED
|
@@ -360,7 +360,7 @@ def narrative(plan: dict) -> dict | None:
|
|
| 360 |
import json
|
| 361 |
for _ in range(2):
|
| 362 |
out = parse_json(call_llm(_PLAN_SYSTEM, json.dumps(plan, default=str),
|
| 363 |
-
max_tokens=1200, tier="standard", label="narrative", effort="
|
| 364 |
if isinstance(out, dict) and out.get("moves"):
|
| 365 |
return out
|
| 366 |
return None
|
|
|
|
| 360 |
import json
|
| 361 |
for _ in range(2):
|
| 362 |
out = parse_json(call_llm(_PLAN_SYSTEM, json.dumps(plan, default=str),
|
| 363 |
+
max_tokens=1200, tier="standard", label="narrative", effort="medium"))
|
| 364 |
if isinstance(out, dict) and out.get("moves"):
|
| 365 |
return out
|
| 366 |
return None
|