Skip to main content
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

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.
# 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 TypeWhen Recorded
ORDER_SUBMITTEDOrder passes compliance gate
ORDER_FILLEDFill recorded
ORDER_CANCELEDCancellation recorded
ORDER_REJECTEDCompliance rejects order
ORDER_AMENDEDOrder amendment
POSITION_CHANGEDPosition update
CONFIG_CHANGEDCompliance config or role change
KILL_SWITCH_ACTIVATEDEmergency halt activated
KILL_SWITCH_DEACTIVATEDEmergency halt deactivated
APPROVAL_REQUESTEDOrder held for review
APPROVAL_GRANTEDReviewer approves
APPROVAL_DENIEDReviewer denies
APPROVAL_EXPIREDRequest auto-expired
SURVEILLANCE_ALERTSurveillance detector fires
LIMIT_BREACHRegulatory limit breached
REPORT_GENERATEDCompliance report created
DATA_PURGEDRetention purge executed

Trade Surveillance

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

Detectors

DetectorWhat It CatchesSeverity
Wash TradingBuy + sell same market within thresholdCritical
SpoofingOrder placed and canceled before fillWarning
LayeringN stacked orders on one sideWarning
Rapid-FireOrder rate exceeding limit/minuteWarning
ConcentrationSingle market exceeding portfolio shareWarning
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

# 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.
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

# 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

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.
RolePermissions
ViewerView positions, feeds, orders, audit trail, alerts, reports
TraderSubmit/cancel orders, view positions/feeds/orders/alerts
Risk ManagerTrader + kill switch, modify risk config
Compliance OfficerRisk Manager + approve/deny orders, resolve alerts, generate reports, modify compliance config, export audit
AdminCompliance Officer + manage roles, purge data
# 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.
LimitDescription
restricted_marketsBlacklisted market IDs - orders rejected immediately
max_position_per_marketPer-market position size cap
max_total_notionalPortfolio-wide notional cap
max_concentration_pctMax % of portfolio in one market (default 25%)
max_daily_volumeDaily traded volume cap
max_orders_per_minuteOrder rate limit (default 120)
max_cancel_ratioMax cancel-to-order ratio (default 95%)
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.
# 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

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

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

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).
# 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.
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():
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

FieldTypeDefaultDescription
enabledboolTrueMaster switch for all compliance checks
audit_enabledboolTrueEnable audit trail recording
surveillance_enabledboolTrueEnable trade surveillance
approval_enabledboolFalseEnable human-in-the-loop approval
regulatory_limitsRegulatoryLimitConfig(defaults)Firm-wide limits
approval_threshold_notionalfloat5000Notional threshold for approval
approval_timeout_secsfloat300Approval request TTL
retention_daysint2555Data retention period (~7 years)
db_pathstr | NoneNoneSQLite database path
surveillance_window_secsfloat300Sliding window for surveillance
wash_trade_threshold_secsfloat5.0Buy+sell same market threshold
spoof_cancel_threshold_secsfloat2.0Order+cancel threshold
layering_depthint3Stacked orders threshold
rolesdict[str, Role]{}Actor-to-role mapping
kill_switch_requires_roleRoleRISK_MANAGERMinimum role for kill switch
approval_callbackCallable | NoneNoneNotification callback
webhook_urlstr | NoneNoneWebhook URL for alerts

RegulatoryLimitConfig

FieldTypeDefaultDescription
max_position_per_marketdict[str, float]{}Per-market position limits
max_total_notionalfloatinfTotal portfolio notional cap
max_concentration_pctfloat25.0Max single-market concentration
restricted_marketsset[str]set()Blacklisted market IDs
max_daily_volumefloatinfDaily volume cap
max_orders_per_minuteint120Order rate limit
max_cancel_ratiofloat0.95Max cancel-to-order ratio