feat: done vibing

This commit is contained in:
ytshih committed 2026-09-05 09:55:24 +08:00
1 parent 3de31a57c3
commit 4352fd1ad1
27 files changed
+1922 -13

No files matched your search

+75
View File
@@ -0,0 +1,75 @@
// 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)
}