Spaces:
Sleeping
Sleeping
feat: Deterministic data gate on the Researcher->Analyzer edge
Browse filesApplies the AgentAsk edge-contract principle deterministically (no LLM):
- DG: per-category required-metric rules audited on the exact extracted
view the Analyzer consumes; gaps render as an explicit MISSING section
in the reference table instructing 'DATA NOT PROVIDED' - the silent
empty-valuation case can no longer pass unflagged, and the Critic's
existing constraint rule finally has something to enforce
- SC: sanity bounds quarantine impossible magnitudes (decimal/unit slips)
before they can enter the reference table and be cited; bounds reject
the impossible, not the unusual (Boeing's real 9.92 D/E passes)
- Both findings logged to the activity stream; 7 gate tests added
- src/nodes/analyzer.py +31 -7
- src/nodes/researcher.py +17 -0
- src/utils/data_gate.py +105 -0
- tests/test_data_gate.py +66 -0
src/nodes/analyzer.py
CHANGED
|
@@ -970,13 +970,17 @@ def _format_metric_for_reference(key: str, value, temporal_info: dict = None) ->
|
|
| 970 |
return formatted, as_of_date
|
| 971 |
|
| 972 |
|
| 973 |
-
def _generate_metric_reference_table(extracted: dict, is_financial: bool = False
|
|
|
|
| 974 |
"""
|
| 975 |
Generate an immutable metric reference table for LLM grounding.
|
| 976 |
|
| 977 |
Args:
|
| 978 |
extracted: Extracted metrics dictionary from _extract_key_metrics()
|
| 979 |
is_financial: If True, exclude EV/EBITDA
|
|
|
|
|
|
|
|
|
|
| 980 |
|
| 981 |
Returns:
|
| 982 |
tuple: (table_string, metric_lookup_dict)
|
|
@@ -1099,6 +1103,15 @@ def _generate_metric_reference_table(extracted: dict, is_financial: bool = False
|
|
| 1099 |
lines.extend(sentiment_lines)
|
| 1100 |
lines.append("")
|
| 1101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1102 |
lines.append("=" * 60)
|
| 1103 |
lines.append("")
|
| 1104 |
|
|
@@ -1250,7 +1263,8 @@ def _build_revision_prompt(
|
|
| 1250 |
company_data: str,
|
| 1251 |
current_draft: str,
|
| 1252 |
is_financial: bool,
|
| 1253 |
-
extracted: dict = None
|
|
|
|
| 1254 |
) -> str:
|
| 1255 |
"""Build revision prompt with conditional focus areas based on failed criteria.
|
| 1256 |
|
|
@@ -1267,7 +1281,7 @@ def _build_revision_prompt(
|
|
| 1267 |
# Generate metric reference table for revision (same as initial mode)
|
| 1268 |
reference_table = ""
|
| 1269 |
if extracted:
|
| 1270 |
-
reference_table, _ = _generate_metric_reference_table(extracted, is_financial)
|
| 1271 |
scores = critique_details.get("scores", {})
|
| 1272 |
|
| 1273 |
# Determine which focus areas to include based on failed criteria
|
|
@@ -1406,7 +1420,8 @@ Simply output the improved SWOT as a clean, final deliverable."""
|
|
| 1406 |
|
| 1407 |
|
| 1408 |
def _build_analyzer_prompt(company: str, ticker: str, formatted_data: str,
|
| 1409 |
-
is_financial: bool, extracted: dict = None
|
|
|
|
| 1410 |
"""Build analyzer prompt with metric reference table for hallucination prevention.
|
| 1411 |
|
| 1412 |
Args:
|
|
@@ -1425,7 +1440,8 @@ def _build_analyzer_prompt(company: str, ticker: str, formatted_data: str,
|
|
| 1425 |
ref_hash = ""
|
| 1426 |
|
| 1427 |
if extracted:
|
| 1428 |
-
reference_table, metric_lookup = _generate_metric_reference_table(
|
|
|
|
| 1429 |
ref_hash = _compute_reference_hash(metric_lookup)
|
| 1430 |
|
| 1431 |
if is_financial:
|
|
@@ -1509,6 +1525,12 @@ def analyzer_node(state, workflow_id=None, progress_store=None):
|
|
| 1509 |
|
| 1510 |
# Extract and format metrics for better LLM understanding
|
| 1511 |
extracted = _extract_key_metrics(raw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1512 |
formatted_data = _format_metrics_for_prompt(extracted, is_financial=is_financial)
|
| 1513 |
|
| 1514 |
# Generate detailed data report (shown before SWOT)
|
|
@@ -1533,7 +1555,8 @@ def analyzer_node(state, workflow_id=None, progress_store=None):
|
|
| 1533 |
company_data=formatted_data,
|
| 1534 |
current_draft=state.get("draft_report", ""),
|
| 1535 |
is_financial=is_financial,
|
| 1536 |
-
extracted=extracted
|
|
|
|
| 1537 |
)
|
| 1538 |
|
| 1539 |
# Update progress with revision info
|
|
@@ -1547,7 +1570,8 @@ def analyzer_node(state, workflow_id=None, progress_store=None):
|
|
| 1547 |
_add_activity_log(workflow_id, progress_store, "analyzer",
|
| 1548 |
f"Calling LLM to generate SWOT analysis...")
|
| 1549 |
prompt, metric_lookup, ref_hash = _build_analyzer_prompt(
|
| 1550 |
-
company, ticker, formatted_data, is_financial, extracted
|
|
|
|
| 1551 |
)
|
| 1552 |
# Store metric reference for validation (Layer 1 hallucination prevention)
|
| 1553 |
state["metric_reference"] = metric_lookup
|
|
|
|
| 970 |
return formatted, as_of_date
|
| 971 |
|
| 972 |
|
| 973 |
+
def _generate_metric_reference_table(extracted: dict, is_financial: bool = False,
|
| 974 |
+
data_gaps: list = None) -> tuple:
|
| 975 |
"""
|
| 976 |
Generate an immutable metric reference table for LLM grounding.
|
| 977 |
|
| 978 |
Args:
|
| 979 |
extracted: Extracted metrics dictionary from _extract_key_metrics()
|
| 980 |
is_financial: If True, exclude EV/EBITDA
|
| 981 |
+
data_gaps: Required metrics found missing by the data gate; rendered
|
| 982 |
+
as an explicit MISSING section so the LLM must say
|
| 983 |
+
"DATA NOT PROVIDED" rather than silently omit or invent
|
| 984 |
|
| 985 |
Returns:
|
| 986 |
tuple: (table_string, metric_lookup_dict)
|
|
|
|
| 1103 |
lines.extend(sentiment_lines)
|
| 1104 |
lines.append("")
|
| 1105 |
|
| 1106 |
+
if data_gaps:
|
| 1107 |
+
lines.append("[MISSING - REQUIRED DATA NOT AVAILABLE]")
|
| 1108 |
+
lines.append(" The following required metrics could not be obtained.")
|
| 1109 |
+
lines.append(" If a point would depend on one, write exactly: DATA NOT PROVIDED")
|
| 1110 |
+
lines.append(" Do NOT estimate, infer, or substitute values for these:")
|
| 1111 |
+
for gap in data_gaps:
|
| 1112 |
+
lines.append(f" - {gap}")
|
| 1113 |
+
lines.append("")
|
| 1114 |
+
|
| 1115 |
lines.append("=" * 60)
|
| 1116 |
lines.append("")
|
| 1117 |
|
|
|
|
| 1263 |
company_data: str,
|
| 1264 |
current_draft: str,
|
| 1265 |
is_financial: bool,
|
| 1266 |
+
extracted: dict = None,
|
| 1267 |
+
data_gaps: list = None
|
| 1268 |
) -> str:
|
| 1269 |
"""Build revision prompt with conditional focus areas based on failed criteria.
|
| 1270 |
|
|
|
|
| 1281 |
# Generate metric reference table for revision (same as initial mode)
|
| 1282 |
reference_table = ""
|
| 1283 |
if extracted:
|
| 1284 |
+
reference_table, _ = _generate_metric_reference_table(extracted, is_financial, data_gaps=data_gaps)
|
| 1285 |
scores = critique_details.get("scores", {})
|
| 1286 |
|
| 1287 |
# Determine which focus areas to include based on failed criteria
|
|
|
|
| 1420 |
|
| 1421 |
|
| 1422 |
def _build_analyzer_prompt(company: str, ticker: str, formatted_data: str,
|
| 1423 |
+
is_financial: bool, extracted: dict = None,
|
| 1424 |
+
data_gaps: list = None) -> tuple:
|
| 1425 |
"""Build analyzer prompt with metric reference table for hallucination prevention.
|
| 1426 |
|
| 1427 |
Args:
|
|
|
|
| 1440 |
ref_hash = ""
|
| 1441 |
|
| 1442 |
if extracted:
|
| 1443 |
+
reference_table, metric_lookup = _generate_metric_reference_table(
|
| 1444 |
+
extracted, is_financial, data_gaps=data_gaps)
|
| 1445 |
ref_hash = _compute_reference_hash(metric_lookup)
|
| 1446 |
|
| 1447 |
if is_financial:
|
|
|
|
| 1525 |
|
| 1526 |
# Extract and format metrics for better LLM understanding
|
| 1527 |
extracted = _extract_key_metrics(raw)
|
| 1528 |
+
|
| 1529 |
+
# Data gate (SC): drop values the researcher flagged as implausible so
|
| 1530 |
+
# they can never enter the reference table or be cited
|
| 1531 |
+
from src.utils.data_gate import scrub_suspect_metrics
|
| 1532 |
+
extracted = scrub_suspect_metrics(extracted, state.get("suspect_metrics") or [])
|
| 1533 |
+
|
| 1534 |
formatted_data = _format_metrics_for_prompt(extracted, is_financial=is_financial)
|
| 1535 |
|
| 1536 |
# Generate detailed data report (shown before SWOT)
|
|
|
|
| 1555 |
company_data=formatted_data,
|
| 1556 |
current_draft=state.get("draft_report", ""),
|
| 1557 |
is_financial=is_financial,
|
| 1558 |
+
extracted=extracted,
|
| 1559 |
+
data_gaps=state.get("data_gaps")
|
| 1560 |
)
|
| 1561 |
|
| 1562 |
# Update progress with revision info
|
|
|
|
| 1570 |
_add_activity_log(workflow_id, progress_store, "analyzer",
|
| 1571 |
f"Calling LLM to generate SWOT analysis...")
|
| 1572 |
prompt, metric_lookup, ref_hash = _build_analyzer_prompt(
|
| 1573 |
+
company, ticker, formatted_data, is_financial, extracted,
|
| 1574 |
+
data_gaps=state.get("data_gaps")
|
| 1575 |
)
|
| 1576 |
# Store metric reference for validation (Layer 1 hallucination prevention)
|
| 1577 |
state["metric_reference"] = metric_lookup
|
src/nodes/researcher.py
CHANGED
|
@@ -153,6 +153,23 @@ def researcher_node(state, workflow_id=None, progress_store=None):
|
|
| 153 |
state["raw_data"] = json.dumps(result, indent=2, default=str)
|
| 154 |
state["sources_failed"] = sources_failed
|
| 155 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
print(f" - Sources available: {result['sources_available']}")
|
| 157 |
if sources_failed:
|
| 158 |
print(f" - Sources failed: {sources_failed}")
|
|
|
|
| 153 |
state["raw_data"] = json.dumps(result, indent=2, default=str)
|
| 154 |
state["sources_failed"] = sources_failed
|
| 155 |
|
| 156 |
+
# Deterministic data gate on the exact view the Analyzer consumes:
|
| 157 |
+
# metric-level gaps (DG) and impossible magnitudes (SC)
|
| 158 |
+
try:
|
| 159 |
+
from src.nodes.analyzer import _extract_key_metrics
|
| 160 |
+
from src.utils.data_gate import audit_extracted_metrics
|
| 161 |
+
audit = audit_extracted_metrics(_extract_key_metrics(state["raw_data"]))
|
| 162 |
+
state["data_gaps"] = audit["gaps"]
|
| 163 |
+
state["suspect_metrics"] = audit["suspect"]
|
| 164 |
+
if audit["gaps"]:
|
| 165 |
+
add_log("researcher",
|
| 166 |
+
f"Data gaps (will be marked DATA NOT PROVIDED): {', '.join(audit['gaps'])}")
|
| 167 |
+
for cat, key, value in audit["suspect"]:
|
| 168 |
+
add_log("researcher",
|
| 169 |
+
f"Discarded implausible value: {cat}.{key} = {value:g} (outside sanity bounds)")
|
| 170 |
+
except Exception as gate_err:
|
| 171 |
+
print(f"[data-gate] audit skipped: {gate_err}")
|
| 172 |
+
|
| 173 |
print(f" - Sources available: {result['sources_available']}")
|
| 174 |
if sources_failed:
|
| 175 |
print(f" - Sources failed: {sources_failed}")
|
src/utils/data_gate.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Deterministic data gate for the Researcher -> Analyzer edge.
|
| 3 |
+
|
| 4 |
+
Two checks, run on the same extracted-metrics view the Analyzer consumes:
|
| 5 |
+
|
| 6 |
+
- DG (data gap): required metrics per category. Gaps are surfaced to the
|
| 7 |
+
Analyzer as explicit DATA NOT PROVIDED entries instead of silent omission,
|
| 8 |
+
giving the Critic's constraint-compliance rule something to enforce.
|
| 9 |
+
- SC (signal corruption): impossible magnitudes (unit/decimal slips upstream)
|
| 10 |
+
are quarantined before they can enter the reference table and be cited.
|
| 11 |
+
|
| 12 |
+
Bounds are deliberately loose - they reject the impossible, not the unusual.
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
# Per-category requirement rules: (metric keys, minimum count present)
|
| 16 |
+
REQUIRED_METRICS = {
|
| 17 |
+
"fundamentals": (["revenue", "net_margin", "eps"], 2),
|
| 18 |
+
"valuation": (["pe_trailing", "pe_forward", "pb_ratio", "ps_ratio"], 1),
|
| 19 |
+
"volatility": (["beta", "vix", "historical_volatility"], 1),
|
| 20 |
+
"macro": (["gdp_growth", "interest_rate", "inflation", "unemployment"], 2),
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
# (min, max) inclusive. Values are in the units the extractor produces
|
| 24 |
+
# (margins/rates/vol in percent, ratios as multiples, currency in dollars).
|
| 25 |
+
SANITY_BOUNDS = {
|
| 26 |
+
"revenue": (0, 1e13),
|
| 27 |
+
"net_income": (-1e12, 1e12),
|
| 28 |
+
"free_cash_flow": (-1e12, 1e12),
|
| 29 |
+
"net_margin": (-200, 100),
|
| 30 |
+
"gross_margin": (-200, 100),
|
| 31 |
+
"operating_margin": (-200, 100),
|
| 32 |
+
"eps": (-10000, 10000),
|
| 33 |
+
"debt_to_equity": (-100, 100),
|
| 34 |
+
"revenue_cagr_3yr": (-100, 300),
|
| 35 |
+
"pe_trailing": (-1000, 1000),
|
| 36 |
+
"pe_forward": (-1000, 1000),
|
| 37 |
+
"pb_ratio": (-100, 500),
|
| 38 |
+
"ps_ratio": (0, 500),
|
| 39 |
+
"ev_ebitda": (-1000, 1000),
|
| 40 |
+
"beta": (-5, 10),
|
| 41 |
+
"vix": (5, 150),
|
| 42 |
+
"historical_volatility": (0, 500),
|
| 43 |
+
"gdp_growth": (-30, 30),
|
| 44 |
+
"interest_rate": (-5, 50),
|
| 45 |
+
"inflation": (-20, 100),
|
| 46 |
+
"unemployment": (0, 50),
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _numeric_value(metric_val):
|
| 51 |
+
"""Extracted metrics are either numbers or {'value': number, ...} dicts."""
|
| 52 |
+
if isinstance(metric_val, dict):
|
| 53 |
+
metric_val = metric_val.get("value")
|
| 54 |
+
if isinstance(metric_val, (int, float)) and not isinstance(metric_val, bool):
|
| 55 |
+
return float(metric_val)
|
| 56 |
+
return None
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def audit_extracted_metrics(extracted: dict) -> dict:
|
| 60 |
+
"""
|
| 61 |
+
Audit the extracted metrics view.
|
| 62 |
+
|
| 63 |
+
Args:
|
| 64 |
+
extracted: dict from analyzer._extract_key_metrics -
|
| 65 |
+
{"fundamentals": {...}, "valuation": {...}, ...}
|
| 66 |
+
|
| 67 |
+
Returns:
|
| 68 |
+
{
|
| 69 |
+
"gaps": ["fundamentals: revenue", ...] # DG findings
|
| 70 |
+
"suspect": [("volatility", "vix", 1673.0), ...] # SC findings
|
| 71 |
+
}
|
| 72 |
+
"""
|
| 73 |
+
gaps = []
|
| 74 |
+
suspect = []
|
| 75 |
+
|
| 76 |
+
for category, (keys, min_present) in REQUIRED_METRICS.items():
|
| 77 |
+
data = extracted.get(category) or {}
|
| 78 |
+
present = [k for k in keys if _numeric_value(data.get(k)) is not None]
|
| 79 |
+
if len(present) < min_present:
|
| 80 |
+
for k in keys:
|
| 81 |
+
if k not in present:
|
| 82 |
+
gaps.append(f"{category}: {k}")
|
| 83 |
+
|
| 84 |
+
for category in REQUIRED_METRICS:
|
| 85 |
+
data = extracted.get(category) or {}
|
| 86 |
+
if not isinstance(data, dict):
|
| 87 |
+
continue
|
| 88 |
+
for key, raw in data.items():
|
| 89 |
+
value = _numeric_value(raw)
|
| 90 |
+
bounds = SANITY_BOUNDS.get(key)
|
| 91 |
+
if value is None or bounds is None:
|
| 92 |
+
continue
|
| 93 |
+
lo, hi = bounds
|
| 94 |
+
if not (lo <= value <= hi):
|
| 95 |
+
suspect.append((category, key, value))
|
| 96 |
+
|
| 97 |
+
return {"gaps": gaps, "suspect": suspect}
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def scrub_suspect_metrics(extracted: dict, suspect: list) -> dict:
|
| 101 |
+
"""Remove quarantined values so they cannot enter the reference table."""
|
| 102 |
+
for category, key, _value in suspect:
|
| 103 |
+
if isinstance(extracted.get(category), dict):
|
| 104 |
+
extracted[category].pop(key, None)
|
| 105 |
+
return extracted
|
tests/test_data_gate.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tests for the deterministic Researcher -> Analyzer data gate."""
|
| 2 |
+
|
| 3 |
+
from src.utils.data_gate import audit_extracted_metrics, scrub_suspect_metrics
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def _healthy():
|
| 7 |
+
return {
|
| 8 |
+
"fundamentals": {"revenue": 89.5e9, "net_margin": 2.5, "eps": 2.49},
|
| 9 |
+
"valuation": {"pe_trailing": {"value": 24.1}},
|
| 10 |
+
"volatility": {"beta": 1.15, "vix": 16.73},
|
| 11 |
+
"macro": {"gdp_growth": 2.1, "interest_rate": 3.63},
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def test_healthy_data_passes_clean():
|
| 16 |
+
audit = audit_extracted_metrics(_healthy())
|
| 17 |
+
assert audit["gaps"] == []
|
| 18 |
+
assert audit["suspect"] == []
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def test_empty_valuation_is_a_gap():
|
| 22 |
+
# The MSFT run shipped an empty valuation basket with no flag anywhere
|
| 23 |
+
data = _healthy()
|
| 24 |
+
data["valuation"] = {}
|
| 25 |
+
audit = audit_extracted_metrics(data)
|
| 26 |
+
assert any(g.startswith("valuation:") for g in audit["gaps"])
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def test_missing_fundamentals_below_minimum():
|
| 30 |
+
data = _healthy()
|
| 31 |
+
data["fundamentals"] = {"eps": 2.49} # only 1 of required 2
|
| 32 |
+
audit = audit_extracted_metrics(data)
|
| 33 |
+
assert "fundamentals: revenue" in audit["gaps"]
|
| 34 |
+
assert "fundamentals: net_margin" in audit["gaps"]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def test_impossible_magnitude_is_quarantined():
|
| 38 |
+
data = _healthy()
|
| 39 |
+
data["fundamentals"]["net_margin"] = 1218.0 # decimal-slip: 12.18 -> 1218
|
| 40 |
+
audit = audit_extracted_metrics(data)
|
| 41 |
+
assert ("fundamentals", "net_margin", 1218.0) in audit["suspect"]
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def test_unusual_but_possible_values_pass():
|
| 45 |
+
# Boeing's real D/E of 9.92 and hist vol of 34% must NOT be flagged
|
| 46 |
+
data = _healthy()
|
| 47 |
+
data["fundamentals"]["debt_to_equity"] = 9.92
|
| 48 |
+
data["volatility"]["historical_volatility"] = 34.13
|
| 49 |
+
audit = audit_extracted_metrics(data)
|
| 50 |
+
assert audit["suspect"] == []
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def test_scrub_removes_only_quarantined_values():
|
| 54 |
+
data = _healthy()
|
| 55 |
+
data["volatility"]["vix"] = 1673.0
|
| 56 |
+
audit = audit_extracted_metrics(data)
|
| 57 |
+
scrubbed = scrub_suspect_metrics(data, audit["suspect"])
|
| 58 |
+
assert "vix" not in scrubbed["volatility"]
|
| 59 |
+
assert scrubbed["volatility"]["beta"] == 1.15
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def test_dict_wrapped_values_audited():
|
| 63 |
+
data = _healthy()
|
| 64 |
+
data["fundamentals"]["net_margin"] = {"value": 1218.0, "end_date": "2025-12-31"}
|
| 65 |
+
audit = audit_extracted_metrics(data)
|
| 66 |
+
assert ("fundamentals", "net_margin", 1218.0) in audit["suspect"]
|