mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-08-07 01:33:52 +00:00
652 lines
16 KiB
Go
652 lines
16 KiB
Go
package msn
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/enetx/g"
|
|
"github.com/enetx/surf"
|
|
)
|
|
|
|
// MSNClientConfig holds configuration for the MSN client
|
|
type MSNClientConfig struct {
|
|
Proxy string // Proxy URL (http://, https://, socks5://)
|
|
RateLimiter *RateLimiter
|
|
}
|
|
|
|
// MSNClient is the base client for MSN Finance APIs
|
|
type MSNClient struct {
|
|
client *surf.Client
|
|
proxy string
|
|
rateLimiter *RateLimiter
|
|
}
|
|
|
|
// NewMSNClient creates a new MSN API client with Chrome impersonation
|
|
func NewMSNClient() *MSNClient {
|
|
return NewMSNClientWithConfig(MSNClientConfig{})
|
|
}
|
|
|
|
// NewMSNClientWithConfig creates a new MSN API client with custom configuration
|
|
func NewMSNClientWithConfig(config MSNClientConfig) *MSNClient {
|
|
builder := surf.NewClient().
|
|
Builder().
|
|
Impersonate().
|
|
Chrome()
|
|
|
|
// Add proxy if configured
|
|
if config.Proxy != "" {
|
|
builder = builder.Proxy(g.String(config.Proxy))
|
|
}
|
|
|
|
client := builder.Build().Unwrap()
|
|
|
|
return &MSNClient{
|
|
client: client,
|
|
proxy: config.Proxy,
|
|
rateLimiter: config.RateLimiter,
|
|
}
|
|
}
|
|
|
|
// waitForRateLimit waits for rate limiter if configured
|
|
func (c *MSNClient) waitForRateLimit() {
|
|
if c.rateLimiter != nil {
|
|
c.rateLimiter.Wait()
|
|
}
|
|
}
|
|
|
|
// Close closes idle connections
|
|
func (c *MSNClient) Close() {
|
|
c.client.CloseIdleConnections()
|
|
}
|
|
|
|
// commonHeaders returns common headers for MSN API requests
|
|
func (c *MSNClient) commonHeaders() map[string]string {
|
|
return map[string]string{
|
|
"Accept": "application/json",
|
|
"Accept-Language": "en-US,en;q=0.9,id;q=0.8",
|
|
"Origin": "https://www.msn.com",
|
|
"Referer": "https://www.msn.com/",
|
|
}
|
|
}
|
|
|
|
// GetQuotes fetches real-time quotes for given stock IDs
|
|
func (c *MSNClient) GetQuotes(ids []string) ([]QuoteData, error) {
|
|
if len(ids) == 0 {
|
|
return nil, fmt.Errorf("no stock IDs provided")
|
|
}
|
|
|
|
c.waitForRateLimit()
|
|
|
|
apiURL := fmt.Sprintf("%sFinance/Quotes?apikey=%s&ids=%s&wrapodata=false",
|
|
MSNAssetsBaseURL,
|
|
MSNAPIKey,
|
|
strings.Join(ids, ","),
|
|
)
|
|
|
|
req := c.client.Get(g.String(apiURL))
|
|
for k, v := range c.commonHeaders() {
|
|
req = req.SetHeaders(k, v)
|
|
}
|
|
|
|
resp := req.Do()
|
|
if resp.IsErr() {
|
|
return nil, fmt.Errorf("quotes request failed: %w", resp.Err())
|
|
}
|
|
|
|
r := resp.Ok()
|
|
if r.StatusCode != 200 {
|
|
return nil, fmt.Errorf("quotes API returned status %d", r.StatusCode)
|
|
}
|
|
|
|
body := r.Body.String().Ok().Std()
|
|
|
|
var quotes []QuoteData
|
|
if err := json.Unmarshal([]byte(body), "es); err != nil {
|
|
return nil, fmt.Errorf("failed to parse quotes response: %w", err)
|
|
}
|
|
|
|
return quotes, nil
|
|
}
|
|
|
|
// GetQuoteSummary fetches detailed quote summary with multiple intents
|
|
func (c *MSNClient) GetQuoteSummary(id string, intents []string) (map[string]json.RawMessage, error) {
|
|
if id == "" {
|
|
return nil, fmt.Errorf("no stock ID provided")
|
|
}
|
|
|
|
c.waitForRateLimit()
|
|
|
|
intentStr := strings.Join(intents, ",")
|
|
apiURL := fmt.Sprintf("%sFinance/QuoteSummary?apikey=%s&ids=%s&intents=%s&wrapodata=false",
|
|
MSNAssetsBaseURL,
|
|
MSNAPIKey,
|
|
id,
|
|
intentStr,
|
|
)
|
|
|
|
req := c.client.Get(g.String(apiURL))
|
|
for k, v := range c.commonHeaders() {
|
|
req = req.SetHeaders(k, v)
|
|
}
|
|
|
|
resp := req.Do()
|
|
if resp.IsErr() {
|
|
return nil, fmt.Errorf("quote summary request failed: %w", resp.Err())
|
|
}
|
|
|
|
r := resp.Ok()
|
|
if r.StatusCode != 200 {
|
|
return nil, fmt.Errorf("quote summary API returned status %d", r.StatusCode)
|
|
}
|
|
|
|
body := r.Body.String().Ok().Std()
|
|
|
|
var result []map[string]json.RawMessage
|
|
if err := json.Unmarshal([]byte(body), &result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse quote summary response: %w", err)
|
|
}
|
|
|
|
if len(result) == 0 {
|
|
return nil, fmt.Errorf("empty quote summary response")
|
|
}
|
|
|
|
return result[0], nil
|
|
}
|
|
|
|
// GetCharts fetches historical chart data
|
|
func (c *MSNClient) GetCharts(ids []string, chartType string) ([]ChartResponse, error) {
|
|
if len(ids) == 0 {
|
|
return nil, fmt.Errorf("no stock IDs provided")
|
|
}
|
|
|
|
c.waitForRateLimit()
|
|
|
|
apiURL := fmt.Sprintf("%sFinance/Charts?apikey=%s&cm=id-id&ids=%s&type=%s&wrapodata=false",
|
|
MSNAssetsBaseURL,
|
|
MSNAPIKey,
|
|
strings.Join(ids, ","),
|
|
chartType,
|
|
)
|
|
|
|
req := c.client.Get(g.String(apiURL))
|
|
for k, v := range c.commonHeaders() {
|
|
req = req.SetHeaders(k, v)
|
|
}
|
|
|
|
resp := req.Do()
|
|
if resp.IsErr() {
|
|
return nil, fmt.Errorf("charts request failed: %w", resp.Err())
|
|
}
|
|
|
|
r := resp.Ok()
|
|
if r.StatusCode != 200 {
|
|
return nil, fmt.Errorf("charts API returned status %d", r.StatusCode)
|
|
}
|
|
|
|
body := r.Body.String().Ok().Std()
|
|
|
|
var charts []ChartResponse
|
|
if err := json.Unmarshal([]byte(body), &charts); err != nil {
|
|
return nil, fmt.Errorf("failed to parse charts response: %w", err)
|
|
}
|
|
|
|
return charts, nil
|
|
}
|
|
|
|
// GetEquities fetches company information
|
|
func (c *MSNClient) GetEquities(ids []string) ([]EquityData, error) {
|
|
if len(ids) == 0 {
|
|
return nil, fmt.Errorf("no stock IDs provided")
|
|
}
|
|
|
|
c.waitForRateLimit()
|
|
|
|
apiURL := fmt.Sprintf("%sFinance/Equities?apikey=%s&ids=%s&wrapodata=false",
|
|
MSNAssetsBaseURL,
|
|
MSNAPIKey,
|
|
strings.Join(ids, ","),
|
|
)
|
|
|
|
req := c.client.Get(g.String(apiURL))
|
|
for k, v := range c.commonHeaders() {
|
|
req = req.SetHeaders(k, v)
|
|
}
|
|
|
|
resp := req.Do()
|
|
if resp.IsErr() {
|
|
return nil, fmt.Errorf("equities request failed: %w", resp.Err())
|
|
}
|
|
|
|
r := resp.Ok()
|
|
if r.StatusCode != 200 {
|
|
return nil, fmt.Errorf("equities API returned status %d", r.StatusCode)
|
|
}
|
|
|
|
body := r.Body.String().Ok().Std()
|
|
|
|
var equities []EquityData
|
|
if err := json.Unmarshal([]byte(body), &equities); err != nil {
|
|
return nil, fmt.Errorf("failed to parse equities response: %w", err)
|
|
}
|
|
|
|
return equities, nil
|
|
}
|
|
|
|
// GetFinancialStatements fetches financial statements
|
|
func (c *MSNClient) GetFinancialStatements(id string) (FinancialStatementsResponse, error) {
|
|
if id == "" {
|
|
return nil, fmt.Errorf("no stock ID provided")
|
|
}
|
|
|
|
c.waitForRateLimit()
|
|
|
|
// URL encode the filter parameter
|
|
filter := fmt.Sprintf("_p eq '%s'", id)
|
|
apiURL := fmt.Sprintf("%sFinance/Equities/financialstatements?apikey=%s&$filter=%s&wrapodata=false",
|
|
MSNAssetsBaseURL,
|
|
MSNAPIKey,
|
|
url.QueryEscape(filter),
|
|
)
|
|
|
|
req := c.client.Get(g.String(apiURL))
|
|
for k, v := range c.commonHeaders() {
|
|
req = req.SetHeaders(k, v)
|
|
}
|
|
|
|
resp := req.Do()
|
|
if resp.IsErr() {
|
|
return nil, fmt.Errorf("financial statements request failed: %w", resp.Err())
|
|
}
|
|
|
|
r := resp.Ok()
|
|
if r.StatusCode != 200 {
|
|
return nil, fmt.Errorf("financial statements API returned status %d", r.StatusCode)
|
|
}
|
|
|
|
body := r.Body.String().Ok().Std()
|
|
|
|
// Response is a direct array of FinancialStatement
|
|
var result FinancialStatementsResponse
|
|
if err := json.Unmarshal([]byte(body), &result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse financial statements response: %w", err)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetEarnings fetches earnings events
|
|
func (c *MSNClient) GetEarnings(ids []string) ([]EarningsEvent, error) {
|
|
if len(ids) == 0 {
|
|
return nil, fmt.Errorf("no stock IDs provided")
|
|
}
|
|
|
|
c.waitForRateLimit()
|
|
|
|
apiURL := fmt.Sprintf("%sFinance/Events/Earnings?apikey=%s&ids=%s&wrapodata=false",
|
|
MSNAssetsBaseURL,
|
|
MSNAPIKey,
|
|
strings.Join(ids, ","),
|
|
)
|
|
|
|
req := c.client.Get(g.String(apiURL))
|
|
for k, v := range c.commonHeaders() {
|
|
req = req.SetHeaders(k, v)
|
|
}
|
|
|
|
resp := req.Do()
|
|
if resp.IsErr() {
|
|
return nil, fmt.Errorf("earnings request failed: %w", resp.Err())
|
|
}
|
|
|
|
r := resp.Ok()
|
|
if r.StatusCode != 200 {
|
|
return nil, fmt.Errorf("earnings API returned status %d", r.StatusCode)
|
|
}
|
|
|
|
body := r.Body.String().Ok().Std()
|
|
|
|
// Parse the actual API response format
|
|
var apiResp EarningsAPIResponse
|
|
if err := json.Unmarshal([]byte(body), &apiResp); err != nil {
|
|
return nil, fmt.Errorf("failed to parse earnings response: %w", err)
|
|
}
|
|
|
|
// Convert quarterly earnings to EarningsEvent array
|
|
var earnings []EarningsEvent
|
|
for periodKey, data := range apiResp.History.Quarterly {
|
|
// Parse fiscal year and quarter from CiqFiscalPeriodType (e.g., "Q42025")
|
|
fiscalYear := 0
|
|
fiscalQuarter := 0
|
|
if len(data.CiqFiscalPeriodType) >= 6 {
|
|
// Format: Q{quarter}{year} e.g., Q42025
|
|
fmt.Sscanf(data.CiqFiscalPeriodType, "Q%d%d", &fiscalQuarter, &fiscalYear)
|
|
}
|
|
if fiscalYear == 0 && len(periodKey) >= 6 {
|
|
// Fallback: parse from period key (e.g., "202512")
|
|
fmt.Sscanf(periodKey[:4], "%d", &fiscalYear)
|
|
month := 0
|
|
fmt.Sscanf(periodKey[4:6], "%d", &month)
|
|
fiscalQuarter = (month-1)/3 + 1
|
|
}
|
|
|
|
// Parse event date
|
|
eventDate := ""
|
|
if data.EarningReleaseDate != "" {
|
|
// Extract date portion from ISO timestamp
|
|
if len(data.EarningReleaseDate) >= 10 {
|
|
eventDate = data.EarningReleaseDate[:10]
|
|
}
|
|
}
|
|
|
|
earnings = append(earnings, EarningsEvent{
|
|
ID: fmt.Sprintf("%s_%s", apiResp.InstrumentID, periodKey),
|
|
EventDate: eventDate,
|
|
FiscalYear: fiscalYear,
|
|
FiscalQuarter: fiscalQuarter,
|
|
EPSEstimate: data.EpsForecast,
|
|
EPSActual: data.EpsActual,
|
|
EPSSurprise: data.EpsSurprise,
|
|
EPSSurprisePct: data.EpsSurprisePercent,
|
|
RevenueEstimate: data.RevenueForecast,
|
|
RevenueActual: data.RevenueActual,
|
|
RevenueSurprise: data.RevenueSurprise,
|
|
})
|
|
}
|
|
|
|
return earnings, nil
|
|
}
|
|
|
|
// GetSentiment fetches market sentiment
|
|
func (c *MSNClient) GetSentiment(ids []string) ([]SentimentData, error) {
|
|
if len(ids) == 0 {
|
|
return nil, fmt.Errorf("no stock IDs provided")
|
|
}
|
|
|
|
c.waitForRateLimit()
|
|
|
|
apiURL := fmt.Sprintf("%sFinance/SentimentBrowser?apikey=%s&cm=id-id&it=web&scn=ANON&ids=%s&wrapodata=false&flightId=INeedDau",
|
|
MSNAssetsBaseURL,
|
|
MSNAPIKey,
|
|
strings.Join(ids, ","),
|
|
)
|
|
|
|
req := c.client.Get(g.String(apiURL))
|
|
for k, v := range c.commonHeaders() {
|
|
req = req.SetHeaders(k, v)
|
|
}
|
|
|
|
resp := req.Do()
|
|
if resp.IsErr() {
|
|
return nil, fmt.Errorf("sentiment request failed: %w", resp.Err())
|
|
}
|
|
|
|
r := resp.Ok()
|
|
if r.StatusCode != 200 {
|
|
return nil, fmt.Errorf("sentiment API returned status %d", r.StatusCode)
|
|
}
|
|
|
|
body := r.Body.String().Ok().Std()
|
|
|
|
var sentiment []SentimentData
|
|
if err := json.Unmarshal([]byte(body), &sentiment); err != nil {
|
|
return nil, fmt.Errorf("failed to parse sentiment response: %w", err)
|
|
}
|
|
|
|
return sentiment, nil
|
|
}
|
|
|
|
// GetKeyRatios fetches key financial ratios from api.msn.com
|
|
func (c *MSNClient) GetKeyRatios(ids []string) ([]KeyRatios, error) {
|
|
if len(ids) == 0 {
|
|
return nil, fmt.Errorf("no stock IDs provided")
|
|
}
|
|
|
|
c.waitForRateLimit()
|
|
|
|
apiURL := fmt.Sprintf("%skeyratios?apikey=%s&ids=%s&wrapodata=false",
|
|
MSNAPIBaseURL,
|
|
MSNAPIKey,
|
|
strings.Join(ids, ","),
|
|
)
|
|
|
|
req := c.client.Get(g.String(apiURL))
|
|
for k, v := range c.commonHeaders() {
|
|
req = req.SetHeaders(k, v)
|
|
}
|
|
|
|
resp := req.Do()
|
|
if resp.IsErr() {
|
|
return nil, fmt.Errorf("key ratios request failed: %w", resp.Err())
|
|
}
|
|
|
|
r := resp.Ok()
|
|
if r.StatusCode != 200 {
|
|
return nil, fmt.Errorf("key ratios API returned status %d", r.StatusCode)
|
|
}
|
|
|
|
body := r.Body.String().Ok().Std()
|
|
|
|
var ratios []KeyRatios
|
|
if err := json.Unmarshal([]byte(body), &ratios); err != nil {
|
|
return nil, fmt.Errorf("failed to parse key ratios response: %w", err)
|
|
}
|
|
|
|
return ratios, nil
|
|
}
|
|
|
|
// GetInsights fetches AI-generated insights from api.msn.com
|
|
func (c *MSNClient) GetInsights(id string) (*InsightData, error) {
|
|
if id == "" {
|
|
return nil, fmt.Errorf("no stock ID provided")
|
|
}
|
|
|
|
c.waitForRateLimit()
|
|
|
|
apiURL := fmt.Sprintf("%sinsights?apikey=%s&ids=%s&wrapodata=false",
|
|
MSNAPIBaseURL,
|
|
MSNAPIKey,
|
|
id,
|
|
)
|
|
|
|
req := c.client.Get(g.String(apiURL))
|
|
for k, v := range c.commonHeaders() {
|
|
req = req.SetHeaders(k, v)
|
|
}
|
|
|
|
resp := req.Do()
|
|
if resp.IsErr() {
|
|
return nil, fmt.Errorf("insights request failed: %w", resp.Err())
|
|
}
|
|
|
|
r := resp.Ok()
|
|
if r.StatusCode != 200 {
|
|
return nil, fmt.Errorf("insights API returned status %d", r.StatusCode)
|
|
}
|
|
|
|
body := r.Body.String().Ok().Std()
|
|
|
|
var insights []InsightData
|
|
if err := json.Unmarshal([]byte(body), &insights); err != nil {
|
|
return nil, fmt.Errorf("failed to parse insights response: %w", err)
|
|
}
|
|
|
|
if len(insights) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return &insights[0], nil
|
|
}
|
|
|
|
// GetNewsFeed fetches stock-related news
|
|
func (c *MSNClient) GetNewsFeed(id string) ([]NewsItem, error) {
|
|
if id == "" {
|
|
return nil, fmt.Errorf("no stock ID provided")
|
|
}
|
|
|
|
c.waitForRateLimit()
|
|
|
|
// Use the stock-specific entity feed format from MSN website
|
|
apiURL := fmt.Sprintf("%sMSN/Feed/me?$top=30&apikey=%s&cm=id-id&contentType=article,video,slideshow&it=web&query=ef_stock_%s&queryType=entityfeed&responseSchema=cardview&scn=ANON&wrapodata=false",
|
|
MSNAssetsBaseURL,
|
|
MSNAPIKey,
|
|
id,
|
|
)
|
|
|
|
req := c.client.Get(g.String(apiURL))
|
|
for k, v := range c.commonHeaders() {
|
|
req = req.SetHeaders(k, v)
|
|
}
|
|
|
|
resp := req.Do()
|
|
if resp.IsErr() {
|
|
return nil, fmt.Errorf("news feed request failed: %w", resp.Err())
|
|
}
|
|
|
|
r := resp.Ok()
|
|
if r.StatusCode != 200 {
|
|
return nil, fmt.Errorf("news feed API returned status %d", r.StatusCode)
|
|
}
|
|
|
|
body := r.Body.String().Ok().Std()
|
|
|
|
var newsFeed NewsFeedResponse
|
|
if err := json.Unmarshal([]byte(body), &newsFeed); err != nil {
|
|
return nil, fmt.Errorf("failed to parse news feed response: %w", err)
|
|
}
|
|
|
|
// Use SubCards if available (cardview response), otherwise use Value
|
|
if len(newsFeed.SubCards) > 0 {
|
|
return newsFeed.SubCards, nil
|
|
}
|
|
return newsFeed.Value, nil
|
|
}
|
|
|
|
// GetAllCharts fetches all chart timeframes for a stock
|
|
func (c *MSNClient) GetAllCharts(id string) (map[string][]ChartPoint, error) {
|
|
chartTypes := []string{"1D1M", "1M", "3M", "1Y", "3Y"}
|
|
result := make(map[string][]ChartPoint)
|
|
|
|
for _, chartType := range chartTypes {
|
|
charts, err := c.GetCharts([]string{id}, chartType)
|
|
if err != nil {
|
|
continue // Skip failed chart types
|
|
}
|
|
if len(charts) > 0 {
|
|
// Map chart type to friendlier names
|
|
typeName := chartType
|
|
switch chartType {
|
|
case "1D1M":
|
|
typeName = "1D"
|
|
}
|
|
result[typeName] = charts[0].Points
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// FetchStockData fetches all data for a single stock
|
|
func (c *MSNClient) FetchStockData(id string) (*StockData, error) {
|
|
stock := &StockData{
|
|
ID: id,
|
|
FetchedAt: time.Now().UTC().Format(time.RFC3339),
|
|
FetchStatus: make(map[string]string),
|
|
Charts: make(map[string][]ChartPoint),
|
|
}
|
|
|
|
// Fetch quote
|
|
quotes, err := c.GetQuotes([]string{id})
|
|
if err != nil {
|
|
stock.FetchStatus["quote"] = fmt.Sprintf("failed: %v", err)
|
|
} else if len(quotes) > 0 {
|
|
stock.Quote = "es[0]
|
|
stock.Ticker = quotes[0].Symbol
|
|
stock.Name = quotes[0].ShortName
|
|
stock.Exchange = quotes[0].ExchangeID
|
|
stock.FetchStatus["quote"] = "ok"
|
|
}
|
|
|
|
// Fetch company info
|
|
equities, err := c.GetEquities([]string{id})
|
|
if err != nil {
|
|
stock.FetchStatus["company"] = fmt.Sprintf("failed: %v", err)
|
|
} else if len(equities) > 0 {
|
|
stock.Company = &equities[0]
|
|
stock.Sector = equities[0].Sector
|
|
stock.Industry = equities[0].Industry
|
|
if stock.Name == "" {
|
|
stock.Name = equities[0].ShortName
|
|
}
|
|
stock.FetchStatus["company"] = "ok"
|
|
}
|
|
|
|
// Fetch charts
|
|
charts, err := c.GetAllCharts(id)
|
|
if err != nil {
|
|
stock.FetchStatus["charts"] = fmt.Sprintf("failed: %v", err)
|
|
} else {
|
|
stock.Charts = charts
|
|
stock.FetchStatus["charts"] = "ok"
|
|
}
|
|
|
|
// Fetch key ratios
|
|
ratios, err := c.GetKeyRatios([]string{id})
|
|
if err != nil {
|
|
stock.FetchStatus["key_ratios"] = fmt.Sprintf("failed: %v", err)
|
|
} else if len(ratios) > 0 {
|
|
stock.KeyRatios = &ratios[0]
|
|
stock.FetchStatus["key_ratios"] = "ok"
|
|
}
|
|
|
|
// Fetch earnings
|
|
earnings, err := c.GetEarnings([]string{id})
|
|
if err != nil {
|
|
stock.FetchStatus["earnings"] = fmt.Sprintf("failed: %v", err)
|
|
} else {
|
|
stock.Earnings = earnings
|
|
stock.FetchStatus["earnings"] = "ok"
|
|
}
|
|
|
|
// Fetch sentiment
|
|
sentiment, err := c.GetSentiment([]string{id})
|
|
if err != nil {
|
|
stock.FetchStatus["sentiment"] = fmt.Sprintf("failed: %v", err)
|
|
} else if len(sentiment) > 0 {
|
|
stock.Sentiment = &sentiment[0]
|
|
stock.FetchStatus["sentiment"] = "ok"
|
|
}
|
|
|
|
// Fetch insights
|
|
insights, err := c.GetInsights(id)
|
|
if err != nil {
|
|
stock.FetchStatus["insights"] = fmt.Sprintf("failed: %v", err)
|
|
} else if insights != nil {
|
|
stock.Insights = insights
|
|
stock.FetchStatus["insights"] = "ok"
|
|
}
|
|
|
|
// Fetch financial statements
|
|
financials, err := c.GetFinancialStatements(id)
|
|
if err != nil {
|
|
stock.FetchStatus["financials"] = fmt.Sprintf("failed: %v", err)
|
|
} else if len(financials) > 0 {
|
|
stock.Financials = &FinancialData{
|
|
Statements: financials,
|
|
}
|
|
stock.FetchStatus["financials"] = "ok"
|
|
}
|
|
|
|
// Fetch news
|
|
news, err := c.GetNewsFeed(id)
|
|
if err != nil {
|
|
stock.FetchStatus["news"] = fmt.Sprintf("failed: %v", err)
|
|
} else {
|
|
stock.News = news
|
|
stock.FetchStatus["news"] = "ok"
|
|
}
|
|
|
|
return stock, nil
|
|
}
|