@baguskto/saham
Version:
MCP Server untuk data saham Indonesia (IDX) - Implementasi Node.js/TypeScript
272 lines • 10.7 kB
JavaScript
;
/**
* 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