feat(financial/dcf): dormant separate-index inflation capability (default OFF) #1906
2 changed files with 281 additions and 14 deletions
|
|
@ -303,6 +303,27 @@ _SALES_DURATION_MAX: int = 120
|
|||
# Сохранён константой для будущей рассрочки (PR-4).
|
||||
_FIRST_PAYMENT_PCT: float = 0.80
|
||||
|
||||
# ── DCF: РАЗДЕЛЬНАЯ ИНФЛЯЦИЯ ИНДЕКСОВ — DORMANT CAPABILITY (default OFF) ──────────
|
||||
# Excel-эталон применяет РАЗНУЮ годовую инфляцию к затратам СМР (индекс СМР Росстата
|
||||
# ~7-8%/год) и к цене продажи (индекс цен ~6%/год) на многолетнем горизонте. Эта
|
||||
# возможность ПРОВОДНА в `_build_monthly_cashflow` (см. `smr_inflation_annual` /
|
||||
# `sale_inflation_annual`), но ДЕФОЛТ строго 0.0 — прод-поведение НЕ меняется.
|
||||
#
|
||||
# ПОЧЕМУ OFF ПО УМОЛЧАНИЮ (hard constraint):
|
||||
# * Статический БДР (`compute_financial`) считается в деньгах СЕГОДНЯ (today-money);
|
||||
# есть ТЕСТИРУЕМЫЙ инвариант `Σ undiscounted cashflow == net_profit`.
|
||||
# * Раздельная инфляция делает НОМИНАЛЬНЫЕ помесячные потоки РАСХОДЯЩИМИСЯ с
|
||||
# today-money `net_profit` → инвариант ломается при ставке > 0.
|
||||
# * Поэтому включение требует (а) КАЛИБРОВАННЫХ индексов (СМР ~7-8%/год, цена
|
||||
# ~6%/год — ориентиры Росстата) И (б) переосмысления headline-инварианта
|
||||
# (номинальный DCF vs today-money БДР — это РАЗНЫЕ метрики). До этого решения
|
||||
# ставки держим 0.0: capability разведена и протестирована, но выключена.
|
||||
#
|
||||
# При 0.0 месячный фактор `(1+0)**t == 1.0` для любого t → побайтово идентичный
|
||||
# cashflow → существующий инвариант и все текущие тесты проходят без изменений.
|
||||
_SMR_INFLATION_ANNUAL: float = 0.0
|
||||
_SALE_INFLATION_ANNUAL: float = 0.0
|
||||
|
||||
|
||||
# ── DCF helpers — hand-rolled, чистая математика (без numpy-financial / deps) ───
|
||||
|
||||
|
|
@ -495,6 +516,8 @@ def _build_monthly_cashflow(
|
|||
construction_duration_months: int,
|
||||
residential_area_sqm: float,
|
||||
market_velocity_sqm_per_month: float | None = None,
|
||||
smr_inflation_annual: float = 0.0,
|
||||
sale_inflation_annual: float = 0.0,
|
||||
) -> _CashflowResult:
|
||||
"""Разложить статический каскад в помесячный чистый cashflow по графику фаз.
|
||||
|
||||
|
|
@ -503,6 +526,26 @@ def _build_monthly_cashflow(
|
|||
по окну распродажи; маркетинг прорейтится на график продаж; НДС+налог на прибыль
|
||||
прорейтятся на признание выручки. Выручка — полностью в месяц продажи (упрощение).
|
||||
|
||||
РАЗДЕЛЬНАЯ ИНФЛЯЦИЯ ИНДЕКСОВ — DORMANT CAPABILITY (default OFF, см. модульные
|
||||
константы `_SMR_INFLATION_ANNUAL` / `_SALE_INFLATION_ANNUAL`):
|
||||
* ``smr_inflation_annual`` — годовая инфляция СМР-стороны затрат (индекс СМР):
|
||||
эскалирует ПИР, СМР+сети+услуги+резерв. ЗЕМЛЯ НЕ эскалируется — это лумп-сумма
|
||||
в мес 0 (платится сегодня, t=0 → фактор `(1+r)**0 == 1.0` и так бы дал нейтрал,
|
||||
но земля исключена ЯВНО: участок покупается по сегодняшней цене, будущий индекс
|
||||
СМР к ней неприменим).
|
||||
* ``sale_inflation_annual`` — годовая инфляция цены продажи (индекс цен): эскалирует
|
||||
приток выручки. Маркетинг/НДС/налог прорейтятся на УЖЕ ПРОЭСКАЛИРОВАННУЮ выручку
|
||||
(доля = эскалированная revenue_cf[m] / Σ эскалированной выручки) → они едут на
|
||||
той же номинальной выручке, что их рождает (консистентно).
|
||||
* Месячный фактор: ``smr_m = (1+smr_inflation_annual)**(1/12) − 1``, аналогично
|
||||
``sale_m``; компонент месяца ``t`` (0-based) множится на ``(1+factor_m)**t``.
|
||||
* ДЕФОЛТ 0.0 ⇒ ``(1+0)**t == 1.0`` для всех t ⇒ ПОБАЙТОВО идентичный cashflow ⇒
|
||||
инвариант ниже и все текущие тесты держатся (safety-гарантия zero-diff).
|
||||
|
||||
ВНИМАНИЕ: при ставке > 0 номинальные потоки РАСХОДЯТСЯ с today-money ``net_profit``
|
||||
→ инвариант ниже верен ТОЛЬКО при нулевой инфляции (today-money режим). Это
|
||||
ожидаемо: номинальный DCF != today-money БДР (см. модульные константы).
|
||||
|
||||
RANK 1 — окно продаж по рыночной абсорбции: если передан конечный
|
||||
``market_velocity_sqm_per_month > 0`` (темп выбытия ОДНОГО типового конкурента
|
||||
района) и ``residential_area_sqm > 0``, базовое окно =
|
||||
|
|
@ -546,33 +589,55 @@ def _build_monthly_cashflow(
|
|||
# Горизонт = максимум всех фаз (+1 — месяц как индекс, длина = последний+1).
|
||||
horizon = max(constr_end, sales_end, 1)
|
||||
|
||||
# ── Месячные факторы инфляции (DORMANT: при ставке 0.0 фактор тождественно 1.0) ─
|
||||
# smr_m / sale_m — месячные эквиваленты годовых ставок. (1+0)**(1/12)−1 == 0.0 →
|
||||
# все факторы (1+0)**t == 1.0 → побайтово идентичный cashflow при дефолте (zero-diff).
|
||||
smr_m = (1.0 + smr_inflation_annual) ** (1.0 / 12.0) - 1.0
|
||||
sale_m = (1.0 + sale_inflation_annual) ** (1.0 / 12.0) - 1.0
|
||||
smr_factor = [(1.0 + smr_m) ** t for t in range(horizon)]
|
||||
sale_factor = [(1.0 + sale_m) ** t for t in range(horizon)]
|
||||
|
||||
# ── Затраты (отток, со знаком −) ───────────────────────────────────────────
|
||||
# ЗЕМЛЯ НЕ эскалируется: лумп-сумма в мес 0 (участок покупается по сегодняшней цене,
|
||||
# индекс СМР к ней неприменим). При t=0 фактор и так == 1.0, но исключаем явно.
|
||||
land_cf = _spread_linear(land, 0, 0, horizon) # земля — единоразово в мес 0
|
||||
pir_cf = _spread_linear(pir, 0, pir_end, horizon) # ПИР линейно [0..pir_end)
|
||||
# ПИР и СМР+сети+услуги+резерв — СМР-сторона: эскалируются индексом СМР по месяцу t.
|
||||
pir_cf = [c * smr_factor[m] for m, c in enumerate(_spread_linear(pir, 0, pir_end, horizon))]
|
||||
# СМР+сети+услуги+резерв линейно по окну строительства [pir_end .. constr_end).
|
||||
constr_total = construction + networks + dev_services + contingency
|
||||
constr_cf = _spread_linear(constr_total, pir_end, construction_duration_months, horizon)
|
||||
constr_cf = [
|
||||
c * smr_factor[m]
|
||||
for m, c in enumerate(
|
||||
_spread_linear(constr_total, pir_end, construction_duration_months, horizon)
|
||||
)
|
||||
]
|
||||
|
||||
# ── Выручка (приток, +) и привязанные к ней маркетинг/налоги ────────────────
|
||||
# Темп продаж пересчитан на ФАКТИЧЕСКОЕ окно [sales_start, sales_end): выручка
|
||||
# линейно по sales_duration (>= _SALES_DURATION_MONTHS, растянуто до ввода).
|
||||
revenue_cf = _spread_linear(revenue, sales_start, sales_duration, horizon)
|
||||
# Маркетинг + НДС + налог на прибыль прорейтятся на признание выручки: доля месяца
|
||||
# = revenue_cf[m] / revenue. Так отток налога идёт за притоком, который его рождает,
|
||||
# и Σ undiscounted cashflow == net_profit (инвариант). При revenue==0 — нули.
|
||||
# Выручка эскалируется индексом цены продажи по месяцу t (sale_factor).
|
||||
revenue_cf = [
|
||||
c * sale_factor[m]
|
||||
for m, c in enumerate(_spread_linear(revenue, sales_start, sales_duration, horizon))
|
||||
]
|
||||
# Маркетинг + НДС + налог на прибыль прорейтятся на признание выручки. Прорейт идёт
|
||||
# на УЖЕ ПРОЭСКАЛИРОВАННУЮ выручку: доля месяца = revenue_cf[m] / total_nominal_revenue
|
||||
# → они едут на той же номинальной выручке, что их рождает (консистентно).
|
||||
# ZERO-DIFF GUARANTEE: при выключенной инфляции продаж знаменатель = сырая ``revenue``
|
||||
# (ровно как прежде, без зависимости от порядка float-суммирования) → формула
|
||||
# ПОБАЙТОВО тождественна прежней → Σ undiscounted cashflow == net_profit (инвариант).
|
||||
# При revenue==0 — нули.
|
||||
tax_marketing_total = marketing + vat + profit_tax
|
||||
if revenue > 0.0:
|
||||
prorate = [tax_marketing_total * (revenue_cf[m] / revenue) for m in range(horizon)]
|
||||
total_nominal_revenue = revenue if sale_inflation_annual == 0.0 else sum(revenue_cf)
|
||||
if total_nominal_revenue > 0.0:
|
||||
prorate = [
|
||||
tax_marketing_total * (revenue_cf[m] / total_nominal_revenue) for m in range(horizon)
|
||||
]
|
||||
else:
|
||||
prorate = [0.0] * horizon
|
||||
|
||||
cashflow = [
|
||||
revenue_cf[m]
|
||||
- land_cf[m]
|
||||
- pir_cf[m]
|
||||
- constr_cf[m]
|
||||
- prorate[m]
|
||||
for m in range(horizon)
|
||||
revenue_cf[m] - land_cf[m] - pir_cf[m] - constr_cf[m] - prorate[m] for m in range(horizon)
|
||||
]
|
||||
return _CashflowResult(
|
||||
cashflow=cashflow,
|
||||
|
|
@ -708,6 +773,11 @@ def compute_financial(
|
|||
construction_duration_months=construction_months,
|
||||
residential_area_sqm=teap.residential_area_sqm,
|
||||
market_velocity_sqm_per_month=market_velocity_sqm_per_month,
|
||||
# DORMANT capability: раздельная инфляция СМР/цены. Дефолт 0.0 → zero-diff
|
||||
# (фактор (1+0)**t == 1.0). Включение требует калибровки индексов И
|
||||
# переосмысления инварианта Σ cashflow == net_profit (см. модульные константы).
|
||||
smr_inflation_annual=_SMR_INFLATION_ANNUAL,
|
||||
sale_inflation_annual=_SALE_INFLATION_ANNUAL,
|
||||
)
|
||||
cashflow = cf_result.cashflow
|
||||
# Годовая ставка → месячная: (1+r_год)**(1/12) − 1.
|
||||
|
|
|
|||
|
|
@ -835,3 +835,200 @@ def test_velocity_invariant_holds_across_window_sizes() -> None:
|
|||
- 40_000_000.0
|
||||
)
|
||||
assert abs(sum(cf) - expected) < 1.0, vel
|
||||
|
||||
|
||||
# ── РАЗДЕЛЬНАЯ ИНФЛЯЦИЯ ИНДЕКСОВ — DORMANT CAPABILITY (default OFF) ───────────────
|
||||
|
||||
|
||||
def _build_inflation(
|
||||
*,
|
||||
smr_inflation_annual: float = 0.0,
|
||||
sale_inflation_annual: float = 0.0,
|
||||
construction_months: int = 36,
|
||||
revenue: float = 1_000_000_000.0,
|
||||
construction: float = 400_000_000.0,
|
||||
pir: float = 10_000_000.0,
|
||||
networks: float = 30_000_000.0,
|
||||
dev_services: float = 6_000_000.0,
|
||||
contingency: float = 13_000_000.0,
|
||||
marketing: float = 70_000_000.0,
|
||||
land: float = 100_000_000.0,
|
||||
vat: float = 50_000_000.0,
|
||||
profit_tax: float = 40_000_000.0,
|
||||
) -> object:
|
||||
"""Прямой вызов _build_monthly_cashflow с явными ставками инфляции (capability)."""
|
||||
return _build_monthly_cashflow(
|
||||
revenue=revenue,
|
||||
construction=construction,
|
||||
pir=pir,
|
||||
networks=networks,
|
||||
dev_services=dev_services,
|
||||
contingency=contingency,
|
||||
marketing=marketing,
|
||||
land=land,
|
||||
vat=vat,
|
||||
profit_tax=profit_tax,
|
||||
construction_duration_months=construction_months,
|
||||
residential_area_sqm=22_000.0,
|
||||
market_velocity_sqm_per_month=None,
|
||||
smr_inflation_annual=smr_inflation_annual,
|
||||
sale_inflation_annual=sale_inflation_annual,
|
||||
)
|
||||
|
||||
|
||||
def test_module_inflation_constants_default_off() -> None:
|
||||
# SAFETY: модульные константы строго 0.0 (capability разведена, но выключена в проде).
|
||||
assert financial._SMR_INFLATION_ANNUAL == 0.0
|
||||
assert financial._SALE_INFLATION_ANNUAL == 0.0
|
||||
|
||||
|
||||
def test_zero_diff_explicit_zero_vs_omitted_byte_identical() -> None:
|
||||
# ZERO-DIFF (критично): дефолт без аргументов vs явные 0.0 → ПОБАЙТОВО идентичный
|
||||
# cashflow. Пин: прод (константы 0.0) не изменился вообще.
|
||||
omitted = _build(residential=22_000.0, velocity=None, construction_months=36)
|
||||
explicit = _build_inflation(
|
||||
smr_inflation_annual=0.0, sale_inflation_annual=0.0, construction_months=36
|
||||
)
|
||||
cf_omitted = omitted.cashflow # type: ignore[attr-defined]
|
||||
cf_explicit = explicit.cashflow # type: ignore[attr-defined]
|
||||
assert cf_explicit == cf_omitted # списки float идентичны элемент-в-элемент
|
||||
|
||||
|
||||
def test_zero_diff_npv_irr_invariant_unchanged_at_default() -> None:
|
||||
# ZERO-DIFF: NPV, IRR и инвариант Σ cashflow == net_profit идентичны при явных 0.0.
|
||||
omitted = _build(residential=22_000.0, velocity=None, construction_months=48)
|
||||
explicit = _build_inflation(
|
||||
sale_inflation_annual=0.0, smr_inflation_annual=0.0, construction_months=48
|
||||
)
|
||||
cf_o = omitted.cashflow # type: ignore[attr-defined]
|
||||
cf_e = explicit.cashflow # type: ignore[attr-defined]
|
||||
monthly_rate = (1.0 + financial._DISCOUNT_RATE_ANNUAL) ** (1.0 / 12.0) - 1.0
|
||||
assert _npv(monthly_rate, cf_e) == _npv(monthly_rate, cf_o)
|
||||
assert _irr_monthly(cf_e) == _irr_monthly(cf_o)
|
||||
# Инвариант (today-money): Σ == revenue − все затраты − vat − tax.
|
||||
expected = (
|
||||
1_000_000_000.0
|
||||
- 400_000_000.0
|
||||
- 10_000_000.0
|
||||
- 30_000_000.0
|
||||
- 6_000_000.0
|
||||
- 13_000_000.0
|
||||
- 70_000_000.0
|
||||
- 100_000_000.0
|
||||
- 50_000_000.0
|
||||
- 40_000_000.0
|
||||
)
|
||||
assert abs(sum(cf_e) - expected) < 1.0
|
||||
|
||||
|
||||
def test_compute_financial_zero_diff_with_module_defaults() -> None:
|
||||
# ZERO-DIFF на уровне compute_financial: пересборка cashflow с теми же ставками,
|
||||
# что использует прод (константы 0.0), даёт инвариант Σ == net_profit (как было).
|
||||
t = _teap(residential=10_000.0, gfa=11_000.0, parking=50)
|
||||
model = financial.compute_financial(
|
||||
teap=t, housing_class="comfort", land_cost_rub=200_000_000.0, development_type="mid_rise"
|
||||
)
|
||||
cf = _rebuild_cashflow(model, construction_months=36, residential_area_sqm=10_000.0)
|
||||
# _rebuild_cashflow не передаёт ставки → дефолт 0.0 → тот же поток, что построил прод.
|
||||
assert abs(sum(cf) - model.net_profit_rub) < 1.0
|
||||
|
||||
|
||||
def test_inflation_escalates_late_cost_by_monthly_compound_factor() -> None:
|
||||
# COMPOUNDING (a): поздний СМР-отток = базовый × (1+smr_m)**t, где
|
||||
# smr_m = (1.08)**(1/12) − 1. Сравниваем месяц t для базового vs инфлированного
|
||||
# потока → отношение == ровно фактор компаундинга месяца t.
|
||||
base = _build_inflation(smr_inflation_annual=0.0, construction_months=36)
|
||||
infl = _build_inflation(smr_inflation_annual=0.08, construction_months=36)
|
||||
cf_base = base.cashflow # type: ignore[attr-defined]
|
||||
cf_infl = infl.cashflow # type: ignore[attr-defined]
|
||||
smr_m = (1.08) ** (1.0 / 12.0) - 1.0
|
||||
# Возьмём месяц ВНУТРИ окна СМР, ДО старта продаж (чистый СМР-отток, без выручки):
|
||||
# pir_end=6, constr_start=6, sales_start=6+6=12. Месяц 10 — чистый СМР+сети+резерв.
|
||||
t = 10
|
||||
# Затраты — отрицательные; отношение по модулю == фактор компаундинга.
|
||||
factor = (1.0 + smr_m) ** t
|
||||
assert cf_infl[t] == pytest.approx(cf_base[t] * factor, rel=1e-9)
|
||||
# Sanity: фактор для t=10 близок к (1.08)**(10/12).
|
||||
assert factor == pytest.approx(1.08 ** (10.0 / 12.0), rel=1e-9)
|
||||
|
||||
|
||||
def test_inflation_revenue_escalates_by_sale_factor() -> None:
|
||||
# COMPOUNDING: приток выручки месяца t = базовый × (1+sale_m)**t. Берём проект без
|
||||
# затрат в окне продаж, чтобы изолировать выручку: месяц с чистым притоком.
|
||||
base = _build_inflation(sale_inflation_annual=0.0, construction_months=24)
|
||||
infl = _build_inflation(sale_inflation_annual=0.06, construction_months=24)
|
||||
cf_base = base.cashflow # type: ignore[attr-defined]
|
||||
cf_infl = infl.cashflow # type: ignore[attr-defined]
|
||||
# Последний месяц горизонта — чистый приток (СМР завершён, продажи идут): на нём
|
||||
# выручка эскалирована сильнее всего. Проверяем направление: инфл-приток > базового.
|
||||
last = len(cf_base) - 1
|
||||
assert cf_infl[last] > cf_base[last]
|
||||
|
||||
|
||||
def test_nominal_cashflow_sum_diverges_from_net_profit_when_on() -> None:
|
||||
# COMPOUNDING (b): при ставках > 0 Σ НОМИНАЛЬНОГО потока != today-money сумме.
|
||||
# Это ОЖИДАЕМО (номинальный DCF vs today-money БДР — разные метрики), а не баг.
|
||||
today_money_sum = (
|
||||
1_000_000_000.0
|
||||
- 400_000_000.0
|
||||
- 10_000_000.0
|
||||
- 30_000_000.0
|
||||
- 6_000_000.0
|
||||
- 13_000_000.0
|
||||
- 70_000_000.0
|
||||
- 100_000_000.0
|
||||
- 50_000_000.0
|
||||
- 40_000_000.0
|
||||
)
|
||||
infl = _build_inflation(smr_inflation_annual=0.08, sale_inflation_annual=0.06)
|
||||
cf = infl.cashflow # type: ignore[attr-defined]
|
||||
# Номинальная сумма ОТЛИЧАЕТСЯ от today-money (инвариант намеренно не держится on).
|
||||
assert abs(sum(cf) - today_money_sum) > 1.0
|
||||
|
||||
|
||||
def test_npv_lower_when_cost_inflation_applied_leverage_on_cost() -> None:
|
||||
# COMPOUNDING (c): ИЗОЛИРОВАННЫЙ эффект СМР-инфляции — leverage на затраты.
|
||||
# СМР-инфляция эскалирует ОТТОКИ (затраты), не трогая притоки → NPV строго НИЖЕ
|
||||
# нулевого сценария. Это однозначное направление «leverage on cost»: затраты
|
||||
# дорожают, выручка нет → ценность падает. Проверяем НАПРАВЛЕНИЕ, не магич. число.
|
||||
#
|
||||
# ПРИМЕЧАНИЕ (почему изолируем СМР, а не сравниваем smr8/sale6 vs zero): на этом
|
||||
# фикстуре выручка (1.0B) много больше затрат, поэтому sale-инфляция 6% на крупном
|
||||
# back-loaded притоке ПЕРЕВЕШИВАЕТ cost-инфляцию 8% — комбинированный NPV выходит ВЫШЕ
|
||||
# нулевого. Это корректно (направление комбинации зависит от отношения выручка/затраты);
|
||||
# «leverage on cost» — про ИЗОЛИРОВАННЫЙ cost-канал, который монотонно режет NPV.
|
||||
monthly_rate = (1.0 + financial._DISCOUNT_RATE_ANNUAL) ** (1.0 / 12.0) - 1.0
|
||||
zero = _build_inflation(smr_inflation_annual=0.0, sale_inflation_annual=0.0)
|
||||
cost_only = _build_inflation(smr_inflation_annual=0.08, sale_inflation_annual=0.0)
|
||||
npv_zero = _npv(monthly_rate, zero.cashflow) # type: ignore[attr-defined]
|
||||
npv_cost = _npv(monthly_rate, cost_only.cashflow) # type: ignore[attr-defined]
|
||||
assert npv_cost < npv_zero
|
||||
# Зеркало: ИЗОЛИРОВАННАЯ sale-инфляция эскалирует притоки → NPV ВЫШЕ нулевого.
|
||||
sale_only = _build_inflation(smr_inflation_annual=0.0, sale_inflation_annual=0.06)
|
||||
npv_sale = _npv(monthly_rate, sale_only.cashflow) # type: ignore[attr-defined]
|
||||
assert npv_sale > npv_zero
|
||||
|
||||
|
||||
def test_npv_monotonic_decreasing_in_smr_inflation() -> None:
|
||||
# MONOTONICITY: выше СМР-инфляция → ниже (или равный) NPV, всё прочее равно
|
||||
# (sale_inflation фиксирована 0). Проверяем по сетке ставок.
|
||||
monthly_rate = (1.0 + financial._DISCOUNT_RATE_ANNUAL) ** (1.0 / 12.0) - 1.0
|
||||
rates = [0.0, 0.02, 0.05, 0.08, 0.12]
|
||||
npvs = [
|
||||
_npv(monthly_rate, _build_inflation(smr_inflation_annual=r).cashflow) # type: ignore[attr-defined]
|
||||
for r in rates
|
||||
]
|
||||
for prev, nxt in pairwise(npvs):
|
||||
assert nxt <= prev # NPV не растёт при росте СМР-инфляции
|
||||
|
||||
|
||||
def test_land_not_escalated_by_smr_inflation() -> None:
|
||||
# ЗЕМЛЯ НЕ эскалируется: лумп в мес 0. Меняем ТОЛЬКО землю при той же СМР-инфляции —
|
||||
# cashflow[0] двигается ровно на дельту земли (без множителя инфляции).
|
||||
a = _build_inflation(smr_inflation_annual=0.08, land=100_000_000.0)
|
||||
b = _build_inflation(smr_inflation_annual=0.08, land=200_000_000.0)
|
||||
cf_a = a.cashflow # type: ignore[attr-defined]
|
||||
cf_b = b.cashflow # type: ignore[attr-defined]
|
||||
# Месяц 0 — только земля (ПИР тоже в окне [0..6), но он одинаков в обоих вызовах).
|
||||
# Разница cashflow[0] == разница земли × 1.0 (фактор t=0 == 1.0, земля не эскалирует).
|
||||
assert cf_a[0] - cf_b[0] == pytest.approx(200_000_000.0 - 100_000_000.0, rel=1e-9)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue