Sam Fredrickson
f67323c5f4
All checks were successful
Build & Test / Main (push) Successful in 1m0s
The Coindesk API doesn't have data going all the way back. But since history isn't changing, we can simply put in known prices. Also, extend the CDPR cells to have four digits instead of just two.
192 lines
4.1 KiB
Go
192 lines
4.1 KiB
Go
package moon
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"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 {
|
|
return c.project(ctx, m, now)
|
|
})
|
|
}
|
|
err = tasks.Wait()
|
|
|
|
return
|
|
}
|
|
|
|
type Column struct {
|
|
Base Base
|
|
StartingDate time.Time
|
|
StartingPrice coindesk.Price
|
|
Gain float64
|
|
CDPR float64
|
|
Projections Projection
|
|
}
|
|
|
|
func (c *Column) project(ctx context.Context, m *Math, now time.Time) (err error) {
|
|
err = c.fillStartingPrice(ctx, m.Asset, now)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (c *Column) fillStartingPrice(
|
|
ctx context.Context, asset coindesk.Asset, now time.Time,
|
|
) error {
|
|
// if base provides a hardcoded starting price, use it
|
|
c.StartingDate = c.Base.From(now)
|
|
c.StartingPrice = coindesk.Price(c.Base.GetStartingPrice())
|
|
if c.StartingPrice != 0 {
|
|
return nil
|
|
}
|
|
|
|
// otherwise, look up the starting price via Coindesk
|
|
nextDay := c.StartingDate.Add(time.Hour * 24)
|
|
resp, err := coindesk.GetPriceValues(ctx, asset, c.StartingDate, nextDay)
|
|
if err != nil {
|
|
err = fmt.Errorf("getting price for %s on %v: %w",
|
|
asset, c.StartingDate, err)
|
|
return err
|
|
}
|
|
if len(resp.Data.Entries) == 0 {
|
|
c.Projections.Dates = nil
|
|
return errEmptyPriceEntries
|
|
}
|
|
c.StartingPrice = resp.Data.Entries[0].Price
|
|
return nil
|
|
}
|
|
|
|
var errEmptyPriceEntries = errors.New("price values response has no entries")
|
|
|
|
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
|
|
GetStartingPrice() float64
|
|
}
|
|
|
|
// 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"`
|
|
StartingPrice float64 `koanf:"startingPrice"`
|
|
}
|
|
|
|
func (cb ConstantBase) From(_ time.Time) time.Time {
|
|
return cb.Time
|
|
}
|
|
|
|
func (cb ConstantBase) Label() string {
|
|
return cb.Name
|
|
}
|
|
|
|
func (cb ConstantBase) GetStartingPrice() float64 {
|
|
return cb.StartingPrice
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func (rb RelativeBase) GetStartingPrice() float64 {
|
|
return 0
|
|
}
|