initial tradingView handler
This commit is contained in:
parent
3843dc95a3
commit
f6a47f7a8c
8 changed files with 255 additions and 1 deletions
|
|
@ -11,6 +11,7 @@ import { CeoHandler } from './ceo/ceo.handler';
|
|||
import { IbHandler } from './ib/ib.handler';
|
||||
import { QMHandler } from './qm/qm.handler';
|
||||
import { WebShareHandler } from './webshare/webshare.handler';
|
||||
import { TradingViewHandler } from './tradingview/tradingview.handler';
|
||||
|
||||
// Add more handler imports as needed
|
||||
|
||||
|
|
@ -26,7 +27,7 @@ export async function initializeAllHandlers(serviceContainer: IServiceContainer)
|
|||
// The HandlerScanner in the DI container will handle the actual registration
|
||||
// We just need to ensure handlers are imported so their decorators run
|
||||
|
||||
const handlers = [CeoHandler, IbHandler, QMHandler, WebShareHandler];
|
||||
const handlers = [CeoHandler, IbHandler, QMHandler, WebShareHandler, TradingViewHandler];
|
||||
|
||||
logger.info('Handler imports loaded', {
|
||||
count: handlers.length,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,155 @@
|
|||
import type { TradingViewHandler } from '../tradingview.handler';
|
||||
import { TradingViewExchange } from '../shared/types';
|
||||
import { TRADINGVIEW_CONFIG } from '../shared/config';
|
||||
|
||||
export async function fetchExchanges(handler: TradingViewHandler): Promise<TradingViewExchange[] | null> {
|
||||
const { logger, mongodb } = handler;
|
||||
|
||||
if (!mongodb?.db) {
|
||||
logger.error('MongoDB not available');
|
||||
return null;
|
||||
}
|
||||
|
||||
const db = mongodb.db;
|
||||
|
||||
try {
|
||||
// 1. Fetch the HTML page
|
||||
const response = await fetch(TRADINGVIEW_CONFIG.DATA_COVERAGE_URL, {
|
||||
headers: {
|
||||
'User-Agent': TRADINGVIEW_CONFIG.USER_AGENT,
|
||||
},
|
||||
});
|
||||
|
||||
logger.debug('Response status:', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
url: response.url
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
logger.info('Fetched HTML length:', { length: html.length });
|
||||
|
||||
// Save HTML for debugging
|
||||
if (process.env.DEBUG_HTML) {
|
||||
const fs = await import('fs/promises');
|
||||
await fs.writeFile('/tmp/tradingview-data-coverage.html', html);
|
||||
logger.info('Saved HTML to /tmp/tradingview-data-coverage.html');
|
||||
}
|
||||
|
||||
// 2. Parse HTML to extract exchange data from the table
|
||||
const exchanges: TradingViewExchange[] = [];
|
||||
|
||||
// Match table rows containing exchange data
|
||||
const rowRegex = /<tr[^>]*class="row-qJcpoITA[^"]*"[^>]*>[\s\S]*?<\/tr>/g;
|
||||
const rows = html.match(rowRegex) || [];
|
||||
|
||||
logger.debug('Found table rows:', { count: rows.length });
|
||||
|
||||
for (const row of rows) {
|
||||
try {
|
||||
// Extract exchange ID
|
||||
const exchangeMatch = row.match(/alt="([^"]+)"/);
|
||||
const exchangeNameMatch = row.match(/<span[^>]*class="exchangeName-qJcpoITA"[^>]*>([^<]+)<\/span>/);
|
||||
const descriptionMatch = row.match(/<span[^>]*class="exchangeDescName-qJcpoITA"[^>]*>([^<]+)<\/span>/);
|
||||
const countryFlagMatch = row.match(/src="https:\/\/s3-symbol-logo\.tradingview\.com\/country\/([^.]+)\.svg"/);
|
||||
const countryNameMatch = row.match(/<img[^>]*class="[^"]*exchangeCountryFlag[^"]*"[^>]*\/>([^<]+)<\/span>/);
|
||||
|
||||
// Extract data types from badges
|
||||
const badgeMatches = row.matchAll(/<span[^>]*class="badge-PlSmolIm[^"]*"[^>]*>[\s\S]*?<span[^>]*class="content-PlSmolIm"[^>]*>([^<]+)<\/span>/g);
|
||||
const dataTypes: string[] = [];
|
||||
for (const match of badgeMatches) {
|
||||
dataTypes.push(match[1].toLowerCase());
|
||||
}
|
||||
|
||||
if (exchangeNameMatch && descriptionMatch && countryFlagMatch) {
|
||||
const exchange: TradingViewExchange = {
|
||||
exchange: descriptionMatch[1].toLowerCase().replace(/\s+/g, '_').replace(/[()]/g, ''),
|
||||
name: exchangeNameMatch[1],
|
||||
description: descriptionMatch[1],
|
||||
dataType: dataTypes,
|
||||
countryCode: countryFlagMatch[1].toUpperCase(),
|
||||
countryName: '', // Will be filled from alt text or other sources
|
||||
};
|
||||
|
||||
// Try to extract country name
|
||||
if (countryNameMatch) {
|
||||
exchange.countryName = countryNameMatch[1].trim();
|
||||
} else if (exchangeMatch) {
|
||||
// Use alt text to extract country name
|
||||
const parts = exchangeMatch[1].split(' ');
|
||||
if (parts.length > 1) {
|
||||
exchange.countryName = parts[0];
|
||||
}
|
||||
}
|
||||
|
||||
exchanges.push(exchange);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.debug('Failed to parse row', { error: e });
|
||||
}
|
||||
}
|
||||
|
||||
if (exchanges.length === 0) {
|
||||
throw new Error('No exchanges found in HTML');
|
||||
}
|
||||
|
||||
logger.info('Extracted exchanges from HTML', { count: exchanges.length });
|
||||
|
||||
// Batch update tvExchanges collection
|
||||
try {
|
||||
const batchOps = exchanges.map(exchange => ({
|
||||
updateOne: {
|
||||
filter: { exchange: exchange.exchange },
|
||||
update: {
|
||||
$set: {
|
||||
...exchange,
|
||||
updated_at: new Date(),
|
||||
},
|
||||
$setOnInsert: {
|
||||
created_at: new Date(),
|
||||
},
|
||||
},
|
||||
upsert: true,
|
||||
},
|
||||
}));
|
||||
|
||||
if (batchOps.length > 0) {
|
||||
await db.collection('tvExchanges').bulkWrite(batchOps);
|
||||
}
|
||||
|
||||
logger.info('TradingView exchanges saved to MongoDB');
|
||||
} catch (dbError) {
|
||||
logger.warn('Failed to save exchanges to MongoDB', { error: dbError });
|
||||
}
|
||||
|
||||
logger.info('TradingView exchanges fetched', {
|
||||
count: exchanges.length,
|
||||
byType: groupExchangesByType(exchanges),
|
||||
});
|
||||
|
||||
return exchanges;
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch TradingView exchanges', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function groupExchangesByType(exchanges: TradingViewExchange[]) {
|
||||
const groups: Record<string, number> = {};
|
||||
|
||||
for (const exchange of exchanges) {
|
||||
for (const type of exchange.dataType) {
|
||||
groups[type] = (groups[type] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './fetch-exchanges.action';
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export const TRADINGVIEW_CONFIG = {
|
||||
DATA_COVERAGE_URL: 'https://www.tradingview.com/data-coverage/',
|
||||
REQUEST_TIMEOUT: 30000,
|
||||
USER_AGENT: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
||||
};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export * from './types';
|
||||
export * from './config';
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
export interface TradingViewExchange {
|
||||
exchange: string;
|
||||
name: string;
|
||||
description: string;
|
||||
dataType: string[];
|
||||
countryCode: string; // All caps
|
||||
countryName: string;
|
||||
popularityRank?: number;
|
||||
}
|
||||
|
||||
// Raw data from TradingView
|
||||
export interface TradingViewRawExchange {
|
||||
text_id?: string;
|
||||
exchange: string;
|
||||
provider_id?: string | null;
|
||||
name: string;
|
||||
description: string;
|
||||
data_type: string[];
|
||||
country: string;
|
||||
country_name: string;
|
||||
popularity_rank?: number;
|
||||
[key: string]: any; // Other fields we don't need
|
||||
}
|
||||
|
||||
export interface ExchangeGroup {
|
||||
name: string;
|
||||
exchanges: TradingViewRawExchange[];
|
||||
}
|
||||
|
||||
export interface TradingViewData {
|
||||
locale: string;
|
||||
user: any;
|
||||
is_authenticated: boolean;
|
||||
exchanges: ExchangeGroup[];
|
||||
currency: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import {
|
||||
BaseHandler,
|
||||
Disabled,
|
||||
Handler,
|
||||
Operation,
|
||||
ScheduledOperation,
|
||||
} from '@stock-bot/handlers';
|
||||
import { fetchExchanges } from './actions';
|
||||
|
||||
@Disabled()
|
||||
@Handler('tradingview')
|
||||
export class TradingViewHandler extends BaseHandler {
|
||||
constructor(services: any) {
|
||||
super(services);
|
||||
}
|
||||
|
||||
@Operation('fetch-exchanges')
|
||||
async fetchExchanges(): Promise<unknown[] | null> {
|
||||
return fetchExchanges(this);
|
||||
}
|
||||
|
||||
@ScheduledOperation('tradingview-exchanges', '0 0 * * 0', {
|
||||
priority: 5,
|
||||
description: 'Fetch and update TradingView exchanges data',
|
||||
immediately: false,
|
||||
})
|
||||
@Disabled()
|
||||
async scheduledFetchExchanges(): Promise<unknown> {
|
||||
return this.fetchExchanges();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue