Compare commits
No commits in common. 'main' and 'master' have entirely different histories.
@ -1,23 +1,24 @@ |
||||
# ---> Go |
||||
# If you prefer the allow list template instead of the deny list, see community template: |
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore |
||||
# |
||||
# Binaries for programs and plugins |
||||
*.exe |
||||
*.exe~ |
||||
*.dll |
||||
*.so |
||||
*.dylib |
||||
# Mac OS |
||||
.DS_Store |
||||
|
||||
# Test binary, built with `go test -c` |
||||
*.test |
||||
# TextMate |
||||
*.tmproj |
||||
tmtags |
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE |
||||
*.out |
||||
# Vim |
||||
*.swp |
||||
|
||||
# Dependency directories (remove the comment below to include it) |
||||
# vendor/ |
||||
# Atom |
||||
cmd/mop/debug |
||||
|
||||
# Go workspace file |
||||
go.work |
||||
# Other |
||||
~* |
||||
*~ |
||||
*.*~ |
||||
|
||||
# Builds and logs. |
||||
bin/mop* |
||||
logs/* |
||||
/mop |
||||
|
||||
.idea |
||||
|
||||
@ -0,0 +1,9 @@ |
||||
FROM golang:alpine |
||||
|
||||
RUN apk update && apk add --no-cache git |
||||
RUN git clone https://github.com/mop-tracker/mop ./mop |
||||
RUN cd mop && \ |
||||
go build ./cmd/mop && \ |
||||
chmod a+x ./mop |
||||
WORKDIR /go/mop/ |
||||
CMD ["./mop"] |
||||
@ -0,0 +1,20 @@ |
||||
Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved. |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining |
||||
a copy of this software and associated documentation files (the |
||||
"Software"), to deal in the Software without restriction, including |
||||
without limitation the rights to use, copy, modify, merge, publish, |
||||
distribute, sublicense, and/or sell copies of the Software, and to |
||||
permit persons to whom the Software is furnished to do so, subject to |
||||
the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be |
||||
included in all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
||||
@ -0,0 +1,22 @@ |
||||
# Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
# Use of this source code is governed by a MIT-style license that can
|
||||
# be found in the LICENSE file.
|
||||
|
||||
VERSION = 1.0.0
|
||||
PACKAGE = ./cmd/mop
|
||||
|
||||
run: |
||||
go run ./cmd/mop/main.go
|
||||
|
||||
build: |
||||
go build -x -o ./bin/mop $(PACKAGE)
|
||||
|
||||
install: |
||||
go install -x $(PACKAGE)
|
||||
|
||||
buildall: |
||||
GOOS=darwin GOARCH=amd64 go build $(GOFLAGS) -o ./bin/mop-$(VERSION)-osx-64 $(PACKAGE)
|
||||
GOOS=freebsd GOARCH=amd64 go build $(GOFLAGS) -o ./bin/mop-$(VERSION)-freebsd-64 $(PACKAGE)
|
||||
GOOS=linux GOARCH=amd64 go build $(GOFLAGS) -o ./bin/mop-$(VERSION)-linux-64 $(PACKAGE)
|
||||
GOOS=windows GOARCH=amd64 go build $(GOFLAGS) -o ./bin/mop-$(VERSION)-windows-64.exe $(PACKAGE)
|
||||
GOOS=windows GOARCH=386 go build $(GOFLAGS) -o ./bin/mop-$(VERSION)-windows-32.exe $(PACKAGE)
|
||||
@ -1,2 +1,74 @@ |
||||
# mop |
||||
### mop: track stocks the hacker way |
||||
A command-line utility that displays continuous up-to-date information about select markets and individual stocks. |
||||
|
||||
 |
||||
|
||||
### Installing mop from source |
||||
|
||||
Ensure GO language is installed. Download from: https://go.dev/dl/ and the $GOPATH is set then: |
||||
|
||||
``` |
||||
git clone https://github.com/mop-tracker/mop |
||||
cd mop |
||||
go build ./cmd/mop |
||||
./mop |
||||
``` |
||||
|
||||
### Using mop |
||||
For demonstration purposes Mop comes preconfigured with a number of stock tickers. You can easily change the default list by using the following keyboard commands: |
||||
|
||||
+ Add stocks to the list. |
||||
- Remove stocks from the list. |
||||
o Change column sort order. |
||||
g Group stocks by advancing/declining issues. |
||||
f Set a filtering expression. |
||||
F Unset a filtering expression. |
||||
PgDn Scroll Down, down arrow key also works. |
||||
PgUp Scroll up, up arrow key also works. |
||||
? Display help screen. |
||||
esc Quit mop. |
||||
|
||||
When prompted please enter comma-delimited list of stock tickers. The list and other settings are stored in the profile file (default: ``.moprc`` in your ``$HOME`` directory) |
||||
|
||||
### Expression-based Filtering |
||||
Mop has an in realtime expression-based filtering engine that is very easy to use. |
||||
|
||||
At the main screen, press `f` and a prompt will appear. Write an expression that uses the stock properties. |
||||
|
||||
Example: |
||||
|
||||
```last <= 5``` |
||||
|
||||
This expression will make Mop show only the stocks whose `last` values are less than $5. |
||||
|
||||
The available properties are: `last`, `change`, `changePercent`, `open`, `low`, `high`, `low52`, `high52`, `volume`, `avgVolume`, `pe`, `peX`, `dividend`, `yield`, `mktCap`, `mktCapX` and `advancing`. |
||||
|
||||
The expression **must** return a boolean value, otherwise it will fail. |
||||
|
||||
For detailed information about the syntax, please refer to [Knetic/govaluate#what-operators-and-types-does-this-support](https://github.com/Knetic/govaluate#what-operators-and-types-does-this-support). |
||||
|
||||
To clear the filter, press `Shift+F`. |
||||
|
||||
You can specify the profile you want to use by passing ``-profile <filename>`` to the command-line. |
||||
|
||||
### Saving & Downloading to CSV |
||||
Please refer to the wiki by contributor @joce: [How to fetch market data from Yahoo! Finance in CSV form, from the command line](https://github.com/mop-tracker/mop/wiki/How-to-fetch-market-data-from-Yahoo!-Finance-in-CSV-form,-from-the-command-line). |
||||
Further information may be found in the following article: [Pulling Yahoo! Finance data to CSV](https://jocelyn.legau.lt/posts/pulling-yahoo-finance-tickers-to-csv/) |
||||
|
||||
### Contributing |
||||
* Pull requests accepted. |
||||
|
||||
### License |
||||
Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved. |
||||
"mike" + "@dvorkin" + ".net" || "twitter.com/mid" |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
||||
|
||||
|
||||
todo: |
||||
scode -sname interface |
||||
some issues to fix:e.g. cannot select correctly. |
||||
@ -0,0 +1,730 @@ |
||||
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package main |
||||
|
||||
import ( |
||||
"flag" |
||||
"fmt" |
||||
"os" |
||||
"os/user" |
||||
"path" |
||||
"strings" |
||||
"time" |
||||
"net/http" |
||||
"bufio" |
||||
"encoding/json" |
||||
"strconv" |
||||
"log" |
||||
//"io/ioutil"
|
||||
|
||||
"easyquotation" |
||||
"easyquotation/sina" |
||||
"easyquotation/stock" |
||||
"github.com/gocolly/colly" |
||||
|
||||
"github.com/eiannone/keyboard" |
||||
"github.com/mop-tracker/mop" |
||||
"github.com/nsf/termbox-go" |
||||
"github.com/olekukonko/tablewriter" |
||||
|
||||
mqtt "github.com/eclipse/paho.mqtt.golang" |
||||
) |
||||
|
||||
// File name in user's home directory where we store the settings.
|
||||
const defaultProfile = `.moprc` |
||||
|
||||
const help = `Mop v1.0.0 -- Copyright (c) 2013-2022 by Michael Dvorkin. All Rights Reserved. |
||||
NO WARRANTIES OF ANY KIND WHATSOEVER. SEE THE LICENSE FILE FOR DETAILS. |
||||
|
||||
<u>Command</u> <u>Description </u> |
||||
+ Add stocks to the list. |
||||
- Remove stocks from the list. |
||||
? Display this help screen. |
||||
f Set filtering expression. |
||||
F Unset filtering expression. |
||||
g Group stocks by advancing/declining issues. |
||||
o Change column sort order. |
||||
p Pause market data and stock updates. |
||||
Scroll Scroll up/down. |
||||
PgUp/PgDn; Up/Down arrow; j/k;J/K also all scroll up/down |
||||
q Quit mop. |
||||
esc Ditto. |
||||
|
||||
Enter comma-delimited list of stock tickers when prompted. |
||||
|
||||
<r> Press any key to continue </r> |
||||
` |
||||
|
||||
|
||||
func getuserinput(preset *Preset, sc mop.Stock) string{ |
||||
scanner := bufio.NewScanner(os.Stdin) |
||||
var precondition string |
||||
var instrumentname string |
||||
var buyorsell string |
||||
|
||||
if sc.Ticker == "" { |
||||
fmt.Print("Enter Command: ") |
||||
scanner.Scan() |
||||
cmdstr := scanner.Text() |
||||
|
||||
inputcmd := strings.Split(cmdstr, " ") |
||||
if len(inputcmd) == 2 { |
||||
if inputcmd[0] == "buy" { |
||||
preset.Direction = 23 |
||||
}else if inputcmd[0] == "sell" { |
||||
preset.Direction = 24 |
||||
}else{ |
||||
return "" |
||||
} |
||||
}else{ |
||||
return "" |
||||
} |
||||
|
||||
preset.Scode = inputcmd[1] |
||||
}else{ |
||||
preset.Direction = 23 |
||||
preset.Scode = sc.Dividend[2:] |
||||
f1, _ := strconv.ParseFloat(sc.LastTrade, 64) |
||||
f2, _ := strconv.ParseFloat(sc.Change, 64) |
||||
precondition = fmt.Sprintf("%s>%.2f", sc.LastTrade, f1-f2) |
||||
instrumentname = sc.Ticker[2:] |
||||
} |
||||
|
||||
if preset.Direction == 23 { |
||||
buyorsell = "buy" |
||||
} else { |
||||
buyorsell = "sell" |
||||
} |
||||
|
||||
if preset.Ifbelow == 0 && preset.Ifabove == 0 { |
||||
|
||||
}else if preset.Ifbelow == 0 { |
||||
precondition = fmt.Sprintf(">%f", preset.Ifabove) |
||||
}else{ |
||||
precondition = fmt.Sprintf("<%f", preset.Ifbelow) |
||||
} |
||||
|
||||
fmt.Printf("Enter Condition[%s %s][%s]: ", buyorsell, preset.Scode+instrumentname, precondition) |
||||
|
||||
scanner.Scan() |
||||
condition := scanner.Text() |
||||
if strings.Contains(condition, ">") { |
||||
preset.Ifbelow = 0 |
||||
preset.Ifabove, _ = strconv.ParseFloat(strings.Replace(condition, ">", "", -1), 64) |
||||
}else if strings.Contains(condition, "<") { |
||||
preset.Ifabove = 0 |
||||
preset.Ifbelow, _ = strconv.ParseFloat(strings.Replace(condition, "<", "", -1), 64) |
||||
}else{ |
||||
return "" |
||||
} |
||||
|
||||
if preset.Ifbelow == 0 && preset.Ifabove == 0 { |
||||
precondition = "" |
||||
}else if preset.Ifbelow == 0 { |
||||
precondition = fmt.Sprintf(">%.2f", preset.Ifabove) |
||||
}else{ |
||||
precondition = fmt.Sprintf("<%.2f", preset.Ifbelow) |
||||
} |
||||
fmt.Printf("Enter Vol[%s %s][%s]: ", buyorsell, preset.Scode+instrumentname, precondition) |
||||
scanner.Scan() |
||||
vol := scanner.Text() |
||||
preset.Vol, _ = strconv.ParseFloat(vol, 64) |
||||
if preset.Vol == 0 { |
||||
return "" |
||||
} |
||||
|
||||
//fmt.Print(preset)
|
||||
jsonData, err := json.Marshal(preset) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
} |
||||
extraField := `,"cmd": "preset"}` |
||||
jsonData = append(jsonData[:len(jsonData)-1], extraField...) |
||||
message := string(jsonData) |
||||
fmt.Println(message) |
||||
|
||||
return message |
||||
} |
||||
|
||||
func getwatchlist() mop.Watchlist{ |
||||
var watchlist mop.Watchlist |
||||
|
||||
response, err := http.Get("http://119.29.166.226/q/dayjson/ml.json")//("http://119.29.166.226/q/dayjson/ml.json")
|
||||
if err != nil { |
||||
// Handle error
|
||||
fmt.Println(err) |
||||
} |
||||
defer response.Body.Close() |
||||
|
||||
err = json.NewDecoder(response.Body).Decode(&watchlist) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
} |
||||
|
||||
return watchlist |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func mainLoop(screen *mop.Screen, profile *mop.Profile, mode string) { |
||||
var lineEditor *mop.LineEditor |
||||
var columnEditor *mop.ColumnEditor |
||||
|
||||
termbox.SetInputMode(termbox.InputMouse) |
||||
|
||||
// use buffered channel for keyboard event queue
|
||||
keyboardQueue := make(chan termbox.Event, 128) |
||||
|
||||
timestampQueue := time.NewTicker(1 * time.Second) |
||||
quotesQueue := time.NewTicker(2 * time.Second) |
||||
marketQueue := time.NewTicker(12 * time.Second) |
||||
showingHelp := false |
||||
paused := false |
||||
upDownJump := profile.UpDownJump |
||||
redrawQuotesFlag := false |
||||
redrawMarketFlag := false |
||||
account := "" |
||||
var canstock mop.Stock |
||||
|
||||
// 创建一个用于存储用户输入的缓冲区
|
||||
input := "" |
||||
easyquotation.Init() |
||||
c := colly.NewCollector() |
||||
SinaStock_spider := sina.NewSinaStock(c) |
||||
si := stock.G_STOCK_MANAGER.StockList |
||||
if mode == "review" {
|
||||
SinaStock_spider.Start("") |
||||
}else{ |
||||
SinaStock_spider.Start("stock_in.json") |
||||
} |
||||
|
||||
go func() { |
||||
for { |
||||
keyboardQueue <- termbox.PollEvent() |
||||
} |
||||
}() |
||||
|
||||
// create a new MQTT client
|
||||
opts := mqtt.NewClientOptions() |
||||
opts.AddBroker("tcp://119.29.166.226:1883") |
||||
currentTime := time.Now().UnixNano() / int64(time.Millisecond) |
||||
millisecondsString := strconv.FormatInt(currentTime, 10) |
||||
opts.SetClientID("client-mop-"+millisecondsString) |
||||
opts.SetUsername("sandy") |
||||
opts.SetPassword("Abb123456.") |
||||
|
||||
client := mqtt.NewClient(opts) |
||||
|
||||
// connect to the MQTT broker
|
||||
if token := client.Connect(); token.Wait() && token.Error() != nil { |
||||
panic(token.Error()) |
||||
} |
||||
|
||||
// create a channel for receiving MQTT messages
|
||||
messages := make(chan mqtt.Message) |
||||
|
||||
// subscribe to the MQTT topic of interest
|
||||
topic := "stock/response/#" |
||||
if mode == "standby" { |
||||
topic = "stock/standby/#" |
||||
} |
||||
if token := client.Subscribe(topic, 0, func(client mqtt.Client, message mqtt.Message) { |
||||
//fmt.Printf("Received message: %s from topic: %s\n", message.Payload(), message.Topic())
|
||||
// send the message to the messages channel
|
||||
messages <- message |
||||
}); token.Wait() && token.Error() != nil { |
||||
panic(token.Error()) |
||||
} |
||||
/* |
||||
response, err := http.Get("http://119.29.166.226/q/dayjson/ml.json")//("http://119.29.166.226/q/dayjson/ml.json")
|
||||
if err != nil { |
||||
// Handle error
|
||||
fmt.Println(err) |
||||
} |
||||
defer response.Body.Close() |
||||
var watchlist mop.Watchlist |
||||
err = json.NewDecoder(response.Body).Decode(&watchlist) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
}*/ |
||||
watchlist := getwatchlist() |
||||
market := mop.NewMarket(stock.G_STOCK_MANAGER.StockList, &watchlist) |
||||
|
||||
quotes := mop.NewQuotes(market, profile, stock.G_STOCK_MANAGER.StockList, &watchlist, client) |
||||
quotes.Setquotes() |
||||
codestoadd := []string{"sh000001", "sz399001", "sz399006"} |
||||
quotes.Addstockcodetofile(codestoadd) |
||||
screen.Draw(market) |
||||
screen.Draw(quotes) |
||||
|
||||
loop: |
||||
for { |
||||
select { |
||||
case event := <-keyboardQueue: |
||||
switch event.Type { |
||||
case termbox.EventKey: |
||||
if lineEditor == nil && columnEditor == nil && !showingHelp { |
||||
if event.Key == termbox.KeyEsc || event.Ch == 'q' || event.Ch == 'Q' { |
||||
// 取消订阅并断开连接
|
||||
if token := client.Unsubscribe(topic); token.Wait() && token.Error() != nil { |
||||
panic(token.Error()) |
||||
} |
||||
client.Disconnect(250) |
||||
quotes.SaveStocks() |
||||
break loop |
||||
} else if event.Ch == '+' || event.Ch == '-' { |
||||
lineEditor = mop.NewLineEditor(screen, quotes) |
||||
lineEditor.Prompt(event.Ch) |
||||
} else if event.Ch == 'f' { |
||||
lineEditor = mop.NewLineEditor(screen, quotes) |
||||
lineEditor.Prompt(event.Ch) |
||||
} else if event.Ch == 'F' { |
||||
profile.SetFilter("") |
||||
} else if event.Ch == 'T' { |
||||
watchlist := getwatchlist() |
||||
log.Println("watchlist update.", watchlist) |
||||
quotes.ResetforNewday(&watchlist) |
||||
} else if event.Ch == 'o' || event.Ch == 'O' { |
||||
columnEditor = mop.NewColumnEditor(screen, quotes) |
||||
} else if event.Ch == 'g' || event.Ch == 'G' { |
||||
if profile.Regroup() == nil { |
||||
screen.Draw(quotes) |
||||
} |
||||
} else if event.Ch == 'p' || event.Ch == 'P' { |
||||
paused = !paused |
||||
screen.Pause(paused).Draw(time.Now()) |
||||
} else if event.Ch == '?' || event.Ch == 'h' || event.Ch == 'H' { |
||||
showingHelp = true |
||||
screen.Clear().Draw(help) |
||||
} else if event.Key == termbox.KeyPgdn || |
||||
event.Ch == 'J' { |
||||
screen.IncreaseOffset(upDownJump) |
||||
redrawQuotesFlag = true |
||||
} else if event.Key == termbox.KeyPgup { |
||||
screen.DecreaseOffset(upDownJump) |
||||
redrawQuotesFlag = true |
||||
}else if event.Ch == 'K' || event.Ch == 'k' { |
||||
quotes.Allflag = !quotes.Allflag |
||||
}else if event.Ch == 'A' || event.Ch == 'a' { |
||||
if mode == "standby" { |
||||
//quotes.Reload()
|
||||
quotes.Reload() |
||||
}else{ |
||||
data := map[string]string{ |
||||
"cmd": "getlist", |
||||
} |
||||
jsonData, err := json.Marshal(data) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
} |
||||
message := string(jsonData) |
||||
token := client.Publish("stock/standby", 0, false, message) |
||||
token.Wait() |
||||
} |
||||
}else if event.Key == termbox.KeyArrowUp { |
||||
screen.DecreaseOffset(1) |
||||
screen.Selectmoveup(quotes) |
||||
selstock := quotes.Getselectedinfo(screen.Selectindex()) |
||||
if selstock != nil { |
||||
canstock = *selstock |
||||
}else{ |
||||
canstock = mop.Stock{} |
||||
} |
||||
redrawQuotesFlag = true |
||||
} else if event.Key == termbox.KeyArrowDown || event.Ch == 'j' { |
||||
screen.IncreaseOffset(1) |
||||
screen.Selectmovedown(quotes) |
||||
selstock := quotes.Getselectedinfo(screen.Selectindex()) |
||||
if selstock != nil { |
||||
canstock = *selstock |
||||
}else{ |
||||
canstock = mop.Stock{} |
||||
} |
||||
redrawQuotesFlag = true |
||||
} else if event.Key == termbox.KeyHome { |
||||
screen.ScrollTop() |
||||
redrawQuotesFlag = true |
||||
} else if event.Key == termbox.KeyEnd { |
||||
screen.ScrollBottom() |
||||
redrawQuotesFlag = true |
||||
} else if event.Ch == 'b' { |
||||
data := map[string]string{ |
||||
"cmd": "getaccount", |
||||
} |
||||
jsonData, err := json.Marshal(data) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
} |
||||
message := string(jsonData) |
||||
token := client.Publish("stock/request/470100037961", 0, false, message) |
||||
token.Wait() |
||||
account = "470100037961" |
||||
} else if event.Key == termbox.KeySpace { |
||||
data := map[string]string{ |
||||
"cmd": "getaccount", |
||||
} |
||||
jsonData, err := json.Marshal(data) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
} |
||||
message := string(jsonData) |
||||
token := client.Publish("stock/request/620000301588", 0, false, message) |
||||
token.Wait() |
||||
account = "620000301588" |
||||
} else if event.Ch >= '0' && event.Ch <= '9' { |
||||
input += string(event.Ch) |
||||
} else if event.Key == termbox.KeyEnter || event.Key == termbox.KeyArrowLeft || event.Key == termbox.KeyArrowRight { |
||||
indexnum, err := strconv.Atoi(input) |
||||
selectindex := screen.Selectindex() |
||||
if selectindex > 0 { |
||||
selcode := canstock.Dividend |
||||
if event.Key == termbox.KeyArrowLeft || event.Key == termbox.KeyEnter { |
||||
quotes.Sendstockgraphreq(selcode, false) |
||||
} |
||||
if event.Key == termbox.KeyArrowRight || event.Key == termbox.KeyEnter { |
||||
quotes.Sendstockgraphreq(selcode, true) |
||||
} |
||||
}else{ |
||||
//fmt.Println("indexnum:", indexnum)
|
||||
if err == nil && indexnum > 0 { |
||||
if event.Key == termbox.KeyArrowLeft || event.Key == termbox.KeyEnter { |
||||
quotes.Sendstockgraphreq(indexnum, false) |
||||
} |
||||
if event.Key == termbox.KeyArrowRight || event.Key == termbox.KeyEnter { |
||||
quotes.Sendstockgraphreq(indexnum, true) |
||||
} |
||||
} |
||||
} |
||||
// 清空输入缓冲区
|
||||
input = "" |
||||
} |
||||
} else if lineEditor != nil { |
||||
if done := lineEditor.Handle(event); done { |
||||
lineEditor = nil |
||||
} |
||||
} else if columnEditor != nil { |
||||
if done := columnEditor.Handle(event); done { |
||||
columnEditor = nil |
||||
} |
||||
} else if showingHelp { |
||||
showingHelp = false |
||||
screen.Clear().Draw(market, quotes) |
||||
} |
||||
case termbox.EventResize: |
||||
screen.Resize() |
||||
if !showingHelp { |
||||
//screen.Draw(market)
|
||||
//redrawQuotesFlag = true
|
||||
//screen.Draw(market)
|
||||
redrawQuotesFlag = true |
||||
redrawMarketFlag = true |
||||
//screen.DrawOldQuotes(quotes)
|
||||
} else { |
||||
screen.Draw(help) |
||||
} |
||||
case termbox.EventMouse: |
||||
if lineEditor == nil && columnEditor == nil && !showingHelp { |
||||
switch event.Key { |
||||
case termbox.MouseWheelUp: |
||||
screen.DecreaseOffset(5) |
||||
redrawQuotesFlag = true |
||||
case termbox.MouseWheelDown: |
||||
screen.IncreaseOffset(5) |
||||
redrawQuotesFlag = true |
||||
} |
||||
} |
||||
} |
||||
|
||||
case <-timestampQueue.C: |
||||
now := time.Now() |
||||
//add for save stock records
|
||||
hour := now.Hour() |
||||
minute := now.Minute() |
||||
second := now.Second() |
||||
|
||||
if hour > 9 && hour < 16 { |
||||
// Check if the current time is 1 minute and 0 seconds past every hour
|
||||
if minute == 1 && second == 0 { |
||||
//fmt.Println("It's 1 minute and 0 seconds past the hour")
|
||||
quotes.SaveStocks() |
||||
}
|
||||
} else if hour == 9 && minute == 1 && second == 0 { |
||||
paused = false |
||||
} else if hour == 16 && minute == 1 && second == 0{ |
||||
paused = true |
||||
} |
||||
|
||||
if hour == 9 && minute == 11 && second == 0 && mode == "standby" { |
||||
watchlist := getwatchlist() |
||||
log.Println("watchlist update enter.") |
||||
quotes.ResetforNewday(&watchlist) |
||||
paused = false |
||||
} |
||||
|
||||
if !showingHelp && !paused { |
||||
screen.Draw(now) |
||||
} |
||||
|
||||
case <-quotesQueue.C: |
||||
if !showingHelp && !paused && len(keyboardQueue) == 0 { |
||||
//res := stock.G_STOCK_MANAGER.StockList
|
||||
//fmt.Println(res)
|
||||
//if res["sh600000"].Market.Open != 0 {
|
||||
// fmt.Println("got quotes")
|
||||
go quotes.Fetch() |
||||
redrawQuotesFlag = true |
||||
//}
|
||||
} |
||||
|
||||
case <-marketQueue.C: |
||||
if !showingHelp && !paused { |
||||
screen.Draw(market) |
||||
} |
||||
|
||||
case msg := <-messages: |
||||
//fmt.Printf("Received message: %s\n", msg.Payload())
|
||||
screen.Close() |
||||
// 清除屏幕
|
||||
|
||||
if strings.HasPrefix(msg.Topic(), "stock/image/"){ |
||||
if strings.HasPrefix(msg.Topic(), "stock/image/day"){ |
||||
fmt.Print("\033[2J")
|
||||
os.Stdout.Write(msg.Payload()) |
||||
|
||||
break |
||||
}else if strings.HasPrefix(msg.Topic(), "stock/image/time"){ |
||||
os.Stdout.Write(msg.Payload()) |
||||
|
||||
preset := &Preset{} |
||||
jsonstr := getuserinput(preset, canstock) |
||||
if jsonstr != "" { |
||||
token := client.Publish("stock/request/"+account, 0, false, jsonstr) |
||||
token.Wait() |
||||
} |
||||
} |
||||
}else if strings.HasPrefix(msg.Topic(), "stock/response/standby"){ |
||||
//here we get the response json of totalstocks
|
||||
//quotes.Sendtotalstocks()
|
||||
quotes.Reloadbyjson(msg.Payload()) |
||||
}else if strings.HasPrefix(msg.Topic(), "stock/response"){ |
||||
fmt.Print("\033[2J")
|
||||
showposition(string(msg.Payload()) ,si) |
||||
|
||||
preset := &Preset{} |
||||
jsonstr := getuserinput(preset, canstock) |
||||
if jsonstr != "" { |
||||
token := client.Publish("stock/request/"+account, 0, false, jsonstr) |
||||
token.Wait() |
||||
} |
||||
}else if strings.HasPrefix(msg.Topic(), "stock/standby"){ |
||||
//here we reponse with json of totalstocks
|
||||
quotes.Sendtotalstocks() |
||||
} |
||||
|
||||
//time.Sleep(1 * time.Second)
|
||||
screen := mop.NewScreen(profile) |
||||
defer screen.Close() |
||||
} |
||||
|
||||
if redrawQuotesFlag && len(keyboardQueue) == 0 { |
||||
screen.DrawOldQuotes(quotes) |
||||
linetxt := screen.Getline3() |
||||
|
||||
selstock := quotes.GetselectedinfobyTicker(linetxt) |
||||
if selstock != nil { |
||||
canstock = *selstock |
||||
}else{ |
||||
canstock = mop.Stock{} |
||||
} |
||||
screen.Setline3(canstock.Ticker) |
||||
redrawQuotesFlag = false |
||||
} |
||||
if redrawMarketFlag && len(keyboardQueue) == 0 { |
||||
screen.Draw(market) |
||||
redrawMarketFlag = false |
||||
}
|
||||
} |
||||
} |
||||
|
||||
type Position struct { |
||||
Scode string `json:"scode"` |
||||
Sname string `json:"sname"` |
||||
Openprice float64 `json:"openprice"` |
||||
Floatprofit float64 `json:"floatprofit"` |
||||
Marketvalue float64 `json:"marketvalue"` |
||||
} |
||||
|
||||
type Preset struct { |
||||
Direction int64 `json:"direction"` |
||||
Scode string `json:"scode"` |
||||
Sname string `json:"sname"` |
||||
Ifabove float64 `json:"ifabove"` |
||||
Ifbelow float64 `json:"ifbelow"` |
||||
Vol float64 `json:"vol"` |
||||
} |
||||
|
||||
type Positions struct { |
||||
Position []Position `json:"position"` |
||||
Preset []Preset `json:"preset"` |
||||
Latestinfo string `json:"lastinfo"` |
||||
} |
||||
|
||||
func float2Str(f float64) string { |
||||
return fmt.Sprintf("%.2f", f) |
||||
} |
||||
|
||||
func showposition(payload string,si map[string]*stock.Stock) string{ |
||||
|
||||
var positions Positions |
||||
//fmt.Println(payload)
|
||||
if err := json.Unmarshal([]byte(payload), &positions); err != nil { |
||||
fmt.Println("Error parsing JSON:", err) |
||||
return "" |
||||
} |
||||
|
||||
var data [][]string |
||||
var totalFloatProfit float64 = 0.0 |
||||
var totalMarketValue float64 = 0.0 |
||||
//fmt.Println(positions.Position)
|
||||
for _, pos := range positions.Position { |
||||
row := []string{pos.Scode + " " +pos.Sname, fmt.Sprintf("%.2f", pos.Openprice), fmt.Sprintf("%.2f", pos.Floatprofit), fmt.Sprintf("%.2f", pos.Marketvalue)} |
||||
data = append(data, row) |
||||
totalFloatProfit += pos.Floatprofit |
||||
totalMarketValue += pos.Marketvalue |
||||
} |
||||
|
||||
for _, pos := range positions.Preset { |
||||
condition := "" |
||||
newscode := pos.Scode |
||||
row := []string{} |
||||
if pos.Ifabove != 0 { |
||||
condition = "> " + fmt.Sprintf("%.2f", pos.Ifabove) |
||||
}else{ |
||||
condition = "< " + fmt.Sprintf("%.2f", pos.Ifbelow) |
||||
} |
||||
//if pos.Scode starts with 's'
|
||||
if !strings.HasPrefix(pos.Scode, "s"){ |
||||
if strings.HasPrefix(pos.Scode, "6"){ |
||||
newscode = "sh" + pos.Scode |
||||
}else{ |
||||
newscode = "sz" + pos.Scode |
||||
} |
||||
} |
||||
if pos.Direction == 23 { |
||||
row = []string{pos.Scode+" "+si[newscode].Base.Name , "buy", condition, fmt.Sprintf("%.2f", pos.Vol)} |
||||
}else{ |
||||
row = []string{pos.Scode+" "+si[newscode].Base.Name , "sell", condition, fmt.Sprintf("%.2f", pos.Vol)} |
||||
} |
||||
data = append(data, row) |
||||
} |
||||
|
||||
tableString := &strings.Builder{} |
||||
table := tablewriter.NewWriter(tableString) |
||||
table.SetHeader([]string{"Instrument", "Avg. Price", "Float Profit", "Market Value"}) |
||||
table.SetFooter([]string{"", "Total", float2Str(totalFloatProfit), float2Str(totalMarketValue)}) // Add Footer
|
||||
//table.EnableBorder(false) // Set Border to false
|
||||
|
||||
table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor}, |
||||
tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor}, |
||||
tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor}, |
||||
tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor}) |
||||
|
||||
table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, |
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor}, |
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, |
||||
tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor}) |
||||
|
||||
table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{}, |
||||
tablewriter.Colors{tablewriter.Bold}, |
||||
tablewriter.Colors{tablewriter.FgHiRedColor}) |
||||
|
||||
table.AppendBulk(data) |
||||
table.Render() |
||||
fmt.Println(tableString.String())//()
|
||||
fmt.Println(positions.Latestinfo) |
||||
return tableString.String() |
||||
} |
||||
//-----------------------------------------------------------------------------
|
||||
func main() { |
||||
usr, err := user.Current() |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
var profileName string |
||||
flag.StringVar(&profileName, "profile", path.Join(usr.HomeDir, defaultProfile), "path to profile") |
||||
var mode string |
||||
flag.StringVar(&mode, "mode", "normal", "execution mode (normal/review)") |
||||
flag.Parse() |
||||
|
||||
profile, err := mop.NewProfile(profileName) |
||||
if err != nil { |
||||
fmt.Fprintf(os.Stderr, "The profile read from `%s` is corrupted.\n\tError: %s\n\n", profileName, err) |
||||
|
||||
// Loop until we get a "y" or "n" answer.
|
||||
// Note: This is only for the interactive mode. Once we have the "one-shot", this should be skipped
|
||||
for { |
||||
fmt.Fprintln(os.Stderr, "Do you want to overwrite the current profile with the default one? [y/n]") |
||||
rne, _, _ := keyboard.GetSingleKey() |
||||
res := strings.ToLower(string(rne)) |
||||
if res != "y" && res != "n" { |
||||
fmt.Fprintf(os.Stderr, "Invalid answer `%s`\n\n", res) |
||||
continue |
||||
} |
||||
|
||||
if res == "y" { |
||||
profile.InitDefaultProfile() |
||||
break |
||||
} else { |
||||
os.Exit(1) |
||||
} |
||||
} |
||||
} |
||||
|
||||
profile.SetMode(mode) |
||||
fmt.Println("mode:", mode) |
||||
|
||||
if mode == "review" { |
||||
scanner := bufio.NewScanner(os.Stdin) |
||||
|
||||
// Prompt the user to enter the first date
|
||||
fmt.Print("Enter the first date (YYYY-MM-DD): ") |
||||
scanner.Scan() |
||||
firstDateStr := scanner.Text() |
||||
|
||||
// Parse the first date
|
||||
firstDate, err := time.Parse("2006-01-02", firstDateStr) |
||||
if err != nil { |
||||
fmt.Println("Error parsing start date:", err) |
||||
return |
||||
} |
||||
|
||||
// Prompt the user to enter the second date
|
||||
fmt.Print("Enter the end date (YYYY-MM-DD): ") |
||||
scanner.Scan() |
||||
secondDateStr := scanner.Text() |
||||
|
||||
// Parse the second date
|
||||
secondDate, err := time.Parse("2006-01-02", secondDateStr) |
||||
if err != nil { |
||||
fmt.Println("Error parsing end date:", err) |
||||
return |
||||
} |
||||
|
||||
// Print the two dates
|
||||
fmt.Println("Start date:", firstDate) |
||||
fmt.Println("End date:", secondDate) |
||||
for d := firstDate; d.Before(secondDate); d = d.AddDate(0, 0, 1) { |
||||
profile.AddDate(d.Format("2006-01-02")) |
||||
} |
||||
//fmt.Println("Dates:", profile.date_json)
|
||||
}else if mode == "standby"{ |
||||
|
||||
} |
||||
|
||||
screen := mop.NewScreen(profile) |
||||
defer screen.Close() |
||||
|
||||
mainLoop(screen, profile, mode) |
||||
profile.Save() |
||||
} |
||||
@ -0,0 +1,100 @@ |
||||
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package mop |
||||
|
||||
import `github.com/nsf/termbox-go` |
||||
|
||||
// ColumnEditor handles column sort order. When activated it highlights
|
||||
// current column name in the header, then waits for arrow keys (choose
|
||||
// another column), Enter (reverse sort order), or Esc (exit).
|
||||
type ColumnEditor struct { |
||||
screen *Screen // Pointer to Screen so we could use screen.Draw().
|
||||
quotes *Quotes // Pointer to Quotes to redraw them when the sort order changes.
|
||||
layout *Layout // Pointer to Layout to redraw stock quotes header.
|
||||
profile *Profile // Pointer to Profile where we save newly selected sort order.
|
||||
} |
||||
|
||||
// Returns new initialized ColumnEditor struct. As part of initialization it
|
||||
// highlights current column name (as stored in Profile).
|
||||
func NewColumnEditor(screen *Screen, quotes *Quotes) *ColumnEditor { |
||||
editor := &ColumnEditor{ |
||||
screen: screen, |
||||
quotes: quotes, |
||||
layout: screen.layout, |
||||
profile: quotes.profile, |
||||
} |
||||
|
||||
editor.selectCurrentColumn() |
||||
|
||||
return editor |
||||
} |
||||
|
||||
// Handle takes over the keyboard events and dispatches them to appropriate
|
||||
// column editor handlers. It returns true when user presses Esc.
|
||||
func (editor *ColumnEditor) Handle(event termbox.Event) bool { |
||||
defer editor.redrawHeader() |
||||
|
||||
switch event.Key { |
||||
case termbox.KeyEsc: |
||||
return editor.done() |
||||
|
||||
case termbox.KeyEnter: |
||||
editor.execute() |
||||
|
||||
case termbox.KeyArrowLeft: |
||||
editor.selectLeftColumn() |
||||
|
||||
case termbox.KeyArrowRight: |
||||
editor.selectRightColumn() |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *ColumnEditor) selectCurrentColumn() *ColumnEditor { |
||||
editor.profile.selectedColumn = editor.profile.SortColumn |
||||
editor.redrawHeader() |
||||
return editor |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *ColumnEditor) selectLeftColumn() *ColumnEditor { |
||||
editor.profile.selectedColumn-- |
||||
if editor.profile.selectedColumn < 0 { |
||||
editor.profile.selectedColumn = editor.layout.TotalColumns() - 1 |
||||
} |
||||
return editor |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *ColumnEditor) selectRightColumn() *ColumnEditor { |
||||
editor.profile.selectedColumn++ |
||||
if editor.profile.selectedColumn > editor.layout.TotalColumns()-1 { |
||||
editor.profile.selectedColumn = 0 |
||||
} |
||||
return editor |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *ColumnEditor) execute() *ColumnEditor { |
||||
if editor.profile.Reorder() == nil { |
||||
editor.screen.Draw(editor.quotes) |
||||
} |
||||
|
||||
return editor |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *ColumnEditor) done() bool { |
||||
editor.profile.selectedColumn = -1 |
||||
return true |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *ColumnEditor) redrawHeader() { |
||||
editor.screen.DrawLine(0, 4, editor.layout.Header(editor.profile)) |
||||
termbox.Flush() |
||||
} |
||||
@ -0,0 +1,297 @@ |
||||
package mop |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
|
||||
//"github.com/markcheno/go-talib"
|
||||
) |
||||
|
||||
type KLineData struct { |
||||
Day string `json:"day"` |
||||
Open float64 `json:"open,string"` |
||||
High float64 `json:"high,string"` |
||||
Low float64 `json:"low,string"` |
||||
Close float64 `json:"close,string"` |
||||
Volume float64 `json:"volume,string"` |
||||
} |
||||
|
||||
func getURL(code string, ts int, count int) string { |
||||
return fmt.Sprintf("http://money.finance.sina.com.cn/quotes_service/api/json_v2.php/CN_MarketData.getKLineData?symbol=%s&scale=%d&ma=5&datalen=%d", code, ts, count) |
||||
} |
||||
|
||||
func getKLineData(url string) ([]KLineData, error) { |
||||
resp, err := http.Get(url) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
body, err := ioutil.ReadAll(resp.Body) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var data []KLineData |
||||
err = json.Unmarshal(body, &data) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return data, nil |
||||
} |
||||
func get_price_sina(code string, end_date string, count int, frequency string, startdate string) ([]KLineData, error) { |
||||
frequency = strings.Replace(frequency, "1d", "240m", 1) |
||||
frequency = strings.Replace(frequency, "1w", "1200m", 1) |
||||
frequency = strings.Replace(frequency, "1M", "7200m", 1) |
||||
|
||||
ts, err := strconv.Atoi(strings.TrimSuffix(frequency, "m")) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if end_date != "" && (frequency == "240m" || frequency == "1200m" || frequency == "7200m") { |
||||
endDate, err := time.Parse("2006-01-02", end_date) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
unit := 1 |
||||
if frequency == "1200m" { |
||||
unit = 4 |
||||
} else if frequency == "7200m" { |
||||
unit = 29 |
||||
} |
||||
|
||||
days := int(time.Since(endDate).Hours() / 24) |
||||
count = count + days/unit |
||||
} |
||||
|
||||
url := getURL(code, ts, count) |
||||
//fmt.Printf("url: %s\n", url)
|
||||
data, err := getKLineData(url) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
//fmt.Printf("data: %v\n", data)
|
||||
if startdate != "" && (frequency == "240m" || frequency == "1200m" || frequency == "7200m") { |
||||
endDate := time.Now() |
||||
//endDate, err := time.Parse("2006-01-02", end_date)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
|
||||
start_date, err := time.Parse("2006-01-02", startdate) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
filteredData := []KLineData{} |
||||
for _, d := range data { |
||||
day, err := time.Parse("2006-01-02", d.Day) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
if day.After(start_date) && (day.Before(endDate) || day.Equal(endDate)) { |
||||
filteredData = append(filteredData, d) |
||||
} |
||||
} |
||||
|
||||
return filteredData, nil |
||||
} |
||||
|
||||
return data, nil |
||||
} |
||||
|
||||
func findMaxHighAndMinLow(data []KLineData) (float64, float64) { |
||||
maxHigh := data[0].High |
||||
minLow := data[0].Low |
||||
|
||||
for _, d := range data { |
||||
if d.High > maxHigh { |
||||
maxHigh = d.High |
||||
} |
||||
if d.Low < minLow { |
||||
minLow = d.Low |
||||
} |
||||
} |
||||
|
||||
return maxHigh, minLow |
||||
} |
||||
|
||||
// 将interface{}类型转换为float64类型
|
||||
func convertToFloat64(value interface{}) float64 { |
||||
if floatValue, ok := value.(float64); ok { |
||||
return floatValue |
||||
} |
||||
if stringValue, ok := value.(string); ok { |
||||
floatValue, err := strconv.ParseFloat(stringValue, 64) |
||||
if err == nil { |
||||
return floatValue |
||||
} |
||||
} |
||||
return 0 |
||||
} |
||||
|
||||
func getURLTencent(code string, start string, count int) string { |
||||
// 给定的日期
|
||||
datestr := "" |
||||
date, err := time.Parse("2006-01-02", start) |
||||
if err != nil { |
||||
fmt.Println("解析日期失败:", err) |
||||
}else{ |
||||
futureDate := date.AddDate(0, 0, 10) |
||||
if futureDate.Before(time.Now()){ |
||||
datestr = futureDate.Format("2006-01-02") |
||||
fmt.Println(datestr) |
||||
} |
||||
} |
||||
|
||||
return fmt.Sprintf("https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?param=%s,day,,%s,%d,qfq", code, datestr, count) |
||||
} |
||||
|
||||
func getKLineTencent(url string, code string)([]KLineData, error){ |
||||
resp, err := http.Get(url) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer resp.Body.Close() |
||||
|
||||
body, err := ioutil.ReadAll(resp.Body) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var dataMap map[string]interface{} |
||||
err = json.Unmarshal(body, &dataMap) |
||||
if err != nil { |
||||
fmt.Println("解析JSON字符串失败", err) |
||||
return nil, err |
||||
} |
||||
klineData := make([]KLineData, 0) |
||||
// 获取K线数据
|
||||
if shData, ok := dataMap["data"].(map[string]interface{})[code].(map[string]interface{}); ok { |
||||
if qfqday, ok := shData["qfqday"].([]interface{}); ok { |
||||
|
||||
for _, item := range qfqday { |
||||
if data, ok := item.([]interface{}); ok { |
||||
klineData = append(klineData, KLineData{ |
||||
Day: data[0].(string), |
||||
Open: convertToFloat64(data[1]), |
||||
High: convertToFloat64(data[3]), |
||||
Low: convertToFloat64(data[4]), |
||||
Close: convertToFloat64(data[2]), |
||||
Volume: convertToFloat64(data[5]), |
||||
}) |
||||
} |
||||
} |
||||
//fmt.Println(klineData)
|
||||
return klineData,nil |
||||
}else if day, ok := shData["day"].([]interface{}); ok { |
||||
for _, item := range day { |
||||
if data, ok := item.([]interface{}); ok { |
||||
klineData = append(klineData, KLineData{ |
||||
Day: data[0].(string), |
||||
Open: convertToFloat64(data[1]), |
||||
High: convertToFloat64(data[3]), |
||||
Low: convertToFloat64(data[4]), |
||||
Close: convertToFloat64(data[2]), |
||||
Volume: convertToFloat64(data[5]), |
||||
}) |
||||
} |
||||
} |
||||
return klineData,nil |
||||
} |
||||
} |
||||
return klineData,nil |
||||
} |
||||
|
||||
func get_price_tencent(code string, end_date string, count int, frequency string, startdate string) ([]KLineData, error) { |
||||
frequency = strings.Replace(frequency, "1d", "240m", 1) |
||||
frequency = strings.Replace(frequency, "1w", "1200m", 1) |
||||
frequency = strings.Replace(frequency, "1M", "7200m", 1) |
||||
|
||||
if end_date != "" && (frequency == "240m" || frequency == "1200m" || frequency == "7200m") { |
||||
endDate, err := time.Parse("2006-01-02", end_date) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
unit := 1 |
||||
if frequency == "1200m" { |
||||
unit = 4 |
||||
} else if frequency == "7200m" { |
||||
unit = 29 |
||||
} |
||||
|
||||
days := int(time.Since(endDate).Hours() / 24) |
||||
count = count + days/unit |
||||
} |
||||
/* |
||||
url := getURL(code, ts, count) |
||||
fmt.Printf("url: %s\n", url) |
||||
data, err := getKLineData(url) |
||||
*/ |
||||
url := getURLTencent(code, startdate, 10) |
||||
//fmt.Printf("url: %s\n", url)
|
||||
data, err := getKLineTencent(url, code) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
//fmt.Printf("data: %v\n", data)
|
||||
if startdate != "" && (frequency == "240m" || frequency == "1200m" || frequency == "7200m") { |
||||
endDate := time.Now() |
||||
//endDate, err := time.Parse("2006-01-02", end_date)
|
||||
//if err != nil {
|
||||
// return nil, err
|
||||
//}
|
||||
start_date, err := time.Parse("2006-01-02", startdate) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
filteredData := []KLineData{} |
||||
for _, d := range data { |
||||
day, err := time.Parse("2006-01-02", d.Day) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
//fmt.Println(start_date, endDate, day)
|
||||
if day.After(start_date) && (day.Before(endDate) || day.Equal(endDate)) { |
||||
filteredData = append(filteredData, d) |
||||
} |
||||
} |
||||
|
||||
return filteredData, nil |
||||
} |
||||
|
||||
return data, nil |
||||
} |
||||
|
||||
func getnextdaysHL(code string, start_date string) (float64, float64, float64, float64 ,int) { |
||||
//data, err := get_price_sina(code, "", 30, "240m", start_date)
|
||||
data, err := get_price_tencent(code, "", 30, "240m", start_date) |
||||
if err != nil { |
||||
return 0, 0, 0, 0, 0 |
||||
} |
||||
|
||||
pre := 3 |
||||
if len(data) < 3 { |
||||
pre = len(data) |
||||
} |
||||
|
||||
//fmt.Printf("data: %v\n", data[:3])
|
||||
caldata := data[:pre] |
||||
maxHigh, minLow := findMaxHighAndMinLow(caldata) |
||||
//fmt.Println(maxHigh, minLow)
|
||||
|
||||
return caldata[0].Open, caldata[pre-1].Close, maxHigh, minLow, pre |
||||
} |
||||
|
||||
|
||||
@ -0,0 +1,94 @@ |
||||
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package mop |
||||
|
||||
import ( |
||||
"strings" |
||||
"strconv" |
||||
) |
||||
|
||||
// Filter gets called to sort stock quotes by one of the columns. The
|
||||
// setup is rather lengthy; there should probably be more concise way
|
||||
// that uses reflection and avoids hardcoding the column names.
|
||||
type Filter struct { |
||||
profile *Profile // Pointer to where we store sort column and order.
|
||||
} |
||||
|
||||
// Returns new Filter struct.
|
||||
func NewFilter(profile *Profile) *Filter { |
||||
return &Filter{ |
||||
profile: profile, |
||||
} |
||||
} |
||||
|
||||
// Changes money and % notation to a plain float for math, comparisons.
|
||||
func stringToNumber (numberString string) float64 { |
||||
// If the string "$3.6B" is passed in, the returned float will be 3.6E+09.
|
||||
// If 0.03% is passed in, the returned float will be 0.03 (NOT 0.0003!).
|
||||
newString := strings.TrimSpace(numberString) // Take off whitespace.
|
||||
newString = strings.Replace(newString,"$","",1) // Remove the $ symbol.
|
||||
newString = strings.Replace(newString,"%","",1) // Remove the $ symbol.
|
||||
newString = strings.Replace(newString,"K","E+3",1) // Thousand (kilo)
|
||||
newString = strings.Replace(newString,"M","E+6",1) // Million
|
||||
newString = strings.Replace(newString,"B","E+9",1) // Billion
|
||||
newString = strings.Replace(newString,"T","E+12",1) // Trillion
|
||||
finalValue, _ := strconv.ParseFloat(newString, 64) |
||||
return finalValue |
||||
} |
||||
|
||||
// Apply builds a list of sort interface based on current sort
|
||||
// order, then calls sort.Sort to do the actual job.
|
||||
func (filter *Filter) Apply(stocks []Stock) []Stock { |
||||
var filteredStocks []Stock |
||||
|
||||
for _, stock := range stocks { |
||||
var values = make(map[string]interface{}) |
||||
// Make conversions from the strings to floats where necessary.
|
||||
values["ticker"] = strings.TrimSpace(stock.Ticker) // Remains string
|
||||
values["last"] = stringToNumber(stock.LastTrade) |
||||
values["change"] = stringToNumber(stock.Change) |
||||
values["changePercent"] = stringToNumber(stock.ChangePct) |
||||
values["open"] = stringToNumber(stock.Open) |
||||
values["low"] = stringToNumber(stock.Low) |
||||
values["high"] = stringToNumber(stock.High) |
||||
values["low52"] = stringToNumber(stock.Low52) |
||||
values["high52"] = stringToNumber(stock.High52) |
||||
values["dividend"] = stringToNumber(stock.Dividend) |
||||
values["yield"] = stringToNumber(stock.Yield) |
||||
values["mktCap"] = stringToNumber(stock.MarketCap) |
||||
values["mktCapX"] = stringToNumber(stock.MarketCapX) |
||||
values["volume"] = stringToNumber(stock.Volume) |
||||
values["avgVolume"] = stringToNumber(stock.AvgVolume) |
||||
values["pe"] = stringToNumber(stock.PeRatio) |
||||
values["peX"] = stringToNumber(stock.PeRatioX) |
||||
values["direction"] = stock.Direction // Remains int.
|
||||
|
||||
result, err := filter.profile.filterExpression.Evaluate(values) |
||||
|
||||
if err != nil { |
||||
// The filter isn't working, so reset to no filter.
|
||||
filter.profile.Filter = "" |
||||
// Return an empty list. The next main loop cycle will
|
||||
// show unfiltered.
|
||||
return filteredStocks |
||||
} |
||||
|
||||
truthy, ok := result.(bool) |
||||
|
||||
if !ok { |
||||
// The filter isn't working, so reset to no filter.
|
||||
filter.profile.Filter = "" |
||||
// Return an empty list. The next main loop cycle will
|
||||
// show unfiltered.
|
||||
return filteredStocks |
||||
} |
||||
|
||||
if truthy { |
||||
filteredStocks = append(filteredStocks, stock) |
||||
} |
||||
} |
||||
|
||||
return filteredStocks |
||||
} |
||||
@ -0,0 +1,23 @@ |
||||
module github.com/mop-tracker/mop |
||||
|
||||
go 1.15 |
||||
|
||||
require ( |
||||
github.com/Knetic/govaluate v3.0.0+incompatible |
||||
github.com/PuerkitoBio/goquery v1.8.1 // indirect |
||||
github.com/antchfx/htmlquery v1.3.0 // indirect |
||||
github.com/antchfx/xmlquery v1.3.15 // indirect |
||||
github.com/eclipse/paho.mqtt.golang v1.4.2 // indirect |
||||
github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 |
||||
github.com/gizak/termui/v3 v3.1.0 // indirect |
||||
github.com/gobwas/glob v0.2.3 // indirect |
||||
github.com/gocolly/colly v1.2.0 // indirect |
||||
github.com/kennygrant/sanitize v1.2.4 // indirect |
||||
github.com/mattn/go-runewidth v0.0.13 // indirect |
||||
github.com/nsf/termbox-go v1.1.1 |
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect |
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect |
||||
github.com/temoto/robotstxt v1.1.2 // indirect |
||||
golang.org/x/net v0.9.0 // indirect |
||||
google.golang.org/appengine v1.6.7 // indirect |
||||
) |
||||
@ -0,0 +1,109 @@ |
||||
github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg= |
||||
github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= |
||||
github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= |
||||
github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= |
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= |
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= |
||||
github.com/antchfx/htmlquery v1.3.0 h1:5I5yNFOVI+egyia5F2s/5Do2nFWxJz41Tr3DyfKD25E= |
||||
github.com/antchfx/htmlquery v1.3.0/go.mod h1:zKPDVTMhfOmcwxheXUsx4rKJy8KEY/PU6eXr/2SebQ8= |
||||
github.com/antchfx/xmlquery v1.3.15 h1:aJConNMi1sMha5G8YJoAIF5P+H+qG1L73bSItWHo8Tw= |
||||
github.com/antchfx/xmlquery v1.3.15/go.mod h1:zMDv5tIGjOxY/JCNNinnle7V/EwthZ5IT8eeCGJKRWA= |
||||
github.com/antchfx/xpath v1.2.3 h1:CCZWOzv5bAqjVv0offZ2LVgVYFbeldKQVuLNbViZdes= |
||||
github.com/antchfx/xpath v1.2.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= |
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
||||
github.com/eclipse/paho.mqtt.golang v1.4.2 h1:66wOzfUHSSI1zamx7jR6yMEI5EuHnT1G6rNA5PM12m4= |
||||
github.com/eclipse/paho.mqtt.golang v1.4.2/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA= |
||||
github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807 h1:jdjd5e68T4R/j4PWxfZqcKY8KtT9oo8IPNVuV4bSXDQ= |
||||
github.com/eiannone/keyboard v0.0.0-20200508000154-caf4b762e807/go.mod h1:Xoiu5VdKMvbRgHuY7+z64lhu/7lvax/22nzASF6GrO8= |
||||
github.com/gizak/termui/v3 v3.1.0 h1:ZZmVDgwHl7gR7elfKf1xc4IudXZ5qqfDh4wExk4Iajc= |
||||
github.com/gizak/termui/v3 v3.1.0/go.mod h1:bXQEBkJpzxUAKf0+xq9MSWAvWZlE7c+aidmyFlkYTrY= |
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= |
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= |
||||
github.com/gocolly/colly v1.2.0 h1:qRz9YAn8FIH0qzgNUw+HT9UN7wm1oF9OBAilwEWpyrI= |
||||
github.com/gocolly/colly v1.2.0/go.mod h1:Hof5T3ZswNVsOHYmba1u03W65HDWgpV5HifSuueE0EA= |
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= |
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= |
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= |
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= |
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= |
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= |
||||
github.com/kennygrant/sanitize v1.2.4 h1:gN25/otpP5vAsO2djbMhF/LQX6R7+O1TB4yv8NzpJ3o= |
||||
github.com/kennygrant/sanitize v1.2.4/go.mod h1:LGsjYYtgxbetdg5owWB2mpgUL6e2nfw2eObZ0u0qvak= |
||||
github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= |
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= |
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= |
||||
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= |
||||
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= |
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= |
||||
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= |
||||
github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d/go.mod h1:IuKpRQcYE1Tfu+oAQqaLisqDeXgjyyltCfsaoYN18NQ= |
||||
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 h1:Rl8NelBe+n7SuLbJyw13ho7CGWUt2BjGGKIoreCWQ/c= |
||||
github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= |
||||
github.com/nsf/termbox-go v1.1.1 h1:nksUPLCb73Q++DwbYUBEglYBRPZyoXJdrj5L+TkjyZY= |
||||
github.com/nsf/termbox-go v1.1.1/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= |
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= |
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= |
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= |
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= |
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= |
||||
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= |
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
||||
github.com/temoto/robotstxt v1.1.2 h1:W2pOjSJ6SWvldyEuiFXNxz3xZ8aiWX5LbfDiOFd7Fxg= |
||||
github.com/temoto/robotstxt v1.1.2/go.mod h1:+1AmkuG3IYkh1kv0d2qEB9Le88ehNO0zwOr3ujewlOo= |
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= |
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= |
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= |
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= |
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= |
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= |
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= |
||||
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= |
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= |
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= |
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= |
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= |
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= |
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= |
||||
golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= |
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= |
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= |
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= |
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= |
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= |
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs= |
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= |
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= |
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= |
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= |
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= |
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= |
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= |
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= |
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= |
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= |
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= |
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= |
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= |
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= |
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= |
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= |
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= |
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= |
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= |
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= |
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= |
||||
@ -0,0 +1,16 @@ |
||||
require "formula" |
||||
|
||||
class Mop < Formula |
||||
homepage "https://github.com/mop-tracker/mop" |
||||
head "https://github.com/mop-tracker/mop.git" |
||||
url "https://github.com/mop-tracker/mop/archive/refs/tags/v1.0.0.tar.gz" |
||||
sha1 "bc666ec165d08b43134f7ec0bf29083ad5466243" # Needs updating. |
||||
|
||||
depends_on "go" => :build |
||||
|
||||
def install |
||||
system "go", "get", "github.com/nsf/termbox-go" |
||||
system "go build cmd/mop.go" |
||||
bin.install "mop" |
||||
end |
||||
end |
||||
@ -0,0 +1,384 @@ |
||||
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package mop |
||||
|
||||
import ( |
||||
"bytes" |
||||
"fmt" |
||||
"reflect" |
||||
"regexp" |
||||
"strconv" |
||||
"strings" |
||||
"text/template" |
||||
"time" |
||||
"unicode" |
||||
//"unicode/utf8"
|
||||
) |
||||
|
||||
var currencies = map[string]string{ |
||||
"RUB": "₽", |
||||
"GBP": "£", |
||||
"GBp": "p", |
||||
"SEK": "kr", |
||||
"EUR": "€", |
||||
"JPY": "¥", |
||||
} |
||||
|
||||
// Column describes formatting rules for individual column within the list
|
||||
// of stock quotes.
|
||||
type Column struct { |
||||
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 // List of stock quotes columns.
|
||||
sorter *Sorter // Pointer to sorting receiver.
|
||||
filter *Filter // Pointer to filtering 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.
|
||||
} |
||||
|
||||
// Creates the layout and assigns the default values that stay unchanged.
|
||||
func NewLayout() *Layout { |
||||
layout := &Layout{} |
||||
layout.columns = []Column{ |
||||
{-5, `Ticker`, `Ticker `, nil}, |
||||
{10, `LastTrade`, `Last`, currency}, |
||||
{10, `Change`, `Change`, currency}, |
||||
{10, `ChangePct`, `Change%`, last}, |
||||
{10, `Open`, `Open`, currency}, |
||||
{10, `Low`, `Low`, currency}, |
||||
{10, `High`, `High`, currency}, |
||||
{10, `Low52`, `52w Low`, currency}, |
||||
{10, `High52`, `52w High`, currency}, |
||||
{11, `Volume`, `Volume`, integer}, |
||||
{11, `AvgVolume`, `AvgVolume`, integer}, |
||||
{9, `PeRatio`, `P/E`, blank}, |
||||
{9, `Dividend`, `Dividend`, zero}, |
||||
{9, `Yield`, `Yield`, percent}, |
||||
{11, `MarketCap`, `MktCap`, currency}, |
||||
{13, `PreOpen`, `PreMktChg%`, percent}, |
||||
{13, `AfterHours`, `AfterMktChg%`, percent}, |
||||
} |
||||
layout.regex = regexp.MustCompile(`(\.\d+)[TBMK]?$`) |
||||
layout.marketTemplate = buildMarketTemplate() |
||||
layout.quotesTemplate = buildQuotesTemplate() |
||||
|
||||
return layout |
||||
} |
||||
|
||||
// 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, |
||||
market.Tokyo, market.HongKong, market.London, market.Frankfurt, |
||||
market.Yield, market.Oil, market.Euro, market.Yen, market.Gold) |
||||
buffer := new(bytes.Buffer) |
||||
layout.marketTemplate.Execute(buffer, market) |
||||
|
||||
return buffer.String() |
||||
} |
||||
|
||||
// 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 { |
||||
zonename, _ := time.Now().In(time.Local).Zone() |
||||
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 // Current timestamp.
|
||||
Header string // Formatted header line.
|
||||
Stocks []Stock // List of formatted stock quotes.
|
||||
}{ |
||||
time.Now().Format(`3:04:05pm ` + zonename), |
||||
layout.Header(quotes.profile), |
||||
layout.prettify(quotes), |
||||
} |
||||
//fmt.Println(vars.Stocks)
|
||||
buffer := new(bytes.Buffer) |
||||
layout.quotesTemplate.Execute(buffer, vars) |
||||
//fmt.Println(buffer.String())
|
||||
return buffer.String() |
||||
} |
||||
|
||||
// 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 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) |
||||
} |
||||
} |
||||
|
||||
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 (layout *Layout) prettify(quotes *Quotes) []Stock { |
||||
pretty := make([]Stock, len(quotes.stocks)) |
||||
//
|
||||
// Iterate over the list of stocks and properly format all its columns.
|
||||
//
|
||||
for i, stock := range quotes.stocks { |
||||
pretty[i].Direction = stock.Direction |
||||
//
|
||||
// Iterate over the list of stock columns. For each column name:
|
||||
// - Get current column value.
|
||||
// - If the column has the formatter method then call it.
|
||||
// - Set the column value padding it to the given width.
|
||||
//
|
||||
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, stock.Currency) |
||||
} |
||||
// 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 profile.Filter != "" { // Fix for blank display if invalid filter expression was cleared.
|
||||
if profile.filterExpression != nil { |
||||
if layout.filter == nil { // Initialize filter on first invocation.
|
||||
layout.filter = NewFilter(profile) |
||||
} |
||||
pretty = layout.filter.Apply(pretty) |
||||
} |
||||
} |
||||
|
||||
if layout.sorter == nil { // Initialize sorter on first invocation.
|
||||
layout.sorter = NewSorter(profile) |
||||
} |
||||
layout.sorter.SortByCurrentColumn(pretty) |
||||
//
|
||||
// Group stocks by advancing/declining unless sorted by Chanage or Change%
|
||||
// in which case the grouping has been done already.
|
||||
//
|
||||
if profile.Grouped && (profile.SortColumn < 2 || profile.SortColumn > 3) { |
||||
pretty = group(pretty) |
||||
} |
||||
|
||||
return pretty |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (layout *Layout) pad(str string, width int) string { |
||||
match := layout.regex.FindStringSubmatch(str) |
||||
if len(match) > 0 { |
||||
switch len(match[1]) { |
||||
case 2: |
||||
str = strings.Replace(str, match[1], match[1]+`0`, 1) |
||||
case 4, 5: |
||||
str = strings.Replace(str, match[1], match[1][0:3], 1) |
||||
} |
||||
} |
||||
|
||||
newstr := fmt.Sprintf(`%*s`, width, str) |
||||
//fmt.Println(newstr)
|
||||
return newstr |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func buildMarketTemplate() *template.Template { |
||||
markup := `<tag>Dow</> {{.Dow.change}} ({{.Dow.percent}}) at {{.Dow.latest}} <tag>S&P 500</> {{.Sp500.change}} ({{.Sp500.percent}}) at {{.Sp500.latest}} <tag>NASDAQ</> {{.Nasdaq.change}} ({{.Nasdaq.percent}}) at {{.Nasdaq.latest}} |
||||
<tag>{{.Tokyo.name}}</> {{.Tokyo.change}} ({{.Tokyo.percent}}) at {{.Tokyo.latest}} <tag>{{.London.name}}</> {{.London.change}} ({{.London.percent}}) at {{.London.latest}} <tag>{{.Frankfurt.name}}</> {{.Frankfurt.change}} ({{.Frankfurt.percent}}) at {{.Frankfurt.latest}} <tag>HK</> {{.HongKong.change}} ({{.HongKong.percent}}) at {{.HongKong.latest}} {{if .IsClosed}}<right>U.S. markets closed</right>{{end}} |
||||
<tag>{{.Yield.name}}</> {{.Yield.latest}} ({{.Yield.change}}) <tag>Euro</> ${{.Euro.latest}} ({{.Euro.change}}) <tag>Yen</> ¥{{.Yen.latest}} ({{.Yen.change}}) <tag>Oil</> ${{.Oil.latest}} ({{.Oil.change}}) <tag>Gold</> ${{.Gold.latest}} ({{.Gold.change}})` |
||||
|
||||
return template.Must(template.New(`market`).Parse(markup)) |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func buildQuotesTemplate() *template.Template { |
||||
markup := `<right><time>{{.Now}}</></right> |
||||
|
||||
|
||||
|
||||
<header>{{.Header}}</> |
||||
{{range.Stocks}}{{if eq .Direction 1}}<gain>{{else if eq .Direction -1}}<loss>{{end}}{{.Ticker}}{{.LastTrade}}{{.Change}}{{.ChangePct}}{{.Open}}{{.Low}}{{.High}}{{.Low52}}{{.High52}}{{.Volume}}{{.AvgVolume}}{{.PeRatio}}{{.Dividend}}{{.Yield}}{{.MarketCap}}{{.PreOpen}}{{.AfterHours}}</> |
||||
{{end}}` |
||||
|
||||
return template.Must(template.New(`quotes`).Parse(markup)) |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func highlight(collections ...map[string]string) { |
||||
for _, collection := range collections { |
||||
change := collection[`change`] |
||||
if change[len(change)-1:] == `%` { |
||||
change = change[0 : len(change)-1] |
||||
} |
||||
adv, err := strconv.ParseFloat(change, 64) |
||||
if err == nil { |
||||
if adv < 0.0 { |
||||
collection[`change`] = `<loss>` + collection[`change`] + `</>` |
||||
} else if adv > 0.0 { |
||||
collection[`change`] = `<gain>` + collection[`change`] + `</>` |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func group(stocks []Stock) []Stock { |
||||
grouped := make([]Stock, len(stocks)) |
||||
current := 0 |
||||
|
||||
for _, stock := range stocks { |
||||
if stock.Direction >= 0 { |
||||
grouped[current] = stock |
||||
current++ |
||||
} |
||||
} |
||||
for _, stock := range stocks { |
||||
if stock.Direction < 0 { |
||||
grouped[current] = stock |
||||
current++ |
||||
} |
||||
} |
||||
|
||||
return grouped |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func arrowFor(column int, profile *Profile) string { |
||||
if column == profile.SortColumn { |
||||
if profile.Ascending { |
||||
return string('▲') |
||||
} |
||||
return string('▼') |
||||
} |
||||
return `` |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func blank(str ...string) string { |
||||
if len(str) < 1 { |
||||
return "ERR" |
||||
} |
||||
if (len(str[0]) == 3 && str[0][0:3] == `N/A`) || len(str[0]) == 0 { |
||||
return `-` |
||||
} |
||||
|
||||
return str[0] |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func zero(str ...string) string { |
||||
if len(str) < 2 { |
||||
return "ERR" |
||||
} |
||||
if str[0] == `0.00` { |
||||
return `-` |
||||
} |
||||
|
||||
return currency(str[0], str[1]) |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func last(str ...string) string { |
||||
if len(str) < 1 { |
||||
return "ERR" |
||||
} |
||||
if len(str[0]) >= 6 && str[0][0:6] == `N/A - ` { |
||||
return str[0][6:] |
||||
} |
||||
|
||||
return percent(str[0]) |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func currency(str ...string) string { |
||||
if len(str) < 2 { |
||||
return "ERR" |
||||
} |
||||
//default to $
|
||||
symbol := "" |
||||
c, ok := currencies[str[1]] |
||||
if ok { |
||||
symbol = c |
||||
} |
||||
if str[0] == `N/A` || len(str[0]) == 0 { |
||||
return `-` |
||||
} |
||||
if sign := str[0][0:1]; sign == `+` || sign == `-` { |
||||
return sign + symbol + str[0][1:] |
||||
} |
||||
|
||||
return symbol + str[0] |
||||
} |
||||
|
||||
// Returns percent value truncated at 2 decimal points.
|
||||
//-----------------------------------------------------------------------------
|
||||
func percent(str ...string) string { |
||||
if len(str) < 1 { |
||||
return "ERR" |
||||
} |
||||
if str[0] == `N/A` || len(str[0]) == 0 { |
||||
return `-` |
||||
} |
||||
|
||||
split := strings.Split(str[0], ".") |
||||
if len(split) == 2 { |
||||
digits := len(split[1]) |
||||
if digits > 2 { |
||||
digits = 2 |
||||
} |
||||
str[0] = split[0] + "." + split[1][0:digits] |
||||
} |
||||
if str[0][len(str)-1] != '%' { |
||||
str[0] += `%` |
||||
} |
||||
return str[0] |
||||
} |
||||
|
||||
// Returns value as integer (no trailing digits after a '.').
|
||||
//-----------------------------------------------------------------------------
|
||||
func integer(str ...string) string { |
||||
if len(str) < 1 { |
||||
return "ERR" |
||||
} |
||||
if str[0] == `N/A` || len(str[0]) == 0 { |
||||
return `-` |
||||
} |
||||
|
||||
// Don't strip after the '.' if we have a value such as 123.45M
|
||||
if unicode.IsDigit(rune(str[0][len(str[0])-1])) { |
||||
split := strings.Split(str[0], ".") |
||||
if len(split) == 2 { |
||||
return split[0] |
||||
} |
||||
} |
||||
return str[0] |
||||
} |
||||
@ -0,0 +1,302 @@ |
||||
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package mop |
||||
|
||||
import ( |
||||
"regexp" |
||||
"strings" |
||||
"strconv" |
||||
|
||||
"github.com/nsf/termbox-go" |
||||
) |
||||
|
||||
// LineEditor kicks in when user presses '+' or '-' to add or delete stock
|
||||
// tickers. The data structure and methods are used to collect the input
|
||||
// data and keep track of cursor movements (left, right, beginning of the
|
||||
// line, end of the line, and backspace).
|
||||
type LineEditor struct { |
||||
command rune // Keyboard command such as '+' or '-'.
|
||||
cursor int // Current cursor position within the input line.
|
||||
prompt string // Prompt string for the command.
|
||||
input string // User typed input string.
|
||||
screen *Screen // Pointer to Screen.
|
||||
quotes *Quotes // Pointer to Quotes.
|
||||
regex *regexp.Regexp // Regex to split comma-delimited input string.
|
||||
currentTextIndex int |
||||
} |
||||
|
||||
// Returns new initialized LineEditor struct.
|
||||
func NewLineEditor(screen *Screen, quotes *Quotes) *LineEditor { |
||||
return &LineEditor{ |
||||
screen: screen, |
||||
quotes: quotes, |
||||
regex: regexp.MustCompile(`[,\s]+`), |
||||
currentTextIndex: -1, |
||||
} |
||||
} |
||||
|
||||
// Prompt displays a prompt in response to '+' or '-' commands. Unknown commands
|
||||
// are simply ignored. The prompt is displayed on the 3rd line (between the market
|
||||
// data and the stock quotes).
|
||||
func (editor *LineEditor) Prompt(command rune) *LineEditor { |
||||
filterPrompt := `Set filter: ` |
||||
|
||||
if filter := editor.quotes.profile.Filter; len(filter) > 0 { |
||||
filterPrompt = `Set filter (` + filter + `): ` |
||||
} |
||||
|
||||
prompts := map[rune]string{ |
||||
'+': `Add tickers: `, '-': `Remove tickers: `, |
||||
'f': filterPrompt, |
||||
} |
||||
if prompt, ok := prompts[command]; ok { |
||||
editor.prompt = prompt |
||||
editor.command = command |
||||
|
||||
editor.screen.DrawLine(0, 3, `<white>`+editor.prompt+`</>`) |
||||
termbox.SetCursor(len(editor.prompt), 3) |
||||
termbox.Flush() |
||||
|
||||
} |
||||
|
||||
return editor |
||||
} |
||||
|
||||
func (editor *LineEditor) PromptInstrumet(command rune) *LineEditor { |
||||
filterPrompt := `Set filter: ` |
||||
|
||||
if filter := editor.quotes.profile.Filter; len(filter) > 0 { |
||||
filterPrompt = `Set filter (` + filter + `): ` |
||||
} |
||||
|
||||
prompts := map[rune]string{ |
||||
'+': `Add tickers: `, '-': `Remove tickers: `, |
||||
'f': filterPrompt, |
||||
} |
||||
if prompt, ok := prompts[command]; ok { |
||||
editor.prompt = prompt |
||||
editor.command = command |
||||
|
||||
editor.screen.DrawLine(0, 3, `<white>`+editor.prompt+`</>`) |
||||
termbox.SetCursor(len(editor.prompt), 3) |
||||
termbox.Flush() |
||||
|
||||
} |
||||
|
||||
return editor |
||||
} |
||||
|
||||
func (editor *LineEditor) insertString(str string) { |
||||
for _, ch := range str { |
||||
editor.insertCharacter(ch) |
||||
} |
||||
} |
||||
|
||||
// Handle takes over the keyboard events and dispatches them to appropriate
|
||||
// line editor handlers. As user types or edits the text cursor movements
|
||||
// are tracked in `editor.cursor` while the text itself is stored in
|
||||
// `editor.input`. The method returns true when user presses Esc (discard)
|
||||
// or Enter (process).
|
||||
func (editor *LineEditor) Handle(ev termbox.Event) bool { |
||||
defer termbox.Flush() |
||||
|
||||
switch ev.Key { |
||||
case termbox.KeyEsc: |
||||
return editor.done() |
||||
|
||||
case termbox.KeyEnter: |
||||
return editor.execute().done() |
||||
|
||||
case termbox.KeyBackspace, termbox.KeyBackspace2: |
||||
editor.deletePreviousCharacter() |
||||
|
||||
case termbox.KeyCtrlB, termbox.KeyArrowLeft: |
||||
editor.moveLeft() |
||||
|
||||
case termbox.KeyCtrlF, termbox.KeyArrowRight: |
||||
editor.moveRight() |
||||
|
||||
case termbox.KeyCtrlA: |
||||
editor.jumpToBeginning() |
||||
|
||||
case termbox.KeyCtrlE: |
||||
editor.jumpToEnd() |
||||
|
||||
case termbox.KeySpace: |
||||
editor.insertCharacter(' ') |
||||
|
||||
case termbox.KeyArrowUp: |
||||
// Add some text when the up arrow is pressed
|
||||
editor.currentTextIndex++ |
||||
if(editor.currentTextIndex >= len(editor.quotes.stocks)) { |
||||
editor.currentTextIndex = len(editor.quotes.stocks) - 1 |
||||
}/* |
||||
fullstr := editor.quotes.stocks[editor.currentTextIndex].Ticker
|
||||
indexnum, err := strconv.Atoi(fullstr[:2]) |
||||
editor.input = strconv.Itoa(indexnum) |
||||
//editor.insertString(showstr)
|
||||
editor.screen.DrawLine(len(editor.prompt), 3, editor.input+` `) |
||||
topic := "my/topic" |
||||
itemsel := editor.quotes.getitembyscode(editor.quotes.stocks[editor.currentTextIndex].Dividend) |
||||
data := map[string]interface{}{ |
||||
"scode": itemsel.Scode, |
||||
"tier": 0, |
||||
"daysback": itemsel.Daysback, |
||||
"stdprice": itemsel.Enterprice, |
||||
"name": editor.quotes.stocks[editor.currentTextIndex].Ticker, |
||||
"ed": itemsel.AnalyseDay, |
||||
} |
||||
|
||||
jsonData, err := json.Marshal(data) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
} |
||||
message := string(jsonData) |
||||
token := editor.quotes.client.Publish(topic, 0, false, message) |
||||
token.Wait() |
||||
*/ |
||||
|
||||
case termbox.KeyArrowDown: |
||||
if 0 != len(editor.input) { |
||||
indexnum, err := strconv.Atoi(editor.input) |
||||
if err == nil && indexnum > 0 && indexnum <= len(editor.quotes.totalstocks) { |
||||
editor.quotes.Sendstockgraphreq(indexnum ,true) |
||||
} |
||||
} |
||||
|
||||
default: |
||||
if ev.Ch != 0 { |
||||
editor.insertCharacter(ev.Ch) |
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *LineEditor) deletePreviousCharacter() *LineEditor { |
||||
if editor.cursor > 0 { |
||||
if editor.cursor < len(editor.input) { |
||||
// Remove character in the middle of the input string.
|
||||
editor.input = editor.input[0:editor.cursor-1] + editor.input[editor.cursor:len(editor.input)] |
||||
} else { |
||||
// Remove last input character.
|
||||
editor.input = editor.input[:len(editor.input)-1] |
||||
} |
||||
editor.screen.DrawLine(len(editor.prompt), 3, editor.input+` `) // Erase last character.
|
||||
editor.moveLeft() |
||||
} |
||||
|
||||
return editor |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *LineEditor) insertCharacter(ch rune) *LineEditor { |
||||
if editor.cursor < len(editor.input) { |
||||
// Insert the character in the middle of the input string.
|
||||
editor.input = editor.input[0:editor.cursor] + string(ch) + editor.input[editor.cursor:len(editor.input)] |
||||
} else { |
||||
// Append the character to the end of the input string.
|
||||
editor.input += string(ch) |
||||
} |
||||
editor.screen.DrawLine(len(editor.prompt), 3, editor.input) |
||||
editor.moveRight() |
||||
|
||||
return editor |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *LineEditor) moveLeft() *LineEditor { |
||||
if editor.cursor > 0 { |
||||
editor.cursor-- |
||||
termbox.SetCursor(len(editor.prompt)+editor.cursor, 3) |
||||
} |
||||
|
||||
return editor |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *LineEditor) moveRight() *LineEditor { |
||||
if editor.cursor < len(editor.input) { |
||||
editor.cursor++ |
||||
termbox.SetCursor(len(editor.prompt)+editor.cursor, 3) |
||||
} else if 0 != len(editor.input) { |
||||
indexnum, err := strconv.Atoi(editor.input) |
||||
if err == nil && indexnum > 0 && indexnum <= len(editor.quotes.totalstocks) { |
||||
editor.quotes.Sendstockgraphreq(indexnum, false) |
||||
} |
||||
} |
||||
|
||||
return editor |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *LineEditor) jumpToBeginning() *LineEditor { |
||||
editor.cursor = 0 |
||||
termbox.SetCursor(len(editor.prompt)+editor.cursor, 3) |
||||
|
||||
return editor |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *LineEditor) jumpToEnd() *LineEditor { |
||||
editor.cursor = len(editor.input) |
||||
termbox.SetCursor(len(editor.prompt)+editor.cursor, 3) |
||||
|
||||
return editor |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *LineEditor) execute() *LineEditor { |
||||
switch editor.command { |
||||
case '+': |
||||
tickers := editor.tokenize() |
||||
if len(tickers) > 0 { |
||||
if added, _ := editor.quotes.AddTickers(tickers); added > 0 { |
||||
editor.quotes.Addstockcodetofile([]string{}) |
||||
editor.screen.Draw(editor.quotes) |
||||
} |
||||
} |
||||
case '-': |
||||
tickers := editor.tokenize() |
||||
if len(tickers) > 0 { |
||||
before := len(editor.quotes.profile.Tickers) |
||||
if removed, _ := editor.quotes.RemoveTickers(tickers); removed > 0 { |
||||
editor.screen.Draw(editor.quotes) |
||||
|
||||
// Clear the lines at the bottom of the list, if any.
|
||||
after := before - removed |
||||
for i := before + 1; i > after; i-- { |
||||
editor.screen.ClearLine(0, i+4) |
||||
} |
||||
} |
||||
} |
||||
case 'f': |
||||
if len(editor.input) == 0 { |
||||
editor.input = editor.quotes.profile.Filter |
||||
} |
||||
|
||||
editor.quotes.profile.SetFilter(editor.input) |
||||
case 'F': |
||||
editor.quotes.profile.SetFilter("") |
||||
} |
||||
|
||||
return editor |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (editor *LineEditor) done() bool { |
||||
editor.screen.ClearLine(0, 3) |
||||
termbox.HideCursor() |
||||
|
||||
return true |
||||
} |
||||
|
||||
// Split by whitespace/comma to convert a string to array of tickers. Make sure
|
||||
// the string is trimmed to avoid empty tickers in the array.
|
||||
func (editor *LineEditor) tokenize() []string { |
||||
input := strings.Trim(editor.input, `, `)//strings.ToUpper(
|
||||
return editor.regex.Split(input, -1) |
||||
} |
||||
@ -0,0 +1,188 @@ |
||||
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package mop |
||||
|
||||
import ( |
||||
"regexp" |
||||
"strings" |
||||
|
||||
"github.com/nsf/termbox-go" |
||||
) |
||||
|
||||
// Markup implements some minimalistic text formatting conventions that
|
||||
// get translated to Termbox colors and attributes. To colorize a string
|
||||
// wrap it in <color-name>...</> tags. Unlike HTML each tag sets a new
|
||||
// color whereas the </> tag changes color back to default. For example:
|
||||
//
|
||||
// <green>Hello, <red>world!</>
|
||||
//
|
||||
// The color tags could be combined with the attributes: <b>...</b> for
|
||||
// bold, <u>...</u> for underline, and <r>...</r> for reverse. Unlike
|
||||
// colors the attributes require matching closing tag.
|
||||
//
|
||||
// The <right>...</right> tag is used to right align the enclosed string
|
||||
// (ex. when displaying current time in the upper right corner).
|
||||
type Markup struct { |
||||
Foreground termbox.Attribute // Foreground color.
|
||||
Background termbox.Attribute // Background color (so far always termbox.ColorDefault).
|
||||
RightAligned bool // True when the string is right aligned.
|
||||
tags map[string]termbox.Attribute // Tags to Termbox translation hash.
|
||||
regex *regexp.Regexp // Regex to identify the supported tag names.
|
||||
} |
||||
|
||||
// Creates markup to define tag to Termbox translation rules and store default
|
||||
// colors and column alignments.
|
||||
func NewMarkup(profile *Profile) *Markup { |
||||
markup := &Markup{} |
||||
|
||||
markup.tags = make(map[string]termbox.Attribute) |
||||
markup.tags[`/`] = termbox.ColorDefault |
||||
markup.tags[`black`] = termbox.ColorBlack |
||||
markup.tags[`red`] = termbox.ColorRed |
||||
markup.tags[`green`] = termbox.ColorGreen |
||||
markup.tags[`yellow`] = termbox.ColorYellow |
||||
markup.tags[`blue`] = termbox.ColorBlue |
||||
markup.tags[`magenta`] = termbox.ColorMagenta |
||||
markup.tags[`cyan`] = termbox.ColorCyan |
||||
markup.tags[`white`] = termbox.ColorWhite |
||||
markup.tags[`darkgray`] = termbox.ColorDarkGray |
||||
markup.tags[`lightred`] = termbox.ColorLightRed |
||||
markup.tags[`lightgreen`] = termbox.ColorLightGreen |
||||
markup.tags[`lightyellow`] = termbox.ColorLightYellow |
||||
markup.tags[`lightblue`] = termbox.ColorLightBlue |
||||
markup.tags[`lightmagenta`] = termbox.ColorLightMagenta |
||||
markup.tags[`lightcyan`] = termbox.ColorLightCyan |
||||
markup.tags[`lightgray`] = termbox.ColorLightGray |
||||
|
||||
markup.tags[`right`] = termbox.ColorDefault // Termbox can combine attributes and a single color using bitwise OR.
|
||||
markup.tags[`b`] = termbox.AttrBold // Attribute = 1 << (iota + 4)
|
||||
markup.tags[`u`] = termbox.AttrUnderline |
||||
markup.tags[`r`] = termbox.AttrReverse |
||||
|
||||
// Semantic markups
|
||||
markup.tags[`gain`] = markup.tags[profile.Colors.Gain] |
||||
markup.tags[`loss`] = markup.tags[profile.Colors.Loss] |
||||
markup.tags[`tag`] = markup.tags[profile.Colors.Tag] |
||||
markup.tags[`header`] = markup.tags[profile.Colors.Header] |
||||
markup.tags[`time`] = markup.tags[profile.Colors.Time] |
||||
markup.tags[`default`] = markup.tags[profile.Colors.Default] |
||||
|
||||
markup.Foreground = markup.tags[profile.Colors.Default] |
||||
|
||||
markup.Background = termbox.ColorDefault |
||||
markup.RightAligned = false |
||||
|
||||
markup.regex = markup.supportedTags() // Once we have the hash we could build the regex.
|
||||
|
||||
return markup |
||||
} |
||||
|
||||
// Tokenize works just like strings.Split() except the resulting array includes
|
||||
// the delimiters. For example, the "<green>Hello, <red>world!</>" string when
|
||||
// tokenized by tags produces the following:
|
||||
//
|
||||
// [0] "<green>"
|
||||
// [1] "Hello, "
|
||||
// [2] "<red>"
|
||||
// [3] "world!"
|
||||
// [4] "</>"
|
||||
//
|
||||
func (markup *Markup) Tokenize(str string) []string { |
||||
matches := markup.regex.FindAllStringIndex(str, -1) |
||||
strings := make([]string, 0, len(matches)) |
||||
|
||||
head, tail := 0, 0 |
||||
for _, match := range matches { |
||||
tail = match[0] |
||||
if match[1] != 0 { |
||||
if head != 0 || tail != 0 { |
||||
// Append the text between tags.
|
||||
strings = append(strings, str[head:tail]) |
||||
} |
||||
// Append the tag itmarkup.
|
||||
strings = append(strings, str[match[0]:match[1]]) |
||||
} |
||||
head = match[1] |
||||
} |
||||
|
||||
if head != len(str) && tail != len(str) { |
||||
strings = append(strings, str[head:]) |
||||
} |
||||
|
||||
return strings |
||||
} |
||||
|
||||
// IsTag returns true when the given string looks like markup tag. When the
|
||||
// tag name matches one of the markup-supported tags it gets translated to
|
||||
// relevant Termbox attributes and colors.
|
||||
func (markup *Markup) IsTag(str string) bool { |
||||
tag, open := probeForTag(str) |
||||
|
||||
if tag == `` { |
||||
return false |
||||
} |
||||
|
||||
return markup.process(tag, open) |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func (markup *Markup) process(tag string, open bool) bool { |
||||
if attribute, ok := markup.tags[tag]; ok { |
||||
switch tag { |
||||
case `right`: |
||||
markup.RightAligned = open // On for <right>, off for </right>.
|
||||
default: |
||||
if open { |
||||
if attribute >= termbox.AttrBold { |
||||
markup.Foreground |= attribute // Set the Termbox attribute.
|
||||
} else { |
||||
markup.Foreground = attribute // Set the Termbox color.
|
||||
} |
||||
} else { |
||||
if attribute >= termbox.AttrBold { |
||||
markup.Foreground &= ^attribute // Clear the Termbox attribute.
|
||||
} else { |
||||
markup.Foreground = markup.tags[`default`] |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
return true |
||||
} |
||||
|
||||
// supportedTags returns regular expression that matches all possible tags
|
||||
// supported by the markup, i.e. </?black>|</?red>| ... |<?b>| ... |</?right>
|
||||
func (markup *Markup) supportedTags() *regexp.Regexp { |
||||
arr := []string{} |
||||
|
||||
for tag := range markup.tags { |
||||
arr = append(arr, `</?`+tag+`>`) |
||||
} |
||||
|
||||
return regexp.MustCompile(strings.Join(arr, `|`)) |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func probeForTag(str string) (string, bool) { |
||||
if len(str) > 2 && str[0:1] == `<` && str[len(str)-1:] == `>` { |
||||
return extractTagName(str), str[1:2] != `/` |
||||
} |
||||
|
||||
return ``, false |
||||
} |
||||
|
||||
// Extract tag name from the given tag, i.e. `<hello>` => `hello`.
|
||||
func extractTagName(str string) string { |
||||
if len(str) < 3 { |
||||
return `` |
||||
} else if str[1:2] != `/` { |
||||
return str[1 : len(str)-1] |
||||
} else if len(str) > 3 { |
||||
return str[2 : len(str)-1] |
||||
} |
||||
|
||||
return `/` |
||||
} |
||||
@ -0,0 +1,37 @@ |
||||
# 全局配置 |
||||
user www; |
||||
worker_processes auto; |
||||
error_log /www/wwwlogs/nginx_error.log; |
||||
pid /var/run/nginx.pid; |
||||
|
||||
# 事件模块配置 |
||||
events { |
||||
worker_connections 1024; |
||||
} |
||||
|
||||
# HTTP模块配置 |
||||
http { |
||||
include mime.types; |
||||
default_type application/octet-stream; |
||||
|
||||
# 日志格式 |
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' |
||||
'$status $body_bytes_sent "$http_referer" ' |
||||
'"$http_user_agent" "$http_x_forwarded_for"'; |
||||
|
||||
access_log off; |
||||
|
||||
# 代理服务器配置 |
||||
server { |
||||
listen 80; |
||||
server_name racknerd.jatus.top; |
||||
|
||||
location / { |
||||
proxy_pass https://query1.finance.yahoo.com/; |
||||
#proxy_set_header Host $host; |
||||
#proxy_set_header X-Real-IP $remote_addr; |
||||
#proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
||||
proxy_ssl_verify off; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,235 @@ |
||||
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package mop |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"io/ioutil" |
||||
"sort" |
||||
"strings" |
||||
|
||||
"github.com/Knetic/govaluate" |
||||
) |
||||
|
||||
const defaultGainColor = "green" |
||||
const defaultLossColor = "red" |
||||
const defaultTagColor = "yellow" |
||||
const defaultHeaderColor = "lightgray" |
||||
const defaultTimeColor = "lightgray" |
||||
const defaultColor = "lightgray" |
||||
|
||||
// Profile manages Mop program settings as defined by user (ex. list of
|
||||
// stock tickers). The settings are serialized using JSON and saved in
|
||||
// the ~/.moprc file.
|
||||
type Profile struct { |
||||
Tickers []string // List of stock tickers to display.
|
||||
MarketRefresh int // Time interval to refresh market data.
|
||||
QuotesRefresh int // Time interval to refresh stock quotes.
|
||||
SortColumn int // Column number by which we sort stock quotes.
|
||||
Ascending bool // True when sort order is ascending.
|
||||
Grouped bool // True when stocks are grouped by advancing/declining.
|
||||
Filter string // Filter in human form
|
||||
UpDownJump int // Number of lines to go up/down when scrolling.
|
||||
Colors struct { // User defined colors
|
||||
Gain string |
||||
Loss string |
||||
Tag string |
||||
Header string |
||||
Time string |
||||
Default string |
||||
} |
||||
filterExpression *govaluate.EvaluableExpression // The filter as a govaluate expression
|
||||
selectedColumn int // Stores selected column number when the column editor is active.
|
||||
filename string // Path to the file in which the configuration is stored
|
||||
mode string |
||||
date_json []string |
||||
|
||||
} |
||||
|
||||
// Checks if a string represents a supported color or not.
|
||||
func IsSupportedColor(colorName string) bool { |
||||
switch colorName { |
||||
case |
||||
"black", |
||||
"red", |
||||
"green", |
||||
"yellow", |
||||
"blue", |
||||
"magenta", |
||||
"cyan", |
||||
"white", |
||||
"darkgray", |
||||
"lightred", |
||||
"lightgreen", |
||||
"lightyellow", |
||||
"lightblue", |
||||
"lightmagenta", |
||||
"lightcyan", |
||||
"lightgray": |
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// Creates the profile and attempts to load the settings from ~/.moprc file.
|
||||
// If the file is not there it gets created with default values.
|
||||
func NewProfile(filename string) (*Profile, error) { |
||||
profile := &Profile{filename: filename} |
||||
data, err := ioutil.ReadFile(filename) |
||||
if err == nil { |
||||
err = json.Unmarshal(data, profile) |
||||
|
||||
if err == nil { |
||||
InitColor(&profile.Colors.Gain, defaultGainColor) |
||||
InitColor(&profile.Colors.Loss, defaultLossColor) |
||||
InitColor(&profile.Colors.Tag, defaultTagColor) |
||||
InitColor(&profile.Colors.Header, defaultHeaderColor) |
||||
InitColor(&profile.Colors.Time, defaultTimeColor) |
||||
InitColor(&profile.Colors.Default, defaultColor) |
||||
|
||||
profile.SetFilter(profile.Filter) |
||||
} |
||||
} else { |
||||
profile.InitDefaultProfile() |
||||
err = nil |
||||
} |
||||
profile.selectedColumn = -1 |
||||
|
||||
if profile.UpDownJump < 1 { |
||||
profile.UpDownJump = 10 |
||||
} |
||||
|
||||
return profile, err |
||||
} |
||||
|
||||
// Initializes a profile with the default values
|
||||
func (profile *Profile) InitDefaultProfile() { |
||||
profile.MarketRefresh = 12 // Market data gets fetched every 12s (5 times per minute).
|
||||
profile.QuotesRefresh = 3 // Stock quotes get updated every 5s (12 times per minute).
|
||||
profile.Grouped = false // Stock quotes are *not* grouped by advancing/declining.
|
||||
profile.Tickers = []string{`AAPL`, `C`, `GOOG`, `IBM`, `KO`, `ORCL`, `V`} |
||||
profile.SortColumn = 0 // Stock quotes are sorted by ticker name.
|
||||
profile.Ascending = true // A to Z.
|
||||
profile.Filter = "" |
||||
profile.UpDownJump = 10 |
||||
profile.Colors.Gain = defaultGainColor |
||||
profile.Colors.Loss = defaultLossColor |
||||
profile.Colors.Tag = defaultTagColor |
||||
profile.Colors.Header = defaultHeaderColor |
||||
profile.Colors.Time = defaultTimeColor |
||||
profile.Colors.Default = defaultColor |
||||
profile.Save() |
||||
} |
||||
|
||||
//add mode for profile
|
||||
func (profile *Profile) SetMode(mode string) { |
||||
profile.mode = mode |
||||
} |
||||
|
||||
func (profile *Profile) AddDate(date string) { |
||||
profile.date_json = append(profile.date_json, date) |
||||
} |
||||
|
||||
// Initializes a color to the given string, or to the default value if the given
|
||||
// string does not represent a supported color.
|
||||
func InitColor(color *string, defaultValue string) { |
||||
*color = strings.ToLower(*color) |
||||
if !IsSupportedColor(*color) { |
||||
*color = defaultValue |
||||
} |
||||
} |
||||
|
||||
// Save serializes settings using JSON and saves them in ~/.moprc file.
|
||||
func (profile *Profile) Save() error { |
||||
data, err := json.MarshalIndent(profile, "", " ") |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return ioutil.WriteFile(profile.filename, data, 0644) |
||||
} |
||||
|
||||
// AddTickers updates the list of existing tickers to add the new ones making
|
||||
// sure there are no duplicates.
|
||||
func (profile *Profile) AddTickers(tickers []string) (added int, err error) { |
||||
added, err = 0, nil |
||||
existing := make(map[string]bool) |
||||
|
||||
// Build a hash of existing tickers so we could look it up quickly.
|
||||
for _, ticker := range profile.Tickers { |
||||
existing[ticker] = true |
||||
} |
||||
|
||||
// Iterate over the list of new tickers excluding the ones that
|
||||
// already exist.
|
||||
for _, ticker := range tickers { |
||||
if _, found := existing[ticker]; !found { |
||||
profile.Tickers = append(profile.Tickers, ticker) |
||||
added++ |
||||
} |
||||
} |
||||
|
||||
if added > 0 { |
||||
sort.Strings(profile.Tickers) |
||||
err = profile.Save() |
||||
} |
||||
|
||||
return |
||||
} |
||||
|
||||
// RemoveTickers removes requested stock tickers from the list we track.
|
||||
func (profile *Profile) RemoveTickers(tickers []string) (removed int, err error) { |
||||
removed, err = 0, nil |
||||
for _, ticker := range tickers { |
||||
for i, existing := range profile.Tickers { |
||||
if ticker == existing { |
||||
// Requested ticker is there: remove i-th slice item.
|
||||
profile.Tickers = append(profile.Tickers[:i], profile.Tickers[i+1:]...) |
||||
removed++ |
||||
} |
||||
} |
||||
} |
||||
|
||||
if removed > 0 { |
||||
err = profile.Save() |
||||
} |
||||
|
||||
return |
||||
} |
||||
|
||||
// Reorder gets called by the column editor to either reverse sorting order
|
||||
// for the current column, or to pick another sort column.
|
||||
func (profile *Profile) Reorder() error { |
||||
if profile.selectedColumn == profile.SortColumn { |
||||
profile.Ascending = !profile.Ascending // Reverse sort order.
|
||||
} else { |
||||
profile.SortColumn = profile.selectedColumn // Pick new sort column.
|
||||
} |
||||
return profile.Save() |
||||
} |
||||
|
||||
// Regroup flips the flag that controls whether the stock quotes are grouped
|
||||
// by advancing/declining issues.
|
||||
func (profile *Profile) Regroup() error { |
||||
profile.Grouped = !profile.Grouped |
||||
return profile.Save() |
||||
} |
||||
|
||||
// SetFilter creates a govaluate.EvaluableExpression.
|
||||
func (profile *Profile) SetFilter(filter string) { |
||||
if len(filter) > 0 { |
||||
var err error |
||||
profile.filterExpression, err = govaluate.NewEvaluableExpression(filter) |
||||
|
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
} else if len(filter) == 0 && profile.filterExpression != nil { |
||||
profile.filterExpression = nil |
||||
} |
||||
|
||||
profile.Filter = filter |
||||
} |
||||
@ -0,0 +1,341 @@ |
||||
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package mop |
||||
|
||||
import ( |
||||
"fmt" |
||||
"strconv" |
||||
"strings" |
||||
"time" |
||||
"unicode" |
||||
|
||||
"github.com/nsf/termbox-go" |
||||
//"github.com/olekukonko/tablewriter"
|
||||
) |
||||
|
||||
// Screen is thin wrapper around Termbox library to provide basic display
|
||||
// capabilities as required by Mop.
|
||||
type Screen struct { |
||||
width int // Current number of columns.
|
||||
height int // Current number of rows.
|
||||
cleared bool // True after the screens gets cleared.
|
||||
layout *Layout // Pointer to layout (gets created by screen).
|
||||
markup *Markup // Pointer to markup processor (gets created by screen).
|
||||
pausedAt *time.Time // Timestamp of the pause request or nil if none.
|
||||
offset int // Offset for scolling
|
||||
headerLine int // Line number of header for scroll feature
|
||||
max int // highest offset
|
||||
selectindex int // selected index of the list
|
||||
myseltext string |
||||
} |
||||
|
||||
func isChineseChar(r rune) bool { |
||||
return unicode.Is(unicode.Han, r) |
||||
} |
||||
|
||||
// Initializes Termbox, creates screen along with layout and markup, and
|
||||
// calculates current screen dimensions. Once initialized the screen is
|
||||
// ready for display.
|
||||
func NewScreen(profile *Profile) *Screen { |
||||
if err := termbox.Init(); err != nil { |
||||
panic(err) |
||||
} |
||||
screen := &Screen{} |
||||
screen.layout = NewLayout() |
||||
screen.markup = NewMarkup(profile) |
||||
screen.offset = 0 |
||||
screen.selectindex = 4 |
||||
|
||||
return screen.Resize() |
||||
} |
||||
|
||||
// Close gets called upon program termination to close the Termbox.
|
||||
func (screen *Screen) Close() *Screen { |
||||
termbox.Close() |
||||
|
||||
return screen |
||||
} |
||||
|
||||
// Resize gets called when the screen is being resized. It recalculates screen
|
||||
// dimensions and requests to clear the screen on next update.
|
||||
func (screen *Screen) Resize() *Screen { |
||||
screen.width, screen.height = termbox.Size() |
||||
screen.cleared = false |
||||
|
||||
return screen |
||||
} |
||||
|
||||
// Pause is a toggle function that either creates a timestamp of the pause
|
||||
// request or resets it to nil.
|
||||
func (screen *Screen) Pause(pause bool) *Screen { |
||||
if pause { |
||||
screen.pausedAt = new(time.Time) |
||||
*screen.pausedAt = time.Now() |
||||
} else { |
||||
screen.pausedAt = nil |
||||
} |
||||
|
||||
return screen |
||||
} |
||||
|
||||
// Clear makes the entire screen blank using default background color.
|
||||
func (screen *Screen) Clear() *Screen { |
||||
termbox.Clear(termbox.ColorDefault, termbox.ColorDefault) |
||||
screen.cleared = true |
||||
|
||||
return screen |
||||
} |
||||
|
||||
// ClearLine erases the contents of the line starting from (x,y) coordinate
|
||||
// till the end of the line.
|
||||
func (screen *Screen) ClearLine(x int, y int) *Screen { |
||||
for i := x; i < screen.width; i++ { |
||||
termbox.SetCell(i, y, ' ', termbox.ColorDefault, termbox.ColorDefault) |
||||
} |
||||
termbox.Flush() |
||||
|
||||
return screen |
||||
} |
||||
|
||||
// Increase the offset for scrolling feature by n
|
||||
// Takes number of tickers as max, so not scrolling down forever
|
||||
func (screen *Screen) IncreaseOffset(n int) { |
||||
if screen.offset+n <= screen.max { |
||||
screen.offset += n |
||||
} else if screen.max > screen.height { |
||||
screen.offset = screen.max |
||||
} |
||||
} |
||||
|
||||
// Decrease the offset for scrolling feature by n
|
||||
func (screen *Screen) DecreaseOffset(n int) { |
||||
if screen.offset > n { |
||||
screen.offset -= n |
||||
} else { |
||||
screen.offset = 0 |
||||
} |
||||
} |
||||
|
||||
func (screen *Screen) Selectmoveup(quotes *Quotes) { |
||||
if screen.selectindex >= 5 { |
||||
screen.selectindex -= 1 |
||||
} |
||||
if screen.selectindex == 4 { |
||||
//screen.DrawLine(0, 3, `<white>`+" "+`</>`)
|
||||
}else{ |
||||
//screen.DrawLine(0, 3, `<white>`+quotes.stocks[screen.selectindex-5].Ticker+`</>`)
|
||||
} |
||||
} |
||||
|
||||
func (screen *Screen) Selectmovedown(quotes *Quotes) { |
||||
if screen.selectindex < len(quotes.stocks) + 4 { |
||||
screen.selectindex += 1 |
||||
} |
||||
//screen.DrawLine(0, 3, `<white>`+quotes.stocks[screen.selectindex-5].Ticker+`</>`)
|
||||
} |
||||
|
||||
func (screen *Screen) Setline3(mytext string) { |
||||
//strSlice := strings.Split(screen.myseltext, " ")
|
||||
if screen.selectindex == 4 { |
||||
screen.DrawLine(0, 3, `<white>`+" "+`</>`) |
||||
}else{ |
||||
screen.DrawLine(0, 3, `<white>`+ mytext +`</>`) |
||||
} |
||||
} |
||||
|
||||
func (screen *Screen) Getline3() string{ |
||||
strSlice := strings.Split(screen.myseltext, " ") |
||||
if screen.selectindex == 4 { |
||||
//screen.DrawLine(0, 3, `<white>`+" "+`</>`)
|
||||
return "" |
||||
}else{ |
||||
//screen.DrawLine(0, 3, `<white>`+ strSlice[0] +`</>`)
|
||||
return strSlice[0] |
||||
} |
||||
} |
||||
|
||||
func (screen *Screen) Selectindex() int { |
||||
return screen.selectindex - 4 |
||||
} |
||||
|
||||
func (screen *Screen) Selectpreset() int { |
||||
return screen.selectindex - 4 |
||||
} |
||||
|
||||
func (screen *Screen) ScrollTop() { |
||||
screen.offset = 0 |
||||
} |
||||
|
||||
func (screen *Screen) ScrollBottom() { |
||||
if screen.max > screen.height { |
||||
screen.offset = screen.max |
||||
} |
||||
} |
||||
|
||||
func (screen *Screen) DrawOldQuotes(quotes *Quotes) { |
||||
screen.draw(screen.layout.Quotes(quotes), true) |
||||
termbox.Flush() |
||||
} |
||||
|
||||
func (screen *Screen) DrawOldMarket(market *Market) { |
||||
screen.draw(screen.layout.Market(market), false) |
||||
termbox.Flush() |
||||
} |
||||
|
||||
|
||||
|
||||
// Draw accepts variable number of arguments and knows how to display the
|
||||
// market data, stock quotes, current time, and an arbitrary string.
|
||||
func (screen *Screen) Draw(objects ...interface{}) *Screen { |
||||
zonename, _ := time.Now().In(time.Local).Zone() |
||||
if screen.pausedAt != nil { |
||||
defer screen.DrawLine(0, 0, `<right><r>`+screen.pausedAt.Format(`3:04:05pm `+zonename)+`</r></right>`) |
||||
} |
||||
for _, ptr := range objects { |
||||
switch ptr.(type) { |
||||
case *Market: |
||||
object := ptr.(*Market) |
||||
screen.draw(screen.layout.Market(object.Fetch()), false) |
||||
case *Quotes: |
||||
object := ptr.(*Quotes) |
||||
screen.draw(screen.layout.Quotes(object.Fetch()), true) |
||||
case time.Time: |
||||
timestamp := ptr.(time.Time).Format(`3:04:05pm ` + zonename) |
||||
screen.DrawLine(0, 0, `<right><time>`+timestamp+`</></right>`) |
||||
default: |
||||
screen.draw(ptr.(string), false)
|
||||
} |
||||
} |
||||
|
||||
termbox.Flush() |
||||
|
||||
return screen |
||||
} |
||||
|
||||
// DrawLine takes the incoming string, tokenizes it to extract markup
|
||||
// elements, and displays it all starting at (x,y) location.
|
||||
|
||||
// DrawLineFlush gives the option to flush screen after drawing
|
||||
|
||||
// wrapper for DrawLineFlush
|
||||
func (screen *Screen) DrawLine(x int, y int, str string) { |
||||
screen.DrawLineFlush(x, y, str, true, 0) |
||||
} |
||||
|
||||
func (screen *Screen) DrawLineFlush(x int, y int, str string, flush bool, swap int) { |
||||
start, column := 0, 0 |
||||
//fmt.Println(str)
|
||||
for _, token := range screen.markup.Tokenize(str) { |
||||
// First check if it's a tag. Tags are eaten up and not displayed.
|
||||
if screen.markup.IsTag(token) { |
||||
continue |
||||
} |
||||
|
||||
// Here comes the actual text: display it one character at a time.
|
||||
//fmt.Println(token)
|
||||
for i, char := range token { |
||||
if !screen.markup.RightAligned { |
||||
start = x + column |
||||
column++ |
||||
if char == '|'{ |
||||
//bg = screen.markup.Background //termbox.ColorLightRed
|
||||
} |
||||
if isChineseChar(char) { |
||||
column ++ |
||||
//bg, fg = fg, bg
|
||||
} |
||||
} else { |
||||
start = screen.width - len(token) + i //- 15
|
||||
} |
||||
//fmt.Println(string(char))
|
||||
bg := screen.markup.Background
|
||||
fg := screen.markup.Foreground |
||||
if swap == 1 { |
||||
termbox.SetCell(start, y, char, bg, fg) |
||||
} else{ |
||||
termbox.SetCell(start, y, char, fg, bg) |
||||
} |
||||
//bg, fg = fg, bg
|
||||
} |
||||
} |
||||
if flush { |
||||
termbox.Flush() |
||||
} |
||||
} |
||||
|
||||
// Underlying workhorse function that takes multiline string, splits it into
|
||||
// lines, and displays them row by row.
|
||||
func (screen *Screen) draw(str string, offset bool) { |
||||
if !screen.cleared { |
||||
screen.Clear() |
||||
} |
||||
var allLines []string |
||||
drewHeading := false |
||||
|
||||
screen.width, screen.height = termbox.Size() |
||||
|
||||
tempFormat := "%" + strconv.Itoa(screen.width) + "s" |
||||
blankLine := fmt.Sprintf(tempFormat, "") |
||||
allLines = strings.Split(str, "\n") |
||||
|
||||
if offset { |
||||
screen.max = len(allLines) - screen.height + screen.headerLine |
||||
} |
||||
|
||||
// Write the lines being updated.
|
||||
for row := 0; row < len(allLines); row++ { |
||||
if offset { |
||||
// Did we draw the underlined heading row? This is a crude
|
||||
// check, but--see comments below...
|
||||
// --- Heading row only appears for quotes, so offset is true
|
||||
if !drewHeading { |
||||
if strings.Contains(allLines[row], "Ticker") && |
||||
strings.Contains(allLines[row], "Last") && |
||||
strings.Contains(allLines[row], "Change") { |
||||
drewHeading = true |
||||
screen.headerLine = row |
||||
screen.DrawLine(0, row, allLines[row]) |
||||
// move on to the point to offset to
|
||||
row += screen.offset |
||||
} |
||||
} else { |
||||
// only write the necessary lines
|
||||
if row <= len(allLines) && |
||||
row > screen.headerLine { |
||||
swap := 0 |
||||
if row == screen.selectindex{ |
||||
swap = 1 |
||||
//screen.DrawLine(0, 3, `<white>`+allLines[row]+`</>`)
|
||||
screen.myseltext = allLines[row] |
||||
} |
||||
screen.DrawLineFlush(0, row-screen.offset, allLines[row], false, swap) |
||||
} else if row > len(allLines) + 1 { |
||||
row = len(allLines) |
||||
} |
||||
} |
||||
} else { |
||||
screen.DrawLineFlush(0, row, allLines[row], false, 0) |
||||
} |
||||
} |
||||
// If the quotes lines in this cycle are shorter than in the previous
|
||||
// cycles, e.g., because a filter was just applied, then one or more
|
||||
// lines from the previous cycles will not be cleared. Since the
|
||||
// incoming lines don't mark explicitly whether they are part of the
|
||||
// market summary or quotes, we can't check whether quotes were updated
|
||||
// in a way that is robust for code changes. This is a simple test: if
|
||||
// we drew the heading row ("Ticker Last Change..."), then we are
|
||||
// updating the quotes section in this cycle, and we should pad the
|
||||
// quotes section with blank lines. If we didn't draw the heading row,
|
||||
// then we probably only updated the market summary at the top in this
|
||||
// cycle. In that case, padding with blank lines would overwrite the
|
||||
// stocks list.)
|
||||
if drewHeading { |
||||
for i := len(allLines) - 1 - screen.offset; i < screen.height; i++ { |
||||
if i > screen.headerLine { |
||||
screen.DrawLine(0, i, blankLine) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,263 @@ |
||||
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package mop |
||||
|
||||
import ( |
||||
`sort` |
||||
`strconv` |
||||
`strings` |
||||
) |
||||
|
||||
// Sorter gets called to sort stock quotes by one of the columns. The
|
||||
// setup is rather lengthy; there should probably be more concise way
|
||||
// that uses reflection and avoids hardcoding the column names.
|
||||
type Sorter struct { |
||||
profile *Profile // Pointer to where we store sort column and order.
|
||||
} |
||||
|
||||
type sortable []Stock |
||||
|
||||
func (list sortable) Len() int { return len(list) } |
||||
func (list sortable) Swap(i, j int) { list[i], list[j] = list[j], list[i] } |
||||
|
||||
type byTickerAsc struct{ sortable } |
||||
type byLastTradeAsc struct{ sortable } |
||||
type byChangeAsc struct{ sortable } |
||||
type byChangePctAsc struct{ sortable } |
||||
type byOpenAsc struct{ sortable } |
||||
type byLowAsc struct{ sortable } |
||||
type byHighAsc struct{ sortable } |
||||
type byLow52Asc struct{ sortable } |
||||
type byHigh52Asc struct{ sortable } |
||||
type byVolumeAsc struct{ sortable } |
||||
type byAvgVolumeAsc struct{ sortable } |
||||
type byPeRatioAsc struct{ sortable } |
||||
type byDividendAsc struct{ sortable } |
||||
type byYieldAsc struct{ sortable } |
||||
type byMarketCapAsc struct{ sortable } |
||||
type byPreOpenAsc struct{ sortable } |
||||
type byAfterHoursAsc struct{ sortable } |
||||
|
||||
type byTickerDesc struct{ sortable } |
||||
type byLastTradeDesc struct{ sortable } |
||||
type byChangeDesc struct{ sortable } |
||||
type byChangePctDesc struct{ sortable } |
||||
type byOpenDesc struct{ sortable } |
||||
type byLowDesc struct{ sortable } |
||||
type byHighDesc struct{ sortable } |
||||
type byLow52Desc struct{ sortable } |
||||
type byHigh52Desc struct{ sortable } |
||||
type byVolumeDesc struct{ sortable } |
||||
type byAvgVolumeDesc struct{ sortable } |
||||
type byPeRatioDesc struct{ sortable } |
||||
type byDividendDesc struct{ sortable } |
||||
type byYieldDesc struct{ sortable } |
||||
type byMarketCapDesc struct{ sortable } |
||||
type byPreOpenDesc struct{ sortable } |
||||
type byAfterHoursDesc struct{ sortable } |
||||
|
||||
func (list byTickerAsc) Less(i, j int) bool { |
||||
return list.sortable[i].Ticker < list.sortable[j].Ticker |
||||
} |
||||
func (list byLastTradeAsc) Less(i, j int) bool { |
||||
return list.sortable[i].LastTrade < list.sortable[j].LastTrade |
||||
} |
||||
func (list byChangeAsc) Less(i, j int) bool { |
||||
return c(list.sortable[i].Change) < c(list.sortable[j].Change) |
||||
} |
||||
func (list byChangePctAsc) Less(i, j int) bool { |
||||
return c(list.sortable[i].ChangePct) < c(list.sortable[j].ChangePct) |
||||
} |
||||
func (list byOpenAsc) Less(i, j int) bool { |
||||
return list.sortable[i].Open < list.sortable[j].Open |
||||
} |
||||
func (list byLowAsc) Less(i, j int) bool {
|
||||
return list.sortable[i].Low < list.sortable[j].Low |
||||
} |
||||
func (list byHighAsc) Less(i, j int) bool { |
||||
return list.sortable[i].High < list.sortable[j].High |
||||
} |
||||
func (list byLow52Asc) Less(i, j int) bool { |
||||
return list.sortable[i].Low52 < list.sortable[j].Low52 |
||||
} |
||||
func (list byHigh52Asc) Less(i, j int) bool { |
||||
return list.sortable[i].High52 < list.sortable[j].High52 |
||||
} |
||||
func (list byVolumeAsc) Less(i, j int) bool { |
||||
return m(list.sortable[i].Volume) < m(list.sortable[j].Volume) |
||||
} |
||||
func (list byAvgVolumeAsc) Less(i, j int) bool { |
||||
return m(list.sortable[i].AvgVolume) < m(list.sortable[j].AvgVolume) |
||||
} |
||||
func (list byPeRatioAsc) Less(i, j int) bool { |
||||
return list.sortable[i].PeRatio < list.sortable[j].PeRatio |
||||
} |
||||
func (list byDividendAsc) Less(i, j int) bool { |
||||
return list.sortable[i].Dividend < list.sortable[j].Dividend |
||||
} |
||||
func (list byYieldAsc) Less(i, j int) bool { |
||||
return list.sortable[i].Yield < list.sortable[j].Yield |
||||
} |
||||
func (list byMarketCapAsc) Less(i, j int) bool { |
||||
return m(list.sortable[i].MarketCap) < m(list.sortable[j].MarketCap) |
||||
} |
||||
func (list byPreOpenAsc) Less(i, j int) bool { |
||||
return c(list.sortable[i].PreOpen) < c(list.sortable[j].PreOpen) |
||||
} |
||||
func (list byAfterHoursAsc) Less(i, j int) bool { |
||||
return c(list.sortable[i].AfterHours) < c(list.sortable[j].AfterHours) |
||||
} |
||||
|
||||
|
||||
func (list byTickerDesc) Less(i, j int) bool { |
||||
return list.sortable[j].Ticker < list.sortable[i].Ticker |
||||
} |
||||
func (list byLastTradeDesc) Less(i, j int) bool { |
||||
return list.sortable[j].LastTrade < list.sortable[i].LastTrade |
||||
} |
||||
func (list byChangeDesc) Less(i, j int) bool { |
||||
return c(list.sortable[j].Change) < c(list.sortable[i].Change) |
||||
} |
||||
func (list byChangePctDesc) Less(i, j int) bool { |
||||
return c(list.sortable[j].ChangePct) < c(list.sortable[i].ChangePct) |
||||
} |
||||
func (list byOpenDesc) Less(i, j int) bool { |
||||
return list.sortable[j].Open < list.sortable[i].Open |
||||
} |
||||
func (list byLowDesc) Less(i, j int) bool {
|
||||
return list.sortable[j].Low < list.sortable[i].Low |
||||
} |
||||
func (list byHighDesc) Less(i, j int) bool { |
||||
return list.sortable[j].High < list.sortable[i].High |
||||
} |
||||
func (list byLow52Desc) Less(i, j int) bool { |
||||
return list.sortable[j].Low52 < list.sortable[i].Low52 |
||||
} |
||||
func (list byHigh52Desc) Less(i, j int) bool { |
||||
return list.sortable[j].High52 < list.sortable[i].High52 |
||||
} |
||||
func (list byVolumeDesc) Less(i, j int) bool { |
||||
return m(list.sortable[j].Volume) < m(list.sortable[i].Volume) |
||||
} |
||||
func (list byAvgVolumeDesc) Less(i, j int) bool { |
||||
return m(list.sortable[j].AvgVolume) < m(list.sortable[i].AvgVolume) |
||||
} |
||||
func (list byPeRatioDesc) Less(i, j int) bool { |
||||
return list.sortable[j].PeRatio < list.sortable[i].PeRatio |
||||
} |
||||
func (list byDividendDesc) Less(i, j int) bool { |
||||
return list.sortable[j].Dividend < list.sortable[i].Dividend |
||||
} |
||||
func (list byYieldDesc) Less(i, j int) bool { |
||||
return list.sortable[j].Yield < list.sortable[i].Yield |
||||
} |
||||
func (list byMarketCapDesc) Less(i, j int) bool { |
||||
return m(list.sortable[j].MarketCap) < m(list.sortable[i].MarketCap) |
||||
} |
||||
func (list byPreOpenDesc) Less(i, j int) bool { |
||||
return c(list.sortable[j].PreOpen) < c(list.sortable[i].PreOpen) |
||||
} |
||||
func (list byAfterHoursDesc) Less(i, j int) bool { |
||||
return c(list.sortable[j].AfterHours) < c(list.sortable[i].AfterHours) |
||||
} |
||||
|
||||
// Returns new Sorter struct.
|
||||
func NewSorter(profile *Profile) *Sorter { |
||||
return &Sorter{ |
||||
profile: profile, |
||||
} |
||||
} |
||||
|
||||
// SortByCurrentColumn builds a list of sort interface based on current sort
|
||||
// order, then calls sort.Sort to do the actual job.
|
||||
func (sorter *Sorter) SortByCurrentColumn(stocks []Stock) *Sorter { |
||||
var interfaces []sort.Interface |
||||
|
||||
if sorter.profile.Ascending { |
||||
interfaces = []sort.Interface{ |
||||
byTickerAsc{stocks}, |
||||
byLastTradeAsc{stocks}, |
||||
byChangeAsc{stocks}, |
||||
byChangePctAsc{stocks}, |
||||
byOpenAsc{stocks}, |
||||
byLowAsc{stocks}, |
||||
byHighAsc{stocks}, |
||||
byLow52Asc{stocks}, |
||||
byHigh52Asc{stocks}, |
||||
byVolumeAsc{stocks}, |
||||
byAvgVolumeAsc{stocks}, |
||||
byPeRatioAsc{stocks}, |
||||
byDividendAsc{stocks}, |
||||
byYieldAsc{stocks}, |
||||
byMarketCapAsc{stocks}, |
||||
byPreOpenAsc{stocks}, |
||||
byAfterHoursAsc{stocks}, |
||||
} |
||||
} else { |
||||
interfaces = []sort.Interface{ |
||||
byTickerDesc{stocks}, |
||||
byLastTradeDesc{stocks}, |
||||
byChangeDesc{stocks}, |
||||
byChangePctDesc{stocks}, |
||||
byOpenDesc{stocks}, |
||||
byLowDesc{stocks}, |
||||
byHighDesc{stocks}, |
||||
byLow52Desc{stocks}, |
||||
byHigh52Desc{stocks}, |
||||
byVolumeDesc{stocks}, |
||||
byAvgVolumeDesc{stocks}, |
||||
byPeRatioDesc{stocks}, |
||||
byDividendDesc{stocks}, |
||||
byYieldDesc{stocks}, |
||||
byMarketCapDesc{stocks}, |
||||
byPreOpenDesc{stocks}, |
||||
byAfterHoursDesc{stocks}, |
||||
} |
||||
} |
||||
|
||||
sort.Sort(interfaces[sorter.profile.SortColumn]) |
||||
|
||||
return sorter |
||||
} |
||||
|
||||
// The same exact method is used to sort by $Change and Change%. In both cases
|
||||
// we sort by the value of Change% so that multiple $0.00s get sorted properly.
|
||||
func c(str string) float32 { |
||||
c := "$" |
||||
for _, v := range currencies { |
||||
if strings.Contains(str,v) { |
||||
c = v |
||||
} |
||||
} |
||||
trimmed := strings.Replace(strings.Trim(str, ` %`), c, ``, 1) |
||||
value, _ := strconv.ParseFloat(trimmed, 32) |
||||
return float32(value) |
||||
} |
||||
|
||||
// When sorting by the market value we must first convert 42B etc. notations
|
||||
// to proper numeric values.
|
||||
func m(str string) float32 { |
||||
if len(str) == 0 { |
||||
return 0 |
||||
} |
||||
|
||||
multiplier := 1.0 |
||||
|
||||
switch str[len(str)-1:] { // Check the last character.
|
||||
case `T`: |
||||
multiplier = 1000000000000.0 |
||||
case `B`: |
||||
multiplier = 1000000000.0 |
||||
case `M`: |
||||
multiplier = 1000000.0 |
||||
case `K`: |
||||
multiplier = 1000.0 |
||||
} |
||||
|
||||
trimmed := strings.Trim(str, ` $TBMK`) // Get rid of non-numeric characters.
|
||||
value, _ := strconv.ParseFloat(trimmed, 32) |
||||
|
||||
return float32(value * multiplier) |
||||
} |
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,102 @@ |
||||
// Copyright (c) 2013-2023 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package mop |
||||
|
||||
import ( |
||||
"io/ioutil" |
||||
"net/http" |
||||
"strings" |
||||
//"net/url"
|
||||
"log" |
||||
) |
||||
|
||||
const crumbURL = "https://query1.finance.yahoo.com/v1/test/getcrumb" |
||||
const cookieURL = "https://login.yahoo.com" |
||||
const userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/113.0" |
||||
const substitute = "https://racknerd.jatus.top" |
||||
|
||||
func fetchCrumb(cookies string, subs string) (string, string) { |
||||
substituteUrl := crumbURL |
||||
if len(subs) > 0 { |
||||
substituteUrl = strings.Replace(substituteUrl, "https://query1.finance.yahoo.com", substitute, -1) // 替换为你想要的新 URL
|
||||
} |
||||
client := http.Client{} |
||||
request, err := http.NewRequest("GET", substituteUrl, nil) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
request.Header = http.Header{ |
||||
"Accept": {"*/*"}, |
||||
"Accept-Encoding": {"gzip, deflate, br"}, |
||||
"Accept-Language": {"en-US,en;q=0.5"}, |
||||
"Connection": {"keep-alive"}, |
||||
"Content-Type": {"text/plain"}, |
||||
"Cookie": {cookies}, |
||||
"Host": {"query1.finance.yahoo.com"},//query1.finance.yahoo.com
|
||||
"Sec-Fetch-Dest": {"empty"}, |
||||
"Sec-Fetch-Mode": {"cors"}, |
||||
"Sec-Fetch-Site": {"same-site"}, |
||||
"TE": {"trailers"}, |
||||
"User-Agent": {userAgent}, |
||||
} |
||||
|
||||
response, err := client.Do(request) |
||||
if err != nil { |
||||
//panic(err)
|
||||
return "failed","" |
||||
} |
||||
defer response.Body.Close() |
||||
|
||||
body, err := ioutil.ReadAll(response.Body) |
||||
if err != nil { |
||||
//panic(err)
|
||||
return "failed","" |
||||
} |
||||
if len(body) > 50 {//means error happened
|
||||
return "failed","" |
||||
} |
||||
log.Println("Originl Crumb:", string(body[:])) |
||||
return string(body[:]),substitute |
||||
} |
||||
|
||||
func fetchCookies() string { |
||||
client := http.Client{} |
||||
request, err := http.NewRequest("GET", cookieURL, nil) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
request.Header = http.Header{ |
||||
"Accept": {"*/*"}, |
||||
"Accept-Encoding": {"gzip, deflate, br"}, |
||||
"Accept-Language": {"en-US,en;q=0.5"}, |
||||
"Connection": {"keep-alive"}, |
||||
"Host": {"login.yahoo.com"}, |
||||
"Sec-Fetch-Dest": {"document"}, |
||||
"Sec-Fetch-Mode": {"navigate"}, |
||||
"Sec-Fetch-Site": {"none"}, |
||||
"Sec-Fetch-User": {"?1"}, |
||||
"TE": {"trailers"}, |
||||
"Update-Insecure-Requests": {"1"}, |
||||
"User-Agent": {userAgent}, |
||||
} |
||||
|
||||
response, err := client.Do(request) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
defer response.Body.Close() |
||||
|
||||
var result string |
||||
for _, cookie := range response.Cookies() { |
||||
if cookie.Name != "AS" { |
||||
result += cookie.Name + "=" + cookie.Value + "; " |
||||
} |
||||
} |
||||
result = strings.TrimSuffix(result, "; ") |
||||
log.Println("Cookies:", result) |
||||
return result |
||||
} |
||||
@ -0,0 +1,220 @@ |
||||
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package mop |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"net/http" |
||||
//"log"
|
||||
"strings" |
||||
|
||||
"easyquotation/stock" |
||||
) |
||||
|
||||
const marketURL = `https://query1.finance.yahoo.com/v7/finance/quote?crumb=%s&symbols=%s` |
||||
const marketURLQueryParts = `&range=1d&interval=5m&indicators=close&includeTimestamps=false&includePrePost=false&corsDomain=finance.yahoo.com&.tsrc=finance` |
||||
|
||||
// Market stores current market information displayed in the top three lines of
|
||||
// the screen. The market data is fetched and parsed from the HTML page above.
|
||||
type Market struct { |
||||
IsClosed bool // True when U.S. markets are closed.
|
||||
Dow map[string]string // Hash of Dow Jones indicators.
|
||||
Nasdaq map[string]string // Hash of NASDAQ indicators.
|
||||
Sp500 map[string]string // Hash of S&P 500 indicators.
|
||||
Tokyo map[string]string |
||||
HongKong map[string]string |
||||
London map[string]string |
||||
Frankfurt map[string]string |
||||
Yield map[string]string |
||||
Oil map[string]string |
||||
Yen map[string]string |
||||
Euro map[string]string |
||||
Gold map[string]string |
||||
errors string // Error(s), if any.
|
||||
url string // URL with symbols to fetch data
|
||||
quotes *Quotes |
||||
|
||||
res map[string]*stock.Stock |
||||
watchlist *Watchlist |
||||
cookies string // cookies for auth
|
||||
crumb string // crumb for the cookies, to be applied as a query param
|
||||
} |
||||
|
||||
// Returns new initialized Market struct.
|
||||
func NewMarket(res map[string]*stock.Stock, watchlist *Watchlist) *Market { |
||||
market := &Market{} |
||||
market.IsClosed = false |
||||
market.Dow = make(map[string]string) |
||||
market.Nasdaq = make(map[string]string) |
||||
market.Sp500 = make(map[string]string) |
||||
|
||||
market.Tokyo = make(map[string]string) |
||||
market.HongKong = make(map[string]string) |
||||
market.London = make(map[string]string) |
||||
market.Frankfurt = make(map[string]string) |
||||
|
||||
market.Yield = make(map[string]string) |
||||
market.Oil = make(map[string]string) |
||||
market.Yen = make(map[string]string) |
||||
market.Euro = make(map[string]string) |
||||
market.Gold = make(map[string]string) |
||||
|
||||
market.cookies = fetchCookies() |
||||
//market.quotes = quotes
|
||||
substitute := "" |
||||
market.crumb, substitute = fetchCrumb(market.cookies, "") |
||||
if market.crumb == "failed" { |
||||
market.crumb, substitute = fetchCrumb(market.cookies, "subs") |
||||
} |
||||
if substitute != "" { |
||||
newURL := strings.Replace(marketURL, "https://query1.finance.yahoo.com", substitute, -1) |
||||
//log.Println("URL is ", newURL)
|
||||
|
||||
market.url = fmt.Sprintf(newURL, market.crumb, `^DJI,^IXIC,^GSPC,^N225,^HSI,^FTSE,^GDAXI,^TNX,CL=F,CNH=X,EUR=X,GC=F`) + marketURLQueryParts |
||||
//log.Println("full URL is ", market.url)
|
||||
}else{ |
||||
market.url = fmt.Sprintf(marketURL, market.crumb, `^DJI,^IXIC,^GSPC,^N225,^HSI,^FTSE,^GDAXI,^TNX,CL=F,CNH=X,EUR=X,GC=F`) + marketURLQueryParts |
||||
} |
||||
market.errors = `` |
||||
market.res = res |
||||
market.watchlist = watchlist |
||||
return market |
||||
} |
||||
|
||||
// Fetch downloads HTML page from the 'marketURL', parses it, and stores resulting data
|
||||
// in internal hashes. If download or data parsing fails Fetch populates 'market.errors'.
|
||||
func (market *Market) Fetch() (self *Market) { |
||||
self = market // <-- This ensures we return correct market after recover() from panic().
|
||||
defer func() { |
||||
if err := recover(); err != nil { |
||||
market.errors = fmt.Sprintf("Error fetching market data...\n%s", err) |
||||
} else { |
||||
market.errors = "" |
||||
} |
||||
}() |
||||
|
||||
client := http.Client{} |
||||
request, err := http.NewRequest("GET", market.url, nil) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
request.Header = http.Header{ |
||||
"Accept": {"*/*"}, |
||||
"Accept-Language": {"en-US,en;q=0.5"}, |
||||
"Connection": {"keep-alive"}, |
||||
"Content-Type": {"application/json"}, |
||||
"Cookie": {market.cookies}, |
||||
"Host": {"query1.finance.yahoo.com"}, |
||||
"Origin": {"https://finance.yahoo.com"}, |
||||
"Referer": {"https://finance.yahoo.com"}, |
||||
"Sec-Fetch-Dest": {"empty"}, |
||||
"Sec-Fetch-Mode": {"cors"}, |
||||
"Sec-Fetch-Site": {"same-site"}, |
||||
"TE": {"trailers"}, |
||||
"User-Agent": {userAgent}, |
||||
} |
||||
|
||||
//request.Header.Set("User-Agent", "Android 10; K")
|
||||
response, err := client.Do(request) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
defer response.Body.Close() |
||||
body, err := ioutil.ReadAll(response.Body) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
body = market.isMarketOpen(body) |
||||
return market.extract(body) |
||||
} |
||||
|
||||
// Ok returns two values: 1) boolean indicating whether the error has occurred,
|
||||
// and 2) the error text itself.
|
||||
func (market *Market) Ok() (bool, string) { |
||||
return market.errors == ``, market.errors |
||||
} |
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
func (market *Market) isMarketOpen(body []byte) []byte { |
||||
// TBD -- CNN page doesn't seem to have market open/close indicator.
|
||||
return body |
||||
} |
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
func assign(results []map[string]interface{}, position int, changeAsPercent bool) map[string]string { |
||||
out := make(map[string]string) |
||||
out[`change`] = float2Str(results[position]["regularMarketChange"].(float64)) |
||||
out[`latest`] = float2Str(results[position]["regularMarketPrice"].(float64)) |
||||
if changeAsPercent{ |
||||
out[`change`] = float2Str(results[position]["regularMarketChangePercent"].(float64)) + `%` |
||||
} else {
|
||||
out[`percent`] = float2Str(results[position]["regularMarketChangePercent"].(float64)) |
||||
} |
||||
return out |
||||
} |
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
func (market *Market) extract(body []byte) *Market { |
||||
d := map[string]map[string][]map[string]interface{}{} |
||||
err := json.Unmarshal(body, &d) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
results := d["quoteResponse"]["result"] |
||||
market.Dow = assign(results, 0, false) |
||||
market.Nasdaq = assign(results, 1, false) |
||||
market.Sp500 = assign(results, 2, false) |
||||
market.Tokyo = assign(results, 3, false) |
||||
market.HongKong = assign(results, 4, false) |
||||
market.London = assign(results, 5, false) |
||||
market.Frankfurt = assign(results, 6, false) |
||||
market.Yield[`name`] = `10-year Yield` |
||||
market.Yield = assign(results, 7, false) |
||||
|
||||
market.Tokyo[`name`] = `SH` |
||||
q := market.res["sh000001"].Market |
||||
thechange := q.LastPrice - q.PreClose |
||||
thechangepercent := thechange / q.PreClose * 100 |
||||
market.Tokyo[`change`] = float2Str(thechange) |
||||
market.Tokyo[`latest`] = float2Str(q.LastPrice) |
||||
market.Tokyo[`percent`] = float2Str(thechangepercent) |
||||
|
||||
market.London[`name`] = `SZ` |
||||
q = market.res["sz399001"].Market |
||||
thechange = q.LastPrice - q.PreClose |
||||
thechangepercent = thechange / q.PreClose * 100 |
||||
market.London[`change`] = float2Str(thechange) |
||||
market.London[`latest`] = float2Str(q.LastPrice) |
||||
market.London[`percent`] = float2Str(thechangepercent) |
||||
|
||||
market.Frankfurt[`name`] = `CYB` |
||||
q = market.res["sz399006"].Market |
||||
thechange = q.LastPrice - q.PreClose |
||||
thechangepercent = thechange / q.PreClose * 100 |
||||
market.Frankfurt[`change`] = float2Str(thechange) |
||||
market.Frankfurt[`latest`] = float2Str(q.LastPrice) |
||||
market.Frankfurt[`percent`] = float2Str(thechangepercent) |
||||
|
||||
market.Yield[`name`] = `Time` |
||||
if q.Time == "" { |
||||
market.Yield[`change`] = "00:00:00" |
||||
}else{ |
||||
market.Yield[`change`] = q.Time//market.watchlist.Baseon
|
||||
} |
||||
avgper, _ := market.quotes.getemotionindex(market.res) |
||||
market.Yield[`latest`] = float2Str(avgper) |
||||
//log.Println("here we update the market ,time is", q.Time)
|
||||
|
||||
market.Oil = assign(results, 8, true) |
||||
market.Yen = assign(results, 9, true) |
||||
market.Euro = assign(results, 10, true) |
||||
market.Gold = assign(results, 11, true) |
||||
|
||||
return market |
||||
} |
||||
@ -0,0 +1,882 @@ |
||||
// Copyright (c) 2013-2019 by Michael Dvorkin and contributors. All Rights Reserved.
|
||||
// Use of this source code is governed by a MIT-style license that can
|
||||
// be found in the LICENSE file.
|
||||
|
||||
package mop |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io/ioutil" |
||||
"net/http" |
||||
"reflect" |
||||
"strconv" |
||||
"strings" |
||||
//"io"
|
||||
"time" |
||||
"log" |
||||
|
||||
"easyquotation/stock" |
||||
mqtt "github.com/eclipse/paho.mqtt.golang" |
||||
) |
||||
|
||||
// const quotesURL = `http://download.finance.yahoo.com/d/quotes.csv?s=%s&f=sl1c1p2oghjkva2r2rdyj3j1`
|
||||
const quotesURLv7 = `https://query1.finance.yahoo.com/v7/finance/quote?symbols=%s` |
||||
const quotesURLv7QueryParts = `&range=1d&interval=5m&indicators=close&includeTimestamps=false&includePrePost=false&corsDomain=finance.yahoo.com&.tsrc=finance` |
||||
|
||||
const noDataIndicator = `N/A` |
||||
|
||||
// Stock stores quote information for the particular stock ticker. The data
|
||||
// for all the fields except 'Direction' is fetched using Yahoo market API.
|
||||
type Stock struct { |
||||
Ticker string `json:"symbol"` // Stock ticker.
|
||||
LastTrade string `json:"regularMarketPrice"` // l1: last trade.
|
||||
Change string `json:"regularMarketChange"` // c6: change real time.
|
||||
ChangePct string `json:"regularMarketChangePercent"` // k2: percent change real time.
|
||||
Open string `json:"regularMarketOpen"` // o: market open price.
|
||||
Low string `json:"regularMarketDayLow"` // g: day's low.
|
||||
High string `json:"regularMarketDayHigh"` // h: day's high.
|
||||
Low52 string `json:"fiftyTwoWeekLow"` // j: 52-weeks low.
|
||||
High52 string `json:"fiftyTwoWeekHigh"` // k: 52-weeks high.
|
||||
Volume string `json:"regularMarketVolume"` // v: volume.
|
||||
AvgVolume string `json:"averageDailyVolume10Day"` // a2: average volume.
|
||||
PeRatio string `json:"trailingPE"` // r2: P/E ration real time.
|
||||
PeRatioX string `json:"trailingPE"` // r: P/E ration (fallback when real time is N/A).
|
||||
Dividend string `json:"trailingAnnualDividendRate"` // d: dividend.
|
||||
Yield string `json:"trailingAnnualDividendYield"` // y: dividend yield.
|
||||
MarketCap string `json:"marketCap"` // j3: market cap real time.
|
||||
MarketCapX string `json:"marketCap"` // j1: market cap (fallback when real time is N/A).
|
||||
Currency string `json:"currency"` // String code for currency of stock.
|
||||
Direction int // -1 when change is < $0, 0 when change is = $0, 1 when change is > $0.
|
||||
PreOpen string `json:"preMarketChangePercent,omitempty"` |
||||
AfterHours string `json:"postMarketChangePercent,omitempty"` |
||||
} |
||||
|
||||
type stockinfo struct { |
||||
Scode string |
||||
Sname string |
||||
Ft string |
||||
Upt string |
||||
} |
||||
|
||||
type recordinfo struct { |
||||
Date string |
||||
Time string |
||||
Totalstocks string |
||||
} |
||||
// Quotes stores relevant pointers as well as the array of stock quotes for
|
||||
// the tickers we are tracking.
|
||||
type Quotes struct { |
||||
market *Market // Pointer to Market.
|
||||
profile *Profile // Pointer to Profile.
|
||||
stocks []Stock // Array of stock quote data.
|
||||
errors string // Error string if any.
|
||||
res map[string]*stock.Stock |
||||
|
||||
watchlist *Watchlist |
||||
client mqtt.Client |
||||
upstocks map[string]string |
||||
totalstocks []stockinfo |
||||
needrefresh bool |
||||
addedstocks []string |
||||
Allflag bool |
||||
} |
||||
|
||||
// Sets the initial values and returns new Quotes struct.
|
||||
func NewQuotes(market *Market, profile *Profile, res map[string]*stock.Stock, watchlist *Watchlist, client mqtt.Client) *Quotes { |
||||
/*var watchlist Watchlist |
||||
err := json.NewDecoder(respbody).Decode(&watchlist) |
||||
if err != nil { |
||||
// Handle error
|
||||
fmt.Println(err) |
||||
}*/ |
||||
return &Quotes{ |
||||
market: market, |
||||
profile: profile, |
||||
errors: ``, |
||||
res: res, |
||||
watchlist: watchlist, |
||||
client: client, |
||||
totalstocks: []stockinfo{}, |
||||
upstocks: map[string]string{}, |
||||
needrefresh: true, |
||||
Allflag: false, |
||||
} |
||||
} |
||||
|
||||
// Define a struct that matches the structure of the JSON
|
||||
type WatchlistItem struct { |
||||
Scode string `json:"scode"` |
||||
AnalyseFrom string `json:"analyse_from"` |
||||
AnalyseDay string `json:"analyse_day"` |
||||
Enterprice string `json:"enterprice"` |
||||
Enterdays int `json:"enterdays"` |
||||
Exchangerate string `json:"exchangerate"` |
||||
Uplist []int `json:"uplist"` |
||||
Last3days string `json:"last3days"` |
||||
Daysback int `json:"daysback"` |
||||
Inhklist bool `json:"inhklist"` |
||||
} |
||||
|
||||
type Watchlist struct { |
||||
Baseon string `json:"baseon"` |
||||
Pdate string `json:"pdate"` |
||||
Preparam int `json:"preparam"` |
||||
Dates []string `json:"dates"` |
||||
Total int `json:"total"` |
||||
Watchlist []WatchlistItem `json:"watchlist"` |
||||
} |
||||
|
||||
func (quotes *Quotes) Setquotes(){ |
||||
quotes.market.quotes = quotes |
||||
} |
||||
|
||||
// Fetch the latest stock quotes and parse raw fetched data into array of
|
||||
// []Stock structs.
|
||||
func (quotes *Quotes) Fetch() (self *Quotes) { |
||||
self = quotes // <-- This ensures we return correct quotes after recover() from panic().
|
||||
if quotes.isReady() { |
||||
defer func() { |
||||
if err := recover(); err != nil { |
||||
quotes.errors = fmt.Sprintf("\n\n\n\nError fetching stock quotes...\n%s", err) |
||||
} else { |
||||
quotes.errors = "" |
||||
} |
||||
}() |
||||
|
||||
if quotes.profile.mode == "review" { |
||||
//fmt.Println("review mode")
|
||||
if quotes.res["sh600000"].Market.Open == 0 { |
||||
return quotes |
||||
} |
||||
//fmt.Println("review mode")
|
||||
var watchlist_selected []WatchlistItem |
||||
var baseonlist []string |
||||
|
||||
//date_json := []string{"2023-04-24"}
|
||||
for _, date := range quotes.profile.date_json { |
||||
url := fmt.Sprintf("http://119.29.166.226/q/dayjson/%sml.json", date) |
||||
response, err := http.Get(url) |
||||
if err != nil { |
||||
// Handle error
|
||||
fmt.Println(err) |
||||
continue |
||||
} |
||||
defer response.Body.Close() |
||||
|
||||
var watchlist Watchlist |
||||
error := json.NewDecoder(response.Body).Decode(&watchlist) |
||||
if error != nil { |
||||
// Handle error
|
||||
fmt.Println(error) |
||||
} |
||||
for _, item := range watchlist.Watchlist { |
||||
dayinfo := item.Last3days |
||||
if _, ok := quotes.res[item.Scode]; ok { |
||||
if dayinfo[4] == '|' { |
||||
item.AnalyseFrom = date |
||||
watchlist_selected = append(watchlist_selected, item) |
||||
} |
||||
|
||||
} |
||||
} |
||||
baseonlist = append(baseonlist, watchlist.Baseon) |
||||
} |
||||
|
||||
quotes.watchlist.Watchlist = watchlist_selected |
||||
if len(baseonlist) > 0 { |
||||
quotes.watchlist.Baseon = strings.Join(baseonlist, ",") |
||||
}else{ |
||||
quotes.watchlist.Baseon = "" |
||||
} |
||||
quotes.parsereview(quotes.res) |
||||
}else{ |
||||
url := fmt.Sprintf(quotesURLv7, strings.Join(quotes.profile.Tickers, `,`)) |
||||
response, err := http.Get(url + quotesURLv7QueryParts) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
defer response.Body.Close() |
||||
body, err := ioutil.ReadAll(response.Body) |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
res := quotes.res |
||||
quotes.parse2(body, res) |
||||
//fmt.Println(res["sh600111"])
|
||||
//fmt.Println("mode:", quotes.profile.mode)
|
||||
|
||||
} |
||||
}else{ |
||||
fmt.Println("***not ready***") |
||||
} |
||||
|
||||
return quotes |
||||
} |
||||
|
||||
//write a function to save the slice of quotes.stocks to file
|
||||
func (quotes *Quotes) SaveStocks() { |
||||
if quotes.profile.mode == "review"{ |
||||
return |
||||
} |
||||
|
||||
filedata, err := json.MarshalIndent(quotes.totalstocks, "", " ") |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
|
||||
err = ioutil.WriteFile("stocklist.json", filedata, 0644) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
} |
||||
|
||||
//read stocks
|
||||
func (quotes *Quotes) ReadStocks() { |
||||
filedata, err := ioutil.ReadFile("stocklist.json") |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
|
||||
var stocks []stockinfo |
||||
err = json.Unmarshal(filedata, &stocks) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
} |
||||
|
||||
func (quotes *Quotes)Addstockcodetofile(codestoadd []string) { |
||||
var stockcode []string |
||||
for _, item := range quotes.watchlist.Watchlist { |
||||
stockcode = append(stockcode, item.Scode) |
||||
} |
||||
|
||||
stockcode = append(stockcode, quotes.profile.Tickers...) |
||||
|
||||
if len(codestoadd) > 0 { |
||||
stockcode = append(stockcode, codestoadd...) |
||||
if len(quotes.addedstocks) > 1 { |
||||
quotes.addedstocks = append(quotes.addedstocks, codestoadd...) |
||||
} |
||||
} |
||||
// Marshal the combined array to a JSON-encoded byte slice
|
||||
data, err := json.Marshal(stockcode) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
|
||||
// Write the byte slice to a file
|
||||
err = ioutil.WriteFile("stock_in.json", data, 0644) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
} |
||||
|
||||
func gettimediff(start string, end string) time.Duration { |
||||
layout := "15:04:05" |
||||
closepm,_ := time.Parse(layout, "15:01:00") |
||||
openpm, _ := time.Parse(layout, "12:59:59") |
||||
closespan := -90 * time.Minute |
||||
|
||||
if start =="" || end ==""{ |
||||
return 0*time.Second |
||||
} |
||||
|
||||
t1, err := time.Parse(layout, start) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return 0*time.Second |
||||
} |
||||
|
||||
t2, err := time.Parse(layout, end) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return 0*time.Second |
||||
} |
||||
|
||||
if t2.After(closepm) { |
||||
return 0*time.Second |
||||
} |
||||
|
||||
if t1.After(openpm) { |
||||
t1 = t1.Add(closespan) |
||||
} |
||||
|
||||
if t2.After(openpm) { |
||||
t2 = t2.Add(closespan) |
||||
} |
||||
|
||||
// 计算时间差
|
||||
diff := t2.Sub(t1) |
||||
if diff > time.Hour { |
||||
//fmt.Printf("Time difference between %s and %s: %v\n", end, start, diff)
|
||||
return diff |
||||
} |
||||
|
||||
return 0*time.Second |
||||
} |
||||
|
||||
// Ok returns two values: 1) boolean indicating whether the error has occurred,
|
||||
// and 2) the error text itself.
|
||||
func (quotes *Quotes) Ok() (bool, string) { |
||||
return quotes.errors == ``, quotes.errors |
||||
} |
||||
|
||||
// AddTickers saves the list of tickers and refreshes the stock data if new
|
||||
// tickers have been added. The function gets called from the line editor
|
||||
// when user adds new stock tickers.
|
||||
func (quotes *Quotes) AddTickers(tickers []string) (added int, err error) { |
||||
if added, err = quotes.profile.AddTickers(tickers); err == nil && added > 0 { |
||||
quotes.stocks = nil // Force fetch.
|
||||
} |
||||
return |
||||
} |
||||
|
||||
// RemoveTickers saves the list of tickers and refreshes the stock data if some
|
||||
// tickers have been removed. The function gets called from the line editor
|
||||
// when user removes existing stock tickers.
|
||||
func (quotes *Quotes) RemoveTickers(tickers []string) (removed int, err error) { |
||||
if removed, err = quotes.profile.RemoveTickers(tickers); err == nil && removed > 0 { |
||||
quotes.stocks = nil // Force fetch.
|
||||
} |
||||
return |
||||
} |
||||
|
||||
// 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
|
||||
// cases we make sure the list of requested tickers is not empty.
|
||||
func (quotes *Quotes) isReady() bool { |
||||
return (quotes.stocks == nil || !quotes.market.IsClosed) && len(quotes.profile.Tickers) > 0 && quotes.needrefresh == true |
||||
} |
||||
|
||||
func inlist(slice []string, str string) bool { |
||||
for _, s := range slice { |
||||
if s == str { |
||||
return true |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
|
||||
func contains(slice []stockinfo, str string) (bool, int) { |
||||
for i, s := range slice { |
||||
if s.Scode == str { |
||||
return true, i |
||||
} |
||||
} |
||||
return false, -1 |
||||
} |
||||
|
||||
func indexer(slice []stockinfo, str string) int { |
||||
for i, s := range slice { |
||||
if s.Scode == str { |
||||
return i+1 |
||||
} |
||||
} |
||||
return 0 |
||||
} |
||||
|
||||
func (quotes *Quotes) getitembyscode(scode string) WatchlistItem { |
||||
watchlist := quotes.watchlist.Watchlist |
||||
for _, s := range watchlist { |
||||
if s.Scode == scode { |
||||
return s |
||||
} |
||||
} |
||||
return WatchlistItem{} |
||||
} |
||||
|
||||
func padString(str string, length int) string { |
||||
if len(str) < length { |
||||
str = str + strings.Repeat(" ", length-len(str)) |
||||
} |
||||
return str |
||||
} |
||||
|
||||
func (quotes *Quotes) Sendtotalstocks() { |
||||
stockdata, err := json.Marshal(quotes.totalstocks)//json.MarshalIndent(quotes.totalstocks, "", " ")
|
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
|
||||
topic := "stock/response/standby" |
||||
|
||||
dataresp := map[string]interface{}{ |
||||
"totalstocks": string(stockdata), |
||||
"date": quotes.watchlist.Baseon, |
||||
"time": time.Now().Format("15:04:05"), |
||||
} |
||||
|
||||
jsonData, err := json.MarshalIndent(dataresp, "", " ") |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
message := string(jsonData) |
||||
token := quotes.client.Publish(topic, 0, false, message) |
||||
token.Wait() |
||||
} |
||||
|
||||
func (quotes *Quotes) Sendstockgraphreq(index interface{}, istime bool) { |
||||
var selcode string |
||||
if intvalue, ok := index.(int); ok { |
||||
if intvalue > len(quotes.totalstocks){ |
||||
return |
||||
} |
||||
selcode = quotes.totalstocks[intvalue-1].Scode |
||||
} |
||||
|
||||
if stringValue, ok := index.(string); ok { |
||||
selcode = stringValue |
||||
} |
||||
|
||||
topic := "my/topic" |
||||
itemsel := quotes.getitembyscode(selcode) |
||||
data := map[string]interface{}{ |
||||
"scode": selcode,//quotes.totalstocks[index-1].Scode,
|
||||
"tier": 0, |
||||
"daysback": itemsel.Daysback, |
||||
"stdprice": itemsel.Enterprice, |
||||
"name": quotes.res[selcode].Base.Name, //strings.TrimSpace(quotes.totalstocks[index-1].Sname),
|
||||
"ed": itemsel.AnalyseDay, |
||||
} |
||||
if istime { |
||||
currentTime := time.Now() |
||||
data["date"] = currentTime.Format("2006-01-02") |
||||
} |
||||
jsonData, err := json.Marshal(data) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
} |
||||
message := string(jsonData) |
||||
token := quotes.client.Publish(topic, 0, false, message) |
||||
token.Wait() |
||||
} |
||||
|
||||
func (quotes *Quotes) sendstockforevaluation(selcode string, datestring string, buyp string, pcp string) { |
||||
|
||||
topic := "astock/candistock" |
||||
itemsel := quotes.getitembyscode(selcode) |
||||
data := map[string]interface{}{ |
||||
"scode": selcode,//quotes.totalstocks[index-1].Scode,
|
||||
"tier": 0, |
||||
"daysback": itemsel.Daysback, |
||||
"stdprice": itemsel.Enterprice, |
||||
"name": quotes.res[selcode].Base.Name, //strings.TrimSpace(quotes.totalstocks[index-1].Sname),
|
||||
"ed": itemsel.AnalyseDay, |
||||
} |
||||
|
||||
data["time"] = datestring |
||||
data["buyp"] = buyp |
||||
data["pcp"] = pcp |
||||
|
||||
jsonData, err := json.Marshal(data) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
} |
||||
message := string(jsonData) |
||||
token := quotes.client.Publish(topic, 0, false, message) |
||||
token.Wait() |
||||
} |
||||
|
||||
func (quotes *Quotes) Getselectedinfo(index int) *Stock { |
||||
if index > len(quotes.stocks) || index < 1 { |
||||
return nil |
||||
} |
||||
return "es.stocks[index-1] |
||||
} |
||||
|
||||
func (quotes *Quotes) GetselectedinfobyTicker(ticker string) *Stock { |
||||
index := -1 |
||||
if ticker == "" { |
||||
return nil |
||||
} |
||||
for i, s := range quotes.stocks { |
||||
realname := strings.TrimRight(s.Ticker, string(byte(32))) |
||||
if realname == ticker[6:] { |
||||
//log.Println(i)
|
||||
index = i |
||||
break |
||||
} |
||||
} |
||||
if index == -1 { |
||||
log.Println("Not found in quotes.stocks") |
||||
return nil |
||||
} |
||||
log.Println(quotes.stocks[index]) |
||||
return "es.stocks[index] |
||||
} |
||||
|
||||
func (quotes *Quotes) parsereview(res map[string]*stock.Stock) (*Quotes, error) { |
||||
var scodes []string |
||||
fmt.Println("Start parsing review") |
||||
wamap := make(map[string]WatchlistItem, len(quotes.watchlist.Watchlist)) |
||||
for _, item := range quotes.watchlist.Watchlist { |
||||
scodes = append(scodes, item.Scode) |
||||
wamap[item.Scode] = item |
||||
} |
||||
|
||||
var snames []string |
||||
quotes.stocks = make([]Stock, len(scodes))
|
||||
fmt.Println(scodes) |
||||
for i, scode := range scodes { |
||||
q := res[scode].Market |
||||
b := res[scode].Base |
||||
|
||||
quotes.totalstocks = append(quotes.totalstocks, stockinfo{scode, b.Name, q.Time, q.Time}) |
||||
quotes.upstocks[scode] = q.Time |
||||
|
||||
open, close, high, low, ndays := getnextdaysHL(scode, wamap[scode].AnalyseFrom) |
||||
|
||||
quotes.stocks[i].Ticker = fmt.Sprintf("%02d", indexer(quotes.totalstocks ,scode)) + padString(b.Name, 11) |
||||
snames = append(snames, b.Name) |
||||
quotes.stocks[i].LastTrade = wamap[scode].Enterprice |
||||
stdprice, _ := strconv.ParseFloat(wamap[scode].Enterprice, 64) |
||||
|
||||
thelast := (close - stdprice) / stdprice * 100 |
||||
quotes.stocks[i].ChangePct = float2Str(thelast)+"%" |
||||
|
||||
quotes.stocks[i].Change = strconv.Itoa(ndays)+"day(s)" |
||||
|
||||
theopen := (open - stdprice) / stdprice * 100 |
||||
quotes.stocks[i].Open = float2Str(theopen)+"%" |
||||
quotes.stocks[i].Low = float2Str(low) |
||||
quotes.stocks[i].High = float2Str(high) |
||||
|
||||
thehigh := (high - stdprice) / stdprice * 100 |
||||
thelow := (low - stdprice) / stdprice * 100 |
||||
quotes.stocks[i].Low52 = float2Str(thelow) |
||||
quotes.stocks[i].High52 = float2Str(thehigh) |
||||
|
||||
quotes.stocks[i].Volume = "" |
||||
quotes.stocks[i].AvgVolume = wamap[scode].AnalyseFrom |
||||
|
||||
adv, err := strconv.ParseFloat(quotes.stocks[i].High52, 64) |
||||
if err == nil { |
||||
if adv < 10.0 { |
||||
quotes.stocks[i].Direction = -1 |
||||
} else if adv > 10.0 { |
||||
quotes.stocks[i].Direction = 1 |
||||
} |
||||
} |
||||
|
||||
quotes.stocks[i].Low52 = quotes.stocks[i].Low52 + "%" |
||||
quotes.stocks[i].High52 = quotes.stocks[i].High52 + "%" |
||||
quotes.stocks[i].MarketCap = quotes.upstocks[scode] |
||||
quotes.stocks[i].Dividend = b.Symbol |
||||
|
||||
if inlist(quotes.profile.Tickers, scode) == true { |
||||
quotes.stocks[i].MarketCap = "M" |
||||
} |
||||
//fmt.Println(scode,"****")
|
||||
} |
||||
quotes.needrefresh = false |
||||
|
||||
return quotes, nil
|
||||
} |
||||
|
||||
func (quotes *Quotes) Reload(){ |
||||
filedata, err := ioutil.ReadFile("stocklist.json") |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
|
||||
var stocks []stockinfo |
||||
err = json.Unmarshal(filedata, &stocks) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
quotes.totalstocks = quotes.totalstocks[:0] |
||||
for _, item := range quotes.profile.Tickers {//profile stock to trace
|
||||
if _, ok := quotes.res[item]; ok { |
||||
quotes.totalstocks = append(quotes.totalstocks, stockinfo{item, quotes.res[item].Base.Name, "09:00:00", "09:00:00"}) |
||||
quotes.upstocks["sh600000"] = "09:00:00" |
||||
} |
||||
} |
||||
quotes.totalstocks = append(quotes.totalstocks, stocks...) |
||||
for _, item := range stocks { |
||||
quotes.upstocks[item.Scode] = item.Upt |
||||
} |
||||
quotes.stocks = nil |
||||
} |
||||
|
||||
func (quotes *Quotes) Reloadbyjson(payload []byte){ |
||||
/*filedata, err := ioutil.ReadFile("stocklist0616.json") |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
}*/ |
||||
|
||||
var records recordinfo |
||||
err := json.Unmarshal(payload, &records) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
var stocks []stockinfo |
||||
err = json.Unmarshal([]byte(records.Totalstocks), &stocks) |
||||
if err != nil { |
||||
fmt.Println(err) |
||||
return |
||||
} |
||||
quotes.totalstocks = quotes.totalstocks[:0] |
||||
for _, item := range quotes.profile.Tickers {//profile stock to trace
|
||||
if _, ok := quotes.res[item]; ok { |
||||
quotes.totalstocks = append(quotes.totalstocks, stockinfo{item, quotes.res[item].Base.Name, "09:00:00", "09:00:00"}) |
||||
quotes.upstocks["sh600000"] = "09:00:00" |
||||
} |
||||
} |
||||
quotes.totalstocks = append(quotes.totalstocks, stocks...) |
||||
for _, item := range stocks { |
||||
quotes.upstocks[item.Scode] = item.Upt |
||||
} |
||||
quotes.stocks = nil |
||||
} |
||||
|
||||
func (quotes* Quotes) ResetforNewday(watchlist *Watchlist) { |
||||
quotes.totalstocks = quotes.totalstocks[:0] |
||||
quotes.watchlist = watchlist |
||||
quotes.upstocks = map[string]string{} |
||||
log.Println("here we retrieve data :", quotes.watchlist.Baseon) |
||||
} |
||||
|
||||
func (quotes* Quotes) getemotionindex(res map[string]*stock.Stock) (float64, int) { |
||||
avgpercent := 100.0 |
||||
avgcount := 0 |
||||
array_percent := []float64{} |
||||
|
||||
wamap := make(map[string]WatchlistItem, len(quotes.watchlist.Watchlist)) |
||||
for _, item := range quotes.watchlist.Watchlist { |
||||
if _, ok := res[item.Scode]; ok { |
||||
wamap[item.Scode] = item |
||||
} |
||||
//fmt.Println(scodes)
|
||||
} |
||||
for _, sitem := range quotes.totalstocks { |
||||
if inlist(quotes.profile.Tickers, sitem.Scode) == false { |
||||
scode := sitem.Scode |
||||
q := res[scode].Market |
||||
|
||||
if _, ok := wamap[scode]; ok { |
||||
strprice, _ := strconv.ParseFloat(wamap[scode].Enterprice, 64) |
||||
stdchangepercent := q.LastPrice*100 / strprice |
||||
avgcount++ |
||||
avgpercent += stdchangepercent |
||||
array_percent = append(array_percent, stdchangepercent) |
||||
} |
||||
} |
||||
} |
||||
if avgcount > 0 { |
||||
log.Println(avgpercent, avgcount) |
||||
//log.Println(array_percent)
|
||||
avgpercent = avgpercent / float64(avgcount) |
||||
//quotes.stocks[0].High52 = float2Str(avgpercent)
|
||||
} |
||||
return avgpercent, avgcount |
||||
} |
||||
|
||||
// this will parse the json objects
|
||||
func (quotes *Quotes) parse2(body []byte, res map[string]*stock.Stock) (*Quotes, error) { |
||||
var scodes []string |
||||
wamap := make(map[string]WatchlistItem, len(quotes.watchlist.Watchlist)) |
||||
for _, item := range quotes.watchlist.Watchlist { |
||||
enterPrice, err := strconv.ParseFloat(item.Enterprice, 64) |
||||
if err != nil { |
||||
// Handle error
|
||||
fmt.Println(err) |
||||
} |
||||
if _, ok := res[item.Scode]; ok { |
||||
//fmt.Println(item.Scode)
|
||||
q := res[item.Scode].Market |
||||
isin, _ := contains(quotes.totalstocks, item.Scode) |
||||
//fmt.Println(q.Name, q.PreClose , q.LastPrice ,q.LastPrice , enterPrice)
|
||||
if ((q.PreClose < q.LastPrice && q.LastPrice >= enterPrice && q.PreClose < enterPrice )|| |
||||
isin == true && quotes.Allflag == true){ |
||||
//fmt.Println(enterPrice)
|
||||
scodes = append(scodes, item.Scode) |
||||
//fmt.Println(scodes)
|
||||
} |
||||
wamap[item.Scode] = item |
||||
} |
||||
//fmt.Println(scodes)
|
||||
} |
||||
|
||||
for _, item := range quotes.profile.Tickers {//profile stock to trace
|
||||
if _, ok := res[item]; ok { |
||||
scodes = append(scodes, item) |
||||
} |
||||
} |
||||
|
||||
var snames []string |
||||
quotes.stocks = make([]Stock, len(scodes))
|
||||
//fmt.Println(res["sh600000"])
|
||||
for i, scode := range scodes { |
||||
newadd := 0 |
||||
q := res[scode].Market |
||||
b := res[scode].Base |
||||
//fmt.Println(q)
|
||||
|
||||
isin, index := contains(quotes.totalstocks, scode) |
||||
if isin == false { |
||||
quotes.totalstocks = append(quotes.totalstocks, stockinfo{scode, q.Name, q.Time, q.Time}) |
||||
quotes.upstocks[scode] = q.Time |
||||
newadd = 1 |
||||
}else { |
||||
|
||||
//compare uptime and a string as time, if the diff is bigger than 1 hour, then change quotes.upstocks[scode] to q.Time
|
||||
//quotes.upstocks[scode] = uptime
|
||||
diff := gettimediff(quotes.totalstocks[index].Ft, q.Time) |
||||
if diff > time.Hour { |
||||
quotes.upstocks[scode] = q.Time |
||||
quotes.totalstocks[index].Upt = q.Time |
||||
} |
||||
quotes.totalstocks[index].Ft = q.Time |
||||
} |
||||
|
||||
quotes.stocks[i].Ticker = fmt.Sprintf("%02d", indexer(quotes.totalstocks ,scode)) + padString(q.Name, 11) |
||||
snames = append(snames, q.Name) |
||||
quotes.stocks[i].LastTrade = float2Str(q.LastPrice) |
||||
thechange := q.LastPrice - q.PreClose |
||||
thechangepercent := thechange / q.PreClose * 100 |
||||
|
||||
quotes.stocks[i].Change = float2Str(thechange)+"*" |
||||
quotes.stocks[i].ChangePct = float2Str(thechangepercent) |
||||
quotes.stocks[i].Open = float2Str(q.Open) |
||||
quotes.stocks[i].Low = float2Str(q.Low) |
||||
quotes.stocks[i].High = float2Str(q.High) |
||||
quotes.stocks[i].Low52 = float2Str(q.BidPice) |
||||
quotes.stocks[i].High52 = float2Str(q.OfferPice) |
||||
quotes.stocks[i].Volume = float2Str(q.Volumn) |
||||
quotes.stocks[i].MarketCap = q.Time |
||||
|
||||
adv, err := strconv.ParseFloat(quotes.stocks[i].Change, 64) |
||||
//quotes.stocks[i].Direction = 0
|
||||
//fmt.Println(q.LastPrice, q.High, q.Low)
|
||||
/**/ |
||||
if err == nil { |
||||
if adv < 0.0 { |
||||
quotes.stocks[i].Direction = -1 |
||||
} else if adv > 0.0 { |
||||
quotes.stocks[i].Direction = 1 |
||||
} |
||||
} |
||||
|
||||
if q.LastPrice == q.High { |
||||
quotes.stocks[i].Direction = -1 |
||||
} else { |
||||
quotes.stocks[i].Direction = 1 |
||||
} |
||||
|
||||
if q.OfferPice == 0 { |
||||
quotes.stocks[i].Direction = 1 |
||||
}
|
||||
|
||||
if q.OfferPice >= q.High { |
||||
quotes.stocks[i].Direction = -1 |
||||
} |
||||
|
||||
quotes.stocks[i].AvgVolume = quotes.upstocks[scode] |
||||
quotes.stocks[i].Dividend = b.Symbol |
||||
quotes.stocks[i].PeRatio = quotes.watchlist.Baseon[5:] |
||||
if _, ok := wamap[scode]; ok { |
||||
strprice, _ := strconv.ParseFloat(wamap[scode].Enterprice, 64) |
||||
stdchange := q.LastPrice - strprice |
||||
quotes.stocks[i].Change = float2Str(stdchange) |
||||
|
||||
} |
||||
if inlist(quotes.profile.Tickers, scode) == true { |
||||
quotes.stocks[i].AvgVolume = "M" |
||||
} else if newadd == 1 { |
||||
strprice, _ := strconv.ParseFloat(wamap[scode].Enterprice, 64) |
||||
pcp := ((strprice - q.PreClose) / q.PreClose) * 100 |
||||
quotes.sendstockforevaluation(scode, quotes.upstocks[scode], float2Str(q.LastPrice), float2Str(pcp)) |
||||
} |
||||
} |
||||
|
||||
return quotes, nil |
||||
} |
||||
|
||||
// Use reflection to parse and assign the quotes data fetched using the Yahoo
|
||||
// market API.
|
||||
func (quotes *Quotes) parse(body []byte) *Quotes { |
||||
lines := bytes.Split(body, []byte{'\n'}) |
||||
quotes.stocks = make([]Stock, len(lines)) |
||||
//
|
||||
// Get the total number of fields in the Stock struct. Skip the last
|
||||
// Advancing field which is not fetched.
|
||||
//
|
||||
fieldsCount := reflect.ValueOf(quotes.stocks[0]).NumField() - 1 |
||||
//
|
||||
// Split each line into columns, then iterate over the Stock struct
|
||||
// fields to assign column values.
|
||||
//
|
||||
for i, line := range lines { |
||||
columns := bytes.Split(bytes.TrimSpace(line), []byte{','}) |
||||
for j := 0; j < fieldsCount; j++ { |
||||
// ex. quotes.stocks[i].Ticker = string(columns[0])
|
||||
reflect.ValueOf("es.stocks[i]).Elem().Field(j).SetString(string(columns[j])) |
||||
} |
||||
//
|
||||
// Try realtime value and revert to the last known if the
|
||||
// realtime is not available.
|
||||
//
|
||||
if quotes.stocks[i].PeRatio == `N/A` && quotes.stocks[i].PeRatioX != `N/A` { |
||||
quotes.stocks[i].PeRatio = quotes.stocks[i].PeRatioX |
||||
} |
||||
if quotes.stocks[i].MarketCap == `N/A` && quotes.stocks[i].MarketCapX != `N/A` { |
||||
quotes.stocks[i].MarketCap = quotes.stocks[i].MarketCapX |
||||
} |
||||
//
|
||||
// Get the direction of the stock
|
||||
//
|
||||
adv, err := strconv.ParseFloat(quotes.stocks[i].Change, 64) |
||||
quotes.stocks[i].Direction = 0 |
||||
if err == nil { |
||||
if adv < 0 { |
||||
quotes.stocks[i].Direction = -1 |
||||
} else if (adv > 0) { |
||||
quotes.stocks[i].Direction = 1 |
||||
} |
||||
} |
||||
} |
||||
|
||||
return quotes |
||||
} |
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
func sanitize(body []byte) []byte { |
||||
return bytes.Replace(bytes.TrimSpace(body), []byte{'"'}, []byte{}, -1) |
||||
} |
||||
|
||||
func float2Str(v float64) string { |
||||
unit := "" |
||||
switch { |
||||
case v > 1.0e12: |
||||
v = v / 1.0e12 |
||||
unit = "T" |
||||
case v > 1.0e9: |
||||
v = v / 1.0e9 |
||||
unit = "B" |
||||
case v > 1.0e6: |
||||
v = v / 1.0e6 |
||||
unit = "M" |
||||
case v > 1.0e5: |
||||
v = v / 1.0e3 |
||||
unit = "K" |
||||
default: |
||||
unit = "" |
||||
} |
||||
// parse
|
||||
return fmt.Sprintf("%0.3f%s", v, unit) |
||||
} |
||||
@ -0,0 +1,143 @@ |
||||
{ |
||||
"quoteResponse": { |
||||
"result": [ |
||||
{ |
||||
"language": "en-US", |
||||
"region": "US", |
||||
"quoteType": "EQUITY", |
||||
"quoteSourceName": "Delayed Quote", |
||||
"currency": "USD", |
||||
"twoHundredDayAverageChange": -12.790649, |
||||
"priceToBook": -132.59792, |
||||
"averageDailyVolume10Day": 2399157, |
||||
"regularMarketPrice": 331.76, |
||||
"regularMarketTime": 1534363219, |
||||
"regularMarketChange": -7.380005, |
||||
"regularMarketOpen": 336.57, |
||||
"regularMarketDayHigh": 337.1433, |
||||
"regularMarketDayLow": 328.03, |
||||
"regularMarketVolume": 3798600, |
||||
"fiftyTwoWeekHighChange": -42.72, |
||||
"regularMarketDayRange": "328.03 - 337.1433", |
||||
"fiftyTwoWeekHighChangePercent": -0.11407819, |
||||
"exchange": "NYQ", |
||||
"exchangeTimezoneShortName": "EDT", |
||||
"forwardPE": 18.946888, |
||||
"postMarketTime": 1534373934, |
||||
"fiftyDayAverageChangePercent": -0.04269833, |
||||
"epsTrailingTwelveMonths": 15.922, |
||||
"priceHint": 2, |
||||
"earningsTimestampStart": 1540297800, |
||||
"trailingAnnualDividendYield": 0.018458454, |
||||
"postMarketChange": 2.0899963, |
||||
"bookValue": -2.502, |
||||
"twoHundredDayAverageChangePercent": -0.037122697, |
||||
"epsForward": 17.51, |
||||
"fiftyTwoWeekHigh": 374.48, |
||||
"postMarketPrice": 333.85, |
||||
"esgPopulated": false, |
||||
"tradeable": true, |
||||
"earningsTimestampEnd": 1540816200, |
||||
"sourceInterval": 15, |
||||
"fiftyDayAverage": 346.55743, |
||||
"trailingAnnualDividendRate": 6.26, |
||||
"marketState": "PREPRE", |
||||
"shortName": "Boeing Company (The)", |
||||
"ask": 0, |
||||
"gmtOffSetMilliseconds": -14400000, |
||||
"exchangeTimezoneName": "America/New_York", |
||||
"fiftyDayAverageChange": -14.797424, |
||||
"regularMarketChangePercent": -2.1760938, |
||||
"fiftyTwoWeekLowChangePercent": 0.41602296, |
||||
"postMarketChangePercent": 0.62997234, |
||||
"askSize": 8, |
||||
"exchangeDataDelayedBy": 0, |
||||
"sharesOutstanding": 574508032, |
||||
"fullExchangeName": "NYSE", |
||||
"twoHundredDayAverage": 344.55066, |
||||
"averageDailyVolume3Month": 3364801, |
||||
"trailingPE": 20.836578, |
||||
"financialCurrency": "USD", |
||||
"dividendDate": 1536278400, |
||||
"fiftyTwoWeekRange": "234.29 - 374.48", |
||||
"marketCap": 190598791168, |
||||
"bidSize": 8, |
||||
"bid": 0, |
||||
"regularMarketPreviousClose": 339.14, |
||||
"market": "us_market", |
||||
"fiftyTwoWeekLowChange": 97.47002, |
||||
"messageBoardId": "finmb_370857", |
||||
"longName": "The Boeing Company", |
||||
"fiftyTwoWeekLow": 234.29, |
||||
"earningsTimestamp": 1532521800, |
||||
"symbol": "BA" |
||||
}, |
||||
{ |
||||
"language": "en-US", |
||||
"region": "US", |
||||
"quoteType": "EQUITY", |
||||
"quoteSourceName": "Delayed Quote", |
||||
"currency": "USD", |
||||
"twoHundredDayAverageChange": 101.65552, |
||||
"priceToBook": 5.2169247, |
||||
"averageDailyVolume10Day": 1171814, |
||||
"regularMarketPrice": 1214.38, |
||||
"regularMarketTime": 1534363202, |
||||
"regularMarketChange": -27.71997, |
||||
"regularMarketOpen": 1229.26, |
||||
"regularMarketDayHigh": 1235.17, |
||||
"regularMarketDayLow": 1209.51, |
||||
"regularMarketVolume": 1645951, |
||||
"fiftyTwoWeekHighChange": -59.51001, |
||||
"regularMarketDayRange": "1209.51 - 1235.17", |
||||
"fiftyTwoWeekHighChangePercent": -0.046715185, |
||||
"exchange": "NMS", |
||||
"exchangeTimezoneShortName": "EDT", |
||||
"forwardPE": 25.299583, |
||||
"postMarketTime": 1534376624, |
||||
"fiftyDayAverageChangePercent": 0.018335816, |
||||
"epsTrailingTwelveMonths": 23.155, |
||||
"priceHint": 2, |
||||
"postMarketChange": 1.1199951, |
||||
"bookValue": 232.777, |
||||
"twoHundredDayAverageChangePercent": 0.09135731, |
||||
"epsForward": 48, |
||||
"fiftyTwoWeekHigh": 1273.89, |
||||
"postMarketPrice": 1215.5, |
||||
"esgPopulated": false, |
||||
"tradeable": true, |
||||
"sourceInterval": 15, |
||||
"fiftyDayAverage": 1192.5143, |
||||
"marketState": "PREPRE", |
||||
"shortName": "Alphabet Inc.", |
||||
"ask": 0, |
||||
"gmtOffSetMilliseconds": -14400000, |
||||
"exchangeTimezoneName": "America/New_York", |
||||
"fiftyDayAverageChange": 21.865723, |
||||
"regularMarketChangePercent": -2.231702, |
||||
"fiftyTwoWeekLowChangePercent": 0.34423286, |
||||
"postMarketChangePercent": 0.09222773, |
||||
"askSize": 9, |
||||
"exchangeDataDelayedBy": 0, |
||||
"sharesOutstanding": 349883008, |
||||
"fullExchangeName": "NasdaqGS", |
||||
"twoHundredDayAverage": 1112.7245, |
||||
"averageDailyVolume3Month": 1445187, |
||||
"trailingPE": 52.44569, |
||||
"financialCurrency": "USD", |
||||
"fiftyTwoWeekRange": "903.4 - 1273.89", |
||||
"marketCap": 849186848768, |
||||
"bidSize": 9, |
||||
"bid": 0, |
||||
"regularMarketPreviousClose": 1242.1, |
||||
"market": "us_market", |
||||
"fiftyTwoWeekLowChange": 310.97998, |
||||
"messageBoardId": "finmb_29096", |
||||
"longName": "Alphabet Inc.", |
||||
"fiftyTwoWeekLow": 903.4, |
||||
"symbol": "GOOG" |
||||
} |
||||
], |
||||
"error": null |
||||
} |
||||
} |
||||
Loading…
Reference in new issue