Sam Fredrickson
1c6d5e9917
All checks were successful
Build & Test / Main (push) Successful in 1m28s
Instead of pre-allocating the grid and using tricky indexing to fill in the cells, just fully regenerate it. But do it columnwise first, and then transpose it.
197 lines
4.2 KiB
Go
197 lines
4.2 KiB
Go
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
|
|
}
|
|
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
|
|
}
|
|
|
|
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)},
|
|
{"2019-", time.Unix(1546300800, 0)},
|
|
{"2018-", time.Unix(1514764800, 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
|
|
}
|