Расчет процентов по нескольким процентным ставкам

У меня есть таблица, в которой я храню процентные ставки, каждая из которых имеет дату начала, когда она стала применимой. Более поздние записи в таблице заменяют более ранние записи. Я должен запросить эту таблицу с датой начала, датой окончания и суммой. Из этих значений мне нужно получить общую сумму процентов, которая учитывает различные процентные ставки для диапазона дат.

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

У меня есть четыре «диапазона» процентных ставок:

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

Что я хотел бы знать, так это возможно ли рассчитать процентную ставку за период времени, начиная с момента, когда процентная ставка составляла, скажем, 11,5%, и заканчивая более поздним временем, когда процентная ставка выросла вдвое до 13,5%, в рамках одного запроса.

Кажется, что расчет процентов для каждой «группы» может быть выполнен с использованием замечательного примера Suprotim Agarwal следующим образом:

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

(Процентная ставка 15,5% в приведенном выше примере)

Где я застрял, так это в том, чтобы выяснить, как связать расчет с таблицей процентных ставок, чтобы объединение учитывало, в какую «полосу» попадает каждый подраздел диапазона дат.

Любая помощь или совет будут высоко оценены.


person Frank Bailey    schedule 03.03.2017    source источник
comment
Создайте хранимую процедуру для этой ситуации.   -  person Prasanna Kumar J    schedule 03.03.2017


Ответы (2)


Вкратце: завершенный запрос — это последний блок кода в конце этого длинного объяснения.

Давайте рассмотрим это шаг за шагом, а затем представим окончательное решение в виде одного запроса. Для решения этой проблемы необходимо выполнить несколько шагов.

1) Выясните, какие рейтинги охватывают желаемый диапазон дат.

2) Разработайте умный способ выбрать эти ставки

3) Объедините эти даты и ставки таким образом, чтобы получить общую сумму начисленных процентов.


Некоторые предварительные замечания

Поскольку в вашем примере расчета процентной ставки наилучшим разрешением считаются дни, я просто использую типы данных date вместо datetime. Если вам нужно более точное разрешение, дайте мне знать, и я могу обновить.

Я использую следующие объявленные переменные

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) Интервалы дат

Прямо сейчас в вашей таблице Interest_rates указаны такие даты:

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

Но вы хотите, чтобы он отображал такие интервалы:

+ ------------- + ------------ + ------------ +
| 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   |
+ ------------- + ------------ + ------------ +

Следующий запрос может превратить ваш список дат в интервалы:

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

Примечание. Здесь я немного ошибаюсь с арифметикой дат, не используя команду dateadd().

Отслеживание таких интервалов дат значительно упрощает выбор применимых тарифов.


2) Выбор ставок

Теперь мы можем выбрать записи, которые находятся в желаемом диапазоне, используя приведенный выше запрос в качестве CTE. Этот запрос немного сложен, поэтому потребуется некоторое время, чтобы понять его.

; 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
    )

Это эффективно отфильтровывает любые ставки, которые нам не нужны, и оставляет нам

+ ------------- + ------------ + ------------ +
| 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) Рассчитать проценты

Теперь у нас есть все, что нужно, и расчет процентов — это всего лишь вопрос выбора нужных вещей из этой таблицы. Большая часть того, что вы написали для своих расчетов, остается прежней; основные изменения коснулись команды datediff(). Использование @StartDate и @EndDate не даст нам точного подсчета дней, проведенных по каждой конкретной ставке. Мы сталкиваемся с той же проблемой, используя inter_begin и inter_end. Вместо этого мы должны использовать оператор case, что-то вроде

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

Поместите это в приведенный выше запрос, чтобы получить

; 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

что дает нам эту таблицу

+ ------------- + ------------ + ------------ + ----------- + --------------- +
| 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         |
+ ------------- + ------------ + ------------ + ----------- + --------------- +

Наконец, поместите это в CTE и получите сумму поля Actual_interest:

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
person KindaTechy    schedule 03.03.2017
comment
Это отличный ответ, как из-за того, что вы ответили на мой вопрос удивительно удобным способом, так и из-за того, что вы подробно объяснили каждый шаг на этом пути. Именно на такой ответ я всегда надеюсь, когда задаю вопрос здесь. Большое спасибо! - person Frank Bailey; 06.03.2017

Возможно, немного больше, чем вы искали, но в этом примере вы можете рассчитать все кредиты в одном запросе.

Вы также можете заметить последние 3 столбца, которые представляют собой общее количество дней, общий заработанный процент и общую средневзвешенную процентную ставку.

Пример

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

Возврат

введите здесь описание изображения

person John Cappelletti    schedule 03.03.2017
comment
Хотя в целом это отличный ответ, метод расчета количества дней в году неточен. Я бы предложил использовать DIY = DATEDIFF(day, datefromparts(Year(D), 1, 1), datefromparts(Year(D) + 1, 1, 1)). Информация: DATEFROMPARTS доступен с SQL Server 2016. - person Michael Kremser; 29.11.2019
comment
@MichaelKremser Да, вы правы, указывая на ленивый расчет. Однако результаты действительны для периода между 1901 и 2099 годами. Возможно, DIY = 365.0 + IIF( try_convert(date,left(Yr,4)+'-02-29') is null , 0 , 1 ) является еще одним вариантом - person John Cappelletti; 29.11.2019