fix: 6 audit issues — missing await, regime range, pct_out scaling, ticker validation, dead code, flag merge

1. app.py: await _update_in_progress (coroutine was silently dropped)
2. models.py + tier1.py: regime_score_adjustment range ±2→±10 (was negligible on 0-100 scale)
3. y_finance.py: pct_out * 100 (was fraction, displayed as percent)
4. app.py: ticker validation accepts dots/hyphens (BRK.B, BF-B)
5. portfolio.py: wire _fetch_peer_basics into theme substitution (was dead code)
6. setup.py: accumulate global_flags across parallel agents (dict.update was dropping them)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dtarkent2-sys 2026-03-09 21:56:38 +00:00
parent ee80a42971
commit 5e8c81e738
6 changed files with 46 additions and 17 deletions

5
app.py
View File

@ -1,6 +1,7 @@
"""FastAPI SSE backend for the structured equity ranking engine.""" """FastAPI SSE backend for the structured equity ranking engine."""
import os import os
import re
import time import time
import uuid import uuid
import asyncio import asyncio
@ -231,7 +232,7 @@ async def _run_analysis_inner(analysis_id: str, ticker: str, trade_date: str):
await q.put(evt) await q.put(evt)
# Mark in-progress agents for upcoming stages # Mark in-progress agents for upcoming stages
_update_in_progress(chunk, emitted_fields, prev_agent_statuses, state, q, start_time) await _update_in_progress(chunk, emitted_fields, prev_agent_statuses, state, q, start_time)
except Exception as e: except Exception as e:
print(f"[ANALYSIS] Stream error: {e}\n{_tb.format_exc()}", flush=True) print(f"[ANALYSIS] Stream error: {e}\n{_tb.format_exc()}", flush=True)
@ -396,7 +397,7 @@ async def _start_cleanup():
@app.post("/analyze", dependencies=[Depends(verify_api_key)]) @app.post("/analyze", dependencies=[Depends(verify_api_key)])
async def start_analysis(req: AnalyzeRequest): async def start_analysis(req: AnalyzeRequest):
ticker = req.ticker.upper().strip() ticker = req.ticker.upper().strip()
if not ticker or len(ticker) > 5 or not ticker.isalpha(): if not ticker or not re.match(r'^[A-Z0-9.\-]{1,6}$', ticker):
raise HTTPException(400, "Invalid ticker") raise HTTPException(400, "Invalid ticker")
trade_date = req.date or str(date.today()) trade_date = req.date or str(date.today())
analysis_id = str(uuid.uuid4()) analysis_id = str(uuid.uuid4())

View File

@ -104,14 +104,36 @@ def create_theme_substitution_node(llm):
industry = card.get("industry", "") industry = card.get("industry", "")
sector = card.get("sector", "") sector = card.get("sector", "")
# Fetch peer data for comparison # Fetch competitor/peer data to ground the LLM's comparison
# First, ask LLM to identify theme peers, then we'll fetch their data competitors = card.get("competitors") or []
peer_data = _fetch_peer_basics(competitors) if competitors else []
peer_summary = ""
if peer_data:
lines = []
for p in peer_data:
if p.get("error"):
continue
rg = p.get("revenue_growth")
rg_str = f"{rg*100:.1f}%" if rg else "N/A"
pm = p.get("profit_margins")
pm_str = f"{pm*100:.1f}%" if pm else "N/A"
lines.append(
f" {p['ticker']}: P/E={p.get('trailing_pe', 'N/A')}, "
f"Fwd P/E={p.get('forward_pe', 'N/A')}, "
f"RevGrowth={rg_str}, "
f"Margins={pm_str}, "
f"52W={p.get('52w_range_pct', 'N/A')}%"
)
peer_summary = "\n".join(lines)
theme_prompt = f"""You are a Theme Substitution Analyst. Your job: determine if {ticker} is the BEST theme_prompt = f"""You are a Theme Substitution Analyst. Your job: determine if {ticker} is the BEST
expression of its investment theme, or if better alternatives exist. expression of its investment theme, or if better alternatives exist.
CANDIDATE STOCK: CANDIDATE STOCK:
{summary} {summary}
{f'PEER FUNDAMENTALS (live data):{chr(10)}{peer_summary}' if peer_summary else 'No live peer data available — use your knowledge of these companies.'}
INSTRUCTIONS do this in order: INSTRUCTIONS do this in order:
1. IDENTIFY THE THEME: What macro/sector theme does {ticker} express? 1. IDENTIFY THE THEME: What macro/sector theme does {ticker} express?
@ -120,9 +142,10 @@ INSTRUCTIONS — do this in order:
Name it clearly in theme_name. Name it clearly in theme_name.
2. LIST THEME PEERS: Name 3-6 other publicly traded stocks that express the SAME theme. 2. LIST THEME PEERS: Name 3-6 other publicly traded stocks that express the SAME theme.
These should be the strongest competitors for capital allocation in this theme. Use the peer data above if available. These should be the strongest competitors
For each peer, estimate a master_score_estimate (0-10) based on your knowledge of for capital allocation in this theme.
their fundamentals, momentum, and positioning vs {ticker}. For each peer, score master_score_estimate (0-10) based on fundamentals, momentum,
and positioning vs {ticker}.
3. RANK WITHIN THEME: Rank all stocks (including {ticker}) by investment quality. 3. RANK WITHIN THEME: Rank all stocks (including {ticker}) by investment quality.
The stock with the best combination of: business quality, valuation, momentum, The stock with the best combination of: business quality, valuation, momentum,

View File

@ -188,13 +188,13 @@ INSTRUCTIONS:
2. Classify liquidity_regime: "expansion" / "contraction" / "neutral". 2. Classify liquidity_regime: "expansion" / "contraction" / "neutral".
- expansion: falling yields, dovish Fed, credit flowing, dollar weakening. - expansion: falling yields, dovish Fed, credit flowing, dollar weakening.
- contraction: rising yields, hawkish Fed, tight credit, dollar strengthening. - contraction: rising yields, hawkish Fed, tight credit, dollar strengthening.
3. Set regime_score_adjustment (-2 to +2): 3. Set regime_score_adjustment (-10 to +10):
- +2 = strong macro tailwind for this specific stock/sector. - +5 to +10 = strong macro tailwind for this specific stock/sector.
- +1 = mild tailwind. - +1 to +4 = mild tailwind.
- 0 = neutral. - 0 = neutral.
- -1 = mild headwind. - -1 to -4 = mild headwind.
- -2 = severe macro headwind (risk-off + contraction + hostile sector). - -5 to -10 = severe macro headwind (risk-off + contraction + hostile sector).
This adjustment directly modifies the master score for ALL stocks. This adjustment directly modifies the 0-100 master score for ALL stocks.
4. Score macro_alignment_0_to_10: how well macro supports {ticker} specifically. 4. Score macro_alignment_0_to_10: how well macro supports {ticker} specifically.
5. Also provide score_0_to_10 (overall macro health). 5. Also provide score_0_to_10 (overall macro health).
6. Set regime_label: descriptive label (e.g., "Late Cycle Risk-Off"). 6. Set regime_label: descriptive label (e.g., "Late Cycle Risk-Off").

View File

@ -613,7 +613,7 @@ def get_institutional_flow(ticker):
{ {
"holder": str(r.get("Holder", "")), "holder": str(r.get("Holder", "")),
"shares": int(r["Shares"]) if r.get("Shares") else None, "shares": int(r["Shares"]) if r.get("Shares") else None,
"pct_out": float(r["% Out"]) if r.get("% Out") else None, "pct_out": round(float(r["% Out"]) * 100, 2) if r.get("% Out") else None,
"value": float(r["Value"]) if r.get("Value") else None, "value": float(r["Value"]) if r.get("Value") else None,
} }
for r in top for r in top

View File

@ -191,11 +191,16 @@ def _create_parallel_node(agent_fns: List[tuple], label: str):
results = await asyncio.gather(*tasks, return_exceptions=True) results = await asyncio.gather(*tasks, return_exceptions=True)
merged: Dict[str, Any] = {} merged: Dict[str, Any] = {}
all_flags: list = []
for (name, _), result in zip(agent_fns, results): for (name, _), result in zip(agent_fns, results):
if isinstance(result, Exception): if isinstance(result, Exception):
logger.error("[%s] %s failed: %s", label, name, result) logger.error("[%s] %s failed: %s", label, name, result)
continue continue
flags = result.pop("global_flags", [])
all_flags.extend(flags)
merged.update(result) merged.update(result)
if all_flags:
merged["global_flags"] = all_flags
logger.info("[%s] completed in %.1fs", label, time.time() - t0) logger.info("[%s] completed in %.1fs", label, time.time() - t0)
return merged return merged

View File

@ -121,9 +121,9 @@ class MacroRegimeOutput(AgentBaseOutput):
risk_appetite: str = "neutral" # risk-on / risk-off / transitional risk_appetite: str = "neutral" # risk-on / risk-off / transitional
liquidity_regime: str = "neutral" # expansion / contraction / neutral liquidity_regime: str = "neutral" # expansion / contraction / neutral
regime_score_adjustment: float = Field( regime_score_adjustment: float = Field(
default=0.0, ge=-2, le=2, default=0.0, ge=-10, le=10,
description="Adjustment applied to all downstream scores. " description="Adjustment applied to the 0-100 master score. "
"+2 = strong macro tailwind, -2 = severe macro headwind.", "+10 = strong macro tailwind, -10 = severe macro headwind.",
) )