testing ratelimit

This commit is contained in:
Boki 2025-07-06 17:18:01 -04:00
parent 95b1381480
commit a616c92656
14 changed files with 411 additions and 3 deletions

View file

@ -182,7 +182,7 @@
},
"services": {
"dataIngestion": {
"port": 2001,
"port": 2009,
"workers": 5,
"queues": {
"ceo": { "concurrency": 2 },

View file

@ -0,0 +1,62 @@
import type { EodHandler } from '../eod.handler';
import { EOD_CONFIG } from '../shared/config';
/**
* Simulates fetching daily price data
* This is a high-volume operation that should be rate limited
*/
export async function fetchDailyPrices(
this: EodHandler
): Promise<unknown> {
const { logger, mongodb } = this;
// Generate fake data for testing
const symbols = ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'TSLA'];
const date = new Date().toISOString().split('T')[0];
logger.info('Fetching daily prices', {
symbols: symbols.length,
date,
timestamp: new Date().toISOString()
});
try {
// Simulate API call delay
await new Promise(resolve => setTimeout(resolve, 100 + Math.random() * 200));
// Simulate processing each symbol
const results = [];
for (const symbol of symbols) {
const priceData = {
symbol,
date,
open: 100 + Math.random() * 50,
high: 120 + Math.random() * 50,
low: 90 + Math.random() * 50,
close: 110 + Math.random() * 50,
volume: Math.floor(1000000 + Math.random() * 5000000),
timestamp: new Date().toISOString()
};
results.push(priceData);
logger.debug('Processed price data', { symbol, date });
}
// Simulate saving to database
if (mongodb) {
await mongodb.batchUpsert('testPrices',results, ['symbol', 'date']);
logger.info('Saved price data to MongoDB', { count: results.length });
}
return {
success: true,
processed: results.length,
date,
message: `Fetched daily prices for ${symbols.length} symbols`
};
} catch (error) {
logger.error('Failed to fetch daily prices', { error, symbols });
throw error;
}
}

View file

@ -0,0 +1,60 @@
import type { EodHandler } from '../eod.handler';
/**
* Simulates fetching fundamental data
* This is a medium-volume operation with moderate rate limits
*/
export async function fetchFundamentals(
this: EodHandler
): Promise<unknown> {
const { logger, mongodb } = this;
// Generate fake data for testing
const symbols = ['AAPL', 'GOOGL', 'MSFT'];
const metrics = ['pe_ratio', 'market_cap', 'revenue', 'earnings'];
logger.info('Fetching fundamentals', {
symbols: symbols.length,
metrics,
timestamp: new Date().toISOString()
});
try {
// Simulate API call delay (longer than prices)
await new Promise(resolve => setTimeout(resolve, 200 + Math.random() * 300));
// Simulate processing each symbol
const results = [];
for (const symbol of symbols) {
const fundamentalData = {
symbol,
pe_ratio: 15 + Math.random() * 20,
market_cap: Math.floor(1000000000 + Math.random() * 500000000000),
revenue: Math.floor(10000000 + Math.random() * 100000000000),
earnings: Math.floor(1000000 + Math.random() * 10000000000),
last_updated: new Date().toISOString()
};
results.push(fundamentalData);
logger.debug('Processed fundamental data', { symbol });
}
// Simulate saving to database
if (mongodb) {
const result = await mongodb.batchUpsert('testFundamentals', results, ['symbol']);
logger.info('Saved fundamental data to MongoDB', {
count: results.length
});
}
return {
success: true,
processed: results.length,
message: `Fetched fundamentals for ${symbols.length} symbols`
};
} catch (error) {
logger.error('Failed to fetch fundamentals', { error, symbols });
throw error;
}
}

View file

@ -0,0 +1,65 @@
import type { EodHandler } from '../eod.handler';
/**
* Simulates fetching news data
* This is a low-volume operation with strict rate limits
*/
export async function fetchNews(
this: EodHandler
): Promise<unknown> {
const { logger, mongodb } = this;
// Generate fake data for testing
const symbols = ['AAPL', 'TSLA'];
const keywords = ['earnings', 'market'];
const limit = 10;
logger.info('Fetching news', {
symbols: symbols.length,
keywords: keywords.length,
limit,
timestamp: new Date().toISOString()
});
try {
// Simulate API call delay (longest delay for news)
await new Promise(resolve => setTimeout(resolve, 300 + Math.random() * 500));
// Simulate fetching news articles
const articles = [];
const articleCount = Math.min(limit, 5 + Math.floor(Math.random() * 10));
for (let i = 0; i < articleCount; i++) {
const article = {
id: `news_${Date.now()}_${i}`,
title: `Breaking: Market Update ${i + 1}`,
summary: `Important market news regarding ${symbols.join(', ') || 'general market'}`,
symbols: symbols,
keywords: keywords,
published_at: new Date(Date.now() - Math.random() * 86400000).toISOString(),
source: ['Reuters', 'Bloomberg', 'CNBC', 'WSJ'][Math.floor(Math.random() * 4)],
sentiment: ['positive', 'negative', 'neutral'][Math.floor(Math.random() * 3)],
fetched_at: new Date().toISOString()
};
articles.push(article);
}
logger.debug('Fetched news articles', { count: articles.length });
// Simulate saving to database
if (mongodb && articles.length > 0) {
await mongodb.batchUpsert('testNews', articles, ['id']);
logger.info('Saved news articles to MongoDB', { count: articles.length });
}
return {
success: true,
articles: articles.length,
message: `Fetched ${articles.length} news articles`
};
} catch (error) {
logger.error('Failed to fetch news', { error, symbols, keywords });
throw error;
}
}

View file

@ -0,0 +1,3 @@
export * from './fetch-daily-prices.action';
export * from './fetch-fundamentals.action';
export * from './fetch-news.action';

View file

@ -0,0 +1,139 @@
import {
BaseHandler,
Handler,
Operation,
RateLimit,
ScheduledOperation
} from '@stock-bot/handlers';
import type { DataIngestionServices } from '../../types';
import {
fetchDailyPrices,
fetchFundamentals,
fetchNews,
} from './actions';
/**
* EOD (End of Day) Handler for testing rate limits
* This handler demonstrates different rate limit configurations
*
* Handler-level rate limit: 100 requests per minute for all operations
* Individual operations can override this with their own limits
*/
@Handler('eod')
// @Disabled()
@RateLimit({ points: 1, duration: 10, blockDuration: 10 })
export class EodHandler extends BaseHandler<DataIngestionServices> {
constructor(services: any) {
super(services);
}
/**
* Fetch daily price data - High volume operation
* Rate limit: 50 requests per minute (overrides handler-level limit)
*/
@Operation('fetch-daily-prices')
@RateLimit({ points: 3, duration: 10, blockDuration: 5 })
fetchDailyPrices = fetchDailyPrices;
/**
* Fetch fundamental data - Medium volume operation
* Rate limit: 20 requests per minute
*/
@Operation('fetch-fundamentals')
@RateLimit({ points: 2, duration: 10, blockDuration: 10 })
fetchFundamentals = fetchFundamentals;
/**
* Fetch news data - Low volume operation
* Rate limit: 10 requests per minute (most restrictive)
*/
@Operation('fetch-news')
fetchNews = fetchNews;
/**
* Test burst operations - For testing rate limit behavior
* This doesn't have its own rate limit, so it uses the handler-level limit (100/min)
*/
@Operation('test-burst')
async testBurstOperations(input: { operationsToTest: string[], burstSize: number }): Promise<unknown> {
this.logger.info('Testing burst operations', input);
const results = {
attempted: 0,
scheduled: 0,
failed: 0
};
try {
const promises = [];
for (let i = 0; i < input.burstSize; i++) {
const operation = input.operationsToTest[i % input.operationsToTest.length] || 'fetch-news';
results.attempted++;
const promise = this.scheduleOperation(operation, {}).then(() => {
results.scheduled++;
}).catch(() => {
results.failed++;
});
promises.push(promise);
}
await Promise.allSettled(promises);
return {
success: true,
results,
message: `Scheduled ${results.scheduled}/${results.attempted} operations`
};
} catch (error) {
this.logger.error('Burst test failed', { error });
throw error;
}
}
/**
* Scheduled job to fetch daily prices
* Runs every day at 6 PM (after market close)
*/
@ScheduledOperation('eod-daily-prices', '0 18 * * *', {
priority: 5,
description: 'Fetch daily price data after market close',
immediately: false,
})
async scheduledFetchDailyPrices(): Promise<unknown> {
this.logger.info('Starting scheduled daily price fetch');
return this.fetchDailyPrices();
}
/**
* Scheduled job to fetch fundamentals
* Runs weekly on Sunday
*/
@ScheduledOperation('eod-fundamentals', '0 0 * * 0', {
priority: 5,
description: 'Weekly fundamental data update',
immediately: false,
})
async scheduledFetchFundamentals(): Promise<unknown> {
this.logger.info('Starting scheduled fundamentals fetch');
return this.fetchFundamentals();
}
/**
* Scheduled job to test rate limits
* Runs every 5 minutes for testing
*/
@ScheduledOperation('eod-rate-limit-test', '*/5 * * * *', {
priority: 5,
description: 'Test rate limit behavior',
immediately: true,
})
async scheduledRateLimitTest(): Promise<unknown> {
this.logger.info('Starting rate limit test');
return this.testBurstOperations({
operationsToTest: ['fetch-daily-prices', 'fetch-fundamentals', 'fetch-news'],
burstSize: 200
});
}
}

View file

@ -0,0 +1,4 @@
export const EOD_CONFIG = {
// API configuration
API_BASE_URL: 'https://eodhd.com/api/',
};

View file

@ -0,0 +1 @@
export * from './config';

View file

@ -13,6 +13,7 @@ import { QMHandler } from './qm/qm.handler';
import { WebShareHandler } from './webshare/webshare.handler';
import { TradingViewHandler } from './tradingview/tradingview.handler';
import { TeHandler } from './te/te.handler';
import { EodHandler } from './eod/eod.handler';
// Add more handler imports as needed
@ -28,7 +29,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, TradingViewHandler, TeHandler];
const handlers = [CeoHandler, IbHandler, QMHandler, WebShareHandler, TradingViewHandler, TeHandler, EodHandler];
logger.info('Handler imports loaded', {
count: handlers.length,