adam-hanna / goal-seek

goal-seek is a javascript library that can be used to solve for the value of an independent variable: "x"; of a function: "f(x)"; such that f(x) equals some defined goal. In other words: do you know the desired output of a function but not the input to yield such an output? If so, then use this goal seek!
MIT License
35 stars 13 forks source link

Could I set `Optional` for maxIterations? #8

Closed thearabbit closed 3 years ago

thearabbit commented 3 years ago
const result = goalSeek({
        fn,
        fnParams,
        percentTolerance: 1,
        // maxIterations: 100, // do until for resulted
        maxStep: 1,
        goal: 5000,
        independentVariableIdx: 0,
      })
adam-hanna commented 3 years ago

Not presently (IIRC), but I could probably just add a default to make it optional

thearabbit commented 3 years ago

thanks for your reply, something I don't know to set maxIterations because the Goal is dynamic??? I would like to try this goal-seek to adjust Total Payment of PMT Function for Annuity Loan Schedule.

adam-hanna commented 3 years ago

I'm not sure I understand. Could you provide an example?

On Mon, Apr 19, 2021, 11:45 PM Yuom Theara @.***> wrote:

thanks for your reply, something I don't know to set maxIterations because the Goal is dynamic???

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/adam-hanna/goal-seek/issues/8#issuecomment-822990569, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABJPMFWCLTCYZFFHBE5I3TTTJUIJNANCNFSM43HKTS5A .

thearabbit commented 3 years ago

Excuse me for late. This is my example (Sorry my English not good)

### PMT Monthly

- Rate: 3%
- Term: 36
- Loan amount: 20,000.00
- Total payment (PMT Function):  PMT(Rate, Term,-Loan amount) = 916.08
- 
**NOTE:** 
- Interest payment =  calculate by declining method
- Principal payment = Total payment - Interest payment

image

And then I would like to set ending balance Cell G46 closely to Zero (0) by changing total payment (PMT) Cell D11 value

### Goal seek

- Set cell: G46
- To value: 0
- By changing cell: $D$11
- ------------
- Result = 921.05

image

My JS Code (I base on Meteor JS + Vue)

import { pmt } from 'financial'
import _ from 'lodash'
import dayjs from 'dayjs'
import goalSeek from 'goal-seek'

// ---------------------------
let rate = 3, term = 36, amount = 20000, disDate = '2021-01-15
let totalPayment = pmt(rate / 100, term, -amount)
totalPayment = _.round(totalPayment, 2) // 916.08

const result = goalSeek({
  fn: checkEndingBalance,
  fnParams: [{ rate, term, amount, disDate }, totalPayment],
  percentTolerance: 1,
  maxIterations: 1000000,
  maxStep: 1,
  goal: 0,
  independentVariableIdx: 1,
})
// Result => ???? (failed to converge)
// ---------------------------

function checkEndingBalance(params, totalPayment) {
  const { rate, term, amount, disDate } = params

  try {
    let schedules = [
      {
        no: 0,
        date: disDate,
        day: 0,
        totalPayment: 0,
        principal: 0,
        interest: 0,
        balance: amount,
      },
    ]

    for (i = 1; i <= term; i++) {
      let preSchedule = schedules[i - 1]
      let dueDate = dayjs(disDate, 'YYYY-MM-DD').add(i, 'month')
      let day = dueDate.diff(preSchedule.date, 'day')
      let interest = preSchedule.balance * day * (rate / 100 / 30)
      interest = _.round(interest, 2)
      let principal = totalPayment - interest
      let balance = preSchedule.balance - principal

      schedules.push({
        no: i,
        date: dueDate.format('YYYY-MM-DD'),
        day: day,
        totalPayment,
        principal,
        interest,
        balance,
      })
    }

    const endingBalance = schedules[term].balance
    console.log('Ending Balance: ', endingBalance)

    return endingBalance
  } catch (e) {
    console.log(e)
  }
}

I thinks that some variable/prop don't need in Goal Seek Function:

Thanks for your package and helping 💯

Goal Seek copy.xlsx

adam-hanna commented 3 years ago

Thanks for the example, and your English is great! :)

I definitely agree that I could make some of those inputs optional, but that means that I would need to come up with sane defaults, programmatically. Currently, I put that burden on the user.

From your example, I noticed that the current parameters of the function may not be sufficient for some users. For example, in your case, you had a stated goal of 0, with a percent tolerance of 1. However, 1% of 0 is still 0, so the function will never converge. Therefore, I made percentTolerance optional and added a new optional param, customToleranceFn?: (arg0: number) => boolean;.

If you update your dep to version 0.1.4 you can try the following:

import { pmt } from 'financial'
import _ from 'lodash'
import dayjs from 'dayjs'
import goalSeek from 'goal-seek'

// ---------------------------
let rate = 3, term = 36, amount = 20000, disDate = '2021-01-15'
let totalPayment = pmt(rate / 100, term, -amount)
totalPayment = _.round(totalPayment, 2) // 916.08

const customToleranceFn = (x) => {
  return x <= 0.01
}

const result = goalSeek({
  fn: checkEndingBalance,
  fnParams: [{ rate, term, amount, disDate }, totalPayment],
  customToleranceFn,
  maxIterations: 1000000,
  maxStep: 0.0005,
  goal: 0.01,
  independentVariableIdx: 1,
})
// ---------------------------

function checkEndingBalance(params, totalPayment) {
  const { rate, term, amount, disDate } = params

  try {
    let schedules = [
      {
        no: 0,
        date: disDate,
        day: 0,
        totalPayment: 0,
        principal: 0,
        interest: 0,
        balance: amount,
      },
    ]

    for (let i = 1; i <= term; i++) {
      let preSchedule = schedules[i - 1]
      let dueDate = dayjs(disDate, 'YYYY-MM-DD').add(i, 'month')
      let day = dueDate.diff(preSchedule.date, 'day')
      let interest = preSchedule.balance * day * (rate / 100 / 30)
      interest = _.round(interest, 2)
      let principal = totalPayment - interest
      let balance = preSchedule.balance - principal

      schedules.push({
        no: i,
        date: dueDate.format('YYYY-MM-DD'),
        day: day,
        totalPayment,
        principal,
        interest,
        balance,
      })
    }

    const endingBalance = schedules[term].balance
    console.log('Ending Balance: ', endingBalance)

    return endingBalance
  } catch (e) {
    console.log(e)
  }
}

console.log(result) // result 921.0457222230434
thearabbit commented 3 years ago

thanks for your helping. I have some questions:

Very thanks again 👍

adam-hanna commented 3 years ago
  • Goal: Can't set 0? if true you could NOTE on document.

Good point. I should make a note of this in the documentation.

  • Why don't use the same param name percentTolerance & customToleranceFn, with have 2 data types Ex: percentTolerance: Number | Fn (Boolean)

I guess I could have but I personally don't like overloading types. Just a personal preference of mine.

  • maxIterations: Why you don't use While loop/Do...While to check until finding the result. For my case I am not sure to set it, because sometime the Loan Term is very long term. So I must to set maxIterations = big number as default?

maxIterations is necessary because somethings the stepsize is too large and it gets stuck in an infinite loop. At some point, the program needs to give up.

thearabbit commented 3 years ago

Thanks agin, Now It work fine for this case. If have any problem of other cases, I will create new issue!!!!

adam-hanna commented 3 years ago

Great!! Glad it's working!

On Thu, Apr 22, 2021, 5:58 PM Yuom Theara @.***> wrote:

Thanks agin, Now It work fine for this case. If have any problem of other cases, I will create new issue!!!!

— You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub https://github.com/adam-hanna/goal-seek/issues/8#issuecomment-825275137, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABJPMFRPZJPH7XFLAKGTMOLTKCZ23ANCNFSM43HKTS5A .