From 60a4574c5a3509bf3490784f762aca4d4364b97f Mon Sep 17 00:00:00 2001 From: Sam Fredrickson Date: Tue, 19 Mar 2024 21:44:11 -0700 Subject: [PATCH] Initial configuration support. --- .vscode/launch.json | 10 +- .vscode/settings.json | 3 + coindesk/model.go | 326 +++++++++++++++++++++++++++++++++++++++++- config/config.go | 79 ++++++++++ config/eth.toml | 25 ++++ go.mod | 12 ++ go.sum | 24 ++++ moon/moon.go | 87 +++++------ moon/moon_test.go | 9 +- moonmath.go | 24 +++- tui/tui.go | 15 +- 11 files changed, 550 insertions(+), 64 deletions(-) create mode 100644 config/config.go create mode 100644 config/eth.toml diff --git a/.vscode/launch.json b/.vscode/launch.json index af57857..e816779 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,14 +5,16 @@ "version": "0.2.0", "configurations": [ { - "name": "Launch file", + "name": "Launch", "type": "go", "request": "launch", "mode": "debug", "console": "integratedTerminal", - "program": "${file}" + "program": "${workspaceFolder}/moonmath.go", + "args": [ + "--config-file", + "moonmath.toml" + ] } - - ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 9b1fd0e..a3941ad 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "alecthomas", "bitcoinity", "Bitfinex", "Bitstamp", @@ -7,6 +8,8 @@ "CDPR", "charmbracelet", "Coindesk", + "knadh", + "koanf", "lipgloss", "moonmath", "OHLC", diff --git a/coindesk/model.go b/coindesk/model.go index 72c0e40..0c4324d 100644 --- a/coindesk/model.go +++ b/coindesk/model.go @@ -11,10 +11,328 @@ import ( type Asset string const ( - // BTC is the Bitcoin asset. - BTC Asset = "BTC" - // ETH is the Ethereum asset. - ETH Asset = "ETH" + BTC Asset = "BTC" + ETH Asset = "ETH" + XRP Asset = "XRP" + BCH Asset = "BCH" + EOS Asset = "EOS" + XLM Asset = "XLM" + LTC Asset = "LTC" + ADA Asset = "ADA" + XMR Asset = "XMR" + DASH Asset = "DASH" + IOTA Asset = "IOTA" + TRX Asset = "TRX" + NEO Asset = "NEO" + ETC Asset = "ETC" + XEM Asset = "XEM" + ZEC Asset = "ZEC" + BTG Asset = "BTG" + LSK Asset = "LSK" + QTUM Asset = "QTUM" + BSV Asset = "BSV" + DOGE Asset = "DOGE" + DCR Asset = "DCR" + USDT Asset = "USDT" + USDC Asset = "USDC" + LINK Asset = "LINK" + XTZ Asset = "XTZ" + ZRX Asset = "ZRX" + DAI Asset = "DAI" + BAT Asset = "BAT" + OXT Asset = "OXT" + ALGO Asset = "ALGO" + ATOM Asset = "ATOM" + KNC Asset = "KNC" + OMG Asset = "OMG" + ANT Asset = "ANT" + REP Asset = "REP" + BAND Asset = "BAND" + BTT Asset = "BTT" + MANA Asset = "MANA" + FET Asset = "FET" + ICX Asset = "ICX" + KAVA Asset = "KAVA" + LRC Asset = "LRC" + MKR Asset = "MKR" + MLN Asset = "MLN" + NANO Asset = "NANO" + NMR Asset = "NMR" + PAXG Asset = "PAXG" + USDP Asset = "USDP" + SC Asset = "SC" + STORJ Asset = "STORJ" + WAVES Asset = "WAVES" + FIL Asset = "FIL" + CVC Asset = "CVC" + DNT Asset = "DNT" + REN Asset = "REN" + BNT Asset = "BNT" + WBTC Asset = "WBTC" + GRT Asset = "GRT" + UNI Asset = "UNI" + DOT Asset = "DOT" + YFI Asset = "YFI" + AAVE Asset = "AAVE" + MATIC Asset = "MATIC" + AMP Asset = "AMP" + CELO Asset = "CELO" + COMP Asset = "COMP" + CRV Asset = "CRV" + RLC Asset = "RLC" + KSM Asset = "KSM" + NKN Asset = "NKN" + SHIB Asset = "SHIB" + SKL Asset = "SKL" + SNX Asset = "SNX" + LUNC Asset = "LUNC" + UMA Asset = "UMA" + ICP Asset = "ICP" + SOL Asset = "SOL" + AVAX Asset = "AVAX" + UST Asset = "UST" + ENJ Asset = "ENJ" + IOTX Asset = "IOTX" + AXS Asset = "AXS" + XYO Asset = "XYO" + SUSHI Asset = "SUSHI" + ANKR Asset = "ANKR" + CHZ Asset = "CHZ" + LPT Asset = "LPT" + COTI Asset = "COTI" + KEEP Asset = "KEEP" + SAND Asset = "SAND" + GALA Asset = "GALA" + APE Asset = "APE" + CRO Asset = "CRO" + ACHP Asset = "ACHP" + JASMY Asset = "JASMY" + REQ Asset = "REQ" + SLP Asset = "SLP" + NEAR Asset = "NEAR" + MBOX Asset = "MBOX" + POLIS Asset = "POLIS" + MOVR Asset = "MOVR" + POLS Asset = "POLS" + QUICK Asset = "QUICK" + MINA Asset = "MINA" + IMX Asset = "IMX" + XEC Asset = "XEC" + NEXO Asset = "NEXO" + RUNE Asset = "RUNE" + QNT Asset = "QNT" + VET Asset = "VET" + CAKE Asset = "CAKE" + BNB Asset = "BNB" + THETA Asset = "THETA" + HBAR Asset = "HBAR" + FTM Asset = "FTM" + RVN Asset = "RVN" + ZIL Asset = "ZIL" + DGB Asset = "DGB" + FTT Asset = "FTT" + ENS Asset = "ENS" + WRX Asset = "WRX" + WAXP Asset = "WAXP" + EGLD Asset = "EGLD" + BUSD Asset = "BUSD" + CEL Asset = "CEL" + OP Asset = "OP" + LUNA Asset = "LUNA" + RAY Asset = "RAY" + FLOW Asset = "FLOW" + AUDIO Asset = "AUDIO" + CKB Asset = "CKB" + VGX Asset = "VGX" + YGG Asset = "YGG" + CHR Asset = "CHR" + STMX Asset = "STMX" + SXP Asset = "SXP" + INJ Asset = "INJ" + JOE Asset = "JOE" + POLY Asset = "POLY" + STX Asset = "STX" + SFP Asset = "SFP" + FARM Asset = "FARM" + XVG Asset = "XVG" + CLV Asset = "CLV" + WOO Asset = "WOO" + GLMR Asset = "GLMR" + STEEM Asset = "STEEM" + RARE Asset = "RARE" + IDEX Asset = "IDEX" + SRM Asset = "SRM" + PYR Asset = "PYR" + MIR Asset = "MIR" + SYS Asset = "SYS" + ALPACA Asset = "ALPACA" + QSP Asset = "QSP" + SCRT Asset = "SCRT" + SUN Asset = "SUN" + APT Asset = "APT" + MASK Asset = "MASK" + DYDX Asset = "DYDX" + CVX Asset = "CVX" + GMT Asset = "GMT" + CTSI Asset = "CTSI" + METIS Asset = "METIS" + FORTH Asset = "FORTH" + RBN Asset = "RBN" + SAMO Asset = "SAMO" + SPELL Asset = "SPELL" + LDO Asset = "LDO" + ARB Asset = "ARB" + BLUR Asset = "BLUR" + GAS Asset = "GAS" + RACA Asset = "RACA" + BABYDOGE Asset = "BABYDOGE" + FLOKI Asset = "FLOKI" + HOT Asset = "HOT" + BFC Asset = "BFC" + KISHU Asset = "KISHU" + ELON Asset = "ELON" + SAITAMA Asset = "SAITAMA" + REEF Asset = "REEF" + CEEK Asset = "CEEK" + ATLAS Asset = "ATLAS" + LOOKS Asset = "LOOKS" + WIN Asset = "WIN" + ONE Asset = "ONE" + DENT Asset = "DENT" + GST Asset = "GST" + TWT Asset = "TWT" + HNT Asset = "HNT" + AGLD Asset = "AGLD" + BTRST Asset = "BTRST" + ETHW Asset = "ETHW" + ILV Asset = "ILV" + RARI Asset = "RARI" + STG Asset = "STG" + SYN Asset = "SYN" + TOKE Asset = "TOKE" + BLZ Asset = "BLZ" + FLR Asset = "FLR" + FIS Asset = "FIS" + GNS Asset = "GNS" + ID Asset = "ID" + PEPE Asset = "PEPE" + DIA Asset = "DIA" + TLM Asset = "TLM" + XCN Asset = "XCN" + BIT Asset = "BIT" + RPL Asset = "RPL" + RNDR Asset = "RNDR" + ONEINCH Asset = "1INCH" + BAL Asset = "BAL" + T Asset = "T" + GNO Asset = "GNO" + ASTR Asset = "ASTR" + GLM Asset = "GLM" + OCEAN Asset = "OCEAN" + BICO Asset = "BICO" + CELR Asset = "CELR" + LQTY Asset = "LQTY" + TRAC Asset = "TRAC" + ZEN Asset = "ZEN" + API3 Asset = "API3" + PLA Asset = "PLA" + AXL Asset = "AXL" + HFT Asset = "HFT" + MC Asset = "MC" + C98 Asset = "C98" + GAL Asset = "GAL" + GTC Asset = "GTC" + RAD Asset = "RAD" + POWR Asset = "POWR" + POND Asset = "POND" + ALICE Asset = "ALICE" + TRU Asset = "TRU" + OGN Asset = "OGN" + DAR Asset = "DAR" + BADGER Asset = "BADGER" + GHST Asset = "GHST" + LCX Asset = "LCX" + ARPA Asset = "ARPA" + MXC Asset = "MXC" + PERP Asset = "PERP" + LOKA Asset = "LOKA" + BOBA Asset = "BOBA" + BOND Asset = "BOND" + ALCX Asset = "ALCX" + KP3R Asset = "KP3R" + TON Asset = "TON" + AR Asset = "AR" + AVA Asset = "AVA" + BONE Asset = "BONE" + BONK Asset = "BONK" + CORE Asset = "CORE" + CSPR Asset = "CSPR" + DG Asset = "DG" + ERN Asset = "ERN" + FXS Asset = "FXS" + GMX Asset = "GMX" + GT Asset = "GT" + GUSD Asset = "GUSD" + HMT Asset = "HMT" + HT Asset = "HT" + KCS Asset = "KCS" + KLAY Asset = "KLAY" + LEO Asset = "LEO" + MPL Asset = "MPL" + OKB Asset = "OKB" + PIT Asset = "PIT" + OSMO Asset = "OSMO" + RLY Asset = "RLY" + SANTOS Asset = "SANTOS" + SUI Asset = "SUI" + SWEAT Asset = "SWEAT" + TUSD Asset = "TUSD" + TVK Asset = "TVK" + UNFI Asset = "UNFI" + USDD Asset = "USDD" + VLX Asset = "VLX" + WEMIX Asset = "WEMIX" + XDC Asset = "XDC" + XRD Asset = "XRD" + FB Asset = "FB" + BRISE Asset = "BRISE" + KAS Asset = "KAS" + XEN Asset = "XEN" + HAM Asset = "HAM" + TAMA Asset = "TAMA" + KDA Asset = "KDA" + CFX Asset = "CFX" + VRA Asset = "VRA" + BDX Asset = "BDX" + RDNT Asset = "RDNT" + WLD Asset = "WLD" + AGIX Asset = "AGIX" + PYUSD Asset = "PYUSD" + MOON Asset = "MOON" + SEI Asset = "SEI" + AKT Asset = "AKT" + MAGIC Asset = "MAGIC" + SNT Asset = "SNT" + ALPHA Asset = "ALPHA" + ALI Asset = "ALI" + CQT Asset = "CQT" + HIGH Asset = "HIGH" + AERGO Asset = "AERGO" + GODS Asset = "GODS" + ZBC Asset = "ZBC" + ACA Asset = "ACA" + MDT Asset = "MDT" + LIT Asset = "LIT" + QI Asset = "QI" + AURORA Asset = "AURORA" + TOMI Asset = "TOMI" + XCH Asset = "XCH" + MANTA Asset = "MANTA" + PYTH Asset = "PYTH" + STRK Asset = "STRK" + ETHFI Asset = "ETHFI" + TIA Asset = "TIA" + EETH Asset = "EETH" ) // Response represents the general top-level format of Coindesk API responses. diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..dbf4ea0 --- /dev/null +++ b/config/config.go @@ -0,0 +1,79 @@ +package config + +import ( + "slices" + "time" + + "code.humancabbage.net/sam/moonmath/coindesk" + "code.humancabbage.net/sam/moonmath/moon" + "github.com/knadh/koanf/parsers/toml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/providers/structs" + "github.com/knadh/koanf/v2" +) + +var k = koanf.New(".") + +func Load(path string) (data Data, err error) { + err = k.Load(structs.Provider(Default, "koanf"), nil) + if err != nil { + return + } + if path != "" { + err = k.Load(file.Provider(path), toml.Parser()) + if err != nil { + return + } + } + err = k.Unmarshal("", &data) + if err != nil { + return + } + + return +} + +var Default = Data{ + Asset: coindesk.BTC, + Goals: moon.DefaultGoals, + ConstantBases: moon.DefaultConstantBases, + RelativeBases: moon.DefaultRelativeBases, +} + +type Data struct { + Asset coindesk.Asset `koanf:"asset"` + Goals []moon.Goal `koanf:"goals"` + ConstantBases []moon.ConstantBase `koanf:"constantBases"` + RelativeBases []moon.RelativeBase `koanf:"relativeBases"` +} + +type Goal struct { + Name string `koanf:"name"` + Value float64 `koanf:"value"` +} + +// GetBases returns the concatenation of the constant and relative bases, sorted +// from most recent to least recent in time. +func GetBases(d *Data) (bases []moon.Base) { + for _, b := range d.ConstantBases { + bases = append(bases, b) + } + for _, b := range d.RelativeBases { + bases = append(bases, b) + } + now := time.Now() + slices.SortFunc(bases, func(a, b moon.Base) int { + aTime := a.From(now) + bTime := b.From(now) + aFirst := aTime.Before(bTime) + bFirst := bTime.Before(aTime) + switch { + case aFirst: + return 1 + case bFirst: + return -1 + } + return 0 + }) + return +} diff --git a/config/eth.toml b/config/eth.toml new file mode 100644 index 0000000..a355948 --- /dev/null +++ b/config/eth.toml @@ -0,0 +1,25 @@ +asset = "ETH" + +[[goals]] +name = "$5k" +value = 5000 + +[[goals]] +name = "$7.5k" +value = 7500 + +[[goals]] +name = "$10k" +value = 10000 + +[[goals]] +name = "$15k" +value = 15000 + +[[goals]] +name = "$20k" +value = 15000 + +[[goals]] +name = "$25k" +value = 25000 diff --git a/go.mod b/go.mod index 50da58e..955c65d 100644 --- a/go.mod +++ b/go.mod @@ -12,16 +12,28 @@ require ( ) require ( + github.com/alecthomas/kong v0.9.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/containerd/console v1.0.4 // indirect + github.com/fatih/structs v1.1.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/knadh/koanf/parsers/toml v0.1.0 // indirect + github.com/knadh/koanf/providers/file v0.1.0 // indirect + github.com/knadh/koanf/providers/structs v0.1.0 // indirect + github.com/knadh/koanf/v2 v2.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect github.com/rivo/uniseg v0.4.7 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.9.0 // indirect diff --git a/go.sum b/go.sum index e21b16d..51ff4aa 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA= +github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/carlmjohnson/requests v0.23.5 h1:NPANcAofwwSuC6SIMwlgmHry2V3pLrSqRiSBKYbNHHA= @@ -13,6 +15,22 @@ github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6 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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= +github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= +github.com/knadh/koanf/providers/file v0.1.0 h1:fs6U7nrV58d3CFAFh8VTde8TM262ObYf3ODrc//Lp+c= +github.com/knadh/koanf/providers/file v0.1.0/go.mod h1:rjJ/nHQl64iYCtAW2QQnF0eSmDEX/YZ/eNFj5yR6BvA= +github.com/knadh/koanf/providers/structs v0.1.0 h1:wJRteCNn1qvLtE5h8KQBvLJovidSdntfdyIbbCzEyE0= +github.com/knadh/koanf/providers/structs v0.1.0/go.mod h1:sw2YZ3txUcqA3Z27gPlmmBzWn1h8Nt9O6EP/91MkcWE= +github.com/knadh/koanf/v2 v2.1.0 h1:eh4QmHHBuU8BybfIJ8mB8K8gsGCD/AUQTdwGq/GzId8= +github.com/knadh/koanf/v2 v2.1.0/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= @@ -22,6 +40,10 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -30,6 +52,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= diff --git a/moon/moon.go b/moon/moon.go index efd83e0..24b8595 100644 --- a/moon/moon.go +++ b/moon/moon.go @@ -10,18 +10,17 @@ import ( ) type Math struct { + Asset coindesk.Asset CurrentPrice coindesk.Price Columns []Column - Goals []float64 + Goals []Goal } -func NewMath(goals []float64, bases []Base) (m Math) { - if goals == nil { - goals = DefaultGoals - } - if bases == nil { - bases = DefaultBases +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.Columns = make([]Column, len(bases)) for i := range bases { @@ -43,7 +42,7 @@ type Projection struct { } func ProjectDates( - from time.Time, currentPrice float64, cdpr float64, goals []float64, + from time.Time, currentPrice float64, cdpr float64, goals []Goal, ) (p Projection) { if cdpr <= 0 { return @@ -51,7 +50,7 @@ func ProjectDates( logP := math.Log(currentPrice) logR := math.Log(cdpr) for _, goal := range goals { - daysToGo := (math.Log(goal) - logP) / logR + daysToGo := (math.Log(goal.Value) - logP) / logR date := from.Add(time.Hour * 24 * time.Duration(daysToGo)) p.Dates = append(p.Dates, date) } @@ -60,11 +59,11 @@ func ProjectDates( } func (m *Math) Refresh(ctx context.Context) (err error) { - resp, err := coindesk.GetAssetTickers(ctx, coindesk.BTC) + resp, err := coindesk.GetAssetTickers(ctx, m.Asset) if err != nil { return } - m.CurrentPrice = resp.Data[coindesk.BTC].OHLC.Closing + m.CurrentPrice = resp.Data[m.Asset].OHLC.Closing tasks := pool.New().WithErrors() tasks.WithMaxGoroutines(len(m.Columns)) @@ -77,7 +76,7 @@ func (m *Math) Refresh(ctx context.Context) (err error) { c.StartingDate = c.Base.From(now) nextDay := c.StartingDate.Add(time.Hour * 24) resp, err := coindesk.GetPriceValues(ctx, - coindesk.BTC, c.StartingDate, nextDay) + m.Asset, c.StartingDate, nextDay) if err != nil { return err } @@ -107,58 +106,66 @@ type Column struct { Projections Projection } -var DefaultGoals = []float64{ - 100000, - 150000, - 200000, - 250000, - 300000, - 500000, - 1000000, +var DefaultGoals = []Goal{ + {"$100k", 100000}, + {"$150k", 150000}, + {"$200k", 200000}, + {"$250k", 250000}, + {"$300k", 300000}, + {"$500k", 500000}, + {"$1m", 1000000}, } -var DefaultBases = []Base{ - RelativeBase{"Month", time.Duration(-30) * time.Hour * 24}, - RelativeBase{"Quarter", time.Duration(-90) * time.Hour * 24}, - RelativeBase{"Half-Year", time.Duration(-182) * time.Hour * 24}, - RelativeBase{"Year", time.Duration(-365) * time.Hour * 24}, - ConstantBase{"2020-", time.Unix(1577836800, 0)}, - ConstantBase{"2019-", time.Unix(1546300800, 0)}, - ConstantBase{"2018-", time.Unix(1514764800, 0)}, - ConstantBase{"2017-", time.Unix(1483228800, 0)}, +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 - Name() string + Label() string } // ConstantBase is a base that is a constant time, e.g. 2020-01-01. type ConstantBase struct { - name string - time time.Time + Name string `koanf:"name"` + Time time.Time `koanf:"time"` } func (cb ConstantBase) From(_ time.Time) time.Time { - return cb.time + return cb.Time } -func (cb ConstantBase) Name() string { - return cb.name +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 - offset time.Duration + 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)) + then := now.Add(time.Duration(rb.Offset)) return then } -func (rb RelativeBase) Name() string { - return rb.name +func (rb RelativeBase) Label() string { + return rb.Name } diff --git a/moon/moon_test.go b/moon/moon_test.go index 4942922..4ea4e6b 100644 --- a/moon/moon_test.go +++ b/moon/moon_test.go @@ -11,13 +11,6 @@ func TestCDPR(t *testing.T) { } func TestProjection(t *testing.T) { - p := moon.ProjectDates(time.Now(), 68900, 1.0055, []float64{ - 100000, - 150000, - 200000, - 250000, - 300000, - 350000, - }) + p := moon.ProjectDates(time.Now(), 68900, 1.0055, moon.DefaultGoals) _ = p } diff --git a/moonmath.go b/moonmath.go index 66d522c..35f0319 100644 --- a/moonmath.go +++ b/moonmath.go @@ -4,14 +4,32 @@ import ( "fmt" "os" + "code.humancabbage.net/sam/moonmath/config" "code.humancabbage.net/sam/moonmath/tui" + "github.com/alecthomas/kong" tea "github.com/charmbracelet/bubbletea" ) +var CLI struct { + ConfigFile string `help:"Path to TOML configuration file."` +} + func main() { - p := tea.NewProgram(tui.New()) + ctx := kong.Parse(&CLI) + if ctx.Error != nil { + fail(ctx.Error) + } + cfg, err := config.Load(CLI.ConfigFile) + if err != nil { + fail(err) + } + p := tea.NewProgram(tui.New(cfg)) if _, err := p.Run(); err != nil { - fmt.Printf("program error: %v\n", err) - os.Exit(1) + fail(err) } } + +func fail(err error) { + fmt.Printf("program error: %v\n", err) + os.Exit(1) +} diff --git a/tui/tui.go b/tui/tui.go index 2001632..a21c107 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "code.humancabbage.net/sam/moonmath/config" "code.humancabbage.net/sam/moonmath/moon" "github.com/charmbracelet/bubbles/table" tea "github.com/charmbracelet/bubbletea" @@ -22,13 +23,17 @@ type Model struct { projections table.Model } -func New() Model { - math := moon.NewMath(nil, nil) +func New(cfg config.Data) Model { + math := moon.NewMath( + cfg.Asset, + cfg.Goals, + config.GetBases(&cfg)) tableStyle := table.DefaultStyles() tableStyle.Selected = lipgloss.NewStyle() prices := table.New( table.WithColumns([]table.Column{ + {Title: "Asset", Width: 6}, {Title: "Price", Width: 9}, }), table.WithHeight(1), @@ -40,7 +45,7 @@ func New() Model { } for i := range math.Columns { projectionCols = append(projectionCols, table.Column{ - Title: math.Columns[i].Base.Name(), + Title: math.Columns[i].Base.Label(), Width: 10, }) } @@ -51,7 +56,7 @@ func New() Model { projectionRows[0][0] = "Starting" projectionRows[1][0] = "CDPR" for i := range math.Goals { - projectionRows[i+2][0] = fmt.Sprintf("$%.0f", math.Goals[i]) + projectionRows[i+2][0] = fmt.Sprintf("$%.0f", math.Goals[i].Value) } projections := table.New( table.WithColumns(projectionCols), @@ -95,7 +100,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func refillPrice(m *Model) { rows := []table.Row{ - []string{fmt.Sprintf("$%0.2f", m.math.CurrentPrice)}, + []string{string(m.math.Asset), fmt.Sprintf("$%0.2f", m.math.CurrentPrice)}, } m.prices.SetRows(rows) }