You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
mop/yahoo_quotes.go

784 lines
23 KiB

// 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"
"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
}
// 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
}
// 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,
}
}
// 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"`
}
// 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() {
//fmt.Println("save")
//fmt.Println(quotes.stocks)
//fmt.Println(quotes.profile.mode)
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...)
}
// 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) Sendstockgraphreq(index int, istime bool) {
if index > len(quotes.totalstocks){
return
}
topic := "my/topic"
itemsel := quotes.getitembyscode(quotes.totalstocks[index-1].Scode)
data := map[string]interface{}{
"scode": quotes.totalstocks[index-1].Scode,
"tier": 0,
"daysback": itemsel.Daysback,
"stdprice": itemsel.Enterprice,
"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) 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})
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
}
// this will parse the json objects
func (quotes *Quotes) parse2(body []byte, res map[string]*stock.Stock) (*Quotes, error) {
// response -> quoteResponse -> result|error (array) -> map[string]interface{}
// Stocks has non-int things
// d := map[string]map[string][]Stock{}
// some of these are numbers vs strings
// d := map[string]map[string][]map[string]string{}
//d := map[string]map[string][]map[string]interface{}{}
//err := json.Unmarshal(body, &d)
//if err != nil {
// return nil, err
//}
//results := d["quoteResponse"]["result"]
/*
quotes.stocks = make([]Stock, len(results))
for i, raw := range results {
result := map[string]string{}
for k, v := range raw {
switch v.(type) {
case string:
result[k] = v.(string)
case float64:
result[k] = float2Str(v.(float64))
default:
result[k] = fmt.Sprintf("%v", v)
}
}
quotes.stocks[i].Ticker = result["symbol"]
quotes.stocks[i].LastTrade = result["regularMarketPrice"]
quotes.stocks[i].Change = result["regularMarketChange"]
quotes.stocks[i].ChangePct = result["regularMarketChangePercent"]
quotes.stocks[i].Open = result["regularMarketOpen"]
quotes.stocks[i].Low = result["regularMarketDayLow"]
quotes.stocks[i].High = result["regularMarketDayHigh"]
quotes.stocks[i].Low52 = result["fiftyTwoWeekLow"]
quotes.stocks[i].High52 = result["fiftyTwoWeekHigh"]
quotes.stocks[i].Volume = result["regularMarketVolume"]
quotes.stocks[i].AvgVolume = result["averageDailyVolume10Day"]
quotes.stocks[i].PeRatio = result["trailingPE"]
// TODO calculate rt
quotes.stocks[i].PeRatioX = result["trailingPE"]
quotes.stocks[i].Dividend = result["trailingAnnualDividendRate"]
quotes.stocks[i].Yield = result["trailingAnnualDividendYield"]
quotes.stocks[i].MarketCap = result["marketCap"]
// TODO calculate rt?
quotes.stocks[i].MarketCapX = result["marketCap"]
quotes.stocks[i].Currency = result["currency"]
quotes.stocks[i].PreOpen = result["preMarketChangePercent"]
quotes.stocks[i].AfterHours = result["postMarketChangePercent"]
fmt.Println(i)
fmt.Println("-------------------")
for k, v := range result {
fmt.Println(k, v)
}
fmt.Println("-------------------")
adv, err := strconv.ParseFloat(quotes.stocks[i].Change, 64)
quotes.stocks[i].Direction = 0
if err == nil {
if adv < 0.0 {
quotes.stocks[i].Direction = -1
} else if adv > 0.0 {
quotes.stocks[i].Direction = 1
}
}
}
// Use the "http.Get" function to fetch the URL and get the response
response, err := http.Get("http://119.29.166.226/q/dayjson/2023-04-20ml.json")//("http://119.29.166.226/q/dayjson/ml.json")
if err != nil {
// Handle error
fmt.Println(err)
}
defer response.Body.Close()
//respbody, err := ioutil.ReadAll(response.Body)
//if err != nil {
// handle error
//}
// Use the "json" package to decode the response body into a JSON object
var watchlist Watchlist
var scodes []string
err = json.NewDecoder(response.Body).Decode(&watchlist)
if err != nil {
// Handle error
fmt.Println(err)
}
//var data Data
//err = json.Unmarshal(respbody, &data)
if err != nil {
// Handle error
fmt.Println(err)
}
*/
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
//fmt.Println(q.Name, q.PreClose , q.LastPrice ,q.LastPrice , enterPrice)
if q.PreClose < q.LastPrice && q.LastPrice >= enterPrice && q.PreClose < enterPrice{
//fmt.Println(enterPrice)
scodes = append(scodes, item.Scode)
//fmt.Println(scodes)
}
wamap[item.Scode] = item
}
//fmt.Println(scodes)
}
//fmt.Println(scodes)
//canstocklen := len(scodes)
for _, item := range quotes.profile.Tickers {
if _, ok := res[item]; ok {
scodes = append(scodes, item)
}
}
//scodes = append(scodes, quotes.profile.Tickers...)
//fmt.Println(scodes)
//
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})
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].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
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"
}
}
//quotes.stocks[0].PeRatio = result["trailingPE"]
/*
// TODO calculate rt
quotes.stocks[0].PeRatioX = result["trailingPE"]
quotes.stocks[0].Dividend = result["trailingAnnualDividendRate"]
quotes.stocks[0].Yield = result["trailingAnnualDividendYield"]
quotes.stocks[0].MarketCap = result["marketCap"]
// TODO calculate rt?
quotes.stocks[0].MarketCapX = result["marketCap"]
quotes.stocks[0].Currency = result["currency"]
quotes.stocks[0].PreOpen = result["preMarketChangePercent"]
quotes.stocks[0].AfterHours = result["postMarketChangePercent"]
*/
//fmt.Println(quotes)
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(&quotes.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)
}