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.
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.
Capital de départ : 100K€. Objectif : compound à 100% sans retrait.
Trajectoire réaliste avec un Sharpe de 1.5-2.0 et un système multi-stratégie diversifié :
| Mois | Equity | PnL mensuel | Retrait | Compound |
|---|---|---|---|---|
| 0 | 100 000 € | — | 0 € | 100% |
| 3 | 118 000 € | ~5 500 €/m | 0 € | 100% |
| 6 | 142 000 € | ~7 200 €/m | 0 € | 100% |
| 9 | 173 000 € | ~9 800 €/m | 0 € | 100% |
| 12 | 215 000 € | ~13 000 €/m | 0 € | 100% |
| 15 | 278 000 € | ~18 500 €/m | 0 € | 100% |
| 18 | 370 000 € | ~26 000 €/m | 0 € | 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%.
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 :
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 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.
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 :
| Dimension | Retraite classique (60/40) | Algo Trading Rentier |
|---|---|---|
| Rendement attendu | 7-8% nominal/an | 30-60% nominal/an |
| Volatilité | 10-12% annualisée | 25-45% annualisée |
| Distribution des returns | ~Gaussienne | Fat tails, skew variable |
| Drawdown max typique | 30-40% (2008) | 15-25% (circuit breakers) |
| Source de rendement | Beta marché | Alpha + beta adaptatif |
| Risque principal | Inflation, longévité | Alpha decay, régime shift |
| Taux de retrait soutenable | 3-4% / an | 15-25% / an (adaptatif) |
| Horizon | 30+ ans | 5-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.
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)
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,
)
}
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.
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étrique | Valeur | Interprétation |
|---|---|---|
| Probabilité de ruine (10 ans) | 2.3% | Acceptable (< 5%) |
| Equity médiane à 10 ans | 8.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édian | 22% | Cohérent avec nos guardrails |
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.
| Structure | Taux effectif | Plafond | Liquidité | Complexité | Pour qui ? |
|---|---|---|---|---|---|
| CTO personnel | 30% (PFU) | Illimité | Immédiate | Nulle | Simple, < 500K€ |
| PEA | 17.2% (PS seuls après 5 ans) | 150K€ versements | 5 ans min | Faible | ETF EU, long terme |
| PEA-PME | 17.2% (PS seuls après 5 ans) | 225K€ (cumul PEA) | 5 ans min | Faible | Small caps EU |
| SAS/SASU | IS 15% (≤ 42.5K€) puis 25% | Illimité | Salaire/dividende | Moyenne | Trader pro, > 100K€ PnL/an |
| Holding (SASU mère) | IS 15-25% + mère-fille 5% net | Illimité | Via dividendes | Élevée | > 500K€ PnL/an, multi-poches |
| AV Luxembourg | Différé, PS seuls au rachat | Illimité | Rachats partiels | Élevée | > 250K€, triangle sécurité |
| AV France (fonds €) | 7.5% + PS après 8 ans (≤ 4.6K€) | Illimité | Rachats partiels | Faible | Poche sécurité |
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
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.
| Juridiction | Taux sur plus-values | Conditions | Qualité de vie | Verdict |
|---|---|---|---|---|
| Dubaï (UAE) | 0% | Visa freelance ~5K€/an, 183 jours résidence | Excellente (si chaleur OK) | Optimal > 500K€/an de PnL |
| Andorre | 10% max | Résidence passive 400K€ dépôt, ~15K€/an | Correcte (montagne) | Bon compromis Europe |
| Portugal (NHR) | 0% (10 ans, supprimé 2024) | Programme fermé aux nouveaux | Excellente | Fermé, legacy only |
| Suisse (forfait) | Forfait fiscal ~200K CHF/an | Pas d'activité lucrative en CH | Excellente | Intéressant > 1M€/an PnL |
| Malte | 0-5% (non-dom) | Résidence, remittance basis | Correcte | EU, intéressant pour holding |
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.
┌─────────────────────────────────────────────────────────────────┐
│ 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 │
└─────────────────────────────────────────────────────────────────┘
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
}
}
}
}
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)
}
// 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"}
}]
}
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.
| Couche | Trigger | Action | Impact retraits |
|---|---|---|---|
| L1 — Soft guardrail | DD ≥ 10% | Réduction risk 30%, retraits -50% | ~5K€/mois au lieu de 10K€ |
| L2 — Hard guardrail | DD ≥ 20% | Suspension retraits, risk -60% | 0€ — puise dans buffer |
| L3 — Soft floor | Equity ≤ 900K€ | Retraits -50%, mode défensif | ~5K€/mois si PnL positif |
| L4 — Hard floor | Equity ≤ 800K€ | STOP total retraits | 0€ — survie du capital |
| L5 — Circuit breaker | DD > 15% en 1 mois | Pause trading 1 semaine | 0€ — analyse post-mortem |
# 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"
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
}
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étrique | Annuel | Mensuel | Aprè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.
| Poste | Mensuel | % du budget | Notes |
|---|---|---|---|
| Logement (loyer/crédit) | 2 000 € | 20% | T3 Paris ou T4 province |
| Charges fixes (énergie, internet, tel) | 350 € | 3.5% | Inclut fibre + 5G |
| Alimentation | 600 € | 6% | Courses + restaurants |
| Transport | 300 € | 3% | Pas de voiture, transports + VTC |
| Santé / mutuelle | 200 € | 2% | Mutuelle TNS |
| Infrastructure trading | 300 € | 3% | Hetzner, data feeds, Tailscale, domaines |
| Comptable + juridique | 250 € | 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 / voyages | 1 000 € | 10% | Budget flexible |
| Réserve / imprévus | 1 000 € | 10% | Non dépensé → cumule |
| Total | 10 000 € | 100% |
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.
| Poche | Allocation | Montant (sur 1.5M€ patrimoine) | Instruments | Objectif |
|---|---|---|---|---|
| Algo Trading | 60% | 900 000 € | Compte IBKR SASU | Génération alpha |
| ETF Passif | 20% | 300 000 € | PEA (MSCI Europe) + CTO (S&P 500, MSCI World) | Diversification beta |
| Immobilier SCPI | 10% | 150 000 € | Corum Origin, Iroko Zen, Remake Live | Revenus réguliers, décorrélation |
| Crypto | 5% | 75 000 € | BTC 60%, ETH 30%, SOL 10% | Asymétrie convexe |
| Cash / T-Bills | 5% | 75 000 € | BIL, SHV, Livret A | Liquidité d'urgence |
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% (...)
}
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 }