Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ The following list of indicators are currently supported by this package:

### 💰 Asset Valuation
- [Future Value (FV)](valuation/README.md#Fv)
- [Net Present Value (NPV)](valuation/README.md#Npv)
- [Present Value (PV)](valuation/README.md#Pv)

🧠 Strategies Provided
Expand Down
60 changes: 60 additions & 0 deletions helper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,19 @@ The information provided on this project is strictly for informational purposes
- [func First\[T any\]\(c \<\-chan T, count int\) \<\-chan T](<#First>)
- [func Gcd\(values ...int\) int](<#Gcd>)
- [func Head\[T Number\]\(c \<\-chan T, count int\) \<\-chan T](<#Head>)
- [func Highest\[T Number\]\(c \<\-chan T, w int\) \<\-chan T](<#Highest>)
- [func IncrementBy\[T Number\]\(c \<\-chan T, i T\) \<\-chan T](<#IncrementBy>)
- [func JSONToChan\[T any\]\(r io.Reader\) \<\-chan T](<#JSONToChan>)
- [func JSONToChanWithLogger\[T any\]\(r io.Reader, logger \*slog.Logger\) \<\-chan T](<#JSONToChanWithLogger>)
- [func KeepNegatives\[T Number\]\(c \<\-chan T\) \<\-chan T](<#KeepNegatives>)
- [func KeepPositives\[T Number\]\(c \<\-chan T\) \<\-chan T](<#KeepPositives>)
- [func Last\[T any\]\(c \<\-chan T, count int\) \<\-chan T](<#Last>)
- [func Lcm\(values ...int\) int](<#Lcm>)
- [func Lowest\[T Number\]\(c \<\-chan T, w int\) \<\-chan T](<#Lowest>)
- [func Map\[F, T any\]\(c \<\-chan F, f func\(F\) T\) \<\-chan T](<#Map>)
- [func MapWithPrevious\[F, T any\]\(c \<\-chan F, f func\(T, F\) T, previous T\) \<\-chan T](<#MapWithPrevious>)
- [func MaxSince\[T Number\]\(c \<\-chan T, w int\) \<\-chan T](<#MaxSince>)
- [func MinSince\[T Number\]\(c \<\-chan T, w int\) \<\-chan T](<#MinSince>)
- [func Multiply\[T Number\]\(ac, bc \<\-chan T\) \<\-chan T](<#Multiply>)
- [func MultiplyBy\[T Number\]\(c \<\-chan T, m T\) \<\-chan T](<#MultiplyBy>)
- [func Operate\[A any, B any, R any\]\(ac \<\-chan A, bc \<\-chan B, o func\(A, B\) R\) \<\-chan R](<#Operate>)
Expand All @@ -81,10 +85,12 @@ The information provided on this project is strictly for informational purposes
- [func Skip\[T any\]\(c \<\-chan T, count int\) \<\-chan T](<#Skip>)
- [func SkipLast\[T any\]\(c \<\-chan T, count int\) \<\-chan T](<#SkipLast>)
- [func SliceToChan\[T any\]\(slice \[\]T\) \<\-chan T](<#SliceToChan>)
- [func SlicesReverse\[T any\]\(r \[\]T, i int, f func\(T\) bool\)](<#SlicesReverse>)
- [func Sqrt\[T Number\]\(c \<\-chan T\) \<\-chan T](<#Sqrt>)
- [func Subtract\[T Number\]\(ac, bc \<\-chan T\) \<\-chan T](<#Subtract>)
- [func SyncPeriod\[T any\]\(commonPeriod, period int, c \<\-chan T\) \<\-chan T](<#SyncPeriod>)
- [func Waitable\[T any\]\(wg \*sync.WaitGroup, c \<\-chan T\) \<\-chan T](<#Waitable>)
- [func Window\[T any\]\(c \<\-chan T, f func\(\[\]T, int\) T, w int\) \<\-chan T](<#Window>)
- [type Bst](<#Bst>)
- [func NewBst\[T Number\]\(\) \*Bst\[T\]](<#NewBst>)
- [func \(b \*Bst\[T\]\) Contains\(value T\) bool](<#Bst[T].Contains>)
Expand Down Expand Up @@ -582,6 +588,15 @@ actual := helper.Head(c, 2)
fmt.Println(helper.ChanToSlice(actual)) // [2, 4]
```

<a name="Highest"></a>
## func [Highest](<https://github.com/cinar/indicator/blob/master/helper/highest.go#L11>)

```go
func Highest[T Number](c <-chan T, w int) <-chan T
```

Highest returns a channel that emits the highest value within a sliding window of size w from the input channel c.

<a name="IncrementBy"></a>
## func [IncrementBy](<https://github.com/cinar/indicator/blob/master/helper/increment_by.go#L16>)

Expand Down Expand Up @@ -669,6 +684,15 @@ func Lcm(values ...int) int

Lcm calculates the Least Common Multiple of the given numbers.

<a name="Lowest"></a>
## func [Lowest](<https://github.com/cinar/indicator/blob/master/helper/Lowest.go#L11>)

```go
func Lowest[T Number](c <-chan T, w int) <-chan T
```

Lowest returns a channel that emits the lowest value within a sliding window of size w from the input channel c.

<a name="Map"></a>
## func [Map](<https://github.com/cinar/indicator/blob/master/helper/map.go#L17>)

Expand Down Expand Up @@ -703,6 +727,24 @@ sum := helper.MapWithPrevious(c, func(p, c int) int {
}, 0)
```

<a name="MaxSince"></a>
## func [MaxSince](<https://github.com/cinar/indicator/blob/master/helper/max_since.go#L14>)

```go
func MaxSince[T Number](c <-chan T, w int) <-chan T
```

MaxSince returns a channel of T indicating since when \(number of previous values\) the respective value was the maximum within the window of size w.

<a name="MinSince"></a>
## func [MinSince](<https://github.com/cinar/indicator/blob/master/helper/min_since.go#L13>)

```go
func MinSince[T Number](c <-chan T, w int) <-chan T
```

MinSince returns a channel of T indicating since when \(number of previous values\) the respective value was the minimum.

<a name="Multiply"></a>
## func [Multiply](<https://github.com/cinar/indicator/blob/master/helper/multiply.go#L20>)

Expand Down Expand Up @@ -987,6 +1029,15 @@ fmt.Println(<- c) // 6
fmt.Println(<- c) // 8
```

<a name="SlicesReverse"></a>
## func [SlicesReverse](<https://github.com/cinar/indicator/blob/master/helper/slices_reverse.go#L6>)

```go
func SlicesReverse[T any](r []T, i int, f func(T) bool)
```

SlicesReverse loops through a slice in reverse order starting from the given index. The given function is called for each element in the slice. If the function returns false, the loop is terminated.

<a name="Sqrt"></a>
## func [Sqrt](<https://github.com/cinar/indicator/blob/master/helper/sqrt.go#L16>)

Expand Down Expand Up @@ -1040,6 +1091,15 @@ func Waitable[T any](wg *sync.WaitGroup, c <-chan T) <-chan T

Waitable increments the wait group before reading from the channel and signals completion when the channel is closed.

<a name="Window"></a>
## func [Window](<https://github.com/cinar/indicator/blob/master/helper/window.go#L12>)

```go
func Window[T any](c <-chan T, f func([]T, int) T, w int) <-chan T
```

Window returns a channel that emits the passed function result within a sliding window of size w from the input channel c. Note: the slice is in the same order than in source channel but the 1st element may not be 0, use modulo window size if order is important.

<a name="Bst"></a>
## type [Bst](<https://github.com/cinar/indicator/blob/master/helper/bst.go#L15-L17>)

Expand Down
14 changes: 14 additions & 0 deletions valuation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import "github.com/cinar/indicator/v2/valuation"
## Index

- [func Fv\(pv, rate float64, years int\) float64](<#Fv>)
- [func Npv\(rate float64, cfs \[\]float64\) float64](<#Npv>)
- [func Pv\(fv, rate float64, years int\) float64](<#Pv>)


Expand All @@ -25,6 +26,19 @@ Fv calculates the Future Value \(FV\) of a Present Value \(PV\).
Formula: FV = PV * (1 + rate)^years
```

<a name="Npv"></a>
## func [Npv](<https://github.com/cinar/indicator/blob/master/valuation/npv.go#L12>)

```go
func Npv(rate float64, cfs []float64) float64
```

Npv calculates the Net Present Value \(NPV\) of a series of cash flows.

```
Formula: NPV = sum(CF_i / (1 + rate)^i) for i = 1 to n
```

<a name="Pv"></a>
## func [Pv](<https://github.com/cinar/indicator/blob/master/valuation/pv.go#L12>)

Expand Down
20 changes: 20 additions & 0 deletions valuation/npv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) 2021-2024 Onur Cinar.
// The source code is provided under GNU AGPLv3 License.
// https://github.com/cinar/indicator

package valuation

import "math"

// Npv calculates the Net Present Value (NPV) of a series of cash flows.
//
// Formula: NPV = sum(CF_i / (1 + rate)^i) for i = 1 to n
func Npv(rate float64, cfs []float64) float64 {
var npv float64
for i, cf := range cfs {
npv += cf / math.Pow(1+rate, float64(i+1))
}

return npv
}
Comment on lines +12 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid O(n) math.Pow calls; guard rate == -1

Use a running discount factor for O(1) per element and handle the division-by-zero edge case.

Apply this diff:

 func Npv(rate float64, cfs []float64) float64 {
 	var npv float64
-	for i, cf := range cfs {
-		npv += cf / math.Pow(1+rate, float64(i+1))
-	}
-
-	return npv
+	// Guard: 1+rate must not be 0 (division by zero at first term).
+	if rate == -1 {
+		return math.NaN()
+	}
+	if len(cfs) == 0 {
+		return 0
+	}
+	disc := 1 + rate
+	denom := disc
+	for _, cf := range cfs {
+		npv += cf / denom
+		denom *= disc
+	}
+	return npv
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func Npv(rate float64, cfs []float64) float64 {
var npv float64
for i, cf := range cfs {
npv += cf / math.Pow(1+rate, float64(i+1))
}
return npv
}
func Npv(rate float64, cfs []float64) float64 {
var npv float64
// Guard: 1+rate must not be 0 (division by zero at first term).
if rate == -1 {
return math.NaN()
}
if len(cfs) == 0 {
return 0
}
disc := 1 + rate
denom := disc
for _, cf := range cfs {
npv += cf / denom
denom *= disc
}
return npv
}
🤖 Prompt for AI Agents
In valuation/npv.go around lines 12 to 19, replace the per-iteration math.Pow
usage with a running discount factor to avoid O(n) pow calls and add a guard for
rate == -1 to prevent division-by-zero: check if rate == -1 at the top and
return math.NaN() (or another sentinel you prefer), otherwise initialize
discount := 1.0 + rate and in the loop divide cf by discount and then multiply
discount by (1.0 + rate) for the next iteration (or update discount
incrementally before/after use to match the current period), ensuring you import
math if needed.


44 changes: 44 additions & 0 deletions valuation/npv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2021-2024 Onur Cinar.
// The source code is provided under GNU AGPLv3 License.
// https://github.com/cinar/indicator

package valuation_test

import (
"strconv"
"strings"
"testing"

"github.com/cinar/indicator/v2/helper"
"github.com/cinar/indicator/v2/valuation"
)

func TestNpv(t *testing.T) {
type NpvData struct {
Rate float64
CashFlows string
NPV float64
}

parseCashFlows := func(s string) []float64 {
var cashFlows []float64
for _, cfStr := range strings.Split(s, " ") {
cf, _ := strconv.ParseFloat(cfStr, 64)
cashFlows = append(cashFlows, cf)
}
return cashFlows
}

input, err := helper.ReadFromCsvFile[NpvData]("testdata/npv.csv")
if err != nil {
t.Fatal(err)
}

for row := range input {
cashFlows := parseCashFlows(row.CashFlows)
npv := helper.RoundDigit(valuation.Npv(row.Rate, cashFlows), 2)
if npv != row.NPV {
t.Fatalf("actual %v expected %v", npv, row.NPV)
}
}
}
3 changes: 3 additions & 0 deletions valuation/testdata/npv.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Rate,CashFlows,NPV
0.1,"100 100 100",248.69
0.05,"100 200",276.64
Loading