UNPKG

maher-tradingview-api

Version:

Tradingview instant stocks API, indicator alerts, trading bot, and more !

827 lines (715 loc) 28.6 kB
/* eslint-disable linebreak-style */ /* eslint-disable no-plusplus */ /* eslint-disable no-unused-vars */ /* eslint-disable prefer-const */ /* eslint-disable no-undef */ /** * data-sources-manager.js * مدير مصادر البيانات المتعددة للرسوم البيانية * يتيح التبديل بين مصادر مختلفة في حالة فشل المصدر الرئيسي */ // تعريف مصادر البيانات المتاحة const DATA_SOURCES = { PRIMARY: 'tradingview', // المصدر الرئيسي - TradingView API SECONDARY: 'alphavantage', // المصدر الثانوي - Alpha Vantage API FALLBACK: 'yahoo', // المصدر الاحتياطي - Yahoo Finance HISTORICAL: 'iex', // مصدر بيانات تاريخية - IEX Cloud REALTIME: 'finnhub' // بيانات فورية - Finnhub }; // مفاتيح API (يتم تحميلها من متغيرات البيئة) const API_KEYS = { ALPHAVANTAGE: 'HQRYOMVV0758HND5', FINNHUB: 'd0ka9a9r01qn937jjnn0d0ka9a9r01qn937jjnng', IEX: '' // TODO: Add IEX API key }; // المصدر الحالي النشط let currentSource = DATA_SOURCES.PRIMARY; // سجل محاولات الفشل لكل مصدر const failureAttempts = { [DATA_SOURCES.PRIMARY]: 0, [DATA_SOURCES.SECONDARY]: 0, [DATA_SOURCES.FALLBACK]: 0, [DATA_SOURCES.HISTORICAL]: 0, [DATA_SOURCES.REALTIME]: 0 }; // الحد الأقصى لمحاولات الفشل قبل التبديل إلى مصدر آخر const MAX_FAILURE_ATTEMPTS = 3; /** * حالة مصادر البيانات ومؤشراتها */ const sourcesStatus = { [DATA_SOURCES.PRIMARY]: { available: true, lastCheck: Date.now(), errorCount: 0 }, [DATA_SOURCES.SECONDARY]: { available: true, lastCheck: null, errorCount: 0 }, [DATA_SOURCES.FALLBACK]: { available: true, lastCheck: null, errorCount: 0 }, [DATA_SOURCES.HISTORICAL]: { available: true, lastCheck: null, errorCount: 0 }, [DATA_SOURCES.REALTIME]: { available: true, lastCheck: null, errorCount: 0 } }; /** * الحصول على المصدر الحالي النشط * @return {string} رمز المصدر الحالي */ function getCurrentSource() { return currentSource; } /** * تعيين مصدر البيانات النشط * @param {string} source - رمز المصدر من كائن DATA_SOURCES */ function setDataSource(source) { if (Object.values(DATA_SOURCES).includes(source)) { currentSource = source; console.log(`تم تغيير مصدر البيانات إلى: ${source}`); // إرسال حدث تغيير مصدر البيانات const event = new CustomEvent('dataSourceChanged', { detail: { source: source } }); document.dispatchEvent(event); return true; } else { console.error(`مصدر البيانات غير صالح: ${source}`); return false; } } /** * تسجيل فشل في مصدر البيانات الحالي * والانتقال إلى المصدر التالي إذا تجاوزنا الحد الأقصى للمحاولات */ function reportFailure() { failureAttempts[currentSource]++; console.warn(`تم تسجيل فشل في مصدر البيانات ${currentSource} (عدد المحاولات: ${failureAttempts[currentSource]})`); if (failureAttempts[currentSource] >= MAX_FAILURE_ATTEMPTS) { // انتقل إلى المصدر التالي switchToNextSource(); } } /** * إعادة تعيين عداد فشل المصدر المحدد * @param {string} source - رمز المصدر (اختياري، إذا لم يتم تحديده يتم استخدام المصدر الحالي) */ function resetFailureCount(source = currentSource) { if (failureAttempts[source] !== undefined) { failureAttempts[source] = 0; console.log(`تم إعادة تعيين عداد الفشل لمصدر البيانات: ${source}`); } } /** * الانتقال إلى المصدر التالي حسب الأولوية * @return {string} رمز المصدر الجديد */ function switchToNextSource() { const sources = Object.values(DATA_SOURCES); const currentIndex = sources.indexOf(currentSource); // البحث عن المصدر التالي الذي لديه أقل من الحد الأقصى للمحاولات for (let i = 1; i <= sources.length; i++) { const nextIndex = (currentIndex + i) % sources.length; const nextSource = sources[nextIndex]; if (failureAttempts[nextSource] < MAX_FAILURE_ATTEMPTS) { // تعيين المصدر الجديد setDataSource(nextSource); console.log(`تم الانتقال إلى مصدر البيانات التالي: ${nextSource}`); // تخزين المصدر الحالي في التخزين المحلي للمتصفح try { localStorage.setItem('preferredDataSource', nextSource); } catch (e) { console.warn('لا يمكن تخزين مصدر البيانات المفضل في التخزين المحلي:', e); } return nextSource; } } // إذا وصلنا إلى هنا، فجميع المصادر فشلت console.error('جميع مصادر البيانات استنفذت الحد الأقصى للمحاولات'); // إعادة ضبط عدادات الفشل وبدء من المصدر الرئيسي Object.keys(failureAttempts).forEach(source => { failureAttempts[source] = 0; }); setDataSource(DATA_SOURCES.PRIMARY); return DATA_SOURCES.PRIMARY; } /** * التحقق من توافر مصدر بيانات محدد * @param {string} source - مصدر البيانات للتحقق منه * @returns {Promise<boolean>} وعد يُحل لقيمة boolean تُظهر ما إذا كان المصدر متاحًا */ async function checkSourceAvailability(source) { try { let endpoint; switch (source) { case DATA_SOURCES.PRIMARY: // لا يمكن التحقق مباشرة، نفترض أنه متاح return true; case DATA_SOURCES.SECONDARY: endpoint = '/api/alphavantage?symbol=AAPL'; break; case DATA_SOURCES.FALLBACK: endpoint = '/api/yahoo?symbol=AAPL'; break; case DATA_SOURCES.HISTORICAL: endpoint = '/api/iex?symbol=AAPL'; break; case DATA_SOURCES.REALTIME: endpoint = '/api/finnhub?symbol=AAPL'; break; default: return false; } // تعيين وقت آخر فحص sourcesStatus[source].lastCheck = Date.now(); const response = await fetch(endpoint); if (!response.ok) { throw new Error(`فشل الاتصال بـ ${source}: ${response.status}`); } const data = await response.json(); // تحديث حالة المصدر sourcesStatus[source].available = true; sourcesStatus[source].errorCount = 0; return true; } catch (error) { console.error(`خطأ أثناء التحقق من توافر ${source}:`, error); // تحديث حالة المصدر sourcesStatus[source].available = false; sourcesStatus[source].errorCount++; return false; } } /** * التحقق من توافر جميع مصادر البيانات المتاحة * @returns {Promise<Object>} وعد يُظهر حالة كل مصدر */ async function checkAllSourcesAvailability() { const results = {}; for (const source of Object.values(DATA_SOURCES)) { results[source] = await checkSourceAvailability(source); } return results; } /** * تمكين مصدر بديل بناءً على معلومات الحالة والأولوية * @returns {string|null} المصدر الجديد إذا تم التبديل، أو null إذا لم يتم */ function enableFallbackSource() { // بناء قائمة بالمصادر المتاحة حسب الأولوية const availableSources = Object.values(DATA_SOURCES).filter( source => sourcesStatus[source].available && source !== currentSource ); if (availableSources.length === 0) { console.warn('لا توجد مصادر بيانات بديلة متاحة حاليًا'); return null; } // اختيار المصدر الأعلى أولوية const newSource = availableSources[0]; setDataSource(newSource); // إرسال حدث بتغيير المصدر const event = new CustomEvent('dataSourceChanged', { detail: { source: newSource, reason: 'fallback_enabled', previousSource: currentSource } }); document.dispatchEvent(event); return newSource; } /** * محاولة التبديل إلى مصدر بديل في حالة فشل مصدر محدد * @param {string} failedSource - المصدر الذي فشل * @param {Error} error - كائن الخطأ (اختياري) * @returns {string|null} المصدر الجديد إذا تم التبديل، أو null إذا لم يتم */ function handleSourceFailure(failedSource, error = null) { if (error) { console.error(`فشل مصدر البيانات ${failedSource}:`, error); } else { console.warn(`تم الإبلاغ عن فشل مصدر البيانات ${failedSource}`); } // تحديث حالة المصدر sourcesStatus[failedSource].available = false; sourcesStatus[failedSource].errorCount++; // محاولة التبديل إلى مصدر بديل return switchToNextSource(); } /** * تنفيذ فحص دوري لتوافر مصادر البيانات * @param {number} interval - الفاصل الزمني بالمللي ثانية بين عمليات الفحص */ function startPeriodicHealthCheck(interval = 300000) { // افتراضيًا 5 دقائق setInterval(async () => { console.log('جارٍ إجراء فحص دوري لتوافر مصادر البيانات...'); const results = await checkAllSourcesAvailability(); // إذا كان المصدر الحالي غير متاح، قم بالتبديل if (!results[currentSource]) { console.warn(`المصدر الحالي ${currentSource} غير متاح، محاولة التبديل...`); switchToNextSource(); } // تحديث واجهة المستخدم بحالة المصادر updateDataSourcesStatus(); }, interval); } /** * تحديث حالة مصادر البيانات في واجهة المستخدم */ function updateDataSourcesStatus() { const container = document.querySelector('.data-sources-status'); if (!container) return; // إنشاء أو تحديث عناصر حالة المصدر for (const [source, status] of Object.entries(sourcesStatus)) { const sourceElement = document.getElementById(`source-status-${source}`); if (sourceElement) { // تحديث العنصر الموجود const statusClass = status.available ? 'available' : 'unavailable'; sourceElement.className = `data-source-status ${statusClass}`; sourceElement.textContent = status.available ? 'متاح' : 'غير متاح'; } else { // إنشاء عنصر جديد const sourceItem = document.createElement('div'); sourceItem.className = 'data-source-status-item'; sourceItem.innerHTML = ` <span class="data-source-name">${getSourceDisplayName(source)}</span> <span id="source-status-${source}" class="data-source-status ${status.available ? 'available' : 'unavailable'}"> ${status.available ? 'متاح' : 'غير متاح'} </span> `; container.appendChild(sourceItem); } } } /** * الحصول على اسم العرض لمصدر البيانات * @param {string} source - رمز المصدر * @returns {string} الاسم المعروض */ function getSourceDisplayName(source) { const names = { [DATA_SOURCES.PRIMARY]: 'TradingView API', [DATA_SOURCES.SECONDARY]: 'Alpha Vantage', [DATA_SOURCES.FALLBACK]: 'Yahoo Finance', [DATA_SOURCES.HISTORICAL]: 'IEX Cloud', [DATA_SOURCES.REALTIME]: 'Finnhub' }; return names[source] || source; } /** * التبديل إلى مصدر بيانات محدد يدويًا * @param {string} source - المصدر المطلوب الانتقال إليه * @returns {boolean} ما إذا كان التبديل ناجحًا */ function switchToSource(source) { if (!Object.values(DATA_SOURCES).includes(source)) { console.error(`مصدر بيانات غير صالح: ${source}`); return false; } // التحقق من توافر المصدر if (!sourcesStatus[source].available && sourcesStatus[source].errorCount > 0) { console.warn(`المصدر ${source} مُبلغ عنه بأنه غير متاح. محاولة الاتصال...`); } // محاولة التبديل const previousSource = currentSource; const success = setDataSource(source); if (success) { // إرسال حدث بتغيير المصدر const event = new CustomEvent('dataSourceChanged', { detail: { source, reason: 'manual_switch', previousSource } }); document.dispatchEvent(event); // تحديث واجهة المستخدم if (typeof updateDataSourceIndicators === 'function') { updateDataSourceIndicators(source); } return true; } return false; } /** * الحصول على بيانات من Alpha Vantage API * @param {string} symbol - رمز السهم * @param {string} timeframe - الإطار الزمني ('D' لليومي، 'W' للأسبوعي، إلخ) * @return {Promise} وعد بالبيانات */ async function fetchAlphaVantageData(symbol, timeframe = 'D') { const apiKey = API_KEYS.ALPHAVANTAGE; let interval = 'daily'; // افتراضي // تحويل الإطار الزمني إلى تنسيق Alpha Vantage switch (timeframe) { case 'D': interval = 'daily'; break; case 'W': interval = 'weekly'; break; case 'M': interval = 'monthly'; break; default: interval = 'daily'; } try { const response = await fetch(`https://www.alphavantage.co/query?function=TIME_SERIES_${interval.toUpperCase()}&symbol=${symbol}&apikey=${apiKey}`); if (!response.ok) { throw new Error(`Alpha Vantage API error: ${response.status}`); } const data = await response.json(); const timeSeriesKey = `Time Series (${interval.charAt(0).toUpperCase() + interval.slice(1)})`; if (!data || !data[timeSeriesKey]) { throw new Error('بيانات غير صالحة من Alpha Vantage'); } // تحويل البيانات إلى تنسيق متوافق مع التطبيق const timeSeriesData = data[timeSeriesKey]; const formattedData = Object.entries(timeSeriesData).map(([date, values]) => ({ time: date, open: parseFloat(values['1. open']), high: parseFloat(values['2. high']), low: parseFloat(values['3. low']), close: parseFloat(values['4. close']), volume: parseFloat(values['5. volume']) })); // ترتيب البيانات تصاعدياً حسب التاريخ return formattedData.sort((a, b) => new Date(a.time) - new Date(b.time)); } catch (error) { console.error('خطأ في جلب بيانات Alpha Vantage:', error); reportFailure(); return null; } } /** * الحصول على بيانات من Yahoo Finance * @param {string} symbol - رمز السهم * @param {string} timeframe - الإطار الزمني ('D' لليومي، 'W' للأسبوعي، إلخ) * @return {Promise} وعد بالبيانات */ async function fetchYahooData(symbol, timeframe = 'D') { // تحويل الإطار الزمني إلى تنسيق Yahoo Finance let interval = '1d'; // افتراضي let range = '6mo'; // افتراضي switch (timeframe) { case 'D': interval = '1d'; range = '6mo'; break; case 'W': interval = '1wk'; range = '2y'; break; case 'M': interval = '1mo'; range = '5y'; break; case '60': interval = '60m'; range = '1mo'; break; case '30': interval = '30m'; range = '1mo'; break; case '15': interval = '15m'; range = '7d'; break; case '5': interval = '5m'; range = '5d'; break; case '1': interval = '1m'; range = '1d'; break; } try { const response = await fetch(`/api/yahoo?symbol=${symbol}&interval=${interval}&range=${range}`); if (!response.ok) { throw new Error(`Yahoo Finance API error: ${response.status}`); } const data = await response.json(); if (!data || !data.chart || !data.chart.result || data.chart.result.length === 0) { throw new Error('بيانات غير صالحة من Yahoo Finance'); } // تحويل البيانات إلى تنسيق متوافق مع التطبيق const result = data.chart.result[0]; const timestamps = result.timestamp; const quotes = result.indicators.quote[0]; const formattedData = timestamps.map((time, i) => ({ time: new Date(time * 1000).toISOString().split('T')[0], open: quotes.open[i], high: quotes.high[i], low: quotes.low[i], close: quotes.close[i], volume: quotes.volume[i] || 0 })); // ترتيب البيانات تصاعدياً حسب التاريخ return formattedData.sort((a, b) => new Date(a.time) - new Date(b.time)); } catch (error) { console.error('خطأ في جلب بيانات Yahoo Finance:', error); reportFailure(); return null; } } /** * الحصول على بيانات من IEX Cloud * @param {string} symbol - رمز السهم * @param {string} timeframe - الإطار الزمني ('D' لليومي، 'W' للأسبوعي، إلخ) * @return {Promise} وعد بالبيانات */ async function fetchIEXData(symbol, timeframe = 'D') { // تحويل الإطار الزمني إلى تنسيق IEX Cloud let range = '6m'; // افتراضي switch (timeframe) { case 'D': range = '6m'; break; case 'W': range = '1y'; break; case 'M': range = '5y'; break; case '60': range = '1m'; break; case '30': case '15': case '5': range = '5d'; break; case '1': range = '1d'; break; } try { const response = await fetch(`/api/iex?symbol=${symbol}&range=${range}`); if (!response.ok) { throw new Error(`IEX Cloud API error: ${response.status}`); } const data = await response.json(); if (!Array.isArray(data) || data.length === 0) { throw new Error('بيانات غير صالحة من IEX Cloud'); } // تحويل البيانات إلى تنسيق متوافق مع التطبيق const formattedData = data.map(candle => ({ time: candle.date, open: candle.open, high: candle.high, low: candle.low, close: candle.close, volume: candle.volume || 0 })); // ترتيب البيانات تصاعدياً حسب التاريخ return formattedData.sort((a, b) => new Date(a.time) - new Date(b.time)); } catch (error) { console.error('خطأ في جلب بيانات IEX Cloud:', error); reportFailure(); return null; } } /** * الحصول على بيانات من Finnhub * @param {string} symbol - رمز السهم * @param {string} timeframe - الإطار الزمني ('D' لليومي، 'W' للأسبوعي، إلخ) * @return {Promise} وعد بالبيانات */ async function fetchFinnhubData(symbol, timeframe = 'D') { // تحويل الإطار الزمني إلى تنسيق Finnhub let resolution = 'D'; // افتراضي let from = Math.floor(Date.now() / 1000 - 180 * 24 * 60 * 60); // 180 يوم للخلف (افتراضي) switch (timeframe) { case 'D': resolution = 'D'; from = Math.floor(Date.now() / 1000 - 365 * 24 * 60 * 60); // سنة واحدة break; case 'W': resolution = 'W'; from = Math.floor(Date.now() / 1000 - 3 * 365 * 24 * 60 * 60); // 3 سنوات break; case 'M': resolution = 'M'; from = Math.floor(Date.now() / 1000 - 5 * 365 * 24 * 60 * 60); // 5 سنوات break; case '60': resolution = '60'; from = Math.floor(Date.now() / 1000 - 30 * 24 * 60 * 60); // 30 يوم break; case '30': resolution = '30'; from = Math.floor(Date.now() / 1000 - 15 * 24 * 60 * 60); // 15 يوم break; case '15': resolution = '15'; from = Math.floor(Date.now() / 1000 - 10 * 24 * 60 * 60); // 10 أيام break; case '5': resolution = '5'; from = Math.floor(Date.now() / 1000 - 5 * 24 * 60 * 60); // 5 أيام break; case '1': resolution = '1'; from = Math.floor(Date.now() / 1000 - 24 * 60 * 60); // يوم واحد break; } const to = Math.floor(Date.now() / 1000); // الوقت الحالي try { const response = await fetch(`/api/finnhub?symbol=${symbol}&resolution=${resolution}&from=${from}&to=${to}`); if (!response.ok) { throw new Error(`Finnhub API error: ${response.status}`); } const data = await response.json(); if (!data || data.s !== 'ok' || !data.t || data.t.length === 0) { throw new Error('بيانات غير صالحة من Finnhub'); } // تحويل البيانات إلى تنسيق متوافق مع التطبيق const formattedData = data.t.map((timestamp, i) => ({ time: new Date(timestamp * 1000).toISOString().split('T')[0], open: data.o[i], high: data.h[i], low: data.l[i], close: data.c[i], volume: data.v[i] || 0 })); // ترتيب البيانات تصاعدياً حسب التاريخ return formattedData.sort((a, b) => new Date(a.time) - new Date(b.time)); } catch (error) { console.error('خطأ في جلب بيانات Finnhub:', error); reportFailure(); return null; } } /** * جلب بيانات الرسم البياني من المصدر الحالي * @param {string} symbol - رمز السهم * @param {string} timeframe - الإطار الزمني * @return {Promise} وعد بالبيانات */ async function fetchChartData(symbol, timeframe = 'D') { console.log(`جلب بيانات الرسم البياني لـ ${symbol} (${timeframe}) من المصدر: ${currentSource}`); try { let data = null; // استخدام المصدر الحالي لجلب البيانات switch (currentSource) { case DATA_SOURCES.PRIMARY: // استخدام TradingView API الأصلي (من خلال بوابة الخادم) data = await fetchTradingViewData(symbol, timeframe); break; case DATA_SOURCES.SECONDARY: data = await fetchAlphaVantageData(symbol, timeframe); break; case DATA_SOURCES.FALLBACK: data = await fetchYahooData(symbol, timeframe); break; case DATA_SOURCES.HISTORICAL: data = await fetchIEXData(symbol, timeframe); break; case DATA_SOURCES.REALTIME: data = await fetchFinnhubData(symbol, timeframe); break; default: throw new Error(`مصدر بيانات غير معروف: ${currentSource}`); } // التحقق من نجاح جلب البيانات if (!data || data.length === 0) { throw new Error(`فشل في جلب البيانات من المصدر ${currentSource}`); } // تم الحصول على البيانات بنجاح، إعادة تعيين عداد الفشل resetFailureCount(); // تخزين البيانات في ذاكرة التخزين المؤقت if (typeof cacheChartData === 'function') { cacheChartData(symbol, data); } // إرسال حدث تحميل البيانات if (typeof window.dispatchChartDataLoaded === 'function') { window.dispatchChartDataLoaded(symbol, data); } return data; } catch (error) { console.error(`فشل في جلب بيانات الرسم البياني من المصدر ${currentSource}:`, error); // تسجيل الفشل وتجربة المصدر التالي reportFailure(); // محاولة استخدام المصدر التالي if (failureAttempts[currentSource] >= MAX_FAILURE_ATTEMPTS) { const nextSource = switchToNextSource(); return fetchChartData(symbol, timeframe); // تكرار الطلب بالمصدر الجديد } // إذا كانت هناك بيانات مخزنة مؤقتًا، استخدمها كحل احتياطي if (typeof getCachedChartData === 'function') { const cachedData = getCachedChartData(symbol); if (cachedData) { console.log(`استخدام البيانات المخزنة مؤقتًا لـ ${symbol} كبديل`); return cachedData; } } return null; } } /** * استدعاء TradingView API الأصلي من خلال خادم الوسيط * ملاحظة: هذه الوظيفة تفترض وجود طرف خادم يتعامل مع TradingView API * @param {string} symbol - رمز السهم * @param {string} timeframe - الإطار الزمني * @return {Promise} وعد بالبيانات */ async function fetchTradingViewData(symbol, timeframe = 'D') { try { // هذا مجرد مثال، في النظام الحقيقي سيكون لديك نقطة نهاية API خاصة بك const response = await fetch(`/api/chart-data?symbol=${symbol}&timeframe=${timeframe}`); if (!response.ok) { throw new Error(`TradingView API error: ${response.status}`); } const data = await response.json(); if (!data || !data.data || data.data.length === 0) { throw new Error('بيانات غير صالحة من TradingView API'); } return data.data; } catch (error) { console.error('خطأ في جلب بيانات TradingView:', error); reportFailure(); return null; } } // إنشاء كائن مدير مصادر البيانات العام window.DataSourcesManager = { // الثوابت DATA_SOURCES, API_KEYS, // خصائص الحالة currentSource, failureAttempts, sourcesStatus, // الدوال الأساسية getCurrentSource, setDataSource, reportFailure, resetFailureCount, switchToNextSource, switchToSource, // دوال فحص الصحة checkSourceAvailability, checkAllSourcesAvailability, startPeriodicHealthCheck, // دوال المصادر البديلة enableFallbackSource, handleSourceFailure, // دوال جلب البيانات fetchAlphaVantageData, fetchYahooData, fetchIEXData, fetchFinnhubData, // دوال واجهة المستخدم updateDataSourcesStatus, getSourceDisplayName }; // بدء الفحص الدوري للصحة document.addEventListener('DOMContentLoaded', function() { console.log('تهيئة مدير مصادر البيانات...'); // استعادة المصدر المفضل من التخزين المحلي try { const savedSource = localStorage.getItem('preferredDataSource'); if (savedSource && Object.values(DATA_SOURCES).includes(savedSource)) { console.log(`استخدام مصدر البيانات المحفوظ: ${savedSource}`); setDataSource(savedSource); } } catch (e) { console.warn('لا يمكن استعادة مصدر البيانات المفضل:', e); } // بدء الفحص الدوري كل 5 دقائق startPeriodicHealthCheck(300000); });