diff --git a/pkg/datasource/wise/README.md b/pkg/datasource/wise/README.md new file mode 100644 index 000000000..1b6a04f80 --- /dev/null +++ b/pkg/datasource/wise/README.md @@ -0,0 +1,25 @@ +# Wise + +[Wise API Docs](https://docs.wise.com/api-docs) + +```go +c := wise.NewClient() +c.Auth(os.Getenv("WISE_TOKEN")) + +ctx := context.Background() +rates, err := c.QueryRate(ctx, "USD", "TWD") +if err != nil { + panic(err) +} +fmt.Printf("%+v\n", rates) + +// or +now := time.Now() +rates, err = c.QueryRateHistory(ctx, "USD", "TWD", now.Add(-time.Hour*24*7), now, types.Interval1h) +if err != nil { + panic(err) +} +for _, rate := range rates { + fmt.Printf("%+v\n", rate) +} +``` diff --git a/pkg/datasource/wise/client.go b/pkg/datasource/wise/client.go new file mode 100644 index 000000000..7f0fee7b5 --- /dev/null +++ b/pkg/datasource/wise/client.go @@ -0,0 +1,80 @@ +package wise + +import ( + "context" + "fmt" + "net/http" + "net/url" + "time" + + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +const ( + defaultHTTPTimeout = time.Second * 15 + defaultBaseURL = "https://api.transferwise.com" + sandboxBaseURL = "https://api.sandbox.transferwise.tech" +) + +type Client struct { + requestgen.BaseAPIClient + + token string +} + +func NewClient() *Client { + u, err := url.Parse(defaultBaseURL) + if err != nil { + panic(err) + } + + return &Client{ + BaseAPIClient: requestgen.BaseAPIClient{ + BaseURL: u, + HttpClient: &http.Client{ + Timeout: defaultHTTPTimeout, + }, + }, + } +} + +func (c *Client) Auth(token string) { + c.token = token +} + +func (c *Client) NewAuthenticatedRequest(ctx context.Context, method, refURL string, params url.Values, payload interface{}) (*http.Request, error) { + req, err := c.NewRequest(ctx, method, refURL, params, payload) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/json") + + if c.token != "" { + req.Header.Set("Authorization", "Bearer "+c.token) + } + return req, nil +} + +func (c *Client) QueryRate(ctx context.Context, source string, target string) ([]Rate, error) { + req := c.NewRateRequest().Source(source).Target(target) + return req.Do(ctx) +} + +func (c *Client) QueryRateHistory(ctx context.Context, source string, target string, from time.Time, to time.Time, interval types.Interval) ([]Rate, error) { + req := c.NewRateRequest().Source(source).Target(target).From(from).To(to) + + switch interval { + case types.Interval1h: + req.Group(GroupHour) + case types.Interval1d: + req.Group(GroupDay) + case types.Interval1m: + req.Group(GroupMinute) + default: + return nil, fmt.Errorf("unsupported interval: %s", interval) + } + + return req.Do(ctx) +} diff --git a/pkg/datasource/wise/group.go b/pkg/datasource/wise/group.go new file mode 100644 index 000000000..2770237b5 --- /dev/null +++ b/pkg/datasource/wise/group.go @@ -0,0 +1,9 @@ +package wise + +type Group string + +const ( + GroupMinute = Group("minute") + GroupHour = Group("hour") + GroupDay = Group("day") +) diff --git a/pkg/datasource/wise/rate.go b/pkg/datasource/wise/rate.go new file mode 100644 index 000000000..7a901626f --- /dev/null +++ b/pkg/datasource/wise/rate.go @@ -0,0 +1,10 @@ +package wise + +import "github.com/c9s/bbgo/pkg/fixedpoint" + +type Rate struct { + Value fixedpoint.Value `json:"rate"` + Target string `json:"target"` + Source string `json:"source"` + Time Time `json:"time"` +} diff --git a/pkg/datasource/wise/rate_request.go b/pkg/datasource/wise/rate_request.go new file mode 100644 index 000000000..f8d8561b0 --- /dev/null +++ b/pkg/datasource/wise/rate_request.go @@ -0,0 +1,27 @@ +package wise + +import ( + "time" + + "github.com/c9s/requestgen" +) + +// https://docs.wise.com/api-docs/api-reference/rate + +//go:generate requestgen -method GET -url "/v1/rates" -type RateRequest -responseType []Rate +type RateRequest struct { + client requestgen.AuthenticatedAPIClient + + source string `param:"source"` + target string `param:"target"` + time *time.Time `param:"time" timeFormat:"2006-01-02T15:04:05-0700"` + from *time.Time `param:"from" timeFormat:"2006-01-02T15:04:05-0700"` + to *time.Time `param:"to" timeFormat:"2006-01-02T15:04:05-0700"` + group *Group `param:"group"` +} + +func (c *Client) NewRateRequest() *RateRequest { + return &RateRequest{ + client: c, + } +} diff --git a/pkg/datasource/wise/rate_request_requestgen.go b/pkg/datasource/wise/rate_request_requestgen.go new file mode 100644 index 000000000..786949198 --- /dev/null +++ b/pkg/datasource/wise/rate_request_requestgen.go @@ -0,0 +1,229 @@ +// Code generated by "requestgen -method GET -url /v1/rates -type RateRequest -responseType []Rate"; DO NOT EDIT. + +package wise + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "time" +) + +func (r *RateRequest) Source(source string) *RateRequest { + r.source = source + return r +} + +func (r *RateRequest) Target(target string) *RateRequest { + r.target = target + return r +} + +func (r *RateRequest) Time(time time.Time) *RateRequest { + r.time = &time + return r +} + +func (r *RateRequest) From(from time.Time) *RateRequest { + r.from = &from + return r +} + +func (r *RateRequest) To(to time.Time) *RateRequest { + r.to = &to + return r +} + +func (r *RateRequest) Group(group Group) *RateRequest { + r.group = &group + return r +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (r *RateRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (r *RateRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check source field -> json key source + source := r.source + + // assign parameter of source + params["source"] = source + // check target field -> json key target + target := r.target + + // assign parameter of target + params["target"] = target + // check time field -> json key time + if r.time != nil { + time := *r.time + + // assign parameter of time + params["time"] = time.Format("2006-01-02T15:04:05-0700") + } else { + } + // check from field -> json key from + if r.from != nil { + from := *r.from + + // assign parameter of from + params["from"] = from.Format("2006-01-02T15:04:05-0700") + } else { + } + // check to field -> json key to + if r.to != nil { + to := *r.to + + // assign parameter of to + params["to"] = to.Format("2006-01-02T15:04:05-0700") + } else { + } + // check group field -> json key group + if r.group != nil { + group := *r.group + + // assign parameter of group + params["group"] = group + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (r *RateRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := r.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if r.isVarSlice(_v) { + r.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (r *RateRequest) GetParametersJSON() ([]byte, error) { + params, err := r.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (r *RateRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (r *RateRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (r *RateRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (r *RateRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (r *RateRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := r.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (r *RateRequest) GetPath() string { + return "/v1/rates" +} + +// Do generates the request object and send the request object to the API endpoint +func (r *RateRequest) Do(ctx context.Context) ([]Rate, error) { + + // empty params for GET operation + var params interface{} + query, err := r.GetParametersQuery() + if err != nil { + return nil, err + } + + var apiURL string + + apiURL = r.GetPath() + + req, err := r.client.NewAuthenticatedRequest(ctx, "GET", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := r.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []Rate + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + + type responseValidator interface { + Validate() error + } + validator, ok := interface{}(apiResponse).(responseValidator) + if ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + return apiResponse, nil +} diff --git a/pkg/datasource/wise/time.go b/pkg/datasource/wise/time.go new file mode 100644 index 000000000..ba12abd7d --- /dev/null +++ b/pkg/datasource/wise/time.go @@ -0,0 +1,34 @@ +package wise + +import ( + "encoding/json" + "time" +) + +const layout = "2006-01-02T15:04:05-0700" + +type Time time.Time + +func (t Time) Time() time.Time { + return time.Time(t) +} + +func (t Time) String() string { + return time.Time(t).Format(layout) +} + +func (t *Time) UnmarshalJSON(data []byte) error { + var s string + err := json.Unmarshal(data, &s) + if err != nil { + return err + } + + parsed, err := time.Parse(layout, s) + if err != nil { + return err + } + + *t = Time(parsed) + return nil +}