Compare commits

...

8 Commits

Author SHA1 Message Date
0f4e9f4c41 Also update golangci-lint.
All checks were successful
Build & Test / Main (push) Successful in 19s
Release / Release (push) Successful in 24s
2024-09-09 23:55:04 -07:00
2c8e198163 Update Go, Goreleaser.
Some checks are pending
Build & Test / Main (push) Waiting to run
2024-09-09 23:48:32 -07:00
eda4200585 Switch to GoLand.
All checks were successful
Build & Test / Main (push) Successful in 1m49s
2024-03-31 01:07:31 -07:00
5c22d85e2b Add custom .golangci.yaml config.
All checks were successful
Build & Test / Main (push) Successful in 1m0s
Release / Release (push) Successful in 1m35s
2024-03-29 19:29:29 -07:00
f67323c5f4 Support hardcoded starting prices.
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.
2024-03-29 19:24:10 -07:00
4d5dcc46d2 Misc small improvements.
All checks were successful
Build & Test / Main (push) Successful in 1m0s
2024-03-29 18:40:33 -07:00
9e6abb1112 Naming things is hard.
All checks were successful
Build & Test / Main (push) Successful in 2m26s
2024-03-29 01:15:59 -07:00
2d991880ce Schedule refreshes more consistently.
All checks were successful
Build & Test / Main (push) Successful in 1m1s
Instead of returning the `scheduleRefresh` command only after receiving
an `update` message, do it while handling the `refresh` message. For
this not to cause weird behavior, the refresh deadline should be shorter
than the refresh interval.
2024-03-24 23:01:51 -07:00
20 changed files with 193 additions and 164 deletions

View File

@ -17,12 +17,12 @@ jobs:
name: Set up Go name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: 1.22.1 go-version: 1.23.1
- -
name: Run linter name: Run linter
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:
version: v1.56 version: v1.61
- -
name: Run tests name: Run tests
run: go test ./... run: go test ./...
@ -31,7 +31,7 @@ jobs:
uses: goreleaser/goreleaser-action@v5 uses: goreleaser/goreleaser-action@v5
with: with:
distribution: goreleaser distribution: goreleaser
version: 1.24.0 version: 2.2.0
args: release --clean --snapshot args: release --clean --snapshot
env: env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}

View File

@ -17,13 +17,13 @@ jobs:
name: Set up Go name: Set up Go
uses: actions/setup-go@v4 uses: actions/setup-go@v4
with: with:
go-version: 1.22.1 go-version: 1.23.1
- -
name: Run GoReleaser name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5 uses: goreleaser/goreleaser-action@v5
with: with:
distribution: goreleaser distribution: goreleaser
version: 1.24.0 version: 2.2.0
args: release --clean args: release --clean
env: env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}

15
.golangci.yaml Normal file
View File

@ -0,0 +1,15 @@
linters:
disable-all: true
enable:
- errcheck
- godot
- goimports
- gosimple
- govet
- ineffassign
- nilerr
- nilnil
- staticcheck
- typecheck
- unused
- usestdlibvars

View File

@ -6,7 +6,7 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json # yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=cnqoj # vim: set ts=2 sw=2 tw=0 fo=cnqoj
version: 1 version: 2
before: before:
hooks: hooks:

1
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
workspace.xml

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/moonmath.iml" filepath="$PROJECT_DIR$/.idea/moonmath.iml" />
</modules>
</component>
</project>

9
.idea/moonmath.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

20
.vscode/launch.json vendored
View File

@ -1,20 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch",
"type": "go",
"request": "launch",
"mode": "debug",
"console": "integratedTerminal",
"program": "${workspaceFolder}/moonmath.go",
"args": [
"--config-file",
"moonmath.toml"
]
}
]
}

18
.vscode/settings.json vendored
View File

@ -1,18 +0,0 @@
{
"cSpell.words": [
"alecthomas",
"bitcoinity",
"Bitfinex",
"Bitstamp",
"bubbletea",
"CDPR",
"charmbracelet",
"Coindesk",
"knadh",
"koanf",
"lipgloss",
"moonmath",
"OHLC",
"sourcegraph"
]
}

View File

@ -1,5 +1,6 @@
package coindesk package coindesk
//goland:noinspection ALL
const ( const (
BTC Asset = "BTC" BTC Asset = "BTC"
ETH Asset = "ETH" ETH Asset = "ETH"

View File

@ -16,62 +16,58 @@ import (
var k = koanf.New(".") var k = koanf.New(".")
func Load(path string) (all All, err error) { func Load(filePath string) (r Root, err error) {
err = k.Load(structs.Provider(Default, "koanf"), nil) err = k.Load(structs.Provider(Default, "koanf"), nil)
if err != nil { if err != nil {
return return
} }
if path != "" { if filePath != "" {
err = k.Load(file.Provider(path), yaml.Parser()) err = k.Load(file.Provider(filePath), yaml.Parser())
if err != nil { if err != nil {
return return
} }
} }
err = k.Unmarshal("", &all) err = k.Unmarshal("", &r)
if err != nil {
return
}
return return
} }
var Default All var Default Root
type All struct { type Root struct {
Defaults Data `koanf:"defaults"` Defaults Asset `koanf:"defaults"`
Assets map[coindesk.Asset]Data `koanf:"assets"` Assets map[coindesk.Asset]Asset `koanf:"assets"`
} }
type Data struct { type Asset struct {
Asset coindesk.Asset `koanf:"asset"` Asset coindesk.Asset `koanf:"asset"`
Goals []moon.Goal `koanf:"goals"` Goals []moon.Goal `koanf:"goals"`
ConstantBases []moon.ConstantBase `koanf:"constantBases"` ConstantBases []moon.ConstantBase `koanf:"constantBases"`
RelativeBases []moon.RelativeBase `koanf:"relativeBases"` RelativeBases []moon.RelativeBase `koanf:"relativeBases"`
} }
func (all All) GetData(asset coindesk.Asset) (data Data) { func (r Root) ForAsset(a coindesk.Asset) (cfg Asset) {
data, ok := all.Assets[asset] cfg = merge(r.Assets[a], r.Defaults)
if !ok { cfg.Asset = a
data = all.Defaults
}
if data.Asset == "" {
data.Asset = asset
}
if data.Goals == nil || len(data.Goals) == 0 {
data.Goals = all.Defaults.Goals
}
if data.ConstantBases == nil || len(data.ConstantBases) == 0 {
data.ConstantBases = all.Defaults.ConstantBases
}
if data.RelativeBases == nil || len(data.RelativeBases) == 0 {
data.RelativeBases = all.Defaults.RelativeBases
}
return return
} }
func merge(dst, src Asset) Asset {
if len(dst.Goals) == 0 {
dst.Goals = src.Goals
}
if len(dst.ConstantBases) == 0 {
dst.ConstantBases = src.ConstantBases
}
if len(dst.RelativeBases) == 0 {
dst.RelativeBases = src.RelativeBases
}
return dst
}
// GetBases returns the concatenation of the constant and relative bases, sorted // GetBases returns the concatenation of the constant and relative bases, sorted
// from most recent to least recent in time. // from most recent to least recent in time.
func GetBases(d *Data) (bases []moon.Base) { func GetBases(d *Asset) (bases []moon.Base) {
for _, b := range d.ConstantBases { for _, b := range d.ConstantBases {
bases = append(bases, b) bases = append(bases, b)
} }

View File

@ -28,6 +28,12 @@ defaults:
time: 2019-12-31T16:00:00-08:00 time: 2019-12-31T16:00:00-08:00
- name: 2017- - name: 2017-
time: 2016-12-31T16:00:00-08:00 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: assets:
ETH: ETH:
@ -44,6 +50,11 @@ assets:
value: 20000 value: 20000
- name: $25k - name: $25k
value: 25000 value: 25000
constantBases:
- name: 2020-
time: 2019-12-31T16:00:00-08:00
- name: 2017-
time: 2016-12-31T16:00:00-08:00
LTC: LTC:
goals: goals:
- name: $100 - name: $100

3
go.mod
View File

@ -36,8 +36,7 @@ require (
github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect github.com/muesli/termenv v0.15.2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/sync v0.6.0 // indirect golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/sys v0.18.0 // indirect
golang.org/x/term v0.18.0 // indirect golang.org/x/term v0.18.0 // indirect

9
go.sum
View File

@ -17,7 +17,6 @@ github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy12
github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
@ -75,14 +74,10 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=

View File

@ -2,6 +2,7 @@ package moon
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"math" "math"
"time" "time"
@ -75,30 +76,10 @@ func (m *Math) Refresh(ctx context.Context) (err error) {
tasks.WithMaxGoroutines(len(m.Columns)) tasks.WithMaxGoroutines(len(m.Columns))
//tasks.WithMaxGoroutines(1) //tasks.WithMaxGoroutines(1)
now := time.Now() now := time.Now()
for i := range m.Columns { for i := range m.Columns {
c := &m.Columns[i] c := &m.Columns[i]
tasks.Go(func() error { tasks.Go(func() error {
c.StartingDate = c.Base.From(now) return c.project(ctx, m, 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() err = tasks.Wait()
@ -115,46 +96,49 @@ type Column struct {
Projections Projection Projections Projection
} }
func (c *Column) Column() (entries []string) { func (c *Column) project(ctx context.Context, m *Math, now time.Time) (err error) {
entries = append(entries, fmt.Sprintf("$%.2f", c.StartingPrice)) err = c.fillStartingPrice(ctx, m.Asset, now)
entries = append(entries, fmt.Sprintf("%.2f%%", (c.CDPR-1)*100)) if err != nil {
never := c.CDPR <= 1 return
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)
} }
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 return
} }
var DefaultGoals = []Goal{ func (c *Column) fillStartingPrice(
{"$100k", 100000}, ctx context.Context, asset coindesk.Asset, now time.Time,
{"$150k", 150000}, ) error {
{"$200k", 200000}, // if base provides a hardcoded starting price, use it
{"$250k", 250000}, c.StartingDate = c.Base.From(now)
{"$300k", 300000}, c.StartingPrice = coindesk.Price(c.Base.GetStartingPrice())
{"$500k", 500000}, if c.StartingPrice != 0 {
{"$1m", 1000000}, 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 DefaultConstantBases = []ConstantBase{ var errEmptyPriceEntries = errors.New("price values response has no entries")
{"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 { type Goal struct {
Name string `koanf:"name"` Name string `koanf:"name"`
@ -165,12 +149,14 @@ type Goal struct {
type Base interface { type Base interface {
From(now time.Time) time.Time From(now time.Time) time.Time
Label() string Label() string
GetStartingPrice() float64
} }
// ConstantBase is a base that is a constant time, e.g. 2020-01-01. // ConstantBase is a base that is a constant time, e.g. 2020-01-01.
type ConstantBase struct { type ConstantBase struct {
Name string `koanf:"name"` Name string `koanf:"name"`
Time time.Time `koanf:"time"` Time time.Time `koanf:"time"`
StartingPrice float64 `koanf:"startingPrice"`
} }
func (cb ConstantBase) From(_ time.Time) time.Time { func (cb ConstantBase) From(_ time.Time) time.Time {
@ -181,17 +167,25 @@ func (cb ConstantBase) Label() string {
return cb.Name return cb.Name
} }
// RelativeBase is a base that is relative, e.g. "90 days ago." func (cb ConstantBase) GetStartingPrice() float64 {
return cb.StartingPrice
}
// RelativeBase is a base that is relative, e.g. "90 days ago".
type RelativeBase struct { type RelativeBase struct {
Name string `koanf:"name"` Name string `koanf:"name"`
Offset time.Duration `koanf:"offset"` Offset time.Duration `koanf:"offset"`
} }
func (rb RelativeBase) From(now time.Time) time.Time { func (rb RelativeBase) From(now time.Time) time.Time {
then := now.Add(time.Duration(rb.Offset)) then := now.Add(rb.Offset)
return then return then
} }
func (rb RelativeBase) Label() string { func (rb RelativeBase) Label() string {
return rb.Name return rb.Name
} }
func (rb RelativeBase) GetStartingPrice() float64 {
return 0
}

View File

@ -11,6 +11,14 @@ func TestCDPR(t *testing.T) {
} }
func TestProjection(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 _ = p
} }

View File

@ -30,7 +30,7 @@ type Msg struct {
inner tea.Msg inner tea.Msg
} }
func New(cfg config.Data) (m Model) { func New(cfg config.Asset) (m Model) {
m.math = moon.NewMath( m.math = moon.NewMath(
cfg.Asset, cfg.Asset,
cfg.Goals, cfg.Goals,
@ -89,10 +89,6 @@ func New(cfg config.Data) (m Model) {
return return
} }
func (m Model) Handles(a coindesk.Asset) bool {
return m.math.Asset == a
}
func (m Model) Init() tea.Cmd { func (m Model) Init() tea.Cmd {
return tea.Batch( return tea.Batch(
m.indicator.Tick, m.indicator.Tick,
@ -102,6 +98,10 @@ func (m Model) Init() tea.Cmd {
) )
} }
func (m Model) Handles(a coindesk.Asset) bool {
return m.math.Asset == a
}
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
switch msg := msg.(type) { switch msg := msg.(type) {
case Msg: case Msg:
@ -112,18 +112,19 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
return m, tea.Batch( return m, tea.Batch(
m.resumeIndicator, m.resumeIndicator,
m.refresh, m.refresh,
m.scheduleRefresh(),
) )
case update: case update:
commands := []tea.Cmd{m.scheduleRefresh()} var cmd tea.Cmd
if msg.err == nil { if msg.err == nil {
m.math = msg.math m.math = msg.math
refillProperties(&m) refillProperties(&m)
refillProjections(&m) refillProjections(&m)
commands = append(commands, m.stopIndicator()) cmd = m.stopIndicator()
} else { } else {
m.indicator.Style = indicatorErrorStyle m.indicator.Style = indicatorErrorStyle
} }
return m, tea.Batch(commands...) return m, cmd
case stopIndicator: case stopIndicator:
m.refreshing = false m.refreshing = false
return m, nil return m, nil
@ -147,9 +148,9 @@ type update struct {
type stopIndicator struct{} type stopIndicator struct{}
func (m Model) refresh() tea.Msg { func (m Model) refresh() tea.Msg {
ctx, cancel := context.WithDeadline( ctx, cancel := context.WithTimeout(
context.Background(), context.Background(),
time.Now().Add(time.Second*15)) refreshTimeout)
defer cancel() defer cancel()
err := m.math.Refresh(ctx) err := m.math.Refresh(ctx)
if err != nil { if err != nil {
@ -166,7 +167,7 @@ func (m Model) resumeIndicator() tea.Msg {
} }
func (m Model) scheduleRefresh() tea.Cmd { func (m Model) scheduleRefresh() tea.Cmd {
return tea.Tick(time.Second*30, return tea.Tick(refreshInterval,
func(t time.Time) tea.Msg { func(t time.Time) tea.Msg {
return Msg{m.math.Asset, refresh{}} return Msg{m.math.Asset, refresh{}}
}) })
@ -175,12 +176,16 @@ func (m Model) scheduleRefresh() tea.Cmd {
func (m Model) stopIndicator() tea.Cmd { func (m Model) stopIndicator() tea.Cmd {
// wait a bit to stop the indicator, so that it's more obvious // wait a bit to stop the indicator, so that it's more obvious
// even when the refresh completes quickly. // even when the refresh completes quickly.
return tea.Tick(time.Millisecond*500, return tea.Tick(stopIndicatorDelay,
func(t time.Time) tea.Msg { func(t time.Time) tea.Msg {
return Msg{m.math.Asset, stopIndicator{}} return Msg{m.math.Asset, stopIndicator{}}
}) })
} }
var refreshInterval = time.Second * 30
var refreshTimeout = time.Second * 15
var stopIndicatorDelay = time.Millisecond * 500
func refillProperties(m *Model) { func refillProperties(m *Model) {
rows := []table.Row{ rows := []table.Row{
{"Asset", string(m.math.Asset)}, {"Asset", string(m.math.Asset)},
@ -190,14 +195,34 @@ func refillProperties(m *Model) {
} }
func refillProjections(m *Model) { func refillProjections(m *Model) {
rows := []table.Row{m.math.Labels} cols := []table.Row{m.math.Labels}
for i := range m.math.Columns { for i := range m.math.Columns {
rows = append(rows, m.math.Columns[i].Column()) entries := renderEntries(m.math.Columns[i])
cols = append(cols, entries)
} }
rows = transpose(rows) rows := transpose(cols)
m.projections.SetRows(rows) m.projections.SetRows(rows)
} }
func renderEntries(c moon.Column) (entries []string) {
entries = append(entries, fmt.Sprintf("$%.2f", c.StartingPrice))
entries = append(entries, fmt.Sprintf("%.4f%%", (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
}
func transpose(slice []table.Row) []table.Row { func transpose(slice []table.Row) []table.Row {
xl := len(slice[0]) xl := len(slice[0])
yl := len(slice) yl := len(slice)

View File

@ -22,7 +22,7 @@ func (m Model) Init() tea.Cmd {
return nil return nil
} }
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { func (m Model) Update(_ tea.Msg) (Model, tea.Cmd) {
return m, nil return m, nil
} }

View File

@ -17,17 +17,16 @@ type Model struct {
displayStats bool displayStats bool
} }
func New(assets []coindesk.Asset, cfg config.All, displayStats bool) (m Model) { func New(assets []coindesk.Asset, cfg config.Root, displayStats bool) (m Model) {
m.stats = perf.New() m.stats = perf.New()
m.displayStats = displayStats m.displayStats = displayStats
// construct models for each asset, but don't filter out dupes // construct models for each asset, but remove dupes
seen := map[coindesk.Asset]struct{}{} seen := map[coindesk.Asset]struct{}{}
for _, a := range assets { for _, a := range assets {
_, ok := seen[a] if _, ok := seen[a]; ok {
if ok {
continue continue
} }
assetCfg := cfg.GetData(a) assetCfg := cfg.ForAsset(a)
assetModel := asset.New(assetCfg) assetModel := asset.New(assetCfg)
m.assets = append(m.assets, assetModel) m.assets = append(m.assets, assetModel)
seen[a] = struct{}{} seen[a] = struct{}{}