11// pages/api/market-data.js
22
3- // Free / no-key market data source (CSV ):
4- // https://stooq. com/q/l/?s=gs.us,^spx&f=sd2t2ohlcv&h&e=csv
3+ // Free / no-key market data source (JSON ):
4+ // https://query1.finance.yahoo. com/v8/finance/chart/GS
55// Note: data may be delayed / not real-time for all symbols.
66
77const SYMBOLS = [
8- { label : "GS" , stooq : "gs.us " , currency : "USD" , shortName : "Goldman Sachs Group, Inc." } ,
9- { label : "SPX" , stooq : "^spx " , currency : "USD" , shortName : "S&P 500 Index" } ,
10- { label : "UKX" , stooq : "^ftse " , currency : "GBP" , shortName : "FTSE 100 Index" } ,
11- { label : "NDX" , stooq : "^ndx " , currency : "USD" , shortName : "NASDAQ 100 Index" } ,
12- { label : "NKY" , stooq : "^n225 " , currency : "JPY" , shortName : "Nikkei 225" } ,
8+ { label : "GS" , yahoo : "GS " , currency : "USD" , shortName : "Goldman Sachs Group, Inc." } ,
9+ { label : "SPX" , yahoo : "^GSPC " , currency : "USD" , shortName : "S&P 500 Index" } ,
10+ { label : "UKX" , yahoo : "^FTSE " , currency : "GBP" , shortName : "FTSE 100 Index" } ,
11+ { label : "NDX" , yahoo : "^NDX " , currency : "USD" , shortName : "NASDAQ 100 Index" } ,
12+ { label : "NKY" , yahoo : "^N225 " , currency : "JPY" , shortName : "Nikkei 225" } ,
1313] ;
1414
1515// Keep your baseline snapshot and simulated fallback behavior.
@@ -32,51 +32,36 @@ const parseNumber = (v) => {
3232 return Number . isFinite ( n ) ? n : null ;
3333} ;
3434
35- const parseTimestampMs = ( dateStr , timeStr ) => {
36- if ( ! dateStr ) return null ;
37- const t = ( timeStr && timeStr . trim ( ) ) ? timeStr . trim ( ) : "00:00:00" ;
38- const d = new Date ( `${ dateStr } T${ t } ` ) ;
39- const ms = d . getTime ( ) ;
40- return Number . isFinite ( ms ) ? ms : null ;
41- } ;
42-
43- const parseStooqCsv = ( csvText ) => {
44- const text = ( csvText ?? "" ) . trim ( ) ;
45- if ( ! text ) return [ ] ;
46-
47- const lines = text . split ( / \r ? \n / ) . filter ( Boolean ) ;
48- if ( lines . length < 2 ) return [ ] ;
49-
50- // Expected header:
51- // Symbol,Date,Time,Open,High,Low,Close,Volume
52- const header = lines [ 0 ] . split ( "," ) . map ( ( s ) => s . trim ( ) ) ;
53- const idx = ( name ) => header . findIndex ( ( h ) => h . toLowerCase ( ) === name . toLowerCase ( ) ) ;
54-
55- const iSymbol = idx ( "Symbol" ) ;
56- const iDate = idx ( "Date" ) ;
57- const iTime = idx ( "Time" ) ;
58- const iOpen = idx ( "Open" ) ;
59- const iClose = idx ( "Close" ) ;
60-
61- if ( iSymbol < 0 || iDate < 0 || iOpen < 0 || iClose < 0 ) return [ ] ;
62-
63- const rows = [ ] ;
64- for ( let i = 1 ; i < lines . length ; i += 1 ) {
65- const cols = lines [ i ] . split ( "," ) . map ( ( s ) => s . trim ( ) ) ;
66- const symbol = cols [ iSymbol ] ?. toLowerCase ( ) ;
67- if ( ! symbol ) continue ;
35+ const parseYahooChart = ( payload ) => {
36+ const result = payload ?. chart ?. result ?. [ 0 ] ;
37+ if ( ! result ) return null ;
38+
39+ const timestamps = Array . isArray ( result . timestamp ) ? result . timestamp : [ ] ;
40+ const quote = result . indicators ?. quote ?. [ 0 ] ?? { } ;
41+ const opens = Array . isArray ( quote . open ) ? quote . open : [ ] ;
42+ const closes = Array . isArray ( quote . close ) ? quote . close : [ ] ;
43+
44+ let lastIdx = - 1 ;
45+ for ( let i = closes . length - 1 ; i >= 0 ; i -= 1 ) {
46+ if ( parseNumber ( closes [ i ] ) !== null ) {
47+ lastIdx = i ;
48+ break ;
49+ }
50+ }
51+ if ( lastIdx < 0 ) return null ;
6852
69- const open = parseNumber ( cols [ iOpen ] ) ;
70- const close = parseNumber ( cols [ iClose ] ) ;
53+ const close = parseNumber ( closes [ lastIdx ] ) ;
54+ const open = parseNumber ( opens [ lastIdx ] ) ;
55+ const tsSec = timestamps [ lastIdx ] ;
7156
72- const dateStr = cols [ iDate ] || null ;
73- const timeStr = iTime >= 0 ? ( cols [ iTime ] || null ) : null ;
74- const ts = parseTimestampMs ( dateStr , timeStr ) ;
57+ const timestamp = Number . isFinite ( tsSec ) ? tsSec * 1000 : Date . now ( ) ;
7558
76- rows . push ( { symbol, open, close, ts } ) ;
77- }
78-
79- return rows ;
59+ return {
60+ open,
61+ close,
62+ timestamp,
63+ shortName : result . meta ?. shortName ?? null ,
64+ } ;
8065} ;
8166
8267const simulateFromBaseline = ( error ) => {
@@ -129,13 +114,13 @@ const buildFallbackItem = (label, now, index) => {
129114 } ;
130115} ;
131116
132- const fetchTextWithTimeout = async ( url , ms ) => {
117+ const fetchWithTimeout = async ( url , ms ) => {
133118 const controller = new AbortController ( ) ;
134119 const timeout = setTimeout ( ( ) => controller . abort ( ) , ms ) ;
135120 try {
136121 const resp = await fetch ( url , {
137122 method : "GET" ,
138- headers : { Accept : "text/csv ,*/*" } ,
123+ headers : { Accept : "application/json ,*/*" } ,
139124 signal : controller . signal ,
140125 } ) ;
141126 return resp ;
@@ -144,6 +129,18 @@ const fetchTextWithTimeout = async (url, ms) => {
144129 }
145130} ;
146131
132+ const fetchYahooQuote = async ( symbol ) => {
133+ const url = `https://query1.finance.yahoo.com/v8/finance/chart/${ encodeURIComponent ( symbol ) } ?interval=1d&range=5d` ;
134+ const resp = await fetchWithTimeout ( url , FETCH_TIMEOUT_MS ) ;
135+ if ( ! resp . ok ) throw new Error ( `Yahoo request failed for ${ symbol } with ${ resp . status } ` ) ;
136+
137+ const json = await resp . json ( ) ;
138+ const parsed = parseYahooChart ( json ) ;
139+ if ( ! parsed ) throw new Error ( `Yahoo returned no usable chart data for ${ symbol } ` ) ;
140+
141+ return parsed ;
142+ } ;
143+
147144export default async function handler ( req , res ) {
148145 const now = Date . now ( ) ;
149146
@@ -155,48 +152,39 @@ export default async function handler(req, res) {
155152 }
156153
157154 try {
158- const stooqSymbols = SYMBOLS . map ( ( s ) => s . stooq ) . join ( "," ) ;
159- const url = `https://stooq.com/q/l/?s=${ encodeURIComponent (
160- stooqSymbols
161- ) } &f=sd2t2ohlcv&h&e=csv`;
162-
163- const resp = await fetchTextWithTimeout ( url , FETCH_TIMEOUT_MS ) ;
164- if ( ! resp . ok ) throw new Error ( `Stooq request failed with ${ resp . status } ` ) ;
165-
166- const csv = await resp . text ( ) ;
167- const rows = parseStooqCsv ( csv ) ;
168-
169- const bySymbol = new Map ( ) ;
170- for ( const r of rows ) bySymbol . set ( r . symbol , r ) ;
171-
172155 const missing = [ ] ;
173- const data = SYMBOLS . map ( ( cfg , index ) => {
174- const r = bySymbol . get ( cfg . stooq . toLowerCase ( ) ) ;
175- if ( ! r || r . open === null || r . close === null ) {
156+ const data = await Promise . all ( SYMBOLS . map ( async ( cfg , index ) => {
157+ try {
158+ const quote = await fetchYahooQuote ( cfg . yahoo ) ;
159+ if ( quote . open === null || quote . close === null ) {
160+ missing . push ( cfg . label ) ;
161+ return buildFallbackItem ( cfg . label , now , index ) ;
162+ }
163+
164+ const change = quote . close - quote . open ;
165+ const changePercent = quote . open ? ( change / quote . open ) * 100 : null ;
166+
167+ return {
168+ label : cfg . label ,
169+ price : quote . close ,
170+ currency : cfg . currency ,
171+ change : Number . isFinite ( change ) ? Number ( change . toFixed ( 2 ) ) : null ,
172+ changePercent : Number . isFinite ( changePercent ) ? Number ( changePercent . toFixed ( 2 ) ) : null ,
173+ timestamp : quote . timestamp ?? now ,
174+ shortName : quote . shortName ?? cfg . shortName ?? cfg . label ,
175+ } ;
176+ } catch {
176177 missing . push ( cfg . label ) ;
177178 return buildFallbackItem ( cfg . label , now , index ) ;
178179 }
180+ } ) ) . then ( ( rows ) => rows . filter ( Boolean ) ) ;
179181
180- const change = r . close - r . open ;
181- const changePercent = r . open ? ( change / r . open ) * 100 : null ;
182-
183- return {
184- label : cfg . label ,
185- price : r . close ,
186- currency : cfg . currency ,
187- change : Number . isFinite ( change ) ? Number ( change . toFixed ( 2 ) ) : null ,
188- changePercent : Number . isFinite ( changePercent ) ? Number ( changePercent . toFixed ( 2 ) ) : null ,
189- timestamp : r . ts ?? now ,
190- shortName : cfg . shortName ?? cfg . label ,
191- } ;
192- } ) . filter ( Boolean ) ;
193-
194- if ( ! data . length ) throw new Error ( "Stooq returned no usable data" ) ;
182+ if ( ! data . length ) throw new Error ( "Yahoo returned no usable data" ) ;
195183
196184 const payload = {
197185 data,
198186 meta : {
199- source : "stooq " ,
187+ source : "yahoo-finance " ,
200188 updatedAt : now ,
201189 partial : missing . length ? missing : null ,
202190 } ,
0 commit comments