Skip to content

Commit

Permalink
feat: added support for CoinCap as a data source for crypto price quotes
Browse files Browse the repository at this point in the history
  • Loading branch information
achannarasappa committed Apr 30, 2024
1 parent fb847bf commit 987c9c9
Show file tree
Hide file tree
Showing 11 changed files with 221 additions and 3 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ require (
github.com/charmbracelet/lipgloss v0.5.0 // indirect
github.com/containerd/console v1.0.3 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
Expand All @@ -47,5 +49,6 @@ require (
golang.org/x/sys v0.1.0 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.5 // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
)
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
Expand Down Expand Up @@ -213,6 +214,7 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
Expand Down Expand Up @@ -711,6 +713,7 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
9 changes: 8 additions & 1 deletion internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,14 @@ func getSymbolAndSource(symbol string, tickerSymbolToSourceSymbol symbol.TickerS
if strings.HasSuffix(symbolUppercase, ".CG") {
return symbolSource{
source: c.QuoteSourceCoingecko,
symbol: strings.TrimSuffix(strings.ToLower(symbol), ".cg"),
symbol: strings.ToLower(symbol)[:len(symbol)-3],
}
}

if strings.HasSuffix(symbolUppercase, ".CC") {
return symbolSource{
source: c.QuoteSourceCoinCap,
symbol: strings.ToLower(symbol)[:len(symbol)-3],
}
}

Expand Down
7 changes: 7 additions & 0 deletions internal/cli/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ var _ = Describe("Cli", func() {
"watchlist:",
" - TSLA", // yahoo finance
" - ETHEREUM.CG", // coingecko
" - BITCOIN.CC", // coincap
" - SOL.X", // ticker
}, "\n"),
AssertionErr: BeNil(),
Expand All @@ -273,6 +274,12 @@ var _ = Describe("Cli", func() {
}),
"Source": Equal(c.QuoteSourceCoingecko),
}),
"4": g.MatchFields(g.IgnoreExtras, g.Fields{
"Symbols": g.MatchAllElementsWithIndex(g.IndexIdentity, g.Elements{
"0": Equal("bitcoin"),
}),
"Source": Equal(c.QuoteSourceCoinCap),
}),
}),
}),
}),
Expand Down
1 change: 1 addition & 0 deletions internal/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ const (
QuoteSourceUserDefined
QuoteSourceCoingecko
QuoteSourceUnknown
QuoteSourceCoinCap
)

// AssetQuote represents a price quote and related attributes for a single security
Expand Down
29 changes: 29 additions & 0 deletions internal/quote/coincap/coincap_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package coincap_test

import (
"testing"

"github.com/go-resty/resty/v2"
"github.com/jarcoal/httpmock"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var client = resty.New()

var _ = BeforeSuite(func() {
httpmock.ActivateNonDefault(client.GetClient())
})

var _ = BeforeEach(func() {
httpmock.Reset()
})

var _ = AfterSuite(func() {
httpmock.DeactivateAndReset()
})

func TestQuote(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "CoinCap Quote Suite")
}
65 changes: 65 additions & 0 deletions internal/quote/coincap/coincap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package coincap_test

import (
"net/http"

"github.com/jarcoal/httpmock"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"

c "github.com/achannarasappa/ticker/internal/common"
. "github.com/achannarasappa/ticker/internal/quote/coincap"
g "github.com/onsi/gomega/gstruct"
)

var _ = Describe("CoinCap Quote", func() {
Describe("GetAssetQuotes", func() {
It("should make a request to get stock quotes and transform the response", func() {
responseFixture := `{
"data": [
{
"id": "bitcoin",
"rank": "1",
"symbol": "BTC",
"name": "Bitcoin",
"supply": "19685775.0000000000000000",
"maxSupply": "21000000.0000000000000000",
"marketCapUsd": "1248489381324.9592799671502700",
"volumeUsd24Hr": "7744198446.5431034815177485",
"priceUsd": "63420.8905326287270868",
"changePercent24Hr": "1.3622077494913284",
"vwap24Hr": "62988.1090433238215198",
"explorer": "https://blockchain.info/"
}
],
"timestamp": 1714453771801
}`
responseUrl := `=~\/v2\/assets.*ids\=bitcoin.*`
httpmock.RegisterResponder("GET", responseUrl, func(req *http.Request) (*http.Response, error) {
resp := httpmock.NewStringResponse(200, responseFixture)
resp.Header.Set("Content-Type", "application/json")
return resp, nil
})

output := GetAssetQuotes(*client, []string{"bitcoin"})
Expect(output).To(g.MatchAllElementsWithIndex(g.IndexIdentity, g.Elements{
"0": g.MatchFields(g.IgnoreExtras, g.Fields{
"QuotePrice": g.MatchFields(g.IgnoreExtras, g.Fields{
"Price": Equal(63420.89053262873),
"PricePrevClose": Equal(64284.8148182606),
"PriceOpen": Equal(0.0),
"PriceDayHigh": Equal(0.0),
"PriceDayLow": Equal(0.0),
"Change": Equal(863.9242856318742),
"ChangePercent": Equal(1.3622077494913285),
}),
"QuoteSource": Equal(c.QuoteSourceCoinCap),
"Exchange": g.MatchFields(g.IgnoreExtras, g.Fields{
"IsActive": BeTrue(),
"IsRegularTradingSession": BeTrue(),
}),
}),
}))
})
})
})
95 changes: 95 additions & 0 deletions internal/quote/coincap/quote.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package coincap

import (
"strconv"
"strings"

c "github.com/achannarasappa/ticker/internal/common"
"github.com/go-resty/resty/v2"
)

// Quote represents a quote of a single security from the API response
type Quote struct {
ShortName string `json:"name"`
Symbol string `json:"symbol"`
RegularMarketChangePercent string `json:"changePercent24Hr"`
RegularMarketPrice string `json:"priceUsd"`
RegularMarketVolume string `json:"volumeUsd24Hr"`
MarketCap string `json:"marketCapUsd"`
}

// Response represents the container object from the API response
type Response struct {
Data []Quote `json:"data"`
}

func transformQuote(responseQuote Quote) c.AssetQuote {

price, _ := strconv.ParseFloat(responseQuote.RegularMarketPrice, 64)
changePercent, _ := strconv.ParseFloat(responseQuote.RegularMarketChangePercent, 64)
pricePrevClose := (1 + changePercent/100) * price
marketCap, _ := strconv.ParseFloat(responseQuote.MarketCap, 64)
volume, _ := strconv.ParseFloat(responseQuote.RegularMarketVolume, 64)

assetQuote := c.AssetQuote{
Name: responseQuote.ShortName,
Symbol: responseQuote.Symbol,
Class: c.AssetClassCryptocurrency,
Currency: c.Currency{
FromCurrencyCode: "USD",
},
QuotePrice: c.QuotePrice{
Price: price,
PricePrevClose: pricePrevClose,
PriceOpen: 0.0,
PriceDayHigh: 0.0,
PriceDayLow: 0.0,
Change: pricePrevClose - price,
ChangePercent: changePercent,
},
QuoteExtended: c.QuoteExtended{
FiftyTwoWeekHigh: 0.0,
FiftyTwoWeekLow: 0.0,
MarketCap: marketCap,
Volume: volume,
},
QuoteSource: c.QuoteSourceCoinCap,
Exchange: c.Exchange{
Name: "Crypto Aggregate via CoinCap",
Delay: 0,
State: c.ExchangeStateOpen,
IsActive: true,
IsRegularTradingSession: true,
},
Meta: c.Meta{
IsVariablePrecision: true,
},
}

return assetQuote

}

func transformQuotes(responseQuotes []Quote) []c.AssetQuote {

quotes := make([]c.AssetQuote, 0)
for _, responseQuote := range responseQuotes {
quotes = append(quotes, transformQuote(responseQuote))
}

return quotes

}

// GetAssetQuotes issues a HTTP request to retrieve quotes from the API and process the response
func GetAssetQuotes(client resty.Client, symbols []string) []c.AssetQuote {
symbolsString := strings.Join(symbols, ",")

res, _ := client.R().
SetResult(Response{}).
SetQueryParam("ids", strings.ToLower(symbolsString)).
Get("https://api.coincap.io/v2/assets")

return transformQuotes((res.Result().(*Response)).Data) //nolint:forcetypeassert

}
4 changes: 2 additions & 2 deletions internal/quote/quote.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ func GetAssetGroupQuote(dep c.Dependencies) func(c.AssetGroup) c.AssetGroupQuote

for _, symbolBySource := range assetGroup.SymbolsBySource {

assetQuotebySource := getQuoteBySource(dep, symbolBySource)
assetQuotes = append(assetQuotes, assetQuotebySource...)
assetQuoteBySource := getQuoteBySource(dep, symbolBySource)
assetQuotes = append(assetQuotes, assetQuoteBySource...)

}

Expand Down
4 changes: 4 additions & 0 deletions internal/ui/util/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ func getPrecision(f float64) int {
return 3
}

if v >= 1000 && f < 0 {
return 1
}

return 2
}

Expand Down
4 changes: 4 additions & 0 deletions internal/ui/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ var _ = Describe("Util", func() {
output := ConvertFloatToString(0.0, true)
Expect(output).To(Equal("0.00"))
})
It("should set a precision of one when the value is negative and over 1000", func() {
output := ConvertFloatToString(-2000.0, true)
Expect(output).To(Equal("-2000.0"))
})
It("should set a precision of zero when the value is over 10000", func() {
output := ConvertFloatToString(10000.0, true)
Expect(output).To(Equal("10000"))
Expand Down

0 comments on commit 987c9c9

Please sign in to comment.