idx-cli/msn/news_analysis.go

194 lines
6.1 KiB
Go

package msn
import (
"strings"
)
// News categories
const (
CategoryEarnings = "earnings"
CategoryDividend = "dividend"
CategoryCorporateAction = "corporate_action"
CategoryRegulation = "regulation"
CategoryRating = "rating"
CategoryExpansion = "expansion"
CategoryLeadership = "leadership"
CategoryMarket = "market"
CategoryGeneral = "general"
)
// Sentiment types
const (
SentimentPositive = "positive"
SentimentNegative = "negative"
SentimentNeutral = "neutral"
)
// Category keywords (Indonesian + English)
var categoryKeywords = map[string][]string{
CategoryEarnings: {
"laba", "rugi", "earnings", "profit", "net income", "pendapatan",
"revenue", "keuntungan", "kerugian", "loss", "income", "untung",
"quarterly", "kuartalan", "annual report", "laporan tahunan",
"eps", "earning per share",
},
CategoryDividend: {
"dividen", "dividend", "pembagian", "interim", "final dividend",
"cum date", "ex date", "payment date", "tanggal pembayaran",
"yield", "payout",
},
CategoryCorporateAction: {
"akuisisi", "merger", "acquisition", "rights issue", "stock split",
"reverse split", "buyback", "ipo", "penawaran umum", "private placement",
"tender offer", "spin off", "spinoff", "demerger", "konsolidasi",
"rights", "waran", "warrant", "obligasi", "bond", "sukuk",
},
CategoryRegulation: {
"ojk", "regulasi", "peraturan", "kebijakan", "regulation", "policy",
"compliance", "kepatuhan", "lisensi", "license", "izin", "permit",
"pemerintah", "government", "bapepam", "bei", "idx", "bursa",
},
CategoryRating: {
"rating", "peringkat", "upgrade", "downgrade", "outlook",
"stable", "positive", "negative", "credit rating", "moody",
"fitch", "s&p", "pefindo", "target price", "rekomendasi",
"buy", "sell", "hold", "analyst",
},
CategoryExpansion: {
"ekspansi", "expansion", "investasi", "investment", "proyek baru",
"new project", "pabrik", "factory", "plant", "cabang", "branch",
"pembangunan", "construction", "development", "joint venture", "jv",
"kerjasama", "partnership", "kontrak", "contract",
},
CategoryLeadership: {
"direktur", "director", "komisaris", "commissioner", "ceo", "cfo",
"president director", "management", "manajemen", "direksi",
"rups", "agm", "annual general meeting", "pengangkatan", "appointment",
"pengunduran", "resignation", "pergantian", "change",
},
CategoryMarket: {
"ihsg", "idx", "pasar modal", "bursa", "market", "saham",
"stock", "trading", "perdagangan", "volume", "kapitalisasi",
"market cap", "blue chip", "lq45", "idx80", "kompas100",
},
}
// Positive sentiment keywords
var positiveKeywords = []string{
// Indonesian
"naik", "untung", "tumbuh", "positif", "optimis", "meningkat",
"surplus", "berhasil", "sukses", "cemerlang", "bagus", "baik",
"membaik", "melonjak", "meroket", "tertinggi", "rekor",
"peningkatan", "pertumbuhan", "keuntungan", "laba bersih",
"ekspansi", "pemulihan", "recovery",
// English
"rise", "gain", "growth", "positive", "optimistic", "increase",
"surplus", "success", "excellent", "good", "improve", "surge",
"soar", "highest", "record", "profit", "expansion", "recovery",
"bullish", "upgrade", "beat", "exceed", "outperform",
}
// Negative sentiment keywords
var negativeKeywords = []string{
// Indonesian
"turun", "rugi", "anjlok", "negatif", "pesimis", "menurun",
"defisit", "gagal", "buruk", "memburuk", "jatuh", "tertekan",
"terendah", "penurunan", "kerugian", "merosot", "melemah",
"default", "bangkrut", "pailit", "koreksi", "tekanan",
// English
"fall", "loss", "plunge", "negative", "pessimistic", "decrease",
"deficit", "fail", "bad", "worsen", "drop", "pressure",
"lowest", "decline", "weak", "default", "bankrupt", "correction",
"bearish", "downgrade", "miss", "underperform", "concern", "risk",
}
// Critical news keywords (alerts)
var criticalKeywords = []string{
// Indonesian
"suspend", "suspensi", "fraud", "penipuan", "korupsi", "corruption",
"default", "gagal bayar", "bangkrut", "pailit", "bankruptcy",
"delisting", "pencabutan", "investigasi", "investigation",
"skandal", "scandal", "illegal", "ilegal", "pelanggaran", "violation",
"tuntutan", "lawsuit", "gugatan", "denda", "fine", "sanksi", "sanction",
"pkpu", "penundaan", "moratorium", "restrukturisasi utang",
// English
"suspend", "fraud", "corruption", "default", "bankrupt", "bankruptcy",
"delisting", "investigation", "scandal", "illegal", "violation",
"lawsuit", "fine", "sanction", "debt restructuring", "warning",
"material adverse", "going concern", "audit opinion", "disclaimer",
}
// categorizeNews determines the category of a news article
func categorizeNews(title, abstract string) string {
text := strings.ToLower(title + " " + abstract)
// Check each category
maxScore := 0
bestCategory := CategoryGeneral
for category, keywords := range categoryKeywords {
score := 0
for _, keyword := range keywords {
if strings.Contains(text, keyword) {
score++
}
}
if score > maxScore {
maxScore = score
bestCategory = category
}
}
return bestCategory
}
// scoreNewsSentiment analyzes sentiment of a news article
func scoreNewsSentiment(title, abstract string) (sentiment string, score float64) {
text := strings.ToLower(title + " " + abstract)
positiveScore := 0
negativeScore := 0
for _, keyword := range positiveKeywords {
if strings.Contains(text, keyword) {
positiveScore++
}
}
for _, keyword := range negativeKeywords {
if strings.Contains(text, keyword) {
negativeScore++
}
}
totalScore := positiveScore + negativeScore
if totalScore == 0 {
return SentimentNeutral, 0.0
}
// Calculate score from -1 (very negative) to +1 (very positive)
score = float64(positiveScore-negativeScore) / float64(totalScore)
if score > 0.2 {
sentiment = SentimentPositive
} else if score < -0.2 {
sentiment = SentimentNegative
} else {
sentiment = SentimentNeutral
}
return sentiment, score
}
// isNewsCritical checks if news contains critical/alert-worthy content
func isNewsCritical(title, abstract string) bool {
text := strings.ToLower(title + " " + abstract)
for _, keyword := range criticalKeywords {
if strings.Contains(text, keyword) {
return true
}
}
return false
}