package database import ( "context" "database/sql" "errors" "fmt" "github.com/ansg191/ibd-trader-backend/internal/analyzer" "github.com/ansg191/ibd-trader-backend/internal/utils" "github.com/Rhymond/go-money" ) var ErrStockNotFound = errors.New("stock not found") type StockStore interface { GetStock(ctx context.Context, symbol string) (Stock, error) AddStock(ctx context.Context, stock Stock) error AddRanking(ctx context.Context, symbol string, ibd50, cap20 int) error AddStockInfo(ctx context.Context, info *StockInfo) (string, error) GetStockInfo(ctx context.Context, id string) (*StockInfo, error) AddAnalysis(ctx context.Context, ratingId string, analysis *analyzer.Analysis) error } func (d *database) GetStock(ctx context.Context, symbol string) (Stock, error) { row, err := d.queryRow(ctx, d.db, "stocks/get_stock", symbol) if err != nil { return Stock{}, err } var stock Stock if err = row.Scan(&stock.Symbol, &stock.Name, &stock.IBDUrl); err != nil { if errors.Is(err, sql.ErrNoRows) { return Stock{}, ErrStockNotFound } return Stock{}, err } return stock, nil } func (d *database) AddStock(ctx context.Context, stock Stock) error { _, err := d.exec(ctx, d.db, "stocks/add_stock", stock.Symbol, stock.Name, stock.IBDUrl) return err } func (d *database) AddRanking(ctx context.Context, symbol string, ibd50, cap20 int) error { if ibd50 > 0 { _, err := d.exec(ctx, d.db, "stocks/add_rank", symbol, "ibd50", ibd50) if err != nil { return err } } if cap20 > 0 { _, err := d.exec(ctx, d.db, "stocks/add_rank", symbol, "cap20", cap20) if err != nil { return err } } return nil } func (d *database) AddStockInfo(ctx context.Context, info *StockInfo) (string, error) { tx, err := d.db.BeginTx(ctx, nil) if err != nil { return "", err } defer func(tx *sql.Tx) { _ = tx.Rollback() }(tx) // Add raw chart analysis row, err := d.queryRow(ctx, tx, "stocks/add_raw_chart_analysis", info.ChartAnalysis) if err != nil { return "", err } var chartAnalysisID string if err = row.Scan(&chartAnalysisID); err != nil { return "", err } // Add stock info row, err = d.queryRow(ctx, tx, "stocks/add_rating", info.Symbol, info.Ratings.Composite, info.Ratings.EPS, info.Ratings.RelStr, info.Ratings.GroupRelStr, info.Ratings.SMR, info.Ratings.AccDis, chartAnalysisID, info.Price.Display(), ) if err != nil { return "", err } var ratingsID string if err = row.Scan(&ratingsID); err != nil { return "", err } return ratingsID, tx.Commit() } func (d *database) GetStockInfo(ctx context.Context, id string) (*StockInfo, error) { row, err := d.queryRow(ctx, d.db, "stocks/get_stock_info", id) if err != nil { return nil, err } var info StockInfo var priceStr string err = row.Scan( &info.Symbol, &info.Name, &info.ChartAnalysis, &info.Ratings.Composite, &info.Ratings.EPS, &info.Ratings.RelStr, &info.Ratings.GroupRelStr, &info.Ratings.SMR, &info.Ratings.AccDis, &priceStr, ) if err != nil { return nil, err } info.Price, err = utils.ParseMoney(priceStr) if err != nil { return nil, err } return &info, nil } func (d *database) AddAnalysis(ctx context.Context, ratingId string, analysis *analyzer.Analysis) error { _, err := d.exec(ctx, d.db, "stocks/add_analysis", ratingId, analysis.Action, analysis.Price.Display(), analysis.Reason, analysis.Confidence, ) return err } type Stock struct { Symbol string Name string IBDUrl string } type StockInfo struct { Symbol string Name string ChartAnalysis string Ratings Ratings Price *money.Money } type Ratings struct { Composite uint8 EPS uint8 RelStr uint8 GroupRelStr LetterRating SMR LetterRating AccDis LetterRating } type LetterRating uint8 const ( LetterRatingE LetterRating = iota LetterRatingEPlus LetterRatingDMinus LetterRatingD LetterRatingDPlus LetterRatingCMinus LetterRatingC LetterRatingCPlus LetterRatingBMinus LetterRatingB LetterRatingBPlus LetterRatingAMinus LetterRatingA LetterRatingAPlus ) func (r LetterRating) String() string { switch r { case LetterRatingE: return "E" case LetterRatingEPlus: return "E+" case LetterRatingDMinus: return "D-" case LetterRatingD: return "D" case LetterRatingDPlus: return "D+" case LetterRatingCMinus: return "C-" case LetterRatingC: return "C" case LetterRatingCPlus: return "C+" case LetterRatingBMinus: return "B-" case LetterRatingB: return "B" case LetterRatingBPlus: return "B+" case LetterRatingAMinus: return "A-" case LetterRatingA: return "A" case LetterRatingAPlus: return "A+" default: return "Unknown" } } func LetterRatingFromString(str string) (LetterRating, error) { switch str { case "N/A": fallthrough case "E": return LetterRatingE, nil case "E+": return LetterRatingEPlus, nil case "D-": return LetterRatingDMinus, nil case "D": return LetterRatingD, nil case "D+": return LetterRatingDPlus, nil case "C-": return LetterRatingCMinus, nil case "C": return LetterRatingC, nil case "C+": return LetterRatingCPlus, nil case "B-": return LetterRatingBMinus, nil case "B": return LetterRatingB, nil case "B+": return LetterRatingBPlus, nil case "A-": return LetterRatingAMinus, nil case "A": return LetterRatingA, nil case "A+": return LetterRatingAPlus, nil default: return 0, fmt.Errorf("unknown rating: %s", str) } }