diff --git a/config/default.yaml b/config/default.yaml index dc4417e..fd83019 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -28,6 +28,12 @@ defaults: time: 2019-12-31T16:00:00-08:00 - name: 2017- time: 2016-12-31T16:00:00-08:00 + - name: 2013- + time: 2012-12-31T16:00:00-08:00 + startingPrice: 13.30 + - name: 2011- + time: 2010-12-31T16:00:00-08:00 + startingPrice: 0.30 assets: ETH: @@ -44,6 +50,11 @@ assets: value: 20000 - name: $25k value: 25000 + constantBases: + - name: 2020- + time: 2019-12-31T16:00:00-08:00 + - name: 2017- + time: 2016-12-31T16:00:00-08:00 LTC: goals: - name: $100 diff --git a/moon/moon.go b/moon/moon.go index ed6ae5e..68484fb 100644 --- a/moon/moon.go +++ b/moon/moon.go @@ -2,6 +2,8 @@ package moon import ( "context" + "errors" + "fmt" "math" "time" @@ -74,30 +76,10 @@ func (m *Math) Refresh(ctx context.Context) (err error) { 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 + return c.project(ctx, m, now) }) } err = tasks.Wait() @@ -114,27 +96,49 @@ type Column struct { Projections Projection } -var DefaultGoals = []Goal{ - {"$100k", 100000}, - {"$150k", 150000}, - {"$200k", 200000}, - {"$250k", 250000}, - {"$300k", 300000}, - {"$500k", 500000}, - {"$1m", 1000000}, +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 } -var DefaultConstantBases = []ConstantBase{ - {"2020-", time.Unix(1577836800, 0)}, - {"2017-", time.Unix(1483228800, 0)}, +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 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}, -} +var errEmptyPriceEntries = errors.New("price values response has no entries") type Goal struct { Name string `koanf:"name"` @@ -145,12 +149,14 @@ type Goal struct { 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"` + Name string `koanf:"name"` + Time time.Time `koanf:"time"` + StartingPrice float64 `koanf:"startingPrice"` } func (cb ConstantBase) From(_ time.Time) time.Time { @@ -161,6 +167,10 @@ 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"` @@ -175,3 +185,7 @@ func (rb RelativeBase) From(now time.Time) time.Time { func (rb RelativeBase) Label() string { return rb.Name } + +func (rb RelativeBase) GetStartingPrice() float64 { + return 0 +} diff --git a/moon/moon_test.go b/moon/moon_test.go index 4ea4e6b..746c084 100644 --- a/moon/moon_test.go +++ b/moon/moon_test.go @@ -11,6 +11,14 @@ func TestCDPR(t *testing.T) { } func TestProjection(t *testing.T) { - p := moon.ProjectDates(time.Now(), 68900, 1.0055, moon.DefaultGoals) + p := moon.ProjectDates(time.Now(), 68900, 1.0055, []moon.Goal{ + {"$100k", 100000}, + {"$150k", 150000}, + {"$200k", 200000}, + {"$250k", 250000}, + {"$300k", 300000}, + {"$500k", 500000}, + {"$1m", 1000000}, + }) _ = p } diff --git a/tui/asset/asset.go b/tui/asset/asset.go index 0243312..c9e815c 100644 --- a/tui/asset/asset.go +++ b/tui/asset/asset.go @@ -206,7 +206,7 @@ func refillProjections(m *Model) { func renderEntries(c moon.Column) (entries []string) { entries = append(entries, fmt.Sprintf("$%.2f", c.StartingPrice)) - entries = append(entries, fmt.Sprintf("%.2f%%", (c.CDPR-1)*100)) + entries = append(entries, fmt.Sprintf("%.4f%%", (c.CDPR-1)*100)) never := c.CDPR <= 1 for i := range c.Projections.Dates { var cell string