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 }