Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
fogodev committed Jun 20, 2020
0 parents commit 611a19d
Show file tree
Hide file tree
Showing 8 changed files with 902 additions and 0 deletions.
594 changes: 594 additions & 0 deletions .gitignore

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# B3 Lib

Dragon: "My name is Balthromaw. Breaker of skies, slayer of mountain."
Rick: "Rule 1: You're now scooper of your own poops, or I will take you down like the black-light poster you are."

![Stonks](https://i.kym-cdn.com/photos/images/newsfeed/001/499/826/2f0.png)

#### From Bezunca Labs
116 changes: 116 additions & 0 deletions b3lib.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package b3lib

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)

// New function to setup B3 fetch and cache prices
func New(cacheTimeout time.Duration, client *http.Client) func(tickers []string) ([]FetchedPrice, []error) {

cache := make(map[string]tickerEntry)

return func(tickers []string) (prices []FetchedPrice, err []error) {

var newTickers []string
for _, ticker := range tickers {
if entry, ok := cache[ticker]; !ok || time.Since(entry.Timestamp) > cacheTimeout {
newTickers = append(newTickers, ticker)
} else {
prices = append(prices, entry.Price)
}
}

var fetched []FetchedPrice
if len(newTickers) > 0 {
fetched, err = fetchNewPrices(client, newTickers)
for _, price := range fetched {
cache[price.Ticker] = tickerEntry{
Price: price,
Timestamp: time.Now(),
}
}
}

return append(prices, fetched...), err
}
}

func fetchNewPrices(client *http.Client, tickers []string) ([]FetchedPrice, []error) {
today := time.Now()
todayWeekday := today.Weekday()
if todayWeekday == time.Sunday {
today = today.Add(-48 * time.Hour)
} else if todayWeekday == time.Saturday || today.Hour() < 6 {
// B3 only got prices values after market opening, so before 6AM, we get yesterday values
// If it's after 6AM, we zero the prices values, waiting for market opening
today = today.Add(-24 * time.Hour)
}
date := today.Format("2006-01-02")

fetchedPricesChan := make(chan FetchedPrice, len(tickers))
errChan := make(chan error)
defer func() {
close(fetchedPricesChan)
close(errChan)
}()

for _, ticker := range tickers {
go getCurrentPrice(client, date, ticker, fetchedPricesChan, errChan)
}

var fetchedPrices []FetchedPrice
var errors []error
for i := 0; i < len(tickers); i++ {
select {
case fetchedPrice := <-fetchedPricesChan:
fetchedPrices = append(fetchedPrices, fetchedPrice)
case err := <-errChan:
errors = append(errors, err)
}
}

if len(errors) != 0 {
return nil, errors
}

return fetchedPrices, nil
}

func getCurrentPrice(client *http.Client, date string, ticker string, prices chan<- FetchedPrice, errChan chan<- error) {
url := fmt.Sprintf("https://arquivos.b3.com.br/apinegocios/ticker/%v/%v", ticker, date)
response, err := client.Get(url)
if err != nil {
errChan <- &FetchError{ticker, date, err}
return
}
defer func() {
if err = response.Body.Close(); err != nil {
errChan <- &CloseBodyError{ticker, date, err}
}
}()

b3Response := new(b3PriceResponse)
decoder := json.NewDecoder(response.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(b3Response); err != nil {
errChan <- &JSONDecodeError{ticker, date, err}
return
}

price := 0
if len(b3Response.Values) != 0 {
price, _ = strconv.Atoi(
strings.ReplaceAll(fmt.Sprint(b3Response.Values[0][2].(float64)), ".", ""),
)
}

prices <- FetchedPrice{
Ticker: b3Response.Name,
IntPrice: price,
}
}
33 changes: 33 additions & 0 deletions b3lib_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package b3lib

import (
"net/http"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestFetch(t *testing.T) {

fetch := New(5*time.Second, http.DefaultClient)

tickers := []string{"ITSA4", "HSML11", "VRTA11"}

prices, errs := fetch(tickers)
t.Logf("Prices: %+v\nErrors: %#v", prices, errs)

t.Logf("Trying ITSA4 again with cache")
prices2, errs2 := fetch([]string{"ITSA4", "CVCB3"})
t.Logf("Prices: %+v\nErrors: %#v", prices2, errs2)

t.Logf("Sleeping")
<-time.After(6 * time.Second)
t.Logf("Woke")

t.Logf("Trying ITSA4 again without cache")
prices3, errs3 := fetch([]string{"ITSA4"})
t.Logf("Prices: %+v\nErrors: %#v", prices3, errs3)

assert.Equal(t, len(tickers)+1, len(prices)+len(errs)+1)
}
99 changes: 99 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package b3lib

import (
"errors"
"fmt"
)

// FetchError for when we had an error while fetching a quotation from B3
type FetchError struct {
Ticker string
Date string
Err error
}

// Error method to comply with error interface
func (e *FetchError) Error() string {
return fmt.Sprintf("failed to fetch B3 quotation for %v on %v: %v", e.Ticker, e.Date, e.Err)
}

// Unwrap method to comply with errors.Unwrap
func (e *FetchError) Unwrap() error {
return e.Err
}

// Is method to comply with errors.Is
func (e *FetchError) Is(target error) bool {
tar, ok := target.(*FetchError)
if !ok {
return false
}

if tar.Ticker == "" && tar.Date == "" && tar.Err == nil {
return true
}

return e.Ticker == tar.Ticker && e.Date == tar.Date && errors.Is(e.Err, tar.Err)
}

// CloseBodyError for when we had an error while closing body from B3 response
type CloseBodyError struct {
Ticker string
Date string
Err error
}

// Error method to comply with error interface
func (e *CloseBodyError) Error() string {
return fmt.Sprintf("failed to close B3 response body for %v on %v: %v", e.Ticker, e.Date, e.Err)
}

// Unwrap method to comply with errors.Unwrap
func (e *CloseBodyError) Unwrap() error {
return e.Err
}

// Is method to comply with errors.Is
func (e *CloseBodyError) Is(target error) bool {
tar, ok := target.(*CloseBodyError)
if !ok {
return false
}

if tar.Ticker == "" && tar.Date == "" && tar.Err == nil {
return true
}

return e.Ticker == tar.Ticker && e.Date == tar.Date && errors.Is(e.Err, tar.Err)
}

// JSONDecodeError for when we had an error while decoding json body from B3 response
type JSONDecodeError struct {
Ticker string
Date string
Err error
}

// Error method to comply with error interface
func (e *JSONDecodeError) Error() string {
return fmt.Sprintf("failed to decode B3 quotation for %v on %v: %v", e.Ticker, e.Date, e.Err)
}

// Unwrap method to comply with errors.Unwrap
func (e *JSONDecodeError) Unwrap() error {
return e.Err
}

// Is method to comply with errors.Is
func (e *JSONDecodeError) Is(target error) bool {
tar, ok := target.(*JSONDecodeError)
if !ok {
return false
}

if tar.Ticker == "" && tar.Date == "" && tar.Err == nil {
return true
}

return e.Ticker == tar.Ticker && e.Date == tar.Date && errors.Is(e.Err, tar.Err)
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module github.com/Bezunca/b3lib

go 1.14

require github.com/stretchr/testify v1.6.1
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
37 changes: 37 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package b3lib

import (
"time"
)

type (
// FetchedPrice struct to group ticker and it's current price
FetchedPrice struct {
Ticker string `json:"ticker"`
IntPrice int `json:"int_price"`
}

b3PriceColumns struct {
Name string `json:"name"`
FriendlyName string `json:"friendlyName"`
FriendlyNamePt string `json:"friendlyNamePt"`
FriendlyNameEn string `json:"friendlyNameEn"`
Type int `json:"type"`
Format string `json:"format"`
ColumnAlignment int `json:"columnAlignment"`
ValueAlignment int `json:"valueAlignment"`
}

b3PriceResponse struct {
Name string `json:"name"`
FriendlyName string `json:"friendlyName"`
Columns []b3PriceColumns `json:"columns"`
Values [][6]interface{} `json:"values"`
// Inner slices follows columns order
}

tickerEntry struct {
Price FetchedPrice
Timestamp time.Time
}
)

0 comments on commit 611a19d

Please sign in to comment.