- 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.
720 lines
21 KiB
Go
720 lines
21 KiB
Go
package sectors
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/url"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// param helpers keep every wrapper narrow by default (credit traps in
|
||
// docs/API-REFERENCE.md): sections explicit, classifications+periods minimal,
|
||
// n_quarters bounded, free-float filters exclusive.
|
||
|
||
func dateRange(start, end string, clamp func(time.Time, time.Time) (time.Time, time.Time)) (string, string) {
|
||
if start == "" || end == "" {
|
||
e := time.Now()
|
||
s := e.AddDate(0, 0, -7)
|
||
if start == "" && end != "" {
|
||
if t, err := time.Parse("2006-01-02", end); err == nil {
|
||
e, s = t, t.AddDate(0, 0, -7)
|
||
}
|
||
}
|
||
return s.Format("2006-01-02"), e.Format("2006-01-02")
|
||
}
|
||
ts, err1 := time.Parse("2006-01-02", start)
|
||
te, err2 := time.Parse("2006-01-02", end)
|
||
if err1 != nil || err2 != nil {
|
||
return start, end
|
||
}
|
||
ts, te = clamp(ts, te)
|
||
return ts.Format("2006-01-02"), te.Format("2006-01-02")
|
||
}
|
||
|
||
func decode(data []byte, v any, endpoint string) error {
|
||
if err := json.Unmarshal(data, v); err != nil {
|
||
return fmt.Errorf("sectors: decode %s: %w", endpoint, err)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// --- Brokers: the moat ---
|
||
|
||
// BrokersRegistry returns the broker registry (cached daily by the scheduler).
|
||
func (c *Client) BrokersRegistry(ctx context.Context, cohort, origin string) ([]BrokerRegistryRow, error) {
|
||
q := url.Values{}
|
||
if cohort != "" && cohort != "all" {
|
||
q.Set("cohort", cohort)
|
||
}
|
||
if origin != "" && origin != "all" {
|
||
q.Set("origin", origin)
|
||
}
|
||
raw, err := c.Get(ctx, "brokers/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var rows []BrokerRegistryRow
|
||
if err := decode(raw, &rows, "v2/brokers/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return rows, nil
|
||
}
|
||
|
||
// BrokersTop returns the daily broker ranking.
|
||
func (c *Client) BrokersTop(ctx context.Context, date, metric string, n int, origin, cohort string) ([]TopBrokerRow, error) {
|
||
q := url.Values{}
|
||
if date != "" {
|
||
q.Set("date", date)
|
||
}
|
||
if metric == "" {
|
||
metric = "net"
|
||
}
|
||
q.Set("metric", metric)
|
||
if n <= 0 || n > 90 {
|
||
n = 20
|
||
}
|
||
q.Set("n_brokers", fmt.Sprint(n))
|
||
if origin != "" {
|
||
q.Set("origin", origin)
|
||
}
|
||
if cohort != "" {
|
||
q.Set("cohort", cohort)
|
||
}
|
||
raw, err := c.Get(ctx, "brokers/top/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var resp struct {
|
||
Results []TopBrokerRow `json:"results"`
|
||
}
|
||
if err := decode(raw, &resp, "v2/brokers/top/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return resp.Results, nil
|
||
}
|
||
|
||
// BrokerActivity returns all (stock,day) rows for one broker (window ≤14d).
|
||
func (c *Client) BrokerActivity(ctx context.Context, brokerCode, symbol, start, end string) ([]BrokerRow, error) {
|
||
start, end = dateRange(start, end, ClampBrokerWindow)
|
||
q := url.Values{"start": {start}, "end": {end}}
|
||
if symbol != "" {
|
||
q.Set("symbol", strings.ToUpper(symbol))
|
||
}
|
||
raw, err := c.Get(ctx, "broker-activity/"+strings.ToUpper(brokerCode)+"/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var rows []BrokerRow
|
||
if err := decode(raw, &rows, "v2/broker-activity/{broker_code}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return rows, nil
|
||
}
|
||
|
||
// BrokerActivityTop returns top accumulations/distributions for one broker.
|
||
func (c *Client) BrokerActivityTop(ctx context.Context, brokerCode, start, end string, n int) (accum, distrib []BrokerRow, err error) {
|
||
start, end = dateRange(start, end, ClampBrokerWindow)
|
||
q := url.Values{"start": {start}, "end": {end}}
|
||
if n > 0 {
|
||
q.Set("n_brokers", fmt.Sprint(n))
|
||
}
|
||
raw, err := c.Get(ctx, "broker-activity/"+strings.ToUpper(brokerCode)+"/top/", q)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
var resp struct {
|
||
TopAccumulations []BrokerRow `json:"top_accumulations"`
|
||
TopDistributions []BrokerRow `json:"top_distributions"`
|
||
TopAccumulation []BrokerRow `json:"top_accumulation"`
|
||
TopDistribution []BrokerRow `json:"top_distribution"`
|
||
Accumulations []BrokerRow `json:"accumulations"`
|
||
Distributions []BrokerRow `json:"distributions"`
|
||
Data []BrokerRow `json:"data"`
|
||
Results []BrokerRow `json:"results"`
|
||
}
|
||
if err := decode(raw, &resp, "v2/broker-activity/{code}/top/"); err != nil {
|
||
return nil, nil, err
|
||
}
|
||
accum = append(append(append(resp.TopAccumulations, resp.TopAccumulation...), resp.Accumulations...), resp.Data...)
|
||
distrib = append(append(resp.TopDistributions, resp.TopDistribution...), resp.Distributions...)
|
||
if len(accum) == 0 && len(resp.Results) > 0 {
|
||
accum = resp.Results
|
||
}
|
||
return accum, distrib, nil
|
||
}
|
||
|
||
// BrokerSummary returns per-broker daily rows for one ticker (window ≤14d).
|
||
func (c *Client) BrokerSummary(ctx context.Context, symbol, brokerCode, start, end string) ([]BrokerRow, error) {
|
||
start, end = dateRange(start, end, ClampBrokerWindow)
|
||
q := url.Values{"start": {start}, "end": {end}}
|
||
if brokerCode != "" {
|
||
q.Set("broker_code", strings.ToUpper(brokerCode))
|
||
}
|
||
raw, err := c.Get(ctx, "broker-summary/"+strings.ToUpper(symbol)+"/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// Shape varies (rows array or {dates: {broker: rows}}); normalize both.
|
||
var rows []BrokerRow
|
||
if err := json.Unmarshal(raw, &rows); err == nil {
|
||
return rows, nil
|
||
}
|
||
var byDate map[string]map[string][]BrokerRow
|
||
if err := json.Unmarshal(raw, &byDate); err == nil {
|
||
for _, brokers := range byDate {
|
||
for _, r := range brokers {
|
||
rows = append(rows, r...)
|
||
}
|
||
}
|
||
return rows, nil
|
||
}
|
||
var wrapped struct {
|
||
Data []BrokerRow `json:"data"`
|
||
Results []BrokerRow `json:"results"`
|
||
}
|
||
if err := decode(raw, &wrapped, "v2/broker-summary/{symbol}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return append(wrapped.Data, wrapped.Results...), nil
|
||
}
|
||
|
||
// BrokerSummaryTop returns top buyers/sellers for one ticker (2 credits).
|
||
func (c *Client) BrokerSummaryTop(ctx context.Context, symbol, start, end string, n int, origin, cohort string) (*BrokerSummaryTop, error) {
|
||
start, end = dateRange(start, end, ClampBrokerWindow)
|
||
q := url.Values{"start": {start}, "end": {end}}
|
||
if n > 0 {
|
||
q.Set("n_brokers", fmt.Sprint(n))
|
||
}
|
||
if origin != "" {
|
||
q.Set("origin", origin)
|
||
}
|
||
if cohort != "" {
|
||
q.Set("cohort", cohort)
|
||
}
|
||
raw, err := c.Get(ctx, "broker-summary/"+strings.ToUpper(symbol)+"/top/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var resp BrokerSummaryTop
|
||
if err := decode(raw, &resp, "v2/broker-summary/{symbol}/top/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return &resp, nil
|
||
}
|
||
|
||
// ForeignFlow returns the daily net foreign inflow series (window ≤90d).
|
||
func (c *Client) ForeignFlow(ctx context.Context, symbol, start, end string) (*ForeignFlow, error) {
|
||
start, end = dateRange(start, end, ClampSeriesWindow)
|
||
q := url.Values{"start": {start}, "end": {end}}
|
||
raw, err := c.Get(ctx, "foreign-flow/"+strings.ToUpper(symbol)+"/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var resp ForeignFlow
|
||
if err := decode(raw, &resp, "v2/foreign-flow/{symbol}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return &resp, nil
|
||
}
|
||
|
||
// --- Company core ---
|
||
|
||
// CompanyReport fetches explicit sections only (1 credit/section).
|
||
func (c *Client) CompanyReport(ctx context.Context, symbol string, sections []string) (map[string]any, error) {
|
||
if len(sections) == 0 {
|
||
sections = []string{"overview", "valuation"}
|
||
}
|
||
q := url.Values{"sections": {strings.Join(sections, ",")}}
|
||
raw, err := c.Get(ctx, "company/report/"+strings.ToUpper(symbol)+"/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var out map[string]any
|
||
if err := decode(raw, &out, "v2/company/report/{symbol}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// Quarterly fetches up to n financial quarters (1 credit/quarter, n≤8).
|
||
func (c *Client) Quarterly(ctx context.Context, symbol string, n int) ([]QuarterRow, error) {
|
||
if n <= 0 {
|
||
n = 4
|
||
}
|
||
if n > 8 {
|
||
n = 8
|
||
}
|
||
q := url.Values{"n_quarters": {fmt.Sprint(n)}}
|
||
raw, err := c.Get(ctx, "financials/quarterly/"+strings.ToUpper(symbol)+"/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var rows []QuarterRow
|
||
if err := decode(raw, &rows, "v2/financials/quarterly/{symbol}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return rows, nil
|
||
}
|
||
|
||
// CorporateActions fetches splits/rights/warrants/AGM/dividends for one ticker.
|
||
func (c *Client) CorporateActions(ctx context.Context, symbol string) (*CorporateActions, error) {
|
||
raw, err := c.Get(ctx, "company/corporate-actions/"+strings.ToUpper(symbol)+"/", url.Values{})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var resp struct {
|
||
Symbol string `json:"symbol"`
|
||
CorporateActions struct {
|
||
Dividend []DividendEvent `json:"dividend"`
|
||
UpcomingDividend []DividendEvent `json:"upcoming_dividend"`
|
||
AGM []DividendEvent `json:"agm"`
|
||
StockSplit []DividendEvent `json:"stock_split"`
|
||
} `json:"corporate_actions"`
|
||
}
|
||
if err := decode(raw, &resp, "v2/company/corporate-actions/{symbol}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return &CorporateActions{
|
||
Symbol: resp.Symbol,
|
||
Dividend: resp.CorporateActions.Dividend,
|
||
UpcomingDividend: resp.CorporateActions.UpcomingDividend,
|
||
AGM: resp.CorporateActions.AGM,
|
||
StockSplit: resp.CorporateActions.StockSplit,
|
||
}, nil
|
||
}
|
||
|
||
// --- Universe / market ---
|
||
|
||
// ClosePage fetches one page of the full-universe close sweep.
|
||
func (c *Client) ClosePage(ctx context.Context, date string, limit, offset int) ([]CloseRow, int, error) {
|
||
q := url.Values{}
|
||
if date != "" {
|
||
q.Set("date", date)
|
||
}
|
||
if limit <= 0 || limit > 30 {
|
||
limit = 30
|
||
}
|
||
q.Set("limit", fmt.Sprint(limit))
|
||
q.Set("offset", fmt.Sprint(offset))
|
||
raw, err := c.Get(ctx, "close/", q)
|
||
if err != nil {
|
||
return nil, 0, err
|
||
}
|
||
var resp struct {
|
||
Results []CloseRow `json:"results"`
|
||
Pagination struct {
|
||
Total int `json:"total"`
|
||
Count int `json:"count"`
|
||
} `json:"pagination"`
|
||
}
|
||
if err := decode(raw, &resp, "v2/close/"); err != nil {
|
||
return nil, 0, err
|
||
}
|
||
return resp.Results, resp.Pagination.Total, nil
|
||
}
|
||
|
||
// QuarterlyDatesSince polls newly-reported companies incrementally.
|
||
func (c *Client) QuarterlyDatesSince(ctx context.Context, since string, limit, offset int) ([]QuarterlyDateRow, error) {
|
||
q := url.Values{}
|
||
if since != "" {
|
||
q.Set("since", since)
|
||
}
|
||
if limit <= 0 || limit > 30 {
|
||
limit = 30
|
||
}
|
||
q.Set("limit", fmt.Sprint(limit))
|
||
q.Set("offset", fmt.Sprint(offset))
|
||
raw, err := c.Get(ctx, "companies/quarterly-financial-dates/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var resp struct {
|
||
Results []QuarterlyDateRow `json:"results"`
|
||
Data []QuarterlyDateRow `json:"data"`
|
||
}
|
||
if err := decode(raw, &resp, "v2/companies/quarterly-financial-dates/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return append(resp.Results, resp.Data...), nil
|
||
}
|
||
|
||
// Daily returns the price+volume+MCap series for one ticker (window ≤90d).
|
||
func (c *Client) Daily(ctx context.Context, symbol, start, end string) ([]DailyBar, error) {
|
||
start, end = dateRange(start, end, ClampSeriesWindow)
|
||
q := url.Values{"start": {start}, "end": {end}}
|
||
raw, err := c.Get(ctx, "daily/"+strings.ToUpper(symbol)+"/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var rows []DailyBar
|
||
if err := decode(raw, &rows, "v2/daily/{symbol}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return rows, nil
|
||
}
|
||
|
||
// IdxTotal returns the IHSG total-MCap trend (macro context).
|
||
func (c *Client) IdxTotal(ctx context.Context, start, end string) ([]IdxTotalPoint, error) {
|
||
start, end = dateRange(start, end, ClampSeriesWindow)
|
||
q := url.Values{"start": {start}, "end": {end}}
|
||
raw, err := c.Get(ctx, "idx-total/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var rows []IdxTotalPoint
|
||
if err := decode(raw, &rows, "v2/idx-total/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return rows, nil
|
||
}
|
||
|
||
// TopChanges requests minimal class×period combos only (1 credit per combo).
|
||
func (c *Client) TopChanges(ctx context.Context, classifications, periods []string, subSector string, n int) (gainers, losers map[string][]MoverRow, err error) {
|
||
if len(classifications) == 0 {
|
||
classifications = []string{"top_gainers"}
|
||
}
|
||
if len(periods) == 0 {
|
||
periods = []string{"1d"}
|
||
}
|
||
q := url.Values{
|
||
"classifications": {strings.Join(classifications, ",")},
|
||
"periods": {strings.Join(periods, ",")},
|
||
}
|
||
if subSector != "" {
|
||
q.Set("sub_sector", subSector)
|
||
}
|
||
if n > 0 {
|
||
q.Set("n_stock", fmt.Sprint(n))
|
||
}
|
||
raw, err := c.Get(ctx, "companies/top-changes/", q)
|
||
if err != nil {
|
||
return nil, nil, err
|
||
}
|
||
var resp struct {
|
||
TopGainers map[string][]MoverRow `json:"top_gainers"`
|
||
TopLosers map[string][]MoverRow `json:"top_losers"`
|
||
}
|
||
if err := decode(raw, &resp, "v2/companies/top-changes/"); err != nil {
|
||
return nil, nil, err
|
||
}
|
||
return resp.TopGainers, resp.TopLosers, nil
|
||
}
|
||
|
||
// MostTraded returns relative volume leaders.
|
||
func (c *Client) MostTraded(ctx context.Context, start, end, subSector string, n int) ([]map[string]any, error) {
|
||
start, end = dateRange(start, end, ClampSeriesWindow)
|
||
q := url.Values{"start": {start}, "end": {end}}
|
||
if subSector != "" {
|
||
q.Set("sub_sector", subSector)
|
||
}
|
||
if n > 0 {
|
||
q.Set("n_stock", fmt.Sprint(n))
|
||
}
|
||
raw, err := c.Get(ctx, "most-traded/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var resp struct {
|
||
Results []map[string]any `json:"results"`
|
||
Data []map[string]any `json:"data"`
|
||
}
|
||
if err := decode(raw, &resp, "v2/most-traded/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return append(resp.Results, resp.Data...), nil
|
||
}
|
||
|
||
// --- Events (incremental) ---
|
||
|
||
// News fetches articles incrementally (extension idx/mining exclusive).
|
||
// Windows clamp to 90d like other series endpoints.
|
||
func (c *Client) News(ctx context.Context, symbols, start, end, keyword string, limit int) ([]NewsArticle, error) {
|
||
if start != "" || end != "" {
|
||
start, end = dateRange(start, end, ClampSeriesWindow)
|
||
}
|
||
q := url.Values{"extension": {"idx"}}
|
||
if symbols != "" {
|
||
q.Set("symbols", symbols)
|
||
}
|
||
if start != "" {
|
||
q.Set("start", start)
|
||
}
|
||
if end != "" {
|
||
q.Set("end", end)
|
||
}
|
||
if keyword != "" {
|
||
q.Set("keyword", keyword)
|
||
}
|
||
if limit > 0 {
|
||
q.Set("limit", fmt.Sprint(limit))
|
||
}
|
||
raw, err := c.Get(ctx, "news/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var resp struct {
|
||
Results []NewsArticle `json:"results"`
|
||
}
|
||
if err := decode(raw, &resp, "v2/news/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return resp.Results, nil
|
||
}
|
||
|
||
// Filings fetches insider/institution transactions incrementally.
|
||
// Windows clamp to 90d like other series endpoints.
|
||
func (c *Client) Filings(ctx context.Context, symbol, txnType, holderType, start, end string) ([]Filing, error) {
|
||
if start != "" || end != "" {
|
||
start, end = dateRange(start, end, ClampSeriesWindow)
|
||
}
|
||
q := url.Values{}
|
||
if symbol != "" {
|
||
q.Set("symbol", strings.ToUpper(symbol))
|
||
}
|
||
if txnType != "" {
|
||
q.Set("transaction_type", txnType)
|
||
}
|
||
if holderType != "" {
|
||
q.Set("holder_type", holderType)
|
||
}
|
||
if start != "" {
|
||
q.Set("start", start)
|
||
}
|
||
if end != "" {
|
||
q.Set("end", end)
|
||
}
|
||
raw, err := c.Get(ctx, "filings/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var resp struct {
|
||
Results []Filing `json:"results"`
|
||
Data []Filing `json:"data"`
|
||
}
|
||
if err := decode(raw, &resp, "v2/filings/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return append(resp.Results, resp.Data...), nil
|
||
}
|
||
|
||
// Suspensions fetches new IDX suspension notices.
|
||
// Windows clamp to 90d like other series endpoints.
|
||
func (c *Client) Suspensions(ctx context.Context, symbol, start, end string) ([]Suspension, error) {
|
||
if start != "" || end != "" {
|
||
start, end = dateRange(start, end, ClampSeriesWindow)
|
||
}
|
||
q := url.Values{}
|
||
if symbol != "" {
|
||
q.Set("symbol", strings.ToUpper(symbol))
|
||
}
|
||
if start != "" {
|
||
q.Set("start", start)
|
||
}
|
||
if end != "" {
|
||
q.Set("end", end)
|
||
}
|
||
raw, err := c.Get(ctx, "suspensions/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var resp struct {
|
||
Results []Suspension `json:"results"`
|
||
Data []Suspension `json:"data"`
|
||
}
|
||
if err := decode(raw, &resp, "v2/suspensions/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return append(resp.Results, resp.Data...), nil
|
||
}
|
||
|
||
// --- Screener / taxonomy ---
|
||
|
||
// Screen runs the companies/ structured screener (q overrides all).
|
||
func (c *Client) Screen(ctx context.Context, where, q string, limit, offset int) ([]ScreenerRow, error) {
|
||
qq := url.Values{}
|
||
if q != "" {
|
||
qq.Set("q", q)
|
||
} else if where != "" {
|
||
qq.Set("where", where)
|
||
}
|
||
if limit <= 0 || limit > 200 {
|
||
limit = 50
|
||
}
|
||
qq.Set("limit", fmt.Sprint(limit))
|
||
qq.Set("offset", fmt.Sprint(offset))
|
||
raw, err := c.Get(ctx, "companies/", qq)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var resp struct {
|
||
Results []ScreenerRow `json:"results"`
|
||
Data []ScreenerRow `json:"data"`
|
||
}
|
||
if err := decode(raw, &resp, "v2/companies/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return append(resp.Results, resp.Data...), nil
|
||
}
|
||
|
||
// FreeFloat fetches one exclusive filter per request (API rejects mixed).
|
||
func (c *Client) FreeFloat(ctx context.Context, sector, subSector, industry, subIndustry string) ([]FreeFloatRow, error) {
|
||
q := url.Values{}
|
||
set := 0
|
||
for k, v := range map[string]string{"sector": sector, "sub_sector": subSector, "industry": industry, "sub_industry": subIndustry} {
|
||
if v != "" {
|
||
q.Set(k, v)
|
||
set++
|
||
}
|
||
}
|
||
if set != 1 {
|
||
return nil, fmt.Errorf("sectors: free-float needs exactly one filter (got %d)", set)
|
||
}
|
||
raw, err := c.Get(ctx, "free-float/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var rows []FreeFloatRow
|
||
if err := decode(raw, &rows, "v2/free-float/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return rows, nil
|
||
}
|
||
|
||
// Taxonomy fetches one slug list (subsectors/industries/subindustries/tags).
|
||
func (c *Client) Taxonomy(ctx context.Context, kind string) ([]map[string]any, error) {
|
||
raw, err := c.Get(ctx, kind+"/", url.Values{})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var rows []map[string]any
|
||
if err := decode(raw, &rows, "v2/"+kind+"/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return rows, nil
|
||
}
|
||
|
||
// SubsectorReport fetches explicit sections only (1 credit/section).
|
||
func (c *Client) SubsectorReport(ctx context.Context, subSector string, sections []string) (map[string]any, error) {
|
||
if len(sections) == 0 {
|
||
sections = []string{"statistics"}
|
||
}
|
||
q := url.Values{"sections": {strings.Join(sections, ",")}}
|
||
raw, err := c.Get(ctx, "subsector/report/"+subSector+"/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var out map[string]any
|
||
if err := decode(raw, &out, "v2/subsector/report/{sub_sector}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// --- Company depth: segments, quarterly dates, IPO, holders, index ---
|
||
|
||
// Segments fetches the revenue breakdown (Sankey-ready) for one ticker.
|
||
func (c *Client) Segments(ctx context.Context, symbol string, year int) (*Segments, error) {
|
||
q := url.Values{}
|
||
if year >= 1900 && year <= 2026 {
|
||
q.Set("financial_year", fmt.Sprint(year))
|
||
}
|
||
raw, err := c.Get(ctx, "company/get-segments/"+strings.ToUpper(symbol)+"/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var out Segments
|
||
if err := decode(raw, &out, "v2/company/get-segments/{symbol}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return &out, nil
|
||
}
|
||
|
||
// QuarterlyDates returns valid [report_date, quarter] pairs for one ticker
|
||
// (use to resolve report_date before Quarterly calls).
|
||
func (c *Client) QuarterlyDates(ctx context.Context, symbol string) ([]QuarterlyDate, error) {
|
||
raw, err := c.Get(ctx, "company/get_quarterly_financial_dates/"+strings.ToUpper(symbol)+"/", url.Values{})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
// Shape: {"YYYY-MM-DD": [["report_date","quarter"], ...], ...} flattened.
|
||
var grouped map[string][][]string
|
||
if err := decode(raw, &grouped, "v2/company/get_quarterly_financial_dates/{symbol}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
var out []QuarterlyDate
|
||
for _, pairs := range grouped {
|
||
for _, p := range pairs {
|
||
if len(p) >= 2 {
|
||
out = append(out, QuarterlyDate{ReportDate: p[0], Quarter: p[1]})
|
||
}
|
||
}
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// ListingPerformance fetches IPO context (post-May-2005 listings only).
|
||
func (c *Client) ListingPerformance(ctx context.Context, symbol string) (*ListingPerformance, error) {
|
||
raw, err := c.Get(ctx, "listing-performance/"+strings.ToUpper(symbol)+"/", url.Values{})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var out ListingPerformance
|
||
if err := decode(raw, &out, "v2/listing-performance/{symbol}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return &out, nil
|
||
}
|
||
|
||
// IndexDaily returns the benchmark series for beta/correlation (window ≤90d).
|
||
func (c *Client) IndexDaily(ctx context.Context, indexCode, start, end string) ([]IndexBar, error) {
|
||
start, end = dateRange(start, end, ClampSeriesWindow)
|
||
q := url.Values{"start": {start}, "end": {end}}
|
||
raw, err := c.Get(ctx, "index-daily/"+strings.ToLower(indexCode)+"/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var rows []IndexBar
|
||
if err := decode(raw, &rows, "v2/index-daily/{index_code}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return rows, nil
|
||
}
|
||
|
||
// Shareholders fetches the local-vs-foreign holder mix (data from 2021).
|
||
func (c *Client) Shareholders(ctx context.Context, symbol string, year int) (*ShareholdersComposition, error) {
|
||
q := url.Values{}
|
||
if year >= 2021 {
|
||
q.Set("year", fmt.Sprint(year))
|
||
}
|
||
raw, err := c.Get(ctx, "company/shareholders-composition/"+strings.ToUpper(symbol)+"/", q)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var out ShareholdersComposition
|
||
if err := decode(raw, &out, "v2/company/shareholders-composition/{symbol}/"); err != nil {
|
||
return nil, err
|
||
}
|
||
return &out, nil
|
||
}
|
||
|
||
// SegmentAvailability checks which symbols have segment data (cache weekly).
|
||
func (c *Client) SegmentAvailability(ctx context.Context) (map[string][]int, error) {
|
||
raw, err := c.Get(ctx, "companies/list_companies_with_segments/", url.Values{})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
var out map[string]struct {
|
||
Years []int `json:"financial_year"`
|
||
}
|
||
if err := decode(raw, &out, "v2/companies/list_companies_with_segments/"); err != nil {
|
||
return nil, err
|
||
}
|
||
res := make(map[string][]int, len(out))
|
||
for k, v := range out {
|
||
res[k] = v.Years
|
||
}
|
||
return res, nil
|
||
}
|