Votre système génère 6 chiffres par an. Et maintenant ? Analyse de capacité, infrastructure multi-VM distribuée via Nomad, nouvelles stratégies accessibles post-million, licensing de vos signaux, structuration en family office mono-personne, et les quatre scénarios de sortie. Le dernier chapitre — celui où la machine vous libère.
Vous avez franchi le million. Félicitations — et bienvenue dans un nouveau problème. À 100K€, vous étiez un bruit de fond dans le carnet d'ordres. À 1M€, vous commencez à laisser une empreinte. À 3M€, certaines de vos stratégies small-cap sont physiquement impossibles à exécuter sans impact de marché significatif.
L'analyse de capacité (capacity analysis) est la discipline qui quantifie combien de capital chaque stratégie peut absorber avant que son alpha ne se dégrade. C'est le plafond invisible que 90% des traders retail ignorent — jusqu'au jour où leur Sharpe se compresse sans raison apparente.
Pour une stratégie donnée, la capacité maximale se calcule ainsi :
Capacity = ADV × MaxParticipation × AvgHolding × UniverseSize × DiversificationFactor
Exemple concret : une stratégie momentum sur les mid-caps US (ADV moyen $5M, participation 2%, holding 10 jours, univers de 200 titres, diversification 0.5) → Capacity = $5M × 0.02 × 10 × 200 × 0.5 = $100M. Vous êtes très loin du plafond. En revanche, sur des micro-caps EU (ADV $200K), la même formule donne $2M — votre million est déjà une contrainte.
| Stratégie | US Large/Mid | US Small | EU Large | EU Small | APAC | Contrainte principale |
|---|---|---|---|---|---|---|
| Momentum 20j | $50M+ | $2M | $20M | $500K | $10M | Liquidité small-caps |
| Mean Reversion 5j | $30M | $1.5M | $15M | $400K | $8M | Spread + impact |
| Cross-Asset Rotation | $200M+ | N/A | $100M+ | N/A | $50M+ | ETFs très liquides |
| Breakout Intraday | $5M | $300K | $3M | $150K | $2M | Slippage |
| Stat Arb Pairs | $20M | $800K | $10M | $300K | $5M | Corrélation instable |
| Options Selling | $100M+ | $2M | $30M | N/A | $15M | Gamma risk |
La conclusion est limpide : avec 1-3M€, vous n'avez aucune contrainte de capacité sur les marchés US et EU large-caps. Le problème se pose uniquement sur les small-caps et les marchés APAC peu liquides. La stratégie d'adaptation est simple : au fur et à mesure que le capital croît, migrez les allocations des small-caps vers les large-caps et les ETFs.
package capacity
import (
"fmt"
"math"
"time"
)
// StrategyProfile décrit les caractéristiques d'une stratégie
// pour le calcul de capacité.
type StrategyProfile struct {
Name string
AvgHoldingDays float64
MaxParticipation float64 // fraction du volume quotidien (0.01 = 1%)
UniverseSize int
DiversificationF float64 // 0.3 à 0.7
MinADV float64 // filtre de liquidité minimum en USD
}
// MarketProfile décrit les caractéristiques de liquidité d'un marché.
type MarketProfile struct {
Name string
MedianADV float64 // volume médian quotidien en USD
SpreadBps float64 // spread moyen en basis points
Timezone string
}
// CapacityResult contient le résultat du calcul.
type CapacityResult struct {
Strategy string
Market string
MaxCapacity float64
CurrentAlloc float64
Utilization float64 // CurrentAlloc / MaxCapacity
ImpactCost float64 // coût d'impact estimé en bps
Degradation float64 // dégradation du Sharpe estimée
}
// Analyzer calcule la capacité de chaque stratégie par marché.
type Analyzer struct {
strategies []StrategyProfile
markets []MarketProfile
}
// NewAnalyzer crée un Analyzer avec les profils donnés.
func NewAnalyzer(strategies []StrategyProfile, markets []MarketProfile) *Analyzer {
return &Analyzer{strategies: strategies, markets: markets}
}
// ComputeCapacity calcule la capacité maximale d'une stratégie sur un marché.
func (a *Analyzer) ComputeCapacity(sp StrategyProfile, mp MarketProfile) CapacityResult {
rawCapacity := mp.MedianADV * sp.MaxParticipation * sp.AvgHoldingDays *
float64(sp.UniverseSize) * sp.DiversificationF
// Ajustement pour le spread : plus le spread est large, plus l'impact est élevé
spreadPenalty := 1.0 - (mp.SpreadBps / 10000.0 * 2.0)
if spreadPenalty < 0.5 {
spreadPenalty = 0.5
}
adjustedCapacity := rawCapacity * spreadPenalty
return CapacityResult{
Strategy: sp.Name,
Market: mp.Name,
MaxCapacity: adjustedCapacity,
}
}
// ComputeImpact estime le coût d'impact de marché pour une taille donnée.
// Modèle racine carrée (Almgren-Chriss simplifié) :
// impact_bps = sigma * sqrt(Q / ADV) * kappa
func (a *Analyzer) ComputeImpact(orderSizeUSD, adv, dailyVolatility float64) float64 {
if adv <= 0 {
return 0
}
kappa := 0.5 // constante empirique (calibrer sur vos données)
participationRate := orderSizeUSD / adv
impactBps := dailyVolatility * 10000.0 * math.Sqrt(participationRate) * kappa
return impactBps
}
// ComputeSharpeDegradation estime la dégradation du Sharpe ratio
// quand on scale de baseCapital à targetCapital.
func (a *Analyzer) ComputeSharpeDegradation(
baseSharpe float64,
baseCapital float64,
targetCapital float64,
capacity float64,
) float64 {
if capacity <= 0 {
return 0
}
baseUtil := baseCapital / capacity
targetUtil := targetCapital / capacity
// Modèle : Sharpe_degraded = Sharpe_base * (1 - alpha * utilization^2)
// alpha calibré pour que Sharpe → 0 quand utilization → 1
alpha := 0.8
baseDeg := baseSharpe * (1.0 - alpha*baseUtil*baseUtil)
targetDeg := baseSharpe * (1.0 - alpha*targetUtil*targetUtil)
if baseDeg <= 0 {
return 0
}
return (baseDeg - targetDeg) / baseDeg
}
// FullAnalysis génère un rapport complet de capacité.
func (a *Analyzer) FullAnalysis(currentAllocations map[string]float64) []CapacityResult {
var results []CapacityResult
for _, sp := range a.strategies {
for _, mp := range a.markets {
r := a.ComputeCapacity(sp, mp)
key := fmt.Sprintf("%s_%s", sp.Name, mp.Name)
if alloc, ok := currentAllocations[key]; ok {
r.CurrentAlloc = alloc
r.Utilization = alloc / r.MaxCapacity
r.ImpactCost = a.ComputeImpact(
alloc/sp.AvgHoldingDays,
mp.MedianADV,
0.02, // volatilité quotidienne 2%
)
}
results = append(results, r)
}
}
return results
}
// DefaultStrategies retourne les profils des stratégies de la série.
func DefaultStrategies() []StrategyProfile {
return []StrategyProfile{
{Name: "Momentum20", AvgHoldingDays: 20, MaxParticipation: 0.02,
UniverseSize: 200, DiversificationF: 0.5, MinADV: 1_000_000},
{Name: "MeanRev5", AvgHoldingDays: 5, MaxParticipation: 0.03,
UniverseSize: 150, DiversificationF: 0.4, MinADV: 2_000_000},
{Name: "CrossAsset", AvgHoldingDays: 30, MaxParticipation: 0.01,
UniverseSize: 25, DiversificationF: 0.6, MinADV: 50_000_000},
{Name: "StatArb", AvgHoldingDays: 3, MaxParticipation: 0.02,
UniverseSize: 100, DiversificationF: 0.3, MinADV: 5_000_000},
}
}
// DefaultMarkets retourne les profils de marché standard.
func DefaultMarkets() []MarketProfile {
return []MarketProfile{
{Name: "US_Large", MedianADV: 50_000_000, SpreadBps: 2, Timezone: "America/New_York"},
{Name: "US_Small", MedianADV: 500_000, SpreadBps: 15, Timezone: "America/New_York"},
{Name: "EU_Large", MedianADV: 20_000_000, SpreadBps: 5, Timezone: "Europe/Paris"},
{Name: "EU_Small", MedianADV: 200_000, SpreadBps: 25, Timezone: "Europe/Paris"},
{Name: "APAC", MedianADV: 10_000_000, SpreadBps: 8, Timezone: "Asia/Tokyo"},
}
}
Les chiffres ci-dessus sont des maximums théoriques. En pratique, divisez par 3 pour obtenir la capacité confortable. À 50% d'utilisation, vous commencez à observer une dégradation mesurable du Sharpe. À 80%, votre alpha est compressé de 30-50%. La règle : ne jamais dépasser 30% de la capacité calculée sur une stratégie unique.
Pendant les 11 premiers chapitres, tout tournait sur une seule VM Hetzner. À 1M€+ avec 8-12 stratégies actives, trois sessions de marché (US/EU/APAC), et des pipelines de données de plus en plus gourmands, la VM unique devient un single point of failure inacceptable et un goulot d'étranglement en CPU/mémoire.
| Signal | Symptôme | Seuil critique | Solution |
|---|---|---|---|
| CPU > 80% | Backtests ralentissent l'exécution live | Sustained > 80% pendant le market open | VM dédiée backtest |
| RAM > 90% | OOM kills, swap thrashing | DuckDB + 12 stratégies > 14 GB | Upgrade ou split |
| Latence IBKR > 200ms | Ordres en retard, fills dégradés | Régulièrement > 200ms au market open | VM régionale US |
| 3 sessions simultanées | APAC 01h-08h, EU 08h-17h, US 14h-21h CET | Overlap EU/US = double charge | 1 VM par région |
| Recovery time > 5min | Reboot/crash = ordres orphelins | Pas de failover automatique | Cluster HA |
L'architecture distribuée utilise le HashiStack que vous connaissez : Nomad pour l'orchestration, Consul pour le service mesh et la discovery, Vault pour les secrets, et Tailscale pour le réseau privé inter-VM. Chaque VM est dédiée à une région de marché.
// infra/main.tf — Provisioning 3 VMs Hetzner avec Tailscale
// Terraform + Hetzner provider + cloud-init
terraform {
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
}
variable "hcloud_token" {
sensitive = true
}
variable "tailscale_auth_key" {
sensitive = true
}
variable "regions" {
type = map(object({
location = string
server_type = string
role = string
}))
default = {
us = {
location = "ash" // Ashburn, VA
server_type = "cpx41" // 8 vCPU, 16 GB
role = "server" // Nomad server + client
}
eu = {
location = "fsn1" // Falkenstein, DE
server_type = "cpx41"
role = "client"
}
apac = {
location = "sin" // Singapore
server_type = "cpx31" // 4 vCPU, 8 GB (marché plus petit)
role = "client"
}
}
}
resource "hcloud_ssh_key" "algo" {
name = "algo-trading"
public_key = file("~/.ssh/algo_ed25519.pub")
}
resource "hcloud_server" "algo" {
for_each = var.regions
name = "algo-${each.key}"
server_type = each.value.server_type
location = each.value.location
image = "ubuntu-24.04"
ssh_keys = [hcloud_ssh_key.algo.id]
user_data = templatefile("${path.module}/cloud-init.yaml", {
region = each.key
role = each.value.role
tailscale_key = var.tailscale_auth_key
nomad_server = each.value.role == "server" ? "true" : "false"
consul_server = each.value.role == "server" ? "true" : "false"
})
labels = {
env = "prod"
region = each.key
role = each.value.role
}
}
resource "hcloud_firewall" "algo" {
name = "algo-trading"
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = ["0.0.0.0/0"] // SSH (Tailscale preferred)
}
rule {
direction = "in"
protocol = "udp"
port = "41641"
source_ips = ["0.0.0.0/0"] // Tailscale WireGuard
}
}
resource "hcloud_firewall_attachment" "algo" {
firewall_id = hcloud_firewall.algo.id
server_ids = [for s in hcloud_server.algo : s.id]
}
output "ips" {
value = { for k, s in hcloud_server.algo : k => s.ipv4_address }
}
// jobs/momentum-us.nomad.hcl — Stratégie Momentum déployée sur vm-us
job "momentum-us" {
datacenters = ["ash"]
type = "service"
constraint {
attribute = "${meta.region}"
value = "us"
}
group "strategy" {
count = 1
restart {
attempts = 3
interval = "10m"
delay = "30s"
mode = "fail"
}
network {
port "metrics" { to = 9090 }
port "health" { to = 8080 }
}
service {
name = "momentum-us"
port = "health"
tags = ["strategy", "us", "momentum"]
check {
type = "http"
path = "/healthz"
interval = "30s"
timeout = "5s"
}
connect {
sidecar_service {} // Consul Connect mesh
}
}
task "run" {
driver = "docker"
config {
image = "ghcr.io/yourorg/algo-strategies:latest"
args = ["--strategy=momentum", "--region=us", "--config=/secrets/config.yaml"]
ports = ["metrics", "health"]
}
vault {
policies = ["algo-strategies"]
}
template {
data = <<-EOF
{{ with secret "secret/data/algo/ibkr-us" }}
ibkr_host: {{ .Data.data.host }}
ibkr_port: {{ .Data.data.port }}
ibkr_client_id: {{ .Data.data.client_id }}
{{ end }}
{{ with secret "secret/data/algo/discord" }}
discord_webhook: {{ .Data.data.webhook_url }}
{{ end }}
EOF
destination = "/secrets/config.yaml"
}
resources {
cpu = 2000 // 2 GHz
memory = 4096 // 4 GB
}
}
}
}
package distributed
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"sync"
"time"
consul "github.com/hashicorp/consul/api"
)
// StrategyStatus représente l'état publié par chaque stratégie dans Consul KV.
type StrategyStatus struct {
Name string `json:"name"`
Region string `json:"region"`
State string `json:"state"` // running, paused, error
Positions int `json:"positions"`
Exposure float64 `json:"exposure_usd"`
DailyPnL float64 `json:"daily_pnl_usd"`
LastUpdate time.Time `json:"last_update"`
}
// PortfolioCoordinator centralise la gestion du risque cross-VM.
type PortfolioCoordinator struct {
consul *consul.Client
mu sync.RWMutex
strategies map[string]*StrategyStatus
maxExposure float64
logger *slog.Logger
}
// NewPortfolioCoordinator crée un coordinateur qui écoute Consul.
func NewPortfolioCoordinator(consulAddr string, maxExposure float64) (*PortfolioCoordinator, error) {
cfg := consul.DefaultConfig()
cfg.Address = consulAddr // ex: "100.64.0.1:8500" via Tailscale
client, err := consul.NewClient(cfg)
if err != nil {
return nil, fmt.Errorf("consul connect: %w", err)
}
return &PortfolioCoordinator{
consul: client,
strategies: make(map[string]*StrategyStatus),
maxExposure: maxExposure,
logger: slog.Default(),
}, nil
}
// WatchStrategies surveille les changements de statut des stratégies
// via Consul KV watch.
func (pc *PortfolioCoordinator) WatchStrategies(ctx context.Context) error {
kv := pc.consul.KV()
var lastIndex uint64
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
pairs, meta, err := kv.List("algo/strategies/", &consul.QueryOptions{
WaitIndex: lastIndex,
WaitTime: 30 * time.Second,
})
if err != nil {
pc.logger.Error("consul KV list failed", "error", err)
time.Sleep(5 * time.Second)
continue
}
lastIndex = meta.LastIndex
pc.mu.Lock()
for _, pair := range pairs {
var status StrategyStatus
if err := json.Unmarshal(pair.Value, &status); err != nil {
continue
}
pc.strategies[status.Name+"_"+status.Region] = &status
}
pc.mu.Unlock()
// Vérifier l'exposition totale
pc.checkTotalExposure()
}
}
// checkTotalExposure vérifie que l'exposition totale ne dépasse pas le max.
func (pc *PortfolioCoordinator) checkTotalExposure() {
pc.mu.RLock()
defer pc.mu.RUnlock()
var totalExposure float64
for _, s := range pc.strategies {
totalExposure += s.Exposure
}
utilization := totalExposure / pc.maxExposure
if utilization > 0.9 {
pc.logger.Warn("exposure near limit",
"total", totalExposure,
"max", pc.maxExposure,
"utilization", fmt.Sprintf("%.1f%%", utilization*100),
)
// Envoyer alerte Discord
pc.sendDiscordAlert(fmt.Sprintf(
"EXPOSITION CRITIQUE : $%.0f / $%.0f (%.0f%%)",
totalExposure, pc.maxExposure, utilization*100,
))
}
}
// RequestAllocation demande une allocation au coordinateur.
// Retourne true si l'allocation est acceptée (ne dépasse pas le max).
func (pc *PortfolioCoordinator) RequestAllocation(
strategy string,
region string,
requestedUSD float64,
) bool {
pc.mu.RLock()
defer pc.mu.RUnlock()
var currentTotal float64
for _, s := range pc.strategies {
currentTotal += s.Exposure
}
if currentTotal+requestedUSD > pc.maxExposure {
pc.logger.Warn("allocation rejected",
"strategy", strategy,
"region", region,
"requested", requestedUSD,
"current_total", currentTotal,
"headroom", pc.maxExposure-currentTotal,
)
return false
}
return true
}
// PublishStatus publie le statut d'une stratégie dans Consul KV.
func (pc *PortfolioCoordinator) PublishStatus(status StrategyStatus) error {
data, err := json.Marshal(status)
if err != nil {
return err
}
key := fmt.Sprintf("algo/strategies/%s_%s", status.Name, status.Region)
_, err = pc.consul.KV().Put(&consul.KVPair{
Key: key,
Value: data,
}, nil)
return err
}
func (pc *PortfolioCoordinator) sendDiscordAlert(msg string) {
// Implémenté via webhook Discord — voir Part 10
}
Avec 3 VMs exposées sur Internet, la surface d'attaque triple. Règles non négociables :
Le capital débloque des stratégies impossibles à 100K€. Pas parce qu'elles nécessitent du levier — on reste long only — mais parce que les frais fixes (données, commissions minimum, spreads) ne sont amortissables qu'au-delà d'un certain seuil. Voici les quatre familles de stratégies qui deviennent rentables post-million.
Le stat arb consiste à identifier des paires d'actions coïntégrées (leur spread est mean-reverting) et à trader la convergence. À 100K€, les commissions et le spread mangent tout le P&L car les mouvements sont petits (20-50 bps par trade). À 500K€+, le sizing permet d'absorber les frais et de dégager un Sharpe de 1.5-2.0 avec un drawdown minimal.
package statarb
import (
"math"
"sort"
)
// Pair représente une paire coïntégrée.
type Pair struct {
SymbolA string
SymbolB string
HedgeRatio float64 // beta de la régression A = alpha + beta*B + epsilon
HalfLife float64 // demi-vie du spread en jours
ZScore float64 // z-score actuel du spread
Correlation float64
}
// PairScanner identifie les paires coïntégrées dans un univers.
type PairScanner struct {
MinCorrelation float64 // 0.7
MaxHalfLife float64 // 30 jours
MinHalfLife float64 // 2 jours
EntryZScore float64 // 2.0
ExitZScore float64 // 0.5
StopZScore float64 // 4.0
}
// SpreadTimeSeries calcule le spread entre deux séries de prix.
func (ps *PairScanner) SpreadTimeSeries(
pricesA []float64,
pricesB []float64,
hedgeRatio float64,
) []float64 {
n := len(pricesA)
if len(pricesB) < n {
n = len(pricesB)
}
spread := make([]float64, n)
for i := 0; i < n; i++ {
spread[i] = math.Log(pricesA[i]) - hedgeRatio*math.Log(pricesB[i])
}
return spread
}
// HalfLife estime la demi-vie du mean reversion via régression OLS
// sur le modèle AR(1) : delta_spread = phi * spread_lag + epsilon
// half_life = -ln(2) / ln(1 + phi)
func (ps *PairScanner) HalfLife(spread []float64) float64 {
n := len(spread)
if n < 20 {
return math.Inf(1)
}
// Régression OLS : y = delta_spread, x = spread_lag
var sumXY, sumX2, sumX, sumY float64
count := float64(n - 1)
for i := 1; i < n; i++ {
x := spread[i-1]
y := spread[i] - spread[i-1]
sumX += x
sumY += y
sumXY += x * y
sumX2 += x * x
}
phi := (sumXY - sumX*sumY/count) / (sumX2 - sumX*sumX/count)
if phi >= 0 {
return math.Inf(1) // pas mean-reverting
}
return -math.Ln2 / math.Log(1+phi)
}
// ZScore calcule le z-score actuel du spread.
func (ps *PairScanner) ZScore(spread []float64, lookback int) float64 {
if len(spread) < lookback {
lookback = len(spread)
}
recent := spread[len(spread)-lookback:]
var sum, sumSq float64
n := float64(len(recent))
for _, v := range recent {
sum += v
sumSq += v * v
}
mean := sum / n
std := math.Sqrt(sumSq/n - mean*mean)
if std == 0 {
return 0
}
return (spread[len(spread)-1] - mean) / std
}
// GenerateSignal produit un signal de trading pour une paire.
func (ps *PairScanner) GenerateSignal(pair Pair) string {
switch {
case math.Abs(pair.ZScore) > ps.StopZScore:
return "STOP" // spread a divergé — clôturer
case pair.ZScore > ps.EntryZScore:
return "SHORT_SPREAD" // short A, long B
case pair.ZScore < -ps.EntryZScore:
return "LONG_SPREAD" // long A, short B
case math.Abs(pair.ZScore) < ps.ExitZScore:
return "EXIT" // spread convergé — prendre le profit
default:
return "HOLD"
}
}
Avec un portefeuille de 1M€+ d'actions, vous détenez le sous-jacent nécessaire pour vendre des options couvertes. Le covered call writing sur vos positions existantes génère 1-3% de rendement mensuel supplémentaire avec un risque limité (vous ne faites que plafonner votre upside).
La règle d'or : ne vendez que des options que vous êtes prêt à honorer. Pas de naked selling. Jamais.
Les événements corporate (earnings, M&A, spin-offs, index rebalancing) créent des dislocations de prix prévisibles et récurrentes. Avec un capital suffisant pour diversifier sur 20-30 événements simultanément, le win rate converge vers la moyenne historique et le Sharpe se stabilise.
| Type d'événement | Fenêtre | Alpha moyen | Win Rate | Capital min |
|---|---|---|---|---|
| Post-Earnings Drift | +1 à +60j après earnings | 3-5% par position | 55-60% | $200K |
| Index Rebalancing | -5j à +2j autour du rebalancing | 1-3% | 65-70% | $500K |
| Spin-Off | +1j à +90j post-séparation | 5-15% | 60% | $100K |
| Share Buyback | Annonce +30j | 2-4% | 55% | $300K |
| Insider Buying cluster | +1j à +30j après 3+ achats | 3-8% | 62% | $200K |
Voici la vérité que les fonds ne vous diront jamais : plus votre capital augmente, plus votre rendement marginal diminue. C'est la loi incontournable des marchés. Un système qui fait 100% par an à 100K€ fera peut-être 40% à 1M€, 20% à 10M€, et 12% à 100M€. La capacité totale du marché pour l'alpha est finie.
C'est pourquoi les meilleurs hedge funds ferment leur fonds aux nouveaux investisseurs (hard close) — pas par élitisme, mais par nécessité mathématique. Renaissance Technologies gère $130B mais le Medallion Fund est limité à $10B pour ses employés. Le reste est en stratégies moins performantes.
package scaling
import "math"
// AlphaDecayModel modélise la décroissance de l'alpha en fonction du capital.
// Modèle : alpha(AUM) = alpha_base * (1 / (1 + k * ln(AUM / AUM_base)))
type AlphaDecayModel struct {
AlphaBase float64 // alpha annualisé au capital de base (ex: 1.0 = 100%)
AUMBase float64 // capital de base en USD (ex: 100_000)
K float64 // constante de décroissance (0.15 = modéré, 0.3 = agressif)
}
// AlphaAtAUM retourne l'alpha annualisé estimé pour un AUM donné.
func (m *AlphaDecayModel) AlphaAtAUM(aum float64) float64 {
if aum <= m.AUMBase {
return m.AlphaBase
}
return m.AlphaBase / (1.0 + m.K*math.Log(aum/m.AUMBase))
}
// OptimalAUM retourne l'AUM qui maximise le profit absolu.
// profit(AUM) = AUM * alpha(AUM)
// d/dAUM [profit] = 0 quand alpha(AUM) + AUM * alpha'(AUM) = 0
func (m *AlphaDecayModel) OptimalAUM() float64 {
// Résolution numérique par recherche binaire
lo, hi := m.AUMBase, m.AUMBase*10000
for i := 0; i < 100; i++ {
mid := (lo + hi) / 2
profitMid := mid * m.AlphaAtAUM(mid)
profitMidPlus := (mid * 1.001) * m.AlphaAtAUM(mid*1.001)
if profitMidPlus > profitMid {
lo = mid
} else {
hi = mid
}
}
return (lo + hi) / 2
}
// ProjectGrowth projette la croissance du capital sur N années
// en tenant compte de la décroissance de l'alpha.
func (m *AlphaDecayModel) ProjectGrowth(years int) []float64 {
aum := m.AUMBase
trajectory := []float64{aum}
for y := 0; y < years; y++ {
alpha := m.AlphaAtAUM(aum)
aum *= (1.0 + alpha)
trajectory = append(trajectory, aum)
}
return trajectory
}
Votre système tourne depuis 2+ ans, le track record est audité, le Sharpe est stable au-dessus de 1.5. Vous avez un actif intellectuel qui vaut potentiellement plus que les profits qu'il génère. Trois options s'offrent à vous.
Vous exposez vos signaux de trading via une API REST. Les clients reçoivent les signaux en temps réel et exécutent eux-mêmes. Vous ne gérez pas leur argent — vous vendez de l'information.
| Tier | Prix/mois | Contenu | Latence signal | Clients cibles |
|---|---|---|---|---|
| Discovery | €99 | 1 stratégie, signaux daily EOD | T+1 (fin de journée) | Retail actifs |
| Pro | €299 | 3 stratégies, signaux real-time | < 5 min | Traders semi-pro |
| Institutional | €999 | Toutes stratégies + portfolio optimizer | < 30 sec | Family offices, RIA |
package signalapi
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
)
// Signal représente un signal de trading envoyé aux clients.
type Signal struct {
ID string `json:"id"`
Strategy string `json:"strategy"`
Symbol string `json:"symbol"`
Action string `json:"action"` // BUY, SELL, HOLD
EntryPrice float64 `json:"entry_price"`
StopLoss float64 `json:"stop_loss"`
Target1 float64 `json:"target_1"`
Target2 float64 `json:"target_2"`
Confidence float64 `json:"confidence"` // 0-1
Timestamp time.Time `json:"timestamp"`
}
// Tier définit les droits d'accès d'un abonnement.
type Tier struct {
Name string
Strategies []string
MaxLatency time.Duration
RealTime bool
}
// SignalServer expose les signaux via API REST.
type SignalServer struct {
router *gin.Engine
signals []Signal
mu sync.RWMutex
tiers map[string]Tier
apiKeys map[string]string // apiKey -> tierName
logger *slog.Logger
}
// NewSignalServer crée un serveur de signaux.
func NewSignalServer(logger *slog.Logger) *SignalServer {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Recovery())
s := &SignalServer{
router: r,
signals: make([]Signal, 0, 1000),
tiers: map[string]Tier{
"discovery": {
Name: "Discovery",
Strategies: []string{"momentum"},
MaxLatency: 24 * time.Hour,
RealTime: false,
},
"pro": {
Name: "Pro",
Strategies: []string{"momentum", "meanrev", "crossasset"},
MaxLatency: 5 * time.Minute,
RealTime: true,
},
"institutional": {
Name: "Institutional",
Strategies: []string{"*"}, // toutes
MaxLatency: 30 * time.Second,
RealTime: true,
},
},
apiKeys: make(map[string]string),
logger: logger,
}
// Routes
r.GET("/v1/signals", s.authMiddleware(), s.getSignals)
r.GET("/v1/signals/latest", s.authMiddleware(), s.getLatestSignals)
r.GET("/v1/portfolio", s.authMiddleware(), s.getPortfolio)
r.GET("/healthz", s.healthCheck)
return s
}
// PublishSignal publie un nouveau signal (appelé par le système de trading).
func (s *SignalServer) PublishSignal(sig Signal) {
s.mu.Lock()
s.signals = append(s.signals, sig)
s.mu.Unlock()
s.logger.Info("signal published",
"strategy", sig.Strategy,
"symbol", sig.Symbol,
"action", sig.Action,
)
}
func (s *SignalServer) authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
key := c.GetHeader("X-API-Key")
if key == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing API key"})
return
}
tierName, ok := s.apiKeys[key]
if !ok {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "invalid API key"})
return
}
c.Set("tier", tierName)
c.Next()
}
}
func (s *SignalServer) getLatestSignals(c *gin.Context) {
tierName := c.GetString("tier")
tier := s.tiers[tierName]
s.mu.RLock()
defer s.mu.RUnlock()
cutoff := time.Now().Add(-tier.MaxLatency)
var filtered []Signal
for _, sig := range s.signals {
if sig.Timestamp.Before(cutoff) {
continue
}
if !s.tierHasAccess(tier, sig.Strategy) {
continue
}
filtered = append(filtered, sig)
}
c.JSON(http.StatusOK, gin.H{
"tier": tier.Name,
"count": len(filtered),
"signals": filtered,
})
}
func (s *SignalServer) getSignals(c *gin.Context) {
s.getLatestSignals(c) // Simplifié — en prod, pagination + filtres
}
func (s *SignalServer) getPortfolio(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok", "positions": []string{}})
}
func (s *SignalServer) healthCheck(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "healthy"})
}
func (s *SignalServer) tierHasAccess(tier Tier, strategy string) bool {
for _, allowed := range tier.Strategies {
if allowed == "*" || allowed == strategy {
return true
}
}
return false
}
// Run démarre le serveur sur le port donné.
func (s *SignalServer) Run(ctx context.Context, addr string) error {
srv := &http.Server{Addr: addr, Handler: s.router}
go func() {
<-ctx.Done()
srv.Shutdown(context.Background())
}()
return srv.ListenAndServe()
}
Le risque principal du modèle SaaS : si 100 clients exécutent le même signal au même moment, l'alpha s'effondre. Chaque client supplémentaire est un concurrent sur le même trade. Mitigations : (1) décaler les signaux de quelques minutes entre tiers, (2) varier les points d'entrée, (3) limiter le nombre de clients par stratégie, (4) facturer un premium pour la priorité d'exécution. La règle empirique : au-delà de 50 clients actifs sur une même stratégie mid-cap, l'impact de marché devient mesurable.
Vous ne vendez pas les signaux — vous vendez le système entier (ou une licence d'utilisation) à un fonds quantitatif. C'est le modèle le plus lucratif en one-shot mais le plus difficile à concrétiser.
Pricing indicatif : upfront €200K-500K + 10-20% des profits générés par le système chez le fonds. Ou un flat fee de €1M-3M pour une licence perpétuelle exclusive.
Vous gérez l'argent de clients dans des comptes séparés (Separately Managed Accounts). Chaque client a son propre compte IBKR, vous avez un accès advisor pour exécuter les trades.
| Modèle | Management Fee | Performance Fee | Hurdle | High Water Mark |
|---|---|---|---|---|
| Classique 2/20 | 2%/an | 20% des profits | Non | Oui |
| Compétitif 0/30 | 0% | 30% des profits | 5% | Oui |
| Hybride 1/15 | 1%/an | 15% des profits | 3% | Oui |
| Juridiction | Statut requis | Coût setup | Délai | Capital minimum |
|---|---|---|---|---|
| France | CIF (Conseiller en Investissements Financiers) | €5-10K | 3-6 mois | Aucun (mais RCP obligatoire) |
| Luxembourg | PSF de support / AIFM | €50-150K | 6-12 mois | €125K (AIFM) |
| UK | FCA Authorized (IFPRU) | £30-80K | 6-12 mois | £75K |
| US | SEC RIA (Registered Investment Advisor) | $20-50K | 3-6 mois | Aucun (< $100M → state, > $100M → SEC) |
| Suisse | Gestionnaire de fortune (FinIA) | CHF 50-100K | 6-18 mois | CHF 100K |
À partir du moment où votre patrimoine financier dépasse le million d'euros, la structuration devient un levier de performance aussi puissant que l'alpha de votre système. Un family office mono-personne bien structuré peut économiser 30-50K€/an en fiscalité et protéger votre patrimoine contre les aléas juridiques.
| Classe d'actifs | Allocation | Véhicule | Rendement cible | Rôle |
|---|---|---|---|---|
| Algo Trading | 50% | Holding SAS + CTO | 15-30%/an | Moteur de performance |
| Immobilier | 20% | SCI à l'IS | 5-8%/an (avec levier) | Revenus réguliers + effet de levier |
| Private Equity / VC | 10% | Holding SAS | 15-25%/an (illiquide) | Diversification, upside convexe |
| Cash / Obligations | 10% | Holding SAS + Fonds € | 3-4%/an | Réserve de sécurité 12 mois |
| Crypto | 10% | CTO / Cold storage | Variable | Asymétrie, décorrélation |
| Poste | Coût/an | Fréquence | Notes |
|---|---|---|---|
| Expert-comptable | €3-5K | Mensuel + bilan annuel | Holding + SCI + déclarations perso |
| Avocat fiscaliste | €2-3K | 1-2 consultations/an | Optimisation, veille réglementaire |
| RC Pro (assurance) | €1-2K | Annuel | Obligatoire si CIF / gestion pour tiers |
| Assurance cyber | €500-1K | Annuel | Couvre les incidents de sécurité IT |
| Assurance homme-clé | €1-2K | Annuel | Si le système dépend uniquement de vous |
| Infrastructure IT | €1-2K | Mensuel | 3 VMs + données marché + domaines |
| Total | €10-15K | Soit ~1-1.5% sur 1M€ (raisonnable) |
Votre système algo est un actif immatériel qui meurt avec vous si vous ne préparez pas la transmission. Trois mesures essentielles :
Tout système a une fin de vie. Pas nécessairement parce qu'il cesse de fonctionner, mais parce que vous changez. Voici les quatre scénarios, classés du plus fréquent au plus rare.
Le système tourne, vous supervisez 30 minutes par jour, les profits composent. C'est le scénario optimal — la machine de rente perpétuelle décrite dans le chapitre 11. Les risques à long terme :
| Risque | Probabilité (10 ans) | Impact | Mitigation |
|---|---|---|---|
| Alpha decay structurel | Élevée (60%) | Sharpe passe de 2.0 à 1.0 | Innovation continue, nouvelles stratégies |
| Changement réglementaire | Moyenne (30%) | Interdiction du HFT retail / taxe transactions | Multi-juridiction, lobbying |
| Disruption technologique | Moyenne (25%) | AI/ML rend vos facteurs obsolètes | Intégrer le ML (Part 9) |
| Broker risk | Faible (5%) | IBKR faillite / changement API | Multi-broker, SIPC/FSCS protection |
| Incident personnel | Variable | Incapacité de superviser | Succession (voir ci-dessus) |
Avec un track record live > 3 ans et un Sharpe > 2.0, vous êtes un candidat prisé pour les pod shops (plateformes multi-PM) : Citadel, Millennium, Point72, Balyasny, ExodusPoint. Le modèle :
Le calcul : si on vous alloue $50M et que vous faites 20% → $10M de profits. Votre payout à 20% = $2M/an. C'est 10× ce que vous gagneriez seul sur votre million. Le trade-off : perte d'autonomie, stress des drawdown limits, bureaucratie corporate.
Vous passez de trader solo à gérant de fonds. C'est un changement de métier radical : 50% de votre temps sera consacré au marketing, à la compliance, et à la gestion des investisseurs.
| Étape | Coût | Délai | Détail |
|---|---|---|---|
| Structure juridique | €30-50K | 2-4 mois | Fonds (SIF/RAIF Luxembourg ou FIA France) + Management Company |
| Compliance & AIFM | €20-40K | 3-6 mois | KYC/AML, prospectus, reporting AIFMD |
| Prime broker | €0 (setup) | 1-2 mois | IBKR Prime Services, Goldman Sachs PB (si > $50M) |
| Admin & audit | €15-25K/an | Ongoing | NAV calculation, audit annuel (Big 4 pour crédibilité) |
| Marketing & fundraise | €10-20K | 6-12 mois | Pitchbook, DDQ, roadshow (capital allocators, family offices) |
| Total setup | €80-150K | 6-12 mois |
Break-even : avec une structure 2/20, il vous faut ~€20M d'AUM pour couvrir les frais fixes (management fee 2% × 20M = €400K, dont ~€200K en frais fixes). En dessous, vous perdez de l'argent en frais de structure. La réalité : la levée de fonds est le métier le plus difficile en finance. Un bon trader n'est pas nécessairement un bon vendeur.
Vous vendez tout : code source, données historiques, track record, infrastructure, documentation. L'acheteur reprend l'exploitation.
| Acquéreur type | Motivation | Budget typique | Due diligence |
|---|---|---|---|
| Prop trading firm | Ajout de stratégies décorrélées | €500K-5M | Technique intensive (code review) |
| Fintech / Robo-advisor | Intégration dans leur plateforme | €200K-2M | Produit + scalabilité |
| Family office | Gestion interne du patrimoine | €300K-3M | Track record + simplicité |
| Fonds quantitatif | Acqui-hire (système + talent) | €1M-10M | La plus rigoureuse |
L'acheteur vérifiera systématiquement :
Vous avez parcouru 12 chapitres, des dizaines de milliers de lignes de code Go, et un framework complet pour transformer 100K€ en machine de rente. Récapitulons.
| # | Chapitre | Livrable clé | Stack |
|---|---|---|---|
| 1 | Infrastructure | VM hardened + IBKR Gateway + monitoring | Hetzner, Tailscale, Docker |
| 2 | Data Pipeline | Ingestion multi-source + TimescaleDB + qualité | Go, TimescaleDB, DuckDB |
| 3 | Alpha Factors | 120+ facteurs techniques, fondamentaux, sentiment | Go, feature store |
| 4 | Portfolio Construction | Optimisation MVO + risk parity + constraints | Go, Gonum |
| 5 | Exécution | Smart order routing + TWAP/VWAP + slippage control | Go, IBKR API |
| 6 | Momentum | 3 stratégies momentum (cross-sectional, time-series, dual) | Go, backtester |
| 7 | Mean Reversion | RSI/Bollinger + pairs + stat arb | Go, z-score engine |
| 8 | Cross-Asset | Rotation ETF multi-région (US/EU/APAC/Commodities) | Go, correlation matrix |
| 9 | Régime ML | Détection de régime + allocation adaptative | Go, HMM, random forest |
| 10 | Production Ops | Nomad + Consul + Vault + alerting + runbooks | HashiStack, Discord |
| 11 | Rente | Withdrawal policy + fiscalité + psychologie | Go, simulation Monte Carlo |
| 12 | Scaling & Exit | Multi-VM + licensing + family office + exit | Terraform, Nomad, Consul |
En intégrant le modèle d'alpha decay (l'alpha diminue à mesure que le capital croît) et un taux de retrait de 3% à partir de l'année 3, voici la trajectoire réaliste sur 5 ans :
Vous avez construit, en 12 chapitres, quelque chose que 99% des traders ne construiront jamais : un système autonome de génération de richesse. Pas un robot magique. Pas un algo qui « bat le marché ». Un framework rigoureux, testé, documenté, monitoré, qui extrait de l'alpha de manière systématique et reproductible.
La beauté de ce système, c'est qu'il vous libère. 30 minutes par jour pour vérifier les dashboards. Une heure par semaine pour la recherche de nouvelles idées. Le reste du temps est à vous — pour vivre, apprendre, créer. C'est ça, le vrai million : pas le chiffre sur le compte, mais le temps récupéré.
Le marché continuera de tourner demain. Votre système aussi. La seule question qui reste : qu'allez-vous faire de votre liberté ?
Cette série n'aurait pas été possible sans :
Fin de la série.
Objectif atteint : 100K€ → 1M€ en 24-36 mois, puis 3M€+ en 5 ans, avec 30 minutes de supervision quotidienne. La machine tourne. L'humain est libre.