ref #65: new formula function ODDFPRICE
This commit is contained in:
parent
e0c6fa1beb
commit
7af55a5455
277
calc.go
277
calc.go
|
@ -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:
|
||||
|
|
29
calc_test.go
29
calc_test.go
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue