127 lines
3.2 KiB
Go
127 lines
3.2 KiB
Go
package etoro
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const baseURL = "https://public-api.etoro.com/api/v1"
|
|
|
|
type Client struct {
|
|
http *http.Client
|
|
apiKey string
|
|
userKey string
|
|
}
|
|
|
|
type Instrument struct {
|
|
InstrumentID int `json:"internalInstrumentId"`
|
|
InstrumentDisplayName string `json:"internalInstrumentDisplayName"`
|
|
SymbolFull string `json:"internalSymbolFull"`
|
|
AssetClassID int `json:"internalAssetClassId"`
|
|
ExchangeID int `json:"internalExchangeId"`
|
|
IsHidden bool `json:"isHiddenFromClient"`
|
|
IsDelisted bool `json:"isDelisted"`
|
|
IsActiveInPlatform bool `json:"isActiveInPlatform"`
|
|
IsBuyEnabled bool `json:"isBuyEnabled"`
|
|
}
|
|
|
|
type searchResponse struct {
|
|
Page int `json:"page"`
|
|
PageSize int `json:"pageSize"`
|
|
TotalItems int `json:"totalItems"`
|
|
Items []Instrument `json:"items"`
|
|
}
|
|
|
|
func New() *Client {
|
|
return &Client{
|
|
http: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
func NewWithKeys(apiKey, userKey string) *Client {
|
|
return &Client{
|
|
http: &http.Client{Timeout: 30 * time.Second},
|
|
apiKey: apiKey,
|
|
userKey: userKey,
|
|
}
|
|
}
|
|
|
|
func (c *Client) SetKeys(apiKey, userKey string) {
|
|
c.apiKey = apiKey
|
|
c.userKey = userKey
|
|
}
|
|
|
|
func (c *Client) get(path string) (*http.Response, error) {
|
|
req, err := http.NewRequest("GET", baseURL+path, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("x-api-key", c.apiKey)
|
|
req.Header.Set("x-user-key", c.userKey)
|
|
req.Header.Set("x-request-id", uuid.NewString())
|
|
req.Header.Set("Accept", "application/json")
|
|
return c.http.Do(req)
|
|
}
|
|
|
|
func (c *Client) fetchPage(assetClassID, pageSize, page int) (*searchResponse, error) {
|
|
resp, err := c.get(fmt.Sprintf("/market-data/search?internalAssetClassId=%d&pageSize=%d&page=%d", assetClassID, pageSize, page))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("etoro: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("etoro: HTTP %d", resp.StatusCode)
|
|
}
|
|
var sr searchResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&sr); err != nil {
|
|
return nil, fmt.Errorf("etoro: parse: %w", err)
|
|
}
|
|
return &sr, nil
|
|
}
|
|
|
|
// FetchStocks retourne tous les stocks actifs disponibles sur eToro (toutes pages).
|
|
func (c *Client) FetchStocks() ([]Instrument, error) {
|
|
if c.apiKey == "" || c.userKey == "" {
|
|
return nil, fmt.Errorf("etoro: clés API non configurées")
|
|
}
|
|
|
|
const pageSize = 500
|
|
var all []Instrument
|
|
|
|
for page := 1; ; page++ {
|
|
resp, err := c.get(fmt.Sprintf("/market-data/search?internalAssetClassId=5&pageSize=%d&page=%d", pageSize, page))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("etoro: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("etoro: HTTP %d", resp.StatusCode)
|
|
}
|
|
|
|
var sr searchResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&sr); err != nil {
|
|
return nil, fmt.Errorf("etoro: parse: %w", err)
|
|
}
|
|
|
|
for _, inst := range sr.Items {
|
|
if !inst.IsHidden && !inst.IsDelisted && inst.IsBuyEnabled {
|
|
all = append(all, inst)
|
|
}
|
|
}
|
|
|
|
if page*pageSize >= sr.TotalItems {
|
|
break
|
|
}
|
|
}
|
|
|
|
if len(all) == 0 {
|
|
return nil, fmt.Errorf("etoro: aucun stock retourné")
|
|
}
|
|
return all, nil
|
|
}
|