package ledger import ( "fmt" "maps" "regexp" "slices" "sort" "time" ) func RegisterWithContributionPrediction(reg Register, window time.Duration) (Register, error) { result := make(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 Register, window time.Duration, name string) error { currencies := make(map[Currency]int) for d := range reg { for c := range reg[d][name] { currencies[c] = 1 } } for c := range currencies { err := registerWithContributionPredictionForNameForCurrency(reg, window, name, c) if err != nil { return err } } return nil } func registerWithContributionPredictionForNameForCurrency(reg Register, window time.Duration, name string, currency 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 v, ok := reg[d][name][currency]; ok { contributions = append(contributions, contribution{t: t, v: v}) } } sort.Slice(contributions, func(i, j int) bool { return contributions[i].t.Before(contributions[j].t) }) 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] } eighth := contributions[int(7.0*len(contributions)/8.0):] quarter := contributions[int(3.0*len(contributions)/4.0):] half := contributions[int(1.0*len(contributions)/2.0):] medianValueDelta := func() float64 { return (4.0*getMedianValueDelta(eighth) + 2.0*getMedianValueDelta(quarter) + 1.0*getMedianValueDelta(half)) / (4.0 + 2.0 + 1.0) }() medianLapse := func() time.Duration { return (4.0*getMedianLapse(eighth) + 2.0*getMedianLapse(quarter) + 1.0*getMedianLapse(half)) / (4.0 + 2.0 + 1.0) }() latest := func() float64 { max := 0.0 for d := range reg { if other := reg[d][name][currency]; other > max { max = other } } return max }() for _, predictionTime := range predictionTimes(window) { k := predictionTime.Format("2006-01") if _, ok := reg[k]; !ok { reg[k] = make(Balances) } if _, ok := reg[k][name]; !ok { reg[k][name] = make(Balance) } expectedDelta := float64(predictionTime.Sub(time.Now())) * medianValueDelta / float64(medianLapse) reg[k][name][currency] = latest + expectedDelta } return nil } func BPIsWithFixedGrowthPrediction(bpis BPIs, window time.Duration, pattern string, apy float64) (BPIs, error) { last := map[Currency]struct { t string v float64 }{} for currency, bpi := range bpis { for date, value := range bpi { if date > last[currency].t { was := last[currency] was.t = date was.v = value last[currency] = was } } } result := make(BPIs) p := regexp.MustCompile(pattern) for currency, v := range bpis { result[currency] = maps.Clone(v) if p.MatchString(string(currency)) { for _, predictionTime := range predictionTimes(window) { k2 := predictionTime.Format("2006-01") was := last[currency] was.v *= 1.0 + apy result[currency][k2] = was.v last[currency] = was } } } return result, nil } func RegisterWithCompoundingInterestPrediction(reg Register, window time.Duration, pattern string, apy float64) (Register, error) { lastBalances := make(Balances) p := regexp.MustCompile(pattern) for _, date := range reg.Dates() { for name := range reg[date] { if p.MatchString(name) { lastBalances[name] = reg[date][name] } } } result := maps.Clone(reg) for _, predictionTime := range predictionTimes(window) { k := predictionTime.Format("2006-01") if _, ok := result[k]; !ok { result[k] = make(Balances) } for name, balance := range lastBalances { balance2 := maps.Clone(balance) for k := range balance2 { balance2[k] *= 1.0 + (apy / 12) } result[k][name] = balance2 lastBalances[name] = balance2 } } return result, nil } func predictionTimes(window time.Duration) []time.Time { result := []time.Time{} last := time.Now() for last.Before(time.Now().Add(window)) { last = last.Add(-1 * time.Hour * 24 * time.Duration(last.Day())).Add(time.Hour * 24 * 33) result = append(result, last) } return result } func dateToTime(s string) (time.Time, error) { for _, layout := range []string{ "2006-01-02", "2006-01", } { if t, err := time.ParseInLocation(layout, s, time.Local); err == nil { return t, err } } return time.Time{}, fmt.Errorf("no layout matching %q", s) }