aboutsummaryrefslogtreecommitdiff
path: root/backend/internal/ibd/stockinfo.go
diff options
context:
space:
mode:
Diffstat (limited to 'backend/internal/ibd/stockinfo.go')
-rw-r--r--backend/internal/ibd/stockinfo.go233
1 files changed, 233 insertions, 0 deletions
diff --git a/backend/internal/ibd/stockinfo.go b/backend/internal/ibd/stockinfo.go
new file mode 100644
index 0000000..1e3b96f
--- /dev/null
+++ b/backend/internal/ibd/stockinfo.go
@@ -0,0 +1,233 @@
+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:
+//
+// <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":
+ 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
+}