From 49dd2101e0088adc39451af3b25a7b9108ec24a2 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 16 Sep 2026 14:30:22 +0700 Subject: [PATCH] fix: local ticker search for screener (Sectors companies/ 401s on sub) - q= now searches universe table locally (ticker + company_name LIKE, order by close) instead of calling the Sectors companies/ endpoint, which returns SUBSCRIPTION_DOES_NOT_ALLOW on the current subscription and silently fell back to the full 780-ticker universe - removes the broken name-fill cmd + scheduler/one-shot Screen() name enrichment (wasted credits, empty names) - Screener UI: search card says 'kode saham', results still show company name when present --- backend/cmd/universe-sweep/main.go | 16 +------------ backend/internal/api/screen.go | 14 ++++++++++- backend/internal/scheduler/scheduler.go | 16 ++----------- backend/internal/store/rows.go | 31 +++++++++++++++++++++++++ web/src/pages/Screener.tsx | 4 ++-- 5 files changed, 49 insertions(+), 32 deletions(-) diff --git a/backend/cmd/universe-sweep/main.go b/backend/cmd/universe-sweep/main.go index 456a9bd..c7d59dd 100644 --- a/backend/cmd/universe-sweep/main.go +++ b/backend/cmd/universe-sweep/main.go @@ -1,7 +1,6 @@ package main import ( - "strings" "context" "encoding/json" "fmt" @@ -55,22 +54,9 @@ 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 { - tk := strings.ToUpper(strings.TrimSuffix(r.Symbol, ".JK")) - uni = append(uni, store.UniverseRow{Symbol: tk, Close: r.Close, Date: r.Date, CompanyName: names[tk]}) + uni = append(uni, store.UniverseRow{Symbol: r.Symbol, Close: r.Close, Date: r.Date}) } if err := db.SaveUniverse(uni); err != nil { log.Fatal(err) diff --git a/backend/internal/api/screen.go b/backend/internal/api/screen.go index 9254d48..28079f8 100644 --- a/backend/internal/api/screen.go +++ b/backend/internal/api/screen.go @@ -46,7 +46,19 @@ 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 != "") { + // Free-text q → local universe search (ticker + company_name, offline). + // The Sectors companies/ endpoint is not on this subscription, so remote + // screening is skipped entirely to avoid a silent full-universe fallback. + if req.Q != "" { + if hits, err := s.DB.SearchTickers(req.Q, limit*2); err == nil { + for _, h := range hits { + universe = append(universe, h.Symbol) + if h.CompanyName != "" { + names[h.Symbol] = h.CompanyName + } + } + } + } else if s.Cfg.HasSectorsKey() && req.Where != "" { if rows, err := s.Sectors.Screen(r.Context(), req.Where, req.Q, limit*2, 0); err == nil { for _, row := range rows { tk := strings.ToUpper(strings.TrimSuffix(row.Symbol, ".JK")) diff --git a/backend/internal/scheduler/scheduler.go b/backend/internal/scheduler/scheduler.go index 1b55cc5..3e3ccdb 100644 --- a/backend/internal/scheduler/scheduler.go +++ b/backend/internal/scheduler/scheduler.go @@ -6,7 +6,6 @@ package scheduler import ( - "strings" "context" "encoding/json" "fmt" @@ -284,21 +283,10 @@ 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. 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 - } - } - } + // IDX set instead of only tickers that have depth data yet. uni := make([]store.UniverseRow, 0, len(all)) for _, r := range all { - tk := strings.ToUpper(strings.TrimSuffix(r.Symbol, ".JK")) - uni = append(uni, store.UniverseRow{Symbol: tk, Close: r.Close, Date: r.Date, CompanyName: names[tk]}) + uni = append(uni, store.UniverseRow{Symbol: r.Symbol, Close: r.Close, Date: r.Date}) } if err := s.DB.SaveUniverse(uni); err != nil { log.Printf("scheduler: universe save: %v", err) diff --git a/backend/internal/store/rows.go b/backend/internal/store/rows.go index 4850518..8b9544a 100644 --- a/backend/internal/store/rows.go +++ b/backend/internal/store/rows.go @@ -682,6 +682,37 @@ func (db *DB) CompanyNames(tickers []string) (map[string]string, error) { return out, rows.Err() } +// TickerHit is one local search result over the universe table. +type TickerHit struct { + Symbol string + CompanyName string +} + +// SearchTickers finds universe tickers matching q in ticker or company_name +// (case-insensitive), most-traded first. Works offline — no Sectors API call. +func (db *DB) SearchTickers(q string, limit int) ([]TickerHit, error) { + if limit <= 0 { + limit = 20 + } + like := "%" + strings.ToUpper(strings.TrimSpace(q)) + "%" + rows, err := db.Query(`SELECT ticker, company_name FROM universe + WHERE ticker LIKE ? OR UPPER(company_name) LIKE ? + ORDER BY close DESC LIMIT ?`, like, like, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var out []TickerHit + for rows.Next() { + var h TickerHit + if err := rows.Scan(&h.Symbol, &h.CompanyName); err != nil { + return nil, err + } + out = append(out, h) + } + 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(?,?,?) diff --git a/web/src/pages/Screener.tsx b/web/src/pages/Screener.tsx index 46a9689..e5ec4e6 100644 --- a/web/src/pages/Screener.tsx +++ b/web/src/pages/Screener.tsx @@ -62,9 +62,9 @@ function ScreenerInner() { )} - 🔎 Cari nama / sektor sendiriKetik nama perusahaan atau sektor (mis. "bank", "energi"). + 🔎 Cari kode sahamKetik kode saham atau nama (mis. "bca", "tlkm", "bank"). - { setQ(e.currentTarget.value); setShowCustom(true); }} onKeyDown={(e: KeyboardEvent) => { if (e.key === "Enter") runCustom(); }} /> + { setQ(e.currentTarget.value); setShowCustom(true); }} onKeyDown={(e: KeyboardEvent) => { if (e.key === "Enter") runCustom(); }} />