Linux 财务计算程序
金融领域充满了数学公式。我们都熟悉一些基本的公式,比如复利公式,或者计算在一段时间内每月存入一定金额的储蓄账户的价值。这些公式相当简单,通常用计算器就能处理。贷款摊销及其逆运算,即偿债基金,稍微复杂一些,但即使是这些类型的金融问题,用计算器或简单的计算机程序也能解决。
在本文中,我想探讨一个更困难的问题,通常被称为寻找内部收益率 (IRR)。简单来说,如果我们多次在不同时间进行支出,并在不同时间收到多笔付款,那么实际利率是多少?由于时间间隔不一定是固定的,因此上面提到的简单公式是不够用的。事实上,摊销,如汽车贷款和房屋贷款,是 IRR 的一种特殊情况,在这种情况下,付款和时间间隔都是恒定的。例如,一个标准的四年期汽车贷款,每月固定还款,实际上是一个 IRR 问题,您在时间零收到一笔总额(贷款价值),并在 48 个月内分期偿还。这些分期付款恰好相等,并且恰好发生在相等的时间间隔内,即每月。
我们将首先推导出 IRR 的方程,然后用 Perl、C 和 Java 三种等效程序来解决它。这样您就可以比较同一种算法在三种不同语言中的实现。我发现这是一种学习计算机语言的有趣且有效的方式。作为额外的福利,我们将在每个程序中处理离散和连续复利的情况。当我们完成时,您将拥有一个非常通用的程序(实际上是三个程序),它可以处理几乎所有金融利率问题。
处理货币问题的诀窍是理解一个叫做“货币的时间价值”的概念。如果有人同意在一年后给您 100 美元,那么从财务角度来看,这个承诺的价值低于 100 美元。而且,如果承诺在两年后支付 100 美元,它的价值就更低了。相反,如果您同意在一年后给别人 100 美元,您现在可以用少于这个数额的钱开始,让您的钱赚取一年的利息,然后再支付出去。人们愿意为能够更早而不是更晚地拥有东西而支付溢价,这种想法导致了利息和货币时间价值的概念。
现在让我们只考虑离散复利,并进一步将讨论限制为年度复利,比如 8%。我们投资 100 美元一年后增长到 108 美元。再投资一年,我们就有了 116.64 美元。因此,我们可以看到,确定以利率 i% 投资 t 年的 D 美元的价值 V 的通用公式是
V = D * (1 + i / 100) t
或者,在我们的例子中,
V = 100 * (1.08) t一般来说,我们乘以 (1 + i / 100) t 来计算未来价值,除以相同的因子来计算过去价值。这就解释了为什么以及彩票机构可以让您选择以未来时间间隔的一系列支票或现在的总额来领取奖金。总额是通过将每张支票按上述因子(为每个时间间隔单独计算)折算,然后将结果金额相加得出的。当然,总额总是小于所有这些支票的总和,这就是您为了现在而不是以后拥有这笔钱而牺牲的金额。在开发完我们的程序后,我们稍后会回到彩票的话题。
现在我们可以陈述一条规则,该规则支配着在一段时间内发生的金融交易:所有交易的总和,使用 IRR 校正到同一(任何)时间,必须总计为零。如果您仔细思考一下,就会发现这完全有道理,因为如果利率为零,所有的校正因子都将为 1,而这条规则只不过是陈述了一个简单的事实,即支出必须等于收入。利息通过引入那些令人讨厌的校正因子来解释货币随时间的增长,从而使问题复杂化(没有双关语的意思)。
首先,交易价值必须是有符号的,流入通常为正,流出为负(尽管一致地颠倒符号不会影响最终的 IRR)。让我们举一个简单的例子。您在银行存入 100 美元,一年后取出 40 美元,两年后取出 70 美元。将所有交易校正到现在,IRR 是满足以下方程的 i 值
-100 + 40 / (1 + i / 100) + 70 / ((1 + i / 100) 2) = 0
我将留给读者来解决这个问题,提示是定义 x = (1 + i / 100) 并首先求解 x,然后求解 i。恭喜,如果您得到 i = 6.02%。您可能解了一个二次方程,如果是这样,您可能已经注意到二次方程的原因——三个不同的交易:现在、一年后和两年后。随着时间段数量的增加,这类问题变得越来越难,这为我们的程序提供了动力。
在我介绍解决金融问题的算法之前,让我们扩大我们的范围,不仅包括离散复利,还包括连续复利。事实证明,这非常容易。我们的因子变成了指数而不是幂,所以 (1 + i /100) t 被 exp(i * t / 100) 取代。连续复利的货币增长速度快于相同利率和相同期限的离散复利货币,因此,对于相同的离散问题,连续复利的 IRR 小于离散复利的 IRR。您可能希望重新计算我们的 100 美元银行账户问题的连续复利。这个问题的 IRR 是 5.85%。
让我们就一个简单的输入格式达成一致,每行一个美元、时间对,美元和时间都使用浮点数以获得完全的通用性。对于我们的小银行账户问题,输入文件是
-100 0.0 40 1.0 70 2.0
注意投资的负号。
我选择牛顿法来解决这个问题,主要是因为我知道问题中的幂及其导数是表现良好的函数。然而,更重要的是,我们掌握了 IRR。利率通常在 3% 到 20% 范围内,这使我们能够选择一个起始值,对于大多数现实世界的问题来说,这将确保收敛。
最初,我使用了一种直接的算法,将牛顿法直接应用于 IRR 方程。虽然我成功了,但我注意到,对于某些数据集,所需的迭代次数非常多,令人不安。我绘制了 IRR 方程与利率的关系图,并很快发现了问题。牛顿法的简单性归功于忽略了线性项以外的所有项,因此,当高阶项相对较小时,它的效果最好。另一种说法是,函数越接近直线,线性牛顿法的收敛速度就越快。
我的图表显示 IRR 函数的曲率很大,所以我尝试将方程转换为“更平坦”的形式。诀窍是将时间校正项收集到单独的负数(支出)和正数(收入)组中。如果我们将这些和表示为 pos_sum 和 neg_sum,我们得到等式
pos_sum = neg_sum
将方程两边同时除以 pos_sum 并取结果的对数,我们得到
ln(neg_sum / pos_sum) = 0对数使函数变平坦,并实现非常快速的收敛。计算对数的轻微权衡被迭代次数的减少所弥补。我做了以下替换
exp(-u) = 1 + i并首先求解 u,然后求解 i,这进一步简化了计算。
可以从 ftp://ftp.linuxjournal.com/pub/lj/listings/issue48/2545.tgz 匿名下载三个列表:列表 1 是 Perl 程序 irr.pl;列表 2 是相同的 C 程序 irr.c;列表 3 irr.java 是等效的 Java 代码。由于篇幅限制,此处仅打印 Perl 代码(见列表 1)。
让我们先看一下 Perl 程序。它是三个程序中最短的,部分原因是 Perl 的许多内置函数,部分原因是 Perl 的内置内存管理。如果用户忘记了预期的格式,它会显示一个简单的用法消息并退出。有三个常数,一个用于控制在退出牛顿循环之前的最大迭代次数,一个用于确定是否发生收敛,以及 IRR 的起始值 (u = i = 0.0)。
数据文件在一个循环中读取,使用数组 @pos_d、@pos_t、@neg_d 和 @neg_t 来保存收入和支出及其各自的时间。请注意,Perl 在读取数据时动态地为数组分配内存。write 函数使用程序末尾的格式执行格式化输出。在启动牛顿算法循环之前,我们确保输入数据同时包含收入和支出——除非这样做,否则算法将不会收敛。
变量 $iters 上的 for 循环是牛顿算法,其中 $d_neg 和 $d_pos 分别是分子和分母对 u 的导数。如果达到收敛,程序将以 IRR 的显示结束。请注意,标量变量以美元符号开头,数组以 @ 符号开头。变量和数组没有类型,可以包含字符串或数字,类型由用法决定。
请注意,Perl 的 print 函数接受变量名并插入实际值,而 printf 的工作方式与其 C 语言对应物非常相似。此外,您可以将“if”测试放在它们控制的语句之后。
作为一个非平凡的例子,请考虑列表 2 中的数据集。在本例中,您今天花费 1000 美元,一年后收回 500 美元,但两年后又花费 2000 美元,等等,总共有六笔交易分布在五年内。我们可以通过键入以下内容使用此数据集运行 Perl 程序
irr.pl irr.dat
这会导致显示输入数据,然后是以下行
IRR = 10.6952% (discrete) = 10.1611% (continuous) after 3 iterations.请注意,当复利是离散的时,需要更高的利率才能获得与连续复利相同的回报。当然,您将从其他两个程序中获得相同的结果。
C 程序比其 Perl 等效程序更长——大约是其两倍长。在 C 程序中,我选择为美元和时间对定义一个结构。一个复杂之处——我必须扫描输入数据两次:第一次确定对的数量,以便我可以使用 malloc 为结构数组分配内存,第二次实际将数据加载到内存中。在这个程序中,我将牛顿算法放在一个名为 irr 的静态函数中。此函数是一种定义的类型 ITERATOR,并根据算法是否收敛返回 OK 或 NO_CONVERGE。迭代函数看起来非常像等效的 Perl 代码。
Java 程序的 IRR 更长,部分原因是异常检查,但也因为除了我的公共 Irr 类之外,我还定义了几个类。Java 中没有结构,所以我将输入数据放入其自己的 Expenses 类中。我使用了接口 Do_irr_returns 来保存返回值,作为与 C 语言中用于相同目的的枚举的等价物。将程序组织成类和接口的这种方式倾向于增加代码量,但会产生清晰且易于阅读的程序。MyDouble 和 MyInt 类也服务于另一个目的。
在 Java 中,本机变量按值传递。至少对于单个内置变量而言,没有按地址传递的概念。解决此限制的一种方法是创建类作为包装器,然后将类传递给方法。MyDouble 类被赋予了一个构造函数,以便我们可以为 du 设置初始值。
请注意,在这种情况下,没有理由扫描输入数据两次。类型为 Vector 的变量 in_data 在添加元素时会自动增长——无需分配或重新分配内存。
我联系了我在科罗拉多州的州彩票机构,并提出了一个很好的 IRR 问题。如果您中了彩票,您可以选择领取相当于您奖金 40% 的总额,或者分 25 年支付,从您奖金的 1/40 开始,然后是每年增加 3.7% 的支票。例如,如果您赢得了一百万美元,您可以带走一张 400,000 美元的支票(扣除联邦和州预扣税,分别为 28% 和 4%),或者每年领取付款,从 25,000 美元开始,然后是 25,925 美元,等等,直到最后一张支票 59,790.22 美元(预扣税也适用于这些支票)。将这 25 张支票加起来,您确实会得到一百万美元。(对于您这些纯粹主义者,或者如果您只是喜欢计算您的便士,您会注意到确切的总额是 1,000,066.48 美元。每年的真实增长略低于 3.7%。)每位彩票中奖者面临的问题是,领取总额还是在一段时间内分期付款更好。从严格的财务角度来看,一旦确定了 IRR,问题就解决了。也就是说,您必须以什么利率投资 400,000 美元才能给自己开出相同的 25 张支票?答案将在我的下一篇文章中揭晓。
我们研究了确定一系列任意金额和任意时间发生的支出和回报的实际利率的问题。我们推导出了内部收益率的方程,并将其转化为适合应用牛顿法的形式。我们研究了 Perl、C 和 Java 中的等效程序。您可以使用这些程序来求解利率,并比较任何涉及时间交易的问题的投资。
自 1967 年以来,Jim 一直在编程,使用 Linux 大约三年了。他使用 Perl、Java 和 C 来处理他专业生活中的电信和 GIS 应用程序,以及他业余生活中的数论和密码学问题。他一如既往地努力使他的壁球和编程技能相称。可以通过电子邮件 jnshapi@easthub.mnet.uswest.com 联系他。