Skip to content

Commit

Permalink
implemented spbex
Browse files Browse the repository at this point in the history
  • Loading branch information
kiberdruzhinnik committed Dec 16, 2023
1 parent 61b95ff commit 259e7d7
Show file tree
Hide file tree
Showing 5 changed files with 155 additions and 46 deletions.
23 changes: 23 additions & 0 deletions api/history.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package api

import (
"encoding/json"
"time"

"github.com/shopspring/decimal"
)

type HistoryEntry struct {
Date time.Time `json:"date"`
Close decimal.Decimal `json:"close"`
High decimal.Decimal `json:"high"`
Low decimal.Decimal `json:"low"`
Volume uint64 `json:"volume"`
Facevalue decimal.Decimal `json:"facevalue"`
}

type HistoryEntries []HistoryEntry

func (entries HistoryEntries) MarshalBinary() ([]byte, error) {
return json.Marshal(entries)
}
49 changes: 17 additions & 32 deletions api/moex.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,6 @@ type MoexSecurityParametersJSON struct {
} `json:"boards"`
}

type MoexHistoryEntry struct {
Date time.Time `json:"date"`
Close decimal.Decimal `json:"close"`
High decimal.Decimal `json:"high"`
Low decimal.Decimal `json:"low"`
Volume uint64 `json:"volume"`
Facevalue decimal.Decimal `json:"facevalue"`
}

type MoexHistoryEntries []MoexHistoryEntry

func (entries MoexHistoryEntries) MarshalBinary() ([]byte, error) {
return json.Marshal(entries)
}

type MoexHistoryJSON struct {
History struct {
Columns []string `json:"columns"`
Expand All @@ -64,20 +49,20 @@ func NewMoexAPI(redis utils.RedisClient) MoexAPI {
}
}

func (api *MoexAPI) GetTicker(ticker string) (MoexHistoryEntries, error) {
func (api *MoexAPI) GetTicker(ticker string) (HistoryEntries, error) {
security, err := api.getSecurityParameters(ticker)
if err != nil {
log.Println(err)
return MoexHistoryEntries{}, err
return HistoryEntries{}, err
}

var history MoexHistoryEntries
var history HistoryEntries
offset := uint(0)
for {
entryHistory, err := api.getSecurityHistoryOffset(ticker, security, offset)
if err != nil {
log.Println(err)
return MoexHistoryEntries{}, err
return HistoryEntries{}, err
}
if len(entryHistory) == 0 {
break
Expand Down Expand Up @@ -164,38 +149,38 @@ func (api *MoexAPI) getSecurityParameters(ticker string) (MoexSecurityParameters
return output, err
}

func (api *MoexAPI) getSecurityHistoryOffsetFromCache(key string) (MoexHistoryEntries, error) {
func (api *MoexAPI) getSecurityHistoryOffsetFromCache(key string) (HistoryEntries, error) {
log.Printf("Getting history data from cache for %s\n", key)
data, err := api.Redis.Client.Get(api.Redis.Context, key).Bytes()
if err != nil {
log.Printf("Got no history data from cache for %s\n", key)
return MoexHistoryEntries{}, err
return HistoryEntries{}, err
}

log.Printf("Got history data from cache for %s\n", key)
var moexHistory MoexHistoryEntries
var moexHistory HistoryEntries
err = json.Unmarshal(data, &moexHistory)
if err != nil {
return MoexHistoryEntries{}, err
return HistoryEntries{}, err
}

return moexHistory, nil
}

func (api *MoexAPI) setSecurityHistoryOffsetToCache(key string, value MoexHistoryEntries) error {
func (api *MoexAPI) setSecurityHistoryOffsetToCache(key string, value HistoryEntries) error {
log.Printf("Saving history data to cache for %s\n", key)
return api.Redis.Client.Set(api.Redis.Context, key, value, 0).Err()
}

func (api *MoexAPI) getSecurityHistoryOffset(ticker string,
params MoexSecurityParameters,
offset uint) (MoexHistoryEntries, error) {
offset uint) (HistoryEntries, error) {
url := fmt.Sprintf("%s/iss/history/engines/%s/markets/%s/boards/%s/"+
"securities/%s.json?iss.meta=off&start=%d&history.columns=TRADEDATE"+
",CLOSE,HIGH,LOW,VOLUME,FACEVALUE",
api.BaseURL, params.Engine, params.Market, params.Board, ticker, offset)

var moexHistory MoexHistoryEntries
var moexHistory HistoryEntries
var cacheKey string

if api.Redis.Client != nil {
Expand All @@ -209,26 +194,26 @@ func (api *MoexAPI) getSecurityHistoryOffset(ticker string,
log.Printf("Fetching history data from url %s for %s\n", url, ticker)
data, err := utils.HttpGet(url)
if err != nil {
return MoexHistoryEntries{}, err
return HistoryEntries{}, err
}

var moexHistoryJSON MoexHistoryJSON
err = json.Unmarshal(data, &moexHistoryJSON)
if err != nil {
return MoexHistoryEntries{}, err
return HistoryEntries{}, err
}

if len(moexHistoryJSON.History.Data) == 0 {
// end of data
return MoexHistoryEntries{}, nil
return HistoryEntries{}, nil
}

moexHistory = make(MoexHistoryEntries, len(moexHistoryJSON.History.Data))
moexHistory = make(HistoryEntries, len(moexHistoryJSON.History.Data))

for i, entry := range moexHistoryJSON.History.Data {
time, err := time.Parse("2006-01-02", entry[0].(string))
if err != nil {
return MoexHistoryEntries{}, err
return HistoryEntries{}, err
}
moexHistory[i].Date = time

Expand Down Expand Up @@ -256,7 +241,7 @@ func (api *MoexAPI) getSecurityHistoryOffset(ticker string,
if len(moexHistory)%PAGE_SIZE == 0 {
err = api.setSecurityHistoryOffsetToCache(cacheKey, moexHistory)
if err != nil {
return MoexHistoryEntries{}, err
return HistoryEntries{}, err
}
}
}
Expand Down
105 changes: 102 additions & 3 deletions api/spbex.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,108 @@
package api

type SpbexAPI struct{}
import (
"encoding/json"
"fmt"
"log"
"time"

custom_errors "github.com/kiberdruzhinnik/go-exchange-api/errors"
"github.com/kiberdruzhinnik/go-exchange-api/utils"
"github.com/shopspring/decimal"
)

type SpbexAPI struct {
BaseURL string
}

type TimeRange struct {
Start uint64
End uint64
}

type SpbexSecurityJSON struct {
Time []int `json:"t"`
Open []float64 `json:"o"`
High []float64 `json:"h"`
Low []float64 `json:"l"`
Close []float64 `json:"c"`
Status string `json:"s"`
}

func NewSpbexAPI() SpbexAPI {
return SpbexAPI{}
return SpbexAPI{
BaseURL: "https://investcab.ru/api",
}
}

func (api *SpbexAPI) GetTicker(ticker string) (HistoryEntries, error) {

jsonHistory, err := api.getHistory(ticker)
if err != nil {
return nil, err
}

historyEntries := make(HistoryEntries, len(jsonHistory.Time))
for i := 0; i < len(jsonHistory.Time); i++ {

entry := HistoryEntry{
Date: api.parseTime(int64(jsonHistory.Time[i])),
Close: decimal.NewFromFloat(jsonHistory.Close[i]),
High: decimal.NewFromFloat(jsonHistory.High[i]),
Low: decimal.NewFromFloat(jsonHistory.Low[i]),
Volume: 0,
Facevalue: decimal.Zero,
}
historyEntries[i] = entry
}

return historyEntries, nil
}

func (api *SpbexAPI) GetTicker() {}
func (api *SpbexAPI) parseTime(timestamp int64) time.Time {
return time.Unix(timestamp, 0)
}

func (api *SpbexAPI) getHistory(ticker string) (SpbexSecurityJSON, error) {

timeRange := api.getTimeRange()
url := api.getUrl(ticker, "D", timeRange)

log.Printf("Fetching history data from url %s for %s\n", url, ticker)
data, err := utils.HttpGet(url)
if err != nil {
return SpbexSecurityJSON{}, err
}

var rawJson string
err = json.Unmarshal(data, &rawJson)
if err != nil {
return SpbexSecurityJSON{}, err
}

var spbexSecurityJson SpbexSecurityJSON
err = json.Unmarshal([]byte(rawJson), &spbexSecurityJson)
if err != nil {
return SpbexSecurityJSON{}, err
}

if len(spbexSecurityJson.Time) == 0 {
return SpbexSecurityJSON{}, custom_errors.ErrorNotFound
}

return spbexSecurityJson, nil
}

func (api *SpbexAPI) getTimeRange() TimeRange {
return TimeRange{
Start: 0,
End: uint64(time.Now().Unix()),
}
}

func (api *SpbexAPI) getUrl(ticker string, resolution string, timeRange TimeRange) string {
return fmt.Sprintf(
"%s/chistory?symbol=%s&resolution=%s&from=%d&to=%d",
api.BaseURL, ticker, resolution, timeRange.Start, timeRange.End,
)
}
22 changes: 12 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,34 +43,36 @@ func init() {
func SanitizedParam(c *gin.Context, param string) string {
out := c.Param(param)
out = strings.ToLower(out)
return utils.RemoveNonAlnum(out)
return utils.StringAllowlist(out)
}

func moexGetTicker(c *gin.Context) {
func getBaseTicker(c *gin.Context, apiGetTicker func(string) (api.HistoryEntries, error)) {
ticker := SanitizedParam(c, "ticker")
data, err := MoexAPI.GetTicker(ticker)
data, err := apiGetTicker(ticker)
if err != nil {
if err == custom_errors.ErrorNotFound {
log.Println(err)
c.JSON(http.StatusNotFound, gin.H{
"status": "not found",
})
return
} else {
log.Println(err)
c.JSON(http.StatusBadRequest, gin.H{
"status": "bad request",
})
}
return
}

c.JSON(http.StatusOK, data)
}

func moexGetTicker(c *gin.Context) {
getBaseTicker(c, MoexAPI.GetTicker)
}

func spbexGetTicker(c *gin.Context) {
ticker := SanitizedParam(c, "ticker")
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"data": ticker,
})
getBaseTicker(c, SpbexAPI.GetTicker)
}

func healthCheck(c *gin.Context) {
Expand All @@ -88,5 +90,5 @@ func mountRoutes(app *gin.Engine) {
func main() {
r := gin.Default()
mountRoutes(r)
r.Run()
log.Fatalln(r.Run())
}
2 changes: 1 addition & 1 deletion utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func HttpGet(url string) ([]byte, error) {
return body, nil
}

func RemoveNonAlnum(s string) string {
func StringAllowlist(s string) string {
valid := []*unicode.RangeTable{
unicode.Letter,
unicode.Digit,
Expand Down

0 comments on commit 259e7d7

Please sign in to comment.