From ad514fb62bc31564778cadb07057d8fae527678a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C3=A9o?= Date: Tue, 14 May 2024 16:49:01 +0200 Subject: [PATCH] :sparkles: getChartRangeRequest (#1) --- README.md | 22 +++++++++ internal/protocols/socket/request.go | 18 ++++++-- internal/protocols/socket/response.go | 25 +++++++++-- xapi.go | 64 ++++++++++++++++++--------- xapi_test.go | 33 ++++++++++++++ 5 files changed, 135 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 1d77f93..4e54099 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ xapiRealClient, err := xapi.NewClient(os.Getenv("XAPI_USER_ID"), os.Getenv("XAPI ### GetCandles +#### With subscription + ```go xapiClient.SubscribeCandles("EURUSD") for { @@ -33,6 +35,26 @@ for { } ``` +#### With query + +```go +end := int(time.Now().Add(-24 * 1 * time.Hour).UnixMilli()) +period := 1 +ticks := 50 +start := int(time.Now().Add(-24 * time.Hour).UnixMilli()) +candles, err := xapiClient.GetCandles(end, period, start, "EURUSD", ticks) +``` + +| Value | Type | Description | +| ----- | ---- | ----------- | +| end | int | End of chart block (rounded down to the nearest interval and excluding) | +| period | int | Period code | +| start | int | Start of chart block (rounded down to the nearest interval and excluding) | +| symbol | string | Symbol | +| ticks | int | Number of ticks needed | + +More details here : http://developers.xstore.pro/documentation/current#getChartRangeRequest + ## Contributions Contributions and feedback are welcome! If you encounter any issues, have suggestions for improvement, or would like to contribute new features, please open an issue or submit a pull request on the GitHub repository. diff --git a/internal/protocols/socket/request.go b/internal/protocols/socket/request.go index e778634..fa59080 100644 --- a/internal/protocols/socket/request.go +++ b/internal/protocols/socket/request.go @@ -1,13 +1,23 @@ package socket type Request struct { - Command string `json:"command"` - Arguments RequestArguments `json:"arguments"` + Command string `json:"command"` + Arguments interface{} `json:"arguments"` } -type RequestArguments interface{} - type LoginRequestArguments struct { UserId string `json:"userId"` Password string `json:"password"` } + +type InfoArguments struct { + Info interface{} `json:"info"` +} + +type GetCandlesInfo struct { + End int `json:"end"` + Period int `json:"period"` + Start int `json:"start"` + Symbol string `json:"symbol"` + Ticks int `json:"ticks"` +} diff --git a/internal/protocols/socket/response.go b/internal/protocols/socket/response.go index 31975b0..3d68820 100644 --- a/internal/protocols/socket/response.go +++ b/internal/protocols/socket/response.go @@ -1,12 +1,31 @@ package socket -type Response struct { - Status bool `json:"status"` +type LoginResponse struct { + Status bool `json:"status"` + StreamSessionId string `json:"streamSessionId"` + ErrorCode string `json:"errorCode"` + ErrorDescr string `json:"errorDescr"` } -type LoginResponse struct { +type Response struct { Status bool `json:"status"` StreamSessionId string `json:"streamSessionId"` ErrorCode string `json:"errorCode"` ErrorDescr string `json:"errorDescr"` + ReturnData struct { + Digits int `json:"digits"` + RateInfos []Candle `json:"rateInfos"` + } `json:"returnData"` +} + +// TODO: Move it to a common package (stream and socket use it) +type Candle struct { + Close float64 `json:"close"` + Ctm int64 `json:"ctm"` + CtmString string `json:"ctmString"` + High float64 `json:"high"` + Low float64 `json:"low"` + Open float64 `json:"open"` + QuoteId int `json:"quoteId"` + Vol float64 `json:"vol"` } diff --git a/xapi.go b/xapi.go index 18797b3..0e4ad19 100644 --- a/xapi.go +++ b/xapi.go @@ -3,6 +3,7 @@ package xapi import ( "encoding/json" "fmt" + "sync" "time" "github.com/MateoGreil/xapi-go/internal/protocols/socket" @@ -14,9 +15,9 @@ type client struct { conn *websocket.Conn streamConn *websocket.Conn streamSessionId string - socketMessageChannel chan interface{} streamMessageChannel chan interface{} CandlesChannel chan stream.Candle + mutexSendMessage sync.Mutex } const ( @@ -53,18 +54,18 @@ func NewClient(userId string, password string, connectionType string) (*client, } getKeepAlive(streamConn, streamSessionId) + var m sync.Mutex c := &client{ conn: conn, streamConn: streamConn, streamSessionId: streamSessionId, - socketMessageChannel: make(chan interface{}), streamMessageChannel: make(chan interface{}), CandlesChannel: make(chan stream.Candle), + mutexSendMessage: m, } go c.pingSocket() go c.pingStream() go c.listenStream() - go c.socketWriteJSON() go c.streamWriteJSON() return c, nil @@ -79,6 +80,33 @@ func (c *client) SubscribeCandles(symbol string) { c.streamMessageChannel <- request } +func (c *client) GetCandles(end int, period int, start int, symbol string, ticks int) ([]socket.Candle, error) { + request := socket.Request{ + Command: "getChartRangeRequest", + Arguments: socket.InfoArguments{ + Info: socket.GetCandlesInfo{ + End: end, + Period: period, + Start: start, + Symbol: symbol, + Ticks: ticks, + }, + }, + } + response := socket.Response{} + c.mutexSendMessage.Lock() + c.conn.WriteJSON(request) + err := c.conn.ReadJSON(&response) + c.mutexSendMessage.Unlock() + if err != nil { + return nil, err + } + if response.Status != true { + return nil, fmt.Errorf("Error on sending getChartRangeRequest: %+v, response:, %+v", request, response) + } + return response.ReturnData.RateInfos, nil +} + func (c *client) listenStream() { for { _, message, err := c.streamConn.ReadMessage() @@ -109,16 +137,20 @@ func (c *client) listenStream() { func (c *client) pingSocket() { for { - request := socket.Request{ - Command: "ping", - Arguments: nil, + request := struct { + Command string `json:"command"` + }{Command: "ping"} + response := socket.Response{} + c.mutexSendMessage.Lock() + c.conn.WriteJSON(request) + err := c.conn.ReadJSON(&response) + c.mutexSendMessage.Unlock() + if err != nil { + //TODO: Handle error + fmt.Printf("Ping socket failed: %s", err.Error()) + } else if response.Status != true { + fmt.Errorf("Error on sending request: %+v, response:, %+v", request, response) } - c.socketMessageChannel <- request - // response := socket.Response{} - // err := c.conn.ReadJSON(&response) - // if err != nil { - // fmt.Println(err.Error()) - // } time.Sleep(pingInterval) } } @@ -142,14 +174,6 @@ func (c *client) streamWriteJSON() { } } -func (c *client) socketWriteJSON() { - for { - message := <-c.socketMessageChannel - c.conn.WriteJSON(message) - fmt.Printf("messageSocket: %+v\n", message) - } -} - func login(conn *websocket.Conn, userId string, password string) (string, error) { request := socket.Request{ Command: "login", diff --git a/xapi_test.go b/xapi_test.go index 14daee9..483ea54 100644 --- a/xapi_test.go +++ b/xapi_test.go @@ -39,3 +39,36 @@ func TestSuscribeCandles(t *testing.T) { t.Error("Did not receive candles") } } + +func TestGetCandles(t *testing.T) { + xapiClient, err := NewClient(os.Getenv("XAPI_USER_ID"), os.Getenv("XAPI_PASSWORD"), "demo") + if err != nil { + t.Error(err) + } + + start := int(time.Now().Add(-24 * 1 * time.Hour).UnixMilli()) + period := 1 + ticks := 1 + end := int(time.Now().Add(-24 * 1 * time.Hour).UnixMilli()) + candles, err := xapiClient.GetCandles(start, period, end, "EURUSD", ticks) + if err != nil { + t.Error(err) + } else { + length := len(candles) + if length != 1 { + t.Errorf("Should contain 1 candle, but contains %d", length) + } + } + + ticks = 50 + candles, err = xapiClient.GetCandles(start, period, end, "EURUSD", ticks) + if err != nil { + t.Error(err) + } else { + length := len(candles) + if length != 50 { + t.Errorf("Should contain 50 candle, but contains %d", length) + } + } + +}