|
|
@ -1,7 +1,6 @@ |
|
|
|
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
|
|
|
|
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
|
|
|
|
// Use of this source code is governed by a MIT-style
|
|
|
|
// Use of this source code is governed by a MIT-style license that can
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
// be found in the LICENSE file.
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
package mop |
|
|
|
package mop |
|
|
|
|
|
|
|
|
|
|
@ -15,50 +14,32 @@ import ( |
|
|
|
) |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
// See http://www.gummy-stuff.org/Yahoo-stocks.htm
|
|
|
|
// See http://www.gummy-stuff.org/Yahoo-stocks.htm
|
|
|
|
|
|
|
|
//
|
|
|
|
// Also http://query.yahooapis.com/v1/public/yql
|
|
|
|
// Also http://query.yahooapis.com/v1/public/yql
|
|
|
|
// ?q=select%20*%20from%20yahoo.finance.quotes%20where%20symbol%20in(%22ALU%22,%22AAPL%22)
|
|
|
|
// ?q=select%20*%20from%20yahoo.finance.quotes%20where%20symbol%20in(%22ALU%22,%22AAPL%22)
|
|
|
|
// &env=http%3A%2F%2Fstockstables.org%2Falltables.env
|
|
|
|
// &env=http%3A%2F%2Fstockstables.org%2Falltables.env&format=json'
|
|
|
|
// &format=json'
|
|
|
|
|
|
|
|
//
|
|
|
|
//
|
|
|
|
// Current, Change, Open, High, Low, 52-W High, 52-W Low, Volume, AvgVolume, P/E, Yield, Market Cap.
|
|
|
|
const quotesURL = `http://download.finance.yahoo.com/d/quotes.csv?s=%s&f=,l1c6k2oghjkva2r2rdyj3j1` |
|
|
|
// l1: last trade
|
|
|
|
|
|
|
|
// c6: change rt
|
|
|
|
|
|
|
|
// k2: change % rt
|
|
|
|
|
|
|
|
// o: open
|
|
|
|
|
|
|
|
// g: day's low
|
|
|
|
|
|
|
|
// h: day's high
|
|
|
|
|
|
|
|
// j: 52w low
|
|
|
|
|
|
|
|
// k: 52w high
|
|
|
|
|
|
|
|
// v: volume
|
|
|
|
|
|
|
|
// a2: avg volume
|
|
|
|
|
|
|
|
// r2: p/e rt
|
|
|
|
|
|
|
|
// r: p/e
|
|
|
|
|
|
|
|
// d: dividend/share
|
|
|
|
|
|
|
|
// y: wield
|
|
|
|
|
|
|
|
// j3: market cap rt
|
|
|
|
|
|
|
|
// j1: market cap
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const quotes_url = `http://download.finance.yahoo.com/d/quotes.csv?s=%s&f=,l1c6k2oghjkva2r2rdyj3j1` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type Stock struct { |
|
|
|
type Stock struct { |
|
|
|
Ticker string |
|
|
|
Ticker string // Stock ticker.
|
|
|
|
LastTrade string |
|
|
|
LastTrade string // l1: last trade.
|
|
|
|
Change string |
|
|
|
Change string // c6: change real time.
|
|
|
|
ChangePct string |
|
|
|
ChangePct string // k2: percent change real time.
|
|
|
|
Open string |
|
|
|
Open string // o: market open price.
|
|
|
|
Low string |
|
|
|
Low string // g: day's low.
|
|
|
|
High string |
|
|
|
High string // h: day's high.
|
|
|
|
Low52 string |
|
|
|
Low52 string // j: 52-weeks low.
|
|
|
|
High52 string |
|
|
|
High52 string // k: 52-weeks high.
|
|
|
|
Volume string |
|
|
|
Volume string // v: volume.
|
|
|
|
AvgVolume string |
|
|
|
AvgVolume string // a2: average volume.
|
|
|
|
PeRatio string |
|
|
|
PeRatio string // r2: P/E ration real time.
|
|
|
|
PeRatioX string |
|
|
|
PeRatioX string // r: P/E ration (fallback when real time is N/A).
|
|
|
|
Dividend string |
|
|
|
Dividend string // d: dividend.
|
|
|
|
Yield string |
|
|
|
Yield string // y: dividend yield.
|
|
|
|
MarketCap string |
|
|
|
MarketCap string // j3: market cap real time.
|
|
|
|
MarketCapX string |
|
|
|
MarketCapX string // j1: market cap (fallback when real time is N/A).
|
|
|
|
Advancing bool |
|
|
|
Advancing bool // True when change is >= $0.
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
type Quotes struct { |
|
|
|
type Quotes struct { |
|
|
@ -69,27 +50,26 @@ type Quotes struct { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
func (self *Quotes) Initialize(market *Market, profile *Profile) *Quotes { |
|
|
|
func (quotes *Quotes) Initialize(market *Market, profile *Profile) *Quotes { |
|
|
|
self.market = market |
|
|
|
quotes.market = market |
|
|
|
self.profile = profile |
|
|
|
quotes.profile = profile |
|
|
|
self.errors = `` |
|
|
|
quotes.errors = `` |
|
|
|
|
|
|
|
|
|
|
|
return self |
|
|
|
return quotes |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Fetch the latest stock quotes and parse raw fetched data into array of
|
|
|
|
// Fetch the latest stock quotes and parse raw fetched data into array of
|
|
|
|
// []Stock structs.
|
|
|
|
// []Stock structs.
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
func (quotes *Quotes) Fetch() (this *Quotes) { |
|
|
|
func (self *Quotes) Fetch() (this *Quotes) { |
|
|
|
this = quotes // <-- This ensures we return correct quotes after recover() from panic() attack.
|
|
|
|
this = self // <-- This ensures we return correct self after recover() from panic() attack.
|
|
|
|
if quotes.isReady() { |
|
|
|
if self.is_ready() { |
|
|
|
|
|
|
|
defer func() { |
|
|
|
defer func() { |
|
|
|
if err := recover(); err != nil { |
|
|
|
if err := recover(); err != nil { |
|
|
|
self.errors = fmt.Sprintf("\n\n\n\nError fetching stock quotes...\n%s", err) |
|
|
|
quotes.errors = fmt.Sprintf("\n\n\n\nError fetching stock quotes...\n%s", err) |
|
|
|
} |
|
|
|
} |
|
|
|
}() |
|
|
|
}() |
|
|
|
|
|
|
|
|
|
|
|
url := fmt.Sprintf(quotes_url, strings.Join(self.profile.Tickers, `+`)) |
|
|
|
url := fmt.Sprintf(quotesURL, strings.Join(quotes.profile.Tickers, `+`)) |
|
|
|
response, err := http.Get(url) |
|
|
|
response, err := http.Get(url) |
|
|
|
if err != nil { |
|
|
|
if err != nil { |
|
|
|
panic(err) |
|
|
|
panic(err) |
|
|
@ -101,85 +81,80 @@ func (self *Quotes) Fetch() (this *Quotes) { |
|
|
|
panic(err) |
|
|
|
panic(err) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
self.parse(sanitize(body)) |
|
|
|
quotes.parse(sanitize(body)) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return self |
|
|
|
return quotes |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
func (self *Quotes) Ok() (bool, string) { |
|
|
|
func (quotes *Quotes) Ok() (bool, string) { |
|
|
|
return self.errors == ``, self.errors |
|
|
|
return quotes.errors == ``, quotes.errors |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
func (self *Quotes) AddTickers(tickers []string) (added int, err error) { |
|
|
|
func (quotes *Quotes) AddTickers(tickers []string) (added int, err error) { |
|
|
|
if added, err = self.profile.AddTickers(tickers); err == nil && added > 0 { |
|
|
|
if added, err = quotes.profile.AddTickers(tickers); err == nil && added > 0 { |
|
|
|
self.stocks = nil // Force fetch.
|
|
|
|
quotes.stocks = nil // Force fetch.
|
|
|
|
} |
|
|
|
} |
|
|
|
return |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
func (self *Quotes) RemoveTickers(tickers []string) (removed int, err error) { |
|
|
|
func (quotes *Quotes) RemoveTickers(tickers []string) (removed int, err error) { |
|
|
|
if removed, err = self.profile.RemoveTickers(tickers); err == nil && removed > 0 { |
|
|
|
if removed, err = quotes.profile.RemoveTickers(tickers); err == nil && removed > 0 { |
|
|
|
self.stocks = nil // Force fetch.
|
|
|
|
quotes.stocks = nil // Force fetch.
|
|
|
|
} |
|
|
|
} |
|
|
|
return |
|
|
|
return |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// "Private" methods.
|
|
|
|
// isReady returns true if we haven't fetched the quotes yet *or* the stock
|
|
|
|
|
|
|
|
// market is still open and we might want to grab the latest quotes. In both
|
|
|
|
// Return true if we haven't fetched the quotes yet *or* the stock market is
|
|
|
|
// cases we make sure the list of requested tickers is not empty.
|
|
|
|
// still open and we might want to grab the latest quotes. In both cases we
|
|
|
|
func (quotes *Quotes) isReady() bool { |
|
|
|
// make sure the list of requested tickers is not empty.
|
|
|
|
return (quotes.stocks == nil || !quotes.market.IsClosed) && len(quotes.profile.Tickers) > 0 |
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
|
|
|
func (self *Quotes) is_ready() bool { |
|
|
|
|
|
|
|
return (self.stocks == nil || !self.market.IsClosed) && len(self.profile.Tickers) > 0 |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
func (self *Quotes) parse(body []byte) *Quotes { |
|
|
|
func (quotes *Quotes) parse(body []byte) *Quotes { |
|
|
|
lines := bytes.Split(body, []byte{'\n'}) |
|
|
|
lines := bytes.Split(body, []byte{'\n'}) |
|
|
|
self.stocks = make([]Stock, len(lines)) |
|
|
|
quotes.stocks = make([]Stock, len(lines)) |
|
|
|
//
|
|
|
|
//
|
|
|
|
// Get the total number of fields in the Stock struct. Skip the last
|
|
|
|
// Get the total number of fields in the Stock struct. Skip the last
|
|
|
|
// Advanicing field which is not fetched.
|
|
|
|
// Advanicing field which is not fetched.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
number_of_fields := reflect.ValueOf(self.stocks[0]).NumField() - 1 |
|
|
|
fieldsCount := reflect.ValueOf(quotes.stocks[0]).NumField() - 1 |
|
|
|
//
|
|
|
|
//
|
|
|
|
// Split each line into columns, then iterate over the Stock struct
|
|
|
|
// Split each line into columns, then iterate over the Stock struct
|
|
|
|
// fields to assign column values.
|
|
|
|
// fields to assign column values.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
for i, line := range lines { |
|
|
|
for i, line := range lines { |
|
|
|
columns := bytes.Split(bytes.TrimSpace(line), []byte{','}) |
|
|
|
columns := bytes.Split(bytes.TrimSpace(line), []byte{','}) |
|
|
|
for j := 0; j < number_of_fields; j++ { |
|
|
|
for j := 0; j < fieldsCount; j++ { |
|
|
|
// ex. self.stocks[i].Ticker = string(columns[0])
|
|
|
|
// ex. quotes.stocks[i].Ticker = string(columns[0])
|
|
|
|
reflect.ValueOf(&self.stocks[i]).Elem().Field(j).SetString(string(columns[j])) |
|
|
|
reflect.ValueOf("es.stocks[i]).Elem().Field(j).SetString(string(columns[j])) |
|
|
|
} |
|
|
|
} |
|
|
|
//
|
|
|
|
//
|
|
|
|
// Try realtime value and revert to the last known if the
|
|
|
|
// Try realtime value and revert to the last known if the
|
|
|
|
// realtime is not available.
|
|
|
|
// realtime is not available.
|
|
|
|
//
|
|
|
|
//
|
|
|
|
if self.stocks[i].PeRatio == `N/A` && self.stocks[i].PeRatioX != `N/A` { |
|
|
|
if quotes.stocks[i].PeRatio == `N/A` && quotes.stocks[i].PeRatioX != `N/A` { |
|
|
|
self.stocks[i].PeRatio = self.stocks[i].PeRatioX |
|
|
|
quotes.stocks[i].PeRatio = quotes.stocks[i].PeRatioX |
|
|
|
} |
|
|
|
} |
|
|
|
if self.stocks[i].MarketCap == `N/A` && self.stocks[i].MarketCapX != `N/A` { |
|
|
|
if quotes.stocks[i].MarketCap == `N/A` && quotes.stocks[i].MarketCapX != `N/A` { |
|
|
|
self.stocks[i].MarketCap = self.stocks[i].MarketCapX |
|
|
|
quotes.stocks[i].MarketCap = quotes.stocks[i].MarketCapX |
|
|
|
} |
|
|
|
} |
|
|
|
//
|
|
|
|
//
|
|
|
|
// Stock is advancing if the change is not negative (i.e. $0.00
|
|
|
|
// Stock is advancing if the change is not negative (i.e. $0.00
|
|
|
|
// is also "advancing").
|
|
|
|
// is also "advancing").
|
|
|
|
//
|
|
|
|
//
|
|
|
|
self.stocks[i].Advancing = (self.stocks[i].Change[0:1] != `-`) |
|
|
|
quotes.stocks[i].Advancing = (quotes.stocks[i].Change[0:1] != `-`) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return self |
|
|
|
return quotes |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Utility methods.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
func sanitize(body []byte) []byte { |
|
|
|
func sanitize(body []byte) []byte { |
|
|
|
return bytes.Replace(bytes.TrimSpace(body), []byte{'"'}, []byte{}, -1) |
|
|
|
return bytes.Replace(bytes.TrimSpace(body), []byte{'"'}, []byte{}, -1) |
|
|
|