feat: add Citations and WatchlistChat components, integrate with API
- 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.
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"flowsight/internal/model"
|
||||
)
|
||||
|
||||
// ScreenRequest is POST /api/screen body.
|
||||
type ScreenRequest struct {
|
||||
Where string `json:"where"`
|
||||
Q string `json:"q"`
|
||||
Institutional *struct {
|
||||
BrokerScoreMin float64 `json:"broker_score_min"`
|
||||
ForeignTrend string `json:"foreign_trend"`
|
||||
InsiderBuying bool `json:"insider_buying"`
|
||||
VolumeAnomaly bool `json:"volume_anomaly"`
|
||||
} `json:"institutional"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
|
||||
// ScreenRow is one ranked result with per-row signal breakdown.
|
||||
type ScreenRow struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Name string `json:"name"`
|
||||
Composite float64 `json:"composite"`
|
||||
Breakdown map[string]any `json:"breakdown"`
|
||||
Citations []model.Citation `json:"citations"`
|
||||
}
|
||||
|
||||
// Screen serves POST /api/screen: companies/ base filter enriched with
|
||||
// broker score + foreign trend + insider flag, ranked composite.
|
||||
func (s *Server) Screen(w http.ResponseWriter, r *http.Request) {
|
||||
var req ScreenRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, "invalid JSON body")
|
||||
return
|
||||
}
|
||||
limit := req.Limit
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 20
|
||||
}
|
||||
// Base universe: live screener when keyed, else stored watchlist.
|
||||
var universe []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")))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(universe) == 0 {
|
||||
universe, _ = s.DB.Watchlist(s.userKey(r))
|
||||
if len(universe) == 0 {
|
||||
universe = s.Cfg.Watchlist
|
||||
}
|
||||
}
|
||||
var rows []ScreenRow
|
||||
for _, tk := range universe {
|
||||
row := s.scoreTicker(tk)
|
||||
if req.Institutional != nil {
|
||||
inst := req.Institutional
|
||||
if b, _ := row.Breakdown["broker_score"].(float64); b < inst.BrokerScoreMin {
|
||||
continue
|
||||
}
|
||||
if inst.ForeignTrend != "" {
|
||||
if t, _ := row.Breakdown["foreign_trend"].(string); t != inst.ForeignTrend {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if inst.InsiderBuying {
|
||||
if b, _ := row.Breakdown["insider_buying"].(bool); !b {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if inst.VolumeAnomaly {
|
||||
if b, _ := row.Breakdown["volume_anomaly"].(bool); !b {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
sort.Slice(rows, func(i, j int) bool { return rows[i].Composite > rows[j].Composite })
|
||||
if len(rows) > limit {
|
||||
rows = rows[:limit]
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]any{"rows": rows, "count": len(rows)})
|
||||
}
|
||||
|
||||
// scoreTicker computes the composite (broker 40 + foreign 25 + insider 15 + volume 20).
|
||||
func (s *Server) scoreTicker(tk string) ScreenRow {
|
||||
tk = strings.ToUpper(tk)
|
||||
row := ScreenRow{Symbol: tk, Name: tk, Breakdown: map[string]any{}}
|
||||
// Broker score from 5d net imbalance.
|
||||
brokerScore := 0.0
|
||||
if nets, err := s.DB.NetBuySum5d(tk); err == nil && len(nets) > 0 {
|
||||
pos, neg := 0.0, 0.0
|
||||
for _, v := range nets {
|
||||
if v > 0 {
|
||||
pos += v
|
||||
} else {
|
||||
neg -= v
|
||||
}
|
||||
}
|
||||
if tot := pos + neg; tot > 0 {
|
||||
brokerScore = (pos - neg) / tot * 100
|
||||
}
|
||||
row.Citations = append(row.Citations, model.Cite("v2/broker-summary/"+tk+"/top/", tk, "stored"))
|
||||
}
|
||||
// Foreign trend from last-6 series.
|
||||
foreignScore, trend := 0.0, "flat"
|
||||
if dates, nets, err := s.DB.ForeignLast6(tk); err == nil && len(nets) > 0 {
|
||||
last := nets[len(nets)-1]
|
||||
if last > 0 {
|
||||
foreignScore, trend = 50, "inflow"
|
||||
} else if last < 0 {
|
||||
foreignScore, trend = -50, "outflow"
|
||||
}
|
||||
row.Citations = append(row.Citations, model.Cite("v2/foreign-flow/"+tk+"/", tk, dates[len(dates)-1]))
|
||||
}
|
||||
// Insider flag from filings average.
|
||||
insider := s.DB.FilingAvg30(tk) > 0
|
||||
// Volume anomaly from stored daily bars.
|
||||
volAnom, volMult := false, 0.0
|
||||
if vols, _, err := s.DB.DailyVolumes(tk, 21); err == nil && len(vols) >= 2 {
|
||||
n := len(vols)
|
||||
if a := avgF(vols[:n-1]); a > 0 {
|
||||
volMult = vols[n-1] / a
|
||||
volAnom = volMult > 2
|
||||
}
|
||||
row.Citations = append(row.Citations, model.Cite("v2/daily/"+tk+"/", tk, "stored"))
|
||||
}
|
||||
volScore := 0.0
|
||||
if volAnom {
|
||||
volScore = 50
|
||||
}
|
||||
row.Composite = brokerScore*0.4 + foreignScore*0.25 + volScore*0.2
|
||||
if insider {
|
||||
row.Composite += 7.5
|
||||
}
|
||||
row.Breakdown = map[string]any{
|
||||
"broker": brokerScore, "broker_score": brokerScore,
|
||||
"foreign": foreignScore, "foreign_trend": trend,
|
||||
"insider": insider, "insider_buying": insider,
|
||||
"volume_mult": volMult, "volume_anomaly": volAnom,
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
func avgF(xs []float64) float64 {
|
||||
if len(xs) == 0 {
|
||||
return 0
|
||||
}
|
||||
sum := 0.0
|
||||
for _, x := range xs {
|
||||
sum += x
|
||||
}
|
||||
return sum / float64(len(xs))
|
||||
}
|
||||
Reference in New Issue
Block a user