SqlNut
SqlNut

Reputation: 3

sql amortization Schedule Multiple Loans

I have the following code (which is not mine and found on a website which that person did a great job with this)This code does exactly what I need except I am still a newbie and don't understand how I can pass multiple loans through it. I have tried several ways to insert the set command for each declaration and each time I get errors. Any help would be appreciative.

\-------Section I think  my table values go in-------

I am updating this section in response to the answer given. So the Fiddle does what I need the way you have it setup but my only issue is I have read only access for reporting and running sql scripts for data. I can not create function to pass through. Sorry I should have mentioned that. My other issue it that all my loans will be based on 12 months period with varying total months. Sample table data below:

LoanNumber LoanStart LoanEnd LoanAmount YrlyInt LoanMonths
11111 07/10/2023 07/06/2027 11341.98 6.65 48
22222 03/03/2022 02/25/2027 11242.78 3.25 60
33333 07/22/2020 07/25/2024 10496.91 3.43 48

I couldn't figure out the logic of how to make it a static period and then use my loanmonths for the actual calculations. SQL fiddle does it right but when I try to change it to my table data that is where it goes buggy.

How it should be:

enter image description here

How mine is: enter image description here

[enter image description here][3]

[enter image description here][4]

[enter image description here][5]

Desired Output here. [3]: https://i.sstatic.net/J3j6D.png [4]: https://i.sstatic.net/bg32u.png [5]: https://i.sstatic.net/BQSHh.png

Upvotes: 0

Views: 424

Answers (1)

ValNik
ValNik

Reputation: 5731

See example.
Loans:

create table loans(id int,StartDate date,LoanAmount float,InterestRate float
     ,PeriodsPerYear int);
insert into loans values (1,'2023-01-01',10000.0,0.07,12);
insert into loans values (2,'2023-12-01',20000.0,0.05,12);
select * from loans;

Function

create or alter function calcLoan (
  @pN int =null
  ,@pIr float =null
  ,@pPv float =null
  ,@pNpy int =12
  ,@pBegDate date= null)
RETURNS @query TABLE (
    pmt_num int
  , payment_date date
  , beg_balance decimal(19,2)
  , scheduled_pmt decimal(19,2)
  , amt_to_intrest decimal(19,2)
  , amt_to_principal decimal(19,2)
  , end_balance decimal(19,2)
  )
Begin
DECLARE  @n int = 48,-- total # of payments 
@ir float = .0749,-- annual interest rate (note: enter as a decimal... So %5 would be entered as 0.05...) 
@pv float = 17864.78,-- present value (original loan amount) 
@npy int = 12,-- # of periods per year  
@beg_dt date = GETDATE(); -- the date of the first payment.  

if isnull(@pN,0)>0  set @n=@pN;
if isnull(@pIr,0.0)<>0.0  set @ir=@pIr;
if isnull(@pPv,0.0)<>0.0  set @pv=@pPv;
if isnull(@pNpy,0.0)<>0.0  set @npy=@pNpy;
if @pBegDate is null  set @beg_dt=@pBegDate;

WITH  cte_n1 (n) AS (
  SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (n)
)
,cte_n2 (n) AS (
  SELECT 1 FROM cte_n1 a CROSS JOIN cte_n1 b
)
, cte_Tally (n) AS (
  SELECT TOP (@n) 
     ROW_NUMBER() OVER (ORDER BY a.n) 
  FROM cte_n2 a CROSS JOIN cte_n2 b ORDER BY a.n 
) 
insert into @query
SELECT  pmt_num = t.n
  , pd.payment_date
  , beg_balance = CONVERT(decimal(19,2), pv.beg_balance)
  , scheduled_pmt = CONVERT(decimal(19, 2), pmt.pmt_calc)
  , amt_to_intrest = CONVERT(decimal(19,2), ipmt.ipmt)
  , amt_to_principal = CONVERT(decimal(19,2), ppmt.ppmt)
  , end_balance = CONVERT(decimal(19,2), pv.end_balance) 
FROM  cte_Tally t 
CROSS APPLY ( VALUES (
        @pv / (POWER(1 + (@ir / @npy), @n) - 1) 
            * ((@ir / @npy) * POWER(1 + (@ir / @npy), @n))
         )) pmt (pmt_calc) 
CROSS APPLY ( VALUES (
            ABS(-@pv * POWER(1 + (@ir / @npy), t.n-1) 
                + pmt.pmt_calc * (POWER(1 + (@ir / @npy), t.n-1) -1) / (@ir / @npy))
          , ABS(-@pv * POWER(1 + (@ir / @npy), t.n) 
                + pmt.pmt_calc * (POWER(1 + (@ir / @npy), t.n) -1) / (@ir / @npy)) 
         )) pv (beg_balance, end_balance) 
CROSS APPLY ( VALUES (pv.beg_balance * (@ir / @npy)
         ) ) ipmt (ipmt) 
CROSS APPLY ( VALUES (pmt.pmt_calc - ipmt.ipmt
             ) ) ppmt (ppmt) 
CROSS APPLY ( VALUES ( 
             CASE WHEN @npy <= 12 THEN DATEADD(MONTH, (12 / @npy) * (t.n - 1), @beg_dt) 
                  WHEN @npy = 26 THEN DATEADD(WEEK, 2 * (t.n - 1), @beg_dt) 
             ELSE DATEADD(DAY, (365 / @npy) * (t.n - 1), @beg_dt) 
             END 
       ) ) pd (payment_date)
;
return;
end;

Use of function

select * from loans l
cross apply calcLoan( 6,l.InterestRate,LoanAmount,l.PeriodsPerYear,l.StartDate)

Output

id StartDate LoanAmount Interest Rate Periods PerYear pmt_num payment_date beg_ balance scheduled _pmt amt_to_ intrest amt_to_ principal end_ balance
1 2023-01-01 10000 0.07 12 1 2024-03-06 10000.00 2536.56 58.33 2478.23 7521.77
1 2023-01-01 10000 0.07 12 2 2024-04-06 7521.77 2536.56 43.88 2492.69 5029.08
1 2023-01-01 10000 0.07 12 3 2024-05-06 5029.08 2536.56 29.34 2507.23 2521.85
1 2023-01-01 10000 0.07 12 4 2024-06-06 2521.85 2536.56 14.71 2521.85 0.00
2 2023-12-01 20000 0.05 12 1 2024-03-06 20000.00 5052.19 83.33 4968.86 15031.14
2 2023-12-01 20000 0.05 12 2 2024-04-06 15031.14 5052.19 62.63 4989.56 10041.58
2 2023-12-01 20000 0.05 12 3 2024-05-06 10041.58 5052.19 41.84 5010.35 5031.23
2 2023-12-01 20000 0.05 12 4 2024-06-06 5031.23 5052.19 20.96 5031.23 0.00

Example fiddle

See another example, where stored procedure converted to query
Fiddle

There we can put parameters for query from table as

WITH  params as (
  select LoanMonth pN,YrlyInt as pIr, LoanAmount pPv
        , PeriodsPerYear pNpy,LoanStart pBeg_dt
        ,LoanNumber
  from loans 
  -- filter loans 
  -- where LoanNumber=4  or LoanNumber in(5555,1111)
  )

Or parameters can be external

WITH  params as (
  select @LoanMonth pN,@YrlyInt as pIr, @LoanAmount pPv
        , @PeriodsPerYear pNpy,@LoanStart pBeg_dt
        ,@LoanNumber as LoanNumber
  )
WITH  params as (
  select LoanMonth pN,YrlyInt as pIr, LoanAmount pPv
        , PeriodsPerYear pNpy,LoanStart pBeg_dt
        ,LoanNumber
  from loans -- where LoanNumber=4
  )
, cte_Tally (n) AS (
  select rn
  from(
  SELECT pN, ROW_NUMBER() OVER (ORDER BY a.n) rn
  FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) a (n)
  CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) b (n)
  CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) c (n)
  CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) d (n)
  cross apply (select pN from params)e
  ) x
  where rn<=pN
) 

SELECT  LoanNumber
  , pmt_num = t.n
  , pd.payment_date
  , beg_balance = CONVERT(decimal(19,2), pv.beg_balance)
  , scheduled_pmt = CONVERT(decimal(19, 2), pmt.pmt_calc)
  , amt_to_intrest = CONVERT(decimal(19,2), ipmt.ipmt)
  , amt_to_principal = CONVERT(decimal(19,2), ppmt.ppmt)
  , end_balance = CONVERT(decimal(19,2), pv.end_balance) 
FROM  cte_Tally t cross apply params
CROSS APPLY ( VALUES (
        pPv / (POWER(1 + (pIr / pNpy), pN) - 1) 
            * ((pIr / pNpy) * POWER(1 + (pIr / pNpy), pN))
         )) pmt (pmt_calc) 
CROSS APPLY ( VALUES (
            ABS(-pPv * POWER(1 + (pIr / pNpy), t.n-1) 
                + pmt.pmt_calc * (POWER(1 + (pIr / pNpy), t.n-1) -1) / (pIr / pNpy))
          , ABS(-pPv * POWER(1 + (pIr / pNpy), t.n) 
                + pmt.pmt_calc * (POWER(1 + (pIr / pNpy), t.n) -1) / (pIr / pNpy)) 
         )) pv (beg_balance, end_balance) 
CROSS APPLY ( VALUES (pv.beg_balance * (pIr / pNpy)
         ) ) ipmt (ipmt) 
CROSS APPLY ( VALUES (pmt.pmt_calc - ipmt.ipmt
             ) ) ppmt (ppmt) 
CROSS APPLY ( VALUES ( 
             CASE WHEN pNpy <= 12 THEN DATEADD(MONTH, (12 / pNpy) * (t.n - 1), pBeg_dt) 
                  WHEN pNpy = 26 THEN DATEADD(WEEK, 2 * (t.n - 1), pBeg_dt) 
             ELSE DATEADD(DAY, (365 / pNpy) * (t.n - 1), pBeg_dt) 
             END 
       ) ) pd (payment_date)
order by LoanNumber,pmt_num
;

Upvotes: 0

Related Questions