|
|
|
@ -1,6 +1,6 @@ |
|
|
|
|
// Copyright (c) 2013 by Michael Dvorkin. All Rights Reserved.
|
|
|
|
|
// Use of this source code is governed by a MIT-style
|
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
// Use of this source code is governed by a MIT-style license that can
|
|
|
|
|
// be found in the LICENSE file.
|
|
|
|
|
|
|
|
|
|
package mop |
|
|
|
|
|
|
|
|
@ -14,26 +14,29 @@ import ( |
|
|
|
|
`time` |
|
|
|
|
) |
|
|
|
|
|
|
|
|
|
const TotalColumns = 15 |
|
|
|
|
|
|
|
|
|
// Column describes formatting rules for individual column within the list
|
|
|
|
|
// of stock quotes.
|
|
|
|
|
type Column struct { |
|
|
|
|
width int |
|
|
|
|
name string |
|
|
|
|
title string |
|
|
|
|
formatter func(string)string |
|
|
|
|
width int // Column width.
|
|
|
|
|
name string // The name of the field in the Stock struct.
|
|
|
|
|
title string // Column title to display in the header.
|
|
|
|
|
formatter func(string)string // Optional function to format the contents of the column.
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Layout is used to format and display all the collected data, i.e. market
|
|
|
|
|
// updates and the list of stock quotes.
|
|
|
|
|
type Layout struct { |
|
|
|
|
columns []Column |
|
|
|
|
sorter *Sorter |
|
|
|
|
regex *regexp.Regexp |
|
|
|
|
market_template *template.Template |
|
|
|
|
quotes_template *template.Template |
|
|
|
|
columns []Column // List of stock quotes columns.
|
|
|
|
|
sorter *Sorter // Pointer to sorting receiver.
|
|
|
|
|
regex *regexp.Regexp // Pointer to regular expression to align decimal points.
|
|
|
|
|
marketTemplate *template.Template // Pointer to template to format market data.
|
|
|
|
|
quotesTemplate *template.Template // Pointer to template to format the list of stock quotes.
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
func (self *Layout) Initialize() *Layout { |
|
|
|
|
self.columns = []Column{ |
|
|
|
|
// Initialize assigns the default values that stay unchanged for the life of
|
|
|
|
|
// allocated Layout struct.
|
|
|
|
|
func (layout *Layout) Initialize() *Layout { |
|
|
|
|
layout.columns = []Column{ |
|
|
|
|
{ -7, `Ticker`, `Ticker`, nil }, |
|
|
|
|
{ 10, `LastTrade`, `Last`, currency }, |
|
|
|
|
{ 10, `Change`, `Change`, currency }, |
|
|
|
@ -46,59 +49,65 @@ func (self *Layout) Initialize() *Layout { |
|
|
|
|
{ 11, `Volume`, `Volume`, nil }, |
|
|
|
|
{ 11, `AvgVolume`, `AvgVolume`, nil }, |
|
|
|
|
{ 9, `PeRatio`, `P/E`, blank }, |
|
|
|
|
{ 9, `Dividend`, `Dividend`, blank_currency }, |
|
|
|
|
{ 9, `Dividend`, `Dividend`, zero }, |
|
|
|
|
{ 9, `Yield`, `Yield`, percent }, |
|
|
|
|
{ 11, `MarketCap`, `MktCap`, currency }, |
|
|
|
|
} |
|
|
|
|
self.regex = regexp.MustCompile(`(\.\d+)[BMK]?$`) |
|
|
|
|
self.market_template = build_market_template() |
|
|
|
|
self.quotes_template = build_quotes_template() |
|
|
|
|
layout.regex = regexp.MustCompile(`(\.\d+)[BMK]?$`) |
|
|
|
|
layout.marketTemplate = buildMarketTemplate() |
|
|
|
|
layout.quotesTemplate = buildQuotesTemplate() |
|
|
|
|
|
|
|
|
|
return self |
|
|
|
|
return layout |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
func (self *Layout) Market(market *Market) string { |
|
|
|
|
if ok, err := market.Ok(); !ok { |
|
|
|
|
return err |
|
|
|
|
// Market merges given market data structure with the market template and
|
|
|
|
|
// returns formatted string that includes highlighting markup.
|
|
|
|
|
func (layout *Layout) Market(market *Market) string { |
|
|
|
|
if ok, err := market.Ok(); !ok { // If there was an error fetching market data...
|
|
|
|
|
return err // then simply return the error string.
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
highlight(market.Dow, market.Sp500, market.Nasdaq) |
|
|
|
|
buffer := new(bytes.Buffer) |
|
|
|
|
self.market_template.Execute(buffer, market) |
|
|
|
|
layout.marketTemplate.Execute(buffer, market) |
|
|
|
|
|
|
|
|
|
return buffer.String() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
func (self *Layout) Quotes(quotes *Quotes) string { |
|
|
|
|
if ok, err := quotes.Ok(); !ok { |
|
|
|
|
return err |
|
|
|
|
// Quotes uses quotes template to format timestamp, stock quotes header,
|
|
|
|
|
// and the list of given stock quotes. It returns formatted string with
|
|
|
|
|
// all the necessary markup.
|
|
|
|
|
func (layout *Layout) Quotes(quotes *Quotes) string { |
|
|
|
|
if ok, err := quotes.Ok(); !ok { // If there was an error fetching stock quotes...
|
|
|
|
|
return err // then simply return the error string.
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
vars := struct { |
|
|
|
|
Now string |
|
|
|
|
Header string |
|
|
|
|
Stocks []Stock |
|
|
|
|
Now string // Current timestamp.
|
|
|
|
|
Header string // Formatted header line.
|
|
|
|
|
Stocks []Stock // List of formatted stock quotes.
|
|
|
|
|
}{ |
|
|
|
|
time.Now().Format(`3:04:05pm PST`), |
|
|
|
|
self.Header(quotes.profile), |
|
|
|
|
self.prettify(quotes), |
|
|
|
|
layout.Header(quotes.profile), |
|
|
|
|
layout.prettify(quotes), |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
buffer := new(bytes.Buffer) |
|
|
|
|
self.quotes_template.Execute(buffer, vars) |
|
|
|
|
layout.quotesTemplate.Execute(buffer, vars) |
|
|
|
|
|
|
|
|
|
return buffer.String() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
func (self *Layout) Header(profile *Profile) string { |
|
|
|
|
str, selected_column := ``, profile.selected_column |
|
|
|
|
// Header iterates over column titles and formats the header line. The
|
|
|
|
|
// formatting includes placing an arrow next to the sorted column title.
|
|
|
|
|
// When the column editor is active it knows how to highlight currently
|
|
|
|
|
// selected column title.
|
|
|
|
|
func (layout *Layout) Header(profile *Profile) string { |
|
|
|
|
str, selectedColumn := ``, profile.selectedColumn |
|
|
|
|
|
|
|
|
|
for i,col := range self.columns { |
|
|
|
|
arrow := arrow_for(i, profile) |
|
|
|
|
if i != selected_column { |
|
|
|
|
for i,col := range layout.columns { |
|
|
|
|
arrow := arrowFor(i, profile) |
|
|
|
|
if i != selectedColumn { |
|
|
|
|
str += fmt.Sprintf(`%*s`, col.width, arrow + col.title) |
|
|
|
|
} else { |
|
|
|
|
str += fmt.Sprintf(`<r>%*s</r>`, col.width, arrow + col.title) |
|
|
|
@ -108,8 +117,14 @@ func (self *Layout) Header(profile *Profile) string { |
|
|
|
|
return `<u>` + str + `</u>` |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// TotalColumns is the utility method for the column editor that returns
|
|
|
|
|
// total number of columns.
|
|
|
|
|
func (layout *Layout) TotalColumns() int { |
|
|
|
|
return len(layout.columns) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
func (self *Layout) prettify(quotes *Quotes) []Stock { |
|
|
|
|
func (layout *Layout) prettify(quotes *Quotes) []Stock { |
|
|
|
|
pretty := make([]Stock, len(quotes.stocks)) |
|
|
|
|
//
|
|
|
|
|
// Iterate over the list of stocks and properly format all its columns.
|
|
|
|
@ -122,23 +137,23 @@ func (self *Layout) prettify(quotes *Quotes) []Stock { |
|
|
|
|
// - If the column has the formatter method then call it.
|
|
|
|
|
// - Set the column value padding it to the given width.
|
|
|
|
|
//
|
|
|
|
|
for _,column := range self.columns { |
|
|
|
|
for _,column := range layout.columns { |
|
|
|
|
// ex. value = stock.Change
|
|
|
|
|
value := reflect.ValueOf(&stock).Elem().FieldByName(column.name).String() |
|
|
|
|
if column.formatter != nil { |
|
|
|
|
// ex. value = currency(value)
|
|
|
|
|
value = column.formatter(value) |
|
|
|
|
} |
|
|
|
|
// ex. pretty[i].Change = self.pad(value, 10)
|
|
|
|
|
reflect.ValueOf(&pretty[i]).Elem().FieldByName(column.name).SetString(self.pad(value, column.width)) |
|
|
|
|
// ex. pretty[i].Change = layout.pad(value, 10)
|
|
|
|
|
reflect.ValueOf(&pretty[i]).Elem().FieldByName(column.name).SetString(layout.pad(value, column.width)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
profile := quotes.profile |
|
|
|
|
if self.sorter == nil { // Initialize sorter on first invocation.
|
|
|
|
|
self.sorter = new(Sorter).Initialize(profile) |
|
|
|
|
if layout.sorter == nil { // Initialize sorter on first invocation.
|
|
|
|
|
layout.sorter = new(Sorter).Initialize(profile) |
|
|
|
|
} |
|
|
|
|
self.sorter.SortByCurrentColumn(pretty) |
|
|
|
|
layout.sorter.SortByCurrentColumn(pretty) |
|
|
|
|
//
|
|
|
|
|
// Group stocks by advancing/declining unless sorted by Chanage or Change%
|
|
|
|
|
// in which case the grouping has been done already.
|
|
|
|
@ -151,8 +166,8 @@ func (self *Layout) prettify(quotes *Quotes) []Stock { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
func (self *Layout) pad(str string, width int) string { |
|
|
|
|
match := self.regex.FindStringSubmatch(str) |
|
|
|
|
func (layout *Layout) pad(str string, width int) string { |
|
|
|
|
match := layout.regex.FindStringSubmatch(str) |
|
|
|
|
if len(match) > 0 { |
|
|
|
|
switch len(match[1]) { |
|
|
|
|
case 2: |
|
|
|
@ -166,7 +181,7 @@ func (self *Layout) pad(str string, width int) string { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
func build_market_template() *template.Template { |
|
|
|
|
func buildMarketTemplate() *template.Template { |
|
|
|
|
markup := `{{.Dow.name}}: {{.Dow.change}} ({{.Dow.percent}}) at {{.Dow.latest}}, {{.Sp500.name}}: {{.Sp500.change}} ({{.Sp500.percent}}) at {{.Sp500.latest}}, {{.Nasdaq.name}}: {{.Nasdaq.change}} ({{.Nasdaq.percent}}) at {{.Nasdaq.latest}} |
|
|
|
|
{{.Advances.name}}: {{.Advances.nyse}} ({{.Advances.nysep}}) on NYSE and {{.Advances.nasdaq}} ({{.Advances.nasdaqp}}) on Nasdaq. {{.Declines.name}}: {{.Declines.nyse}} ({{.Declines.nysep}}) on NYSE and {{.Declines.nasdaq}} ({{.Declines.nasdaqp}}) on Nasdaq {{if .IsClosed}}<right>U.S. markets closed</right>{{end}} |
|
|
|
|
New highs: {{.Highs.nyse}} on NYSE and {{.Highs.nasdaq}} on Nasdaq. New lows: {{.Lows.nyse}} on NYSE and {{.Lows.nasdaq}} on Nasdaq.` |
|
|
|
@ -175,7 +190,7 @@ New highs: {{.Highs.nyse}} on NYSE and {{.Highs.nasdaq}} on Nasdaq. New lows: {{ |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
func build_quotes_template() *template.Template { |
|
|
|
|
func buildQuotesTemplate() *template.Template { |
|
|
|
|
markup := `<right><white>{{.Now}}</></right> |
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -218,7 +233,7 @@ func group(stocks []Stock) []Stock { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
func arrow_for(column int, profile *Profile) string { |
|
|
|
|
func arrowFor(column int, profile *Profile) string { |
|
|
|
|
if column == profile.SortColumn { |
|
|
|
|
if profile.Ascending { |
|
|
|
|
return string('\U00002191') |
|
|
|
@ -238,7 +253,7 @@ func blank(str string) string { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
//-----------------------------------------------------------------------------
|
|
|
|
|
func blank_currency(str string) string { |
|
|
|
|
func zero(str string) string { |
|
|
|
|
if str == `0.00` { |
|
|
|
|
return `-` |
|
|
|
|
} |
|
|
|
|