package ana import ( "maps" "slices" "sort" "time" "gogs.inhome.blapointe.com/ana-ledger/ledger" ) func RegisterWithContributionPrediction(reg ledger.Register, window time.Duration) (ledger.Register, error) { result := make(ledger.Register) result.PushAll(reg) for _, name := range result.Names() { err := registerWithContributionPredictionForName(result, window, name) if err != nil { return nil, err } } return result, nil } func registerWithContributionPredictionForName(reg ledger.Register, window time.Duration, name string) error { latest := make(ledger.Balance) for _, d := range reg.Dates() { if _, ok := reg[d][name]; ok { latest = reg[d][name] } } for _, predictionTime := range predictionTimes(window) { k := predictionTime.Format("2006-01") if _, ok := reg[k]; !ok { reg[k] = make(ledger.Balances) } reg[k][name] = maps.Clone(latest) } for c := range latest { err := registerWithContributionPredictionForNameForCurrency(reg, window, name, c) if err != nil { return err } } return nil } func registerWithContributionPredictionForNameForCurrency(reg ledger.Register, window time.Duration, name string, currency ledger.Currency) error { type contribution struct { t time.Time v float64 } contributions := make([]contribution, 0) for d := range reg { t, err := dateToTime(d) if err != nil { return err } if time.Since(t) > time.Hour*24*180 || time.Now().Before(t) { // only include -6months..now continue } if v, ok := reg[d][name][currency]; ok && (len(contributions) == 0 || contributions[len(contributions)-1].v != v) { contributions = append(contributions, contribution{t: t, v: v}) } } sort.Slice(contributions, func(i, j int) bool { return contributions[i].t.Before(contributions[j].t) }) if len(contributions) < 5 { return nil } getMedianValueDelta := func(contributions []contribution) float64 { values := make([]float64, len(contributions)) for i := 1; i < len(contributions); i++ { values[i] = contributions[i].v - contributions[i-1].v } slices.Sort(values) return values[len(values)/2] } getMedianLapse := func(contributions []contribution) time.Duration { lapses := make([]time.Duration, len(contributions)-1) for i := 1; i < len(contributions); i++ { lapses = append(lapses, contributions[i].t.Sub(contributions[i-1].t)) } slices.Sort(lapses) return lapses[len(lapses)/2] } contributionsSlice := func(percent float64) []contribution { wouldBe := int(percent * float64(len(contributions))) if wouldBe == 0 { wouldBe = 2 } return contributions[len(contributions)-1-wouldBe:] } eighth := contributionsSlice(7.0 / 8.0) quarter := contributionsSlice(3.0 / 4.0) half := contributionsSlice(1.0 / 2.0) medianValueDelta := func() float64 { medians := []float64{ getMedianValueDelta(eighth), getMedianValueDelta(quarter), getMedianValueDelta(half), } slices.Sort(medians) return medians[1] }() medianLapse := func() time.Duration { medians := []time.Duration{ getMedianLapse(eighth), getMedianLapse(quarter), getMedianLapse(half), } slices.Sort(medians) return medians[1] }() for _, predictionTime := range predictionTimes(window) { k := predictionTime.Format("2006-01") expectedDelta := float64(predictionTime.Sub(time.Now())) * medianValueDelta / float64(medianLapse) reg[k][name][currency] += expectedDelta } return nil }