package ohlc import ( "encoding/json" "fmt" "os" "sort" "time" ) // exchangeTimezone returns the exchange's IANA zone (from meta), falling back // to the most recent gmtoffset, then UTC. The old code formatted dates in the // machine's local timezone, which shifts dates by a day for any exchange in // a different zone. func exchangeTimezone(result map[string]interface{}) *time.Location { if meta, ok := result["meta"].(map[string]interface{}); ok { if name, ok := meta["exchangeTimezoneName"].(string); ok && name != "" { if loc, err := time.LoadLocation(name); err == nil { return loc } } if off, ok := meta["gmtoffset"].(float64); ok { return time.FixedZone("gmt", int(off)) } } return time.UTC } // eventRow is one dividend/capital-gain/split, flattened for sorting. type eventRow struct { epoch float64 date string amount float64 ratio string // splits only } // eventRows flattens an events map, sorts rows by date (JSON object iteration // order is random, so sorting is required for stable output) and formats each // date in the exchange zone. kind is used in error messages. func eventRows(name, kind string, events map[string]interface{}, loc *time.Location) []eventRow { rows := make([]eventRow, 0, len(events)) for _, v := range events { v2, ok := v.(map[string]interface{}) if !ok { fmt.Println(name + ": Bad format (" + kind + ")") return nil } t, ok := v2["date"].(float64) if !ok { fmt.Println(name + ": Bad format (" + kind + " date)") return nil } r := eventRow{epoch: t, date: time.Unix(int64(t), 0).In(loc).Format("2006-01-02")} if v2["amount"] != nil { r.amount = v2["amount"].(float64) } else if kind != "split" { fmt.Println(name + ": Bad format (" + kind + " amount)") return nil } if v2["splitRatio"] != nil { r.ratio = v2["splitRatio"].(string) } else if kind == "split" { fmt.Println(name + ": Bad format (split ratio)") return nil } rows = append(rows, r) } sort.Slice(rows, func(i, j int) bool { return rows[i].epoch < rows[j].epoch }) return rows } func Conv(name string) { text, err := os.ReadFile(name + ".json") if err != nil { fmt.Printf("%s: %s\n", name, err) return } var i interface{} err = json.Unmarshal(text, &i) if err != nil { fmt.Printf("%s: %s\n", name, err) return } j, ok := i.(map[string]interface{}) if !ok { fmt.Println(name + ": Bad format") return } j, ok = j["chart"].(map[string]interface{}) if !ok { fmt.Println(name + ": Bad format (chart)") return } x, ok := j["result"].([]interface{}) if !ok { fmt.Println(name + ": Bad format (result)") return } result, ok := x[0].(map[string]interface{}) if !ok { fmt.Println(name + ": Bad format (result[0])") return } timestamp, ok := result["timestamp"].([]interface{}) if !ok { fmt.Println(name + ": Bad format (timestamp)") return } indicators, ok := result["indicators"].(map[string]interface{}) if !ok { fmt.Println(name + ": Bad format (indicators)") return } events, ok := result["events"].(map[string]interface{}) dividends, ok := events["dividends"].(map[string]interface{}) capgains, ok := events["capitalGains"].(map[string]interface{}) splits, ok := events["splits"].(map[string]interface{}) x, ok = indicators["quote"].([]interface{}) if !ok { fmt.Println(name + ": Bad format (quote)") return } quote, ok := x[0].(map[string]interface{}) if !ok { fmt.Println(name + ": Bad format (quote[0])") return } x, ok = indicators["adjclose"].([]interface{}) if !ok { fmt.Println(name + ": Bad format (adjclose)") return } a, ok := x[0].(map[string]interface{}) if !ok { fmt.Println(name + ": Bad format (adjclose[0])") return } adjclose, ok := a["adjclose"].([]interface{}) if !ok { fmt.Println(name + ": Bad format (adjclose[adjclose])") return } open, ok := quote["open"].([]interface{}) if !ok { fmt.Println(name + ": Bad format (open)") return } high, ok := quote["high"].([]interface{}) if !ok { fmt.Println(name + ": Bad format (high)") return } low, ok := quote["low"].([]interface{}) if !ok { fmt.Println(name + ": Bad format (low)") return } close, ok := quote["close"].([]interface{}) if !ok { fmt.Println(name + ": Bad format (close)") return } volume, ok := quote["volume"].([]interface{}) if !ok { fmt.Println(name + ": Bad format (volume)") return } loc := exchangeTimezone(result) out, err := os.Create(name + "-history.csv") if err != nil { fmt.Printf("%s: %s\n", name, err) os.Exit(-1) } defer out.Close() out.WriteString("Date,Open,High,Low,Close,Adj Close,Volume\n") // Yahoo occasionally emits the same timestamp twice; keep the last one. seen := make(map[int64]int, len(timestamp)) for k, v := range timestamp { seen[int64(v.(float64))] = k } for k, v := range timestamp { if seen[int64(v.(float64))] != k { continue } if open[k] == nil { continue } vol := 0 if volume[k] != nil { vol = int(volume[k].(float64)) } out.WriteString(fmt.Sprintf("%s,%f,%f,%f,%f,%f,%d\n", time.Unix(int64(v.(float64)), 0).In(loc).Format("2006-01-02"), open[k], high[k], low[k], close[k], adjclose[k], vol)) } if dividends != nil { out, err = os.Create(name + "-dividend.csv") if err != nil { fmt.Printf("%s: %s\n", name, err) os.Exit(-1) } defer out.Close() out.WriteString("Date,Dividends\n") for _, r := range eventRows(name, "dividend", dividends, loc) { out.WriteString(fmt.Sprintf("%s,%f\n", r.date, r.amount)) } } if capgains != nil { out, err = os.Create(name + "-capitalGain.csv") if err != nil { fmt.Printf("%s: %s\n", name, err) os.Exit(-1) } defer out.Close() out.WriteString("Date,Capital Gains\n") for _, r := range eventRows(name, "capital gain", capgains, loc) { out.WriteString(fmt.Sprintf("%s,%f\n", r.date, r.amount)) } } if splits != nil { out, err = os.Create(name + "-split.csv") if err != nil { fmt.Printf("%s: %s\n", name, err) os.Exit(-1) } defer out.Close() out.WriteString("Date,Stock Splits\n") for _, r := range eventRows(name, "split", splits, loc) { out.WriteString(fmt.Sprintf("%s,%s\n", r.date, r.ratio)) } } }