From c1f0b9c0898d87ca6184c8e2a61f8451941c1471 Mon Sep 17 00:00:00 2001 From: Hanzo Dev Date: Wed, 22 Jul 2026 00:44:38 -0700 Subject: [PATCH] =?UTF-8?q?feat(world/yahoo):=20forward=20validated=20rang?= =?UTF-8?q?e+interval=20=E2=86=92=20daily=20series?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- VERSION | 2 +- internal/world/handlers_markets.go | 34 ++++++++++++++++++++- internal/world/handlers_yahoo_range_test.go | 23 ++++++++++++++ package.json | 6 +++- 4 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 internal/world/handlers_yahoo_range_test.go diff --git a/VERSION b/VERSION index 24ade4ad..2df0671c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.4.43 \ No newline at end of file +2.4.39 diff --git a/internal/world/handlers_markets.go b/internal/world/handlers_markets.go index f6288cc8..61d9bfba 100644 --- a/internal/world/handlers_markets.go +++ b/internal/world/handlers_markets.go @@ -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 { diff --git a/internal/world/handlers_yahoo_range_test.go b/internal/world/handlers_yahoo_range_test.go new file mode 100644 index 00000000..2ac2a291 --- /dev/null +++ b/internal/world/handlers_yahoo_range_test.go @@ -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) + } + } +} diff --git a/package.json b/package.json index 40077ef1..187ac97a 100644 --- a/package.json +++ b/package.json @@ -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'",