---
title: "Compliance"
description: "Regulatory compliance module for prediction market trading - audit trails, surveillance, RBAC, approval workflows, and reporting."
---

As prediction markets become regulated financial products, Horizon provides a compliance module that gives hedge funds and institutional traders the infrastructure required for regulatory oversight.

## Quick Start

```python
import horizon as hz
from horizon.compliance import (
    ComplianceManager,
    ComplianceConfig,
    RegulatoryLimitConfig,
    Role,
    compliance_gate,
)

# Configure compliance
config = ComplianceConfig(
    enabled=True,
    roles={
        "alice": Role.TRADER,
        "bob": Role.COMPLIANCE_OFFICER,
        "charlie": Role.RISK_MANAGER,
        "admin": Role.ADMIN,
    },
    approval_enabled=True,
    approval_threshold_notional=10_000,
    regulatory_limits=RegulatoryLimitConfig(
        max_total_notional=1_000_000,
        max_concentration_pct=25.0,
        restricted_markets={"BANNED-MARKET-123"},
        max_daily_volume=500_000,
        max_orders_per_minute=120,
    ),
)

compliance = ComplianceManager(config)

# Use in pipeline
hz.run(
    name="compliant_strategy",
    pipeline=[compliance_gate, my_signal, my_sizer],
    markets=["will-x-happen"],
    compliance=compliance,
)
```

## Architecture

The compliance module is Python-only (not on the hot path) and uses a separate SQLite database so compliance data survives strategy changes. All components are thread-safe.

```
ComplianceManager (orchestrator)
├── AuditTrail         - SHA-256 hash chain, tamper-evident
├── TradeSurveillance  - 5 real-time detectors
├── ApprovalGateway    - async human-in-the-loop
├── AccessControl      - RBAC with 5 roles
├── RegulatoryLimits   - firm-wide position/exposure limits
├── ReportGenerator    - daily, position, and compliance reports
├── ComplianceKillSwitch - RBAC-gated emergency halt
└── RetentionManager   - SEC 17a-4 data retention
```

## Audit Trail

Every compliance event is recorded with a SHA-256 hash chain. Each event links to its predecessor, creating a tamper-evident log that auditors can verify end-to-end.

```python
# Record events (happens automatically when using ComplianceManager)
compliance.record_fill("will-btc-100k", "buy", 50.0, 0.62)

# Verify integrity
valid, events_checked = compliance.verify_audit_integrity()
assert valid  # True if no tampering detected

# Export for regulatory submission
compliance.export_audit("audit_2026_q1.csv", start=q1_start, end=q1_end)
```

### Tracked Event Types

| Event Type | When Recorded |
|---|---|
| `ORDER_SUBMITTED` | Order passes compliance gate |
| `ORDER_FILLED` | Fill recorded |
| `ORDER_CANCELED` | Cancellation recorded |
| `ORDER_REJECTED` | Compliance rejects order |
| `ORDER_AMENDED` | Order amendment |
| `POSITION_CHANGED` | Position update |
| `CONFIG_CHANGED` | Compliance config or role change |
| `KILL_SWITCH_ACTIVATED` | Emergency halt activated |
| `KILL_SWITCH_DEACTIVATED` | Emergency halt deactivated |
| `APPROVAL_REQUESTED` | Order held for review |
| `APPROVAL_GRANTED` | Reviewer approves |
| `APPROVAL_DENIED` | Reviewer denies |
| `APPROVAL_EXPIRED` | Request auto-expired |
| `SURVEILLANCE_ALERT` | Surveillance detector fires |
| `LIMIT_BREACH` | Regulatory limit breached |
| `REPORT_GENERATED` | Compliance report created |
| `DATA_PURGED` | Retention purge executed |

## Trade Surveillance

Real-time detection of manipulative trading patterns using sliding-window analysis. Runs once per strategy cycle via `on_cycle()`.

### Detectors

| Detector | What It Catches | Severity |
|---|---|---|
| **Wash Trading** | Buy + sell same market within threshold | Critical |
| **Spoofing** | Order placed and canceled before fill | Warning |
| **Layering** | N stacked orders on one side | Warning |
| **Rapid-Fire** | Order rate exceeding limit/minute | Warning |
| **Concentration** | Single market exceeding portfolio share | Warning |

```python
config = ComplianceConfig(
    surveillance_enabled=True,
    surveillance_window_secs=300,       # 5-minute window
    wash_trade_threshold_secs=5.0,      # buy+sell within 5s
    spoof_cancel_threshold_secs=2.0,    # order+cancel within 2s
    layering_depth=3,                   # 3+ stacked orders
    regulatory_limits=RegulatoryLimitConfig(
        max_orders_per_minute=120,
        max_concentration_pct=25.0,
    ),
)
```

### Accessing Alerts

```python
# Latest alerts from last cycle
alerts = compliance.surveillance.recent_alerts

# Query historical alerts
from horizon.compliance import ComplianceStore
alerts = compliance.store.query_alerts(
    start=yesterday_ts,
    severity="critical",
    resolved=False,
    limit=100,
)

# Resolve an alert
compliance.store.resolve_alert(alert_id, resolved_by="bob", notes="False positive")
```

## Human-in-the-Loop Approval

Orders exceeding a notional threshold are held for manual review. The strategy loop is **never blocked** - pending orders are queued and submitted asynchronously when approved.

```python
config = ComplianceConfig(
    approval_enabled=True,
    approval_threshold_notional=10_000,
    approval_timeout_secs=300,          # 5-minute expiry
    approval_callback=my_notification_fn,  # optional webhook
)
```

### Approval Workflow

```python
# 1. Order above threshold → held for review
result = compliance.check_order("market-x", "buy", 0.65, 20_000, actor="alice")
# result == ComplianceAction.PENDING_APPROVAL

# 2. Reviewer sees pending orders
pending = compliance.approval.pending()

# 3. Approve or deny
compliance.approval.approve(pending[0].request_id, "bob", notes="within risk budget")
# or
compliance.approval.deny(pending[0].request_id, "bob", notes="too concentrated")

# 4. Approved orders are submitted on next cycle via drain_approved()
```

### Notification Callback

```python
def notify_slack(request: ApprovalRequest):
    """Send approval request to Slack (or any webhook)."""
    requests.post(SLACK_WEBHOOK, json={
        "text": f"Order pending approval: {request.market_id} "
                f"notional=${request.order_data['price'] * request.order_data['size']:,.0f} "
                f"(expires in {request.expires_at - request.timestamp:.0f}s)",
    })

config = ComplianceConfig(
    approval_enabled=True,
    approval_callback=notify_slack,
)
```

## Role-Based Access Control (RBAC)

Five hierarchical roles control who can perform compliance-sensitive operations.

| Role | Permissions |
|---|---|
| **Viewer** | View positions, feeds, orders, audit trail, alerts, reports |
| **Trader** | Submit/cancel orders, view positions/feeds/orders/alerts |
| **Risk Manager** | Trader + kill switch, modify risk config |
| **Compliance Officer** | Risk Manager + approve/deny orders, resolve alerts, generate reports, modify compliance config, export audit |
| **Admin** | Compliance Officer + manage roles, purge data |

```python
# Check permission
can_approve = compliance.access.check_permission("alice", "approve_order")

# Require permission (raises PermissionError if denied)
compliance.access.require_permission("bob", "approve_order")

# Assign roles (requires Admin)
compliance.access.assign_role("new_trader", Role.TRADER, assigned_by="admin")

# List current roles
roles = compliance.access.list_roles()
# {"alice": "trader", "bob": "compliance_officer", ...}
```

## Regulatory Limits

Firm-wide limits enforced **before** order submission, separate from strategy-level `RiskConfig`.

| Limit | Description |
|---|---|
| `restricted_markets` | Blacklisted market IDs - orders rejected immediately |
| `max_position_per_market` | Per-market position size cap |
| `max_total_notional` | Portfolio-wide notional cap |
| `max_concentration_pct` | Max % of portfolio in one market (default 25%) |
| `max_daily_volume` | Daily traded volume cap |
| `max_orders_per_minute` | Order rate limit (default 120) |
| `max_cancel_ratio` | Max cancel-to-order ratio (default 95%) |

```python
limits = RegulatoryLimitConfig(
    restricted_markets={"ILLEGAL-MARKET"},
    max_position_per_market={"BTC-ABOVE-100K": 1000.0},
    max_total_notional=5_000_000,
    max_concentration_pct=20.0,
    max_daily_volume=2_000_000,
    max_orders_per_minute=60,
)

# Check utilization
util = compliance.limits.current_utilization(engine)
# {"total_notional": 45.2, "daily_volume": 12.8, "position_BTC-ABOVE-100K": 30.0}
```

## Kill Switch

Emergency halt with RBAC enforcement. Requires `RISK_MANAGER` role or higher. Snapshots all open positions at activation time for post-incident review.

```python
# Activate (cancels all orders, prevents new ones)
compliance.kill_switch.activate(
    reason="Flash crash detected",
    actor="charlie",  # must be risk_manager+
)

# Check status
status = compliance.kill_switch.status()
# {"active": True, "activated_by": "charlie", "reason": "Flash crash detected", ...}

# Deactivate with justification
compliance.kill_switch.deactivate(
    actor="charlie",
    justification="Market stabilized, positions reviewed",
)
```

## Reporting

Generate regulatory-ready reports for internal review and filing.

### Daily Trade Report

```python
report = compliance.daily_report("2026-03-12")
print(f"Orders: {report.total_orders}")
print(f"Fills: {report.total_fills}")
print(f"Volume: ${report.total_volume:,.2f}")
print(f"Alerts: {report.surveillance_alerts}")
for entry in report.by_market:
    print(f"  {entry['market_id']}: {entry['orders']} orders, ${entry['volume']:,.2f}")
```

### Position Report

```python
report = compliance.position_report()
print(f"Total notional: ${report.total_notional:,.2f}")
for market, pct in report.concentration.items():
    print(f"  {market}: {pct:.1f}%")
```

### Full Compliance Report

```python
import time
report = compliance.compliance_report(
    period_start=time.time() - 86400,
    period_end=time.time(),
)
print(f"Chain integrity: {report.chain_integrity}")
print(f"Kill switch events: {report.kill_switch_events}")
print(f"Surveillance alerts: {report.surveillance_summary}")
```

## Data Retention

SEC Rule 17a-4 requires 6+ years of record retention. Default is 2,555 days (~7 years).

```python
# Check retention status
status = compliance.retention.check_retention_status()
print(f"Total events: {status['total_events']}")

# Archive old records to CSV, then purge
result = compliance.retention.archive_and_purge(
    archive_dir="/secure/compliance-archive/",
    actor="admin",
)
print(f"Archived: {result['events_archived']}, Purged: {result['events_purged']}")
```

## Pipeline Integration

Use `compliance_gate` as the first function in your pipeline to automatically enforce compliance on every cycle.

```python
hz.run(
    name="institutional_mm",
    exchange=hz.Polymarket(...),
    markets=["will-x-happen"],
    pipeline=[
        compliance_gate,   # compliance checks first
        my_signal,
        my_sizer,
        hz.market_maker,
    ],
    compliance=compliance,  # binds to engine automatically
)
```

When the kill switch is active, `compliance_gate` returns `None`, which halts the pipeline for that cycle. Surveillance runs every cycle. Stale approvals are auto-expired.

## Standalone Usage

The compliance module works independently from `hz.run()`:

```python
from horizon.compliance import ComplianceManager, ComplianceConfig, Role

config = ComplianceConfig(
    roles={"alice": Role.TRADER, "bob": Role.COMPLIANCE_OFFICER},
    regulatory_limits=RegulatoryLimitConfig(
        max_total_notional=1_000_000,
    ),
)

compliance = ComplianceManager(config)
compliance.bind_engine(engine)

# Manual pre-trade check
action = compliance.check_order("market-1", "buy", 0.55, 100, actor="alice")
if action == ComplianceAction.APPROVED:
    engine.submit_quotes(...)

# Always close when done
compliance.close()
```

## Configuration Reference

### ComplianceConfig

| Field | Type | Default | Description |
|---|---|---|---|
| `enabled` | `bool` | `True` | Master switch for all compliance checks |
| `audit_enabled` | `bool` | `True` | Enable audit trail recording |
| `surveillance_enabled` | `bool` | `True` | Enable trade surveillance |
| `approval_enabled` | `bool` | `False` | Enable human-in-the-loop approval |
| `regulatory_limits` | `RegulatoryLimitConfig` | (defaults) | Firm-wide limits |
| `approval_threshold_notional` | `float` | `5000` | Notional threshold for approval |
| `approval_timeout_secs` | `float` | `300` | Approval request TTL |
| `retention_days` | `int` | `2555` | Data retention period (~7 years) |
| `db_path` | `str \| None` | `None` | SQLite database path |
| `surveillance_window_secs` | `float` | `300` | Sliding window for surveillance |
| `wash_trade_threshold_secs` | `float` | `5.0` | Buy+sell same market threshold |
| `spoof_cancel_threshold_secs` | `float` | `2.0` | Order+cancel threshold |
| `layering_depth` | `int` | `3` | Stacked orders threshold |
| `roles` | `dict[str, Role]` | `{}` | Actor-to-role mapping |
| `kill_switch_requires_role` | `Role` | `RISK_MANAGER` | Minimum role for kill switch |
| `approval_callback` | `Callable \| None` | `None` | Notification callback |
| `webhook_url` | `str \| None` | `None` | Webhook URL for alerts |

### RegulatoryLimitConfig

| Field | Type | Default | Description |
|---|---|---|---|
| `max_position_per_market` | `dict[str, float]` | `{}` | Per-market position limits |
| `max_total_notional` | `float` | `inf` | Total portfolio notional cap |
| `max_concentration_pct` | `float` | `25.0` | Max single-market concentration |
| `restricted_markets` | `set[str]` | `set()` | Blacklisted market IDs |
| `max_daily_volume` | `float` | `inf` | Daily volume cap |
| `max_orders_per_minute` | `int` | `120` | Order rate limit |
| `max_cancel_ratio` | `float` | `0.95` | Max cancel-to-order ratio |
