Если вы когда-нибудь задумывались о том, на сколько лет хватит ваших накоплений после выхода на пенсию — эта статья для вас. Мы разберём, как с помощью open-source библиотеки okama для Python можно моделировать и тестировать различные стратегии снятия денег с инвестиционного портфеля. От классического «правила 4%» до продвинутых адаптивных стратегий — всё с примерами кода.
okama — это Python библиотека для количественного анализа инвестиционных стратегий. okama работает по API с собственной бесплатной базой исторических данных. База данных содержит информацию о российских активах (Мосбиржа, индексы, курсы валют ЦБ), зарубежные акции и ETF, товарные активы, валюты и криптовалюты, цены на недвижимость, макроэкономический данные и много еще чего. Сама библиотека работает с прогнозами, использует метод Монте-Карло, оптимизацию по Марковицу и более продвинутые виды оптимизации, умеет делать бэктесты с учётом денежных потоков. Именно последнее нам и понадобится для моделирования пенсионных стратегий.
1. Что такое инвестиционный портфель пенсионера
На этапе накопления инвестор регулярно пополняет портфель. Его задача — максимизировать капитал к моменту выхода на пенсию. Но в день выхода на пенсию всё переворачивается: вместо пополнения начинаются регулярные изъятия. И тут возникает главный вопрос пенсионного планирования — сколько можно снимать каждый год, чтобы деньги не закончились слишком рано?
Этот вопрос нетривиален по нескольким причинам:
Риск последовательности доходностей (Sequence of returns risk) — если рынок сильно упадёт в первые годы после выхода на пенсию, портфель может истощиться гораздо быстрее, даже если средняя доходность за весь период нормальная.
Инфляция — покупательная способность фиксированной суммы снижается с каждым годом.
Неопределённость продолжительности жизни — сколько лет нужно «протянуть» на накоплениях?
Именно для ответа на эти вопросы и существуют стратегии снятия (withdrawal strategies) — алгоритмы, определяющие, сколько денег пенсионер забирает из портфеля каждый год.
2. Упрощённые подходы. Правило 4%
Самый известный подход — правило 4%, предложенное финансовым консультантом Уильямом Бенгеном в 1994 году. Идея проста: в первый год пенсии вы снимаете 4% от начальной стоимости портфеля, а в каждый последующий год корректируете эту сумму на инфляцию.
Например, при портфеле в 1 000 000 ₽ вы снимаете 40 000 ₽ в первый год. Если инфляция за год составила 8%, то во второй год вы снимаете уже 43 200 ₽ — и так далее, независимо от того, как ведёт себя рынок.
Бенген протестировал эту стратегию на исторических данных за период с 1926 года и обнаружил, что 4% — максимальная начальная ставка, при которой портфель из акций и облигаций США «выживал» как минимум 30 лет даже в наихудших сценариях.
Плюсы: предсказуемый стабильный доход, простота реализации.
Минусы: стратегия полностью игнорирует текущее состояние рынка. В хорошие годы вы недополучаете, а в плохие — продолжаете снимать фиксированную сумму, ускоряя истощение портфеля. Кроме того, правило 4% было рассчитано для рынка США, и его применимость к другим рынкам (включая российский с его более высокой волатильностью и инфляцией) — вопрос открытый.
Другой простой подход — снятие фиксированного процента от текущей стоимости портфеля (например, те же 4% каждый год, но уже от текущего баланса). В этом случае портфель формально никогда не обнулится, но доход становится крайне волатильным: при падении рынка на 30% ваш годовой «доход» тоже упадёт на 30%.
3. Знакомство с okama: установка и первый портфель
Библиотека okama поддерживает бэктестинг стратегий с денежными потоками через класс PortfolioDCF, доступный как атрибут Portfolio.dcf.
Установка
Установка выполняется одной командой в терминале:
pip install okama
Все необходимые зависимости — pandas, numpy, matplotlib, scipy и пара других — подтянутся автоматически, отдельно ставить ничего не нужно.
После установки импортируем библиотеки, которые будем использовать во всех примерах:
import warnings warnings.simplefilter(action="ignore", category=FutureWarning) import matplotlib.pyplot as plt plt.rcParams["figure.figsize"] = [12.0, 6.0] import pandas as pd import okama as ok
Создаём портфель
Для примеров будем использовать портфель из российских индексов и золота:
MCFTR.INDX — Индекс Мосбиржи полной доходности (акции)
RUCBTRNS.INDX — Индекс корпоративных облигаций Мосбиржи
GC.COMM — физическое золото
Класс Portfolio задаёт базовую инвестиционную стратегию (активы, веса и ребалансировку):
pf = ok.Portfolio( ["MCFTR.INDX", "RUCBTRNS.INDX", "GC.COMM"], weights=[0.60, 0.35, 0.05], ccy="RUB", inflation=True, last_date="2026-02", rebalancing_strategy=ok.Rebalance(period="year"), symbol="Pension_portfolio.PF", ) pf
symbol Pension_portfolio.PF assets [MCFTR.INDX, RUCBTRNS.INDX, GC.COMM] weights [0.6, 0.35, 0.05] rebalancing_period year rebalancing_abs_deviation None rebalancing_rel_deviation None currency RUB inflation RUB.INFL first_date 2003-01 last_date 2026-02 period_length 23 years, 2 months dtype: object
IndexationStrategy — стратегия с индексацией (аналог правила 4%)
Класс IndexationStrategy реализует стратегию с фиксированной суммой снятия, которая индексируется на инфляцию. Это прямой аналог правила 4%. Отрицательное значение amount означает снятие, положительное — пополнение.
ind = ok.IndexationStrategy(pf) # создаём стратегию, привязанную к портфелю ind.initial_investment = 10_000 # размер начальных вложений ind.amount = -2_500 # размер годового снятия ind.frequency = "year" # частота — раз в год ind.indexation = "inflation" # индексация на среднюю инфляцию ind
Strategy name fixed_amount Portfolio symbol Pension_portfolio.PF Cash flow initial investment 10000 Cash flow frequency year Cash flow strategy fixed_amount Cash flow amount -2500 Cash flow indexation 0.082999 dtype: object
Привязываем стратегию к портфелю и запускаем бэктест:
pf.dcf.cashflow_parameters = ind
pf.dcf.wealth_index(discounting="fv", include_negative_values=False).plot();

При снятии 2 500 из 10 000 (25% в год!) портфель истощается очень быстро. Узнаем, сколько “протянул” портфель до обнуления баланса. Это называется “периодом выживания” (внимание, портфеля а не пенсионера):
pf.dcf.survival_period_hist() # количество лет
9.0
Точная дата обнуления портфеля при тестировнии на исторических данных:
pf.dcf.survival_date_hist()
Timestamp('2011-12-31 00:00:00')
Уменьшим снятие до разумного уровня:
pf.dcf.cashflow_parameters.amount = -600 # ~6% в первый год
pf.dcf.wealth_index(discounting="fv", include_negative_values=False).plot();

С меньшим снятием портфель проходит исторический бэктест, хотя его реальный (с учётом инфляции) баланс снижается.
Но пройдёт ли стратегия проверку будущим? Для ответа нужна симуляция Монте-Карло.
Симуляция Монте-Карло
Параметры теста Монте-Карло задаются в set_mc_parameters:
pf.dcf.set_mc_parameters( distribution="t", # t-распределение Стьюдента (тяжёлые хвосты) period=60, # прогноз на 60 лет mc_number=400, # 400 случайных сценариев )
Теперь можно посмотреть наглядно как вел себя портфель с изъятиями в каждом из случайных сценариев с помощью plot_forecast_monte_carlo().
pf.dcf.plot_forecast_monte_carlo(backtest=False) plt.yscale("log") # логарифмическая шкала для наглядности

Распределение “периодов выживания” по сценариям:
s = pf.dcf.monte_carlo_survival_period() s
0 20.9 1 60.0 2 60.0 3 20.9 4 60.0 ... 395 60.0 396 60.0 397 60.0 398 60.0 399 60.0 Length: 400, dtype: float64
s.quantile(5 / 100) # худшие 5% сценариев
np.float64(19.9)
s.quantile(50 / 100) # медианный сценарий
np.float64(60.0)
То есть в худших сценариях портфеля хватало примерно на 20 лет. А в медианном (наиболее вероятном) - на 60 лет и более.
Поиск максимальной безопасной ставки снятия
Метод find_the_largest_withdrawals_size() автоматически находит максимальный размер снятия денег, при котором портфель «выживает» с заданной вероятностью. Поддерживаются три цели:
survival_period— не "обнулить" портфель заданное число летmaintain_balance_pv— сохранить покупательную способность портфеля (реальный баланс)maintain_balance_fv— сохранить номинальный баланс
ВНИМАНИЕ: метод может работать медленно при большом числе сценариев Монте-Карло и длинных сроках прогноза.
result = pf.dcf.find_the_largest_withdrawals_size( goal="survival_period", target_survival_period=30, # портфель должен прожить 30 лет percentile=5, # даже в 5% худших сценариев )
2026-04-06 12:06:06,420 - INFO - Iteration 0: error_rel=0.970, gradient=0.000 2026-04-06 12:06:14,751 - INFO - Iteration 1: error_rel=0.937, gradient=-0.033 2026-04-06 12:06:22,779 - INFO - Iteration 2: error_rel=0.870, gradient=-0.067 2026-04-06 12:06:30,947 - INFO - Iteration 3: error_rel=0.737, gradient=-0.133 2026-04-06 12:06:40,022 - INFO - Iteration 4: error_rel=0.370, gradient=-0.367 2026-04-06 12:06:48,368 - INFO - Iteration 5: error_rel=1.000, gradient=0.630 2026-04-06 12:06:57,171 - INFO - Iteration 6: error_rel=0.562, gradient=-0.438 2026-04-06 12:07:05,880 - INFO - Iteration 7: error_rel=0.203, gradient=-0.358 2026-04-06 12:07:14,810 - INFO - Iteration 8: error_rel=0.137, gradient=-0.067 2026-04-06 12:07:23,544 - INFO - Iteration 9: error_rel=0.227, gradient=0.090 2026-04-06 12:07:32,847 - INFO - Iteration 10: error_rel=0.163, gradient=-0.063 2026-04-06 12:07:41,766 - INFO - Iteration 11: error_rel=0.003, gradient=-0.160 2026-04-06 12:07:41,766 - INFO - Solution found: -502.93 or 5.03% after 12 steps.
result.withdrawal_abs # абсолютная сумма снятия
-502.9296875
result.withdrawal_rel # относительная (доля от начальных вложений)
0.05029296875
Если требуется более точный результат, можно настроить параметр tolerance_rel (по умолчанию допускается 10-процентная погрешность).
Проверим результат — подставим найденную сумму в стратегию изъятий и пересчитаем прогноз:
ind.amount = result.withdrawal_abs ind
Strategy name fixed_amount Portfolio symbol Pension_portfolio.PF Cash flow initial investment 10000 Cash flow frequency year Cash flow strategy fixed_amount Cash flow amount -502.929688 Cash flow indexation 0.082999 dtype: object
s = pf.dcf.monte_carlo_survival_period() s.quantile(5 / 100) # 5% самых плохих сценариев
np.float64(36.85)
PercentageStrategy — стратегия фиксированного процента
Класс PercentageStrategy реализует снятие фиксированного процента от текущей стоимости портфеля. Это похоже на популярное «правило 4%», но здесь можно тестировать любой процент снятий.
pc = ok.PercentageStrategy(pf) pc.initial_investment = 10_000 pc.frequency = "year" pc.percentage = -0.12 # 12% от текущего баланса каждый год pc
Strategy name fixed_percentage Portfolio symbol Pension_portfolio.PF Cash flow initial investment 10000 Cash flow frequency year Cash flow strategy fixed_percentage Cash flow percentage -0.12 dtype: object
pf.dcf.cashflow_parameters = pc
12% — заведомо агрессивная ставка, значительно превышающая рекомендуемые 4%. Проверим, переживёт ли портфель историю:
pf.dcf.wealth_index(discounting="fv", include_negative_values=False).plot();

Портфель заметно «худеет», но формально не обнуляется — ведь мы всегда снимаем лишь долю от остатка. Главный минус виден на графике денежных потоков. Они очень сильно меняются от года к году:
cf_percentage = pf.dcf.cash_flow_ts(discounting="fv").resample("Y").sum() cf_percentage.plot(kind="bar");

Монте-Карло для PercentageStrategy
Проверим, как ведут себя такие стратегии методом Моне-Карло.
pf.dcf.plot_forecast_monte_carlo(backtest=False);

В стратегии с фиксированным процентом снятий баланс формально не обнуляется даже на 5й перцентили (в очень плохих сценариях).
# если баланс равен 1%, то портфель считается "обнулившимся" s_percentage = pf.dcf.monte_carlo_survival_period(threshold=0.01) s_percentage.describe([0.05, 0.20, 0.50, 0.70])
count 400.000000 mean 59.610000 std 2.639017 min 24.700000 5% 60.000000 20% 60.000000 50% 60.000000 70% 60.000000 max 60.000000 dtype: float64
Хотя портфель «жив», реальные выплаты к концу периода становятся ничтожными. Чтобы посмотреть на размер выплат в прогнозе возьмем один из случайных сценариев (сценарий номер 0):
mc_cashflow_percentage = pf.dcf.monte_carlo_cash_flow(discounting="pv") mc_cashflow_percentage[0].resample("Y").sum().plot(kind="bar");

Статистика по выплатам в последний год (то есть чрез 60 лет) во всех сценариях тоже неутешительна.
mc_cashflow_percentage.iloc[-1, :].describe()
count 400.000000 mean -46.772482 std 77.148852 min -833.290446 25% -55.448857 50% -22.847218 75% -8.638450 max -0.481002 Name: 2086-01, dtype: float64
Наследство: сохранение баланса портфеля
Если цель — оставить наследство (сохранить покупательную способность портфеля), можно найти максимальную безопасную процентную ставку снятия. Пусть пенсионер хочет оставить наследство через 30 лет.
pf.dcf.set_mc_parameters(distribution="t", period=30, mc_number=400)
pf.dcf.discount_rate = None # дисконтирование денежных потоков происходит по средней инфляции pf.dcf
Portfolio symbol Pension_portfolio.PF Monte Carlo distribution t Monte Carlo period 30 Cash flow strategy fixed_percentage discount_rate 0.083798 dtype: object
result = pf.dcf.find_the_largest_withdrawals_size( goal="maintain_balance_pv", # ставим задач - сохранить покупательную способность стартовой суммы денег percentile=25, # денег должно хватить в умеренно пессимистичном сценарии (25й перцентиль) threshold=0.10, # допустимая погрешность 10% )
2026-04-06 12:08:39,389 - INFO - Iteration 0: error_rel=1.000, gradient=0.000 2026-04-06 12:08:44,720 - INFO - Iteration 1: error_rel=1.000, gradient=-0.000 2026-04-06 12:08:49,884 - INFO - Iteration 2: error_rel=0.997, gradient=-0.003 2026-04-06 12:08:54,620 - INFO - Iteration 3: error_rel=0.800, gradient=-0.197 2026-04-06 12:08:59,297 - INFO - Iteration 4: error_rel=0.217, gradient=-0.583 2026-04-06 12:09:03,943 - INFO - Iteration 5: error_rel=0.464, gradient=0.246 2026-04-06 12:09:08,644 - INFO - Iteration 6: error_rel=0.218, gradient=-0.246 2026-04-06 12:09:13,738 - INFO - Iteration 7: error_rel=0.155, gradient=-0.063 2026-04-06 12:09:18,537 - INFO - Iteration 8: error_rel=0.147, gradient=-0.009 2026-04-06 12:09:23,353 - INFO - Iteration 9: error_rel=0.050, gradient=-0.097 2026-04-06 12:09:23,354 - INFO - Solution found: -683.59 or 6.84% after 10 steps.
Ответ показывает, что при заданной инвестиционной стратегии можно снимать вплоть до 6,8% (с последующей индексацией). Это довольно много. Но не стоит забывать, что здесь рассматривается агрессивный портфель с большим содержанием акций. Риски в такой стратегии больше, а размер изъятий будет очень волатильным, что подойдет далеко не любому пенсионеру.
result.withdrawal_rel
0.068359375
Если достаточно сохранить номинальный (а не реальный) баланс, допустимая ставка снятия может быть еще выше:
result = pf.dcf.find_the_largest_withdrawals_size( goal="maintain_balance_fv", percentile=25, threshold=0.10, )
2026-04-06 12:09:28,162 - INFO - Iteration 0: error_rel=1.000, gradient=0.000 2026-04-06 12:09:32,892 - INFO - Iteration 1: error_rel=1.000, gradient=-0.000 2026-04-06 12:09:37,684 - INFO - Iteration 2: error_rel=0.962, gradient=-0.038 2026-04-06 12:09:42,343 - INFO - Iteration 3: error_rel=1.315, gradient=0.353 2026-04-06 12:09:47,052 - INFO - Iteration 4: error_rel=0.665, gradient=-0.651 2026-04-06 12:09:51,779 - INFO - Iteration 5: error_rel=0.059, gradient=-0.606 2026-04-06 12:09:51,781 - INFO - Solution found: -1562.50 or 15.62% after 6 steps.
result.withdrawal_rel
0.15625
4. Продвинутые стратегии моделирования снятия денег
Фиксированные стратегии (будь то фиксированная сумма или фиксированный процент) страдают от одной и той же проблемы: они не адаптируются к рыночным условиям. В реальной жизни пенсионер, увидев, что портфель сильно просел, скорее всего, сократит расходы. И наоборот — после хорошего года позволит себе чуть больше.
Именно эту идею формализуют адаптивные стратегии. В okama реализованы два таких подхода: Vanguard Dynamic Spending (VDS) и Cut Withdrawals If Drawdown (CWD).
4.1. Vanguard Dynamic Spending (VDS)
Оригинальное исследование Vanguard
В 2012 году команда исследователей Vanguard (Colleen Jaconetti, Francis Kinniry, Michael DiJoseph, Zoe Odenwalder) представила работу «From Assets to Income: A Goals-Based Approach to Retirement Spending», а в 2017-м — итоговый документ «A Rule for All Seasons: Vanguard’s Dynamic Approach to Retirement Spending». Идея — объединить два простых подхода (фиксированная сумма с индексацией и фиксированный процент) в гибридную стратегию.
Алгоритм VDS:
Рассчитываем «целевое» снятие как фиксированный процент от текущей стоимости портфеля.
Сравниваем его с предыдущим снятием, скорректированным на инфляцию.
Ограничиваем рост снятия «потолком» (ceiling) — например, не более +5% от прошлогоднего.
Ограничиваем падение «полом» (floor) — например, не более −2.5% от прошлогоднего.
Так размер выплат плавно следует за рынком, но без резких скачков в обе стороны. По данным Vanguard, при потолке 5% и поле −2.5% вероятность исчерпания портфеля за 35 лет снижается до нескольких процентов (по сравнению с ~15% при классическом правиле 4%), а средний разброс годовых выплат остаётся комфортным.
VDS в okama: класс VanguardDynamicSpending
vds = ok.VanguardDynamicSpending( parent=pf, initial_investment=10_000, percentage=-0.12, # 12% от текущего баланса floor_ceiling=(-0.05, 0.10), # пол: −5%, потолок: +10% adjust_floor_ceiling=True, # корректировать пол/потолок на инфляцию indexation="inflation", ) pf.dcf.cashflow_parameters = vds
Параметры floor_ceiling=(-0.05, 0.10) означают:
1) Рассчитанное снятие в текущем году может быть ниже прошлогоднего только на 5% (нижняя граница или "пол").
2) Рассчитанное снятие в текущем году может быть выше прошлогоднего только на 10% (верхняя граница или "потолок").
Флаг adjust_floor_ceiling=True включает корректировку порогов на инфляцию.
Важно: VDS работает только со снятиями (отрицательные значения amount). Разрешена частота снятий только “раз в год”.
Бэктест VDS
На исторических данных график баланса портфеля при стратегии VDS выглядит очень похоже на обычное снятие процента.
pf.dcf.wealth_index(discounting="fv", include_negative_values=False).plot();

Размер снятий по годам демонстрирует отличие от чистой PercentageStrategy . Выплаты меняются значительно плавнее:
cf_vds = pf.dcf.cash_flow_ts(discounting="fv").resample("Y").sum() cf_vds.plot(kind="bar");

Монте-Карло для VDS
pf.dcf.set_mc_parameters( distribution="t", period=60, mc_number=400, ) pf.dcf.plot_forecast_monte_carlo(backtest=False) plt.yscale("log")

s_vds = pf.dcf.monte_carlo_survival_period(threshold=0.05) s_vds.describe([0.05, 0.20, 0.50, 0.70])
count 400.00000 mean 56.46375 std 7.87094 min 8.90000 5% 36.90000 20% 55.00000 50% 60.00000 70% 60.00000 max 60.00000 dtype: float64
Сравнение VDS и PercentageStrategy
Ключевое отличие хорошо видно при визуализации денежных потоков двух случайных сценариев Монте-Карло:
mc_cashflow_vds = pf.dcf.monte_carlo_cash_flow(discounting="pv")
fig, axes = plt.subplots(1, 2, figsize=(15, 5)) mc_cashflow_vds[0].resample("Y").sum().plot(ax=axes[0], kind="bar", title="VDS") mc_cashflow_percentage[0].resample("Y").sum().plot(ax=axes[1], kind="bar", title="Percentage") plt.tight_layout()

В PercentageStrategy выплаты к концу периода могут упасть в десятки раз. В VDS снижение происходит в несколько раз — “пол” ограничивает скорость падения.
Минимальные и максимальные границы снятий
На практике у пенсионера есть минимально необходимый доход (на жизнь) и максимально разумный. VDS позволяет задать эти границы (уже не процентом а номинальной суммой). Установка нижней границы может существенно сократить период выживания портфеля — ведь вы забираете деньги даже в самые плохие годы.
vds.min_max_annual_withdrawals = ( vds.initial_investment * 0.03, # минимум: 3% от начальных вложений vds.initial_investment * 0.15, # максимум: 15% ) vds
Strategy name VDS Portfolio symbol Pension_portfolio.PF Cash flow initial investment 10000 Cash flow frequency year Cash flow strategy VDS Cash flow percentage -0.12 Minimum annual withdrawal 300.0 Maximum annual withdrawal 1500.0 Max and Min withdrawals are indexed True Floor -0.05 Ceiling 0.1 Floor and Ceiling are indexed True Indexation 0.082999 dtype: object
Посмотрим как выглядят 3 профиля снятия денег для VDS, VDS с лимитами и обычный процентный профиль. Для этого сгенерируем случайные сценарии Монте-Карло с теми же параметрами как для других стратегий снятия.
mc_cashflow_vds_limits = pf.dcf.monte_carlo_cash_flow(discounting="pv")
Теперь можно посмотреть на графики трех случайно выбранных сценариев.
fig, axes = plt.subplots(1, 3, figsize=(15, 5)) mc_cashflow_vds[0].resample("Y").sum().plot(ax=axes[0], kind="bar", title="VDS") mc_cashflow_vds_limits[0].resample("Y").sum().plot(ax=axes[1], kind="bar", title="VDS с лимитами") mc_cashflow_percentage[0].resample("Y").sum().plot(ax=axes[2], kind="bar", title="Percentage") plt.tight_layout()

Хорошо заметно, что VDS с лимитами в отличие от других стратегий "упирается" в нижнее ограничение, после чего снижение выплат останавливается.
4.2. Cut Withdrawals If Drawdown (CWD)
Исследование Guyton–Klinger и концепция «guardrails»
В 2004 году финансовый консультант Джонатан Гайтон опубликовал работу «Decision Rules and Portfolio Management for Retirees: Is the “Safe” Initial Withdrawal Rate Too Safe?». В 2006 году совместно с Уильямом Клингером вышла расширенная версия — «Decision Rules and Maximum Initial Withdrawal Rates» в журнале Financial Planning Association.
Ключевая идея — система «ограждений» (guardrails), которая позволяет начать с более высокой ставки снятия (5–6% вместо 4%), но автоматически корректирует её при неблагоприятных рыночных условиях:
Если текущая ставка снятия (сумма / стоимость портфеля) превышает начальную более чем на 20% — сумма снятия сокращается на 10%.
Если ставка падает ниже начальной более чем на 20% — сумму можно увеличить на 10%.
После года с отрицательной доходностью инфляционная корректировка пропускается.
Guyton и Klinger показали, что при этих правилах начальная ставка 5.2–5.6% обеспечивает 99% вероятность «выживания» за 40 лет.
CWD в okama: класс CutWithdrawalsIfDrawdown
Стратегия CWD в okama реализует родственную идею: когда портфель находится в просадке (drawdown), снятия автоматически сокращаются. Многие инвесторы делают это интуитивно — CWD формализует это поведение.
CWD наследует логику IndexationStrategy (фиксированная сумма с индексацией), но добавляет привязку размера снятия к текущей просадке портфеля.
Важно: CWD работает только со снятиями (отрицательные значения amount).
cwd = ok.CutWithdrawalsIfDrawdown( parent=pf, initial_investment=10_000, frequency="year", amount=-1200, # начальное снятие (12%, для сравнения с VDS) indexation="inflation", ) pf.dcf.cashflow_parameters = cwd cwd
Strategy name CWD Portfolio symbol Pension_portfolio.PF Cash flow initial investment 10000 Cash flow frequency year Cash flow strategy CWD Cash flow amount -1200 Cash flow indexation 0.082999 Crash threshold reduction [(0.2, 0.4), (0.5, 1)] dtype: object
Ключевой параметр — crash_threshold_reduction. Это список пар (просадка, сокращение_снятия). Каждая пара определяет: при какой просадке портфеля на сколько уменьшается снятие.
cwd.crash_threshold_reduction = [ (0.05, 0.20), # просадка ≥5% → снятие сокращается на 20% (0.10, 0.40), # просадка ≥10% → сокращение на 40% (0.20, 0.50), # просадка ≥20% → сокращение на 50% (0.30, 1.00), # просадка ≥30% → снятие полностью прекращается ] cwd
Strategy name CWD Portfolio symbol Pension_portfolio.PF Cash flow initial investment 10000 Cash flow frequency year Cash flow strategy CWD Cash flow amount -1200 Cash flow indexation 0.082999 Crash threshold reduction [(0.05, 0.2), (0.1, 0.4), (0.2, 0.5), (0.3, 1.0)] dtype: object
Бэктест CWD
Посмотрим на исторических данных, как вел себя портфель, если начальная ставка изъятий была равна 12%.
pf.dcf.discount_rate = None # ставка дисконтирована равна средней инфляции
pf.dcf.wealth_index(discounting="pv", include_negative_values=False).plot();

При ставке 12% портфель «выживает» на исторических данных. Более того его реальная стоимость увеличивается (в wealth_index выбран флаг discounting=“pv”).
Поддержать баланс портфеля помогло сокращение снятий в кризисные периоды. Посмотрим на связь просадок и размера снятий:
fig, axes = plt.subplots(2, 1, figsize=(15, 5), sharex=True) pf.drawdowns.plot(ax=axes[0], legend=True, label="Drawdowns") cf_cwd = pf.dcf.cash_flow_ts(discounting="fv").resample("Y").sum() cf_cwd = cf_cwd if isinstance(cf_cwd, pd.Series) else cf_cwd.sum(axis=1) cf_cwd.plot(ax=axes[1], legend="Withdrawals");

12% — заведомо агрессивная ставка, далеко за пределами общепринятых 4%. В отличие от VDS, в CWD снятия растут каждый год (происходит индексация на инфляцию), если только портфель не в просадке. Поэтому подбор разумного размера снятия здесь критически важен.
Монте-Карло для CWD
На исторических данных стратегия хорошо прошла проверку. Проверим теперь на большем количестве сценариев в Монте-Карло:
pf.dcf.set_mc_parameters( distribution="t", period=60, mc_number=400, ) pf.dcf.plot_forecast_monte_carlo(backtest=False) plt.yscale("log")

На графике хорошо видно, что при ставке изъятий 12% было много случайных сценариев, которые обнулили портфель раньше срока. Посмотрим статистику периодов дожития.
s_cwd = pf.dcf.monte_carlo_survival_period(threshold=0.05) s_cwd.describe([0.05, 0.25, 0.50, 0.70])
count 400.000000 mean 33.101750 std 20.192287 min 7.900000 5% 10.850000 25% 14.900000 50% 23.900000 70% 60.000000 max 60.000000 dtype: float64
Размер снятия необходимо уменьшить, чтобы нужный период выживания был достигнут даже при 5-м процентиле. Определим его с помощью find_the_largest_withdrawals_size:
result = pf.dcf.find_the_largest_withdrawals_size( goal="survival_period", target_survival_period=30, percentile=5, )
2026-04-06 12:11:21,545 - INFO - Iteration 0: error_rel=0.937, gradient=0.000 2026-04-06 12:11:32,282 - INFO - Iteration 1: error_rel=0.903, gradient=-0.033 2026-04-06 12:11:43,909 - INFO - Iteration 2: error_rel=0.837, gradient=-0.067 2026-04-06 12:11:53,808 - INFO - Iteration 3: error_rel=0.670, gradient=-0.167 2026-04-06 12:12:03,858 - INFO - Iteration 4: error_rel=0.005, gradient=-0.665 2026-04-06 12:12:03,859 - INFO - Solution found: -625.00 or 6.25% after 5 steps.
result.withdrawal_abs # рекомендуемая сумма снятия
-625.0
result.withdrawal_rel # в долях от начальных инвестиций
0.0625
Для практически гарантированного выживания портфеля (даже в пессимистичном сценарии) размер снятия пришлось сократить почти вдвое. Но благодаря ограничению снятий во время просадок, CWD позволяет добиться более высокой безопасной ставки изъятия по сравнению с обычной IndexationStrategy.
CWD с полным запретом снятий во время просадки
Возможности CWD хорошо демонстрируются, если снятия полностью остановить во время просадки. Это «экстремальный» сценарий, но он применим на практике — например, когда портфельные снятия не являются основным источником дохода, но лишь дополняют пенсию или другие поступления.
cwd.crash_threshold_reduction = [(0.01, 1.0)] # просадка ≥1% → снятие = 0 cwd
Strategy name CWD Portfolio symbol Pension_portfolio.PF Cash flow initial investment 10000 Cash flow frequency year Cash flow strategy CWD Cash flow amount -1200 Cash flow indexation 0.082999 Crash threshold reduction [(0.01, 1.0)] dtype: object
pf.dcf.wealth_index(discounting="pv", include_negative_values=False).plot();

Портфель тоже выживает весь исторический период при ставке 12%, и его реальный размер заметно вырос. Но инвестору пришлось бы чаще отказываться от выплат:
fig, axes = plt.subplots(2, 1, figsize=(15, 5), sharex=True) pf.drawdowns.plot(ax=axes[0], legend=True, label="Drawdowns") cf_cwd = pf.dcf.cash_flow_ts(discounting="fv").resample("Y").sum() cf_cwd.plot(ax=axes[1], legend="Withdrawals");

На графике виден характерный «пилообразный» паттерн. Агрессивный портфель часто уходит в просадку, во время которой снятия прекращаются. Магии здесь нет — средний размер снятия остаётся примерно таким же, как и в сценариях с более мягкими условиями. Это просто другой способ перераспределить снятия во времени. Но такой "экстремальный" вариант стратегии CWD может быть полезен, если необходимо оставить наследство, а сами снятия вторичны.
5. Полезные ссылки
GitHub: github.com/mbk-dev/okama — исходный код, issues, discussions
Документация: okama.readthedocs.io — полная документация по всем классам и методам
Примеры (Jupyter Notebooks): github.com/mbk-dev/okama/tree/master/examples — от базовых до продвинутых сценариев, включая ноутбук по DCF-стратегиям
Русскоязычный форум: community.okama.io — обсуждение библиотеки, стратегий и багов на русском языке
Интерактивные виджеты: okama.io — веб-приложение с финансовыми инструментами на базе okama
PyPI: pypi.org/project/okama — установка через
pip install okama
Публикации на Хабре
Open Source в финансах. Проект Okama — обзор библиотеки и основных возможностей
Дивидендная доходность Индекса Мосбиржи: как рассчитать за 5 минут с помощью Python — практический пример работы с данными
Ребалансировка инвестиционного портфеля с помощью Python и библиотеки okama — стратегии ребалансировки
Библиотека Python для доступа к данным ЦБ: cbrapi — смежный проект для работы с данными ЦБ РФ
Okama — это open-source проект, и вклад приветствуется. Если вы нашли ошибку, хотите предложить функцию или просто задать вопрос — используйте GitHub Discussions или русскоязычный форум.
