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: // // func processSmartRating(node *html.Node, ratings *database.Ratings) error { // Check that the node is a ul if node.Type != html.ElementNode || node.Data != "ul" { return fmt.Errorf("expected ul node, got %s", node.Data) } // Get all `li` children children := findChildren(node, func(node *html.Node) bool { return node.Type == html.ElementNode && node.Data == "li" }) // Extract the rating name ratingName := strings.TrimSpace(extractText(children[0])) // Extract the rating value ratingValueStr := strings.TrimSpace(extractText(children[1])) switch ratingName { case "Composite Rating": ratingValue, err := strconv.ParseUint(ratingValueStr, 10, 8) if err != nil { return fmt.Errorf("failed to parse Composite Rating: %w", err) } ratings.Composite = uint8(ratingValue) case "EPS Rating": ratingValue, err := strconv.ParseUint(ratingValueStr, 10, 8) if err != nil { return fmt.Errorf("failed to parse EPS Rating: %w", err) } ratings.EPS = uint8(ratingValue) case "RS Rating": ratingValue, err := strconv.ParseUint(ratingValueStr, 10, 8) if err != nil { return fmt.Errorf("failed to parse RS Rating: %w", err) } ratings.RelStr = uint8(ratingValue) case "Group RS Rating": ratings.GroupRelStr = database.LetterRatingFromString(ratingValueStr) case "SMR Rating": ratings.SMR = database.LetterRatingFromString(ratingValueStr) case "Acc/Dis Rating": ratings.AccDis = database.LetterRatingFromString(ratingValueStr) default: return fmt.Errorf("unknown rating name: %s", ratingName) } return nil } func extractChartAnalysis(node *html.Node) (string, error) { // Find the div with class "chartAnalysis" chartAnalysisNode := findClass(node, "chartAnalysis") if chartAnalysisNode == nil { return "", fmt.Errorf("could not find `chartAnalysis` div") } // Get the text of the chart analysis div chartAnalysis := strings.TrimSpace(extractText(chartAnalysisNode)) return chartAnalysis, nil }