Files
flowsight/backend/internal/agents/broker_intel.go
T
asepharyana 8c184ccae1 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.
2026-09-15 12:36:48 +07:00

133 lines
3.8 KiB
Go

package agents
import (
"context"
"fmt"
"sort"
"strings"
"flowsight/internal/model"
"flowsight/internal/sectors"
)
// AnalyzeBrokerIntel (A2) classifies broker behavior and emits sector
// rotation on week-over-week sign flips with evidence rows.
func AnalyzeBrokerIntel(ctx context.Context, d Deps, ticker string) model.AgentResult {
_ = ctx
ticker = strings.ToUpper(ticker)
res := model.AgentResult{Summary: "no broker snapshots available"}
var registry []sectors.BrokerRegistryRow
regDate, regOK := payload(d.DB, "IDX", "brokers-registry", &registry)
var top struct {
Date string `json:"date"`
Results []sectors.TopBrokerRow `json:"results"`
}
topDate, topOK := payload(d.DB, "IDX", "brokers-top", &top)
if !regOK && !topOK {
return res
}
if regOK {
res.Citations = append(res.Citations, model.Cite("v2/brokers/", "IDX", regDate))
}
if topOK {
res.Citations = append(res.Citations, model.Cite("v2/brokers/top/", "IDX", topDate))
}
byCode := map[string]sectors.BrokerRegistryRow{}
for _, r := range registry {
byCode[r.Code] = r
}
accum, distrib := 0, 0
var lines []string
for _, b := range top.Results {
row := byCode[b.BrokerCode]
origin := "domestic"
cohort := "unknown"
if row.IsForeign {
origin = "foreign"
}
if row.Cohort != nil && *row.Cohort != "" {
cohort = *row.Cohort
}
class := "neutral"
switch {
case b.Net > 0:
class, accum = "accumulating", accum+1
case b.Net < 0:
class, distrib = "distributing", distrib+1
}
if len(lines) < 5 {
lines = append(lines, fmt.Sprintf("%s (%s/%s) %s %s",
b.BrokerCode, origin, cohort, class, fmtIDR(float64(b.Net))))
}
}
total := accum + distrib
score := 0.0
if total > 0 {
score = float64(accum-distrib) / float64(total) * 100
}
res.Score = clampScore(score, -100, 100)
// Rotation: week-over-week sign flip on stored sector nets.
var flow struct {
Week string `json:"week"`
Current map[string]float64 `json:"current"`
Previous map[string]float64 `json:"previous"`
}
flowDate, flowOK := payload(d.DB, "IDX", "sector-flow", &flow)
rotFrom, rotTo, rotDelta := "", "", 0.0
if flowOK {
res.Citations = append(res.Citations, model.Cite("v2/subsector/report/", "IDX", flowDate))
type flip struct {
sector string
delta float64
}
var flips []flip
for s, cur := range flow.Current {
prev := flow.Previous[s]
if prev < 0 && cur > 0 {
flips = append(flips, flip{s, cur - prev})
}
}
var outflows []flip
for s, cur := range flow.Current {
prev := flow.Previous[s]
if prev > 0 && cur < 0 {
outflows = append(outflows, flip{s, prev - cur})
}
}
sort.Slice(flips, func(i, j int) bool { return flips[i].delta > flips[j].delta })
sort.Slice(outflows, func(i, j int) bool { return outflows[i].delta > outflows[j].delta })
if len(flips) > 0 && len(outflows) > 0 {
rotFrom, rotTo, rotDelta = outflows[0].sector, flips[0].sector, flips[0].delta
res.Flags = append(res.Flags, "sector-rotation")
}
}
res.Values = []model.Value{{
Label: "broker behavior",
Display: fmt.Sprintf("%d accumulating vs %d distributing", accum, distrib),
Citations: res.Citations,
}, {
Label: "top brokers",
Display: strings.Join(lines, "; "),
Citations: res.Citations,
}}
if rotFrom != "" {
res.Values = append(res.Values, model.Value{
Label: "sector rotation",
Display: fmt.Sprintf("%s -> %s (%s swing)", rotFrom, rotTo, fmtIDR(rotDelta)),
Citations: res.Citations,
})
res.Summary = fmt.Sprintf("rotation %s -> %s; score %+.0f", rotFrom, rotTo, score)
} else {
res.Summary = fmt.Sprintf("no rotation flip; score %+.0f (%d vs %d)", score, accum, distrib)
}
res.Extra = map[string]any{
"accumulating": accum, "distributing": distrib,
"rotation_from": rotFrom, "rotation_to": rotTo, "rotation_delta": rotDelta,
}
return res
}