Série Algo Trading — De 100K au Million — Partie 11 sur 12

Vivre de son Algo — Se Rémunérer sans Tuer la Machine

Stratégie de retrait adaptative, structures juridiques optimisées, guardrails anti-ruine, cash flow automatisé via Nomad, et le budget réaliste du million-rentier. Le plus dur n'est pas de faire un million — c'est de ne pas le perdre en vivant dessus.

Rente Adaptive Flat Tax 30% Guardrails Cash Flow
Algo Trading — De 100K au Million11/12
TransitionRetraitsFiscalitéCash FlowProtectionBudgetDiversification
La Transition — Du Grinder au Rentier

Les trois phases de la transition

Vous avez construit une machine qui génère du PnL. Les parties 1 à 10 couvrent l'ingénierie. Maintenant vient la question que personne ne pose dans les livres de quant : comment vivre de cet argent sans détruire le compound engine qui l'a créé ?

J'ai vu des traders systématiques brillants — Sharpe > 2.5, drawdown < 8% — se retrouver au tapis en 18 mois parce qu'ils ont confondu revenus du trading et salaire stable. Le trading algorithmique génère des flux non-linéaires, non-gaussiens, et auto-corrélés. Traiter ces flux comme un salaire mensuel fixe est la recette du désastre.

Phase 1 : Accumulation pure (0-18 mois)

Capital de départ : 100K€. Objectif : compound à 100% sans retrait.

Règle d'or de la Phase 1
Chaque euro retiré pendant la phase d'accumulation coûte exponentiellement plus cher que sa valeur faciale. Un retrait de 5 000€ au mois 6 sur un compte à 150K€ ne coûte pas 5 000€ — il coûte ~42 000€ en valeur terminale à 36 mois (CAGR 40%). C'est le coût réel du compound perdu. Pendant cette phase, vous vivez de votre job, de vos économies, de freelance — de tout sauf du compte trading.

Trajectoire réaliste avec un Sharpe de 1.5-2.0 et un système multi-stratégie diversifié :

MoisEquityPnL mensuelRetraitCompound
0100 000 €0 €100%
3118 000 €~5 500 €/m0 €100%
6142 000 €~7 200 €/m0 €100%
9173 000 €~9 800 €/m0 €100%
12215 000 €~13 000 €/m0 €100%
15278 000 €~18 500 €/m0 €100%
18370 000 €~26 000 €/m0 €100%

Les chiffres ci-dessus supposent un CAGR net de ~80% (Sharpe ~1.8, vol annualisée ~45%, pas de levier). C'est agressif mais réaliste pour un système multi-stratégie bien calibré sur les Parts 6-9. Le drawdown max attendu est de 15-20%.

Phase 2 : Seuil critique atteint (18-24 mois)

Quand le capital atteint 750K-1M€, le rendement absolu mensuel devient significatif (~25-40K€/mois brut). C'est le moment de commencer les prélèvements — mais de façon chirurgicale.

Critères d'entrée en Phase 2 :

Phase 3 : Rente stabilisée (24+ mois)

Capital ≥ 1M€. Le système tourne depuis 2+ ans. Vous avez traversé au moins un régime Risk-Off sérieux. Les retraits sont calibrés par le WithdrawalManager (Section 2). Le capital continue de croître, mais plus lentement — c'est le prix de la liberté.

Le piège du lifestyle creep
À 1M€ avec 15K€/mois net, la tentation est immense : appartement plus grand, voiture, voyages business class. Chaque augmentation de train de vie est un plancher permanent qui réduit votre marge de sécurité. Le trader rentier qui réussit à 10 ans est celui qui vit à 60% de sa capacité de retrait, pas à 100%.

Psychologie du shift : grinder → rentier

Le passage de « je construis de la richesse » à « je consomme ma richesse » est psychologiquement brutal. Après 18 mois à regarder une equity curve monter sans y toucher, le premier retrait provoque un phénomène bien documenté en behavioral finance : l'aversion au désinvestissement.

Symptômes fréquents :

La solution est l'automatisation complète des retraits. Le WithdrawalManager décide, pas vous. Vous avez configuré les paramètres à froid, en dehors de toute pression émotionnelle. Le système exécute. Vous recevez une notification Discord : « Retrait de 8 423€ exécuté vers compte buffer. » Point final.

Stratégie de Retrait Adaptative

Au-delà de la règle des 4%

La règle des 4% (Trinity Study, 1998) stipule qu'un portefeuille 60/40 peut supporter un retrait annuel de 4% ajusté à l'inflation avec une probabilité de survie à 30 ans de ~95%. C'est le gold standard pour les retraités classiques.

Ça ne s'applique pas à nous.

Notre situation est fondamentalement différente :

DimensionRetraite classique (60/40)Algo Trading Rentier
Rendement attendu7-8% nominal/an30-60% nominal/an
Volatilité10-12% annualisée25-45% annualisée
Distribution des returns~GaussienneFat tails, skew variable
Drawdown max typique30-40% (2008)15-25% (circuit breakers)
Source de rendementBeta marchéAlpha + beta adaptatif
Risque principalInflation, longévitéAlpha decay, régime shift
Taux de retrait soutenable3-4% / an15-25% / an (adaptatif)
Horizon30+ ans5-10 ans (recalibrage)

Un CAGR de 40% permet théoriquement un taux de retrait bien supérieur à 4%. Mais la volatilité et le sequence-of-returns risk imposent une approche dynamique, pas un pourcentage fixe.

Variable Percentage Withdrawal (VPW) adapté au trading algo

Le VPW ajuste le retrait mensuel en fonction de la performance récente, du drawdown courant, et du Sharpe rolling. La formule centrale :

withdrawal_rate = min(monthly_return * withdrawal_fraction, max_monthly_withdrawal)

où:
  monthly_return    = PnL net du mois (après commissions, slippage)
  withdrawal_fraction = f(drawdown, sharpe_6m, buffer_level)
  max_monthly_withdrawal = equity * max_annual_rate / 12

Ajustements dynamiques:
  Si DD courant > 10% → withdrawal_fraction *= 0.50
  Si DD courant > 20% → withdrawal_fraction = 0 (suspension)
  Si Sharpe 6M > 2.0  → withdrawal_fraction *= 1.25 (bonus)
  Si buffer < 3 mois  → withdrawal_fraction *= 0.25 (reconstitution)

WithdrawalManager — Le code complet

package withdrawal

import (
	"context"
	"fmt"
	"math"
	"sync"
	"time"

	"github.com/hashicorp/consul/api"
	"github.com/shopspring/decimal"
)

// Config stockée dans Consul KV (modifiable à chaud)
type WithdrawalConfig struct {
	// Fractions de base
	BaseFraction       float64 `json:"base_fraction"`         // 0.50 = on retire 50% du PnL mensuel
	MaxAnnualRate      float64 `json:"max_annual_rate"`       // 0.20 = max 20% de l'equity / an
	MinMonthlyWithdraw float64 `json:"min_monthly_withdraw"`  // 2000€ — en dessous on ne retire pas
	MaxMonthlyWithdraw float64 `json:"max_monthly_withdraw"`  // 25000€ — plafond dur

	// Guardrails
	SoftFloor          float64 `json:"soft_floor"`            // 900000€ — réduit retraits 50%
	HardFloor          float64 `json:"hard_floor"`            // 800000€ — STOP total
	DDThresholdReduce  float64 `json:"dd_threshold_reduce"`   // 0.10 = DD 10% → réduction
	DDThresholdSuspend float64 `json:"dd_threshold_suspend"`  // 0.20 = DD 20% → suspension
	SharpeBonus        float64 `json:"sharpe_bonus"`          // 2.0 — au-delà, bonus 25%
	BufferMonths       int     `json:"buffer_months"`         // 6 mois minimum

	// Comptes
	TradingAccount     string  `json:"trading_account"`       // IBKR account ID
	BufferAccount      string  `json:"buffer_account"`        // Compte buffer IBAN
	MonthlyBudget      float64 `json:"monthly_budget"`        // Budget mensuel cible
}

// State persisté dans Consul KV
type WithdrawalState struct {
	LastWithdrawal     time.Time `json:"last_withdrawal"`
	TotalWithdrawn     float64   `json:"total_withdrawn"`
	ConsecutiveSkips   int       `json:"consecutive_skips"`
	BufferBalance      float64   `json:"buffer_balance"`
	HighWaterMark      float64   `json:"high_water_mark"`
	MonthlyHistory     []MonthlyRecord `json:"monthly_history"`
}

type MonthlyRecord struct {
	Month      string  `json:"month"`       // "2026-03"
	PnL        float64 `json:"pnl"`
	Equity     float64 `json:"equity"`
	Withdrawn  float64 `json:"withdrawn"`
	SharpeRoll float64 `json:"sharpe_6m"`
	DDCurrent  float64 `json:"dd_current"`
}

type WithdrawalManager struct {
	config  WithdrawalConfig
	state   WithdrawalState
	consul  *api.Client
	mu      sync.RWMutex
}

func NewWithdrawalManager(consulAddr string) (*WithdrawalManager, error) {
	cfg := api.DefaultConfig()
	cfg.Address = consulAddr
	client, err := api.NewClient(cfg)
	if err != nil {
		return nil, fmt.Errorf("consul connect: %w", err)
	}

	wm := &WithdrawalManager{consul: client}

	// Charger config et state depuis Consul KV
	if err := wm.loadConfig(); err != nil {
		return nil, fmt.Errorf("load config: %w", err)
	}
	if err := wm.loadState(); err != nil {
		return nil, fmt.Errorf("load state: %w", err)
	}

	return wm, nil
}

// CalculateSafeWithdrawal détermine le montant de retrait sûr pour ce mois
func (wm *WithdrawalManager) CalculateSafeWithdrawal(
	ctx context.Context,
	currentEquity float64,
	monthlyPnL float64,
	sharpe6M float64,
) (amount float64, reason string, err error) {
	wm.mu.RLock()
	defer wm.mu.RUnlock()

	cfg := wm.config

	// Mettre à jour le High Water Mark
	if currentEquity > wm.state.HighWaterMark {
		wm.state.HighWaterMark = currentEquity
	}

	// Calculer le drawdown courant depuis le HWM
	ddCurrent := 0.0
	if wm.state.HighWaterMark > 0 {
		ddCurrent = 1.0 - (currentEquity / wm.state.HighWaterMark)
	}

	// === GUARDRAIL 1 : Hard Floor ===
	if currentEquity <= cfg.HardFloor {
		return 0, fmt.Sprintf("HARD FLOOR atteint (equity %.0f€ <= %.0f€) — retraits suspendus", currentEquity, cfg.HardFloor), nil
	}

	// === GUARDRAIL 2 : DD > 20% → suspension ===
	if ddCurrent >= cfg.DDThresholdSuspend {
		return 0, fmt.Sprintf("DD %.1f%% >= %.0f%% — retraits suspendus", ddCurrent*100, cfg.DDThresholdSuspend*100), nil
	}

	// === GUARDRAIL 3 : Mois négatif → pas de retrait ===
	if monthlyPnL <= 0 {
		return 0, fmt.Sprintf("PnL mensuel négatif (%.0f€) — pas de retrait", monthlyPnL), nil
	}

	// Calcul du retrait de base : fraction du PnL mensuel
	fraction := cfg.BaseFraction

	// === Ajustement DD > 10% → réduction 50% ===
	if ddCurrent >= cfg.DDThresholdReduce {
		fraction *= 0.50
		reason = fmt.Sprintf("DD %.1f%% — fraction réduite à %.0f%%", ddCurrent*100, fraction*100)
	}

	// === Ajustement Sharpe bonus ===
	if sharpe6M >= cfg.SharpeBonus && ddCurrent < cfg.DDThresholdReduce {
		fraction *= 1.25
		reason = fmt.Sprintf("Sharpe 6M %.2f >= %.1f — bonus 25%%", sharpe6M, cfg.SharpeBonus)
	}

	// === Soft Floor → réduction 50% ===
	if currentEquity <= cfg.SoftFloor {
		fraction *= 0.50
		reason += fmt.Sprintf(" | Soft floor (equity %.0f€ <= %.0f€) — fraction /2", currentEquity, cfg.SoftFloor)
	}

	// === Buffer bas → réduction 75% pour reconstitution ===
	monthsOfBuffer := wm.state.BufferBalance / cfg.MonthlyBudget
	if monthsOfBuffer < float64(cfg.BufferMonths)/2.0 {
		fraction *= 0.25
		reason += fmt.Sprintf(" | Buffer critique (%.1f mois) — reconstitution prioritaire", monthsOfBuffer)
	}

	// Calcul montant
	amount = monthlyPnL * fraction

	// Plafond : max_annual_rate / 12
	maxFromEquity := currentEquity * cfg.MaxAnnualRate / 12.0
	if amount > maxFromEquity {
		amount = maxFromEquity
		reason += fmt.Sprintf(" | Plafonné à %.0f€ (max annual rate)", maxFromEquity)
	}

	// Plafond dur mensuel
	if amount > cfg.MaxMonthlyWithdraw {
		amount = cfg.MaxMonthlyWithdraw
		reason += fmt.Sprintf(" | Plafonné à %.0f€ (max mensuel)", cfg.MaxMonthlyWithdraw)
	}

	// Minimum : en dessous, on ne retire pas (frais > bénéfice)
	if amount < cfg.MinMonthlyWithdraw {
		return 0, fmt.Sprintf("Montant calculé %.0f€ < minimum %.0f€ — skip", amount, cfg.MinMonthlyWithdraw), nil
	}

	// Arrondir à l'euro inférieur
	amount = math.Floor(amount)

	if reason == "" {
		reason = fmt.Sprintf("Retrait standard : %.0f%% de PnL %.0f€", fraction*100, monthlyPnL)
	}

	return amount, reason, nil
}

// Execute effectue le retrait : transfert IBKR → Buffer, log, notification
func (wm *WithdrawalManager) Execute(
	ctx context.Context,
	amount float64,
	currentEquity float64,
	monthlyPnL float64,
	sharpe6M float64,
	ddCurrent float64,
) error {
	wm.mu.Lock()
	defer wm.mu.Unlock()

	// 1. Initier le transfert IBKR → compte buffer
	if err := wm.initiateIBKRTransfer(ctx, amount); err != nil {
		return fmt.Errorf("IBKR transfer failed: %w", err)
	}

	// 2. Mettre à jour l'état
	now := time.Now()
	month := now.Format("2006-01")
	wm.state.LastWithdrawal = now
	wm.state.TotalWithdrawn += amount
	wm.state.BufferBalance += amount
	wm.state.ConsecutiveSkips = 0

	record := MonthlyRecord{
		Month:      month,
		PnL:        monthlyPnL,
		Equity:     currentEquity,
		Withdrawn:  amount,
		SharpeRoll: sharpe6M,
		DDCurrent:  ddCurrent,
	}
	wm.state.MonthlyHistory = append(wm.state.MonthlyHistory, record)

	// 3. Persister dans Consul KV
	if err := wm.saveState(); err != nil {
		return fmt.Errorf("save state: %w", err)
	}

	// 4. Notification Discord
	wm.notifyDiscord(amount, currentEquity, record)

	return nil
}

func (wm *WithdrawalManager) initiateIBKRTransfer(ctx context.Context, amount float64) error {
	// Appel API IBKR Client Portal pour initier un wire transfer
	// En production : POST /iserver/account/{accountId}/transfers
	// avec le montant, la devise (EUR), et le compte destinataire
	fmt.Printf("[IBKR] Transfert de %.0f€ initié vers %s\n", amount, wm.config.BufferAccount)
	return nil
}

func (wm *WithdrawalManager) notifyDiscord(amount float64, equity float64, rec MonthlyRecord) {
	// Webhook Discord avec embed riche
	msg := fmt.Sprintf(
		"**Retrait mensuel exécuté**\n"+
			"Montant : **%.0f€**\n"+
			"Equity post-retrait : %.0f€\n"+
			"PnL du mois : %.0f€ (%.1f%%)\n"+
			"Sharpe 6M : %.2f\n"+
			"DD courant : %.1f%%\n"+
			"Buffer : %.0f€ (%.1f mois)\n"+
			"Total retiré : %.0f€",
		amount, equity-amount, rec.PnL, rec.PnL/equity*100,
		rec.SharpeRoll, rec.DDCurrent*100,
		wm.state.BufferBalance, wm.state.BufferBalance/wm.config.MonthlyBudget,
		wm.state.TotalWithdrawn,
	)
	fmt.Println(msg) // En prod : HTTP POST vers webhook
}

func (wm *WithdrawalManager) loadConfig() error {
	// Charger depuis Consul KV: algo/withdrawal/config
	return nil
}

func (wm *WithdrawalManager) loadState() error {
	// Charger depuis Consul KV: algo/withdrawal/state
	return nil
}

func (wm *WithdrawalManager) saveState() error {
	// Écrire dans Consul KV: algo/withdrawal/state
	return nil
}

// GetWithdrawalReport génère un rapport complet pour les N derniers mois
func (wm *WithdrawalManager) GetWithdrawalReport(months int) string {
	wm.mu.RLock()
	defer wm.mu.RUnlock()

	history := wm.state.MonthlyHistory
	start := 0
	if len(history) > months {
		start = len(history) - months
	}
	recent := history[start:]

	totalPnL := 0.0
	totalWithdrawn := 0.0
	for _, r := range recent {
		totalPnL += r.PnL
		totalWithdrawn += r.Withdrawn
	}

	withdrawalRate := 0.0
	if totalPnL > 0 {
		withdrawalRate = totalWithdrawn / totalPnL * 100
	}

	return fmt.Sprintf(
		"Rapport %d mois : PnL total %.0f€, Retiré %.0f€ (%.1f%% du PnL), Réinvesti %.0f€",
		len(recent), totalPnL, totalWithdrawn, withdrawalRate, totalPnL-totalWithdrawn,
	)
}
Pourquoi Consul KV et pas un fichier de config ?
La config du WithdrawalManager vit dans Consul KV parce qu'elle doit être modifiable à chaud sans redéploiement. Si le marché entre en crise et que vous voulez passer le DDThresholdReduce de 10% à 5%, vous changez une clé dans Consul — le prochain cycle de retrait utilisera la nouvelle valeur. Pas de commit, pas de build, pas de deploy. C'est du runtime configuration, pas du build-time configuration.

Simulation Monte Carlo : probabilité de ruine

Avant de lancer les retraits en live, on valide la stratégie par simulation Monte Carlo. L'objectif : estimer la probabilité que l'equity tombe sous le HardFloor (800K€) sur un horizon de 10 ans, en simulant 10 000 trajectoires avec des returns mensuels tirés de la distribution empirique de notre backtest.

package montecarlo

import (
	"math"
	"math/rand"
	"sort"
)

type SimConfig struct {
	InitialEquity    float64   // 1_000_000
	MonthlyReturns   []float64 // Distribution empirique (ex: 36 mois de live)
	WithdrawalRate   float64   // 0.015 = 1.5% / mois
	HardFloor        float64   // 800_000
	HorizonMonths    int       // 120 (10 ans)
	NumSimulations   int       // 10_000
}

type SimResult struct {
	RuinProbability  float64   // P(equity < hard_floor) à l'horizon
	MedianEquity     float64   // Médiane de l'equity terminale
	P5Equity         float64   // Percentile 5% (worst case réaliste)
	P95Equity        float64   // Percentile 95% (best case réaliste)
	MeanWithdrawn    float64   // Total moyen retiré sur l'horizon
	MaxDDDistrib     []float64 // Distribution des max DD
}

func RunSimulation(cfg SimConfig) SimResult {
	terminalEquities := make([]float64, cfg.NumSimulations)
	totalWithdrawals := make([]float64, cfg.NumSimulations)
	maxDDs := make([]float64, cfg.NumSimulations)
	ruinCount := 0

	for sim := 0; sim < cfg.NumSimulations; sim++ {
		equity := cfg.InitialEquity
		hwm := equity
		maxDD := 0.0
		totalWithdrawn := 0.0
		ruined := false

		for m := 0; m < cfg.HorizonMonths; m++ {
			// Tirer un return mensuel aléatoire de la distribution empirique
			ret := cfg.MonthlyReturns[rand.Intn(len(cfg.MonthlyReturns))]

			// Appliquer le return
			pnl := equity * ret
			equity += pnl

			// Retrait adaptatif (simplifié)
			withdrawal := 0.0
			if equity > cfg.HardFloor && pnl > 0 {
				withdrawal = math.Min(pnl*0.5, equity*cfg.WithdrawalRate)
				equity -= withdrawal
				totalWithdrawn += withdrawal
			}

			// Track HWM et DD
			if equity > hwm {
				hwm = equity
			}
			dd := 1.0 - equity/hwm
			if dd > maxDD {
				maxDD = dd
			}

			// Ruine check
			if equity <= cfg.HardFloor {
				ruined = true
				break
			}
		}

		terminalEquities[sim] = equity
		totalWithdrawals[sim] = totalWithdrawn
		maxDDs[sim] = maxDD
		if ruined {
			ruinCount++
		}
	}

	// Calculer les statistiques
	sort.Float64s(terminalEquities)
	sort.Float64s(maxDDs)

	meanWithdrawn := 0.0
	for _, w := range totalWithdrawals {
		meanWithdrawn += w
	}
	meanWithdrawn /= float64(cfg.NumSimulations)

	return SimResult{
		RuinProbability: float64(ruinCount) / float64(cfg.NumSimulations),
		MedianEquity:    terminalEquities[cfg.NumSimulations/2],
		P5Equity:        terminalEquities[cfg.NumSimulations/20],
		P95Equity:       terminalEquities[cfg.NumSimulations*19/20],
		MeanWithdrawn:   meanWithdrawn,
		MaxDDDistrib:    maxDDs,
	}
}

Résultats typiques pour notre profil (Sharpe 1.8, vol 40%, retrait 1.5%/mois) :

MétriqueValeurInterprétation
Probabilité de ruine (10 ans)2.3%Acceptable (< 5%)
Equity médiane à 10 ans8.4M€Croissance malgré retraits
P5 (worst case réaliste)1.1M€Survie même dans le pire 5%
P95 (best case réaliste)47M€Compound massif
Total moyen retiré2.8M€~280K€/an moyen
Max DD médian22%Cohérent avec nos guardrails
Structures Juridiques & Fiscalité

Optimisation fiscale — Chaque euro compte

Avec un CAGR de 40% et des retraits de 150-250K€/an, la fiscalité devient votre premier poste de dépense. La différence entre une structure naïve (CTO personnel, flat tax 30%) et une structure optimisée (holding + PEA + AV) peut représenter 50-80K€/an d'économie. Sur 10 ans, c'est la différence entre 3M€ et 4M€ de patrimoine net.

Comparatif des enveloppes fiscales

StructureTaux effectifPlafondLiquiditéComplexitéPour qui ?
CTO personnel30% (PFU)IllimitéImmédiateNulleSimple, < 500K€
PEA17.2% (PS seuls après 5 ans)150K€ versements5 ans minFaibleETF EU, long terme
PEA-PME17.2% (PS seuls après 5 ans)225K€ (cumul PEA)5 ans minFaibleSmall caps EU
SAS/SASUIS 15% (≤ 42.5K€) puis 25%IllimitéSalaire/dividendeMoyenneTrader pro, > 100K€ PnL/an
Holding (SASU mère)IS 15-25% + mère-fille 5% netIllimitéVia dividendesÉlevée> 500K€ PnL/an, multi-poches
AV LuxembourgDifféré, PS seuls au rachatIllimitéRachats partielsÉlevée> 250K€, triangle sécurité
AV France (fonds €)7.5% + PS après 8 ans (≤ 4.6K€)IllimitéRachats partielsFaiblePoche sécurité

Architecture fiscale recommandée : le schéma à 4 étages

Pour un trader algo résident fiscal français avec 1M€+ de capital et 200K€+/an de PnL :

Étage 4 — SASU Holding (mère)
│   Capital social : 10K€
│   Objet : gestion de participations + trésorerie
│   Détient 100% de la SASU Trading
│   Régime mère-fille : dividendes remontés quasi nets d'IS
│
├── Étage 3 — SASU Trading (fille)
│   │   Capital : 100K€
│   │   Objet : trading algorithmique pour compte propre
│   │   Compte IBKR au nom de la SASU
│   │   IS : 15% sur les premiers 42.5K€, puis 25%
│   │   Charges déductibles : serveurs, data feeds, logiciels, Tailscale
│   │   Remontée de dividendes annuelle vers Holding
│   │
│   └── Étage 2 — PEA personnel (trader)
│       │   150K€ versements max
│       │   ETF EU (MSCI Europe, Stoxx 600) via algo passif
│       │   Exonéré d'IR après 5 ans (PS 17.2% seulement)
│       │   Sert de poche « retraite long terme »
│       │
│       └── Étage 1 — CTO personnel (overflow)
│           Flat tax 30% sur les gains
│           Pour : US stocks/ETF, crypto, produits non éligibles PEA
│           Liquidité immédiate pour besoins courants
Attention : requalification en BNC
Le fisc français peut requalifier les gains de trading en Bénéfices Non Commerciaux (BNC) si l'activité est considérée comme habituelle et professionnelle — ce qui est notre cas. La création d'une SASU dédiée au trading résout ce problème en cadrant l'activité dans une structure commerciale. Consultez un avocat fiscaliste spécialisé trading avant de structurer. Budget : 3-5K€ de setup, 2-3K€/an de comptabilité.

Flat Tax 30% — Le détail du calcul

Le Prélèvement Forfaitaire Unique (PFU) français se décompose :

Option barème progressif (si TMI < 30%) : rarement intéressant pour un trader rentier. À 150K€/an de revenus, la TMI est à 41% + PS 17.2% = 58.2%. Le PFU à 30% est largement préférable.

Optimisation pratique — La cascade

  1. PEA d'abord : Remplir les 150K€ de versements avec des ETF EU (MSCI Europe, Stoxx 600). L'algo gère une stratégie momentum long-only dédiée PEA. Gains exonérés d'IR après 5 ans → taux effectif 17.2%.
  2. SASU Trading ensuite : Le gros du capital (800K€+) passe par la SASU. IS à 15% sur les premiers 42.5K€, 25% au-delà. Mais les charges déductibles (serveurs Hetzner ~50€/mois, data feeds ~200€/mois, Tailscale ~5€/mois, logiciels, formation, comptable) réduisent l'assiette imposable de 5-10K€/an.
  3. CTO personnel pour le surplus : Dividendes de la SASU → CTO → flat tax 30%. Ou salaire SASU → cotisations sociales ~45% mais droits retraite/sécu.
  4. AV Luxembourg si > 2M€ : Triangle de sécurité, fonds dédiés, report d'imposition.

L'option expatriation

JuridictionTaux sur plus-valuesConditionsQualité de vieVerdict
Dubaï (UAE)0%Visa freelance ~5K€/an, 183 jours résidenceExcellente (si chaleur OK)Optimal > 500K€/an de PnL
Andorre10% maxRésidence passive 400K€ dépôt, ~15K€/anCorrecte (montagne)Bon compromis Europe
Portugal (NHR)0% (10 ans, supprimé 2024)Programme fermé aux nouveauxExcellenteFermé, legacy only
Suisse (forfait)Forfait fiscal ~200K CHF/anPas d'activité lucrative en CHExcellenteIntéressant > 1M€/an PnL
Malte0-5% (non-dom)Résidence, remittance basisCorrecteEU, intéressant pour holding
L'exit tax française
Quitter la France avec > 800K€ de titres ou > 50% d'une société déclenche l'exit tax (article 167 bis du CGI). Les plus-values latentes sont taxées à 30% (PFU) — mais avec un sursis automatique si vous partez dans l'UE/EEE. Concrètement : vous ne payez rien tant que vous ne vendez pas, et l'impôt est dégrevé au bout de 2 ans (UE) ou 5 ans (hors UE). Planifier l'expatriation avant que le portefeuille n'explose est fiscalement optimal.
Cash Flow Architecture

Les 4 comptes — Pipeline automatisé

La règle numéro un du trader rentier : ne jamais retirer directement du compte de trading vers le compte courant. La raison est psychologique autant que pratique : si votre carte bleue est reliée à votre compte IBKR, chaque cappuccino à 4.50€ sera mentalement déduit de votre equity. En un mois, vous vérifierez votre PnL 50 fois par jour et votre Sharpe s'effondrera sous la pression du micro-management.

Architecture des 4 comptes

┌─────────────────────────────────────────────────────────────────┐
│  COMPTE 1 — Trading IBKR (SASU)                                │
│  ~850K-1M€ · Capital de travail                                 │
│  Seul le WithdrawalManager y touche                             │
│  Wire mensuel automatique → Compte 2                            │
├─────────────────────────────────────────────────────────────────┤
│  COMPTE 2 — Buffer (compte pro SASU)                            │
│  ~50-80K€ · 6 mois de budget                                    │
│  Reçoit les retraits IBKR                                       │
│  Virement mensuel fixe → Compte 3                               │
│  Si buffer > 8 mois : overflow → Compte 4                       │
├─────────────────────────────────────────────────────────────────┤
│  COMPTE 3 — Courant (perso ou SASU salaire)                     │
│  ~5-10K€ · Budget mensuel                                       │
│  CB, loyer, courses, vie quotidienne                            │
│  Montant fixe peu importe la perf du mois                       │
├─────────────────────────────────────────────────────────────────┤
│  COMPTE 4 — Overflow / ETF passif (PEA + CTO perso)             │
│  Surplus automatique quand buffer > 8 mois                      │
│  DCA automatique sur ETF monde (MSCI World, S&P 500)            │
│  Assurance patrimoine long terme                                │
└─────────────────────────────────────────────────────────────────┘

Pipeline automatisé via Nomad periodic job

Le transfert entre comptes est orchestré par un job Nomad périodique qui tourne le 1er de chaque mois à 08:00 UTC :

// fichier: nomad/cashflow-pipeline.nomad.hcl

job "cashflow-pipeline" {
  type        = "batch"
  datacenters = ["hel1"]
  namespace   = "trading"

  periodic {
    crons            = ["0 8 1 * *"]  // 1er du mois, 08:00 UTC
    prohibit_overlap = true
    time_zone        = "Europe/Paris"
  }

  group "pipeline" {
    count = 1

    task "execute-withdrawal" {
      driver = "docker"

      config {
        image = "ghcr.io/myorg/cashflow-pipeline:latest"
        args  = ["--mode", "monthly-withdrawal"]
      }

      template {
        data = <<EOF
{{ with secret "secret/data/trading/ibkr" }}
IBKR_ACCOUNT_ID={{ .Data.data.account_id }}
IBKR_TOKEN={{ .Data.data.api_token }}
{{ end }}
{{ with secret "secret/data/trading/banking" }}
BUFFER_IBAN={{ .Data.data.buffer_iban }}
{{ end }}
CONSUL_HTTP_ADDR={{ env "CONSUL_HTTP_ADDR" }}
DISCORD_WEBHOOK={{ key "config/discord/webhook_cashflow" }}
EOF
        destination = "secrets/env"
        env         = true
      }

      vault {
        policies = ["trading-cashflow"]
      }

      resources {
        cpu    = 200
        memory = 256
      }
    }
  }
}

Le code Go du pipeline

package cashflow

import (
	"context"
	"fmt"
	"log"
	"time"
)

type CashflowPipeline struct {
	withdrawal *WithdrawalManager
	ibkr       IBKRClient
	banking    BankingClient
	discord    DiscordNotifier
	config     PipelineConfig
}

type PipelineConfig struct {
	MonthlyBudget       float64 // 10_000€ virement fixe vers courant
	BufferTargetMonths  int     // 6 mois
	OverflowThreshold   int     // 8 mois → overflow vers ETF
	ETFAllocation       map[string]float64 // {"IWDA.AS": 0.7, "EIMI.AS": 0.3}
}

type MonthlyResult struct {
	Date             time.Time
	EquityBefore     float64
	MonthlyPnL       float64
	Sharpe6M         float64
	WithdrawnFromIBKR float64
	TransferToDaily  float64
	OverflowToETF    float64
	BufferAfter      float64
	Reason           string
}

func (p *CashflowPipeline) ExecuteMonthly(ctx context.Context) (*MonthlyResult, error) {
	result := &MonthlyResult{Date: time.Now()}

	// 1. Lire l'état du compte IBKR
	account, err := p.ibkr.GetAccountSummary(ctx)
	if err != nil {
		return nil, fmt.Errorf("ibkr account summary: %w", err)
	}
	result.EquityBefore = account.NetLiquidation

	// 2. Calculer le PnL du mois (equity - equity début de mois)
	result.MonthlyPnL = account.NetLiquidation - account.StartOfMonthEquity
	result.Sharpe6M = account.RollingSharpe6M

	// 3. Calculer le retrait sûr
	amount, reason, err := p.withdrawal.CalculateSafeWithdrawal(
		ctx,
		account.NetLiquidation,
		result.MonthlyPnL,
		result.Sharpe6M,
	)
	if err != nil {
		return nil, fmt.Errorf("calculate withdrawal: %w", err)
	}
	result.WithdrawnFromIBKR = amount
	result.Reason = reason

	// 4. Exécuter le retrait IBKR → Buffer (si montant > 0)
	if amount > 0 {
		ddCurrent := 1.0 - account.NetLiquidation/account.HighWaterMark
		if err := p.withdrawal.Execute(ctx, amount, account.NetLiquidation, result.MonthlyPnL, result.Sharpe6M, ddCurrent); err != nil {
			return nil, fmt.Errorf("execute withdrawal: %w", err)
		}
		log.Printf("[CASHFLOW] Retrait IBKR → Buffer : %.0f€", amount)
	}

	// 5. Virement fixe Buffer → Courant
	bufferBalance := p.withdrawal.state.BufferBalance
	if bufferBalance >= p.config.MonthlyBudget {
		result.TransferToDaily = p.config.MonthlyBudget
		if err := p.banking.Transfer(ctx, "buffer", "courant", p.config.MonthlyBudget); err != nil {
			return nil, fmt.Errorf("buffer to daily transfer: %w", err)
		}
		bufferBalance -= p.config.MonthlyBudget
		log.Printf("[CASHFLOW] Buffer → Courant : %.0f€", p.config.MonthlyBudget)
	} else {
		// Buffer insuffisant — virer ce qu'il y a
		result.TransferToDaily = bufferBalance * 0.8 // garder 20% de réserve
		if result.TransferToDaily > 0 {
			if err := p.banking.Transfer(ctx, "buffer", "courant", result.TransferToDaily); err != nil {
				return nil, fmt.Errorf("partial buffer to daily: %w", err)
			}
			bufferBalance -= result.TransferToDaily
		}
		log.Printf("[CASHFLOW] Buffer insuffisant — transfert partiel %.0f€", result.TransferToDaily)
	}

	// 6. Overflow → ETF si buffer > 8 mois
	overflowThreshold := p.config.MonthlyBudget * float64(p.config.OverflowThreshold)
	if bufferBalance > overflowThreshold {
		overflow := bufferBalance - p.config.MonthlyBudget*float64(p.config.BufferTargetMonths)
		if overflow > 0 {
			result.OverflowToETF = overflow
			for etf, weight := range p.config.ETFAllocation {
				orderAmount := overflow * weight
				if err := p.ibkr.PlaceOrder(ctx, etf, orderAmount, "BUY"); err != nil {
					log.Printf("[CASHFLOW] Overflow ETF order failed for %s: %v", etf, err)
					continue
				}
				log.Printf("[CASHFLOW] Overflow → %s : %.0f€", etf, orderAmount)
			}
		}
	}

	result.BufferAfter = bufferBalance

	// 7. Notification Discord
	p.discord.SendCashflowReport(ctx, result)

	return result, nil
}

// IBKRClient — interface vers le broker
type IBKRClient interface {
	GetAccountSummary(ctx context.Context) (*AccountSummary, error)
	PlaceOrder(ctx context.Context, symbol string, amount float64, side string) error
}

type AccountSummary struct {
	NetLiquidation     float64
	StartOfMonthEquity float64
	HighWaterMark      float64
	RollingSharpe6M    float64
}

// BankingClient — interface vers la banque (API Open Banking / GoCardless)
type BankingClient interface {
	Transfer(ctx context.Context, from, to string, amount float64) error
	GetBalance(ctx context.Context, account string) (float64, error)
}

// DiscordNotifier — notifications
type DiscordNotifier interface {
	SendCashflowReport(ctx context.Context, result *MonthlyResult)
}
Pourquoi ne pas utiliser un simple cron ?
Un Nomad periodic job offre des garanties qu'un cron ne donne pas : no-overlap (si le job précédent n'est pas fini, le suivant ne démarre pas), retry automatique avec backoff, injection de secrets Vault à l'exécution (pas de credentials en clair), logs centralisés, et dead man's switch — si le job ne tourne pas, l'absence d'exécution est elle-même alertée. Un cron qui fail silencieusement pendant 3 mois et vous découvrez que votre buffer est vide quand le loyer rebondit.

Notification Discord — Rapport mensuel

// Embed Discord pour le rapport mensuel cashflow
{
  "embeds": [{
    "title": "💰 Rapport Cashflow — Mars 2026",
    "color": 1155899,
    "fields": [
      {"name": "Equity IBKR",       "value": "1 042 300€",   "inline": true},
      {"name": "PnL du mois",       "value": "+38 200€ (+3.8%)", "inline": true},
      {"name": "Retrait → Buffer",   "value": "12 400€",      "inline": true},
      {"name": "Buffer → Courant",   "value": "10 000€",      "inline": true},
      {"name": "Overflow → ETF",     "value": "8 200€",       "inline": true},
      {"name": "Buffer restant",     "value": "62 800€ (6.3 mois)", "inline": true},
      {"name": "Sharpe 6M",          "value": "2.14",          "inline": true},
      {"name": "DD courant",         "value": "3.2%",          "inline": true},
      {"name": "Total retiré (YTD)", "value": "34 800€",       "inline": true}
    ],
    "footer": {"text": "WithdrawalManager v2.1 — Prochain cycle : 1er avril 08:00"}
  }]
}
Protection du Capital

Guardrails — Le filet de sécurité multi-couche

Le compound engine ne pardonne pas les drawdowns profonds. Perdre 50% de votre capital nécessite un gain de 100% pour revenir au point de départ. À 1M€, un drawdown de 50% vous ramène à 500K€ — et votre capacité de retrait mensuel est divisée par 3. La protection du capital n'est pas une option, c'est la première priorité absolue du trader rentier.

Les 5 couches de protection

CoucheTriggerActionImpact retraits
L1 — Soft guardrailDD ≥ 10%Réduction risk 30%, retraits -50%~5K€/mois au lieu de 10K€
L2 — Hard guardrailDD ≥ 20%Suspension retraits, risk -60%0€ — puise dans buffer
L3 — Soft floorEquity ≤ 900K€Retraits -50%, mode défensif~5K€/mois si PnL positif
L4 — Hard floorEquity ≤ 800K€STOP total retraits0€ — survie du capital
L5 — Circuit breakerDD > 15% en 1 moisPause trading 1 semaine0€ — analyse post-mortem

Configuration YAML des guardrails

# guardrails.yaml — Stocké dans Consul KV: algo/guardrails
# Modifiable à chaud via consul kv put ou Claude Code

capital_protection:
  hard_floor: 800000          # € — STOP absolu, jamais modifié
  soft_floor: 900000          # € — mode dégradé
  dd_soft_threshold: 0.10     # 10% DD depuis HWM
  dd_hard_threshold: 0.20     # 20% DD depuis HWM
  monthly_dd_circuit: 0.15    # 15% DD en 1 mois calendaire → pause

withdrawal_guardrails:
  base_fraction: 0.50         # 50% du PnL mensuel
  max_annual_rate: 0.20       # Max 20% de l'equity / an
  min_monthly: 2000           # € — minimum pour exécuter
  max_monthly: 25000          # € — plafond dur
  buffer_target_months: 6     # Mois de budget en réserve
  buffer_critical_months: 3   # En dessous → reconstitution agressive
  sharpe_bonus_threshold: 2.0 # Sharpe 6M au-dessus → bonus 25%

risk_scaling:
  normal:                     # DD < 10%
    position_size_mult: 1.0
    max_gross_exposure: 1.0
    strategies_active: "all"
  defensive:                  # 10% < DD < 20%
    position_size_mult: 0.7
    max_gross_exposure: 0.7
    strategies_active: "core_only"  # Momentum + Mean Reversion
  critical:                   # DD > 20%
    position_size_mult: 0.4
    max_gross_exposure: 0.4
    strategies_active: "mean_reversion_only"  # Le plus défensif

insurance_layer:
  enabled: true
  put_allocation: 0.05       # 5% de l'equity en puts protecteurs
  instruments:
    - symbol: SPY
      weight: 0.60
      strike_delta: 0.30     # 30-delta OTM puts
      expiry_months: 3       # Roulés trimestriellement
    - symbol: QQQ
      weight: 0.40
      strike_delta: 0.30
      expiry_months: 3
  rebalance_frequency: "quarterly"
  max_annual_cost: 0.03      # Max 3% de l'equity / an en primes

cash_reserve:
  target_allocation: 0.10    # 10% en instruments monétaires
  instruments:
    - symbol: BIL             # SPDR 1-3 Month T-Bill ETF
      weight: 0.50
    - symbol: SHV             # iShares Short Treasury Bond ETF
      weight: 0.30
    - symbol: SGOV            # iShares 0-3 Month Treasury Bond ETF
      weight: 0.20
  rebalance_threshold: 0.02  # Rééquilibrer si écart > 2%

alerts:
  discord_webhook: "vault:secret/data/trading/discord#cashflow_webhook"
  channels:
    critical: "#trading-critical"    # Hard floor, circuit breaker
    warning:  "#trading-alerts"      # Soft guardrails
    info:     "#trading-cashflow"    # Retraits normaux
  escalation:
    - level: L1
      delay_minutes: 0
      channel: warning
    - level: L2
      delay_minutes: 0
      channel: critical
    - level: L5
      delay_minutes: 0
      channel: critical
      mention: "@here"

Insurance layer — Protection par puts

Allouer 5% de l'equity (~50K€ sur 1M€) en puts protecteurs sur SPY et QQQ. Coût annuel typique : 2-3% de l'equity (20-30K€/an). C'est le prix de l'assurance anti-crash.

package insurance

import (
	"context"
	"fmt"
	"math"
	"time"
)

type InsuranceManager struct {
	ibkr   IBKRClient
	config InsuranceConfig
}

type InsuranceConfig struct {
	Instruments []InsuranceInstrument
	MaxAnnualCost float64 // 0.03 = 3% max
	Equity        float64
}

type InsuranceInstrument struct {
	Symbol       string
	Weight       float64
	StrikeDelta  float64 // 0.30 = 30-delta
	ExpiryMonths int
}

type PutPosition struct {
	Symbol     string
	Strike     float64
	Expiry     time.Time
	Contracts  int
	Premium    float64
	Delta      float64
}

func (im *InsuranceManager) RollQuarterly(ctx context.Context) ([]PutPosition, error) {
	totalBudget := im.config.Equity * im.config.MaxAnnualCost / 4.0 // Budget trimestriel
	positions := make([]PutPosition, 0)

	for _, inst := range im.config.Instruments {
		budget := totalBudget * inst.Weight

		// Récupérer le prix spot
		quote, err := im.ibkr.GetQuote(ctx, inst.Symbol)
		if err != nil {
			return nil, fmt.Errorf("get quote %s: %w", inst.Symbol, err)
		}

		// Calculer le strike ~30-delta OTM
		// Approximation : strike = spot * (1 - delta * sqrt(T) * vol)
		vol := quote.ImpliedVol
		T := float64(inst.ExpiryMonths) / 12.0
		strike := quote.Last * (1.0 - inst.StrikeDelta*math.Sqrt(T)*vol)
		strike = math.Round(strike) // Arrondir au strike disponible

		// Estimer le premium par contrat
		premiumPerContract := quote.Last * vol * math.Sqrt(T) * inst.StrikeDelta * 0.4
		premiumPerContract *= 100 // 100 shares par contrat

		// Nombre de contrats
		contracts := int(budget / premiumPerContract)
		if contracts < 1 {
			contracts = 1
		}

		pos := PutPosition{
			Symbol:    inst.Symbol,
			Strike:    strike,
			Expiry:    time.Now().AddDate(0, inst.ExpiryMonths, 0),
			Contracts: contracts,
			Premium:   premiumPerContract * float64(contracts),
			Delta:     -inst.StrikeDelta * float64(contracts) * 100,
		}

		// Passer l'ordre
		if err := im.ibkr.PlacePutOrder(ctx, pos); err != nil {
			return nil, fmt.Errorf("place put order %s: %w", inst.Symbol, err)
		}

		positions = append(positions, pos)
		fmt.Printf("[INSURANCE] %s %d puts @ strike %.0f, expiry %s, premium %.0f€\n",
			pos.Symbol, pos.Contracts, pos.Strike,
			pos.Expiry.Format("2006-01-02"), pos.Premium)
	}

	return positions, nil
}
Le coût de l'assurance est non-négociable
20-30K€/an en primes de puts, ça fait mal quand l'année est bonne et que les puts expirent worthless. La tentation est de « skip un trimestre » parce que « le marché est calme ». Ne le faites jamais. L'assurance n'a de valeur que si elle est systématique. Le trimestre où vous skip est statistiquement celui où le cygne noir arrive. C'est le biais de récence à son pire.
Budget du Million-Rentier

1M€, Sharpe 1.8, CAGR ~40% — Le budget réaliste

Capital : 1 000 000€. Sharpe ratio : 1.8. CAGR net de frais : ~40%. Volatilité annualisée : ~22%. Retrait adaptatif : ~50% du PnL mensuel, plafonné à 20% annuel.

En pratique, ça donne :

MétriqueAnnuelMensuelAprès impôts (30%)
PnL brut attendu~400 000 €~33 000 €
Retrait (50% du PnL)~200 000 €~16 600 €
Plafond (20% equity)200 000 €16 600 €
Retrait effectif~180 000 €~15 000 €~10 500 €
Réinvesti (compound)~220 000 €~18 300 €

Avec la structure SASU + PEA (Section 3), le taux effectif descend à ~22-25%, soit ~11 500-12 000€/mois net. Mais budgétons conservativement à 10 000€ net pour avoir de la marge.

Budget type — 10 000€/mois net

PosteMensuel% du budgetNotes
Logement (loyer/crédit)2 000 €20%T3 Paris ou T4 province
Charges fixes (énergie, internet, tel)350 €3.5%Inclut fibre + 5G
Alimentation600 €6%Courses + restaurants
Transport300 €3%Pas de voiture, transports + VTC
Santé / mutuelle200 €2%Mutuelle TNS
Infrastructure trading300 €3%Hetzner, data feeds, Tailscale, domaines
Comptable + juridique250 €2.5%SASU comptabilité + CFE
Assurances (puts + cash)2 500 €25%Puts OTM + T-Bills (amorti)
Épargne long terme (ETF overflow)1 500 €15%DCA MSCI World via PEA
Loisirs / voyages1 000 €10%Budget flexible
Réserve / imprévus1 000 €10%Non dépensé → cumule
Total10 000 €100%
Le danger du lifestyle inflation
À 10 000€/mois net, vous êtes dans le top 3% des revenus français. La tentation d'upgrader est permanente : appartement plus grand (+800€), leasing BMW (+500€), business class systématique (+2 000€/voyage). En 6 mois, votre budget passe de 10K€ à 14K€ et votre marge de sécurité disparaît. Si un trimestre de DD réduit vos retraits à 5K€, vous êtes en stress immédiat. Le trader rentier qui dure est celui qui vit à 7K€/mois et stocke les 3K€ restants. La marge, c'est la liberté.
Réinvestissement & Diversification

L'allocation du patrimoine total

Votre algo n'est pas votre patrimoine — c'est un générateur de cash flow. Le patrimoine, c'est l'ensemble des actifs dans lesquels vous répartissez ce cash flow pour réduire le risque de concentration. Un trader dont 100% du patrimoine dépend d'un seul algo sur un seul broker est un trader à un single point of failure du désastre.

Allocation cible — Le modèle 60/20/10/5/5

PocheAllocationMontant (sur 1.5M€ patrimoine)InstrumentsObjectif
Algo Trading60%900 000 €Compte IBKR SASUGénération alpha
ETF Passif20%300 000 €PEA (MSCI Europe) + CTO (S&P 500, MSCI World)Diversification beta
Immobilier SCPI10%150 000 €Corum Origin, Iroko Zen, Remake LiveRevenus réguliers, décorrélation
Crypto5%75 000 €BTC 60%, ETH 30%, SOL 10%Asymétrie convexe
Cash / T-Bills5%75 000 €BIL, SHV, Livret ALiquidité d'urgence

Rééquilibrage trimestriel automatisé

package rebalance

import (
	"context"
	"fmt"
	"math"
)

type AssetClass struct {
	Name           string
	TargetWeight   float64
	CurrentValue   float64
	MinWeight      float64 // Tolérance basse
	MaxWeight      float64 // Tolérance haute
	Rebalanceable  bool    // false pour SCPI (illiquide)
}

type RebalanceManager struct {
	classes []AssetClass
}

type RebalanceOrder struct {
	Class     string
	Action    string  // "BUY" ou "SELL"
	Amount    float64
	Reason    string
}

func NewRebalanceManager() *RebalanceManager {
	return &RebalanceManager{
		classes: []AssetClass{
			{Name: "Algo Trading", TargetWeight: 0.60, MinWeight: 0.50, MaxWeight: 0.70, Rebalanceable: true},
			{Name: "ETF Passif",   TargetWeight: 0.20, MinWeight: 0.15, MaxWeight: 0.25, Rebalanceable: true},
			{Name: "SCPI",         TargetWeight: 0.10, MinWeight: 0.05, MaxWeight: 0.15, Rebalanceable: false},
			{Name: "Crypto",       TargetWeight: 0.05, MinWeight: 0.02, MaxWeight: 0.08, Rebalanceable: true},
			{Name: "Cash/T-Bills", TargetWeight: 0.05, MinWeight: 0.03, MaxWeight: 0.10, Rebalanceable: true},
		},
	}
}

func (rm *RebalanceManager) CheckAndRebalance(ctx context.Context, values map[string]float64) ([]RebalanceOrder, error) {
	// Calculer le patrimoine total
	totalValue := 0.0
	for _, class := range rm.classes {
		v, ok := values[class.Name]
		if !ok {
			return nil, fmt.Errorf("missing value for class %s", class.Name)
		}
		class.CurrentValue = v
		totalValue += v
	}

	// Mettre à jour les valeurs courantes
	for i := range rm.classes {
		rm.classes[i].CurrentValue = values[rm.classes[i].Name]
	}

	orders := make([]RebalanceOrder, 0)

	for _, class := range rm.classes {
		currentWeight := class.CurrentValue / totalValue
		drift := currentWeight - class.TargetWeight

		// Vérifier si hors bandes de tolérance
		if currentWeight < class.MinWeight || currentWeight > class.MaxWeight {
			if !class.Rebalanceable {
				fmt.Printf("[REBALANCE] %s hors bandes (%.1f%%) mais non rééquilibrable\n",
					class.Name, currentWeight*100)
				continue
			}

			targetValue := totalValue * class.TargetWeight
			delta := targetValue - class.CurrentValue

			action := "BUY"
			if delta < 0 {
				action = "SELL"
				delta = math.Abs(delta)
			}

			orders = append(orders, RebalanceOrder{
				Class:  class.Name,
				Action: action,
				Amount: math.Round(delta),
				Reason: fmt.Sprintf("Drift %.1f%% (current %.1f%%, target %.1f%%, band [%.1f%%-%.1f%%])",
					drift*100, currentWeight*100, class.TargetWeight*100,
					class.MinWeight*100, class.MaxWeight*100),
			})
		}
	}

	return orders, nil
}

// Exemple d'utilisation dans le job Nomad trimestriel
func Example() {
	rm := NewRebalanceManager()

	values := map[string]float64{
		"Algo Trading": 1_150_000, // Sur-pondéré (bonne perf)
		"ETF Passif":   280_000,
		"SCPI":         155_000,   // Stable (illiquide)
		"Crypto":       120_000,   // BTC pump
		"Cash/T-Bills": 60_000,
	}

	orders, _ := rm.CheckAndRebalance(context.Background(), values)
	for _, o := range orders {
		fmt.Printf("[ORDER] %s %s %.0f€ — %s\n", o.Action, o.Class, o.Amount, o.Reason)
	}
	// Output:
	// [ORDER] SELL Algo Trading 95400€ — Drift 5.1% (...)
	// [ORDER] SELL Crypto 36300€ — Drift 1.8% (...)
	// [ORDER] BUY ETF Passif 73800€ — Drift -4.1% (...)
	// [ORDER] BUY Cash/T-Bills 28500€ — Drift -1.6% (...)
}

À 2M€ : le family office mono-personne

Quand le patrimoine total dépasse 2M€, les enjeux changent de nature. Vous n'êtes plus un trader qui gère un compte — vous êtes un micro family office qui gère un patrimoine multi-classes, multi-juridictions, multi-devises.

Le stack évolue :

package patrimoine

import (
	"context"
	"encoding/json"
	"fmt"
	"time"
)

type PatrimoineReport struct {
	Date            time.Time              `json:"date"`
	TotalValue      float64                `json:"total_value"`
	MonthlyChange   float64                `json:"monthly_change"`
	YTDReturn       float64                `json:"ytd_return"`
	Classes         []ClassReport          `json:"classes"`
	TopPerformer    string                 `json:"top_performer"`
	WorstPerformer  string                 `json:"worst_performer"`
	NextRebalance   time.Time              `json:"next_rebalance"`
	Alerts          []string               `json:"alerts"`
}

type ClassReport struct {
	Name         string  `json:"name"`
	Value        float64 `json:"value"`
	Weight       float64 `json:"weight"`
	TargetWeight float64 `json:"target_weight"`
	MonthReturn  float64 `json:"month_return"`
	YTDReturn    float64 `json:"ytd_return"`
}

func GenerateWeeklyReport(ctx context.Context) (*PatrimoineReport, error) {
	report := &PatrimoineReport{
		Date: time.Now(),
	}

	// Agréger depuis chaque source
	sources := []struct {
		name   string
		fetch  func(context.Context) (float64, float64, error)
	}{
		{"Algo Trading", fetchIBKR},
		{"ETF Passif", fetchDegiro},
		{"SCPI", fetchSCPIValuation},
		{"Crypto", fetchCryptoWallet},
		{"Cash/T-Bills", fetchBankAccounts},
	}

	for _, src := range sources {
		value, monthReturn, err := src.fetch(ctx)
		if err != nil {
			report.Alerts = append(report.Alerts,
				fmt.Sprintf("Erreur %s: %v", src.name, err))
			continue
		}
		report.TotalValue += value
		report.Classes = append(report.Classes, ClassReport{
			Name:        src.name,
			Value:       value,
			MonthReturn: monthReturn,
		})
	}

	// Calculer les poids
	for i := range report.Classes {
		report.Classes[i].Weight = report.Classes[i].Value / report.TotalValue
	}

	data, _ := json.MarshalIndent(report, "", "  ")
	fmt.Println(string(data))

	return report, nil
}

func fetchIBKR(ctx context.Context) (float64, float64, error)        { return 1_050_000, 0.038, nil }
func fetchDegiro(ctx context.Context) (float64, float64, error)      { return 310_000, 0.021, nil }
func fetchSCPIValuation(ctx context.Context) (float64, float64, error) { return 158_000, 0.004, nil }
func fetchCryptoWallet(ctx context.Context) (float64, float64, error)  { return 82_000, -0.052, nil }
func fetchBankAccounts(ctx context.Context) (float64, float64, error)  { return 68_000, 0.003, nil }
Pourquoi pas 100% algo ?
Si votre algo génère 40%/an, pourquoi diversifier dans des ETF à 8% ou des SCPI à 4% ? Parce que le risque de ruine d'un portefeuille concentré à 100% dans un seul système de trading, chez un seul broker, dans un seul marché, est non-nul. IBKR peut avoir une panne de 48h pendant un flash crash. Votre algo peut subir un alpha decay soudain. Le régulateur peut changer les règles. La diversification n'optimise pas le rendement — elle optimise la probabilité de survie à long terme.

Résumé — Les 10 commandements du trader rentier

Points clés — Vivre de son Algo

  1. Phase 1 sacrée — Zéro retrait pendant 18 mois minimum. Le compound est votre meilleur allié.
  2. Retraits adaptatifs — Jamais un montant fixe. Le WithdrawalManager décide en fonction du PnL, du DD, et du Sharpe rolling.
  3. Buffer 6 mois — Toujours 50-60K€ de côté pour découpler le trading de la vie quotidienne.
  4. Hard floor 800K€ — Ligne rouge absolue. En dessous, zéro retrait, mode survie.
  5. 4 comptes séparés — Trading → Buffer → Courant → ETF overflow. Jamais de retrait direct.
  6. Flat tax 30% optimisée — SASU + PEA pour descendre à 22-25% effectif.
  7. Insurance layer — 5% en puts SPY/QQQ roulés trimestriellement. Non négociable.
  8. 60/20/10/5/5 — Ne mettez jamais 100% dans l'algo. Diversifiez le patrimoine.
  9. Lifestyle à 60% — Vivez à 60% de votre capacité de retrait. La marge, c'est la liberté.
  10. Automatisez tout — Retraits, transfers, rééquilibrage, reporting. L'humain ne décide plus, il supervise.
Partie suivante
Scaling & Exit — Au-delà du Million →
Algo Trading — De 100K au Million11/12