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
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -55,22 +54,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
raw, _ := json.Marshal(all)
|
raw, _ := json.Marshal(all)
|
||||||
_ = db.SaveSnapshot("IDX", date, "close", string(raw))
|
_ = 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))
|
uni := make([]store.UniverseRow, 0, len(all))
|
||||||
for _, r := range all {
|
for _, r := range all {
|
||||||
tk := strings.ToUpper(strings.TrimSuffix(r.Symbol, ".JK"))
|
uni = append(uni, store.UniverseRow{Symbol: r.Symbol, Close: r.Close, Date: r.Date})
|
||||||
uni = append(uni, store.UniverseRow{Symbol: tk, Close: r.Close, Date: r.Date, CompanyName: names[tk]})
|
|
||||||
}
|
}
|
||||||
if err := db.SaveUniverse(uni); err != nil {
|
if err := db.SaveUniverse(uni); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|||||||
@@ -46,7 +46,19 @@ func (s *Server) Screen(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Base universe: live screener when keyed, else stored watchlist.
|
// Base universe: live screener when keyed, else stored watchlist.
|
||||||
var universe []string
|
var universe []string
|
||||||
names := map[string]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 {
|
if rows, err := s.Sectors.Screen(r.Context(), req.Where, req.Q, limit*2, 0); err == nil {
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
tk := strings.ToUpper(strings.TrimSuffix(row.Symbol, ".JK"))
|
tk := strings.ToUpper(strings.TrimSuffix(row.Symbol, ".JK"))
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
package scheduler
|
package scheduler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -284,21 +283,10 @@ func (s *Scheduler) universe(ctx context.Context) error {
|
|||||||
raw, _ := json.Marshal(all)
|
raw, _ := json.Marshal(all)
|
||||||
_ = s.DB.SaveSnapshot("IDX", date, "close", string(raw))
|
_ = s.DB.SaveSnapshot("IDX", date, "close", string(raw))
|
||||||
// Persist the universe ticker list so AllTickers() can return the full
|
// Persist the universe ticker list so AllTickers() can return the full
|
||||||
// IDX set instead of only tickers that have depth data yet. Company names
|
// IDX set instead of only tickers that have depth data yet.
|
||||||
// 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))
|
uni := make([]store.UniverseRow, 0, len(all))
|
||||||
for _, r := range all {
|
for _, r := range all {
|
||||||
tk := strings.ToUpper(strings.TrimSuffix(r.Symbol, ".JK"))
|
uni = append(uni, store.UniverseRow{Symbol: r.Symbol, Close: r.Close, Date: r.Date})
|
||||||
uni = append(uni, store.UniverseRow{Symbol: tk, Close: r.Close, Date: r.Date, CompanyName: names[tk]})
|
|
||||||
}
|
}
|
||||||
if err := s.DB.SaveUniverse(uni); err != nil {
|
if err := s.DB.SaveUniverse(uni); err != nil {
|
||||||
log.Printf("scheduler: universe save: %v", err)
|
log.Printf("scheduler: universe save: %v", err)
|
||||||
|
|||||||
@@ -682,6 +682,37 @@ func (db *DB) CompanyNames(tickers []string) (map[string]string, error) {
|
|||||||
return out, rows.Err()
|
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).
|
// AddWatch inserts a ticker (idempotent).
|
||||||
func (db *DB) AddWatch(userKey, ticker string) error {
|
func (db *DB) AddWatch(userKey, ticker string) error {
|
||||||
_, err := db.Exec(`INSERT INTO watchlists(user_key,ticker,added_at) VALUES(?,?,?)
|
_, err := db.Exec(`INSERT INTO watchlists(user_key,ticker,added_at) VALUES(?,?,?)
|
||||||
|
|||||||
@@ -62,9 +62,9 @@ function ScreenerInner() {
|
|||||||
</Card>
|
</Card>
|
||||||
)}</For>
|
)}</For>
|
||||||
<Card class={showCustom() ? "border-primary" : ""}>
|
<Card class={showCustom() ? "border-primary" : ""}>
|
||||||
<CardHeader class="pb-2"><CardTitle class="text-base">🔎 Cari nama / sektor sendiri</CardTitle><CardDescription>Ketik nama perusahaan atau sektor (mis. "bank", "energi").</CardDescription></CardHeader>
|
<CardHeader class="pb-2"><CardTitle class="text-base">🔎 Cari kode saham</CardTitle><CardDescription>Ketik kode saham atau nama (mis. "bca", "tlkm", "bank").</CardDescription></CardHeader>
|
||||||
<CardContent class="flex gap-2">
|
<CardContent class="flex gap-2">
|
||||||
<TextField class="flex-1"><TextFieldInput placeholder="mis. bank, bca, energi…" value={q()} onInput={(e) => { setQ(e.currentTarget.value); setShowCustom(true); }} onKeyDown={(e: KeyboardEvent) => { if (e.key === "Enter") runCustom(); }} /></TextField>
|
<TextField class="flex-1"><TextFieldInput placeholder="mis. bca, tlkm, bbri…" value={q()} onInput={(e) => { setQ(e.currentTarget.value); setShowCustom(true); }} onKeyDown={(e: KeyboardEvent) => { if (e.key === "Enter") runCustom(); }} /></TextField>
|
||||||
<Button size="sm" onClick={runCustom} disabled={busy() || !q().trim()}>{busy() && aktif() === "custom" ? "Menyaring…" : "Cari"}</Button>
|
<Button size="sm" onClick={runCustom} disabled={busy() || !q().trim()}>{busy() && aktif() === "custom" ? "Menyaring…" : "Cari"}</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
Reference in New Issue
Block a user