package moon import ( "context" "math" "time" "code.humancabbage.net/sam/moonmath/coindesk" "github.com/sourcegraph/conc/pool" ) type Math struct { CurrentPrice coindesk.Price Columns []Column Goals []float64 } func NewMath(goals []float64, bases []Base) (m Math) { if goals == nil { goals = DefaultGoals } if bases == nil { bases = DefaultBases } m.Goals = goals m.Columns = make([]Column, len(bases)) for i := range bases { m.Columns[i].Base = bases[i] } return } func CDPR(days, gain float64) float64 { if gain <= 0 { return 0 } cdpr := math.Pow(gain, 1/days) return cdpr } type Projection struct { Dates []time.Time } func ProjectDates( from time.Time, currentPrice float64, cdpr float64, goals []float64, ) (p Projection) { if cdpr <= 0 { return } logP := math.Log(currentPrice) logR := math.Log(cdpr) for _, goal := range goals { daysToGo := (math.Log(goal) - logP) / logR date := from.Add(time.Hour * 24 * time.Duration(daysToGo)) p.Dates = append(p.Dates, date) } return } func (m *Math) Refresh(ctx context.Context) (err error) { resp, err := coindesk.GetAssetTickers(ctx, coindesk.BTC) if err != nil { return } m.CurrentPrice = resp.Data[coindesk.BTC].OHLC.Closing tasks := pool.New().WithErrors() tasks.WithMaxGoroutines(len(m.Columns)) //tasks.WithMaxGoroutines(1) now := time.Now() for i := range m.Columns { c := &m.Columns[i] tasks.Go(func() error { c.StartingDate = c.Base.From(now) nextDay := c.StartingDate.Add(time.Hour * 24) resp, err := coindesk.GetPriceValues(ctx, coindesk.BTC, c.StartingDate, nextDay) if err != nil { return err } c.StartingPrice = resp.Data.Entries[0].Price c.Gain = float64(m.CurrentPrice) / float64(c.StartingPrice) days := now.Sub(c.StartingDate).Hours() / 24 c.CDPR = CDPR(days, c.Gain) if c.CDPR > 1 { c.Projections = ProjectDates(now, float64(m.CurrentPrice), c.CDPR, m.Goals) } else { c.Projections.Dates = nil } return nil }) } err = tasks.Wait() return } type Column struct { Base Base StartingDate time.Time StartingPrice coindesk.Price Gain float64 CDPR float64 Projections Projection } var DefaultGoals = []float64{ 100000, 150000, 200000, 250000, 300000, 500000, 1000000, } var DefaultBases = []Base{ RelativeBase{"Month", time.Duration(-30) * time.Hour * 24}, RelativeBase{"Quarter", time.Duration(-90) * time.Hour * 24}, RelativeBase{"Half-Year", time.Duration(-182) * time.Hour * 24}, RelativeBase{"Year", time.Duration(-365) * time.Hour * 24}, ConstantBase{"2020-", time.Unix(1577836800, 0)}, ConstantBase{"2019-", time.Unix(1546300800, 0)}, ConstantBase{"2018-", time.Unix(1514764800, 0)}, ConstantBase{"2017-", time.Unix(1483228800, 0)}, } // Base is a temporal point of comparison used for price projection. type Base interface { From(now time.Time) time.Time Name() string } // ConstantBase is a base that is a constant time, e.g. 2020-01-01. type ConstantBase struct { name string time time.Time } func (cb ConstantBase) From(_ time.Time) time.Time { return cb.time } func (cb ConstantBase) Name() string { return cb.name } // RelativeBase is a base that is relative, e.g. "90 days ago." type RelativeBase struct { name string offset time.Duration } func (rb RelativeBase) From(now time.Time) time.Time { then := now.Add(time.Duration(rb.offset)) return then } func (rb RelativeBase) Name() string { return rb.name }