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:
parent
ee80a42971
commit
5e8c81e738
5
app.py
5
app.py
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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").
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue