s-ttp commited on
Commit
955818e
·
verified ·
1 Parent(s): 259d435

Upload analyst.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. analyst.py +148 -10
analyst.py CHANGED
@@ -52,16 +52,49 @@ DEBATE_CLOSE = re.compile(r"Judge Round|\[4/5\]|── LLM Debate")
52
 
53
  # Per-stage result one-liners (label and result share a line via end=" ").
54
  # Each becomes a timeline milestone: {step, text, done}.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  MILESTONES = [
56
- ("data", re.compile(r"\[(\d+)/17\]\s*(.+?)\.\.\."), lambda m: f"{m.group(1)}/17 · {m.group(2).strip()}", False),
57
- ("features", re.compile(r"PIT fundamental features\.{3}\s*(.+)"), lambda m: m.group(1).strip(), True),
58
- ("tech", re.compile(r"Technical analysis \(yearly\)\.{3}\s*(.+)"), lambda m: m.group(1).strip(), True),
59
- ("fund", re.compile(r"Fundamental scorecard\.{3}\s*(.+)"), lambda m: m.group(1).strip(), True),
60
- ("sent", re.compile(r"Sentiment analysis\.{3}\s*(.+)"), lambda m: m.group(1).strip(), True),
61
- ("fundml", re.compile(r"Blended fair value:\s*(.+?)(?:\s{2,}|$)"), lambda m: f"fair value {m.group(1).strip()}", True),
62
- ("macro", re.compile(r"Macro analysis \(yearly \+ yield curve\)\.{3}\s*(.+)"), lambda m: m.group(1).strip(), True),
63
- ("ml", re.compile(r"ML ensemble \(fund\+macro features\)\.{3}\s*(.+)"), lambda m: m.group(1).strip(), True),
64
- ("screen", re.compile(r"composite screen:\s*(.+)"), lambda m: m.group(1).strip(), True),
65
  ("blend", re.compile(r"Quant blend:\s*(.+?)(?:\s{2,}|$)"), lambda m: m.group(1).strip(), True),
66
  ]
67
 
@@ -125,10 +158,18 @@ def _extract_debate(ld: dict) -> dict | None:
125
  "key_points": entry.get("key_points", []),
126
  }
127
 
 
 
 
 
 
128
  return {
129
  "recommendation": ld.get("recommendation"),
130
  "vote_summary": ld.get("vote_summary"),
131
- "panel_complete": ld.get("panel_complete", True), # absent (pre-fix report) → assume complete
 
 
 
132
  "bear_model": ld.get("bear_model"),
133
  "p_bear": ld.get("p_bear"),
134
  "p_bull": ld.get("p_bull"),
@@ -199,6 +240,24 @@ def _run(job: dict) -> None:
199
  job["confidence"] = report.get("confidence")
200
  job["debate"] = _extract_debate(report.get("llm_debate") or {})
201
  job["damodaran"] = report.get("damodaran")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  except (json.JSONDecodeError, KeyError, ValueError, OSError):
203
  pass
204
  finally:
@@ -330,12 +389,15 @@ def _save_report(job: dict) -> None:
330
  "confidence": job["confidence"], "rating": job["rating"],
331
  "debate": job["debate"], "damodaran": job["damodaran"],
332
  "n_lines": len(job.get("lines") or []),
 
 
333
  "cached_at": time.time(),
334
  "price_at_analysis": (job.get("damodaran") or {}).get("current_price"),
335
  "source": job.get("source", "user"),
336
  }
337
  f = _REPORTS_DIR / f"{job['ticker']}.json"
338
  f.write_text(json.dumps(payload))
 
339
  if _HF_TOKEN:
340
  try:
341
  from huggingface_hub import HfApi
@@ -347,6 +409,78 @@ def _save_report(job: dict) -> None:
347
  pass
348
 
349
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
350
  def _hydrate_reports() -> None:
351
  """On startup, pull recent reports from the dataset repo so the cache survives redeploys."""
352
  if not _HF_TOKEN:
@@ -522,5 +656,9 @@ def result(job_id: str) -> dict:
522
  "rating": job["rating"],
523
  "debate": job["debate"],
524
  "damodaran": job["damodaran"],
 
 
 
 
525
  "n_lines": len(job["lines"]),
526
  }
 
52
 
53
  # Per-stage result one-liners (label and result share a line via end=" ").
54
  # Each becomes a timeline milestone: {step, text, done}.
55
+ def _pct_word(txt):
56
+ """'+2.99% (score:16/32 conf:100%)' → '+3.0% expected · scorecard 16/32'."""
57
+ m = re.search(r"([+-]?\d+(?:\.\d+)?)%", txt)
58
+ return f"{float(m.group(1)):+.1f}% expected" if m else txt.strip()
59
+
60
+
61
+ def _ml_words(txt):
62
+ """'+10.73% (dir_acc:65% RankIC:0.183 OOS:60 MAE:13.9% feat:89)' →
63
+ '+10.7% expected · 65% direction accuracy out of sample · typical error ±14%'."""
64
+ r = re.search(r"([+-]?\d+(?:\.\d+)?)%", txt); da = re.search(r"dir_acc:(\d+)%", txt); mae = re.search(r"MAE:(\d+(?:\.\d+)?)%", txt)
65
+ parts = []
66
+ if r: parts.append(f"{float(r.group(1)):+.1f}% expected")
67
+ if da: parts.append(f"{da.group(1)}% direction accuracy out of sample")
68
+ if mae: parts.append(f"typical error ±{float(mae.group(1)):.0f}%")
69
+ if "low reliability" in txt: parts.append("low reliability — near-zero weight")
70
+ return " · ".join(parts) or txt.strip()
71
+
72
+
73
+ def _fund_words(txt):
74
+ r = re.search(r"([+-]?\d+(?:\.\d+)?)%", txt); sc = re.search(r"score:(\d+/\d+)", txt)
75
+ out = f"{float(r.group(1)):+.1f}% expected" if r else ""
76
+ if sc: out += f" · scorecard {sc.group(1)}"
77
+ return out or txt.strip()
78
+
79
+
80
+ def _data_words(m):
81
+ step = m.group(2).strip().lower()
82
+ nice = {"daily price history (10yr)": "10 years of daily prices", "company overview": "company profile",
83
+ "income statement": "income statements", "balance sheet": "balance sheets", "cash flow": "cash-flow statements",
84
+ "earnings": "earnings history", "news sentiment": "news & sentiment"}
85
+ return f"{m.group(1)} of 17 sources · {nice.get(step, step)}"
86
+
87
+
88
  MILESTONES = [
89
+ ("data", re.compile(r"\[(\d+)/17\]\s*(.+?)\.\.\."), _data_words, False),
90
+ ("features", re.compile(r"PIT fundamental features\.{3}\s*(.+)"), lambda m: "point-in-time features built — no look-ahead", True),
91
+ ("tech", re.compile(r"Technical analysis \(yearly\)\.{3}\s*(.+)"), lambda m: _pct_word(m.group(1)), True),
92
+ ("fund", re.compile(r"Fundamental scorecard\.{3}\s*(.+)"), lambda m: _fund_words(m.group(1)), True),
93
+ ("sent", re.compile(r"Sentiment analysis\.{3}\s*(.+)"), lambda m: _pct_word(m.group(1)), True),
94
+ ("fundml", re.compile(r"Blended fair value:\s*(.+?)(?:\s{2,}|$)"), lambda m: f"market composite {m.group(1).strip()}", True),
95
+ ("macro", re.compile(r"Macro analysis \(yearly \+ yield curve\)\.{3}\s*(.+)"), lambda m: ("yield curve normal" if "NORMAL" in m.group(1) else "yield curve inverted — caution") , True),
96
+ ("ml", re.compile(r"ML ensemble \(fund\+macro features\)\.{3}\s*(.+)"), lambda m: _ml_words(m.group(1)), True),
97
+ ("screen", re.compile(r"Panel-ML rank \(12m cross-sectional[^)]*\):\s*(.+)"), lambda m: re.sub(r"decile \d+/10 · ", "", m.group(1).split("(as-of")[0]).replace("exp ", "expected ").strip(), True),
98
  ("blend", re.compile(r"Quant blend:\s*(.+?)(?:\s{2,}|$)"), lambda m: m.group(1).strip(), True),
99
  ]
100
 
 
158
  "key_points": entry.get("key_points", []),
159
  }
160
 
161
+ _roles_tmp = {
162
+ "neutral": role("round1", "gemini"), "bull_initial": role("round1", "gpt"),
163
+ "bear_initial": role("round1", "kimi"), "pm": role("round2", "claude"),
164
+ }
165
+ _missing = [k for k in ("neutral", "bear_initial", "bull_initial", "pm") if not _roles_tmp.get(k)]
166
  return {
167
  "recommendation": ld.get("recommendation"),
168
  "vote_summary": ld.get("vote_summary"),
169
+ # complete only when EVERY seat produced a result (audit: AVGO's Neutral seat failed
170
+ # silently while the old flag — which only tracked Bull/Bear — still said complete)
171
+ "panel_complete": bool(ld.get("panel_complete", True)) and not _missing,
172
+ "missing_roles": _missing,
173
  "bear_model": ld.get("bear_model"),
174
  "p_bear": ld.get("p_bear"),
175
  "p_bull": ld.get("p_bull"),
 
240
  job["confidence"] = report.get("confidence")
241
  job["debate"] = _extract_debate(report.get("llm_debate") or {})
242
  job["damodaran"] = report.get("damodaran")
243
+ job["blend"] = report.get("blend_sources") or []
244
+ job["eps_gate"] = ((report.get("fundamental_detail") or {}).get("EPS Consensus Gate")
245
+ or (report.get("pred_fund") or {}).get("eps_consensus_gate"))
246
+ # Uncertainty band from the price model's OUT-OF-SAMPLE error (an 80% band =
247
+ # ±1.28 × MAE-as-sigma proxy), only when enough independent observations exist.
248
+ _ml = report.get("ml_detail") or {}
249
+ _mae = _ml.get("mae"); _neff = _ml.get("n_oos_eff") or 0
250
+ if _mae and _neff and _neff >= 3 and job["expected_return"] is not None:
251
+ _hw = round(1.28 * float(_mae) * 100, 1)
252
+ job["band"] = {"low": round(job["expected_return"] - _hw, 1),
253
+ "high": round(job["expected_return"] + _hw, 1),
254
+ "basis": f"80% band from the price model's out-of-sample error (MAE {float(_mae)*100:.0f}%, ~{int(_neff)} independent obs)"}
255
+ else:
256
+ job["band"] = {"low": None, "high": None,
257
+ "basis": "not estimable — too few independent out-of-sample observations"}
258
+ _c = job.get("confidence") or 0
259
+ job["confidence_label"] = ("high" if _c >= 0.65 else "moderate" if _c >= 0.30 else
260
+ "low" if _c >= 0.12 else "very low — treat the point estimate as indicative only")
261
  except (json.JSONDecodeError, KeyError, ValueError, OSError):
262
  pass
263
  finally:
 
389
  "confidence": job["confidence"], "rating": job["rating"],
390
  "debate": job["debate"], "damodaran": job["damodaran"],
391
  "n_lines": len(job.get("lines") or []),
392
+ "blend": job.get("blend") or [], "band": job.get("band"),
393
+ "confidence_label": job.get("confidence_label"), "eps_gate": job.get("eps_gate"),
394
  "cached_at": time.time(),
395
  "price_at_analysis": (job.get("damodaran") or {}).get("current_price"),
396
  "source": job.get("source", "user"),
397
  }
398
  f = _REPORTS_DIR / f"{job['ticker']}.json"
399
  f.write_text(json.dumps(payload))
400
+ _ledger_append(payload)
401
  if _HF_TOKEN:
402
  try:
403
  from huggingface_hub import HfApi
 
409
  pass
410
 
411
 
412
+ _LEDGER = _REPORTS_DIR / "_ledger.jsonl"
413
+
414
+
415
+ def _ledger_append(payload: dict) -> None:
416
+ """Append-only record of every call, so realized accuracy can be measured later
417
+ (the 7-day report cache prunes; the ledger never does). Mirrored to the dataset repo."""
418
+ try:
419
+ row = {"ticker": payload["ticker"], "ts": payload["cached_at"],
420
+ "date": dt_date_str(payload["cached_at"]),
421
+ "price": payload.get("price_at_analysis"),
422
+ "expected_return": payload.get("expected_return"), "rating": payload.get("rating"),
423
+ "value_implied": (payload.get("damodaran") or {}).get("value_implied"),
424
+ "source": payload.get("source", "user")}
425
+ if not row["price"] or row["expected_return"] is None:
426
+ return
427
+ with open(_LEDGER, "a") as fh:
428
+ fh.write(json.dumps(row) + "\n")
429
+ if _HF_TOKEN:
430
+ try:
431
+ from huggingface_hub import HfApi
432
+ HfApi().upload_file(path_or_fileobj=str(_LEDGER), path_in_repo="ledger/_ledger.jsonl",
433
+ repo_id=_REPORTS_REPO, repo_type="dataset", token=_HF_TOKEN)
434
+ except Exception:
435
+ pass
436
+ except Exception:
437
+ pass
438
+
439
+
440
+ def dt_date_str(ts):
441
+ import datetime as _dt
442
+ return _dt.datetime.fromtimestamp(ts, _dt.timezone.utc).strftime("%Y-%m-%d")
443
+
444
+
445
+ def track_record(min_age_days: int = 30) -> dict:
446
+ """Realized accuracy of past calls: for ledger rows older than min_age_days, compare the
447
+ predicted direction/return with the price move since. Empty until the ledger ages."""
448
+ rows = []
449
+ try:
450
+ if _LEDGER.exists():
451
+ rows = [json.loads(l) for l in _LEDGER.read_text().splitlines() if l.strip()]
452
+ except Exception:
453
+ rows = []
454
+ now = time.time()
455
+ aged = [r for r in rows if now - r["ts"] >= min_age_days * 86400]
456
+ total = len(rows)
457
+ if not aged:
458
+ return {"calls_logged": total, "evaluated": 0, "min_age_days": min_age_days,
459
+ "note": f"track record builds automatically — first reading when calls are {min_age_days} days old"}
460
+ # dedupe to latest aged call per ticker to avoid overweighting repeats
461
+ latest = {}
462
+ for r in aged:
463
+ if r["ticker"] not in latest or r["ts"] > latest[r["ticker"]]["ts"]:
464
+ latest[r["ticker"]] = r
465
+ hits = 0; errs = []; det = []
466
+ for r in latest.values():
467
+ p1 = _price_now(r["ticker"])
468
+ if not p1:
469
+ continue
470
+ real = p1 / r["price"] - 1.0
471
+ pred = (r["expected_return"] or 0) / 100.0
472
+ ok = (real >= 0) == (pred >= 0) if abs(pred) >= 0.02 else abs(real) < 0.05
473
+ hits += 1 if ok else 0
474
+ errs.append(abs(real - pred))
475
+ det.append({"ticker": r["ticker"], "date": r["date"], "rating": r["rating"],
476
+ "predicted": round(pred * 100, 1), "realized_to_date": round(real * 100, 1), "direction_ok": ok})
477
+ n = len(det)
478
+ return {"calls_logged": total, "evaluated": n, "min_age_days": min_age_days,
479
+ "direction_hit_rate": round(hits / n, 3) if n else None,
480
+ "mean_abs_error_pct": round(sum(errs) / n * 100, 1) if n else None,
481
+ "detail": sorted(det, key=lambda x: x["date"])[-25:]}
482
+
483
+
484
  def _hydrate_reports() -> None:
485
  """On startup, pull recent reports from the dataset repo so the cache survives redeploys."""
486
  if not _HF_TOKEN:
 
656
  "rating": job["rating"],
657
  "debate": job["debate"],
658
  "damodaran": job["damodaran"],
659
+ "blend": job.get("blend") or [],
660
+ "band": job.get("band"),
661
+ "confidence_label": job.get("confidence_label"),
662
+ "eps_gate": job.get("eps_gate"),
663
  "n_lines": len(job["lines"]),
664
  }