在雪花中创建摊销计划

Creating an amortization schedule in snowflake

我在 snowflake 中有一个视图,它提供了以下内容:

如果你愿意的话,我想从中生成一种摊销时间表。因此,如果我有一笔贷款,日期为 2022 年 1 月 1 日,到期日为 2022 年 3 月 9 日,支付频率为每两周支付一次,每次支付 50 美元,我希望看到如下输出:

LoanID Payment Date Payment Amount Payment Frequency
abc123 1/15/2022 .00 biweekly
abc123 1/29/2022 .00 biweekly
abc123 2/12/2022 .00 biweekly
abc123 2/26/2022 .00 biweekly
abc123 3/09/2022 .00 biweekly

我假设我需要某种循环,同时付款日期 < 到期日和总和(付款金额) < 贷款金额,但我不确定如何正确设置它以查看数千笔贷款.你们能提供的任何帮助都将是不可思议的,我非常感激!

你可以通过写一个Recursive CTE, just remember that the default is limited to 100 iterations, if you need more loops then check this MAX_RECURSIONS参数来得到它。

这只是一个代码示例,您应该扩展它以包括一些极端的数据保护;

示例数据:

CREATE OR REPLACE TABLE LoanTable (
    LoanID STRING,
    Loan_date DATE,
    Loan_amount NUMERIC(12,2),
    Maturity_date DATE,
    Payment_frequency STRING,
    Payment_amount NUMERIC(12,2)
);

INSERT INTO LoanTable
VALUES ('abc123', '1/1/2022', 250, '3/9/2022', 'biweekly', 50);

查询:

WITH Recursive_CTE AS (
  SELECT LoanID, 
         CASE Payment_frequency WHEN 'weekly'      THEN DATEADD(WEEK, 1, Loan_date)
                                WHEN 'biweekly'    THEN DATEADD(WEEK, 2, Loan_date)
                                WHEN 'semimonthly' THEN DATEADD(DAY, 15, Loan_date) -- I don't know how the semimonthly value is determined??
                                WHEN 'monthly'     THEN DATEADD(MONTH, 1, Loan_date) END AS Payment_Date,
         Payment_amount,
         Loan_amount - Payment_amount AS Left_to_pay,
         Payment_frequency,
         Maturity_date
    FROM LoanTable
   UNION ALL
  SELECT LoanID, 
         CASE Payment_frequency WHEN 'weekly'      THEN DATEADD(WEEK, 1, Payment_Date)
                                WHEN 'biweekly'    THEN DATEADD(WEEK, 2, Payment_Date)
                                WHEN 'semimonthly' THEN DATEADD(DAY, 15, Payment_Date) -- I don't know how the semimonthly value is determined??
                                WHEN 'monthly'     THEN DATEADD(MONTH, 1, Payment_Date) END AS Payment_Date,
         Payment_amount,
         IFF(Left_to_pay - Payment_amount < 0, Left_to_pay, Left_to_pay - Payment_amount) AS Left_to_pay,
         Payment_frequency,
         Maturity_date
    FROM Recursive_CTE  
   WHERE Left_to_pay > 0
)
SELECT LoanID, IFF(Payment_Date > Maturity_date, Maturity_date, Payment_Date) AS Payment_Date, Payment_amount, Left_to_pay, Payment_frequency
  FROM Recursive_CTE
 ORDER BY LoanID, Payment_Date;

Table生成器是另一种方法。

感谢 Simon 改进了这个解决方案。尊重!

WITH CTE_MY_DATE AS 
(SELECT  DATEADD(DAY, row_number() over (order by null)-1, '1900-01-01')::date AS MY_DATE FROM table(generator(rowcount => 18000))) 

SELECT
    date(MY_DATE) CALENDAR_DATE,
    concat( decode(extract ('dayofweek_iso', date(MY_DATE)),1,'Monday',2, 'Tuesday',3, 'Wednesday',4, 'Thursday',5, 'Friday',6, 'Saturday',7, 'Sunday'),TO_CHAR(date(MY_DATE), ', MMMM DD, YYYY')) FULL_DATE_DESC  
,row_number() over (partition by 1 order by calendar_date ) MOD_IS_COOL
FROM
    CTE_MY_DATE  
where
    CALENDAR_DATE 
        between '2022-01-02' and '2022-09-03' 
qualify 
    mod(MOD_IS_COOL, 14) = 0

下面是如何通过 JavaScript UDF 进行摊销,以及如何调用它的示例。我在从函数中获取 JSON 时遇到了一些麻烦,因此 return 将其编辑为文本字符串,去除双引号,将其展平,然后转换为 Table。也许更擅长 JavaScript 的人可以将其修改为 return table 预清理。

CREATE OR REPLACE FUNCTION AMORTIZATIONTABLE("AMOUNTFINANCED" FLOAT, "INTEREST" FLOAT, "PERIODS" FLOAT)
    RETURNS STRING
    LANGUAGE javascript
    AS $$
    
    const annuity = (AMOUNTFINANCED, INTEREST, PERIODS) => AMOUNTFINANCED * (INTEREST / (1 - (1 + INTEREST)**(-PERIODS)));
    
    const balance_t = (AMOUNTFINANCED, INTEREST, P) => {
        const period_movements = {
            base: AMOUNTFINANCED
        }
        
        period_movements.interest = AMOUNTFINANCED * INTEREST;
        period_movements.amortization = P - (AMOUNTFINANCED * INTEREST);
        period_movements.annuity = P;
        period_movements.final_value = Math.round((AMOUNTFINANCED - period_movements.amortization) * 100) / 100;
    
        return period_movements;
    }
    
    const display_mortgage = (AMOUNTFINANCED, INTEREST, PERIODS) => {
        var data = [];
        const payements = annuity(AMOUNTFINANCED, INTEREST, PERIODS);
        let movements = balance_t(AMOUNTFINANCED, INTEREST, payements);
        while (movements.final_value > -.01) {
            data.push(movements);
            movements = balance_t(movements.final_value, INTEREST, payements);
        }       
        
        return data;
    }
    
    data2 = display_mortgage(AMOUNTFINANCED, INTEREST, PERIODS);

    return JSON.stringify(data2);

    $$;



SELECT
    INDEX + 1 AS Period,
    a.VALUE:base AS CurrPrincipalBal,
    a.VALUE:annuity AS TotalPayment,
    a.VALUE:amortization AS PrincipalPmt,
    a.VALUE:interest AS InterestPmt,
    a.VALUE:final_value AS NewPrincipalBal
FROM
    (SELECT * FROM TABLE(flatten(INPUT => SELECT parse_json(REPLACE(AMORTIZATIONTABLE(20000.00, 0.04, 12.00),'"',''))))) AS a;

所以我想我可以使用 table 生成器来编写这个“清洁器”。

公平地说,我觉得这比递归 CTE 更清晰。

需要注意的是,您需要为我拥有的1000插入“可能的最大贷款期限”。

双月刊是通过计算每月选项之间的天数来完成的,取“一半”,奇数天使用 15 是正常的。

但像那样:

WITH loans_table(loanid, loan_date, loan_amount,
                 maturity_date, payment_frequency, 
                 payment_amount) as (
    SELECT * FROM VALUES 
        ('abc123', '2022-01-01'::date, 250, '2022-03-09'::date, 'biweekly', 50)
), table_of_numbers as (
    SELECT row_number() over(order by null) as rn
    FROM TABLE(generator(ROWCOUNT => 1000))
    /* that 1000 should be larger than any loan perdiod length you have */
), loan_enrich as (
    SELECT 
        *
        ,CASE Payment_frequency 
            WHEN 'weekly'      THEN 7
            WHEN 'biweekly'    THEN 14
            WHEN 'semimonthly' THEN 14
            WHEN 'monthly'     THEN 28
        END as period_lo_days
        ,datediff('day', loan_date, maturity_date) as loan_days
        ,CEIL(loan_days / period_lo_days) as loan_periods
    FROM loans_table
)
SELECT 
    l.loanid,
    CASE payment_frequency 
        WHEN 'weekly' THEN dateadd('week', r.rn, l.loan_date)
        WHEN 'biweekly' THEN dateadd('week', r.rn * 2, l.loan_date)
        WHEN 'semimonthly' THEN 
            case r.rn%2
                when 0 then dateadd('month', r.rn/2, l.loan_date)
                when 1 then dateadd('days', floor(datediff('days', dateadd('month', (r.rn-1)/2, l.loan_date), dateadd('month', (r.rn+1)/2, l.loan_date))/2), dateadd('month', (r.rn-1)/2, l.loan_date))
            end
        WHEN 'monthly' THEN dateadd('month', r.rn, l.loan_date)
    END as payment_date,
    l.payment_amount,
    l.payment_frequency
FROM loan_enrich AS l
JOIN table_of_numbers AS r 
    ON l.loan_periods >= r.rn
ORDER BY 1, r.rn;

给出:

LOANID PAYMENT_DATE PAYMENT_AMOUNT PAYMENT_FREQUENCY
abc123 2022-01-15 50 biweekly
abc123 2022-01-29 50 biweekly
abc123 2022-02-12 50 biweekly
abc123 2022-02-26 50 biweekly
abc123 2022-03-12 50 biweekly

所以这可以提升,让 semimonthly15 总是 15 天后,我们可以做一些过滤,以防行数超过我们需要的,我们可以显示处理最终付款的逻辑比以前的付款少:

WITH loans_table(loanid, loan_date, loan_amount,
                 maturity_date, payment_frequency, 
                 payment_amount) as (
    SELECT * FROM VALUES 
        ('abc123', '2022-01-01'::date, 250, '2022-03-09'::date, 'biweekly', 50),
        ('abc124', '2022-01-01'::date, 249, '2022-03-09'::date, 'semimonthly', 50),
        ('abc125', '2022-01-01'::date, 249, '2022-03-09'::date, 'semimonthly15', 50)
), table_of_numbers as (
    SELECT row_number() over(order by null) as rn
    FROM TABLE(generator(ROWCOUNT => 1000))
    /* that 1000 should be larger than any loan perdiod length you have */
), loan_enrich as (
    SELECT 
        *
        ,CASE Payment_frequency 
            WHEN 'weekly'      THEN 7
            WHEN 'biweekly'    THEN 14
            WHEN 'semimonthly' THEN 14
            WHEN 'semimonthly15' THEN 14
            WHEN 'monthly'     THEN 28
        END as period_lo_days
        ,datediff('day', loan_date, maturity_date) as loan_days
        ,CEIL(loan_days / period_lo_days) as loan_periods
    FROM loans_table
)
SELECT 
    l.loanid,
    CASE payment_frequency 
        WHEN 'weekly' THEN dateadd('week', r.rn, l.loan_date)
        WHEN 'biweekly' THEN dateadd('week', r.rn * 2, l.loan_date)
        WHEN 'semimonthly' THEN 
            case r.rn%2
                when 0 then dateadd('month', r.rn/2, l.loan_date)
                when 1 then dateadd('days', floor(datediff('days', dateadd('month', (r.rn-1)/2, l.loan_date), dateadd('month', (r.rn+1)/2, l.loan_date))/2), dateadd('month', (r.rn-1)/2, l.loan_date))
            end
        WHEN 'semimonthly15' THEN 
            case r.rn%2
                when 0 then dateadd('month', r.rn/2, l.loan_date)
                when 1 then dateadd('days', 15, dateadd('month', (r.rn-1)/2, l.loan_date))
            end

        WHEN 'monthly' THEN dateadd('month', r.rn, l.loan_date)
    END as payment_date,
    l.payment_amount,
    l.payment_frequency,
    l.loan_amount,
    l.loan_amount - least(l.loan_amount, l.payment_amount * r.rn) as still_to_pay,
    least(l.loan_amount - least(l.loan_amount, l.payment_amount * (r.rn-1)), l.payment_amount) as this_payment
FROM loan_enrich AS l
JOIN table_of_numbers AS r 
    ON l.loan_periods >= r.rn
WHERE this_payment > 0
ORDER BY 1, r.rn
LOANID PAYMENT_DATE PAYMENT_AMOUNT PAYMENT_FREQUENCY LOAN_AMOUNT STILL_TO_PAY THIS_PAYMENT
abc123 2022-01-15 50 biweekly 250 200 50
abc123 2022-01-29 50 biweekly 250 150 50
abc123 2022-02-12 50 biweekly 250 100 50
abc123 2022-02-26 50 biweekly 250 50 50
abc123 2022-03-12 50 biweekly 250 0 50
abc124 2022-01-16 50 semimonthly 249 199 50
abc124 2022-02-01 50 semimonthly 249 149 50
abc124 2022-02-15 50 semimonthly 249 99 50
abc124 2022-03-01 50 semimonthly 249 49 50
abc124 2022-03-16 50 semimonthly 249 0 49
abc125 2022-01-16 50 semimonthly15 249 199 50
abc125 2022-02-01 50 semimonthly15 249 149 50
abc125 2022-02-16 50 semimonthly15 249 99 50
abc125 2022-03-01 50 semimonthly15 249 49 50
abc125 2022-03-16 50 semimonthly15 249 0 49