Renamed module path git.mchus.pro/mchus/quoteforge → git.mchus.pro/mchus/priceforge, renamed package quoteforge → priceforge, moved binary from cmd/qfs to cmd/pfs.
122 lines
2.5 KiB
Go
122 lines
2.5 KiB
Go
package pricing
|
|
|
|
import (
|
|
"math"
|
|
"sort"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/priceforge/internal/repository"
|
|
)
|
|
|
|
// CalculateMedian returns the median of prices
|
|
func CalculateMedian(prices []float64) float64 {
|
|
if len(prices) == 0 {
|
|
return 0
|
|
}
|
|
|
|
sorted := make([]float64, len(prices))
|
|
copy(sorted, prices)
|
|
sort.Float64s(sorted)
|
|
|
|
n := len(sorted)
|
|
if n%2 == 0 {
|
|
return (sorted[n/2-1] + sorted[n/2]) / 2
|
|
}
|
|
return sorted[n/2]
|
|
}
|
|
|
|
// CalculateAverage returns the arithmetic mean of prices
|
|
func CalculateAverage(prices []float64) float64 {
|
|
if len(prices) == 0 {
|
|
return 0
|
|
}
|
|
|
|
var sum float64
|
|
for _, p := range prices {
|
|
sum += p
|
|
}
|
|
return sum / float64(len(prices))
|
|
}
|
|
|
|
// CalculateWeightedMedian calculates median with exponential decay weights
|
|
// More recent prices have higher weight
|
|
func CalculateWeightedMedian(points []repository.PricePoint, decayDays int) float64 {
|
|
if len(points) == 0 {
|
|
return 0
|
|
}
|
|
|
|
type weightedPrice struct {
|
|
price float64
|
|
weight float64
|
|
}
|
|
|
|
now := time.Now()
|
|
weighted := make([]weightedPrice, len(points))
|
|
var totalWeight float64
|
|
|
|
for i, p := range points {
|
|
daysSince := now.Sub(p.Date).Hours() / 24
|
|
// weight = e^(-days / decay_days)
|
|
weight := math.Exp(-daysSince / float64(decayDays))
|
|
weighted[i] = weightedPrice{price: p.Price, weight: weight}
|
|
totalWeight += weight
|
|
}
|
|
|
|
// Sort by price
|
|
sort.Slice(weighted, func(i, j int) bool {
|
|
return weighted[i].price < weighted[j].price
|
|
})
|
|
|
|
// Find weighted median
|
|
targetWeight := totalWeight / 2
|
|
var cumulativeWeight float64
|
|
|
|
for _, wp := range weighted {
|
|
cumulativeWeight += wp.weight
|
|
if cumulativeWeight >= targetWeight {
|
|
return wp.price
|
|
}
|
|
}
|
|
|
|
return weighted[len(weighted)-1].price
|
|
}
|
|
|
|
// CalculatePercentile calculates the nth percentile of prices
|
|
func CalculatePercentile(prices []float64, percentile float64) float64 {
|
|
if len(prices) == 0 {
|
|
return 0
|
|
}
|
|
|
|
sorted := make([]float64, len(prices))
|
|
copy(sorted, prices)
|
|
sort.Float64s(sorted)
|
|
|
|
index := (percentile / 100) * float64(len(sorted)-1)
|
|
lower := int(math.Floor(index))
|
|
upper := int(math.Ceil(index))
|
|
|
|
if lower == upper {
|
|
return sorted[lower]
|
|
}
|
|
|
|
fraction := index - float64(lower)
|
|
return sorted[lower]*(1-fraction) + sorted[upper]*fraction
|
|
}
|
|
|
|
// CalculateStdDev calculates standard deviation
|
|
func CalculateStdDev(prices []float64) float64 {
|
|
if len(prices) < 2 {
|
|
return 0
|
|
}
|
|
|
|
mean := CalculateAverage(prices)
|
|
var sumSquares float64
|
|
|
|
for _, p := range prices {
|
|
diff := p - mean
|
|
sumSquares += diff * diff
|
|
}
|
|
|
|
return math.Sqrt(sumSquares / float64(len(prices)-1))
|
|
}
|