- Implemented Citations component to display citation data. - Created WatchlistDrawer and ChatSidebar components for managing watchlists and AI chat functionality. - Integrated API calls for watchlist management and chat interactions. - Updated index.tsx to include new components in the main application layout. - Added API client in lib/api.ts for structured API interactions. - Developed Alerts, Dashboard, Portfolio, Routines, Screener, and Report pages with relevant data fetching and UI components. - Introduced styles in tokens.css for consistent theming across the application. - Configured TypeScript and Vite for project setup and development.
214 lines
5.8 KiB
Go
214 lines
5.8 KiB
Go
// Seed loads one historical trading week into snapshots + derived tables so
|
|
// the full demo (briefing → radar → report → interrogation) runs offline.
|
|
// Fixtures live in tests/fixtures/ as JSON exports of real API shapes;
|
|
// seed/manifest.json maps each file to (ticker, date, source).
|
|
package store
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
)
|
|
|
|
// Manifest describes one historical-replay seed bundle.
|
|
type Manifest struct {
|
|
Week string `json:"week"`
|
|
Snapshots []ManifestSnap `json:"snapshots"`
|
|
Watchlist []string `json:"watchlist"`
|
|
Routines []ManifestRoutine `json:"routines"`
|
|
}
|
|
|
|
// ManifestSnap maps one fixture file to a snapshots row.
|
|
type ManifestSnap struct {
|
|
Ticker string `json:"ticker"`
|
|
Date string `json:"date"`
|
|
Source string `json:"source"`
|
|
File string `json:"file"`
|
|
}
|
|
|
|
// ManifestRoutine seeds one subscribed routine.
|
|
type ManifestRoutine struct {
|
|
UserKey string `json:"user_key"`
|
|
Type string `json:"type"`
|
|
Schedule string `json:"schedule"`
|
|
Channels []string `json:"channels"`
|
|
}
|
|
|
|
// SeedStats counts what the seed loaded.
|
|
type SeedStats struct {
|
|
Snapshots int
|
|
Watchlist int
|
|
Routines int
|
|
}
|
|
|
|
// NeedsSeed reports whether the DB has no snapshots yet.
|
|
func (db *DB) NeedsSeed() bool {
|
|
var n int
|
|
_ = db.QueryRow(`SELECT COUNT(*) FROM snapshots`).Scan(&n)
|
|
return n == 0
|
|
}
|
|
|
|
// SeedFromDir loads a fixture bundle. It is idempotent for snapshots
|
|
// (skips exact ticker/date/source duplicates) so re-runs are safe.
|
|
func (db *DB) SeedFromDir(dir, userKey string) (SeedStats, error) {
|
|
var st SeedStats
|
|
raw, err := os.ReadFile(filepath.Join(dir, "manifest.json"))
|
|
if err != nil {
|
|
return st, fmt.Errorf("store: read manifest: %w", err)
|
|
}
|
|
var m Manifest
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return st, fmt.Errorf("store: decode manifest: %w", err)
|
|
}
|
|
for _, s := range m.Snapshots {
|
|
body, err := os.ReadFile(filepath.Join(dir, s.File))
|
|
if err != nil {
|
|
return st, fmt.Errorf("store: read fixture %s: %w", s.File, err)
|
|
}
|
|
if !validJSON(body) {
|
|
return st, fmt.Errorf("store: fixture %s is not valid JSON", s.File)
|
|
}
|
|
var n int
|
|
_ = db.QueryRow(`SELECT COUNT(*) FROM snapshots WHERE ticker=? AND date=? AND source=?`,
|
|
s.Ticker, s.Date, s.Source).Scan(&n)
|
|
if n > 0 {
|
|
continue
|
|
}
|
|
if err := db.SaveSnapshot(s.Ticker, s.Date, s.Source, string(body)); err != nil {
|
|
return st, err
|
|
}
|
|
st.Snapshots++
|
|
if err := db.derive(s.Ticker, s.Date, s.Source, body); err != nil {
|
|
return st, err
|
|
}
|
|
}
|
|
for _, t := range m.Watchlist {
|
|
if err := db.AddWatch(userKey, t); err != nil {
|
|
return st, err
|
|
}
|
|
st.Watchlist++
|
|
}
|
|
existing, _ := db.ListRoutines(userKey)
|
|
have := map[string]bool{}
|
|
for _, r := range existing {
|
|
have[r.Type] = true
|
|
}
|
|
for _, r := range m.Routines {
|
|
if have[r.Type] {
|
|
continue
|
|
}
|
|
key := r.UserKey
|
|
if key == "" {
|
|
key = userKey
|
|
}
|
|
if _, err := db.CreateRoutine(key, r.Type, r.Schedule, r.Channels); err != nil {
|
|
return st, err
|
|
}
|
|
st.Routines++
|
|
}
|
|
return st, nil
|
|
}
|
|
|
|
func validJSON(b []byte) bool {
|
|
var v any
|
|
return json.Unmarshal(b, &v) == nil
|
|
}
|
|
|
|
// derive populates typed tables from raw snapshot payloads so rules and
|
|
// agents can query without re-parsing JSON on every cycle.
|
|
func (db *DB) derive(ticker, date, source string, body []byte) error {
|
|
switch source {
|
|
case "broker-summary-top":
|
|
var resp struct {
|
|
TopBuyers []struct {
|
|
BrokerCode string `json:"broker_code"`
|
|
NetIDR float64 `json:"net_idr"`
|
|
BuyIDR float64 `json:"buy_idr"`
|
|
SellIDR float64 `json:"sell_idr"`
|
|
} `json:"top_buyers"`
|
|
TopSellers []struct {
|
|
BrokerCode string `json:"broker_code"`
|
|
NetIDR float64 `json:"net_idr"`
|
|
BuyIDR float64 `json:"buy_idr"`
|
|
SellIDR float64 `json:"sell_idr"`
|
|
} `json:"top_sellers"`
|
|
}
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return err
|
|
}
|
|
for _, b := range resp.TopBuyers {
|
|
if err := db.UpsertBrokerActivity(b.BrokerCode, ticker, date,
|
|
b.BuyIDR, b.SellIDR, b.NetIDR, 0, 0, 0); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, s := range resp.TopSellers {
|
|
if err := db.UpsertBrokerActivity(s.BrokerCode, ticker, date,
|
|
s.BuyIDR, s.SellIDR, s.NetIDR, 0, 0, 0); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case "foreign-flow":
|
|
var resp struct {
|
|
Data []struct {
|
|
Date string `json:"date"`
|
|
NetForeignInflow float64 `json:"net_foreign_inflow"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return err
|
|
}
|
|
for _, d := range resp.Data {
|
|
if err := db.UpsertForeignFlow(ticker, d.Date, d.NetForeignInflow); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case "news":
|
|
var resp struct {
|
|
Results []struct {
|
|
Title string `json:"title"`
|
|
Source string `json:"source"`
|
|
Timestamp string `json:"timestamp"`
|
|
} `json:"results"`
|
|
}
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return err
|
|
}
|
|
for _, a := range resp.Results {
|
|
d := date
|
|
if len(a.Timestamp) >= 10 {
|
|
d = a.Timestamp[:10]
|
|
}
|
|
if err := db.InsertNews(ticker, d, a.Source, "neutral", 0.5, a.Source, a.Title); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case "filings":
|
|
var resp struct {
|
|
Results []struct {
|
|
Timestamp string `json:"timestamp"`
|
|
TransactionType string `json:"transaction_type"`
|
|
HolderType string `json:"holder_type"`
|
|
HolderName string `json:"holder_name"`
|
|
AmountTransaction float64 `json:"amount_transaction"`
|
|
Price float64 `json:"price"`
|
|
} `json:"results"`
|
|
}
|
|
if err := json.Unmarshal(body, &resp); err != nil {
|
|
return err
|
|
}
|
|
for _, f := range resp.Results {
|
|
d := date
|
|
if len(f.Timestamp) >= 10 {
|
|
d = f.Timestamp[:10]
|
|
}
|
|
if err := db.InsertFiling(ticker, d, f.HolderType, f.TransactionType,
|
|
f.AmountTransaction, f.Price, f.HolderName); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|