fix(barchart): add CSRF retry and mostActive fallback to flow command (#51)

The flow command returned no data because:
1. The CSRF token may not be in the DOM yet when Angular is still
   initializing — add a polling loop (up to 5s) to wait for it
2. The unusual_activity list is empty outside market hours — fall back
   to the mostActive list which always has data
3. Remove the DOM table fallback that never matched (barchart uses
   Angular components, not standard <tr> elements)
This commit is contained in:
Alex Yang authored and GitHub committed 2026-03-18 14:43:51 +08:00
1 parent f102501e4a
commit 2d52abde7c
1 file changed
+57 -58
+57 -58
View File
@@ -30,81 +30,80 @@ cli({
(async () => {
const limit = ${limit};
const typeFilter = '${optionType}'.toLowerCase();
const csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
const headers = { 'X-CSRF-TOKEN': csrf };
// Wait for CSRF token to appear (Angular may inject it after initial render)
let csrf = '';
for (let i = 0; i < 10; i++) {
csrf = document.querySelector('meta[name="csrf-token"]')?.content || '';
if (csrf) break;
await new Promise(r => setTimeout(r, 500));
}
if (!csrf) return { error: 'no-csrf' };
const headers = { 'X-CSRF-TOKEN': csrf };
const fields = [
'baseSymbol','strikePrice','expirationDate','optionType',
'lastPrice','volume','openInterest','volumeOpenInterestRatio','volatility',
].join(',');
// Fetch extra rows when filtering by type since server-side filter may not work
// Fetch extra rows when filtering by type since server-side filter doesn't work
const fetchLimit = typeFilter !== 'all' ? limit * 3 : limit;
try {
const url = '/proxies/core-api/v1/options/get?list=options.unusual_activity.stocks.us'
+ '&fields=' + fields
+ '&orderBy=volumeOpenInterestRatio&orderDir=desc'
+ '&raw=1&limit=' + fetchLimit;
const resp = await fetch(url, { credentials: 'include', headers });
if (resp.ok) {
// Try unusual_activity first, fall back to mostActive (unusual_activity is
// empty outside market hours)
const lists = [
'options.unusual_activity.stocks.us',
'options.mostActive.us',
];
for (const list of lists) {
try {
const url = '/proxies/core-api/v1/options/get?list=' + list
+ '&fields=' + fields
+ '&orderBy=volumeOpenInterestRatio&orderDir=desc'
+ '&raw=1&limit=' + fetchLimit;
const resp = await fetch(url, { credentials: 'include', headers });
if (!resp.ok) continue;
const d = await resp.json();
let items = d?.data || [];
if (items.length > 0) {
// Apply client-side type filter
if (typeFilter !== 'all') {
items = items.filter(i => {
const t = ((i.raw || i).optionType || '').toLowerCase();
return t === typeFilter;
});
}
return items.slice(0, limit).map(i => {
const r = i.raw || i;
return {
symbol: r.baseSymbol || r.symbol,
type: r.optionType,
strike: r.strikePrice,
expiration: r.expirationDate,
last: r.lastPrice,
volume: r.volume,
openInterest: r.openInterest,
volOiRatio: r.volumeOpenInterestRatio,
iv: r.volatility,
};
if (items.length === 0) continue;
// Apply client-side type filter
if (typeFilter !== 'all') {
items = items.filter(i => {
const t = ((i.raw || i).optionType || '').toLowerCase();
return t === typeFilter;
});
}
}
} catch(e) {}
// Fallback: parse from DOM table
try {
const rows = document.querySelectorAll('tr[data-ng-repeat], tbody tr');
const results = [];
for (const row of rows) {
const cells = row.querySelectorAll('td');
if (cells.length < 6) continue;
const getText = (idx) => cells[idx]?.textContent?.trim() || null;
results.push({
symbol: getText(0),
type: getText(1),
strike: getText(2),
expiration: getText(3),
last: getText(4),
volume: getText(5),
openInterest: cells.length > 6 ? getText(6) : null,
volOiRatio: cells.length > 7 ? getText(7) : null,
iv: cells.length > 8 ? getText(8) : null,
return items.slice(0, limit).map(i => {
const r = i.raw || i;
return {
symbol: r.baseSymbol || r.symbol,
type: r.optionType,
strike: r.strikePrice,
expiration: r.expirationDate,
last: r.lastPrice,
volume: r.volume,
openInterest: r.openInterest,
volOiRatio: r.volumeOpenInterestRatio,
iv: r.volatility,
};
});
if (results.length >= limit) break;
}
return results;
} catch(e) {
return [];
} catch(e) {}
}
return [];
})()
`);
if (!data || !Array.isArray(data)) return [];
if (!data) return [];
if (data.error === 'no-csrf') {
throw new Error('Could not extract CSRF token from barchart.com. Make sure you are logged in.');
}
if (!Array.isArray(data)) return [];
return data.slice(0, limit).map(r => ({
symbol: r.symbol || '',