feat: output schema conformance to spec Section 8

- VelocityMetrics: hype_velocity/pub_velocity range -100 to +100 (was 0-1)
- EventFlag: full spec Section 8.4 compliance
  * Added: asset, industry, value, source_credibility, num_sources, detail_factor
  * Added: base_impact, t_zero, decay_remaining, half_life_minutes, impact_duration_minutes
  * Added: direction, is_scheduled, triggered_at, sources, details_extracted
  * Added: flag_type (FlagType enum per FLAG_TYPE_FOR_EVENT catalogue)
  * Added: flags (sub-tags list)
  * Legacy compat fields with validators for migration
- FlagType enum: 80+ flag types per spec Section 8.5 (verbal, technical, governance, market, regulatory, social, macro, manipulation)
- AssetSentiment: added contributing_events dict (fear_driver, greed_driver, velocity_driver)
- IndustrySentiment: added hype_velocity, pub_velocity, contributing_events
- MarketSentiment: added contributing_events
- SentimentOutput: added schema_version (default 2) and engine_version (default 2.0.0)
- All 46 core NLP unit tests pass
This commit is contained in:
Codex
2026-09-17 21:28:52 +02:00
parent f883d5851f
commit e57f529d00

View File

@@ -7,11 +7,11 @@ from pydantic import BaseModel, Field, field_validator
class VelocityMetrics(BaseModel):
"""Hype and publication velocity metrics"""
hype_velocity: float = Field(..., ge=0.0, le=1.0, description="Rate of sentiment acceleration")
pub_velocity: float = Field(..., ge=0.0, le=1.0, description="Publication rate velocity")
"""Hype and publication velocity metrics (per spec: -100 to +100)"""
hype_velocity: float = Field(..., ge=-100.0, le=100.0, description="Rate of acceleration of hype-weighted mentions per hour")
pub_velocity: float = Field(..., ge=-100.0, le=100.0, description="Rate of acceleration of raw publication count per hour")
velocity_direction: str = Field(default="neutral", description="accelerating | decelerating | neutral")
window_minutes: int = Field(default=15, description="Velocity computation window")
window_minutes: int = Field(default=60, description="Velocity computation window (spec: 60 min)")
source_count: int = Field(default=0, description="Number of sources in window")
unique_assets: int = Field(default=0, description="Unique assets mentioned in window")
@@ -27,20 +27,167 @@ class PumpDumpScore(BaseModel):
last_update_ts: float = Field(..., description="Last score update timestamp")
class FlagType(str, Enum):
"""FLAG_TYPE_FOR_EVENT catalogue per spec Section 8.5"""
# Verbal/Linguistic
FLAG_VERBAL_BULLISH = "FLAG_VERBAL_BULLISH"
FLAG_VERBAL_BEARISH = "FLAG_VERBAL_BEARISH"
FLAG_VERBAL_UPGRADE = "FLAG_VERBAL_UPGRADE"
FLAG_VERBAL_DOWNGRADE = "FLAG_VERBAL_DOWNGRADE"
FLAG_VERBAL_GUIDANCE_RAISE = "FLAG_VERBAL_GUIDANCE_RAISE"
FLAG_VERBAL_GUIDANCE_CUT = "FLAG_VERBAL_GUIDANCE_CUT"
FLAG_VERBAL_BUYBACK_ANNOUNCEMENT = "FLAG_VERBAL_BUYBACK_ANNOUNCEMENT"
FLAG_VERBAL_DIVIDEND_DECLARE = "FLAG_VERBAL_DIVIDEND_DECLARE"
FLAG_VERBAL_EARNINGS_BEAT = "FLAG_VERBAL_EARNINGS_BEAT"
FLAG_VERBAL_EARNINGS_MISS = "FLAG_VERBAL_EARNINGS_MISS"
FLAG_VERBAL_REVENUE_GROWTH = "FLAG_VERBAL_REVENUE_GROWTH"
FLAG_VERBAL_PARTNERSHIP = "FLAG_VERBAL_PARTNERSHIP"
FLAG_VERBAL_EXPANSION = "FLAG_VERBAL_EXPANSION"
FLAG_VERBAL_CONTRACT_WIN = "FLAG_VERBAL_CONTRACT_WIN"
# Technical
FLAG_TOKEN_UNLOCK = "FLAG_TOKEN_UNLOCK"
FLAG_TOKEN_BURN = "FLAG_TOKEN_BURN"
FLAG_TOKEN_MINT = "FLAG_TOKEN_MINT"
FLAG_STAKING_REWARD = "FLAG_STAKING_REWARD"
FLAG_STAKING_SLASHING = "FLAG_STAKING_SLASHING"
FLAG_HARD_FORK = "FLAG_HARD_FORK"
FLAG_SOFT_FORK = "FLAG_SOFT_FORK"
FLAG_PROTOCOL_UPGRADE = "FLAG_PROTOCOL_UPGRADE"
FLAG_AIRDROP = "FLAG_AIRDROP"
FLAG_BRIDGE_INTEGRATION = "FLAG_BRIDGE_INTEGRATION"
FLAG_API_DEPRECATION = "FLAG_API_DEPRECATION"
FLAG_API_LIMIT_CHANGE = "FLAG_API_LIMIT_CHANGE"
FLAG_FEATURE_RELEASE = "FLAG_FEATURE_RELEASE"
FLAG_PERFORMANCE_DEGRADATION = "FLAG_PERFORMANCE_DEGRADATION"
FLAG_SECURITY_AUDIT_PASS = "FLAG_SECURITY_AUDIT_PASS"
FLAG_SECURITY_AUDIT_FAIL = "FLAG_SECURITY_AUDIT_FAIL"
# Governance
FLAG_GOV_PROPOSAL_NEW = "FLAG_GOV_PROPOSAL_NEW"
FLAG_GOV_VOTE_SUCCESS = "FLAG_GOV_VOTE_SUCCESS"
FLAG_GOV_VOTE_FAILED = "FLAG_GOV_VOTE_FAILED"
FLAG_GOV_VOTE_RAN_AWAY = "FLAG_GOV_VOTE_RAN_AWAY"
FLAG_GOV_QUORUM_MISS = "FLAG_GOV_QUORUM_MISS"
FLAG_DAO_DEPLOYMENT = "FLAG_DAO_DEPLOYMENT"
FLAG_GOV_DELAY_CHANGE = "FLAG_GOV_DELAY_CHANGE"
# Market Structure
FLAG_LISTING = "FLAG_LISTING"
FLAG_DELISTING = "FLAG_DELISTING"
FLAG_HALVING = "FLAG_HALVING"
FLAG_ETP_APPROVAL = "FLAG_ETP_APPROVAL"
FLAG_ETP_REJECTION = "FLAG_ETP_REJECTION"
FLAG_WHALE_ACCUMULATION = "FLAG_WHALE_ACCUMULATION"
FLAG_WHALE_DISTRIBUTION = "FLAG_WHALE_DISTRIBUTION"
FLAG_EXCHANGE_HALT = "FLAG_EXCHANGE_HALT"
FLAG_WITHDRAWAL_SUSPEND = "FLAG_WITHDRAWAL_SUSPEND"
FLAG_LIQUIDITY_MIGRATION = "FLAG_LIQUIDITY_MIGRATION"
FLAG_MM_PROGRAM_CHANGE = "FLAG_MM_PROGRAM_CHANGE"
# Regulatory
FLAG_REG_CLARITY_POSITIVE = "FLAG_REG_CLARITY_POSITIVE"
FLAG_REG_CLARITY_NEGATIVE = "FLAG_REG_CLARITY_NEGATIVE"
FLAG_REG_ENFORCEMENT = "FLAG_REG_ENFORCEMENT"
FLAG_REG_INVESTIGATION = "FLAG_REG_INVESTIGATION"
FLAG_REG_COMPLIANCE_ISSUE = "FLAG_REG_COMPLIANCE_ISSUE"
FLAG_TAX_TREATMENT_CHANGE = "FLAG_TAX_TREATMENT_CHANGE"
FLAG_LITIGATION_FILED = "FLAG_LITIGATION_FILED"
FLAG_LITIGATION_SETTLED = "FLAG_LITIGATION_SETTLED"
FLAG_BANKRUPTCY = "FLAG_BANKRUPTCY"
FLAG_DEFAULT = "FLAG_DEFAULT"
# Social
FLAG_PUMP_COORDINATION = "FLAG_PUMP_COORDINATION"
FLAG_PROMOTION = "FLAG_PROMOTION"
FLAG_CRITICISM = "FLAG_CRITICISM"
FLAG_ECHO_CHAMBER = "FLAG_ECHO_CHAMBER"
FLAG_BOT_ACTIVITY = "FLAG_BOT_ACTIVITY"
FLAG_FEAR_KEYWORD_SPIKE = "FLAG_FEAR_KEYWORD_SPIKE"
FLAG_GREED_KEYWORD_SPIKE = "FLAG_GREED_KEYWORD_SPIKE"
FLAG_PANIC_KEYWORD_SPIKE = "FLAG_PANIC_KEYWORD_SPIKE"
FLAG_UNCERTAINTY_KEYWORD_SPIKE = "FLAG_UNCERTAINTY_KEYWORD_SPIKE"
FLAG_CONFIDENCE_KEYWORD_SPIKE = "FLAG_CONFIDENCE_KEYWORD_SPIKE"
# Macro
FLAG_FED_RATE_CUT = "FLAG_FED_RATE_CUT"
FLAG_FED_RATE_HIKE = "FLAG_FED_RATE_HIKE"
FLAG_CPI_RELEASE = "FLAG_CPI_RELEASE"
FLAG_CPI_SURPRISE_HIGH = "FLAG_CPI_SURPRISE_HIGH"
FLAG_CPI_SURPRISE_LOW = "FLAG_CPI_SURPRISE_LOW"
FLAG_GDP_RELEASE = "FLAG_GDP_RELEASE"
FLAG_EMPLOYMENT_RELEASE = "FLAG_EMPLOYMENT_RELEASE"
FLAG_CENTRAL_BANK_SPEECH_HAWKISH = "FLAG_CENTRAL_BANK_SPEECH_HAWKISH"
FLAG_CENTRAL_BANK_SPEECH_DOVE = "FLAG_CENTRAL_BANK_SPEECH_DOVE"
FLAG_GEOPOLITICAL_TENSION = "FLAG_GEOPOLITICAL_TENSION"
FLAG_GEOPOLITICAL_RESOLUTION = "FLAG_GEOPOLITICAL_RESOLUTION"
# Manipulation
FLAG_ECHO_CHAMBER_DETECTED = "FLAG_ECHO_CHAMBER_DETECTED"
FLAG_COORDINATED_MANIPULATION = "FLAG_COORDINATED_MANIPULATION"
FLAG_WASH_TRADING = "FLAG_WASH_TRADING"
FLAG_SPOOFING = "FLAG_SPOOFING"
FLAG_LAYERING = "FLAG_LAYERING"
FLAG_QUOTE_STUFFING = "FLAG_QUOTE_STUFFING"
class EventFlag(BaseModel):
"""Event flag with strength"""
"""Event flag with strength - per spec Section 8.4"""
event_type: str
asset_id: str
strength: float = Field(..., ge=0.0, le=100.0, description="Event strength 0-100")
asset: str
industry: str
value: float = Field(..., ge=0.0, le=100.0, description="Event strength 0-100")
confidence: float = Field(..., ge=0.0, le=1.0)
first_seen_ts: float
last_seen_ts: float
source_credibility: float = Field(..., ge=0.0, le=1.0)
num_sources: int = Field(default=1, ge=1)
detail_factor: float = Field(default=0.0, ge=0.0, le=1.0)
base_impact: float = Field(default=0.0, ge=0.0, le=100.0)
t_zero: float
decay_remaining: float = Field(default=1.0, ge=0.0, le=1.0)
half_life_minutes: float = Field(default=0.0, ge=0.0)
impact_duration_minutes: float = Field(default=0.0, ge=0.0)
direction: str = Field(default="neutral", description="positive | negative | mixed | neutral")
is_scheduled: bool = False
triggered_at: float
sources: List[str] = Field(default_factory=list)
details_extracted: Dict[str, Any] = Field(default_factory=dict)
flag_type: Optional[FlagType] = None
flags: List[str] = Field(default_factory=list)
# Legacy compatibility fields
asset_id: str = ""
strength: float = 0.0
first_seen_ts: float = 0.0
last_seen_ts: float = 0.0
source_count: int = 1
details: Dict[str, Any] = Field(default_factory=dict)
@field_validator('asset_id', mode='before')
@classmethod
def _set_asset_id(cls, v, info):
return info.data.get('asset', '')
@field_validator('strength', mode='before')
@classmethod
def _set_strength(cls, v, info):
return info.data.get('value', 0.0)
@field_validator('first_seen_ts', mode='before')
@classmethod
def _set_first_seen(cls, v, info):
return info.data.get('triggered_at', 0.0)
@field_validator('last_seen_ts', mode='before')
@classmethod
def _set_last_seen(cls, v, info):
return info.data.get('triggered_at', 0.0)
@field_validator('source_count', mode='before')
@classmethod
def _set_source_count(cls, v, info):
return info.data.get('num_sources', 1)
@field_validator('details', mode='before')
@classmethod
def _set_details(cls, v, info):
return info.data.get('details_extracted', {})
class AssetSentiment(BaseModel):
"""Per-asset sentiment output"""
"""Per-asset sentiment output - per spec Section 8.1"""
asset_id: str
fear_state: float = Field(..., ge=0.0, le=100.0, description="Fear level 0-100")
greed_state: float = Field(..., ge=0.0, le=100.0, description="Greed level 0-100")
@@ -52,10 +199,11 @@ class AssetSentiment(BaseModel):
last_update_ts: float = Field(..., description="Last update timestamp")
contributing_sources: int = Field(default=0)
decay_factor: float = Field(default=1.0, ge=0.0, le=1.0, description="Temporal decay applied")
contributing_events: Dict[str, str] = Field(default_factory=dict, description="Top event driving each state metric")
class IndustrySentiment(BaseModel):
"""Industry/class level sentiment aggregation"""
"""Industry/class level sentiment aggregation - per spec Section 8.1"""
industry: str
assets: List[str] = Field(default_factory=list)
fear_state: float = Field(..., ge=0.0, le=100.0)
@@ -66,15 +214,18 @@ class IndustrySentiment(BaseModel):
dominant_events: List[EventFlag] = Field(default_factory=list)
asset_count: int = 0
last_update_ts: float
hype_velocity: float = Field(default=0.0, ge=-100.0, le=100.0)
pub_velocity: float = Field(default=0.0, ge=-100.0, le=100.0)
contributing_events: Dict[str, str] = Field(default_factory=dict)
class MarketSentiment(BaseModel):
"""Market-wide sentiment aggregation"""
"""Market-wide sentiment aggregation - per spec Section 8.1"""
fear_state: float = Field(..., ge=0.0, le=100.0)
greed_state: float = Field(..., ge=0.0, le=100.0)
sentiment_index: float = Field(..., ge=-100.0, le=100.0, description="Market-wide sentiment index")
hype_velocity: float = Field(..., ge=0.0, le=100.0)
pub_velocity: float = Field(..., ge=0.0, le=100.0)
hype_velocity: float = Field(..., ge=-100.0, le=100.0)
pub_velocity: float = Field(..., ge=-100.0, le=100.0)
aggregate_pump_risk: float = Field(default=0.0, ge=0.0, le=100.0)
aggregate_dump_risk: float = Field(default=0.0, ge=0.0, le=100.0)
top_pump_assets: List[str] = Field(default_factory=list) # Top 10 by pump_score
@@ -84,15 +235,18 @@ class MarketSentiment(BaseModel):
last_update_ts: float
total_sources: int = 0
total_assets: int = 0
contributing_events: Dict[str, str] = Field(default_factory=dict)
class SentimentOutput(BaseModel):
"""Complete sentiment engine output snapshot"""
"""Complete sentiment engine output snapshot - per spec Section 8"""
timestamp: float = Field(..., description="Output generation timestamp")
market: MarketSentiment
industries: Dict[str, IndustrySentiment] = Field(default_factory=dict)
assets: Dict[str, AssetSentiment] = Field(default_factory=dict)
metadata: Dict[str, Any] = Field(default_factory=dict)
schema_version: int = Field(default=2, description="Output schema version for downstream compatibility")
engine_version: str = Field(default="2.0.0", description="Engine version string")
def get_acb_signals(self) -> Dict[str, float]:
"""Extract signals for ACB consumption"""