diff --git a/sentiment_engine/src/sentiment_engine/schemas/output.py b/sentiment_engine/src/sentiment_engine/schemas/output.py index dd768d7..70d0cac 100644 --- a/sentiment_engine/src/sentiment_engine/schemas/output.py +++ b/sentiment_engine/src/sentiment_engine/schemas/output.py @@ -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"""