├── .circleci └── config.yml ├── .gitignore ├── .gitlab-ci.yml ├── .tool-versions ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── finance_math.gemspec ├── lib ├── finance_math.rb ├── finance_math │ └── loan.rb └── version.rb └── spec ├── lib └── loan_spec.rb └── spec_helper.rb /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Ruby CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-ruby/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/ruby:2.4.1-node-browsers 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/postgres:9.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "Gemfile.lock" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: 30 | name: install dependencies 31 | command: | 32 | bundle install --jobs=4 --retry=3 --path vendor/bundle 33 | 34 | - save_cache: 35 | paths: 36 | - ./vendor/bundle 37 | key: v1-dependencies-{{ checksum "Gemfile.lock" }} 38 | 39 | # Database setup 40 | - run: bundle exec rake db:create 41 | - run: bundle exec rake db:schema:load 42 | 43 | # run tests! 44 | - run: 45 | name: run tests 46 | command: | 47 | mkdir /tmp/test-results 48 | TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings)" 49 | 50 | bundle exec rspec --format progress \ 51 | --format RspecJunitFormatter \ 52 | --out /tmp/test-results/rspec.xml \ 53 | --format progress \ 54 | $TEST_FILES 55 | 56 | # collect reports 57 | - store_test_results: 58 | path: /tmp/test-results 59 | - store_artifacts: 60 | path: /tmp/test-results 61 | destination: test-results -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.bundle 19 | /vendor/bundle 20 | *.so 21 | *.o 22 | *.a 23 | mkmf.log 24 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs 3 | - ruby -v 4 | - which ruby 5 | - gem install bundler --no-ri --no-rdoc 6 | - bundle install --jobs $(nproc) "${FLAGS[@]}" 7 | 8 | rspec: 9 | script: 10 | - bundle exec rspec 11 | 12 | rubocop: 13 | script: 14 | - bundle exec rubocop 15 | 16 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 3.3.0 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in finance_math.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Nebojsa Zoric/Kolosek 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Gem Version](https://badge.fury.io/rb/finance_math.svg)](http://badge.fury.io/rb/finance_math) 2 | [![Code Climate](https://codeclimate.com/github/kolosek/finance_math/badges/gpa.svg)](https://codeclimate.com/github/kolosek/finance_math) 3 | 4 | 5 | ## What is FinanceMath? 6 | 7 | FinanceMath is a Ruby library for mapping Loan based Exel functions. It deals with problem of calculating the PMT and APR functions. It implements advanced usage, taking into account bank fees, fee for for each payment and currency protection (if currency other than $ USD). Highly precise with high speed of execution. 8 | 9 | ## Installation 10 | 11 | FinanceMath is available as a gem, to install it just install the gem: 12 | 13 | gem install finance_math 14 | 15 | If you're using Bundler, add the gem to Gemfile. 16 | 17 | gem 'finance_math' 18 | 19 | Run `bundle install`. 20 | 21 | ## Running tests 22 | 23 | bundle exec rspec spec/ 24 | 25 | ## Basic Usage 26 | 27 | Create an instance, and pass parameters for nominal annual rate, duration (in months), and amount of loan. 28 | Defaults are structure_fee = 5, currency protection = 3, so please update if you need other values. 29 | 30 | ```ruby 31 | 32 | FinanceMath::Loan.new(nominal_rate: 10.5, duration: 12, amount: 15000) 33 | ``` 34 | 35 | ## Advanced Usage 36 | 37 | Create an instance, and pass parameters for nominal annual rate, duration (in months), and amount of loan, and additional values such as bank fee, currency protection, and fee for each monthly payment. 38 | 39 | Defaults are structure_fee = 5, currency protection = 3, so please update if you need other values. 40 | 41 | ```ruby 42 | 43 | FinanceMath::Loan.new(nominal_rate: 10.5, duration: 12, amount: 15000, structure_fee: 5.1, currency_protection: 2.75, fee: 25) 44 | ``` 45 | 46 | ## Functions 47 | 48 | This is the list of available functions. 49 | 50 | ### FinanceMath::Loan.pmt 51 | 52 | Calculates the periodic payment for an annuity investment based on constant-amount periodic payments and a constant interest rate. 53 | 54 | ```ruby 55 | 56 | loan = FinanceMath::Loan.new(nominal_rate: 10, duration: 12, amount: 1000) 57 | loan.pmt 58 | # 87.9158872300099 59 | 60 | ``` 61 | 62 | ### FinanceMath::Loan.apr 63 | 64 | Calculates the Annual Percentage Rate. 65 | 66 | ```ruby 67 | 68 | loan = FinanceMath::Loan.new(nominal_rate: 13, duration: 12, amount: 10000) 69 | loan.apr 70 | #29.179538647635006 71 | 72 | loan = FinanceMath::Loan.new(nominal_rate: 15, duration: 36, amount: 10000, structure_fee: 5, currency_protection: 3, fee: 10) 73 | loan.apr 74 | #23.964418264624054 75 | 76 | ``` 77 | 78 | ## Contributing 79 | 80 | 1. Fork it ( https://github.com/kolosek/finance_math/fork ) 81 | 2. Create your feature branch (`git checkout -b my-new-feature`) 82 | 3. Commit your changes (`git commit -am 'Add some feature'`) 83 | 4. Push to the branch (`git push origin my-new-feature`) 84 | 5. Create a new Pull Request 85 | 86 | ## Tests 87 | 88 | Please cover with tests your pull requests 89 | 90 | ## Ruby versions 91 | 92 | Currently supported version of 2.x and 3.x 93 | 94 | ## Credits 95 | 96 | Finance Math is maintained and sponsored by 97 | [Kolosek] (http://kolosek.com). 98 | 99 | ![Kolosek](http://kolosek.com/logo.png) 100 | 101 | Initially developed by Nesha Zoric 102 | 103 | ###Follow up @kolosek 104 | 105 | 106 | ## License 107 | 108 | MIT License. See LICENSE for details. 109 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | RSpec::Core::RakeTask.new 5 | 6 | task :default => :spec 7 | task :test => :spec -------------------------------------------------------------------------------- /finance_math.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "finance_math" 8 | spec.version = FinanceMath::VERSION 9 | spec.authors = ["Nesha Zoric"] 10 | spec.email = ["nesha@kolosek.com"] 11 | spec.summary = %q{Most accurate APR and PMT caluclator for Ruby.} 12 | spec.description = %q{Implementation of Loan/Mortgage functions in Ruby language. APR function and PMT function. In calculations it includes implementation of bank fee, marketplace fee, fees for each payment to provide the most precise calculation at very high speed. } 13 | spec.homepage = "http://kolosek.com" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler", "~> 1.6" 22 | spec.add_development_dependency "rake", "~> 2.0" 23 | spec.add_development_dependency 'rspec', "~> 2.0" 24 | end 25 | -------------------------------------------------------------------------------- /lib/finance_math.rb: -------------------------------------------------------------------------------- 1 | require "version" 2 | 3 | module FinanceMath 4 | autoload :Loan, 'finance_math/loan' 5 | end 6 | -------------------------------------------------------------------------------- /lib/finance_math/loan.rb: -------------------------------------------------------------------------------- 1 | module FinanceMath 2 | # the Loan class provides an interface for working with interest rates. 3 | # @api public 4 | class Loan 5 | # @return [Integer] the duration for which the rate is valid, in months 6 | # @api public 7 | attr_accessor :duration 8 | 9 | # @return [Float] the amount of loan request 10 | # @api public 11 | attr_accessor :amount 12 | 13 | # @return [Float] the nominal annual rate 14 | # @api public 15 | attr_accessor :nominal_rate 16 | 17 | # @return [DecNum] the monthly rate 18 | # @api public 19 | attr_reader :monthly_rate 20 | 21 | # @return [DecNum] the currency protection 22 | # @api public 23 | attr_reader :currency_protection 24 | 25 | # @return [DecNum] the fee for the bank/market 26 | # @api public 27 | attr_reader :structure_fee 28 | 29 | # @return [DecNum] P principal 30 | # @api public 31 | attr_reader :principal 32 | 33 | # @return [Float] fee 34 | # @api public 35 | attr_reader :fee 36 | 37 | # create a new Loan instance 38 | # @return [Loan] 39 | # @param [Numeric] decimal value of the interest rate 40 | # @param [Integer] Duration of the loan period 41 | # @param [Float] Loan amount 42 | # @param [Float] structure fee - fee for the market in percentages 43 | # @param [Float] currency protection - Protection for currency changes - usually 3%, default to 0% 44 | # @example create a 10.5% Nominal rate 45 | # @see http://en.wikipedia.org/wiki/Nominal_interest_rate 46 | # @api public 47 | 48 | def initialize(options = {}) 49 | initialize_options(options) 50 | @principal = principal_calculation 51 | @monthly_rate = @nominal_rate / 100 / 12 52 | end 53 | 54 | def pmt(options = {}) 55 | future_value = options.fetch(:future_value, 0) 56 | type = options.fetch(:type, 0) 57 | ((@amount * interest(@monthly_rate, @duration) - future_value ) / ((1.0 + @monthly_rate * type) * fvifa(@monthly_rate, duration))) 58 | end 59 | 60 | def apr 61 | @apr ||= calculate_apr 62 | end 63 | 64 | protected 65 | 66 | def pow1pm1(x, y) 67 | (x <= -1) ? ((1 + x) ** y) - 1 : Math.exp(y * Math.log(1.0 + x)) - 1 68 | end 69 | 70 | def pow1p(x, y) 71 | (x.abs > 0.5) ? ((1 + x) ** y) : Math.exp(y * Math.log(1.0 + x)) 72 | end 73 | 74 | def interest(monthly_rate, duration) 75 | pow1p(monthly_rate, duration) 76 | end 77 | 78 | def fvifa(monthly_rate, duration) 79 | (monthly_rate == 0) ? duration : pow1pm1(monthly_rate, duration) / monthly_rate 80 | end 81 | 82 | private 83 | 84 | def initialize_options(options) 85 | @nominal_rate = options.fetch(:nominal_rate).to_f 86 | @duration = options.fetch(:duration).to_f 87 | @amount = options.fetch(:amount).to_f 88 | @structure_fee = options.fetch(:structure_fee, 5).to_f 89 | @currency_protection = options.fetch(:currency_protection, 3).to_f 90 | @fee = options.fetch(:fee, 0).to_f 91 | end 92 | 93 | def principal_calculation 94 | amount * (1 - currency_protection/100 - structure_fee / 100 ) - fee * duration 95 | end 96 | 97 | # solves APR 98 | # [a (1 + a)^N] / [(1 + a)^N - 1] - P/C = 0 99 | # where a = APR/1200, N = duration, P = monthly payment, C = loan_amount 100 | # Newton-Raphson finds root (the value for 'a' that makes f(a) = 0) 101 | def calculate_apr 102 | payment_ratio = pmt / principal_calculation 103 | duration = @duration 104 | f = lambda {|k| (k**(duration + 1) - (k**duration * (payment_ratio + 1)) + payment_ratio)} 105 | f_deriv = lambda { |k| ((duration + 1) * k**duration) - (duration * (payment_ratio + 1) * k**(duration - 1))} 106 | 107 | root = newton_raphson(f, f_deriv, monthly_rate + 1) 108 | 100 * 12 * (root -1).to_f 109 | end 110 | 111 | # 'start' is the monthly_rate, Newton Raphson will find the apr root very quickly 112 | # k1 = k0 - f(k0)/f'(k0) 113 | # k_plus_one = k - f(k)/f_deriv(k) f_deriv should be an positive number! 114 | # We find the k-intercept of the tangent line at point k_plus_one and compare k to k_plus_one. 115 | # This is repeated until a sufficiently accurate value is reached, which can be specified with the 'precision' parameter 116 | def newton_raphson(f, f_deriv, start, precision = 5) 117 | k_plus_one = start 118 | k = 0.0 119 | while ((k - 1) * 10**precision).to_f.floor != ((k_plus_one - 1) * 10**precision).to_f.floor 120 | k = k_plus_one 121 | k_plus_one = k - f.call(k) / f_deriv.call(k).abs 122 | end 123 | k_plus_one 124 | end 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /lib/version.rb: -------------------------------------------------------------------------------- 1 | module FinanceMath 2 | VERSION = "1.0.6" 3 | end 4 | -------------------------------------------------------------------------------- /spec/lib/loan_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Loan do 4 | it "should return initial set nominal rate" do 5 | loan = Loan.new(nominal_rate: 10, duration: 12, amount: 1000) 6 | expect(loan.nominal_rate).to eq(10) 7 | end 8 | 9 | it "should return initial set duration period" do 10 | loan = Loan.new(nominal_rate: 10, duration: 12, amount: 1000) 11 | expect(loan.duration).to eq(12) 12 | end 13 | 14 | it "should return initial set amount loaned" do 15 | loan = Loan.new(nominal_rate: 10, duration: 12, amount: 1000) 16 | expect(loan.amount).to eq(1000) 17 | end 18 | 19 | context ".pmt" do 20 | 21 | it "should return correct pmt value" do 22 | loan = Loan.new(nominal_rate: 10, duration: 12, amount: 1000) 23 | expect(loan.pmt).to eq(87.9158872300099) 24 | end 25 | 26 | it "should return correct pmt value" do 27 | loan = Loan.new(nominal_rate: 0, duration: 12, amount: 1200) 28 | expect(loan.pmt).to eq(100) 29 | end 30 | 31 | it "should return correct pmt value" do 32 | loan = Loan.new(nominal_rate: 0, duration: 36, amount: 10000) 33 | expect(loan.pmt).to eq(277.77777777777777) 34 | end 35 | 36 | it "should return correct pmt value" do 37 | loan = Loan.new(nominal_rate: 0, duration: 6, amount: 10000) 38 | expect(loan.pmt).to eq(1666.6666666666667) 39 | end 40 | 41 | it "should return correct pmt value" do 42 | loan = Loan.new(nominal_rate: 13, duration: 90, amount: 1000000) 43 | expect(loan.pmt).to eq(17449.90775727763) 44 | end 45 | end 46 | 47 | context ".apr, edge cases" do 48 | 49 | it "should return correct apr value" do 50 | loan = Loan.new(nominal_rate: 16, duration: 24, amount: 9200) 51 | expect(loan.apr).to eq(24.699310868498614) 52 | end 53 | 54 | it "should return correct apr value" do 55 | loan = Loan.new(nominal_rate: 13, duration: 24, amount: 10000) 56 | expect(loan.apr).to eq(21.589972932434698) 57 | end 58 | 59 | it "should return correct apr value" do 60 | loan = Loan.new(nominal_rate: 13, duration: 18, amount: 10000) 61 | expect(loan.apr).to eq(24.1815502466296) 62 | end 63 | 64 | it "should return correct apr value" do 65 | loan = Loan.new(nominal_rate: 13, duration: 12, amount: 10000) 66 | expect(loan.apr).to eq(29.179538647635006) 67 | end 68 | 69 | it "should return correct apr value" do 70 | loan = Loan.new(nominal_rate: 13, duration: 6, amount: 10000) 71 | expect(loan.apr).to eq(42.82076503747119) 72 | end 73 | 74 | it "should return correct apr value" do 75 | loan = Loan.new(nominal_rate: 13, duration: 36, amount: 10000) 76 | expect(loan.apr).to eq(18.93638316167774) 77 | end 78 | 79 | it "should return correct apr value" do 80 | loan = Loan.new(nominal_rate: 13, duration: 90, amount: 10000000) 81 | expect(loan.apr).to eq(15.690778147507167) 82 | end 83 | 84 | it "should return correct apr value" do 85 | loan = Loan.new(nominal_rate: 13, duration: 90, amount: 50000000) 86 | expect(loan.apr).to eq(15.690778147507167) 87 | end 88 | 89 | it "should return correct apr value" do 90 | loan = Loan.new(nominal_rate: 13, duration: 1, amount: 50000000) 91 | expect(loan.apr).to eq(118.47826151517138) 92 | end 93 | 94 | it "should return correct apr value" do 95 | loan = Loan.new(nominal_rate: 80, duration: 1, amount: 1000) 96 | expect(loan.apr).to eq(191.30434783476406) 97 | end 98 | 99 | it "should return correct apr value" do 100 | loan = Loan.new(nominal_rate: 36, duration: 200, amount: 500) 101 | expect(loan.apr).to eq(39.173057290003044) 102 | end 103 | 104 | it "should return correct apr value" do 105 | loan = Loan.new(nominal_rate: 15, duration: 36, amount: 10000, structure_fee: 5, currency_protection: 3, fee: 10) 106 | expect(loan.apr).to eq(23.964418264624054) 107 | end 108 | end 109 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require_relative '../lib/finance_math/loan.rb' 2 | require 'finance_math' 3 | 4 | include FinanceMath --------------------------------------------------------------------------------