---
title: "Fund Operations"
description: "Alerts, double-entry ledger, paper-to-live promotion, and self-healing recovery."
---

Operational infrastructure that runs alongside the oversight loop. These subsystems handle notifications, accounting, strategy promotion, and automatic recovery from failures.

## Alerts and Notifications

Rate-limited alert system with multi-channel delivery. Alerts fire during the oversight loop for risk events, kill switch activations, strategy failures, and settlements.

### Alert Levels and Categories

| Level | Use |
|---|---|
| `INFO` | Routine events (settlement detected, strategy promoted) |
| `WARNING` | Approaching limits (80% drawdown, strategy paused) |
| `CRITICAL` | Immediate action needed (kill switch, multiple failures) |

| Category | Triggered By |
|---|---|
| `KILL_SWITCH` | Fund drawdown exceeds limit |
| `STRATEGY_FAILURE` | Strategy thread crashes |
| `DRAWDOWN` | Fund or strategy drawdown approaching limit |
| `SETTLEMENT` | Market resolved, P&L booked |
| `CORRELATION` | Cross-strategy correlation exceeds threshold |
| `RISK_LIMIT` | VaR budget breach |
| `RECOVERY` | Self-healing restart attempt |
| `PROMOTION` | Strategy promoted to next stage |
| `REBALANCE` | Capital reallocation triggered |
| `GENERAL` | Everything else |

### Rate Limiting

Each category is limited to 5 alerts per 60 seconds. Excess alerts are silently dropped to prevent notification floods during volatile periods.

### Channels

```python
from horizon.fund import FundConfig

# Webhook channel -- delivers alerts via HTTP POST
fund = FundManager(FundConfig(
    total_capital=100_000,
    alert_webhook_url="https://hooks.slack.com/services/...",
))

# The webhook sends JSON:
# {
#     "level": "CRITICAL",
#     "category": "KILL_SWITCH",
#     "title": "Kill switch activated",
#     "message": "Fund drawdown 15.2% exceeded limit 15.0%",
#     "timestamp": "2026-03-13T14:30:00.000000"
# }
```

Webhook delivery runs in a background thread to avoid blocking the oversight loop. Failed deliveries are logged but do not retry.

### MCP Tool

```
fund_alerts(limit=50)
```

Returns recent alerts with counts by category. Use this to check what happened while the LLM was idle.

## Double-Entry Ledger

SQLite-backed accounting ledger that records every financial event as a debit/credit journal entry. Provides balance sheet and income statement views.

### Accounts

| Account | Type | Tracks |
|---|---|---|
| `CASH` | Asset | Available capital |
| `POSITIONS` | Asset | Capital deployed in positions |
| `FEES_RECEIVABLE` | Asset | Fees owed (internal) |
| `MANAGEMENT_FEES` | Revenue | Annual management fee accruals |
| `PERFORMANCE_FEES` | Revenue | Performance fee accruals |
| `REALIZED_PNL` | Revenue | Booked profit and loss |
| `UNREALIZED_PNL` | Revenue | Mark-to-market P&L |

### Journal Entries

Every financial event creates a balanced journal entry:

| Event | Debit | Credit | Amount |
|---|---|---|---|
| Deposit | CASH | (external) | Deposit amount |
| Withdrawal | (external) | CASH | Withdrawal amount |
| Profitable trade | CASH | REALIZED_PNL | Profit |
| Losing trade | REALIZED_PNL | CASH | Loss |
| Management fee | MANAGEMENT_FEES | FEES_RECEIVABLE | Fee amount |
| Performance fee | PERFORMANCE_FEES | FEES_RECEIVABLE | Fee amount |

### Configuration

```python
fund = FundManager(FundConfig(
    total_capital=100_000,
    ledger_enabled=True,  # Enable double-entry ledger
))
```

When enabled, the oversight loop automatically records:
- Settlement P&L when markets resolve
- Management and performance fee accruals each tick
- All amounts validated (must be finite, positive)

### MCP Tool

```
fund_ledger(limit=50)
```

Returns recent journal entries, balance sheet (assets vs. liabilities), and income statement.

### Reports

```python
# Balance sheet
bs = fund.ledger.balance_sheet()
# {"CASH": 95000.0, "POSITIONS": 5000.0, "MANAGEMENT_FEES": 200.0, ...}

# Income statement
inc = fund.ledger.income_statement()
# {"REALIZED_PNL": 1500.0, "MANAGEMENT_FEES": -200.0, "PERFORMANCE_FEES": -100.0}

# Recent entries
entries = fund.ledger.recent_entries(limit=20)
```

## Paper-to-Live Promotion

Structured lifecycle that gates strategy promotion behind performance criteria. Strategies must prove themselves in paper trading before progressing to shadow mode and then live trading.

### Promotion Stages

```
PAPER --> SHADOW --> LIVE
```

| Stage | Description |
|---|---|
| **PAPER** | Simulated execution, no real capital |
| **SHADOW** | Runs alongside live but orders not submitted |
| **LIVE** | Real execution with real capital |

### Promotion Criteria

Each transition requires meeting minimum thresholds:

| Criteria | Default | Description |
|---|---|---|
| `min_paper_days` | 14 | Minimum days in current stage |
| `min_trades` | 50 | Minimum number of trades |
| `min_sharpe` | 1.0 | Minimum Sharpe ratio |
| `max_drawdown_pct` | 20.0 | Maximum drawdown percentage |
| `min_win_rate` | 0.45 | Minimum win rate |

### Configuration

```python
fund = FundManager(FundConfig(
    total_capital=100_000,
    promotion_enabled=True,  # Enable promotion lifecycle
))
```

When enabled, the oversight loop checks promotion eligibility every 60 ticks. Promotions are recorded with a full audit trail in SQLite (strategy name, from/to stage, timestamp, metrics at promotion time).

### MCP Tool

```
fund_promotion_status()
```

Returns current promotion stage for each strategy and promotion history.

### Persistence

Two SQLite tables track state:
- `promotions`: current stage per strategy
- `promotion_history`: every promotion event with timestamp and reason

Data is stored in `~/.horizon/promotion.db` with WAL mode and `0o600` permissions.

## Self-Healing Recovery

Automatic strategy restart with exponential backoff and circuit breaker. Detects failed strategies and stale feeds, attempts recovery without human intervention.

### How It Works

1. The oversight loop calls `recovery.check_strategies()` every 10 ticks
2. For each failed strategy, it attempts a restart via `controller.restart()`
3. If the restart fails, backoff doubles: 60s, 120s, 240s...
4. After 3 failed attempts, the circuit breaker opens and the strategy is marked permanently failed

### Circuit Breaker

| Attempt | Cooldown | Action |
|---|---|---|
| 1 | 60s | Restart strategy |
| 2 | 120s | Restart strategy |
| 3 | 240s | Restart strategy |
| 4+ | -- | Circuit breaker open, no more retries |

To re-enable recovery for a permanently failed strategy:

```python
fund.recovery.reset("strategy_name")
```

### Feed Monitoring

The recovery manager also detects stale feeds (no update for 300+ seconds) and logs warnings. Feed staleness is informational; automatic feed reconnection is handled by the feed manager.

### Configuration

```python
fund = FundManager(FundConfig(
    total_capital=100_000,
    self_healing_enabled=True,  # Enable self-healing
))
```

### Recovery Events

Every recovery attempt is logged:

```python
events = fund.recovery.event_history(limit=20)
# [
#     RecoveryEvent(
#         action="restart_strategy",
#         target="political_mm",
#         success=True,
#         attempt=1,
#         error=None,
#     ),
# ]

stats = fund.recovery.stats()
# {"total_attempts": 5, "successes": 3, "failures": 2, "circuit_breakers_open": 1}
```

## Configuration Summary

| Parameter | Default | Description |
|---|---|---|
| `alert_webhook_url` | None | Webhook URL for alert delivery |
| `ledger_enabled` | False | Enable double-entry accounting |
| `promotion_enabled` | False | Enable paper-to-live promotion |
| `self_healing_enabled` | False | Enable automatic recovery |

All four subsystems are off by default. Enable them individually in `FundConfig`. They integrate with the oversight loop automatically when enabled.
