144 lines
4.6 KiB
Python
144 lines
4.6 KiB
Python
"""Hook event system — emit events, register handlers, dispatch callbacks."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import fnmatch
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime, timezone
|
|
from typing import Any, Awaitable, Callable
|
|
|
|
from salior.core.logging import setup_logging
|
|
|
|
log = setup_logging()
|
|
|
|
|
|
@dataclass
|
|
class HookEvent:
|
|
"""A hook event with metadata."""
|
|
name: str # e.g. "on_signal", "on_fill", "on_error"
|
|
data: dict # Event payload
|
|
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
source: str = "" # Which agent emitted this
|
|
|
|
|
|
HookHandler = Callable[[HookEvent], Awaitable[None]] | Callable[[HookEvent], None]
|
|
|
|
|
|
class HookRegistry:
|
|
"""Register and dispatch hook handlers."""
|
|
|
|
def __init__(self) -> None:
|
|
self._handlers: dict[str, list[HookHandler]] = {}
|
|
|
|
def on(self, event_name: str, handler: HookHandler) -> None:
|
|
"""Register a handler for an event."""
|
|
if event_name not in self._handlers:
|
|
self._handlers[event_name] = []
|
|
# Prevent duplicate registration
|
|
if handler not in self._handlers[event_name]:
|
|
self._handlers[event_name].append(handler)
|
|
log.debug("hook_registered", event=event_name, handler=handler.__name__)
|
|
|
|
def off(self, event_name: str, handler: HookHandler) -> None:
|
|
"""Unregister a handler."""
|
|
if event_name in self._handlers:
|
|
try:
|
|
self._handlers[event_name].remove(handler)
|
|
log.debug("hook_unregistered", event=event_name, handler=handler.__name__)
|
|
except ValueError:
|
|
pass
|
|
|
|
async def emit(self, event: HookEvent) -> None:
|
|
"""Fire all handlers for an event, async-safe."""
|
|
handlers = self._handlers.get(event.name, [])
|
|
if not handlers:
|
|
return
|
|
|
|
log.debug("hook_fired", event=event.name, count=len(handlers), data=event.data)
|
|
|
|
for handler in handlers:
|
|
try:
|
|
if asyncio.iscoroutinefunction(handler):
|
|
await handler(event)
|
|
else:
|
|
handler(event)
|
|
except Exception as e:
|
|
log.error("hook_handler_error", event=event.name, handler=handler.__name__, error=str(e))
|
|
|
|
def list(self) -> dict[str, int]:
|
|
"""List registered hooks with handler counts."""
|
|
return {name: len(handlers) for name, handlers in self._handlers.items()}
|
|
|
|
|
|
# Global registry — shared across all agents
|
|
global_hooks = HookRegistry()
|
|
|
|
|
|
# ─── Built-in hook events ─────────────────────────────────────────────────────
|
|
|
|
async def on_signal(coin: str, regime: str, conviction: float, reasoning: str) -> None:
|
|
"""Emit a signal event."""
|
|
await global_hooks.emit(HookEvent(
|
|
name="on_signal",
|
|
source="signal_agent",
|
|
data={"coin": coin, "regime": regime, "conviction": conviction, "reasoning": reasoning},
|
|
))
|
|
|
|
|
|
async def on_fill(
|
|
coin: str,
|
|
side: str,
|
|
size: float,
|
|
price: float,
|
|
exec_id: str,
|
|
mode: str,
|
|
) -> None:
|
|
"""Emit a fill event when an order fills."""
|
|
await global_hooks.emit(HookEvent(
|
|
name="on_fill",
|
|
source="exec_agent",
|
|
data={"coin": coin, "side": side, "size": size, "price": price, "exec_id": exec_id, "mode": mode},
|
|
))
|
|
|
|
|
|
async def on_execution(
|
|
coin: str,
|
|
side: str,
|
|
size: float,
|
|
price: float,
|
|
status: str,
|
|
error: str | None = None,
|
|
) -> None:
|
|
"""Emit an execution event (placed, filled, cancelled, failed)."""
|
|
await global_hooks.emit(HookEvent(
|
|
name="on_execution",
|
|
source="exec_agent",
|
|
data={"coin": coin, "side": side, "size": size, "price": price, "status": status, "error": error},
|
|
))
|
|
|
|
|
|
async def on_error(agent: str, error: str, details: dict | None = None) -> None:
|
|
"""Emit an error event."""
|
|
await global_hooks.emit(HookEvent(
|
|
name="on_error",
|
|
source=agent,
|
|
data={"agent": agent, "error": error, "details": details or {}},
|
|
))
|
|
|
|
|
|
async def on_risk_breach(reason: str, details: dict) -> None:
|
|
"""Emit a risk breach event."""
|
|
await global_hooks.emit(HookEvent(
|
|
name="on_risk_breach",
|
|
source="risk_agent",
|
|
data={"reason": reason, "details": details},
|
|
))
|
|
|
|
|
|
async def on_agent_health(agent: str, status: str, iteration: int) -> None:
|
|
"""Emit a health heartbeat."""
|
|
await global_hooks.emit(HookEvent(
|
|
name="on_agent_health",
|
|
source=agent,
|
|
data={"agent": agent, "status": status, "iteration": iteration},
|
|
)) |