From 23ed509200e2c5ae19740924957cd16bf821820c Mon Sep 17 00:00:00 2001 From: Sam Fredrickson Date: Sun, 17 Mar 2024 02:10:05 -0700 Subject: [PATCH] Initial public commit. --- .gitignore | 1 + .vscode/launch.json | 18 +++ .vscode/settings.json | 15 +++ LICENSE | 21 ++++ README.md | 46 ++++++++ bitcoinity/client.go | 44 ++++++++ bitcoinity/get_ticker.json | 1 + bitcoinity/model.go | 101 +++++++++++++++++ bitcoinity/model_test.go | 23 ++++ bitcoinity/websocket.go | 196 +++++++++++++++++++++++++++++++++ coindesk/lib_test.go | 29 +++++ coindesk/model.go | 112 +++++++++++++++++++ coindesk/requests.go | 48 ++++++++ example.json | 217 +++++++++++++++++++++++++++++++++++++ go.mod | 32 ++++++ go.sum | 62 +++++++++++ moon/moon.go | 164 ++++++++++++++++++++++++++++ moon/moon_test.go | 23 ++++ moonmath.go | 145 +++++++++++++++++++++++++ screenshot.png | Bin 0 -> 51895 bytes 20 files changed, 1298 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bitcoinity/client.go create mode 100644 bitcoinity/get_ticker.json create mode 100644 bitcoinity/model.go create mode 100644 bitcoinity/model_test.go create mode 100644 bitcoinity/websocket.go create mode 100644 coindesk/lib_test.go create mode 100644 coindesk/model.go create mode 100644 coindesk/requests.go create mode 100644 example.json create mode 100644 go.mod create mode 100644 go.sum create mode 100644 moon/moon.go create mode 100644 moon/moon_test.go create mode 100644 moonmath.go create mode 100644 screenshot.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00253cc --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/moonmath diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..af57857 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + // 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 file", + "type": "go", + "request": "launch", + "mode": "debug", + "console": "integratedTerminal", + "program": "${file}" + } + + + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9b1fd0e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "cSpell.words": [ + "bitcoinity", + "Bitfinex", + "Bitstamp", + "bubbletea", + "CDPR", + "charmbracelet", + "Coindesk", + "lipgloss", + "moonmath", + "OHLC", + "sourcegraph" + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f501a82 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Samuel Fredrickson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa26ce9 --- /dev/null +++ b/README.md @@ -0,0 +1,46 @@ +# moonmath + +## Bullshit BTC Price Projections, Now in Your CLI! + +![screenshot](./screenshot.png) + +This is a re-implementation of [Moon Math][moon] that runs locally as a CLI +program. It's written in Go using the [Bubble Tea][tea] library, and uses +[Coindesk][coin] to source price data. + +[moon]: https://www.moonmath.win +[tea]: https://github.com/charmbracelet/bubbletea +[coin]: https://www.coindesk.com + +### "Theory" + +Given a pair of quotes taken at the start and end of some period, + +$$ (t_s, p_s), (t_e, p_e) $$ + +we can derive the total gain for that period, and its length in days. + +$$ g = p_e / p_s $$ +$$ d = t_e - t_s $$ + +Combining these, we can calculate the *compounding daily periodic rate* (CDPR). + +$$ r = g^{1/d} $$ + +We can use this rate to project the price $ p_f $ at some $ x $ days in the +future. + +$$ p_f = p_e r^x $$ + +If we instead make $ p_f $ a target price, we can solve this equation for $ x $, +telling us how many days it will take to reach that target. + +$$ x = {{log(p_f) - log(p_e)} \over log(r)} $$ + +### Future Improvements + +* Support other assets available from Coindesk. +* Configurable projection milestones. +* Allow projection by date, e.g. use the CDPR to calculate what the price + would be on a particular date. +* Log errors to a file. diff --git a/bitcoinity/client.go b/bitcoinity/client.go new file mode 100644 index 0000000..a67c62a --- /dev/null +++ b/bitcoinity/client.go @@ -0,0 +1,44 @@ +package bitcoinity + +import ( + "context" + + "github.com/carlmjohnson/requests" + "golang.org/x/net/websocket" +) + +func GetWebsocket() (Websocket, error) { + origin := baseUrl + url := "wss://bitcoinity.org/webs_bridge/websocket?vsn=1.0.0" + conn, err := websocket.Dial(url, "", origin) + if err != nil { + return nil, err + } + ws := newWebsocket(conn) + return ws, nil +} + +func GetTicker( + ctx context.Context, req *GetTickerRequest, +) (*GetTickerResponse, error) { + var resp GetTickerResponse + err := requests.New(commonConfig). + Path("/markets/get_ticker"). + Param("currency", string(req.Currency)). + Param("exchange", string(req.Exchange)). + Param("span", string(req.Span)). + ToJSON(&resp). + Fetch(ctx) + if err != nil { + return nil, err + } + return &resp, nil +} + +const baseUrl = "https://bitcoinity.org" + +func commonConfig(rb *requests.Builder) { + rb. + BaseURL(baseUrl). + Accept("application/json;charset=utf-8") +} diff --git a/bitcoinity/get_ticker.json b/bitcoinity/get_ticker.json new file mode 100644 index 0000000..44bdbed --- /dev/null +++ b/bitcoinity/get_ticker.json @@ -0,0 +1 @@ +{"ticker_life":604800,"volume_resolution":16800,"lasts":[[1709964000000,68475.175456638],[1709967600000,68626.4197800843],[1709971200000,68499.9257360174],[1709974800000,68421.009926165],[1709978400000,68294.0262620086],[1709982000000,68407.5464117156],[1709985600000,68472.031319471],[1709989200000,68572.9757857644],[1709992800000,68438.1716470681],[1709996400000,68426.1125233728],[1710000000000,68308.9215891684],[1710003600000,68285.7647690601],[1710007200000,68408.7159320073],[1710010800000,68397.6048978898],[1710014400000,68487.9638212605],[1710018000000,68496.5038475367],[1710021600000,68540.4920150493],[1710025200000,68506.8734373123],[1710028800000,68481.6942999111],[1710032400000,69040.7820535749],[1710036000000,69171.7318773701],[1710039600000,69086.1129943591],[1710043200000,69575.32472086],[1710046800000,69516.1427381054],[1710050400000,69398.9234834021],[1710054000000,69466.353745482],[1710057600000,69616.6701906639],[1710061200000,69686.1609871012],[1710064800000,69906.5009484715],[1710068400000,69783.6490516607],[1710072000000,69836.9576301872],[1710075600000,69718.7369308144],[1710079200000,69303.9592128747],[1710082800000,69371.2205952118],[1710086400000,69634.3811792792],[1710090000000,69585.0275914514],[1710093600000,69504.8172303926],[1710097200000,69435.8344990212],[1710100800000,69449.5420157399],[1710104400000,69353.4319500268],[1710108000000,68814.6601063395],[1710111600000,68774.335417703],[1710115200000,68234.883079493],[1710118800000,68090.5031206901],[1710122400000,68454.4265317288],[1710126000000,68510.4754354591],[1710129600000,68639.5654746363],[1710133200000,68672.389028808],[1710136800000,68787.9403844506],[1710140400000,70553.2960143432],[1710144000000,71366.3641423778],[1710147600000,71575.2726869142],[1710151200000,71693.5468379788],[1710154800000,71823.6197639393],[1710158400000,71879.4350659567],[1710162000000,71989.322576165],[1710165600000,72076.1832867116],[1710169200000,72380.7144313385],[1710172800000,72406.3048316385],[1710176400000,72486.7488551685],[1710180000000,72472.6563169571],[1710183600000,72421.582937338],[1710187200000,72148.610607249],[1710190800000,72293.6885360615],[1710194400000,72602.1145958952],[1710198000000,72270.8372805915],[1710201600000,72243.6425123162],[1710205200000,72295.753970148],[1710208800000,72267.0415535571],[1710212400000,71799.2409874039],[1710216000000,71807.5133778969],[1710219600000,71922.4876928696],[1710223200000,71823.8607063431],[1710226800000,72196.149507455],[1710230400000,71777.0029119539],[1710234000000,71942.6965307599],[1710237600000,71836.7117110554],[1710241200000,72129.2186949844],[1710244800000,71958.8280105228],[1710248400000,72103.2238484688],[1710252000000,72140.4368883156],[1710255600000,71680.1687418175],[1710259200000,70747.0218963375],[1710262800000,69831.5292617162],[1710266400000,71020.8227625354],[1710270000000,71510.0368509183],[1710273600000,71063.7243890294],[1710277200000,70913.5734506227],[1710280800000,71064.180027509],[1710284400000,71360.2118171413],[1710288000000,71491.3073949422],[1710291600000,71752.4674498549],[1710295200000,71982.5032592749],[1710298800000,72069.3411065831],[1710302400000,72134.9948118648],[1710306000000,72162.9927569381],[1710309600000,72494.363874665],[1710313200000,72953.1451456856],[1710316800000,73392.7700960008],[1710320400000,73430.4451953545],[1710324000000,73286.9601293485],[1710327600000,73304.5250382642],[1710331200000,72961.4163091399],[1710334800000,72543.1262683019],[1710338400000,72550.8794047277],[1710342000000,72770.2855180774],[1710345600000,72740.8119360035],[1710349200000,72986.6418843774],[1710352800000,73095.7647801359],[1710356400000,73134.5200663425],[1710360000000,73275.7428861803],[1710363600000,73214.3208155899],[1710367200000,73102.3823462233],[1710370800000,73069.2916192971],[1710374400000,73011.318660849],[1710378000000,72770.4073414757],[1710381600000,73040.8689109992],[1710385200000,73241.0739310015],[1710388800000,73253.0027495011],[1710392400000,73036.6778028695],[1710396000000,73457.9565808573],[1710399600000,73456.1906982901],[1710403200000,73453.4468810549],[1710406800000,73380.2477991086],[1710410400000,73144.2858733999],[1710414000000,72850.1741419157],[1710417600000,72776.0708790565],[1710421200000,72068.988906122],[1710424800000,71883.2157989861],[1710428400000,71177.4377271873],[1710432000000,70653.1071111006],[1710435600000,70974.5864070723],[1710439200000,70191.097809766],[1710442800000,69399.8909191733],[1710446400000,70395.5871379673],[1710450000000,70734.3573848902],[1710453600000,71410.2966559239],[1710457200000,71520.3538827916],[1710460800000,71827.4449974313],[1710464400000,71814.0100289761],[1710468000000,69467.4527672066],[1710471600000,68112.2789974225],[1710475200000,67742.8109533334],[1710478800000,67694.5728798296],[1710482400000,68093.6361699623],[1710486000000,68422.8101879976],[1710489600000,67741.0151388665],[1710493200000,66702.2557727286],[1710496800000,67611.2455601347],[1710500400000,67209.7504688287],[1710504000000,67682.4801633597],[1710507600000,68033.2811673063],[1710511200000,68168.6172453867],[1710514800000,68368.7297709666],[1710518400000,67958.0525079573],[1710522000000,68027.8200304039],[1710525600000,69120.2995542052],[1710529200000,69629.648031024],[1710532800000,68120.4746177027],[1710536400000,68125.3774427754],[1710540000000,68589.3493624246],[1710543600000,69296.4416776616],[1710547200000,69813.9583085164],[1710550800000,69453.7966481559],[1710554400000,69151.4217090963],[1710558000000,69212.3371480973],[1710561600000,69036.0448883277],[1710565200000,68805.393405542]],"mixed_lasts":null,"buys":[[1709964000000,68687.2],[1709967600000,68700.41],[1709971200000,68599.83],[1709974800000,68527.18],[1709978400000,68459.42],[1709982000000,68500.0],[1709985600000,68666.54],[1709989200000,68687.94],[1709992800000,68574.75],[1709996400000,68516.45],[1710000000000,68494.04],[1710003600000,68389.4],[1710007200000,68525.75],[1710010800000,68508.08],[1710014400000,68549.0],[1710018000000,68597.34],[1710021600000,68598.37],[1710025200000,68590.95],[1710028800000,68647.76],[1710032400000,69399.89],[1710036000000,69369.0],[1710039600000,69282.32],[1710043200000,69790.45],[1710046800000,69672.88],[1710050400000,69523.83],[1710054000000,69573.94],[1710057600000,69817.83],[1710061200000,69970.0],[1710064800000,70000.0],[1710068400000,69978.33],[1710072000000,69978.29],[1710075600000,69962.19],[1710079200000,69612.74],[1710082800000,69559.08],[1710086400000,69893.59],[1710090000000,69730.21],[1710093600000,69615.06],[1710097200000,69586.14],[1710100800000,69592.12],[1710104400000,69514.04],[1710108000000,69218.48],[1710111600000,69065.32],[1710115200000,69038.7],[1710118800000,68428.06],[1710122400000,68559.04],[1710126000000,68660.4],[1710129600000,68838.04],[1710133200000,68835.0],[1710136800000,69372.81],[1710140400000,71312.53],[1710144000000,71700.0],[1710147600000,71855.03],[1710151200000,71859.97],[1710154800000,72303.04],[1710158400000,72218.0],[1710162000000,72422.82],[1710165600000,72447.14],[1710169200000,72764.36],[1710172800000,72689.52],[1710176400000,72675.46],[1710180000000,72712.88],[1710183600000,72943.98],[1710187200000,72324.99],[1710190800000,72620.62],[1710194400000,72788.0],[1710198000000,72500.54],[1710201600000,72372.67],[1710205200000,72505.1],[1710208800000,72477.35],[1710212400000,72270.0],[1710216000000,72095.27],[1710219600000,72087.91],[1710223200000,72294.11],[1710226800000,72375.33],[1710230400000,72227.4],[1710234000000,72119.29],[1710237600000,72015.31],[1710241200000,72355.0],[1710244800000,72281.1],[1710248400000,72355.41],[1710252000000,73027.63],[1710255600000,72081.95],[1710259200000,71836.71],[1710262800000,71136.58],[1710266400000,71493.76],[1710270000000,71709.07],[1710273600000,71440.03],[1710277200000,71161.91],[1710280800000,71294.35],[1710284400000,71562.9],[1710288000000,71768.72],[1710291600000,72142.98],[1710295200000,72107.8],[1710298800000,72241.98],[1710302400000,72244.53],[1710306000000,72244.53],[1710309600000,72770.0],[1710313200000,73229.87],[1710316800000,73709.99],[1710320400000,73688.0],[1710324000000,73438.43],[1710327600000,73513.0],[1710331200000,73229.64],[1710334800000,73095.87],[1710338400000,72999.86],[1710342000000,73167.54],[1710345600000,73118.21],[1710349200000,73291.05],[1710352800000,73396.72],[1710356400000,73541.02],[1710360000000,73549.48],[1710363600000,73456.77],[1710367200000,73355.84],[1710370800000,73188.35],[1710374400000,73235.82],[1710378000000,72998.05],[1710381600000,73267.67],[1710385200000,73373.32],[1710388800000,73732.64],[1710392400000,73297.06],[1710396000000,73657.67],[1710399600000,73835.57],[1710403200000,73606.17],[1710406800000,73550.68],[1710410400000,73375.76],[1710414000000,73072.07],[1710417600000,73096.88],[1710421200000,72958.0],[1710424800000,72531.93],[1710428400000,72062.84],[1710432000000,71408.54],[1710435600000,71408.89],[1710439200000,71006.82],[1710442800000,70428.86],[1710446400000,71149.23],[1710450000000,71091.99],[1710453600000,71634.23],[1710457200000,71706.81],[1710460800000,72414.96],[1710464400000,72030.71],[1710468000000,71702.27],[1710471600000,69449.76],[1710475200000,68478.57],[1710478800000,68205.68],[1710482400000,68622.81],[1710486000000,68811.82],[1710489600000,68552.07],[1710493200000,68064.02],[1710496800000,68065.48],[1710500400000,67833.34],[1710504000000,67987.64],[1710507600000,68637.84],[1710511200000,68632.15],[1710514800000,68731.93],[1710518400000,68422.84],[1710522000000,68337.55],[1710525600000,70517.35],[1710529200000,70647.85],[1710532800000,69029.42],[1710536400000,68299.89],[1710540000000,69210.94],[1710543600000,69765.52],[1710547200000,70050.0],[1710550800000,69886.9],[1710554400000,69318.01],[1710558000000,69430.31],[1710561600000,69295.99],[1710565200000,69044.92]],"sells":[[1709964000000,68305.31],[1709967600000,68424.77],[1709971200000,68426.58],[1709974800000,68300.01],[1709978400000,68109.16],[1709982000000,68293.53],[1709985600000,68350.0],[1709989200000,68486.14],[1709992800000,68266.93],[1709996400000,68312.38],[1710000000000,68174.5],[1710003600000,68163.94],[1710007200000,68277.28],[1710010800000,68308.35],[1710014400000,68378.0],[1710018000000,68370.19],[1710021600000,68495.98],[1710025200000,68414.98],[1710028800000,68367.3],[1710032400000,68478.35],[1710036000000,68925.02],[1710039600000,68853.23],[1710043200000,69159.63],[1710046800000,69333.34],[1710050400000,69189.51],[1710054000000,69377.04],[1710057600000,69456.25],[1710061200000,69345.67],[1710064800000,69700.03],[1710068400000,69563.58],[1710072000000,69572.84],[1710075600000,69265.47],[1710079200000,68816.0],[1710082800000,69113.2],[1710086400000,69314.55],[1710090000000,69477.01],[1710093600000,69333.33],[1710097200000,69254.23],[1710100800000,69356.3],[1710104400000,69200.0],[1710108000000,68221.13],[1710111600000,68465.51],[1710115200000,67112.21],[1710118800000,67636.62],[1710122400000,68280.63],[1710126000000,68314.32],[1710129600000,68501.01],[1710133200000,68529.98],[1710136800000,68483.51],[1710140400000,69294.31],[1710144000000,71003.6],[1710147600000,71162.99],[1710151200000,71489.8],[1710154800000,71330.01],[1710158400000,71446.66],[1710162000000,71350.55],[1710165600000,71508.58],[1710169200000,72053.59],[1710172800000,71850.01],[1710176400000,72275.97],[1710180000000,72253.85],[1710183600000,71810.14],[1710187200000,71883.72],[1710190800000,72066.96],[1710194400000,72408.97],[1710198000000,72084.8],[1710201600000,72005.51],[1710205200000,71933.21],[1710208800000,72143.07],[1710212400000,71422.37],[1710216000000,71324.02],[1710219600000,71706.39],[1710223200000,71450.01],[1710226800000,72022.3],[1710230400000,71432.68],[1710234000000,71720.96],[1710237600000,71647.18],[1710241200000,71843.39],[1710244800000,71462.26],[1710248400000,71710.01],[1710252000000,71622.36],[1710255600000,70852.94],[1710259200000,70080.0],[1710262800000,68603.0],[1710266400000,70351.07],[1710270000000,71209.73],[1710273600000,70679.22],[1710277200000,70641.76],[1710280800000,70696.9],[1710284400000,71042.13],[1710288000000,71337.3],[1710291600000,71381.9],[1710295200000,71900.0],[1710298800000,71912.42],[1710302400000,71935.47],[1710306000000,72019.78],[1710309600000,72138.43],[1710313200000,72678.23],[1710316800000,72879.58],[1710320400000,73188.0],[1710324000000,73165.69],[1710327600000,73088.0],[1710331200000,72469.39],[1710334800000,71700.0],[1710338400000,72051.95],[1710342000000,72350.01],[1710345600000,72410.0],[1710349200000,72753.93],[1710352800000,72811.0],[1710356400000,72863.98],[1710360000000,73000.79],[1710363600000,73021.95],[1710367200000,72884.17],[1710370800000,72939.85],[1710374400000,72800.01],[1710378000000,72530.01],[1710381600000,72851.47],[1710385200000,73048.0],[1710388800000,72750.0],[1710392400000,72782.82],[1710396000000,73182.08],[1710399600000,73147.43],[1710403200000,73260.92],[1710406800000,73185.95],[1710410400000,72885.67],[1710414000000,72664.21],[1710417600000,72470.03],[1710421200000,71441.49],[1710424800000,71155.3],[1710428400000,70501.11],[1710432000000,69813.74],[1710435600000,70643.75],[1710439200000,69300.0],[1710442800000,68454.47],[1710446400000,69168.74],[1710450000000,70229.01],[1710453600000,71046.29],[1710457200000,71334.74],[1710460800000,71226.6],[1710464400000,71534.62],[1710468000000,67920.76],[1710471600000,66729.59],[1710475200000,66848.35],[1710478800000,67070.55],[1710482400000,67374.41],[1710486000000,68078.6],[1710489600000,66970.03],[1710493200000,65565.7],[1710496800000,67172.53],[1710500400000,66696.97],[1710504000000,67411.58],[1710507600000,67379.08],[1710511200000,67814.68],[1710514800000,68102.44],[1710518400000,67500.0],[1710522000000,67604.66],[1710525600000,68191.15],[1710529200000,68543.5],[1710532800000,67423.23],[1710536400000,67804.16],[1710540000000,68191.01],[1710543600000,68791.52],[1710547200000,69413.26],[1710550800000,69051.44],[1710554400000,68913.28],[1710558000000,68965.04],[1710561600000,68796.25],[1710565200000,68589.24]],"price_high":"73835.57","price_low":"65565.70","price_change":"0.48","volume":[[1709954400000,54140347.8057589],[1709971200000,128568714.5682704],[1709988000000,137917775.1257295],[1710004800000,87006824.6201314],[1710021600000,187487255.3040126],[1710038400000,169290240.3140007],[1710055200000,157582818.2607879],[1710072000000,167673416.09453028],[1710088800000,96365849.93703389],[1710105600000,281521608.01542497],[1710122400000,98723352.2583466],[1710139200000,604581446.7700331],[1710156000000,635688958.4949399],[1710172800000,660804644.6176729],[1710189600000,211919388.163924],[1710206400000,183866183.3168899],[1710223200000,154899518.7801852],[1710240000000,544534857.297393],[1710256800000,980404982.515352],[1710273600000,199575821.1975757],[1710290400000,158250334.28557873],[1710307200000,245624787.23434103],[1710324000000,379516869.0720066],[1710340800000,550914008.4902794],[1710357600000,193199059.4069826],[1710374400000,189148587.42480868],[1710391200000,203021814.1975727],[1710408000000,346281210.8443646],[1710424800000,831883113.0070027],[1710441600000,865377709.9212136],[1710458400000,635172581.4621086],[1710475200000,508256301.8164131],[1710492000000,565620807.0176792],[1710508800000,351742902.98374397],[1710525600000,636412289.7386522],[1710542400000,199245807.6176506],[1710559200000,43057372.559788495]]} diff --git a/bitcoinity/model.go b/bitcoinity/model.go new file mode 100644 index 0000000..d6c32f1 --- /dev/null +++ b/bitcoinity/model.go @@ -0,0 +1,101 @@ +package bitcoinity + +import ( + "encoding/json" + "fmt" +) + +var EmptyPayload json.RawMessage = []byte("{}") + +type MessageId string + +type Topic string + +const TopicAll Topic = "all" + +func MarketTopic(e Exchange, c Currency) Topic { + topic := fmt.Sprintf("webs:markets_%s_%s", e, c) + return Topic(topic) +} + +type Event string + +const ( + EventPhxJoin Event = "phx_join" + EventPhxReply Event = "phx_reply" + EventNewMsg Event = "new_msg" +) + +type GetTickerRequest struct { + Currency Currency + Exchange Exchange + Span Span +} + +type GetTickerResponse struct { + TickerLife int `json:"ticker_life"` + VolumeResolution int `json:"volume_resolution"` + PriceChange string `json:"price_change"` + PriceHigh string `json:"price_high"` + PriceLow string `json:"price_low"` + Buys []Trade `json:"buys"` + Sells []Trade `json:"sells"` + Lasts []Trade `json:"lasts"` + Volume []Volume `json:"volume"` +} + +type Currency string + +const ( + USD Currency = "USD" + EUR Currency = "EUR" + GBP Currency = "GBP" + AUD Currency = "AUD" + JPY Currency = "JPY" + CAD Currency = "CAD" +) + +type Exchange string + +const ( + Coinbase Exchange = "coinbase" + Bitfinex Exchange = "bitfinex" + Bitstamp Exchange = "bitstamp" + Kraken Exchange = "kraken" + Gemini Exchange = "gemini" +) + +type Span string + +const ( + Span10Minutes Span = "10m" + Span1Hour Span = "1h" + Span12Hours Span = "12h" + Span24Hours Span = "24h" + Span3Days Span = "3d" + Span7Days Span = "7d" + Span30Days Span = "30d" + Span6Months Span = "6m" + Span2Years Span = "2y" + SpanAll Span = "all" +) + +type Trade struct { + Timestamp int64 + Price float64 +} + +func (t *Trade) UnmarshalJSON(b []byte) error { + a := []interface{}{&t.Timestamp, &t.Price} + return json.Unmarshal(b, &a) +} + +type Volume struct { + Timestamp int64 + Size float64 +} + +func (v *Volume) UnmarshalJSON(b []byte) error { + a := []interface{}{&v.Timestamp, &v.Size} + return json.Unmarshal(b, &a) +} diff --git a/bitcoinity/model_test.go b/bitcoinity/model_test.go new file mode 100644 index 0000000..1b1b842 --- /dev/null +++ b/bitcoinity/model_test.go @@ -0,0 +1,23 @@ +package bitcoinity_test + +import ( + _ "embed" + "encoding/json" + "testing" + + "code.humancabbage.net/moonmath/bitcoinity" +) + +func TestUnmarshalGetTickerResponse(t *testing.T) { + var resp bitcoinity.GetTickerResponse + err := json.Unmarshal(getTickerJson, &resp) + if err != nil { + t.Errorf("failed to unmarshal get_ticker JSON: %v", err) + } + if resp.TickerLife != 604800 { + t.Errorf("expected TickerLife == 604800, not %v", resp.TickerLife) + } +} + +//go:embed get_ticker.json +var getTickerJson []byte diff --git a/bitcoinity/websocket.go b/bitcoinity/websocket.go new file mode 100644 index 0000000..c1c4061 --- /dev/null +++ b/bitcoinity/websocket.go @@ -0,0 +1,196 @@ +package bitcoinity + +import ( + "encoding/json" + "fmt" + "io" + "sync/atomic" + "time" + + "golang.org/x/net/websocket" +) + +type Websocket interface { + Join(Topic, Handler) error + Shutdown() error +} + +type Handler func(*Message) + +type Message struct { + Event Event `json:"event"` + Topic Topic `json:"topic"` + Ref *MessageId `json:"ref"` + Payload json.RawMessage `json:"payload"` +} + +type MarketPayload struct { + Data MarketData `json:"data"` +} + +type MarketData struct { + Currency Currency `json:"currency"` + Exchange Exchange `json:"exchange_name"` + Trade MarketTrade `json:"trade"` +} + +type MarketTrade struct { + Amount float64 `json:"amount"` + Date float64 `json:"date"` + Exchange Exchange `json:"exchange_name"` + Price float64 `json:"price"` +} + +type realWebsocket struct { + conn *websocket.Conn + inbox chan *Message + outbox chan outgoing + + pending map[MessageId]Handler + subscriptions map[Topic]Handler + nextMsgId int64 + + running atomic.Bool +} + +type outgoing struct { + Request *Message + ResponseHandler Handler + TopicHandler Handler +} + +func newWebsocket(conn *websocket.Conn) *realWebsocket { + ws := realWebsocket{ + conn: conn, + inbox: make(chan *Message), + outbox: make(chan outgoing), + pending: map[MessageId]Handler{}, + subscriptions: map[Topic]Handler{}, + } + ws.running.Store(true) + + go ws.heartbeat() + go ws.reader() + go ws.mailman() + return &ws +} + +func (ws *realWebsocket) heartbeat() { + for ws.running.Load() { + ws.outbox <- outgoing{ + Request: &Message{ + Topic: "phoenix", + Event: "heartbeat", + }, + } + time.Sleep(time.Second * 30) + } +} + +func (ws *realWebsocket) reader() { + var partial []byte + failures := 0 + + for ws.running.Load() { + buf := make([]byte, 16*1024) + n, err := ws.conn.Read(buf) + if err != nil { + if err == io.EOF { + ws.running.Store(false) + return + } + fmt.Printf("websocket read: %v\n", err) + continue + } + + useful := buf[0:n] + partial = append(partial, useful...) + + var msg Message + err = json.Unmarshal(partial, &msg) + if err != nil { + failures += 1 + continue + } + failures = 0 + partial = nil + ws.inbox <- &msg + } +} + +func (ws *realWebsocket) mailman() { + for ws.running.Load() { + select { + case i := <-ws.inbox: + ws.dispatch(i) + case o := <-ws.outbox: + _ = ws.send(o) + } + } +} + +func (ws *realWebsocket) dispatch(m *Message) { + var handler Handler + if m.Ref != nil { + handler = ws.pending[*m.Ref] + delete(ws.pending, *m.Ref) + } else if m.Topic != "" { + handler = ws.subscriptions[m.Topic] + } + if handler != nil { + handler(m) + } +} + +func (ws *realWebsocket) send(o outgoing) error { + if o.Request.Ref == nil { + msgId := ws.nextMsgId + 1 + ws.nextMsgId += 1 + msgIdStr := fmt.Sprintf("%d", msgId) + o.Request.Ref = (*MessageId)(&msgIdStr) + } + req, err := json.Marshal(o.Request) + if err != nil { + return err + } + _, err = ws.conn.Write(req) + if err != nil { + return err + } + if o.ResponseHandler != nil { + ws.pending[*o.Request.Ref] = o.ResponseHandler + } + if o.TopicHandler != nil { + ws.subscriptions[o.Request.Topic] = o.TopicHandler + } + return nil +} + +func (ws *realWebsocket) Join(topic Topic, topicHandler Handler) error { + respChan, respHandler := oneShot() + ws.outbox <- outgoing{ + Request: &Message{ + Event: EventPhxJoin, + Topic: topic, + Payload: EmptyPayload, + }, + ResponseHandler: respHandler, + TopicHandler: topicHandler, + } + <-respChan + + return nil +} + +func (ws *realWebsocket) Shutdown() error { + ws.running.Store(false) + err := ws.conn.Close() + return err +} + +func oneShot() (<-chan *Message, Handler) { + pipe := make(chan *Message) + return pipe, func(msg *Message) { + pipe <- msg + } +} diff --git a/coindesk/lib_test.go b/coindesk/lib_test.go new file mode 100644 index 0000000..4a205d3 --- /dev/null +++ b/coindesk/lib_test.go @@ -0,0 +1,29 @@ +package coindesk_test + +import ( + "context" + "fmt" + "testing" + "time" + + "code.humancabbage.net/moonmath/coindesk" +) + +func TestXxx(t *testing.T) { + now := time.Now() + then := now.Add(time.Duration(-24) * time.Hour) + + values, err := coindesk.GetPriceValues(context.Background(), coindesk.BTC, then, now) + if err != nil { + t.Errorf("test failure: %v", err) + } + _ = values + fmt.Println() + + tickers, err := coindesk.GetAssetTickers(context.Background(), coindesk.BTC, coindesk.ETH) + if err != nil { + t.Errorf("test failure: %v", err) + } + _ = tickers + fmt.Println() +} diff --git a/coindesk/model.go b/coindesk/model.go new file mode 100644 index 0000000..72c0e40 --- /dev/null +++ b/coindesk/model.go @@ -0,0 +1,112 @@ +package coindesk + +import ( + "encoding/json" + "strconv" + "strings" + "time" +) + +// Asset is a cryptocurrency, like Bitcoin, Ethereum, etc. +type Asset string + +const ( + // BTC is the Bitcoin asset. + BTC Asset = "BTC" + // ETH is the Ethereum asset. + ETH Asset = "ETH" +) + +// Response represents the general top-level format of Coindesk API responses. +type Response[T any] struct { + StatusCode int `json:"statusCode"` + Message string `json:"message"` + Data T `json:"data"` +} + +// PriceValues contains a series of timestamped prices for a particular asset. +type PriceValues struct { + ISO Asset `json:"iso"` + Name string `json:"name"` + Slug string `json:"slug"` + IngestionStart Date `json:"ingestionStart"` + Entries []TimestampPrice `json:"entries"` +} + +// AssetTickers is a map from an asset to its ticker data. +type AssetTickers map[Asset]AssetTicker + +// AssetTicker is a snapshot of pricing data for an asset. +type AssetTicker struct { + ISO Asset `json:"iso"` + Name string `json:"name"` + Slug string `json:"slug"` + Change struct { + Percent float64 `json:"percent"` + Value float64 `json:"value"` + } `json:"change"` + OHLC struct { + Opening Price `json:"o"` + High Price `json:"h"` + Low Price `json:"l"` + Closing Price `json:"c"` + } `json:"ohlc"` + CirculatingSupply float64 `json:"circulatingSupply"` + MarketCap Price `json:"marketCap"` + Timestamp Timestamp `json:"ts"` +} + +// TimestampPrice represents a JSON array with two elements: an integer Unix +// timestamp expressed in milliseconds, and a floating-point USD price. +type TimestampPrice struct { + Timestamp Timestamp + Price Price +} + +func (t *TimestampPrice) UnmarshalJSON(b []byte) error { + a := []interface{}{&t.Timestamp, &t.Price} + return json.Unmarshal(b, &a) +} + +// Timestamp represents an integer Unix timestamp expressed in milliseconds +// which has been converted into a Golang time.Time object. +type Timestamp time.Time + +func (t *Timestamp) UnmarshalJSON(b []byte) error { + s := string(b) + n := len(s) + secsStr := s[0 : n-3] + millisStr := s[n-3:] + secs, err := strconv.ParseInt(secsStr, 10, 64) + if err != nil { + return err + } + millis, err := strconv.ParseInt(millisStr, 10, 64) + if err != nil { + return err + } + + converted := time.Unix(secs, millis*1e6) + *t = Timestamp(converted) + + return nil +} + +// Price represents the USD price of an asset. +type Price float64 + +// Date represents a date-only string which has been converted into a Golang +// time.Time object. +type Date time.Time + +func (d *Date) UnmarshalJSON(b []byte) error { + s := string(b) + s, _ = strings.CutPrefix(s, "\"") + s, _ = strings.CutSuffix(s, "\"") + t, err := time.Parse(time.DateOnly, s) + if err != nil { + return err + } + *d = Date(t) + return nil +} diff --git a/coindesk/requests.go b/coindesk/requests.go new file mode 100644 index 0000000..367d83b --- /dev/null +++ b/coindesk/requests.go @@ -0,0 +1,48 @@ +package coindesk + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/carlmjohnson/requests" +) + +// GetPriceValues gets timestamped prices for a particular asset. +func GetPriceValues( + ctx context.Context, asset Asset, startDate, endDate time.Time, +) (resp Response[PriceValues], err error) { + const basePath = "v2/tb/price/values" + const timeFormat = "2006-01-02T15:04" + err = requests.New(commonConfig). + Path(fmt.Sprintf("%s/%s", basePath, asset)). + Param("start_date", startDate.Format(timeFormat)). + Param("end_date", endDate.Format(timeFormat)). + ToJSON(&resp). + Fetch(ctx) + return +} + +// GetAssetTickers gets tickers for a set of assets. +func GetAssetTickers(ctx context.Context, assets ...Asset) (resp Response[AssetTickers], err error) { + const basePath = "v2/tb/price/ticker" + var strAssets []string + for _, asset := range assets { + strAssets = append(strAssets, string(asset)) + } + err = requests.New(commonConfig). + Path(basePath). + Param("assets", strings.Join(strAssets, ",")). + ToJSON(&resp). + Fetch(ctx) + return +} + +const baseUrl = "https://production.api.coindesk.com" + +func commonConfig(rb *requests.Builder) { + rb. + BaseURL(baseUrl). + Accept("application/json;charset=utf-8") +} diff --git a/example.json b/example.json new file mode 100644 index 0000000..ded1e51 --- /dev/null +++ b/example.json @@ -0,0 +1,217 @@ +[ + { + "event": "phx_join", + "payload": {}, + "ref": "1", + "topic": "all" + }, + { + "event": "phx_join", + "payload": {}, + "ref": "2", + "topic": "webs:markets" + }, + { + "event": "phx_join", + "payload": {}, + "ref": "3", + "topic": "webs:markets_coinbase_USD" + }, + { + "event": "phx_reply", + "payload": { + "response": {}, + "status": "ok" + }, + "ref": "1", + "topic": "all" + }, + { + "event": "phx_reply", + "payload": { + "response": {}, + "status": "ok" + }, + "ref": "2", + "topic": "webs:markets" + }, + { + "event": "phx_reply", + "payload": { + "response": {}, + "status": "ok" + }, + "ref": "3", + "topic": "webs:markets_coinbase_USD" + }, + { + "event": "new_msg", + "payload": { + "data": { + "currency": "USD", + "exchange_name": "coinbase", + "trade": { + "amount": 7.626e-5, + "date": 1710454598.412, + "exchange_name": "coinbase", + "price": 71296.64 + } + } + }, + "ref": null, + "topic": "webs:markets_coinbase_USD" + }, + { + "event": "new_msg", + "payload": { + "data": { + "currency": "USD", + "exchange_name": "coinbase", + "trade": { + "amount": 2.1277e-4, + "date": 1710454598.529, + "exchange_name": "coinbase", + "price": 71296.51 + } + } + }, + "ref": null, + "topic": "webs:markets_coinbase_USD" + }, + { + "event": "new_msg", + "payload": { + "data": { + "currency": "USD", + "exchange_name": "coinbase", + "trade": { + "amount": 9.298e-5, + "date": 1710454598.571, + "exchange_name": "coinbase", + "price": 71290.31 + } + } + }, + "ref": null, + "topic": "webs:markets_coinbase_USD" + }, + { + "event": "new_msg", + "payload": { + "data": { + "currency": "USD", + "exchange_name": "coinbase", + "trade": { + "amount": 0.0280536, + "date": 1710454599.14, + "exchange_name": "coinbase", + "price": 71282.41 + } + } + }, + "ref": null, + "topic": "webs:markets_coinbase_USD" + }, + { + "event": "new_msg", + "payload": { + "data": { + "currency": "USD", + "exchange_name": "coinbase", + "trade": { + "amount": 0.00179797, + "date": 1710454599.739, + "exchange_name": "coinbase", + "price": 71279.04 + } + } + }, + "ref": null, + "topic": "webs:markets_coinbase_USD" + }, + { + "event": "new_msg", + "payload": { + "data": { + "currency": "USD", + "exchange_name": "coinbase", + "trade": { + "amount": 6.6664e-4, + "date": 1710454599.969, + "exchange_name": "coinbase", + "price": 71279.03 + } + } + }, + "ref": null, + "topic": "webs:markets_coinbase_USD" + }, + { + "event": "new_msg", + "payload": { + "data": { + "currency": "USD", + "exchange_name": "coinbase", + "trade": { + "amount": 0.00269708, + "date": 1710454600.174, + "exchange_name": "coinbase", + "price": 71278.99 + } + } + }, + "ref": null, + "topic": "webs:markets_coinbase_USD" + }, + { + "event": "new_msg", + "payload": { + "data": { + "currency": "USD", + "exchange_name": "coinbase", + "trade": { + "amount": 0.00707958, + "date": 1710454600.722, + "exchange_name": "coinbase", + "price": 71268.25 + } + } + }, + "ref": null, + "topic": "webs:markets_coinbase_USD" + }, + { + "event": "new_msg", + "payload": { + "data": { + "currency": "USD", + "exchange_name": "coinbase", + "trade": { + "amount": 1.6783e-4, + "date": 1710454600.955, + "exchange_name": "coinbase", + "price": 71273.11 + } + } + }, + "ref": null, + "topic": "webs:markets_coinbase_USD" + }, + { + "event": "new_msg", + "payload": { + "data": { + "currency": "USD", + "exchange_name": "coinbase", + "trade": { + "amount": 0.00134701, + "date": 1710454601.033, + "exchange_name": "coinbase", + "price": 71274.89 + } + } + }, + "ref": null, + "topic": "webs:markets_coinbase_USD" + } +] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..df8bcb7 --- /dev/null +++ b/go.mod @@ -0,0 +1,32 @@ +module code.humancabbage.net/moonmath + +go 1.22.1 + +require ( + github.com/carlmjohnson/requests v0.23.5 + github.com/charmbracelet/bubbles v0.18.0 + github.com/charmbracelet/bubbletea v0.25.0 + github.com/charmbracelet/lipgloss v0.10.0 + github.com/sourcegraph/conc v0.3.0 + golang.org/x/net v0.22.0 +) + +require ( + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/containerd/console v1.0.4 // 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/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/rivo/uniseg v0.4.7 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e21b16d --- /dev/null +++ b/go.sum @@ -0,0 +1,62 @@ +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= +github.com/carlmjohnson/requests v0.23.5/go.mod h1:zG9P28thdRnN61aD7iECFhH5iGGKX2jIjKQD9kqYH+o= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= +github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= +github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +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/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= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +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/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= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +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/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= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +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/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/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +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/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/moon/moon.go b/moon/moon.go new file mode 100644 index 0000000..a1f87b3 --- /dev/null +++ b/moon/moon.go @@ -0,0 +1,164 @@ +package moon + +import ( + "context" + "math" + "time" + + "code.humancabbage.net/moonmath/coindesk" + "github.com/sourcegraph/conc/pool" +) + +type Math struct { + CurrentPrice coindesk.Price + Columns []Column + Goals []float64 +} + +func NewMath(goals []float64, bases []Base) (m Math) { + if goals == nil { + goals = DefaultGoals + } + if bases == nil { + bases = DefaultBases + } + m.Goals = goals + m.Columns = make([]Column, len(bases)) + for i := range bases { + m.Columns[i].Base = bases[i] + } + 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 []float64, +) (p Projection) { + if cdpr <= 0 { + return + } + logP := math.Log(currentPrice) + logR := math.Log(cdpr) + for _, goal := range goals { + daysToGo := (math.Log(goal) - 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, coindesk.BTC) + if err != nil { + return + } + m.CurrentPrice = resp.Data[coindesk.BTC].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, + coindesk.BTC, 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 +} + +var DefaultGoals = []float64{ + 100000, + 150000, + 200000, + 250000, + 300000, + 500000, + 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)}, +} + +// Base is a temporal point of comparison used for price projection. +type Base interface { + From(now time.Time) time.Time + Name() string +} + +// ConstantBase is a base that is a constant time, e.g. 2020-01-01. +type ConstantBase struct { + name string + time time.Time +} + +func (cb ConstantBase) From(_ time.Time) time.Time { + return cb.time +} + +func (cb ConstantBase) Name() string { + return cb.name +} + +// RelativeBase is a base that is relative, e.g. "90 days ago." +type RelativeBase struct { + name string + offset time.Duration +} + +func (rb RelativeBase) From(now time.Time) time.Time { + then := now.Add(time.Duration(rb.offset)) + return then +} + +func (rb RelativeBase) Name() string { + return rb.name +} diff --git a/moon/moon_test.go b/moon/moon_test.go new file mode 100644 index 0000000..19b6543 --- /dev/null +++ b/moon/moon_test.go @@ -0,0 +1,23 @@ +package moon_test + +import ( + "testing" + "time" + + "code.humancabbage.net/moonmath/moon" +) + +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 +} diff --git a/moonmath.go b/moonmath.go new file mode 100644 index 0000000..54ef8ba --- /dev/null +++ b/moonmath.go @@ -0,0 +1,145 @@ +package main + +import ( + "context" + "fmt" + "os" + "time" + + "code.humancabbage.net/moonmath/moon" + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +var baseStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("240")) + +type model struct { + math moon.Math + + prices table.Model + projections table.Model +} + +func (m model) Init() tea.Cmd { + return func() tea.Msg { + _ = m.math.Refresh(context.TODO()) + return m.math + } +} + +func initialModel() model { + math := moon.NewMath(nil, nil) + + tableStyle := table.DefaultStyles() + tableStyle.Selected = lipgloss.NewStyle() + prices := table.New( + table.WithColumns([]table.Column{ + {Title: "Price", Width: 9}, + }), + table.WithHeight(1), + table.WithStyles(tableStyle), + ) + + projectionCols := []table.Column{ + {Title: "Labels", Width: 8}, + } + for i := range math.Columns { + projectionCols = append(projectionCols, table.Column{ + Title: math.Columns[i].Base.Name(), + Width: 10, + }) + } + projectionRows := make([]table.Row, len(math.Goals)+1) + for i := range projectionRows { + projectionRows[i] = make(table.Row, len(projectionCols)) + } + projectionRows[0][0] = "CDPR" + for i := range math.Goals { + projectionRows[i+1][0] = fmt.Sprintf("$%.0f", math.Goals[i]) + } + projections := table.New( + table.WithColumns(projectionCols), + table.WithRows(projectionRows), + table.WithHeight(len(math.Goals)+1), + table.WithStyles(tableStyle), + ) + + return model{ + math: math, + prices: prices, + projections: projections, + } +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case moon.Math: + m.math = msg + refillPrice(&m) + refillProjections(&m) + return m, tea.Tick(time.Second*30, func(t time.Time) tea.Msg { + _ = m.math.Refresh(context.TODO()) + return m.math + }) + case tea.KeyMsg: + switch msg.String() { + case "ctrl+c", "q", "esc": + return m, tea.Quit + } + } + return m, nil +} + +func refillPrice(m *model) { + rows := []table.Row{ + []string{fmt.Sprintf("$%0.2f", m.math.CurrentPrice)}, + } + m.prices.SetRows(rows) +} + +func refillProjections(m *model) { + rows := m.projections.Rows() + + for col := range m.math.Columns { + _ = col + never := false + if m.math.Columns[col].CDPR <= 1 { + never = true + } + + rows[0][col+1] = fmt.Sprintf("%.2f%%", (m.math.Columns[col].CDPR-1)*100) + for row := 0; row < len(m.math.Goals); row++ { + var cell string + if never { + cell = "NEVER!!!!!" + } else { + cell = m.math.Columns[col]. + Projections.Dates[row]. + Format("2006-01-02") + } + rows[row+1][col+1] = cell + } + } + m.projections.SetRows(rows) +} + +func (m model) View() string { + var s string + s += lipgloss.JoinHorizontal( + lipgloss.Top, + baseStyle.Render(m.prices.View()), + baseStyle.Render(m.projections.View()), + ) + return s + "\n" +} + +func main() { + p := tea.NewProgram(initialModel()) + if _, err := p.Run(); err != nil { + fmt.Printf("program error: %v\n", err) + os.Exit(1) + } +} diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..26f9b3644221ffc7ead14da1dda948b292a23ec7 GIT binary patch literal 51895 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 literal 0 HcmV?d00001