惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
雷峰网
雷峰网
V
V2EX
博客园_首页
Engineering at Meta
Engineering at Meta
博客园 - 聂微东
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Apple Machine Learning Research
Apple Machine Learning Research
GbyAI
GbyAI
H
Help Net Security
A
About on SuperTechFans
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Blog — PlanetScale
Blog — PlanetScale
W
WeLiveSecurity
云风的 BLOG
云风的 BLOG
D
Docker
Security Archives - TechRepublic
Security Archives - TechRepublic
Help Net Security
Help Net Security
N
News and Events Feed by Topic
Simon Willison's Weblog
Simon Willison's Weblog
G
Google Developers Blog
A
Arctic Wolf
T
The Blog of Author Tim Ferriss
博客园 - 叶小钗
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Google DeepMind News
Google DeepMind News
博客园 - 三生石上(FineUI控件)
aimingoo的专栏
aimingoo的专栏
Hacker News: Ask HN
Hacker News: Ask HN
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 司徒正美
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
P
Privacy International News Feed
T
Troy Hunt's Blog
T
Tenable Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Recorded Future
Recorded Future
F
Fortinet All Blogs
D
DataBreaches.Net
B
Blog
T
Threat Research - Cisco Blogs
MyScale Blog
MyScale Blog
Hacker News - Newest:
Hacker News - Newest: "LLM"
The GitHub Blog
The GitHub Blog
Security Latest
Security Latest
M
MIT News - Artificial intelligence

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как мы считаем досрочное погашение кредита: что реально работает в коде
isergeymd · 2026-04-24 · via Все публикации подряд на Хабре

Как мы считаем досрочное погашение кредита: что реально работает в коде

Уровень сложностиСредний

Время на прочтение8 мин

Охват и читатели20

Кейс

Когда я начал делать кредитный трекер, казалось, что финансовая математика — самая простая часть проекта. Формула аннуитета есть в любом учебнике, Excel справляется за пять минут.

Я ошибался.

Небольшой контекст: до этого я довольно долго не делал ничего для Android — работал в других областях, экосистема успела заметно измениться. Вернуться оказалось неожиданно приятно: Compose после нескольких лет XML-вёрстки ощущается как глоток свежего воздуха, KSP вместо KAPT работает заметно быстрее, а Room с Flow и корутинами — это уже совсем другой уровень удобства по сравнению с тем, что я помнил. Так что статья отчасти и про это: как выглядит возвращение в Android-разработку после перерыва.Плюс технический разбор того, как на самом деле устроен кредитный калькулятор внутри Android-приложения. С реальным кодом, реальными компромиссами и честным признанием того, что мы намеренно упростили.

Архитектура: откуда берутся данные

Стек: Kotlin 2.1, Jetpack Compose, Room, Coroutines. Никакого Hilt и Dagger — зависимости собираются вручную в главном Application-классе.

Данные текут так:

Room Flow → Repository → ViewModel (StateFlow + combine) → Composable (collectAsStateWithLifecycle)

Все stateIn вызовы используют SharingStarted.Eagerly — поток стартует при создании ViewModel, не при первом подписчике.  Это убирает большинство «миганий» пустого состояния при навигации, хотя в холодном старте база всё равно иногда берет своё

Room + Flow: реактивность «из коробки»

Раньше база данных была вещью в себе: нужно было вручную обновлять списки после каждого изменения или городить сложные механизмы с ContentObserver. Сейчас Room в связке с Flow делает всё это за тебя.

База данных (Room) отдает поток долгов, мы подмешиваем к нему выбранную стратегию из настроек, и на выходе получаем готовый uiState. Всё это реактивно: изменился долг в базе или пользователь переключил "Лавину" на "Снежный ком" — UI обновится мгновенно и автоматически.

Базовая формула аннуитета

Стандартная формула ежемесячного платежа:

A = P × r / (1 - (1 + r) ^-n)

где P — остаток долга, r — месячная ставка (годовая / 12), n — оставшийся срок в месяцах.

В коде это выглядит так (CalcLoanUseCase.kt):

val r = annualRate / 100.0 / 12.0
val annuityPayment = if (r > 0)
    balance * r / (1 - (1 + r).pow(-termMonths.toDouble()))
else
    balance / termMonths

Та же формула используется в CalcPayoffUseCase для автоматического расчёта minPayment, если пользователь его не указал.

Симуляция погашения: итерации, а не формула

Для стратегий лавина и снежный ком мы не ищем аналитическое решение — симулируем помесячно:

// CalcPayoffUseCase.kt
while (balances.any { it > 0.01 }) {
    var extra = extraBudget
    months++

    sorted.forEachIndexed { i, debt ->
        if (balances[i] <= 0.01) return@forEachIndexed

        val monthlyInterest = balances[i] * (debt.interestRate / 100.0 / 12.0)
        totalInterestPaid += monthlyInterest
        balances[i] += monthlyInterest

        val payment = when (debt.paymentType) {
            PaymentType.DIFFERENTIAL -> monthlyPrincipal[i] + monthlyInterest
            else -> effectiveMinPayment[i]
        }

        // Весь свободный бюджет идёт первому активному долгу
        val firstActive = balances.indexOfFirst { it > 0.01 }
        var totalPayment = payment
        if (firstActive == i && extra > 0) {
            totalPayment += extra
            extra = 0.0
        }

        totalPayment = minOf(totalPayment, balances[i])
        balances[i] -= totalPayment
    }
    if (months > 600) break
}

Лавина — sortedByDescending { it.interestRate }, математически оптимальна.
Снежный ком — sortedBy { it.currentBalance }, психологически мотивирует.

Скрытые комиссии: три вида

Многие калькуляторы считают только проценты. Мы добавили три дополнительных поля в сущность долга:

// Debt.kt
val originationFeePercent: Double = 0.0   // единоразовая комиссия за выдачу, % от суммы
val monthlyServiceFee: Double = 0.0        // ежемесячные комиссии: обслуживание, СМС и т.д., ₽
val annualInsurancePercent: Double = 0.0   // страховка, % от остатка в год

В симуляции они накапливаются отдельно от процентов:

// Комиссия за выдачу — единоразово, считается от оригинальной суммы
var totalFeesPaid = sorted.sumOf { it.originalAmount * it.originationFeePercent / 100.0 }

// Внутри цикла каждый месяц:
totalFeesPaid += debt.monthlyServiceFee
totalFeesPaid += balances[i] * (debt.annualInsurancePercent / 100.0 / 12.0)

Результат: два числа переплаты — totalInterestPaid (по договору) и totalRealOverpayment (реальная, включая все скрытые расходы). Разница иногда достигает сотен тысяч рублей.

График предстоящих платежей: пересчёт от текущего баланса

Предстоящие платежи строятся через CalcLoanUseCase — отдельный класс с полным помесячным графиком:

// CalcLoanUseCase.kt — строит список PaymentRow с реальными датами
val cal = startCalendar.clone() as Calendar
val payDay = firstPaymentDay.coerceIn(1, 28)
if (cal.get(Calendar.DAY_OF_MONTH) >= payDay) cal.add(Calendar.MONTH, 1)
cal.set(Calendar.DAY_OF_MONTH, payDay)

for (i in 1..termMonths) {
    val interest = rem * r
    val principal = (annuityPayment - interest).coerceAtLeast(0.0)
    rows.add(PaymentRow(i, sdf.format(cal.time), annuityPayment, interest, principal, rem))
    cal.add(Calendar.MONTH, 1)
    rem = (rem - principal).coerceAtLeast(0.0)
}

Когда пользователь вносит платёж — currentBalance обновляется в Room. Room эмитит новый Flow → ViewModel пересчитывает → projectMonthlyUpcoming строит новый график от актуального остатка. Никакого ручного «обновления» — реактивность через Room.

Честное ограничение: проценты считаются как баланс × ставка / 12, без учёта реального количества дней в месяце. Разница с банковским расчётом «по дням» — копейки на обычных суммах, заметна только на крупной ипотеке за много лет.

Досрочное погашение: два режима

На экране «Стратегия» есть калькулятор снежинки — разового досрочного платежа. Два режима:

Уменьшить срок — перезапускаем полную симуляцию с уменьшенным балансом и смотрим разницу в месяцах и переплате:

// StrategyViewModel.kt
val modifiedDebts = currentDebts.toMutableList().also { list ->
    val idx = list.indexOf(targetDebt)
    list[idx] = targetDebt.copy(currentBalance = newBalance)
}
val withSf = calcUseCase.calcAvalanche(modifiedDebts, extraBudget)
val monthsSaved = withoutSf.totalMonths - withSf.totalMonths
val interestSaved = withoutSf.totalInterestPaid - withSf.totalInterestPaid

Уменьшить платёж — считаем новый аннуитетный платёж по формуле для уменьшенного баланса, показываем разницу в ₽/мес:

val oldPayment = balance * r / (1 - (1 + r).pow(-term))
val newPayment = newBalance * r / (1 - (1 + r).pow(-term))
val saved = (oldPayment - newPayment).coerceAtLeast(0.0)

Целевой долг — тот, у которого наибольшая процентная ставка (логика лавины).

Ключевая ставка и инфляция: два потока данных с ЦБ РФ

Ключевая ставка — SOAP раз в 7 дней

Ставка подтягивается через WorkManager:

// KeyRateRepository.kt
val soap = """<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://web.cbr.ru/">
  <soap:Body>
    <web:KeyRateXML>
      <web:fromDate>$weekAgo</web:fromDate>
      <web:ToDate>$today</web:ToDate>
    </web:KeyRateXML>
  </soap:Body>
</soap:Envelope>"""

Используется для двух целей:

  1. Анализ: вклад vs досрочка — если ставка долга ниже ключевой, свободные деньги могут быть эффективнее на депозите

  2. Сигнал рефинансирования — если ставка долга превышает ключевую более чем на 5%, показываем рекомендацию проверить рыночные предложения

val refinanceThreshold = depositRate + 5.0
val refinanceDebts = debts.filter { debt ->
    debt.paymentType != PaymentType.CREDIT_CARD &&
    (debt.termMonths ?: 0) > 12 &&
    debt.interestRate > refinanceThreshold
}

Инфляция — раз в 30 дней

ЦБ РФ публикует данные об инфляции г/г на своём сайте. Подтягиваем через WorkManager раз в 30 дней, кэшируем в SharedPreferences. Fallback — 6%, если данные недоступны.

Имея обе цифры, считаем реальную стоимость долга и реальную доходность вклада:

val realDebt    = item.rate - inflationRate   // реальная стоимость долга
val realDeposit = depositRate - inflationRate // реальная доходность вклада

Это позволяет показать пользователю не просто «ставка 9% ниже ключевой 15%», а конкретный вывод: вклад приносит 9.1%/год реальной доходности, долг обходится 3.1%/год — и объяснить, что с этим делать.

Дата обновления обоих показателей хранится в SharedPreferences и отображается в тултипе на экране стратегии.

Уведомления: два механизма для двух задач

В приложении два вида фоновых уведомлений с принципиально разной природой.

Напоминание о платеже — AlarmManager

Пользователь выбирает за сколько дней и в какое время получать напоминание. Здесь важна точность: уведомление в 9:00 должно прийти в 9:00, а не в 9:23.

WorkManager для этого не подходит — он может задержать задачу на 15+ минут из-за батчинга и Doze mode. Используем AlarmManager.setExactAndAllowWhileIdle:

am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, target, pendingIntent)

На Android 12+ для точных будильников нужно разрешение SCHEDULE_EXACT_ALARM. Если пользователь его не выдал — graceful fallback на setAndAllowWhileIdle с точностью ~15 минут, что для напоминания о платеже вполне приемлемо.

После срабатывания BroadcastReceiver сам перепланирует себя на следующий день — будильник одноразовый, цепочка самоподдерживающаяся. При перезагрузке телефона BootReceiver восстанавливает расписание.

Авто-списание — WorkManager

Авто-списание — это запись планового платежа в базу в заданное время каждый месяц. Здесь точность до минуты не нужна, зато нужна гарантия выполнения. WorkManager подходит идеально — перезагрузка и Doze не мешают, задача всё равно выполнится.

Но есть нюанс: WorkManager может запустить два экземпляра воркера одновременно — плановый тик и повторный запуск после перезагрузки. Без защиты один платёж запишется дважды.

Решение — оптимистичная блокировка прямо в SQL. Вместо SELECT → проверка → UPDATE делаем атомарный UPDATE WHERE next_payment_date = :expectedDate и смотрим на количество затронутых строк:

val claimed = debtDao.claimPayment(
    id           = debt.id,
    expectedDate = debt.nextPaymentDate,
    newBalance   = newBalance,
    newDate      = nextDate
)
if (claimed == 0) continue  // параллельный воркер уже обработал

Если второй воркер добрался до этого долга раньше — дата уже изменилась, UPDATE не затронет ни одной строки и вернёт 0. Никаких транзакций, никаких мьютексов — база данных сама гарантирует атомарность.

Double вместо BigDecimal: осознанный выбор

Деньги хранятся в Double. Классический совет — использовать BigDecimal, и он правильный для банковских систем. Но для трекера долгов:

  • Double даёт ~15 значимых цифр. Для 10 000 000 ₽ точность до копейки — без проблем.

  • Ошибки накопления за всё время симуляции дают отклонение в доли копейки.

  • Room не поддерживает BigDecimal нативно — нужен TypeConverter для каждого поля.

  • Симуляция с BigDecimal заметно медленнее.

Настоящая проблема точности была в другом месте: Animatable в Compose работает с Float (7 значимых цифр). При анимации крупных сумм последние цифры «прыгали». Решение — анимировать целые рубли через toLong(), а не исходный Double.

Что намеренно упрощено

Честно о том, чего нет:

Возможность

Статус

Учёт реальных дней в месяце при расчёте %

Нет, всегда /12

Несколько досрочных платежей с накоплением

Нет, только разовая «снежинка»

Плавающая ставка (ипотека с ЦБ+%)

Нет

Учёт инфляции во времени

Частично — показываем реальную стоимость долга (ставка − инфляция), но симуляция погашения в номинальных рублях

Для личного трекера, где главная цель — мотивировать пользователя гасить долги — этого достаточно. Точность «до копейки» здесь не нужна: нужна честная оценка и понятный интерфейс.

Тестирование

Юнит-тесты алгоритма и мой фаворит — Compose UI-тесты. Вместо того чтобы прокликивать всё приложение, я просто рендерю Composable-функцию через createComposeRule. Это позволяет проверить реакцию UI на специфические данные (например, огромные суммы или пустые списки) за считанные секунды в полной изоляции от Activity и навигации

Итог

Кредитный калькулятор, который делает больше чем одну формулу — это:

  • Итеративная симуляция погашения по месяцам

  • Два вида переплаты: по договору и реальная (со скрытыми комиссиями)

  • Реактивный пересчёт предстоящих платежей от текущего баланса через Room Flow

  • Два режима досрочного погашения с разной математикой

  • Два потока данных с ЦБ РФ: ключевая ставка (SOAP, 7 дней) и инфляция ( 30 дней)

  • Анализ реальной стоимости долга с учётом инфляции: вклад vs досрочка, сигнал рефинансирования

  • Осознанные компромиссы по точности в пользу производительности и простоты

Главное открытие: этого проекта лично для меня: за годы моего отсутствия Android-разработка стала не только быстрее, но и банально человечнее. Там, где раньше приходилось воевать с XML-разметкой и жизненным циклом фрагментов, теперь можно просто описывать логику и наслаждаться результатом

Приложение Гасим доступно в RuStore бесплатно.