feat(world/yahoo): forward validated range+interval → daily series

handleYahooFinance now forwards a whitelisted range/interval to Yahoo's chart
endpoint (e.g. range=1y&interval=1d) instead of always returning the intraday
default. Absent params preserve prior behavior; the range keys the cache so
different spans of a symbol cache separately. Unlocks real 1-year daily
growth charts on lux.fund (was intraday-only).
This commit is contained in:
Hanzo Dev
2026-07-22 00:46:31 -07:00
parent c9f9b822aa
commit c1f0b9c089
4 changed files with 62 additions and 3 deletions
+1 -1
View File
@@ -1 +1 @@
2.4.43
2.4.39
+33 -1
View File
@@ -161,7 +161,16 @@ func (s *Server) handleYahooFinance(w http.ResponseWriter, r *http.Request) {
return
}
upstream := "https://query1.finance.yahoo.com/v8/finance/chart/" + urlQueryEscape(sym)
s.passthrough(w, "yahoo:"+sym, upstream, "application/json",
cacheKey := "yahoo:" + sym
// Forward a validated range/interval so callers can ask for a daily series
// (e.g. range=1y&interval=1d) instead of the intraday default. Absent params
// keep the prior behavior; the range is part of the cache key so different
// spans of the same symbol cache separately.
if q := yahooChartQuery(r); q != "" {
upstream += "?" + q
cacheKey += ":" + q
}
s.passthrough(w, cacheKey, upstream, "application/json",
"public, max-age=60, s-maxage=60, stale-while-revalidate=30",
map[string]string{"User-Agent": browserUA},
60*time.Second, 5*time.Minute,
@@ -170,6 +179,29 @@ func (s *Server) handleYahooFinance(w http.ResponseWriter, r *http.Request) {
})
}
var yahooRanges = map[string]bool{
"1d": true, "5d": true, "1mo": true, "3mo": true, "6mo": true,
"1y": true, "2y": true, "5y": true, "10y": true, "ytd": true, "max": true,
}
var yahooIntervals = map[string]bool{
"1m": true, "2m": true, "5m": true, "15m": true, "30m": true, "60m": true,
"90m": true, "1h": true, "1d": true, "5d": true, "1wk": true, "1mo": true, "3mo": true,
}
// yahooChartQuery returns a validated range/interval query string for Yahoo's
// chart endpoint, or "" when the caller supplies neither (preserving the intraday
// default). Only whitelisted values pass through.
func yahooChartQuery(r *http.Request) string {
parts := make([]string, 0, 2)
if rng := lower(trimSpace(r.URL.Query().Get("range"))); yahooRanges[rng] {
parts = append(parts, "range="+rng)
}
if itv := lower(trimSpace(r.URL.Query().Get("interval"))); yahooIntervals[itv] {
parts = append(parts, "interval="+itv)
}
return strings.Join(parts, "&")
}
func validateSymbol(sym string) string {
sym = upper(trimSpace(sym))
if sym == "" || len(sym) > 20 {
@@ -0,0 +1,23 @@
package world
import (
"net/http/httptest"
"testing"
)
func TestYahooChartQuery(t *testing.T) {
cases := []struct{ q, want string }{
{"?symbol=SMH&range=1y&interval=1d", "range=1y&interval=1d"},
{"?symbol=SMH", ""}, // default preserved
{"?symbol=SMH&range=bogus&interval=1d", "interval=1d"}, // bad range dropped
{"?symbol=SMH&range=1Y&interval=1D", "range=1y&interval=1d"}, // case-normalized
{"?symbol=SMH&range=5y", "range=5y"},
{"?symbol=SMH&interval=evil", ""}, // bad interval dropped
}
for _, c := range cases {
r := httptest.NewRequest("GET", "/v1/world/yahoo-finance"+c.q, nil)
if got := yahooChartQuery(r); got != c.want {
t.Errorf("yahooChartQuery(%q) = %q, want %q", c.q, got, c.want)
}
}
}
+5 -1
View File
@@ -2,7 +2,11 @@
"name": "@hanzo/world",
"description": "Hanzo World — real-time global intelligence dashboard.",
"private": true,
"version": "2.4.43",
<<<<<<< HEAD
"version": "2.4.39",
=======
"version": "2.4.38",
>>>>>>> d2b5a7d8 (feat(world/yahoo): forward validated range+interval daily series)
"type": "module",
"scripts": {
"lint:md": "markdownlint-cli2 '**/*.md'",