Skip to main content
The FundCluster orchestrates multiple FundManager instances as a single unit. Each fund runs independently with its own strategies, capital, and oversight loop. The cluster provides aggregate views and cross-fund rebalancing.

Quick Start

from horizon.fund import FundManager, FundConfig, FundCluster

# Create individual funds
crypto_fund = FundManager(FundConfig(
    total_capital=50_000,
    max_fund_drawdown_pct=20.0,
))

politics_fund = FundManager(FundConfig(
    total_capital=30_000,
    max_fund_drawdown_pct=10.0,
))

sports_fund = FundManager(FundConfig(
    total_capital=20_000,
    max_fund_drawdown_pct=15.0,
))

# Create cluster
cluster = FundCluster()
cluster.add_fund("crypto", crypto_fund)
cluster.add_fund("politics", politics_fund)
cluster.add_fund("sports", sports_fund)

# Start all funds
cluster.start_all()

# Aggregate status
status = cluster.aggregate_status()
# {
#     "total_nav": 100000.0,
#     "fund_count": 3,
#     "funds": {
#         "crypto": {"nav": 50000.0, "drawdown_pct": 0.0, ...},
#         "politics": {"nav": 30000.0, "drawdown_pct": 0.0, ...},
#         "sports": {"nav": 20000.0, "drawdown_pct": 0.0, ...},
#     }
# }

# Stop all
cluster.stop_all()

Aggregate Views

Status

aggregate_status() combines NAV across all funds and provides a per-fund breakdown:
status = cluster.aggregate_status()
print(f"Total NAV: ${status['total_nav']:,.2f}")
print(f"Funds: {status['fund_count']}")

for name, fund_status in status["funds"].items():
    print(f"  {name}: NAV ${fund_status['nav']:,.2f}, DD {fund_status['drawdown_pct']:.1f}%")

Risk

aggregate_risk() computes weighted average drawdown and identifies the riskiest fund:
risk = cluster.aggregate_risk()
# {
#     "weighted_avg_drawdown_pct": 3.2,
#     "max_single_fund_drawdown_pct": 8.5,
#     "max_drawdown_fund": "crypto",
#     "fund_count": 3,
# }
Weights are proportional to each fund’s NAV. A fund with 50% of total NAV contributes 50% to the weighted average drawdown.

Cross-Fund Rebalancing

Move capital between funds to maintain target allocations:
# Define target weights (must sum to ~1.0)
targets = cluster.rebalance_across_funds({
    "crypto": 0.5,
    "politics": 0.3,
    "sports": 0.2,
})
# {
#     "crypto": {"current": 48000.0, "target": 50000.0, "delta": 2000.0},
#     "politics": {"current": 32000.0, "target": 30000.0, "delta": -2000.0},
#     "sports": {"current": 20000.0, "target": 20000.0, "delta": 0.0},
# }
The rebalance method computes target allocations and deltas but does not move capital automatically. This is intentional: capital movement across funds is a high-impact action that should be reviewed before execution. Weight validation:
  • Weights must sum to between 0.99 and 1.01
  • All referenced fund names must exist in the cluster
  • ValueError is raised on validation failure

Fund Access

# Access individual funds by name
crypto = cluster.fund("crypto")
crypto_status = crypto.status()

# List all fund names
names = cluster.fund_names
# ["crypto", "politics", "sports"]

Lifecycle

# Add a fund
cluster.add_fund("new_fund", new_fund_manager)

# Remove a fund (does not stop it)
cluster.remove_fund("sports")

# Start all funds
cluster.start_all()

# Stop all funds
cluster.stop_all()
Duplicate fund names raise ValueError. Removing a non-existent fund also raises ValueError.