关于solidity中数值的复利计算教程-第一节

 介   绍

在上一篇文章中,我们讨论了百分比以及如何在Solidity中对其进行计算。在金融数学中,百分比通常与借贷和存款利息有关。在每个时间段(例如一个月或一年)结束时,本金的一定百分比将支付给贷方或存款持有人。这种模式称为单利,每个期间支付的百分比称为定期利率。

在计算机程序中,通常使用利率代替利率。例如对于3%的利率,比率为0.03。因此可以将一个时期的利息支付金额计算为利率乘以本金金额,从上一篇文章中我们已经知道如何在Solidity中有效而精确地做到这一点。

简单的利率模式很简单,但是如果不立即将利息支付给贷方或存款持有人,而是将其加到本金上,情况就会变得更加复杂。在这种情况下,过去期间累积的利息会影响将来收取的利息。

在本文中,我们讨论了如何在Solidity中实现此模式,该模式的名称为:复利。

定期复利

我们已经知道如何计算单利。计算复利的直接方法是在每个时间段末计算单利,然后将计算出的利加到本金上。在高级语言(例如JavaScript)中,它看起来像这样:

principal += ratio * principal; // Do after each time period

这里的ratio几乎绝对是分数,但是Solidity不支持分数,因此在Solidity中,我们应该这样写:

principal += mulDiv (ratio, principal, 10^18);

我们使用上一篇文章中的mulDiv函数,并假定ratio是一个定点数,点后有18个小数。

上面的代码在大多数情况下都可以使用,但是其+ =操作可能会溢出,因此为了确保代码的安全性,我们需要像这样进行更改:

principal = add (principal, mulDiv (ratio, principal, 10^18));

在本文中,为简单起见,我们将使用普通的算术运算,就像Solidity支持小数并且算术运算不会溢出一样。在实际代码中,这些操作将被适当的功能取代。

一旦我们知道如何在单个期间内增加利息,问题就是:

我们如何在每个时间段结束时触发复利?

与传统应用程序不同,智能合约不能具有任何后台活动。智能合约的字节码仅在交易直接或通过另一个智能合约调用合约时才执行。人们可能会依赖诸如Provable(以前称为Oraclize)之类的第三方服务来定期调用特定的智能合约,或者可能会从经济上激励普通人这样做。

这种方法有效,但是有许多缺点。首先有人必须支付gas费,因此它不是免费的。其次即使每个人在接下来的时间段内都无法访问更新的本金,也必须在每个周期结束时增加利息。第三时间周期越短,必须执行的配混越频繁,因此消耗的gas越多。第四此方法在较短的时间内不准确,因为事务挖掘时间不可预测,并且在网络负载较高时可能会很大。

因此,如果在每个时期的末尾进行复利不是Solidity的好主意,则

我们什么时候应该加息?

更好的方法是只在有人需要获取本金或债务或存款的情况下才进行复利,并在此期间对所有结束的时间段进行复利,而不是在每个时间段末复利。最后一次:

uint currentPeriod = block.timestamp / periodLength;


for (uint period = lastPeriod; period < currentPeriod; period++)


  principal += ratio * principal;


lastPeriod = currentPeriod;

此代码将所有尚未复利的利息加到本金上,并且每次有人要访问本金时都必须执行。这种方法被称为“惰性”复合,实际计算被推迟到有人真正需要它们的结果之前。

但是上面显示的“惰性”混合的实现存在一个重要问题。实际的gas消耗量线性地取决于自上次执行利息混合以来经过了多少时间间隔。如果时间段很短,或者上一次进行复利很长时间,则在所有经过的时间段内复利所需的燃气量可能会超过区块gas限额,从而实际上无法进行进一步的复利。所以问题是:

如何更有效地进行“惰性”复合?

首先我们注意到,单个时间段内的复利可能会被这样重写:

principal *= 1 + ratio;

对于两个时间间隔,这将是:

principal *= (1 + ratio) * (1 + ratio);

然后我们注意到(1 + r)²= 1 +(2r +r²),因此双时间间隔的有效利率为2r +r²,其中r是单时间间隔的利率。如果我们要引起兴趣的时间间隔数是偶数,我们可以通过将时间间隔持续时间加倍来将时间间隔数减半。当时间间隔的数量为奇数时,我们可能只执行一次复利,从而使剩余的时间间隔数量为偶数。这是代码:

function compound (uint principal, uint ratio, uint n)


public pure returns (uint) {


  while (n > 0) {


    if (n % 2 == 1) {


      principal += principal * ratio;


      n -= 1;


    } else {


      ratio = 2 * ratio + ratio * ratio;


      n /= 2;


    }


  }


  return principal;


}

上面的代码具有对数复杂度,并且当本金和比率较大时效果很好,因此,principal * ratio乘积具有足够的有效小数,以实现较高的精度。但是如果principal和ratio较小,则上面的代码可能会产生不正确的结果。现在的问题是:

如何提高延迟复利的精度?

在上面显示的代码中,精度在以下单独代码中:

principal += principal * ratio;

这是因为我们假设主体是整数,所以赋值必须舍入计算值。舍入可能会多次执行,并且舍入误差会加起来。

为了解决这个问题,我们可能会注意到,在n个时间间隔内,利息可能会像这样复杂:

principal *= (1 + ratio) ** n;

如果Solidity支持分数,则此代码将起作用,但只要不支持,我们就需要自己实现幂运算。我们使用与上一部分相同的对数复杂度方法,因此代码非常相似:

function pow (uint x, uint n)


public pure returns (uint r) {


  r = 1.0;


  while (n > 0) {


    if (n % 2 == 1) {


      r *= x;


      n -= 1;


    } else {


      x *= x;


      n /= 2;


    }


  }


}


function compound (uint principal, uint ratio, uint n)


public pure returns (uint) {


  return principal * pow (1 + ratio, n);


}

注意表达式:r = 1.0。这里要记住,我们在这里处理分数时,好像Solidity确实支持它们,而实际上却不支持。人们将不得不用实现分数数学的函数来代替所有算术运算。例如这是使用ABDK Math 64.64库的真实代码的外观,该库为64.64位定点数实现算术运算:

function pow (int128 x, uint n)


public pure returns (int128 r) {


  r = ABDKMath64x64.fromUInt (1);


  while (n > 0) {


    if (n % 2 == 1) {


      r = ABDKMath64x64.mul (r, x);


      n -= 1;


    } else {


      x = ABDKMath64x64.mul (x, x);


      n /= 2;


    }


  }


}


function compound (uint principal, uint ratio, uint n)


public pure returns (uint) {


  return ABDKMath64x64.mulu (


    pow (


      ABDKMath64x64.add (


        ABDKMath64x64.fromUInt (1), 


        ABDKMath64x64.divu (


          ratio,


          10**18)),


      n),


    principal);


}

实际上,该库已经具有pow函数,可以使用它代替我们的实现。

上面的代码非常精确和直接,但是仅适用于离散时间间隔。如果我们需要在任意时间间隔内增加利息怎么办?这种模式被称为

连续复利

连续复利的想法是计算任意(而不是固定)时间段的利息。实现此目的的一种方法是使用小数个周期。我们已经知道如何计算n个周期的复利:

principal *= (1 + ratio) ** n; 

假设时间段为一年,并且我们要计算1个月的复利,即一年的1/12。那么公式应为:

principal *= (1 + ratio) ** (1 / 12);

不幸的是,以上所示的实体性和pow函数均不支持分数指数。我们可以通过整数幂和根或通过固定基数对数和指数来实现它们,但是

有没有更简单的方法来进行连续复合?

现实世界中的时间是连续的,或者至少看起来是这样。以太坊中的时间是离散的。它以秒为单位,用整数表示。因此以1秒为周期进行定期复利就可以连续进行复利,因为没人会在周期的中间观察到本金。

乍一看,每秒复利的想法可能看起来很奇怪,但是在以太坊上,它的效果令人惊讶。3%的年利率实际上等效于0.000000093668115524%的每秒利率或0.000000000936681155每秒钟用18个小数表示的利率。在这里,我们假设1年的时间为31556952秒。

使用上述功能复合1年(31556952个周期),该比率得出2.99999999895%的年利率,因此精度几乎达到10个有效数字。对于大多数应用程序来说已经足够了。使用128.128位定点数而不是64.64位或什至浮点数可以实现更高的精度。

在我们的实验中,复合1年的定期每秒利息消耗了大约90Kgas。对于大多数应用来说,这可能是可以承受的,但总的来说是很高的。在我们的下一篇文章中,我们将介绍提供几乎相同精度的更便宜的方法。

结    论

复杂的分数计算(例如复合定期利率所需的分数计算)由于缺乏本地分数数字支持而可能对Solidity造成挑战。

但是使用平方算法的幂运算和模拟的定点数仍然可以有效地计算复利。

建议的方法功能强大,足以在1年(甚至更长)的时间跨度内提高每秒的利率。然而这种方法相当耗gas。

在下一篇文章中,我们将介绍更好的方法,而下一个主题将是:指数和对数。


关键词: solidity  复利计算  

该内容来自于互联网公开内容,非区块链原创内容,如若转载,请注明出处:https://htzkw.com/archives/28093

联系我们

aliyinhang@gmail.com