feat: full IDX universe (780 ticker) — universe table + close sweep harian, dashboard foreign-flow picker dari semua ticker, rotateDepth incremental fill 10/cylcle, strip .JK, backfill cmd/universe-sweep
CI / go test + web typecheck (push) Canceled after 0s
CI / build + deploy (Nix) — flowsight (push) Canceled after 0s

This commit is contained in:
asepharyana
2026-09-16 01:06:42 +07:00
parent 07ece10474
commit 6fd89b6c19
8 changed files with 275 additions and 14 deletions
@@ -0,0 +1,6 @@
-- 0009_universe.sql: persisted full IDX ticker universe from the close sweep.
CREATE TABLE IF NOT EXISTS universe(
ticker TEXT PRIMARY KEY,
close INTEGER NOT NULL DEFAULT 0,
date TEXT NOT NULL DEFAULT ''
);
+69 -3
View File
@@ -4,6 +4,7 @@ import (
"database/sql"
"encoding/json"
"fmt"
"strings"
"time"
)
@@ -545,23 +546,88 @@ func (db *DB) Watchlist(userKey string) ([]string, error) {
// default universe: dashboard, screener fallback, and scheduler depth all use
// it when a user has no personal watchlist.
func (db *DB) AllTickers() ([]string, error) {
rows, err := db.Query(`SELECT DISTINCT ticker FROM snapshots
// Prefer the persisted full-universe list (from close/ sweep).
rows, err := db.Query(`SELECT ticker FROM universe ORDER BY ticker`)
if err == nil {
var out []string
for rows.Next() {
var t string
if err := rows.Scan(&t); err != nil {
rows.Close()
break
}
out = append(out, t)
}
rows.Close()
if len(out) > 0 {
return out, nil
}
}
// Fallback: distinct snapshots (avoids pseudo indices).
rows2, err := db.Query(`SELECT DISTINCT ticker FROM snapshots
WHERE ticker NOT IN ('IDX','ROE') ORDER BY ticker`)
if err != nil {
return nil, err
}
defer rows2.Close()
var out2 []string
for rows2.Next() {
var t string
if err := rows2.Scan(&t); err != nil {
return nil, err
}
out2 = append(out2, t)
}
return out2, rows2.Err()
}
// TickersWithForeign returns the set of tickers that have ≥1 foreign-flow row.
func (db *DB) TickersWithForeign() (map[string]bool, error) {
rows, err := db.Query(`SELECT DISTINCT ticker FROM foreign_flow`)
if err != nil {
return nil, err
}
defer rows.Close()
var out []string
out := map[string]bool{}
for rows.Next() {
var t string
if err := rows.Scan(&t); err != nil {
return nil, err
}
out = append(out, t)
out[t] = true
}
return out, rows.Err()
}
// 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
}
func (db *DB) SaveUniverse(rows []UniverseRow) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err := tx.Exec(`DELETE FROM universe`); err != nil {
return err
}
for _, r := range rows {
// 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 {
return err
}
}
return tx.Commit()
}
// 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(?,?,?)