Skip to content

Commit

Permalink
Merge pull request #5 from 0xTxbi/sentiment-analysis-service
Browse files Browse the repository at this point in the history
sentiment analysis service
  • Loading branch information
0xTxbi authored Dec 14, 2023
2 parents 3d16e68 + ea0c911 commit ee2b0a3
Show file tree
Hide file tree
Showing 11 changed files with 321 additions and 16 deletions.
4 changes: 2 additions & 2 deletions data-source.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DataSource } from "typeorm";

import { User } from "./authentication-service/entities/User";
import { SentimentData } from "./sentiment-analysis-service/entities/SentimentData";
import { SentimentResult } from "./sentiment-analysis-service/entities/SentimentResult";
import { configDotenv } from "dotenv";
import { Watchlist } from "./authentication-service/entities/Watchlist";
import { StockInfo } from "./stock-info-service/entities/StockInfo";
Expand All @@ -19,7 +19,7 @@ const AppDataSource = new DataSource({
database: process.env.DB_DATABASE || "",
synchronize: true,
logging: true,
entities: [User, Watchlist, News, SentimentData, StockInfo],
entities: [User, Watchlist, News, SentimentResult, StockInfo],
migrations: [],
ssl: {
rejectUnauthorized: false,
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"typeorm": "typeorm-ts-node-commonjs",
"auth-start": "nodemon --watch . --ext ts --exec ts-node authentication-service/src/index.ts",
"stock-info-start": "nodemon --watch . --ext ts --exec ts-node stock-info-service/src/index.ts",
"news-service-start": "nodemon --watch . --ext ts --exec ts-node news-service/src/index.ts"
"news-service-start": "nodemon --watch . --ext ts --exec ts-node news-service/src/index.ts",
"sentiment-service-start": "nodemon --watch . --ext ts --exec ts-node sentiment-analysis-service/src/index.ts"
},
"devDependencies": {
"nodemon": "^3.0.2",
Expand All @@ -31,9 +32,11 @@
"express": "^4.18.2",
"express-rate-limit": "^7.1.5",
"jsonwebtoken": "^9.0.2",
"natural": "^6.10.4",
"pg": "^8.11.3",
"reflect-metadata": "^0.1.13",
"routing-controllers": "^0.10.4",
"typedi": "^0.10.0",
"typeorm": "^0.3.17"
}
}
16 changes: 16 additions & 0 deletions sentiment-analysis-service/1702404205332-migrationsc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class Migrationsc1702404205332 implements MigrationInterface {
name = 'Migrationsc1702404205332'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "sentiment_result" ("id" SERIAL NOT NULL, "stockSymbol" character varying NOT NULL, "sentiment" character varying NOT NULL, "confidence" integer NOT NULL, "currentStockPrice" integer, "priceChange" integer, "percentageChange" integer, CONSTRAINT "PK_86c8d740551a9efe751cbca494f" PRIMARY KEY ("id"))`);
await queryRunner.query(`CREATE TABLE "news" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "link" character varying NOT NULL, "publisher" text NOT NULL, "description" character varying NOT NULL, "imageUrl" character varying NOT NULL, "content" character varying NOT NULL, "timestamp" TIMESTAMP NOT NULL, CONSTRAINT "PK_39a43dfcb6007180f04aff2357e" PRIMARY KEY ("id"))`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "news"`);
await queryRunner.query(`DROP TABLE "sentiment_result"`);
}

}
13 changes: 0 additions & 13 deletions sentiment-analysis-service/entities/SentimentData.ts

This file was deleted.

25 changes: 25 additions & 0 deletions sentiment-analysis-service/entities/SentimentResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm";

@Entity()
export class SentimentResult {
@PrimaryGeneratedColumn()
id: number;

@Column()
stockSymbol: string;

@Column()
sentiment: string;

@Column()
confidence: number;

@Column({ nullable: true })
currentStockPrice: number;

@Column({ nullable: true })
priceChange: number;

@Column({ nullable: true })
percentageChange: number;
}
24 changes: 24 additions & 0 deletions sentiment-analysis-service/src/controllers/SentimentController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { JsonController, Get, Param } from "routing-controllers";
import { Service } from "typedi";
import SentimentService from "../services/SentimentService";

@JsonController()
@Service()
export class SentimentController {
constructor(private sentimentService: SentimentService) {}

@Get("/sentiment/:stockSymbol")
async getSentimentAnalysis(
@Param("stockSymbol") stockSymbol: string
): Promise<any> {
try {
const result =
await this.sentimentService.analyzeSentiment(
stockSymbol
);
return { success: true, data: result };
} catch (error) {
return { success: false, error: error.message };
}
}
}
19 changes: 19 additions & 0 deletions sentiment-analysis-service/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import "reflect-metadata";
import { createExpressServer, useContainer } from "routing-controllers";
import { Container } from "typedi";
import SentimentService from "./services/SentimentService";
import { SentimentController } from "./controllers/SentimentController";

useContainer(Container);

const app = createExpressServer({
controllers: [SentimentController],
});

const sentimentService = new SentimentService();

app.set("sentimentService", sentimentService);

app.listen(3003, () => {
console.log("Sentiment Analysis Service is running on port 3003");
});
130 changes: 130 additions & 0 deletions sentiment-analysis-service/src/services/SentimentService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import * as natural from "natural";
import axios from "axios";
import { Service } from "typedi";
import * as dotenv from "dotenv";

dotenv.config();

@Service()
class SentimentService {
private sentimentAnalyzer: any;
// store fetched articles

constructor() {
// initialize the sentiment analyzer
this.sentimentAnalyzer = new natural.SentimentAnalyzer(
"English",
natural.PorterStemmer,
"afinn"
);
}

async analyzeSentiment(stockSymbol: string): Promise<any> {
try {
// fetch stock information from the Stock Info service
const stockInfo = await this.fetchStockInfo(
stockSymbol
);

// fetch news articles from the news service
const newsArticles = await this.fetchNewsData(
stockSymbol
);

// perform sentiment analysis using fetched data
const sentimentResult = this.performSentimentAnalysis(
stockSymbol,
newsArticles,
stockInfo
);

return sentimentResult;
} catch (error) {
throw new Error(
`Error analyzing sentiment: ${error.message}`
);
}
}

private async fetchStockInfo(stockSymbol: string): Promise<any> {
try {
const response = await axios.get(
`${process.env.STOCK_INFO_SERVICE}/stock/${stockSymbol}`
);

return response.data;
} catch (error) {
throw new Error(
`Error fetching stock information: ${error.message}`
);
}
}

private async fetchNewsData(stockSymbol: string): Promise<any[]> {
try {
const response = await axios.get(
`${process.env.NEWS_SERVICE}/news/${stockSymbol}`
);

return response.data;
} catch (error) {
throw new Error(
`Error fetching news data: ${error.message}`
);
}
}

// logic to perform sentiment analysis
private performSentimentAnalysis(
stockSymbol: string,
newsArticles: any[],
stockInfo: any
): any {
const overallSentiment =
this.analyzeHeadlines(newsArticles) +
this.analyzeStockInfo(stockInfo);

const sentimentResult = {
stockSymbol: stockSymbol,
sentiment:
overallSentiment >= 0 ? "positive" : "negative",
confidence:
Math.abs(overallSentiment) /
(newsArticles.length + 1),
currentStockPrice: stockInfo.currentStockPrice,
};

return sentimentResult;
}

private analyzeHeadlines(newsArticles: any[]): number {
// analyse sentiment based on the headlines of news articles
return newsArticles.reduce((totalSentiment, article) => {
// extract the title from the current article
const articleTitle = article.title;

// get the sentiment analysis result for the title
const sentimentAnalysisResult =
this.sentimentAnalyzer.getSentiment(
articleTitle
);

console.log(sentimentAnalysisResult);

// extract the sentiment score from the analysis result
const sentimentScore = sentimentAnalysisResult.score;

// add the sentiment score to the totalSentiment
const updatedTotalSentiment =
totalSentiment + sentimentScore;

return updatedTotalSentiment;
}, 0);
}

private analyzeStockInfo(stockInfo: any): number {
return 0;
}
}

export default SentimentService;
13 changes: 13 additions & 0 deletions shared/utils/expandContractions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const contractionMap = {
"ain't": "am not",
"aren't": "are not",
"can't": "cannot",
// todo: add more contractions
};

// convert contractions to their standard lexicon
export const expandContractions = (text: string) => {
return text.replace(/(\b\w+'\w+\b)/g, (match) => {
return contractionMap[match] || match;
});
};
21 changes: 21 additions & 0 deletions shared/utils/removeStopwords.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const stopwords = [
"a",
"an",
"the",
"and",
"but",
"or",
"for",
"nor",
"on",
"at",
"to",
"from",
"by",
"in",
"of",
];

export const removeStopwords = (words) => {
return words.filter((word) => !stopwords.includes(word));
};
Loading

0 comments on commit ee2b0a3

Please sign in to comment.