ref #65: new formula function ODDFPRICE

This commit is contained in:
xuri 2021-12-06 08:16:32 +08:00
parent e0c6fa1beb
commit 7af55a5455
No known key found for this signature in database
GPG Key ID: BA5E5BB1C948EDF7
2 changed files with 302 additions and 4 deletions

277
calc.go
View File

@ -504,6 +504,7 @@ type formulaFuncs struct {
// OCT2DEC
// OCT2HEX
// ODD
// ODDFPRICE
// OR
// PDURATION
// PERCENTILE.EXC
@ -9849,10 +9850,8 @@ func (fn *formulaFuncs) COUPNUM(argsList *list.List) formulaArg {
if args.Type != ArgList {
return args
}
maturity, dateValue := timeFromExcelTime(args.List[1].Number, false), fn.COUPPCD(argsList)
date := timeFromExcelTime(dateValue.Number, false)
months := (maturity.Year()-date.Year())*12 + int(maturity.Month()) - int(date.Month())
return newNumberFormulaArg(float64(months) * args.List[2].Number / 12.0)
frac := yearFrac(args.List[0].Number, args.List[1].Number, 0)
return newNumberFormulaArg(math.Ceil(frac.Number * args.List[2].Number))
}
// COUPPCD function returns the previous coupon date, before the settlement
@ -10748,6 +10747,276 @@ func (fn *formulaFuncs) NPV(argsList *list.List) formulaArg {
return newNumberFormulaArg(val)
}
// aggrBetween is a part of implementation of the formula function ODDFPRICE.
func aggrBetween(startPeriod, endPeriod float64, initialValue []float64, f func(acc []float64, index float64) []float64) []float64 {
s := []float64{}
if startPeriod <= endPeriod {
for i := startPeriod; i <= endPeriod; i++ {
s = append(s, i)
}
} else {
for i := startPeriod; i >= endPeriod; i-- {
s = append(s, i)
}
}
return fold(f, initialValue, s)
}
// fold is a part of implementation of the formula function ODDFPRICE.
func fold(f func(acc []float64, index float64) []float64, state []float64, source []float64) []float64 {
length, value := len(source), state
for index := 0; length > index; index++ {
value = f(value, source[index])
}
return value
}
// changeMonth is a part of implementation of the formula function ODDFPRICE.
func changeMonth(date time.Time, numMonths float64, returnLastMonth bool) time.Time {
offsetDay := 0
if returnLastMonth && date.Day() == getDaysInMonth(date.Year(), int(date.Month())) {
offsetDay--
}
newDate := date.AddDate(0, int(numMonths), offsetDay)
if returnLastMonth {
lastDay := getDaysInMonth(newDate.Year(), int(newDate.Month()))
return timeFromExcelTime(daysBetween(excelMinTime1900.Unix(), makeDate(newDate.Year(), newDate.Month(), lastDay))+1, false)
}
return newDate
}
// datesAggregate is a part of implementation of the formula function
// ODDFPRICE.
func datesAggregate(startDate, endDate time.Time, numMonths, basis float64, f func(pcd, ncd time.Time) float64, acc float64, returnLastMonth bool) (time.Time, time.Time, float64) {
frontDate, trailingDate := startDate, endDate
s1 := frontDate.After(endDate) || frontDate.Equal(endDate)
s2 := endDate.After(frontDate) || endDate.Equal(frontDate)
stop := s2
if numMonths > 0 {
stop = s1
}
for !stop {
trailingDate = frontDate
frontDate = changeMonth(frontDate, numMonths, returnLastMonth)
fn := f(frontDate, trailingDate)
acc += fn
s1 = frontDate.After(endDate) || frontDate.Equal(endDate)
s2 = endDate.After(frontDate) || endDate.Equal(frontDate)
stop = s2
if numMonths > 0 {
stop = s1
}
}
return frontDate, trailingDate, acc
}
// coupNumber is a part of implementation of the formula function ODDFPRICE.
func coupNumber(maturity, settlement, numMonths, basis float64) float64 {
maturityTime, settlementTime := timeFromExcelTime(maturity, false), timeFromExcelTime(settlement, false)
my, mm, md := maturityTime.Year(), maturityTime.Month(), maturityTime.Day()
sy, sm, sd := settlementTime.Year(), settlementTime.Month(), settlementTime.Day()
couponsTemp, endOfMonthTemp := 0.0, getDaysInMonth(my, int(mm)) == md
endOfMonth := endOfMonthTemp
if !endOfMonthTemp && mm != 2 && md > 28 && md < getDaysInMonth(my, int(mm)) {
endOfMonth = getDaysInMonth(sy, int(sm)) == sd
}
startDate := changeMonth(settlementTime, 0, endOfMonth)
coupons := couponsTemp
if startDate.After(settlementTime) {
coupons++
}
date := changeMonth(startDate, numMonths, endOfMonth)
f := func(pcd, ncd time.Time) float64 {
return 1
}
_, _, result := datesAggregate(date, maturityTime, numMonths, basis, f, coupons, endOfMonth)
return result
}
// prepareOddfpriceArgs checking and prepare arguments for the formula
// function ODDFPRICE.
func (fn *formulaFuncs) prepareOddfpriceArgs(argsList *list.List) formulaArg {
dateValues := fn.prepareDataValueArgs(4, argsList)
if dateValues.Type != ArgList {
return dateValues
}
settlement, maturity, issue, firstCoupon := dateValues.List[0], dateValues.List[1], dateValues.List[2], dateValues.List[3]
if issue.Number >= settlement.Number {
return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires settlement > issue")
}
if settlement.Number >= firstCoupon.Number {
return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires first_coupon > settlement")
}
if firstCoupon.Number >= maturity.Number {
return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires maturity > first_coupon")
}
rate := argsList.Front().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
if rate.Type != ArgNumber {
return rate
}
if rate.Number < 0 {
return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires rate >= 0")
}
yld := argsList.Front().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
if yld.Type != ArgNumber {
return yld
}
if yld.Number < 0 {
return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires yld >= 0")
}
redemption := argsList.Front().Next().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
if redemption.Type != ArgNumber {
return redemption
}
if redemption.Number <= 0 {
return newErrorFormulaArg(formulaErrorNUM, "ODDFPRICE requires redemption > 0")
}
frequency := argsList.Front().Next().Next().Next().Next().Next().Next().Next().Value.(formulaArg).ToNumber()
if frequency.Type != ArgNumber {
return frequency
}
if !validateFrequency(frequency.Number) {
return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
basis := newNumberFormulaArg(0)
if argsList.Len() == 9 {
if basis = argsList.Back().Value.(formulaArg).ToNumber(); basis.Type != ArgNumber {
return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
}
return newListFormulaArg([]formulaArg{settlement, maturity, issue, firstCoupon, rate, yld, redemption, frequency, basis})
}
// ODDFPRICE function calculates the price per $100 face value of a security
// with an odd (short or long) first period. The syntax of the function is:
//
// ODDFPRICE(settlement,maturity,issue,first_coupon,rate,yld,redemption,frequency,[basis])
//
func (fn *formulaFuncs) ODDFPRICE(argsList *list.List) formulaArg {
if argsList.Len() != 8 && argsList.Len() != 9 {
return newErrorFormulaArg(formulaErrorVALUE, "ODDFPRICE requires 8 or 9 arguments")
}
args := fn.prepareOddfpriceArgs(argsList)
if args.Type != ArgList {
return args
}
settlement, maturity, issue, firstCoupon, rate, yld, redemption, frequency, basisArg :=
args.List[0], args.List[1], args.List[2], args.List[3], args.List[4], args.List[5], args.List[6], args.List[7], args.List[8]
if basisArg.Number < 0 || basisArg.Number > 4 {
return newErrorFormulaArg(formulaErrorNUM, "invalid basis")
}
issueTime := timeFromExcelTime(issue.Number, false)
settlementTime := timeFromExcelTime(settlement.Number, false)
maturityTime := timeFromExcelTime(maturity.Number, false)
firstCouponTime := timeFromExcelTime(firstCoupon.Number, false)
basis := int(basisArg.Number)
monthDays := getDaysInMonth(maturityTime.Year(), int(maturityTime.Month()))
returnLastMonth := monthDays == maturityTime.Day()
numMonths := 12 / frequency.Number
numMonthsNeg := -numMonths
mat := changeMonth(maturityTime, numMonthsNeg, returnLastMonth)
pcd, _, _ := datesAggregate(mat, firstCouponTime, numMonthsNeg, basisArg.Number, func(d1, d2 time.Time) float64 {
return 0
}, 0, returnLastMonth)
if !pcd.Equal(firstCouponTime) {
return newErrorFormulaArg(formulaErrorNUM, formulaErrorNUM)
}
fnArgs := list.New().Init()
fnArgs.PushBack(settlement)
fnArgs.PushBack(maturity)
fnArgs.PushBack(frequency)
fnArgs.PushBack(basisArg)
e := fn.COUPDAYS(fnArgs)
n := fn.COUPNUM(fnArgs)
m := frequency.Number
dfc := coupdays(issueTime, firstCouponTime, basis)
if dfc < e.Number {
dsc := coupdays(settlementTime, firstCouponTime, basis)
a := coupdays(issueTime, settlementTime, basis)
x := yld.Number/m + 1
y := dsc / e.Number
p1 := x
p3 := math.Pow(p1, n.Number-1+y)
term1 := redemption.Number / p3
term2 := 100 * rate.Number / m * dfc / e.Number / math.Pow(p1, y)
f := func(acc []float64, index float64) []float64 {
return []float64{acc[0] + 100*rate.Number/m/math.Pow(p1, index-1+y)}
}
term3 := aggrBetween(2, math.Floor(n.Number), []float64{0}, f)
p2 := rate.Number / m
term4 := a / e.Number * p2 * 100
return newNumberFormulaArg(term1 + term2 + term3[0] - term4)
}
fnArgs.Init()
fnArgs.PushBack(issue)
fnArgs.PushBack(firstCoupon)
fnArgs.PushBack(frequency)
nc := fn.COUPNUM(fnArgs)
lastCoupon := firstCoupon.Number
aggrFunc := func(acc []float64, index float64) []float64 {
lastCouponTime := timeFromExcelTime(lastCoupon, false)
earlyCoupon := daysBetween(excelMinTime1900.Unix(), makeDate(lastCouponTime.Year(), time.Month(float64(lastCouponTime.Month())+numMonthsNeg), lastCouponTime.Day())) + 1
earlyCouponTime := timeFromExcelTime(earlyCoupon, false)
nl := e.Number
if basis == 1 {
nl = coupdays(earlyCouponTime, lastCouponTime, basis)
}
dci := coupdays(issueTime, lastCouponTime, basis)
if index > 1 {
dci = nl
}
startDate := earlyCoupon
if issue.Number > earlyCoupon {
startDate = issue.Number
}
endDate := lastCoupon
if settlement.Number < lastCoupon {
endDate = settlement.Number
}
startDateTime := timeFromExcelTime(startDate, false)
endDateTime := timeFromExcelTime(endDate, false)
a := coupdays(startDateTime, endDateTime, basis)
lastCoupon = earlyCoupon
dcnl := acc[0]
anl := acc[1]
return []float64{dcnl + dci/nl, anl + a/nl}
}
ag := aggrBetween(math.Floor(nc.Number), 1, []float64{0, 0}, aggrFunc)
dcnl, anl := ag[0], ag[1]
dsc := 0.0
fnArgs.Init()
fnArgs.PushBack(settlement)
fnArgs.PushBack(firstCoupon)
fnArgs.PushBack(frequency)
if basis == 2 || basis == 3 {
d := timeFromExcelTime(fn.COUPNCD(fnArgs).Number, false)
dsc = coupdays(settlementTime, d, basis)
} else {
d := timeFromExcelTime(fn.COUPPCD(fnArgs).Number, false)
a := coupdays(d, settlementTime, basis)
dsc = e.Number - a
}
nq := coupNumber(firstCoupon.Number, settlement.Number, numMonths, basisArg.Number)
fnArgs.Init()
fnArgs.PushBack(firstCoupon)
fnArgs.PushBack(maturity)
fnArgs.PushBack(frequency)
fnArgs.PushBack(basisArg)
n = fn.COUPNUM(fnArgs)
x := yld.Number/m + 1
y := dsc / e.Number
p1 := x
p3 := math.Pow(p1, y+nq+n.Number)
term1 := redemption.Number / p3
term2 := 100 * rate.Number / m * dcnl / math.Pow(p1, nq+y)
f := func(acc []float64, index float64) []float64 {
return []float64{acc[0] + 100*rate.Number/m/math.Pow(p1, index+nq+y)}
}
term3 := aggrBetween(1, math.Floor(n.Number), []float64{0}, f)
term4 := 100 * rate.Number / m * anl
return newNumberFormulaArg(term1 + term2 + term3[0] - term4)
}
// PDURATION function calculates the number of periods required for an
// investment to reach a specified future value. The syntax of the function
// is:

View File

@ -1443,6 +1443,7 @@ func TestCalcCellValue(t *testing.T) {
// COUPNUM
"=COUPNUM(\"01/01/2011\",\"10/25/2012\",4)": "8",
"=COUPNUM(\"01/01/2011\",\"10/25/2012\",4,0)": "8",
"=COUPNUM(\"09/30/2017\",\"03/31/2021\",4,0)": "14",
// COUPPCD
"=COUPPCD(\"01/01/2011\",\"10/25/2012\",4)": "40476",
"=COUPPCD(\"01/01/2011\",\"10/25/2012\",4,0)": "40476",
@ -1503,6 +1504,14 @@ func TestCalcCellValue(t *testing.T) {
"=NPER(0.06/4,-2000,60000,30000,1)": "52.794773709274764",
// NPV
"=NPV(0.02,-5000,\"\",800)": "-4133.025759323337",
// ODDFPRICE
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": "107.69183025662932",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,1)": "106.76691501092883",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,3)": "106.7819138146997",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,4,4)": "106.77191377246672",
"=ODDFPRICE(\"11/11/2008\",\"03/01/2021\",\"10/15/2008\",\"03/01/2009\",7.85%,6.25%,100,2,1)": "113.59771747407883",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"09/30/2017\",5.5%,3.5%,100,4,0)": "106.72930611878041",
"=ODDFPRICE(\"11/11/2008\",\"03/29/2021\", \"08/15/2008\", \"03/29/2009\", 0.0785, 0.0625, 100, 2, 1)": "113.61826640813996",
// PDURATION
"=PDURATION(0.04,10000,15000)": "10.33803507150765",
// PMT
@ -3013,6 +3022,26 @@ func TestCalcCellValue(t *testing.T) {
// NPV
"=NPV()": "NPV requires at least 2 arguments",
"=NPV(\"\",0)": "strconv.ParseFloat: parsing \"\": invalid syntax",
// ODDFPRICE
"=ODDFPRICE()": "ODDFPRICE requires 8 or 9 arguments",
"=ODDFPRICE(\"\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": "#VALUE!",
"=ODDFPRICE(\"02/01/2017\",\"\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": "#VALUE!",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"\",\"03/31/2017\",5.5%,3.5%,100,2)": "#VALUE!",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"\",5.5%,3.5%,100,2)": "#VALUE!",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",\"\",3.5%,100,2)": "strconv.ParseFloat: parsing \"\": invalid syntax",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,\"\",100,2)": "strconv.ParseFloat: parsing \"\": invalid syntax",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,\"\",2)": "strconv.ParseFloat: parsing \"\": invalid syntax",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,\"\")": "strconv.ParseFloat: parsing \"\": invalid syntax",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"02/01/2017\",\"03/31/2017\",5.5%,3.5%,100,2)": "ODDFPRICE requires settlement > issue",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"02/01/2017\",5.5%,3.5%,100,2)": "ODDFPRICE requires first_coupon > settlement",
"=ODDFPRICE(\"02/01/2017\",\"02/01/2017\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2)": "ODDFPRICE requires maturity > first_coupon",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",-1,3.5%,100,2)": "ODDFPRICE requires rate >= 0",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,-1,100,2)": "ODDFPRICE requires yld >= 0",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,0,2)": "ODDFPRICE requires redemption > 0",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,\"\")": "#NUM!",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,3)": "#NUM!",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/30/2017\",5.5%,3.5%,100,4)": "#NUM!",
"=ODDFPRICE(\"02/01/2017\",\"03/31/2021\",\"12/01/2016\",\"03/31/2017\",5.5%,3.5%,100,2,5)": "invalid basis",
// PDURATION
"=PDURATION()": "PDURATION requires 3 arguments",
"=PDURATION(\"\",0,0)": "strconv.ParseFloat: parsing \"\": invalid syntax",