You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
89 lines
3.3 KiB
Python
89 lines
3.3 KiB
Python
"""Event-level feature calculations from parser output."""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from libs.schemas.types import ConfidenceOutput, GuidanceOutput, RiskFlagsOutput, SignalsOutput
|
|
|
|
|
|
def guidance_direction_score(guidance: GuidanceOutput) -> float:
|
|
"""raised=1.0, inline_or_maintained=0.5, lowered=0.0, else=0.25."""
|
|
mapping = {
|
|
"raised": 1.0,
|
|
"inline_or_maintained": 0.5,
|
|
"lowered": 0.0,
|
|
"withdrawn": 0.0,
|
|
"not_provided": 0.25,
|
|
"unclear": 0.25,
|
|
}
|
|
return mapping.get(guidance.status, 0.25)
|
|
|
|
|
|
def oneoff_penalty(risk_flags: RiskFlagsOutput) -> float:
|
|
"""Sum of active risk flags / total flags. Higher = more risk."""
|
|
flags = [
|
|
risk_flags.oneoff_item,
|
|
risk_flags.tax_benefit,
|
|
risk_flags.valuation_gain,
|
|
risk_flags.non_gaap_heavy,
|
|
risk_flags.financing_related,
|
|
risk_flags.legal_or_regulatory_overhang,
|
|
]
|
|
total = len(flags)
|
|
active = sum(flags)
|
|
return active / total if total > 0 else 0.0
|
|
|
|
|
|
def signal_strength_score(signals: SignalsOutput) -> float:
|
|
"""Composite score [0..1] of positive business signals."""
|
|
score = 0.0
|
|
weights = {
|
|
"demand_strength": {"strong": 1.0, "stable": 0.5, "weakening": 0.0, "unknown": 0.0},
|
|
"pricing_power": {"present": 1.0, "mixed": 0.5, "absent": 0.0, "unknown": 0.0},
|
|
"backlog_or_bookings": {"present": 1.0, "mixed": 0.5, "absent": 0.0, "unknown": 0.0},
|
|
"customer_expansion": {"present": 1.0, "mixed": 0.5, "absent": 0.0, "unknown": 0.0},
|
|
"margin_quality": {"improving": 1.0, "stable": 0.5, "deteriorating": 0.0, "unknown": 0.0},
|
|
}
|
|
total_weight = len(weights)
|
|
for field, mapping in weights.items():
|
|
val = getattr(signals, field)
|
|
score += mapping.get(val, 0.0)
|
|
return score / total_weight if total_weight > 0 else 0.0
|
|
|
|
|
|
def document_quality_score(confidence: ConfidenceOutput) -> float:
|
|
"""Weighted average of confidence dimensions."""
|
|
return (
|
|
confidence.overall * 0.4
|
|
+ confidence.event_type * 0.2
|
|
+ confidence.event_direction * 0.2
|
|
+ confidence.guidance * 0.1
|
|
+ confidence.risk_flags * 0.1
|
|
)
|
|
|
|
|
|
def compute_event_features(parser_output: dict[str, Any]) -> dict[str, Any]:
|
|
"""Compute all event features from raw parser output dict."""
|
|
from libs.schemas.types import (
|
|
ConfidenceOutput,
|
|
GuidanceOutput,
|
|
RiskFlagsOutput,
|
|
SignalsOutput,
|
|
)
|
|
|
|
guidance = GuidanceOutput.model_validate(parser_output["guidance"])
|
|
signals = SignalsOutput.model_validate(parser_output["signals"])
|
|
risk_flags = RiskFlagsOutput.model_validate(parser_output["risk_flags"])
|
|
confidence = ConfidenceOutput.model_validate(parser_output["confidence"])
|
|
|
|
return {
|
|
"guidance_direction_score": guidance_direction_score(guidance),
|
|
"guidance_status": guidance.status,
|
|
"oneoff_penalty": oneoff_penalty(risk_flags),
|
|
"signal_strength_score": signal_strength_score(signals),
|
|
"document_quality_score": document_quality_score(confidence),
|
|
"event_type": parser_output.get("event_type", "unknown"),
|
|
"event_direction": parser_output.get("event_direction", "unknown"),
|
|
"parse_confidence_overall": confidence.overall,
|
|
}
|