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

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
Files changed (3) hide show
  1. src/orchestrator.py +6 -1
  2. src/personas.py +12 -9
  3. 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="low"))
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