// Package fetch defines the pluggable data-source interface and its // implementations (yfinance-equivalent prices, Finnhub, RSS news). package fetch import ( "context" "fmt" "time" "github.com/ytshih/stonks/internal/config" ) // Quote is a single point-in-time price observation for one symbol. type Quote struct { Symbol string // ticker as passed to the fetcher, e.g. "AAPL", "BTC-USD", "GC=F" Label string // human-friendly name, e.g. "Apple Inc." Price float64 ChangePercent float64 // percent change over the source's default window (usually 1 day) Currency string Source string // which fetcher produced this, e.g. "yfinance", "finnhub" } // NewsItem is a single headline/article. type NewsItem struct { Title string Source string // publication or fetcher name URL string Published time.Time Summary string } // Result is what one fetcher call returns. The orchestrator merges Results // from every configured fetcher before handing them to the analyzer. type Result struct { Quotes []Quote News []NewsItem } // Merge appends other's quotes and news onto r. func (r *Result) Merge(other Result) { r.Quotes = append(r.Quotes, other.Quotes...) r.News = append(r.News, other.News...) } // Fetcher fetches market data and/or news for the given targets. // Implementations should return a partial Result plus an error when only // some data could be retrieved, rather than nothing at all — the caller // decides whether a partial result is still worth reporting. type Fetcher interface { Name() string Fetch(ctx context.Context, targets config.Targets) (Result, error) } // Constructor builds a Fetcher from its PluginConfig (name + params). type Constructor func(cfg config.PluginConfig) (Fetcher, error) var registry = map[string]Constructor{} // Register makes a fetcher constructor available under name. Called from // each fetcher implementation's init(). func Register(name string, ctor Constructor) { if _, exists := registry[name]; exists { panic(fmt.Sprintf("fetch: constructor already registered for %q", name)) } registry[name] = ctor } // Build looks up the constructor for cfg.Name and invokes it. func Build(cfg config.PluginConfig) (Fetcher, error) { ctor, ok := registry[cfg.Name] if !ok { return nil, fmt.Errorf("fetch: no fetcher registered for %q", cfg.Name) } return ctor(cfg) }