diff options
Diffstat (limited to 'backend/internal/ibd/stockinfo.go')
-rw-r--r-- | backend/internal/ibd/stockinfo.go | 237 |
1 files changed, 237 insertions, 0 deletions
diff --git a/backend/internal/ibd/stockinfo.go b/backend/internal/ibd/stockinfo.go new file mode 100644 index 0000000..33fea3d --- /dev/null +++ b/backend/internal/ibd/stockinfo.go @@ -0,0 +1,237 @@ +package ibd + +import ( + "context" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "ibd-trader/internal/database" + "ibd-trader/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 + } + + if resp.Result.StatusCode != http.StatusOK { + return nil, fmt.Errorf( + "unexpected status code %d: %s", + resp.Result.StatusCode, + resp.Result.Content, + ) + } + + node, err := html.Parse(strings.NewReader(resp.Result.Content)) + 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: +// +// <ul class="smartRating"> +// <li><a><span>Composite Rating</span></a></li> +// <li>94</li> +// ... +// </ul> +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": + ratingValue, err := database.LetterRatingFromString(ratingValueStr) + if err != nil { + return fmt.Errorf("failed to parse Group RS Rating: %w", err) + } + ratings.GroupRelStr = ratingValue + case "SMR Rating": + ratingValue, err := database.LetterRatingFromString(ratingValueStr) + if err != nil { + return fmt.Errorf("failed to parse SMR Rating: %w", err) + } + ratings.SMR = ratingValue + case "Acc/Dis Rating": + ratingValue, err := database.LetterRatingFromString(ratingValueStr) + if err != nil { + return fmt.Errorf("failed to parse Acc/Dis Rating: %w", err) + } + ratings.AccDis = ratingValue + 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 +} |