Skip to content

Commit

Permalink
add: endpoint to fetch all stocks and handlong nil pointer exceptions
Browse files Browse the repository at this point in the history
Signed-off-by: shivamsouravjha <[email protected]>
  • Loading branch information
shivamsouravjha committed Oct 18, 2024
1 parent 17b311d commit c67dd7e
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 22 deletions.
78 changes: 78 additions & 0 deletions controllers/stock_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package controllers

import (
"encoding/json"
"fmt"
"os"
mongo_client "stockbackend/clients/mongo"
"stockbackend/utils/helpers"
"strconv"

"github.com/gin-gonic/gin"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"
"gopkg.in/mgo.v2/bson"
)

type StockControllerI interface {
GetStocks(ctx *gin.Context)
}

type stockController struct{}

var StockController StockControllerI = &stockController{}

func (s *stockController) GetStocks(ctx *gin.Context) {
// parse the reuqest for pag number and last fetcehd stock fo r pagination
pageNumberStr := ctx.DefaultQuery("pageNumber", "1")
pageNumber, err := strconv.Atoi(pageNumberStr)
if err != nil || pageNumber < 1 {
ctx.JSON(400, gin.H{"error": "Invalid page number"})
return
}
// fetch the stocks from the database
collection := mongo_client.Client.Database(os.Getenv("DATABASE")).Collection(os.Getenv("COLLECTION"))

//write a code that does the limit annd the page size for the stocks
findOptions := options.Find()
findOptions.SetLimit(10)
findOptions.SetSkip(int64(10 * (pageNumber - 1)))
findOptions.SetSort(bson.M{"rank": -1})
cursor, err := collection.Find(ctx, bson.M{}, findOptions)
if err != nil {
zap.L().Error("Error while fetching documents", zap.Error(err))
ctx.JSON(500, gin.H{"error": "Error while fetching stocks"})
return
}
defer cursor.Close(ctx)
for cursor.Next(ctx) {
var result bson.M
err := cursor.Decode(&result)
if err != nil {
ctx.JSON(500, gin.H{"error": "Error while decoding stocks"})
return
}
stockDetail := make(map[string]interface{})
stockDetail["name"] = result["name"]
stockDetail["marketCapValue"] = result["marketCap"]
stockDetail["url"] = result["url"]
stockDetail["marketCap"] = helpers.GetMarketCapCategory(fmt.Sprintf("%v", result["marketCap"]))
stockDetail["stockRate"] = result["rank"]
fmt.Println(result["fScore"], "fScore")
stockDetail["fScore"] = result["fScore"]
stockDataMarshal, err := json.Marshal(stockDetail)
if err != nil {
zap.L().Error("Error marshalling data", zap.Error(err))
continue
}

_, err = ctx.Writer.Write(append(stockDataMarshal, '\n')) // Send each stockDetail as JSON with a newline separator

if err != nil {
zap.L().Error("Error writing data", zap.Error(err))
break
}
ctx.Writer.Flush() // Flush each chunk immediately to the client
}
ctx.JSON(200, gin.H{"message": "Stocks are fetched"})
}
25 changes: 21 additions & 4 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import (
"os"
"os/exec"
"os/signal"
"stockbackend/middleware"
"stockbackend/routes"
"stockbackend/services"
"strconv"
"syscall"
"time"
Expand Down Expand Up @@ -55,7 +57,7 @@ func CORSMiddleware() gin.HandlerFunc {
}

// GracefulShutdown handles graceful shutdown of the server and ticker
func GracefulShutdown(server *http.Server, ticker *time.Ticker) {
func GracefulShutdown(server *http.Server, ticker, rankUpdater *time.Ticker) {
stopper := make(chan os.Signal, 1)
// Listen for interrupt and SIGTERM signals
signal.Notify(stopper, os.Interrupt, syscall.SIGTERM)
Expand All @@ -66,7 +68,7 @@ func GracefulShutdown(server *http.Server, ticker *time.Ticker) {

// Stop the ticker
ticker.Stop()

rankUpdater.Stop()
// Create a context with a timeout for shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
Expand Down Expand Up @@ -109,11 +111,13 @@ func main() {
setupSentry()

router := gin.New()
router.Use(middleware.RecoveryMiddleware())

router.Use(sentrygin.New(sentrygin.Options{}))
router.Use(CORSMiddleware())

ticker := startTicker()

rankUpdater := startRankUpdater()
routes.Routes(router)

port := os.Getenv("PORT")
Expand All @@ -128,7 +132,7 @@ func main() {
}

// Call GracefulShutdown with the server and ticker
GracefulShutdown(server, ticker)
GracefulShutdown(server, ticker, rankUpdater)

// Start the server
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
Expand Down Expand Up @@ -158,3 +162,16 @@ func startTicker() *time.Ticker {

return ticker
}

func startRankUpdater() *time.Ticker {
ticker := time.NewTicker(30 * time.Hour * 24)

go func() {
for t := range ticker.C {
//write a function that is called every 30 days
zap.L().Info("Rank updater tick at: ", zap.String("time", t.String()))
services.UpdateRating()
}
}()
return ticker
}
28 changes: 28 additions & 0 deletions middleware/middleware.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package middleware

import (
"net/http"
"runtime/debug"

"github.com/gin-gonic/gin"
"go.uber.org/zap"
)

// RecoveryMiddleware catches panics and prevents the server from crashing
func RecoveryMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
defer func() {
if r := recover(); r != nil {
// Log the panic and stack trace
zap.L().Error("Panic recovered", zap.Any("panic", r), zap.String("stack", string(debug.Stack())))
// Respond with a 500 Internal Server Error
ctx.JSON(http.StatusInternalServerError, gin.H{
"error": "Internal server error. Please try again later.",
})
ctx.Abort()
}
}()
// Continue to the next handler
ctx.Next()
}
}
1 change: 1 addition & 0 deletions routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ func Routes(r *gin.Engine) {
v1.POST("/uploadXlsx", controllers.FileController.ParseXLSXFile)
v1.GET("/keepServerRunning", controllers.HealthController.IsRunning)
v1.POST("/fetchGmail", controllers.GmailController.GetEmails)
v1.GET("/fetchStocks", controllers.StockController.GetStocks)
}
}
39 changes: 39 additions & 0 deletions services/rank_service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package services

import (
"context"
"os"
mongo_client "stockbackend/clients/mongo"
"stockbackend/utils/helpers"

"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"
"gopkg.in/mgo.v2/bson"
)

// write a function that travereses throguht al the documents of the mongo db
// and updates the rank of the stock
func UpdateRating() {
collection := mongo_client.Client.Database(os.Getenv("DATABASE")).Collection(os.Getenv("COLLECTION"))
findOptions := options.Find()
cursor, err := collection.Find(context.Background(), bson.M{}, findOptions)
if err != nil {
zap.L().Error("Error while fetching documents", zap.Error(err))
}
defer cursor.Close(context.Background())
var stockRate float64
for cursor.Next(context.Background()) {
var result bson.M
err := cursor.Decode(&result)
if err != nil {
zap.L().Error("Error while decoding document", zap.Error(err))
}
stockRate = helpers.RateStock(result)
fscore := helpers.GenerateFScore(result)
_, err = collection.UpdateOne(context.Background(), bson.M{"_id": result["_id"]}, bson.M{"$set": bson.M{"rank": stockRate, "fScore": fscore}})
if err != nil {
zap.L().Error("Error while updating document", zap.Error(err))
}
zap.L().Info("Updated rank for stock", zap.Any("stock", result["name"]), zap.Any("rate", stockRate))
}
}
86 changes: 68 additions & 18 deletions utils/helpers/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,11 @@ func compareWithPeers(stock types.Stock, peers interface{}) float64 {
}
}
medianRaw := arr[len(arr)-1]
median := medianRaw.(bson.M)

median, ok := medianRaw.(bson.M)
if !ok {
zap.L().Warn("Failed to parse median data")
return peerScore
}
// Parse median values to float64
medianPE := ParseFloat(median["pe"])
medianMarketCap := ParseFloat(median["market_cap"])
Expand Down Expand Up @@ -639,11 +642,31 @@ func calculateRoa(netProfit string, totalAssets string) float64 {
}

func increaseInRoa(netProfit primitive.A, totalAssets primitive.A) bool {
// Ensure there are enough entries in netProfit and totalAssets
if len(netProfit) < 3 || len(totalAssets) < 2 {
// Not enough data to calculate ROA increase
zap.L().Warn("Not enough data to calculate ROA increase")
return false // or handle as appropriate
}

// Safely retrieve netProfit values
netProfitCurrentYear, ok1 := netProfit[len(netProfit)-2].(string)
netProfitPreviousYear, ok2 := netProfit[len(netProfit)-3].(string)

// Safely retrieve totalAssets values
totalAssetsCurrentYear, ok3 := totalAssets[len(totalAssets)-1].(string)
totalAssetsPreviousYear, ok4 := totalAssets[len(totalAssets)-2].(string)

if !ok1 || !ok2 || !ok3 || !ok4 {
zap.L().Warn("Failed to retrieve netProfit and totalAssets values")
return false // or handle as needed
}

// Calculate the Return on Assets (ROA) for the current year
currentYearRoa := calculateRoa(netProfit[len(netProfit)-2].(string), totalAssets[len(totalAssets)-1].(string)) // No TTM in the denominator
currentYearRoa := calculateRoa(netProfitCurrentYear, totalAssetsCurrentYear)

// Calculate the Return on Assets (ROA) for the previous year
previousYearRoa := calculateRoa(netProfit[len(netProfit)-3].(string), totalAssets[len(totalAssets)-2].(string)) // No TTM in the denominator
previousYearRoa := calculateRoa(netProfitPreviousYear, totalAssetsPreviousYear)

return currentYearRoa > previousYearRoa
}
Expand Down Expand Up @@ -673,38 +696,59 @@ func GenerateFScore(stock map[string]interface{}) int {
return fScore
}

func safeToFloat(s string) (float64, error) {
if s == "" {
return 0, fmt.Errorf("input string is empty")
}
return strconv.ParseFloat(s, 64)
}

func calculateProfitabilityScore(stock map[string]interface{}) int {
score := 0

// 1 - Profitability Ratios
// 1.1 - Is the ROA (Return on Assets) positive?
netProfit, err := getNestedArrayField(stock, "profitLoss", "Net Profit +")
if err != nil {
zap.L().Error("Error fetching Net Profit:", zap.Error(err))
return -1
}
totalAssets, err := getNestedArrayField(stock, "balanceSheet", "Total Assets")
if err != nil {
zap.L().Error("Error fetching Total Assets:", zap.Error(err))
return -1
}

if len(netProfit) > 0 && len(totalAssets) > 0 {
roa := calculateRoa(netProfit[len(netProfit)-2].(string), totalAssets[len(totalAssets)-1].(string))
if roa > 0 {
score++
// Ensure arrays have sufficient length
if len(netProfit) >= 2 && len(totalAssets) >= 1 {
netProfitStr, ok1 := netProfit[len(netProfit)-2].(string)
totalAssetsStr, ok2 := totalAssets[len(totalAssets)-1].(string)
if ok1 && ok2 && netProfitStr != "" && totalAssetsStr != "" {
roa := calculateRoa(netProfitStr, totalAssetsStr)
if roa > 0 {
score++
}
}
}

// 1.2 - Positive Cash from Operating Activities in the current year compared to the previous year
cashFlowOps, err := getNestedArrayField(stock, "cashFlows", "Cash from Operating Activity +")
if err != nil {
zap.L().Error("Error fetching Cash Flow from Operating Activities:", zap.Error(err))
return -1
}

if len(cashFlowOps) > 1 {
currentCashFlow := ToFloat(cashFlowOps[len(cashFlowOps)-1])
previousCashFlow := ToFloat(cashFlowOps[len(cashFlowOps)-2])
if currentCashFlow > previousCashFlow {
score++
if len(cashFlowOps) >= 2 {
currentCashFlowStr, ok1 := cashFlowOps[len(cashFlowOps)-1].(string)
previousCashFlowStr, ok2 := cashFlowOps[len(cashFlowOps)-2].(string)
if ok1 && ok2 && currentCashFlowStr != "" && previousCashFlowStr != "" {
currentCashFlow, err1 := safeToFloat(currentCashFlowStr)
previousCashFlow, err2 := safeToFloat(previousCashFlowStr)
if err1 == nil && err2 == nil && currentCashFlow > previousCashFlow {
score++
} else if err1 != nil || err2 != nil {
zap.L().Error("Error converting values to float:", zap.Error(err1), zap.Error(err2))
}
}
}

Expand All @@ -714,11 +758,17 @@ func calculateProfitabilityScore(stock map[string]interface{}) int {
}

// 1.4 - Higher Cash from Operating Activities than Net Profit (excluding TTM value)
if len(cashFlowOps) > 0 && len(netProfit) > 1 {
cashFlow := ToFloat(cashFlowOps[len(cashFlowOps)-1])
profit := ToFloat(netProfit[len(netProfit)-2])
if cashFlow > profit {
score++
if len(cashFlowOps) >= 1 && len(netProfit) >= 2 {
cashFlowStr, ok1 := cashFlowOps[len(cashFlowOps)-1].(string)
profitStr, ok2 := netProfit[len(netProfit)-2].(string)
if ok1 && ok2 && cashFlowStr != "" && profitStr != "" {
cashFlow, err1 := safeToFloat(cashFlowStr)
profit, err2 := safeToFloat(profitStr)
if err1 == nil && err2 == nil && cashFlow > profit {
score++
} else if err1 != nil || err2 != nil {
zap.L().Error("Error converting values to float:", zap.Error(err1), zap.Error(err2))
}
}
}

Expand Down

0 comments on commit c67dd7e

Please sign in to comment.