package moon import ( "context" "fmt" "math" "time" "code.humancabbage.net/sam/moonmath/coindesk" "github.com/sourcegraph/conc/pool" ) type Math struct { Asset coindesk.Asset CurrentPrice coindesk.Price Columns []Column Goals []Goal Labels []string } func NewMath(asset coindesk.Asset, goals []Goal, bases []Base) (m Math) { if goals == nil || bases == nil { panic("goals and bases must be set") } m.Asset = asset m.Goals = goals m.Labels = []string{"Starting", "CDPR"} m.Columns = make([]Column, len(bases)) for i := range bases { m.Columns[i].Base = bases[i] } for i := range goals { m.Labels = append(m.Labels, goals[i].Name) } 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 []Goal, ) (p Projection) { if cdpr <= 0 { return } logP := math.Log(currentPrice) logR := math.Log(cdpr) for _, goal := range goals { daysToGo := (math.Log(goal.Value) - 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, m.Asset) if err != nil { return } m.CurrentPrice = resp.Data[m.Asset].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, m.Asset, c.StartingDate, nextDay) if err != nil { return err } if len(resp.Data.Entries) == 0 { c.Projections.Dates = nil return nil } 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) c.Projections = ProjectDates( now, float64(m.CurrentPrice), c.CDPR, m.Goals, ) return nil }) } err = tasks.Wait() return } type Column struct { Base Base StartingDate time.Time StartingPrice coindesk.Price Gain float64 CDPR float64 Projections Projection } func (c *Column) Column() (entries []string) { entries = append(entries, fmt.Sprintf("$%.2f", c.StartingPrice)) entries = append(entries, fmt.Sprintf("%.2f%%", (c.CDPR-1)*100)) never := c.CDPR <= 1 for i := range c.Projections.Dates { var cell string if never { cell = "NEVER!!!!!" } else { cell = c. Projections. Dates[i]. Format("2006-01-02") } entries = append(entries, cell) } return } var DefaultGoals = []Goal{ {"$100k", 100000}, {"$150k", 150000}, {"$200k", 200000}, {"$250k", 250000}, {"$300k", 300000}, {"$500k", 500000}, {"$1m", 1000000}, } var DefaultConstantBases = []ConstantBase{ {"2020-", time.Unix(1577836800, 0)}, {"2017-", time.Unix(1483228800, 0)}, } var DefaultRelativeBases = []RelativeBase{ {"Month", time.Duration(-30) * time.Hour * 24}, {"Quarter", time.Duration(-90) * time.Hour * 24}, {"Half-Year", time.Duration(-182) * time.Hour * 24}, {"Year", time.Duration(-365) * time.Hour * 24}, } type Goal struct { Name string `koanf:"name"` Value float64 `koanf:"value"` } // Base is a temporal point of comparison used for price projection. type Base interface { From(now time.Time) time.Time Label() string } // ConstantBase is a base that is a constant time, e.g. 2020-01-01. type ConstantBase struct { Name string `koanf:"name"` Time time.Time `koanf:"time"` } func (cb ConstantBase) From(_ time.Time) time.Time { return cb.Time } func (cb ConstantBase) Label() string { return cb.Name } // RelativeBase is a base that is relative, e.g. "90 days ago." type RelativeBase struct { Name string `koanf:"name"` Offset time.Duration `koanf:"offset"` } func (rb RelativeBase) From(now time.Time) time.Time { then := now.Add(time.Duration(rb.Offset)) return then } func (rb RelativeBase) Label() string { return rb.Name }