// 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) 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 { 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 }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) //quotes.stocks[i].PreOpen = wamap[scode].Enterprice //quotes.stocks[i].AfterHours = strconv.Itoa(wamap[scode].Daysback) //quotes.stocks[i].Yield = wamap[scode].AnalyseDay } if inlist(quotes.profile.Tickers, scode) == true { quotes.stocks[i].AvgVolume = "M" } } 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) }