UNPKG

@baguskto/saham

Version:

MCP Server untuk data saham Indonesia (IDX) - Implementasi Node.js/TypeScript

272 lines 10.7 kB
"use strict"; /** * Historical Data Service for Indonesian Stock Market data * Integrates with Dataset-Saham-IDX repository */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HistoricalDataService = void 0; const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const github_api_1 = require("../utils/github-api"); const csv_parser_1 = require("./csv-parser"); const logger_1 = require("../utils/logger"); class HistoricalDataService { githubApi; dataDir; metadataDir; defaultCacheTTL = 24 * 60 * 60 * 1000; // 24 hours constructor() { this.githubApi = new github_api_1.GitHubApiService(); this.dataDir = path_1.default.join(process.cwd(), 'data', 'historical'); this.metadataDir = path_1.default.join(process.cwd(), 'data', 'metadata'); this.ensureDirectories(); } /** * Ensure required directories exist */ async ensureDirectories() { try { await promises_1.default.mkdir(this.dataDir, { recursive: true }); await promises_1.default.mkdir(this.metadataDir, { recursive: true }); } catch (error) { logger_1.logger.error('Failed to create directories:', error); } } /** * Get historical data for a specific stock */ async getStockData(ticker, options = {}) { const { useCache = true, cacheTTL = this.defaultCacheTTL, forceRefresh = false } = options; ticker = ticker.toUpperCase(); // Check cache first (unless force refresh) if (useCache && !forceRefresh) { const cachedData = await this.getCachedData(ticker, cacheTTL); if (cachedData) { logger_1.logger.info(`Using cached data for ${ticker}`); return cachedData; } } // Download fresh data from GitHub logger_1.logger.info(`Downloading fresh data for ${ticker}`); const csvData = await this.githubApi.downloadStockData(ticker); // Parse the CSV data const parsedData = csv_parser_1.CSVParser.parseStockCSV(csvData, ticker); // Cache the parsed data if (useCache) { await this.cacheData(ticker, parsedData, csvData); } return parsedData; } /** * Get historical data for multiple stocks */ async getMultipleStocksData(tickers, options = {}) { const results = {}; // Process stocks in parallel (but limit concurrency) const concurrencyLimit = 5; const chunks = this.chunkArray(tickers, concurrencyLimit); for (const chunk of chunks) { const promises = chunk.map(async (ticker) => { try { const data = await this.getStockData(ticker, options); results[ticker.toUpperCase()] = data; } catch (error) { logger_1.logger.error(`Failed to get data for ${ticker}:`, error); // Continue with other stocks } }); await Promise.all(promises); } return results; } /** * Get available stock tickers from the repository */ async getAvailableStocks() { try { const info = await this.githubApi.getRepositoryInfo(); return info.availableStocks; } catch (error) { logger_1.logger.error('Failed to get available stocks:', error); return []; } } /** * Get repository information */ async getRepositoryInfo() { return this.githubApi.getRepositoryInfo(); } /** * Check if stock data is available */ async isStockAvailable(ticker) { return this.githubApi.isStockAvailable(ticker); } /** * Get stock data for a specific time period */ async getStockDataForPeriod(ticker, period, options = {}) { const fullData = await this.getStockData(ticker, options); return csv_parser_1.CSVParser.getDataForPeriod(fullData, period); } /** * Get stock data for a custom date range */ async getStockDataForDateRange(ticker, startDate, endDate, options = {}) { const fullData = await this.getStockData(ticker, options); return csv_parser_1.CSVParser.filterByDateRange(fullData, startDate, endDate); } /** * Get metadata for cached stocks */ async getCachedStockMetadata() { try { const files = await promises_1.default.readdir(this.metadataDir); const metadataFiles = files.filter(f => f.endsWith('.json')); const metadata = []; for (const file of metadataFiles) { try { const filePath = path_1.default.join(this.metadataDir, file); const content = await promises_1.default.readFile(filePath, 'utf-8'); const meta = JSON.parse(content); // Convert date strings back to Date objects meta.lastUpdated = new Date(meta.lastUpdated); meta.dateRange.start = new Date(meta.dateRange.start); meta.dateRange.end = new Date(meta.dateRange.end); metadata.push(meta); } catch (error) { logger_1.logger.warn(`Failed to read metadata for ${file}:`, error); } } return metadata; } catch (error) { logger_1.logger.error('Failed to get cached metadata:', error); return []; } } /** * Clear cache for a specific stock or all stocks */ async clearCache(ticker) { try { if (ticker) { // Clear specific stock ticker = ticker.toUpperCase(); const dataFile = path_1.default.join(this.dataDir, `${ticker}.json`); const metadataFile = path_1.default.join(this.metadataDir, `${ticker}.json`); await Promise.all([ promises_1.default.unlink(dataFile).catch(() => { }), promises_1.default.unlink(metadataFile).catch(() => { }) ]); logger_1.logger.info(`Cleared cache for ${ticker}`); } else { // Clear all cache const [dataFiles, metadataFiles] = await Promise.all([ promises_1.default.readdir(this.dataDir).catch(() => []), promises_1.default.readdir(this.metadataDir).catch(() => []) ]); const deletePromises = [ ...dataFiles.map(f => promises_1.default.unlink(path_1.default.join(this.dataDir, f)).catch(() => { })), ...metadataFiles.map(f => promises_1.default.unlink(path_1.default.join(this.metadataDir, f)).catch(() => { })) ]; await Promise.all(deletePromises); logger_1.logger.info('Cleared all cache'); } } catch (error) { logger_1.logger.error('Failed to clear cache:', error); } } /** * Get cached data if available and not expired */ async getCachedData(ticker, cacheTTL) { try { const dataFile = path_1.default.join(this.dataDir, `${ticker}.json`); const metadataFile = path_1.default.join(this.metadataDir, `${ticker}.json`); // Check if files exist const [dataStats, metadataStats] = await Promise.all([ promises_1.default.stat(dataFile).catch(() => null), promises_1.default.stat(metadataFile).catch(() => null) ]); if (!dataStats || !metadataStats) { return null; } // Check if cache is expired const age = Date.now() - dataStats.mtime.getTime(); if (age > cacheTTL) { logger_1.logger.debug(`Cache expired for ${ticker} (age: ${age}ms, TTL: ${cacheTTL}ms)`); return null; } // Read cached data const [dataContent] = await Promise.all([ promises_1.default.readFile(dataFile, 'utf-8'), promises_1.default.readFile(metadataFile, 'utf-8') ]); const data = JSON.parse(dataContent); // Convert date strings back to Date objects data.startDate = new Date(data.startDate); data.endDate = new Date(data.endDate); data.dataPoints = data.dataPoints.map(point => ({ ...point, date: new Date(point.date) })); return data; } catch (error) { logger_1.logger.debug(`Failed to read cached data for ${ticker}:`, error); return null; } } /** * Cache parsed data and metadata */ async cacheData(ticker, data, rawCsv) { try { const dataFile = path_1.default.join(this.dataDir, `${ticker}.json`); const metadataFile = path_1.default.join(this.metadataDir, `${ticker}.json`); // Create metadata const metadata = { ticker, lastUpdated: new Date(), dataPoints: data.totalPoints, dateRange: { start: data.startDate, end: data.endDate }, fileSize: Buffer.byteLength(rawCsv, 'utf-8') }; // Write files await Promise.all([ promises_1.default.writeFile(dataFile, JSON.stringify(data, null, 2)), promises_1.default.writeFile(metadataFile, JSON.stringify(metadata, null, 2)) ]); logger_1.logger.debug(`Cached data for ${ticker} (${data.totalPoints} points)`); } catch (error) { logger_1.logger.error(`Failed to cache data for ${ticker}:`, error); } } /** * Utility function to chunk array into smaller arrays */ chunkArray(array, chunkSize) { const chunks = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } return chunks; } } exports.HistoricalDataService = HistoricalDataService; //# sourceMappingURL=historical-data-service.js.map