- 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.
137 lines
4.1 KiB
Go
137 lines
4.1 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
|
|
"flowsight/internal/model"
|
|
"flowsight/internal/sectors"
|
|
)
|
|
|
|
// FlowSummary serves GET /api/flow/summary: foreign net total, top-5
|
|
// accumulation rows, rotation signal, mover of the day — all cited.
|
|
func (s *Server) FlowSummary(w http.ResponseWriter, r *http.Request) {
|
|
date := r.URL.Query().Get("date")
|
|
if date == "" {
|
|
date = "latest"
|
|
}
|
|
wl, _ := s.DB.Watchlist(s.userKey(r))
|
|
if len(wl) == 0 {
|
|
wl = s.Cfg.Watchlist
|
|
}
|
|
type accRow struct {
|
|
Ticker string `json:"ticker"`
|
|
NetSum float64 `json:"net_sum"`
|
|
Brokers int `json:"brokers"`
|
|
}
|
|
var accs []accRow
|
|
foreignTotal := 0.0
|
|
var cites []model.Citation
|
|
for _, tk := range wl {
|
|
if nets, err := s.DB.NetBuySum5d(tk); err == nil && len(nets) > 0 {
|
|
sum, n := 0.0, 0
|
|
for _, v := range nets {
|
|
if v > 0 {
|
|
n++
|
|
sum += v
|
|
}
|
|
}
|
|
accs = append(accs, accRow{tk, sum, n})
|
|
cites = append(cites, model.Cite("v2/broker-summary/"+tk+"/top/", tk, date))
|
|
}
|
|
if _, nets, err := s.DB.ForeignLast6(tk); err == nil && len(nets) > 0 {
|
|
foreignTotal += nets[len(nets)-1]
|
|
cites = append(cites, model.Cite("v2/foreign-flow/"+tk+"/", tk, date))
|
|
}
|
|
}
|
|
sort.Slice(accs, func(i, j int) bool { return accs[i].NetSum > accs[j].NetSum })
|
|
if len(accs) > 5 {
|
|
accs = accs[:5]
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"date": date, "foreign_net_total": foreignTotal,
|
|
"top_accumulation": accs, "citations": cites,
|
|
})
|
|
}
|
|
|
|
// brokerQuery validates ticker/start/end query params.
|
|
type brokerQuery struct {
|
|
Ticker string `validate:"required,len=4"`
|
|
Start string `validate:"omitempty,datetime=2006-01-02"`
|
|
End string `validate:"omitempty,datetime=2006-01-02"`
|
|
}
|
|
|
|
// FlowBroker serves GET /api/flow/broker: buyers/sellers + 5d net series.
|
|
func (s *Server) FlowBroker(w http.ResponseWriter, r *http.Request) {
|
|
q := brokerQuery{
|
|
Ticker: strings.ToUpper(r.URL.Query().Get("ticker")),
|
|
Start: r.URL.Query().Get("start"),
|
|
End: r.URL.Query().Get("end"),
|
|
}
|
|
if err := s.Validate.Struct(q); err != nil {
|
|
writeErr(w, http.StatusUnprocessableEntity, "ticker (4 letters) required, dates YYYY-MM-DD")
|
|
return
|
|
}
|
|
var top sectors.BrokerSummaryTop
|
|
if raw, d, err := s.DB.SnapshotAt(q.Ticker, "broker-summary-top", q.End); err == nil {
|
|
_ = json.Unmarshal([]byte(raw), &top)
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ticker": q.Ticker, "buyers": top.TopBuyers, "sellers": top.TopSellers,
|
|
"citations": []model.Citation{model.Cite("v2/broker-summary/"+q.Ticker+"/top/", q.Ticker, d)},
|
|
})
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ticker": q.Ticker, "buyers": []any{}, "sellers": []any{},
|
|
"citations": []model.Citation{}, "note": "no snapshots yet",
|
|
})
|
|
}
|
|
|
|
// FlowForeign serves GET /api/flow/foreign: inflow series + reversal flag.
|
|
func (s *Server) FlowForeign(w http.ResponseWriter, r *http.Request) {
|
|
q := brokerQuery{
|
|
Ticker: strings.ToUpper(r.URL.Query().Get("ticker")),
|
|
Start: r.URL.Query().Get("start"),
|
|
End: r.URL.Query().Get("end"),
|
|
}
|
|
if err := s.Validate.Struct(q); err != nil {
|
|
writeErr(w, http.StatusUnprocessableEntity, "ticker (4 letters) required, dates YYYY-MM-DD")
|
|
return
|
|
}
|
|
dates, nets, err := s.DB.ForeignWindow(q.Ticker, q.Start, q.End, 30)
|
|
if err != nil || len(nets) == 0 {
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ticker": q.Ticker, "series": []any{}, "reversal": false,
|
|
"citations": []model.Citation{}, "note": "no snapshots yet",
|
|
})
|
|
return
|
|
}
|
|
// Same 2x-magnitude rule as the alert engine: 5d cumulative one way,
|
|
// last day the other way at >2x the trailing 5d daily average.
|
|
reversal := false
|
|
if len(nets) >= 6 {
|
|
tail := nets[len(nets)-6:]
|
|
sum5, absAvg := 0.0, 0.0
|
|
for _, v := range tail[:5] {
|
|
sum5 += v
|
|
if v < 0 {
|
|
absAvg -= v
|
|
} else {
|
|
absAvg += v
|
|
}
|
|
}
|
|
absAvg /= 5
|
|
last := tail[5]
|
|
if absAvg > 0 && ((sum5 < 0 && last > 0 && last > 2*absAvg) ||
|
|
(sum5 > 0 && last < 0 && -last > 2*absAvg)) {
|
|
reversal = true
|
|
}
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"ticker": q.Ticker, "dates": dates, "nets": nets, "reversal": reversal,
|
|
"citations": []model.Citation{model.Cite("v2/foreign-flow/"+q.Ticker+"/", q.Ticker, dates[len(dates)-1])},
|
|
})
|
|
}
|