diff options
Diffstat (limited to 'backend/internal/database/stocks.go')
-rw-r--r-- | backend/internal/database/stocks.go | 264 |
1 files changed, 264 insertions, 0 deletions
diff --git a/backend/internal/database/stocks.go b/backend/internal/database/stocks.go new file mode 100644 index 0000000..701201e --- /dev/null +++ b/backend/internal/database/stocks.go @@ -0,0 +1,264 @@ +package database + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "ibd-trader/internal/analyzer" + "ibd-trader/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) + } +} |