package ibd import ( "context" "fmt" "io" "net/http" "net/url" "strconv" "strings" "github.com/ansg191/ibd-trader/backend/internal/database" "github.com/ansg191/ibd-trader/backend/internal/utils" "github.com/Rhymond/go-money" "golang.org/x/net/html" ) func (c *Client) StockInfo(ctx context.Context, uri string) (*database.StockInfo, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, uri, nil) if err != nil { return nil, err } _, cookie, err := c.getCookie(ctx, nil) if err != nil { return nil, err } req.AddCookie(cookie) // Set required query parameters params := url.Values{} params.Set("list", "ibd50") params.Set("type", "weekly") req.URL.RawQuery = params.Encode() resp, err := c.Do(req) if err != nil { return nil, err } defer func(Body io.ReadCloser) { _ = Body.Close() }(resp.Body) if resp.StatusCode != http.StatusOK { content, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read response body: %w", err) } return nil, fmt.Errorf( "unexpected status code %d: %s", resp.StatusCode, string(content), ) } node, err := html.Parse(resp.Body) if err != nil { return nil, err } name, symbol, err := extractNameAndSymbol(node) if err != nil { return nil, fmt.Errorf("failed to extract name and symbol: %w", err) } chartAnalysis, err := extractChartAnalysis(node) if err != nil { return nil, fmt.Errorf("failed to extract chart analysis: %w", err) } ratings, err := extractRatings(node) if err != nil { return nil, fmt.Errorf("failed to extract ratings: %w", err) } price, err := extractPrice(node) if err != nil { return nil, fmt.Errorf("failed to extract price: %w", err) } return &database.StockInfo{ Symbol: symbol, Name: name, ChartAnalysis: chartAnalysis, Ratings: ratings, Price: price, }, nil } func extractNameAndSymbol(node *html.Node) (name string, symbol string, err error) { // Find span with ID "quote-symbol" quoteSymbolNode := findId(node, "quote-symbol") if quoteSymbolNode == nil { return "", "", fmt.Errorf("could not find `quote-symbol` span") } // Get the text of the quote-symbol span name = strings.TrimSpace(extractText(quoteSymbolNode)) // Find span with ID "qteSymb" qteSymbNode := findId(node, "qteSymb") if qteSymbNode == nil { return "", "", fmt.Errorf("could not find `qteSymb` span") } // Get the text of the qteSymb span symbol = strings.TrimSpace(extractText(qteSymbNode)) // Get index of last closing parenthesis lastParenIndex := strings.LastIndex(name, ")") if lastParenIndex == -1 { return } // Find the last opening parenthesis before the closing parenthesis lastOpenParenIndex := strings.LastIndex(name[:lastParenIndex], "(") if lastOpenParenIndex == -1 { return } // Remove the parenthesis pair name = strings.TrimSpace(name[:lastOpenParenIndex] + name[lastParenIndex+1:]) return } func extractPrice(node *html.Node) (*money.Money, error) { // Find the div with the ID "lstPrice" lstPriceNode := findId(node, "lstPrice") if lstPriceNode == nil { return nil, fmt.Errorf("could not find `lstPrice` div") } // Get the text of the lstPrice div priceStr := strings.TrimSpace(extractText(lstPriceNode)) // Parse the price price, err := utils.ParseMoney(priceStr) if err != nil { return nil, fmt.Errorf("failed to parse price: %w", err) } return price, nil } func extractRatings(node *html.Node) (ratings database.Ratings, err error) { // Find the div with class "smartContent" smartSelectNode := findClass(node, "smartContent") if smartSelectNode == nil { return ratings, fmt.Errorf("could not find `smartContent` div") } // Iterate over children, looking for "smartRating" divs for c := smartSelectNode.FirstChild; c != nil; c = c.NextSibling { if !isClass(c, "smartRating") { continue } err = processSmartRating(c, &ratings) if err != nil { return } } return } // processSmartRating extracts the rating from a "smartRating" div and updates the ratings struct. // // The node should look like this: // //