Calculating interest across multiple interest rates

醉酒当歌 提交于 2019-12-11 06:48:12

问题


I have a table where I store interest rates, each with a start date where it became applicable. Later-dated entries in the table supersede earlier entries. I have to query this table with a start date, an end date, and an amount. From these values I need to end up with an overall interest amount that takes the different interest rates for the date span into account.

CREATE TABLE [dbo].[Interest_Rates](
[Interest_Rate] [float] NULL,
[Incept_Date] [datetime] NULL
) ON [PRIMARY]
GO

I have four 'bands' of interest rates:

INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (10, CAST(N'2001-05-03 11:12:16.000' AS DateTime))
GO
INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (11.5, CAST(N'2014-01-07 10:49:28.433' AS DateTime))
GO
INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (13.5, CAST(N'2016-03-01 00:00:00.000' AS DateTime))
GO
INSERT [dbo].[Interest_Rates] ([Interest_Rate], [Incept_Date]) VALUES (15.5, CAST(N'2016-05-01 00:00:00.000' AS DateTime))
GO

What I'd like to know is whether it's possible to calculate the interest rate for a period of time beginning at a time when the interest rate was, say, 11.5%, and ending at a later time when the interest rate has risen twice to 13.5%, within a single query.

It seems like the interest calculation for each 'band' can be done using the wonderful Suprotim Agarwal's example as follows:

DECLARE @StartDate DateTime
DECLARE @EndDate DateTime
DECLARE @Amount Float

SET @StartDate = '2014-04-22'
SET @EndDate = '2016-04-13'
SET @Amount = 150000.00

SELECT
@Amount*(POWER(1.1550, CONVERT(NUMERIC(8,3),
DATEDIFF(d, @StartDate, @EndDate)/365.25))) - @Amount
as TotalInterest

(Interest rate at 15.5% in above example)

Where I'm getting stuck is at working out how to interrelate the calculation with the Interest Rates table such that the join takes into account which 'band' each subsection of the date span falls into.

Any help or advice would be much appreciated.


回答1:


tl;dr: the completed query is the last code block at the end of this long explanation.

Let's walk through this step-by-step and then present the final solution as one query. A few steps are needed to solve this problem.

1) Figure out which rates our desired date range covers

2) Devise a clever way to choose those rates

3) Combine those dates and rates in such a way to give us that total interest accrued.


Some Preliminary Notes

Since your example calculation of interest rate considers days as its finest resolution, I just use datatypes date instead of datetime. If you need a finer resolution, let me know and I can update.

I'm using the following declared variables

declare @EndOfTime date = '2049-12-31' -- This is some arbitrary end of time value that I chose
declare @StartDate Date = '2012-04-22' -- I made this earlier to cover more rates
declare @EndDate Date = '2016-04-13'
declare @Amount Float = 100000.00 -- I changed it to a softer number



1) Date Intervals

Right now, your interest_rates table lists dates like this:

+ ------------- + ----------- +
| interest_rate | incept_date |
+ ------------- + ----------- +
| 10            | 2001-05-03  |
| 11.5          | 2014-01-07  |
| 13.5          | 2016-03-01  |
| 15.5          | 2016-05-01  |
+ ------------- + ----------- +

But you want it to list intervals like this:

+ ------------- + ------------ + ------------ +
| interest_rate | inter_begin  | inter_end    |
+ ------------- + ------------ + ------------ +
| 10            | 2001-05-03   | 2014-01-06   |
| 11.5          | 2014-01-07   | 2016-02-29   |
| 13.5          | 2016-03-01   | 2016-04-30   |
| 15.5          | 2016-05-01   | 2049-12-31   |
+ ------------- + ------------ + ------------ +

The following query can turn your date list into intervals:

select    i1.interest_rate
        , i1.incept_date as inter_begin
        , isnull(min(i2.incept_date) - 1,@EndOfTime) as inter_end
    from #interest i1
    left join #interest i2 on i2.incept_date > i1.incept_date
    group by i1.interest_rate, i1.incept_date

Note: I'm playing a bit loose with the date arithmetic here without using the dateadd() command.

Keeping track of the date intervals like this makes selecting the applicable rates much easier.


2) Choosing the Rates

Now we can select records that sit within our desired range by using the above query as a CTE. This query is a little tricky, so take some time to really understand it.

; with
    intervals as ( 
        -- The above query/table
    )
select  *
    from intervals
    where inter_begin >= (
        select inter_begin -- selects the first rate covered by our desired interval
            from intervals
            where @StartDate between inter_begin and inter_end
    )
        and inter_end <= (
            select inter_end -- selects the last rate covered by our desired interval
                from intervals
                where @EndDate between inter_begin and inter_end
    )

This effectively filters out any rates we don't care about and leaves us with

+ ------------- + ------------ + ------------ +
| interest_rate | inter_begin  | inter_end    |
+ ------------- + ------------ + ------------ +
| 10            | 2001-05-03   | 2014-01-06   |
| 11.5          | 2014-01-07   | 2016-02-29   |
| 13.5          | 2016-03-01   | 2016-04-30   |
+ ------------- + ------------ + ------------ +


3) Calculate the Interest

Now we have everything we need, and calculating the interest is just a matter selecting the right things from this table. Most of what you wrote for your calculation remains the same; the main changes are in the datediff() command. Using @StartDate and @EndDate won't give us an accurate count of the days spent at each specific rate. We run into the same problem by using inter_begin and inter_end. Instead, we must use a case statement, something like

datediff(day, 
    case when @StartDate > inter_begin then @StartDate else inter_begin end,
    case when @EndDate < inter_end then @EndDate else inter_end end
)

Put this in the above Query to get

; with
    intervals as (...) -- same as above
select  *
        , DATEDIFF(day,
              case when @StartDate > inter_begin then @StartDate else inter_begin end,
              case when @EndDate < inter_end then @EndDate else inter_end end) as days_active
        , @Amount*(POWER((1+interest_rate/100),
              convert(float,
                  DATEDIFF(day,
                      case when @StartDate > inter_begin then @StartDate else inter_begin end,
                      case when @EndDate < inter_end then @EndDate else inter_end end
                  )
              )/365.25)
          ) - @Amount as Actual_Interest
    from ... -- same as above

which gives us this table

+ ------------- + ------------ + ------------ + ----------- + --------------- +
| interest_rate | inter_begin  | inter_end    | days_active | Actual_interest |
+ ------------- + ------------ + ------------ + ----------- + --------------- +
| 10            | 2001-05-03   | 2014-01-06   | 624         | 17683.63        |
| 11.5          | 2014-01-07   | 2016-02-29   | 786         | 26283.00        |
| 13.5          | 2016-03-01   | 2016-04-30   | 43          | 1501.98         |
+ ------------- + ------------ + ------------ + ----------- + --------------- +

Finally, put this in a CTE and take the sum of the Actual_interest field:

declare @EndOfTime date = '2049-12-31' -- This is some arbitrary end of time value that I chose
declare @StartDate Date = '2012-04-22' -- I made this earlier to cover more rates
declare @EndDate Date = '2016-04-13'
declare @Amount Float = 100000.00 -- I changed it to a softer number

; with
    intervals as (
        select    i1.interest_rate
                , i1.incept_date as inter_begin
                , isnull(min(i2.incept_date) - 1,@EndOfTime) as inter_end
            from #interest i1
            left join #interest i2 on i2.incept_date > i1.incept_date
            group by i1.interest_rate, i1.incept_date
    )
    , interest as (
        select  *
                , DATEDIFF(day,
                      case when @StartDate > inter_begin then @StartDate else inter_begin end,
                      case when @EndDate < inter_end then @EndDate else inter_end end) as days_active
                , @Amount*(POWER((1+interest_rate/100),
                      convert(float,
                          DATEDIFF(day,
                              case when @StartDate > inter_begin then @StartDate else inter_begin end,
                              case when @EndDate < inter_end then @EndDate else inter_end end
                          )
                      )/365.25)
                  ) - @Amount as Actual_Interest
            from intervals
            where inter_begin >= (
                select inter_begin -- selects the first rate covered by our desired interval
                    from intervals
                    where @StartDate between inter_begin and inter_end
            )
                and inter_end <= (
                    select inter_end -- selects the last rate covered by our desired interval
                        from intervals
                        where @EndDate between inter_begin and inter_end
            )
    )
select sum(actual_interest) as total_interest
    from interest



回答2:


Perhaps a little more than you were looking for, but in this example, you can calculate all loans in one query.

You may also notice the last 3 columns which represent Total Number of Days, Total Interest Earned and the Total Weighted Average Interest Rate

Example

Declare @Interest_Rate table (interest_rate money,Incept_Date datetime)
Insert Into @Interest_Rate values
(10  ,'2001-05-03 11:12:16.000'),
(11.5,'2014-01-07 10:49:28.433'),
(13.5,'2016-03-01 00:00:00.000'),
(15.5,'2016-05-01 00:00:00.000')

Declare @Loan table (Id int,StartDate date, EndDate date,Amount money)
Insert Into @Loan values
(1,'2014-01-01','2015-11-17',150000),
(1,'2015-11-18','2016-12-31',175000),   -- Notice Balance Change
(2,'2016-01-01','2020-06-15',200000)


Select A.ID
      ,A.Amount
      ,DateR1 = min(D)
      ,DateR2 = max(D)
      ,Days   = count(*)
      ,B.Interest_Rate
      ,Interest_Earned  = cast(sum(((A.Amount*B.Interest_Rate)/B.DIY)/100.0) as decimal(18,2))
      ,Total_Days       = sum(count(*)) over (Partition By A.ID)
      ,Total_Int_Earned = sum(cast(sum(((A.Amount*B.Interest_Rate)/B.DIY)/100.0) as decimal(18,2))) over (Partition By A.ID)
      ,Total_WAIR       = sum(A.Amount * count(*) * B.interest_rate) over (Partition By A.ID)/ sum(A.Amount * count(*)) over (Partition By A.ID)
 From  @Loan A
 Join (
        Select D
              ,D1
              ,interest_rate
              ,DIY = 365.0 + IIF(Year(D) % 4 = 0 , 1 , 0 )
         From ( Select Top (DateDiff(DD,(Select cast(min(Incept_Date) as date) from @Interest_Rate),cast(GetDate() as date))+1) D=DateAdd(DD,-1+Row_Number() Over (Order By (Select NULL)),(Select cast(min(Incept_Date) as date) from @Interest_Rate)) From  master..spt_values N1,master..spt_values N2  ) A
         Join (
                Select interest_rate
                      ,D1 = cast(Incept_Date as Date)
                      ,D2 = cast(DateAdd(DAY,-1,Lead(Incept_Date,1,GetDate()) over (Order by Incept_Date)) as date)
                 From  @Interest_Rate
              ) B on D between D1 and D2
      ) B on D Between StartDate and EndDate
  Group By A.ID,A.Amount,B.D1,B.Interest_Rate

Returns



来源:https://stackoverflow.com/questions/42576707/calculating-interest-across-multiple-interest-rates

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!