feat: company names in screener + universe enrichment
CI / go test + web typecheck (push) Canceled after 0s
CI / build + deploy (Nix) — flowsight (push) Canceled after 0s

- migration 0010: universe.company_name column
- SaveUniverse persists company names; new CompanyNames() store lookup
- scheduler universe sweep + universe-sweep cmd fetch names via one Screen() call (best effort)
- /api/screen now returns real company names (live when keyed, stored as fallback)
- Screener card shows 'PT Bank ...' name under the ticker symbol for search results
This commit is contained in:
asepharyana
2026-09-16 14:23:09 +07:00
parent e203c61279
commit d7eb0a3abf
6 changed files with 86 additions and 10 deletions
+15 -1
View File
@@ -1,6 +1,7 @@
package main
import (
"strings"
"context"
"encoding/json"
"fmt"
@@ -54,9 +55,22 @@ func main() {
}
raw, _ := json.Marshal(all)
_ = db.SaveSnapshot("IDX", date, "close", string(raw))
// Best-effort company-name enrichment (one screener call) so the screener
// search can display "PT Bank Central Asia Tbk" instead of only "BBCA".
names := map[string]string{}
if rows, err := c.Screen(ctx, "", "", 1000, 0); err == nil {
for _, row := range rows {
if row.CompanyName != "" {
names[strings.ToUpper(strings.TrimSuffix(row.Symbol, ".JK"))] = row.CompanyName
}
}
} else {
log.Printf("name enrichment skipped: %v", err)
}
uni := make([]store.UniverseRow, 0, len(all))
for _, r := range all {
uni = append(uni, store.UniverseRow{Symbol: r.Symbol, Close: r.Close, Date: r.Date})
tk := strings.ToUpper(strings.TrimSuffix(r.Symbol, ".JK"))
uni = append(uni, store.UniverseRow{Symbol: tk, Close: r.Close, Date: r.Date, CompanyName: names[tk]})
}
if err := db.SaveUniverse(uni); err != nil {
log.Fatal(err)
+14 -1
View File
@@ -45,10 +45,15 @@ func (s *Server) Screen(w http.ResponseWriter, r *http.Request) {
}
// Base universe: live screener when keyed, else stored watchlist.
var universe []string
names := map[string]string{}
if s.Cfg.HasSectorsKey() && (req.Where != "" || req.Q != "") {
if rows, err := s.Sectors.Screen(r.Context(), req.Where, req.Q, limit*2, 0); err == nil {
for _, row := range rows {
universe = append(universe, strings.ToUpper(strings.TrimSuffix(row.Symbol, ".JK")))
tk := strings.ToUpper(strings.TrimSuffix(row.Symbol, ".JK"))
universe = append(universe, tk)
if row.CompanyName != "" {
names[tk] = row.CompanyName
}
}
}
}
@@ -61,10 +66,18 @@ func (s *Server) Screen(w http.ResponseWriter, r *http.Request) {
universe = s.Cfg.Watchlist
}
}
// Fall back to stored company names when offline (universe table).
storedNames, _ := s.DB.CompanyNames(universe)
for tk, nm := range storedNames {
names[tk] = nm
}
}
var rows []ScreenRow
for _, tk := range universe {
row := s.scoreTicker(tk)
if nm := names[tk]; nm != "" {
row.Name = nm
}
if req.Institutional != nil {
inst := req.Institutional
if b, _ := row.Breakdown["broker_score"].(float64); b < inst.BrokerScoreMin {
+14 -2
View File
@@ -6,6 +6,7 @@
package scheduler
import (
"strings"
"context"
"encoding/json"
"fmt"
@@ -283,10 +284,21 @@ func (s *Scheduler) universe(ctx context.Context) error {
raw, _ := json.Marshal(all)
_ = s.DB.SaveSnapshot("IDX", date, "close", string(raw))
// Persist the universe ticker list so AllTickers() can return the full
// IDX set instead of only tickers that have depth data yet.
// IDX set instead of only tickers that have depth data yet. Company names
// ride along (one extra screener call — best effort, skipped on failure)
// so the screener search can show "PT Bank Central Asia Tbk" not "BBCA".
names := map[string]string{}
if rows, err := s.Sectors.Screen(ctx, "", "", 1000, 0); err == nil {
for _, row := range rows {
if row.CompanyName != "" {
names[strings.ToUpper(strings.TrimSuffix(row.Symbol, ".JK"))] = row.CompanyName
}
}
}
uni := make([]store.UniverseRow, 0, len(all))
for _, r := range all {
uni = append(uni, store.UniverseRow{Symbol: r.Symbol, Close: r.Close, Date: r.Date})
tk := strings.ToUpper(strings.TrimSuffix(r.Symbol, ".JK"))
uni = append(uni, store.UniverseRow{Symbol: tk, Close: r.Close, Date: r.Date, CompanyName: names[tk]})
}
if err := s.DB.SaveUniverse(uni); err != nil {
log.Printf("scheduler: universe save: %v", err)
@@ -0,0 +1,2 @@
-- 0010_universe_names.sql: company names for screener display.
ALTER TABLE universe ADD COLUMN company_name TEXT NOT NULL DEFAULT '';
+35 -5
View File
@@ -626,9 +626,10 @@ func (db *DB) TickersWithForeign() (map[string]bool, error) {
// SaveUniverse inserts the full ticker universe from the close sweep.
// UniverseRow mirrors the CloseRow shape without importing sectors (cycle).
type UniverseRow struct {
Symbol string
Close int64
Date string
Symbol string
Close int64
Date string
CompanyName string
}
func (db *DB) SaveUniverse(rows []UniverseRow) error {
@@ -644,14 +645,43 @@ func (db *DB) SaveUniverse(rows []UniverseRow) error {
// Normalize: API returns "BBCA.JK" — strip the suffix so it matches
// watchlist/depth keys (BBCA) everywhere.
t := strings.TrimSuffix(r.Symbol, ".JK")
if _, err := tx.Exec(`INSERT OR IGNORE INTO universe(ticker, close, date) VALUES(?,?,?)`,
t, r.Close, r.Date); err != nil {
if _, err := tx.Exec(`INSERT OR IGNORE INTO universe(ticker, close, date, company_name) VALUES(?,?,?,?)`,
t, r.Close, r.Date, r.CompanyName); err != nil {
return err
}
}
return tx.Commit()
}
// CompanyNames returns the company name map for the given tickers (from the
// universe table). Tickers with no stored name are omitted.
func (db *DB) CompanyNames(tickers []string) (map[string]string, error) {
if len(tickers) == 0 {
return map[string]string{}, nil
}
ph := strings.Repeat("?,", len(tickers))
ph = ph[:len(ph)-1]
args := make([]any, len(tickers))
for i, t := range tickers {
args[i] = t
}
rows, err := db.Query(`SELECT ticker, company_name FROM universe
WHERE company_name != '' AND ticker IN (`+ph+`)`, args...)
if err != nil {
return nil, err
}
defer rows.Close()
out := map[string]string{}
for rows.Next() {
var t, nm string
if err := rows.Scan(&t, &nm); err != nil {
return nil, err
}
out[t] = nm
}
return out, rows.Err()
}
// AddWatch inserts a ticker (idempotent).
func (db *DB) AddWatch(userKey, ticker string) error {
_, err := db.Exec(`INSERT INTO watchlists(user_key,ticker,added_at) VALUES(?,?,?)