Кредитный калькулятор
В этой статье рассматривается программная реализация универсального кредитного калькулятора. Основное внимание уделяется внутреннему устройству модуля, ключевым алгоритмам, структурам данных и подходам, обеспечивающим точность, гибкость и удобство интеграции с интерфесом масок кодовых ячеек.
1. Общая архитектура
Модуль построен вокруг центральной функции CreditCalculator, которая выступает фасадом для двух независимых алгоритмов расчёта: аннуитетного и дифференцированного. Все вспомогательные функции инкапсулированы, а возвращаемые данные стандартизированы в виде именованных кортежей (NamedTuple) и таблиц DataFrame из пакета DataFrames.jl.
Основные компоненты:
- CreditCalculator – диспетчер, выполняющий валидацию входных параметров и выбор метода.
- calculate_annuity / calculate_differentiated – чистые функции, реализующие финансовые формулы.
- compare_payment_types – сервисная функция для сравнения двух схем.
- print_payment_schedule – функция вывода (side‑effect) для форматированного отображения.
Такой дизайн обеспечивает разделение ответственности: вычисления отделены от ввода/вывода, а возвращаемые структуры могут быть использованы для последующего анализа или визуализации.
include("lib.jl")
2. Входные параметры и валидация
Функция CreditCalculator использует именованные аргументы с типизацией, что повышает читаемость кода и позволяет задавать значения в любом порядке:
function CreditCalculator(; amount::Float64, rate::Float64, term::Int,
payment_type::String="annuity",
start_date::Date=today(),
loan_type::String="consumer")
Валидация выполняется на входе:
- Проверка, что сумма, ставка и срок положительны.
- Проверка, что
payment_typeпринадлежит множеству{"annuity", "differentiated"}.
Это предотвращает некорректные вычисления на раннем этапе и даёт понятные сообщения об ошибках.
3. Алгоритмы расчёта
3.1. Аннуитетные платежи
Аннуитетный коэффициент вычисляется по классической формуле:
где r = ставка / (100 * n) – месячная процентная ставка, n – срок в месяцах.
Особенности реализации:
- Ежемесячный платёж рассчитывается как
amount * coef. - Для каждого месяца вычисляются проценты на остаток и сумма погашения основного долга.
- Коррекция последнего платежа: из‑за округлений может накопиться погрешность, поэтому на последнем шаге сумма основного долга принудительно устанавливается равной остатку, а платёж пересчитывается. Это гарантирует, что итоговый остаток станет равным нулю.
- Все суммы округляются до двух знаков после запятой с помощью
round(..., digits=2). - Данные накапливаются в
DataFrameпутём последовательных вызововpush!.
3.2. Дифференцированные платежи
Здесь ежемесячная часть основного долга фиксирована:
Проценты за месяц рассчитываются от текущего остатка. Платёж уменьшается линейно.
Особенности:
- В отличие от аннуитета, не требуется корректировка последнего платежа, так как остаток обнуляется ровно после последней выплаты (при условии точного деления суммы на целое число месяцев).
- Для удобства сравнения сохраняются первый и последний платежи.
- При построении таблицы
principal_partокругляется только при выводе, но в расчётах используется полная точность, чтобы избежать накопления ошибок.
4. Структуры возвращаемых данных
Оба варианта возвращают NamedTuple с одинаковыми полями, что позволяет писать обобщённый код:
(loan_type = ..., payment_type = ..., amount = ..., rate = ..., term = ...,
total_payment = ..., total_interest = ..., overpayment_percent = ...,
payments_table = DataFrame(...))
Для аннуитета дополнительно есть поле monthly_payment, для дифференцированного – first_payment и last_payment. Такая структура удобна для последующей обработки.
Таблица платежей payments_table содержит колонки:
month– номер месяца (Int)date– дата платежа (Date)payment– сумма платежа (Float64)principal– часть, идущая на основной долг (Float64)interest– часть, идущая на проценты (Float64)remaining_debt– остаток долга после платежа (Float64)
Использование DataFrame позволяет применять к результатам все возможности пакета DataFrames.jl (фильтрация, агрегация, объединение) без дополнительных преобразований.
5. Обработка граничных условий
В коде предусмотрены следующие краевые случаи:
- Нулевые или отрицательные параметры – выбрасывается ошибка.
- Неверный тип платежа – ошибка с понятным сообщением.
- Для аннуитета при
term = 1формула аннуитетного коэффициента всё ещё работает, а коррекция последнего платежа гарантирует точность. - При дифференцированном расчёте
principal_partне округляется, чтобы избежать накопления ошибок при длительных сроках.
6. Тестирование и примеры использования
Представленные в наброске четыре примера служат не только демонстрацией, но и неявными тестами. Рассмотрим, какие аспекты они проверяют.
Пример 1. Аннуитетный кредит
- Проверяет корректность расчёта аннуитетного коэффициента.
- Убеждается, что последний платёж скорректирован и остаток обнулён.
- Демонстрирует округление до копеек.
Ставка = 9.01 # @param {type:"slider",min:0,max:100,step:0.01}
Срок = 12 # @param {type:"slider",min:1,max:240,step:1}
Сумма = 2151100 # @param {type:"slider",min:100,max:50000000,step:1000}
consumer_loan = CreditCalculator(amount=Float64(Сумма), rate=Ставка, term=Срок, payment_type="annuity", loan_type="consumer")
println(consumer_loan.payments_table)
Пример 2. Ипотека (дифференцированные платежи)
- Проверяет работу с датами (передача
start_dateи вычисление дат черезDates.Month). - Выводит полный график через
print_payment_schedule, подтверждая корректность форматирования. - Сравнивает первый и последний платежи, чтобы убедиться в линейном убывании.
print_payment_schedule - вывод графика платежей. Функция print_payment_schedule принимает на вход результат CreditCalculator и выводит форматированный отчёт. Её реализация использует строковые манипуляции (rpad, Dates.format) и итерацию по строкам DataFrame. Такой подход отделяет представление от логики расчёта – при необходимости легко заменить вывод на другой (например, в файл или HTML).
start_date = "2026-03-30" # @param {type:"date"}
Ставка = 9.01 # @param {type:"slider",min:0,max:100,step:0.01}
Срок = 12 # @param {type:"slider",min:1,max:240,step:1}
Сумма = 2151100 # @param {type:"slider",min:100,max:50000000,step:1000}
mortgage = CreditCalculator(amount=Float64(Сумма), rate=Ставка, term=Срок, payment_type="differentiated", loan_type="mortgage", start_date=Date(start_date))
print_payment_schedule(mortgage)
Пример 3. Сравнение типов платежей
- Проверяет, что функция
compare_payment_typesвозвращает структуру с тремя полями. - Сравнивает числовые значения переплат, подтверждая, что аннуитет даёт немного большую переплату при одинаковых параметрах.
compare_payment_types - функция сравнения, она дважды вызывает CreditCalculator с разными payment_type и формирует итоговую таблицу. Таблица создаётся как DataFrame с тремя колонками: показатель, значения для аннуитета и для дифференцированного. Это демонстрирует, как результаты одного модуля могут быть использованы для построения более сложных отчётов.
Ставка = 9.01 # @param {type:"slider",min:0,max:100,step:0.01}
Срок = 12 # @param {type:"slider",min:1,max:240,step:1}
Сумма = 2151100 # @param {type:"slider",min:100,max:50000000,step:1000}
comparison_result = compare_payment_types(amount=Float64(Сумма), rate=Ставка, term=Срок)
println(comparison_result.comparison)
Пример 4. Анализ данных таблицы платежей
- Показывает, как извлечь первые пять строк (
first). - Вычисляет максимум, минимум и сумму процентов – тем самым проверяется, что колонки
paymentиinterestсодержат корректные числовые данные. - Демонстрирует, что возвращаемый
DataFrameполностью функционален и может использоваться для статистического анализа.
first_payments = first(mortgage.payments_table, 5)
println("Первые 5 платежей по ипотеке:")
println(first_payments)
println("\nСтатистика по платежам:")
println("Максимальный платеж: ", maximum(mortgage.payments_table.payment))
println("Минимальный платеж: ", minimum(mortgage.payments_table.payment))
println("Сумма процентов за первый год: ", sum(mortgage.payments_table.interest[1:12]))
Вывод
Разработанный модуль демонстрирует эффективное использование возможностей Julia: множественная диспетчеризация через именованные аргументы, типобезопасность, удобные структуры данных (NamedTuple, DataFrame) и функциональную композицию. Такой подход позволяет легко поддерживать код, добавлять новые типы расчётов (например, с учётом досрочного погашения) и интегрировать калькулятор в более крупные финансовые приложения. Тестовые примеры одновременно служат документацией и подтверждением корректности алгоритмов.