Files
sentiment-engine/MALKHUT/malkhut/clock/events.py

169 lines
4.9 KiB
Python
Raw Normal View History

"""
T19 Event Types — typed events for the UV clock host.
Per Fable's T19 spec (ANNEX B-CORRECTION):
"iceoryx2 = the wires; ASEx = the state cells; UV clock = the conductor."
Events are published via iceoryx2/zinc topics (transport).
ASEx guards the state cells that handlers mutate.
Staleness Law (ANNEX A):
1. BarFire is EDGE-TRIGGERED on scan_number monotonic advance — NEVER timer-synthesized.
2. Every event carries provenance: scan_number, scan_ts, ingest_ts, AGE at dispatch.
3. Freshness watchdog TimerEvent emits StaleInput when stale.
"""
from __future__ import annotations
import time
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Optional
class EventType(str, Enum):
SCAN = "SCAN"
TICK = "TICK"
TIMER = "TIMER"
BAR_FIRE = "BAR_FIRE"
STALE_INPUT = "STALE_INPUT"
@dataclass(frozen=True, slots=True)
class EventProvenance:
"""
First-class freshness payload. Handlers CANNOT NOT know this.
Every event carries provenance per T19 Staleness Law §2.
"""
scan_number: int
scan_ts: int # ns, original scan timestamp
ingest_ts: int # ns, when we received it
source: str # "live" | "replay"
@property
def age_ns(self) -> int:
"""Age at construction time (not dispatch — dispatch adds overhead)."""
return time.time_ns() - self.ingest_ts
@property
def is_fresh(self) -> bool:
"""Fresh if age < 1.5x cadence (default 5.85s * 1.5 = 8.775s)."""
return self.age_ns < 8_775_000_000 # 8.775s in ns
def stale_threshold_ns(self, cadence_ns: int = 5_850_000_000) -> int:
return int(cadence_ns * 1.5)
@dataclass(frozen=True, slots=True)
class ScanEvent:
"""
NG7 scan from HZ (live) or recorded source (replay).
~5.85s cadence live. Edge-triggered on scan_number advance.
"""
event_type: EventType = EventType.SCAN
scan_number: int = 0
symbol: str = ""
data: Any = None # scan payload (dict, bytes, etc.)
provenance: EventProvenance = None
def __post_init__(self):
if self.provenance is None:
object.__setattr__(self, 'provenance', EventProvenance(
scan_number=self.scan_number,
scan_ts=time.time_ns(),
ingest_ts=time.time_ns(),
source="live",
))
@dataclass(frozen=True, slots=True)
class TickEvent:
"""
OBF price tick. ~0.18s cadence live, per-symbol.
TP/SL handlers subscribe to this.
"""
event_type: EventType = EventType.TICK
symbol: str = ""
price: float = 0.0
bid: float = 0.0
ask: float = 0.0
volume: float = 0.0
provenance: EventProvenance = None
def __post_init__(self):
if self.provenance is None:
object.__setattr__(self, 'provenance', EventProvenance(
scan_number=0,
scan_ts=time.time_ns(),
ingest_ts=time.time_ns(),
source="live",
))
@dataclass(frozen=True, slots=True)
class TimerEvent:
"""
Scheduled wakeup: TTL expiries, watchdogs, cadence ticks.
"""
event_type: EventType = EventType.TIMER
timer_id: str = ""
payload: Any = None
provenance: EventProvenance = None
def __post_init__(self):
if self.provenance is None:
object.__setattr__(self, 'provenance', EventProvenance(
scan_number=0,
scan_ts=time.time_ns(),
ingest_ts=time.time_ns(),
source="live",
))
@dataclass(frozen=True, slots=True)
class BarFire:
"""
Brain dispatch event. EDGE-TRIGGERED on scan_number monotonic advance.
Never timer-synthesized. No new scan = no BarFire.
"""
event_type: EventType = EventType.BAR_FIRE
scan_number: int = 0
symbol: str = ""
data: Any = None
provenance: EventProvenance = None
def __post_init__(self):
if self.provenance is None:
object.__setattr__(self, 'provenance', EventProvenance(
scan_number=self.scan_number,
scan_ts=time.time_ns(),
ingest_ts=time.time_ns(),
source="live",
))
@dataclass(frozen=True, slots=True)
class StaleInput:
"""
Freshness watchdog alarm. Emitted when:
now - last_scan_ts > 1.5x cadence
This is the June-22 scan-freeze / silent-scanner-death detector, native.
Fixes BLUE's oldest observability hole as a side effect.
"""
event_type: EventType = EventType.STALE_INPUT
source: str = ""
last_scan_ts: int = 0
current_ts: int = 0
stale_duration_ns: int = 0
provenance: EventProvenance = None
def __post_init__(self):
if self.provenance is None:
object.__setattr__(self, 'provenance', EventProvenance(
scan_number=0,
scan_ts=self.last_scan_ts,
ingest_ts=time.time_ns(),
source="live",
))