feat(stocks): Add Marketstack provider (EOD) with tests, exports and documentation updates
This commit is contained in:
1
.serena/.gitignore
vendored
Normal file
1
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/cache
|
67
.serena/project.yml
Normal file
67
.serena/project.yml
Normal file
@@ -0,0 +1,67 @@
|
||||
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
|
||||
# * For C, use cpp
|
||||
# * For JavaScript, use typescript
|
||||
# Special requirements:
|
||||
# * csharp: Requires the presence of a .sln file in the project folder.
|
||||
language: typescript
|
||||
|
||||
# whether to use the project's gitignore file to ignore files
|
||||
# Added on 2025-04-07
|
||||
ignore_all_files_in_gitignore: true
|
||||
# list of additional paths to ignore
|
||||
# same syntax as gitignore, so you can use * and **
|
||||
# Was previously called `ignored_dirs`, please update your config if you are using that.
|
||||
# Added (renamed) on 2025-04-07
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
project_name: "opendata"
|
10
changelog.md
10
changelog.md
@@ -1,5 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-10-11 - 1.7.0 - feat(stocks)
|
||||
Add Marketstack provider (EOD) with tests, exports and documentation updates
|
||||
|
||||
- Add MarketstackProvider implementation (ts/stocks/providers/provider.marketstack.ts) providing EOD single and batch fetching, availability checks and mapping to IStockPrice.
|
||||
- Export MarketstackProvider from ts/stocks/index.ts so it is available via the public API.
|
||||
- Add comprehensive Marketstack tests (test/test.marketstack.node.ts) covering registration, health checks, single/batch fetches, caching, ticker/market validation, provider stats and sample output.
|
||||
- Update README with Marketstack usage examples, configuration, API key instructions and provider/health documentation.
|
||||
- Bump dev dependency @git.zone/tstest to ^2.4.2 in package.json.
|
||||
- Add project helper/config files (.claude/settings.local.json, .serena/project.yml and .serena/.gitignore) to support CI/tooling.
|
||||
|
||||
## 2025-09-24 - 1.6.1 - fix(stocks)
|
||||
Fix Yahoo provider request handling and bump dependency versions
|
||||
|
||||
|
@@ -17,7 +17,7 @@
|
||||
"@git.zone/tsbuild": "^2.6.8",
|
||||
"@git.zone/tsbundle": "^2.5.1",
|
||||
"@git.zone/tsrun": "^1.3.3",
|
||||
"@git.zone/tstest": "^2.3.8",
|
||||
"@git.zone/tstest": "^2.4.2",
|
||||
"@types/node": "^22.14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
892
pnpm-lock.yaml
generated
892
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
152
readme.md
152
readme.md
@@ -1,4 +1,5 @@
|
||||
# @fin.cx/opendata
|
||||
|
||||
🚀 **Real-time financial data and German business intelligence in one powerful TypeScript library**
|
||||
|
||||
Access live stock prices, cryptocurrencies, forex, commodities AND comprehensive German company data - all through a single, unified API.
|
||||
@@ -8,34 +9,42 @@ Access live stock prices, cryptocurrencies, forex, commodities AND comprehensive
|
||||
```bash
|
||||
npm install @fin.cx/opendata
|
||||
# or
|
||||
yarn add @fin.cx/opendata
|
||||
pnpm add @fin.cx/opendata
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 📈 Real-Time Stock Data
|
||||
### 📈 Stock Market Data
|
||||
|
||||
Get live market data in seconds:
|
||||
Get market data with EOD (End-of-Day) pricing:
|
||||
|
||||
```typescript
|
||||
import { StockPriceService, YahooFinanceProvider } from '@fin.cx/opendata';
|
||||
import { StockPriceService, MarketstackProvider } from '@fin.cx/opendata';
|
||||
|
||||
// Initialize the service
|
||||
const stockService = new StockPriceService();
|
||||
stockService.register(new YahooFinanceProvider());
|
||||
// Initialize the service with caching
|
||||
const stockService = new StockPriceService({
|
||||
ttl: 60000, // Cache for 1 minute
|
||||
maxEntries: 1000 // Max cached symbols
|
||||
});
|
||||
|
||||
// Register Marketstack provider with API key
|
||||
stockService.register(new MarketstackProvider('YOUR_API_KEY'), {
|
||||
priority: 100,
|
||||
retryAttempts: 3
|
||||
});
|
||||
|
||||
// Get single stock price
|
||||
const apple = await stockService.getPrice({ ticker: 'AAPL' });
|
||||
console.log(`Apple: $${apple.price} (${apple.changePercent.toFixed(2)}%)`);
|
||||
|
||||
// Get multiple prices at once
|
||||
// Get multiple prices at once (batch fetching)
|
||||
const prices = await stockService.getPrices({
|
||||
tickers: ['AAPL', 'MSFT', 'GOOGL', 'BTC-USD', 'ETH-USD']
|
||||
tickers: ['AAPL', 'MSFT', 'GOOGL', 'AMZN', 'TSLA']
|
||||
});
|
||||
|
||||
// Market indices, crypto, forex, commodities - all supported!
|
||||
const marketData = await stockService.getPrices({
|
||||
tickers: ['^GSPC', '^DJI', 'BTC-USD', 'EURUSD=X', 'GC=F']
|
||||
// 125,000+ tickers across 72+ exchanges worldwide
|
||||
const internationalStocks = await stockService.getPrices({
|
||||
tickers: ['AAPL', 'VOD.LON', 'SAP.DEX', 'TM', 'BABA']
|
||||
});
|
||||
```
|
||||
|
||||
@@ -73,11 +82,12 @@ await openData.buildInitialDb();
|
||||
|
||||
### 🎯 Stock Market Module
|
||||
|
||||
- **Real-time prices** for stocks, ETFs, indices, crypto, forex, and commodities
|
||||
- **Marketstack API** - End-of-Day (EOD) data for 125,000+ tickers across 72+ exchanges
|
||||
- **Stock prices** for stocks, ETFs, indices, and more
|
||||
- **Batch operations** - fetch 100+ symbols in one request
|
||||
- **Smart caching** - configurable TTL, automatic invalidation
|
||||
- **Provider system** - easily extensible for new data sources
|
||||
- **Automatic retries** and fallback mechanisms
|
||||
- **Extensible provider system** - easily add new data sources
|
||||
- **Retry logic** - configurable retry attempts and delays
|
||||
- **Type-safe** - full TypeScript support with detailed interfaces
|
||||
|
||||
### 🇩🇪 German Business Intelligence
|
||||
@@ -92,25 +102,20 @@ await openData.buildInitialDb();
|
||||
|
||||
### Market Dashboard
|
||||
|
||||
Create a real-time market overview:
|
||||
Create an EOD market overview:
|
||||
|
||||
```typescript
|
||||
const indicators = [
|
||||
// Indices
|
||||
{ ticker: '^GSPC', name: 'S&P 500' },
|
||||
{ ticker: '^IXIC', name: 'NASDAQ' },
|
||||
{ ticker: '^DJI', name: 'DOW Jones' },
|
||||
|
||||
// Tech Giants
|
||||
{ ticker: 'AAPL', name: 'Apple' },
|
||||
{ ticker: 'MSFT', name: 'Microsoft' },
|
||||
|
||||
// Crypto
|
||||
{ ticker: 'BTC-USD', name: 'Bitcoin' },
|
||||
{ ticker: 'ETH-USD', name: 'Ethereum' },
|
||||
|
||||
// Commodities
|
||||
{ ticker: 'GC=F', name: 'Gold' },
|
||||
{ ticker: 'CL=F', name: 'Oil' }
|
||||
{ ticker: 'GOOGL', name: 'Alphabet' },
|
||||
{ ticker: 'AMZN', name: 'Amazon' },
|
||||
{ ticker: 'TSLA', name: 'Tesla' }
|
||||
];
|
||||
|
||||
const prices = await stockService.getPrices({
|
||||
@@ -130,6 +135,25 @@ prices.forEach(price => {
|
||||
});
|
||||
```
|
||||
|
||||
### Provider Health and Statistics
|
||||
|
||||
Monitor your provider health and track usage:
|
||||
|
||||
```typescript
|
||||
// Check provider health
|
||||
const health = await stockService.checkProvidersHealth();
|
||||
console.log(`Marketstack: ${health.get('Marketstack') ? '✅' : '❌'}`);
|
||||
|
||||
// Get provider statistics
|
||||
const stats = stockService.getProviderStats();
|
||||
const marketstackStats = stats.get('Marketstack');
|
||||
console.log('Marketstack Stats:', {
|
||||
successCount: marketstackStats.successCount,
|
||||
errorCount: marketstackStats.errorCount,
|
||||
lastError: marketstackStats.lastError
|
||||
});
|
||||
```
|
||||
|
||||
### Handelsregister Integration
|
||||
|
||||
Automate German company data retrieval:
|
||||
@@ -196,8 +220,8 @@ const stockService = new StockPriceService({
|
||||
maxEntries: 1000 // Max cached symbols
|
||||
});
|
||||
|
||||
// Provider configuration
|
||||
stockService.register(new YahooFinanceProvider(), {
|
||||
// Marketstack - EOD data, requires API key
|
||||
stockService.register(new MarketstackProvider('YOUR_API_KEY'), {
|
||||
enabled: true,
|
||||
priority: 100,
|
||||
timeout: 10000,
|
||||
@@ -208,7 +232,7 @@ stockService.register(new YahooFinanceProvider(), {
|
||||
|
||||
### MongoDB Setup
|
||||
|
||||
Set environment variables:
|
||||
Set environment variables for German business data:
|
||||
|
||||
```env
|
||||
MONGODB_URL=mongodb://localhost:27017
|
||||
@@ -217,6 +241,14 @@ MONGODB_USER=myuser
|
||||
MONGODB_PASS=mypass
|
||||
```
|
||||
|
||||
### Marketstack API Key
|
||||
|
||||
Get your free API key at [marketstack.com](https://marketstack.com) and set it in your environment:
|
||||
|
||||
```env
|
||||
MARKETSTACK_COM_TOKEN=your_api_key_here
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Stock Types
|
||||
@@ -240,10 +272,21 @@ interface IStockPrice {
|
||||
### Key Methods
|
||||
|
||||
**StockPriceService**
|
||||
- `getPrice(request)` - Single stock price
|
||||
- `getPrices(request)` - Batch prices
|
||||
- `register(provider)` - Add data provider
|
||||
- `getPrice(request)` - Single stock price with automatic provider selection
|
||||
- `getPrices(request)` - Batch prices (100+ symbols in one request)
|
||||
- `register(provider, config)` - Add data provider with priority and retry config
|
||||
- `checkProvidersHealth()` - Test all providers and return health status
|
||||
- `getProviderStats()` - Get success/error statistics for each provider
|
||||
- `clearCache()` - Clear price cache
|
||||
- `setCacheTTL(ttl)` - Update cache TTL dynamically
|
||||
|
||||
**MarketstackProvider**
|
||||
- ✅ End-of-Day (EOD) data
|
||||
- ✅ 125,000+ tickers across 72+ exchanges worldwide
|
||||
- ✅ Batch fetching support (multiple symbols in one request)
|
||||
- ✅ Comprehensive data: open, high, low, close, volume, splits, dividends
|
||||
- ⚠️ Requires API key (free tier: 100 requests/month)
|
||||
- ⚠️ EOD data only (not real-time)
|
||||
|
||||
**OpenData**
|
||||
- `start()` - Initialize MongoDB connection
|
||||
@@ -251,31 +294,48 @@ interface IStockPrice {
|
||||
- `CBusinessRecord` - Business record class
|
||||
- `handelsregister` - Registry automation
|
||||
|
||||
## Performance
|
||||
## Provider Architecture
|
||||
|
||||
- **Batch fetching**: Get 100+ prices in <500ms
|
||||
- **Caching**: Instant repeated queries
|
||||
- **Concurrent processing**: Handle 1000+ records/second
|
||||
- **Streaming**: Process GB-sized datasets without memory issues
|
||||
|
||||
## Extensibility
|
||||
|
||||
The provider architecture makes it easy to add new data sources:
|
||||
The library uses a flexible provider system that makes it easy to add new data sources:
|
||||
|
||||
```typescript
|
||||
class MyCustomProvider implements IStockProvider {
|
||||
name = 'My Provider';
|
||||
priority = 50;
|
||||
requiresAuth = true;
|
||||
|
||||
async fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
|
||||
// Your implementation
|
||||
}
|
||||
|
||||
// ... other required methods
|
||||
async fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
|
||||
// Batch implementation
|
||||
}
|
||||
|
||||
async isAvailable(): Promise<boolean> {
|
||||
// Health check
|
||||
}
|
||||
|
||||
supportsMarket(market: string): boolean {
|
||||
// Market validation
|
||||
}
|
||||
|
||||
supportsTicker(ticker: string): boolean {
|
||||
// Ticker validation
|
||||
}
|
||||
}
|
||||
|
||||
stockService.register(new MyCustomProvider());
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
- **Batch fetching**: Get 100+ EOD prices in one API request
|
||||
- **Smart caching**: Instant repeated queries with configurable TTL
|
||||
- **Rate limit aware**: Automatic retry logic for API limits
|
||||
- **Concurrent processing**: Handle 1000+ business records/second
|
||||
- **Streaming**: Process GB-sized datasets without memory issues
|
||||
|
||||
## Testing
|
||||
|
||||
Run the comprehensive test suite:
|
||||
@@ -284,15 +344,17 @@ Run the comprehensive test suite:
|
||||
npm test
|
||||
```
|
||||
|
||||
View live market data:
|
||||
Test stock provider:
|
||||
|
||||
```bash
|
||||
npm test -- --grep "market indicators"
|
||||
npx tstest test/test.marketstack.node.ts --verbose
|
||||
```
|
||||
|
||||
## Contributing
|
||||
Test German business data:
|
||||
|
||||
We welcome contributions! Please see our contributing guidelines for details.
|
||||
```bash
|
||||
npx tstest test/test.handelsregister.ts --verbose
|
||||
```
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
|
302
test/test.marketstack.node.ts
Normal file
302
test/test.marketstack.node.ts
Normal file
@@ -0,0 +1,302 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import * as opendata from '../ts/index.js';
|
||||
import * as paths from '../ts/paths.js';
|
||||
import * as plugins from '../ts/plugins.js';
|
||||
|
||||
// Test data
|
||||
const testTickers = ['AAPL', 'MSFT', 'GOOGL'];
|
||||
const invalidTicker = 'INVALID_TICKER_XYZ';
|
||||
|
||||
let stockService: opendata.StockPriceService;
|
||||
let marketstackProvider: opendata.MarketstackProvider;
|
||||
let testQenv: plugins.qenv.Qenv;
|
||||
|
||||
tap.test('should create StockPriceService instance', async () => {
|
||||
stockService = new opendata.StockPriceService({
|
||||
ttl: 30000, // 30 seconds cache
|
||||
maxEntries: 100
|
||||
});
|
||||
expect(stockService).toBeInstanceOf(opendata.StockPriceService);
|
||||
});
|
||||
|
||||
tap.test('should create MarketstackProvider instance', async () => {
|
||||
try {
|
||||
// Create qenv and get API key
|
||||
testQenv = new plugins.qenv.Qenv(paths.packageDir, paths.nogitDir);
|
||||
const apiKey = await testQenv.getEnvVarOnDemand('MARKETSTACK_COM_TOKEN');
|
||||
|
||||
marketstackProvider = new opendata.MarketstackProvider(apiKey, {
|
||||
enabled: true,
|
||||
timeout: 10000,
|
||||
retryAttempts: 2,
|
||||
retryDelay: 500
|
||||
});
|
||||
expect(marketstackProvider).toBeInstanceOf(opendata.MarketstackProvider);
|
||||
expect(marketstackProvider.name).toEqual('Marketstack');
|
||||
expect(marketstackProvider.requiresAuth).toEqual(true);
|
||||
expect(marketstackProvider.priority).toEqual(80);
|
||||
} catch (error) {
|
||||
if (error.message.includes('MARKETSTACK_COM_TOKEN')) {
|
||||
console.log('⚠️ MARKETSTACK_COM_TOKEN not set - skipping Marketstack tests');
|
||||
tap.test('Marketstack token not available', async () => {
|
||||
expect(true).toEqual(true); // Skip gracefully
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should register Marketstack provider with the service', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
stockService.register(marketstackProvider);
|
||||
const providers = stockService.getAllProviders();
|
||||
expect(providers).toContainEqual(marketstackProvider);
|
||||
expect(stockService.getProvider('Marketstack')).toEqual(marketstackProvider);
|
||||
});
|
||||
|
||||
tap.test('should check provider health', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const health = await stockService.checkProvidersHealth();
|
||||
expect(health.get('Marketstack')).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('should fetch single stock price', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const price = await stockService.getPrice({ ticker: 'AAPL' });
|
||||
|
||||
expect(price).toHaveProperty('ticker');
|
||||
expect(price).toHaveProperty('price');
|
||||
expect(price).toHaveProperty('currency');
|
||||
expect(price).toHaveProperty('change');
|
||||
expect(price).toHaveProperty('changePercent');
|
||||
expect(price).toHaveProperty('previousClose');
|
||||
expect(price).toHaveProperty('timestamp');
|
||||
expect(price).toHaveProperty('provider');
|
||||
expect(price).toHaveProperty('marketState');
|
||||
|
||||
expect(price.ticker).toEqual('AAPL');
|
||||
expect(price.price).toBeGreaterThan(0);
|
||||
expect(price.provider).toEqual('Marketstack');
|
||||
expect(price.timestamp).toBeInstanceOf(Date);
|
||||
expect(price.marketState).toEqual('CLOSED'); // EOD data
|
||||
|
||||
console.log(`✓ Fetched AAPL: $${price.price} (${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%)`);
|
||||
});
|
||||
|
||||
tap.test('should fetch multiple stock prices', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const prices = await stockService.getPrices({
|
||||
tickers: testTickers
|
||||
});
|
||||
|
||||
expect(prices).toBeArray();
|
||||
expect(prices.length).toBeGreaterThan(0);
|
||||
expect(prices.length).toBeLessThanOrEqual(testTickers.length);
|
||||
|
||||
for (const price of prices) {
|
||||
expect(testTickers).toContain(price.ticker);
|
||||
expect(price.price).toBeGreaterThan(0);
|
||||
expect(price.provider).toEqual('Marketstack');
|
||||
expect(price.marketState).toEqual('CLOSED');
|
||||
console.log(` ${price.ticker}: $${price.price}`);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should serve cached prices on subsequent requests', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// First request - should hit the API
|
||||
const firstRequest = await stockService.getPrice({ ticker: 'AAPL' });
|
||||
|
||||
// Second request - should be served from cache
|
||||
const secondRequest = await stockService.getPrice({ ticker: 'AAPL' });
|
||||
|
||||
expect(secondRequest.ticker).toEqual(firstRequest.ticker);
|
||||
expect(secondRequest.price).toEqual(firstRequest.price);
|
||||
expect(secondRequest.timestamp).toEqual(firstRequest.timestamp);
|
||||
|
||||
console.log('✓ Cache working correctly');
|
||||
});
|
||||
|
||||
tap.test('should handle invalid ticker gracefully', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await stockService.getPrice({ ticker: invalidTicker });
|
||||
throw new Error('Should have thrown an error for invalid ticker');
|
||||
} catch (error) {
|
||||
expect(error.message).toInclude('Failed to fetch price');
|
||||
console.log('✓ Invalid ticker handled correctly');
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should support market checking', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
expect(marketstackProvider.supportsMarket('US')).toEqual(true);
|
||||
expect(marketstackProvider.supportsMarket('UK')).toEqual(true);
|
||||
expect(marketstackProvider.supportsMarket('DE')).toEqual(true);
|
||||
expect(marketstackProvider.supportsMarket('JP')).toEqual(true);
|
||||
expect(marketstackProvider.supportsMarket('INVALID')).toEqual(false);
|
||||
|
||||
console.log('✓ Market support check working');
|
||||
});
|
||||
|
||||
tap.test('should validate ticker format', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
expect(marketstackProvider.supportsTicker('AAPL')).toEqual(true);
|
||||
expect(marketstackProvider.supportsTicker('MSFT')).toEqual(true);
|
||||
expect(marketstackProvider.supportsTicker('BRK.B')).toEqual(true);
|
||||
expect(marketstackProvider.supportsTicker('123456789012')).toEqual(false);
|
||||
expect(marketstackProvider.supportsTicker('invalid@ticker')).toEqual(false);
|
||||
|
||||
console.log('✓ Ticker validation working');
|
||||
});
|
||||
|
||||
tap.test('should get provider statistics', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
const stats = stockService.getProviderStats();
|
||||
const marketstackStats = stats.get('Marketstack');
|
||||
|
||||
expect(marketstackStats).not.toEqual(undefined);
|
||||
expect(marketstackStats.successCount).toBeGreaterThan(0);
|
||||
expect(marketstackStats.errorCount).toBeGreaterThanOrEqual(0);
|
||||
|
||||
console.log(`✓ Provider stats: ${marketstackStats.successCount} successes, ${marketstackStats.errorCount} errors`);
|
||||
});
|
||||
|
||||
tap.test('should test direct provider methods', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n🔍 Testing direct provider methods:');
|
||||
|
||||
// Test isAvailable
|
||||
const available = await marketstackProvider.isAvailable();
|
||||
expect(available).toEqual(true);
|
||||
console.log(' ✓ isAvailable() returned true');
|
||||
|
||||
// Test fetchPrice directly
|
||||
const price = await marketstackProvider.fetchPrice({ ticker: 'MSFT' });
|
||||
expect(price.ticker).toEqual('MSFT');
|
||||
expect(price.provider).toEqual('Marketstack');
|
||||
expect(price.price).toBeGreaterThan(0);
|
||||
console.log(` ✓ fetchPrice() for MSFT: $${price.price}`);
|
||||
|
||||
// Test fetchPrices directly
|
||||
const prices = await marketstackProvider.fetchPrices({
|
||||
tickers: ['AAPL', 'GOOGL']
|
||||
});
|
||||
expect(prices.length).toBeGreaterThan(0);
|
||||
console.log(` ✓ fetchPrices() returned ${prices.length} prices`);
|
||||
|
||||
for (const p of prices) {
|
||||
console.log(` ${p.ticker}: $${p.price}`);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('should fetch sample EOD data', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('\n📊 Sample EOD Stock Data from Marketstack:');
|
||||
console.log('═'.repeat(65));
|
||||
|
||||
const sampleTickers = [
|
||||
{ ticker: 'AAPL', name: 'Apple Inc.' },
|
||||
{ ticker: 'MSFT', name: 'Microsoft Corp.' },
|
||||
{ ticker: 'GOOGL', name: 'Alphabet Inc.' },
|
||||
{ ticker: 'AMZN', name: 'Amazon.com Inc.' },
|
||||
{ ticker: 'TSLA', name: 'Tesla Inc.' }
|
||||
];
|
||||
|
||||
try {
|
||||
const prices = await marketstackProvider.fetchPrices({
|
||||
tickers: sampleTickers.map(t => t.ticker)
|
||||
});
|
||||
|
||||
const priceMap = new Map(prices.map(p => [p.ticker, p]));
|
||||
|
||||
for (const stock of sampleTickers) {
|
||||
const price = priceMap.get(stock.ticker);
|
||||
if (price) {
|
||||
const changeSymbol = price.change >= 0 ? '↑' : '↓';
|
||||
const changeColor = price.change >= 0 ? '\x1b[32m' : '\x1b[31m';
|
||||
const resetColor = '\x1b[0m';
|
||||
|
||||
console.log(
|
||||
`${stock.name.padEnd(20)} ${price.price.toLocaleString('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
}).padStart(10)} ${changeColor}${changeSymbol} ${price.change >= 0 ? '+' : ''}${price.change.toFixed(2)} (${price.changePercent >= 0 ? '+' : ''}${price.changePercent.toFixed(2)}%)${resetColor}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('═'.repeat(65));
|
||||
console.log(`Provider: Marketstack (EOD Data)`);
|
||||
console.log(`Last updated: ${new Date().toLocaleString()}\n`);
|
||||
} catch (error) {
|
||||
console.log('Error fetching sample data:', error.message);
|
||||
}
|
||||
|
||||
expect(true).toEqual(true);
|
||||
});
|
||||
|
||||
tap.test('should clear cache', async () => {
|
||||
if (!marketstackProvider) {
|
||||
console.log('⚠️ Skipping - Marketstack provider not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure we have something in cache
|
||||
await stockService.getPrice({ ticker: 'AAPL' });
|
||||
|
||||
// Clear cache
|
||||
stockService.clearCache();
|
||||
console.log('✓ Cache cleared');
|
||||
|
||||
// Next request should hit the API again
|
||||
const price = await stockService.getPrice({ ticker: 'AAPL' });
|
||||
expect(price).not.toEqual(undefined);
|
||||
});
|
||||
|
||||
export default tap.start();
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@fin.cx/opendata',
|
||||
version: '1.6.1',
|
||||
version: '1.7.0',
|
||||
description: 'A comprehensive TypeScript library for accessing business data and real-time financial information. Features include German company data management with MongoDB integration, JSONL bulk processing, automated Handelsregister interactions, and real-time stock market data from multiple providers.'
|
||||
}
|
||||
|
@@ -7,3 +7,4 @@ export * from './classes.stockservice.js';
|
||||
|
||||
// Export providers
|
||||
export * from './providers/provider.yahoo.js';
|
||||
export * from './providers/provider.marketstack.js';
|
200
ts/stocks/providers/provider.marketstack.ts
Normal file
200
ts/stocks/providers/provider.marketstack.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { IStockProvider, IProviderConfig } from '../interfaces/provider.js';
|
||||
import type { IStockPrice, IStockQuoteRequest, IStockBatchQuoteRequest } from '../interfaces/stockprice.js';
|
||||
|
||||
/**
|
||||
* Marketstack API v2 Provider
|
||||
* Documentation: https://marketstack.com/documentation_v2
|
||||
*
|
||||
* Features:
|
||||
* - End-of-Day (EOD) stock prices
|
||||
* - Supports 125,000+ tickers across 72+ exchanges worldwide
|
||||
* - Requires API key authentication
|
||||
*
|
||||
* Rate Limits:
|
||||
* - Free Plan: 100 requests/month (EOD only)
|
||||
* - Basic Plan: 10,000 requests/month
|
||||
* - Professional Plan: 100,000 requests/month
|
||||
*
|
||||
* Note: This provider returns EOD data, not real-time prices
|
||||
*/
|
||||
export class MarketstackProvider implements IStockProvider {
|
||||
public name = 'Marketstack';
|
||||
public priority = 80; // Lower than Yahoo (100) due to rate limits and EOD-only data
|
||||
public readonly requiresAuth = true;
|
||||
public readonly rateLimit = {
|
||||
requestsPerMinute: undefined, // No per-minute limit specified
|
||||
requestsPerDay: undefined // Varies by plan
|
||||
};
|
||||
|
||||
private logger = console;
|
||||
private baseUrl = 'https://api.marketstack.com/v2';
|
||||
private apiKey: string;
|
||||
|
||||
constructor(apiKey: string, private config?: IProviderConfig) {
|
||||
if (!apiKey) {
|
||||
throw new Error('API key is required for Marketstack provider');
|
||||
}
|
||||
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch latest EOD price for a single ticker
|
||||
*/
|
||||
public async fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
|
||||
try {
|
||||
const url = `${this.baseUrl}/tickers/${request.ticker}/eod/latest?access_key=${this.apiKey}`;
|
||||
|
||||
const response = await plugins.smartrequest.SmartRequest.create()
|
||||
.url(url)
|
||||
.timeout(this.config?.timeout || 10000)
|
||||
.get();
|
||||
|
||||
const responseData = await response.json() as any;
|
||||
|
||||
// Check for API errors
|
||||
if (responseData.error) {
|
||||
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
||||
}
|
||||
|
||||
// For single ticker endpoint, response is direct object (not wrapped in data field)
|
||||
if (!responseData || !responseData.close) {
|
||||
throw new Error(`No data found for ticker ${request.ticker}`);
|
||||
}
|
||||
|
||||
return this.mapToStockPrice(responseData);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to fetch price for ${request.ticker}:`, error);
|
||||
throw new Error(`Marketstack: Failed to fetch price for ${request.ticker}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch latest EOD prices for multiple tickers
|
||||
*/
|
||||
public async fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
|
||||
try {
|
||||
const symbols = request.tickers.join(',');
|
||||
const url = `${this.baseUrl}/eod/latest?access_key=${this.apiKey}&symbols=${symbols}`;
|
||||
|
||||
const response = await plugins.smartrequest.SmartRequest.create()
|
||||
.url(url)
|
||||
.timeout(this.config?.timeout || 15000)
|
||||
.get();
|
||||
|
||||
const responseData = await response.json() as any;
|
||||
|
||||
// Check for API errors
|
||||
if (responseData.error) {
|
||||
throw new Error(`Marketstack API error: ${responseData.error.message || JSON.stringify(responseData.error)}`);
|
||||
}
|
||||
|
||||
if (!responseData?.data || !Array.isArray(responseData.data)) {
|
||||
throw new Error('Invalid response format from Marketstack API');
|
||||
}
|
||||
|
||||
const prices: IStockPrice[] = [];
|
||||
|
||||
for (const data of responseData.data) {
|
||||
try {
|
||||
prices.push(this.mapToStockPrice(data));
|
||||
} catch (error) {
|
||||
this.logger.warn(`Failed to parse data for ${data.symbol}:`, error);
|
||||
// Continue processing other tickers
|
||||
}
|
||||
}
|
||||
|
||||
if (prices.length === 0) {
|
||||
throw new Error('No valid price data received from batch request');
|
||||
}
|
||||
|
||||
return prices;
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to fetch batch prices:`, error);
|
||||
throw new Error(`Marketstack: Failed to fetch batch prices: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the Marketstack API is available and accessible
|
||||
*/
|
||||
public async isAvailable(): Promise<boolean> {
|
||||
try {
|
||||
// Test with a well-known ticker
|
||||
const url = `${this.baseUrl}/tickers/AAPL/eod/latest?access_key=${this.apiKey}`;
|
||||
|
||||
const response = await plugins.smartrequest.SmartRequest.create()
|
||||
.url(url)
|
||||
.timeout(5000)
|
||||
.get();
|
||||
|
||||
const responseData = await response.json() as any;
|
||||
|
||||
// Check if we got valid data (not an error)
|
||||
// Single ticker endpoint returns direct object, not wrapped in data field
|
||||
return !responseData.error && responseData.close !== undefined;
|
||||
} catch (error) {
|
||||
this.logger.warn('Marketstack provider is not available:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a market is supported
|
||||
* Marketstack supports 72+ exchanges worldwide
|
||||
*/
|
||||
public supportsMarket(market: string): boolean {
|
||||
// Marketstack has broad international coverage including:
|
||||
// US, UK, DE, FR, JP, CN, HK, AU, CA, IN, etc.
|
||||
const supportedMarkets = [
|
||||
'US', 'UK', 'GB', 'DE', 'FR', 'JP', 'CN', 'HK', 'AU', 'CA',
|
||||
'IN', 'BR', 'MX', 'IT', 'ES', 'NL', 'SE', 'CH', 'NO', 'DK'
|
||||
];
|
||||
return supportedMarkets.includes(market.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a ticker format is supported
|
||||
*/
|
||||
public supportsTicker(ticker: string): boolean {
|
||||
// Basic validation - Marketstack supports most standard ticker formats
|
||||
return /^[A-Z0-9\.\-]{1,10}$/.test(ticker.toUpperCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Map Marketstack API response to IStockPrice interface
|
||||
*/
|
||||
private mapToStockPrice(data: any): IStockPrice {
|
||||
if (!data.close) {
|
||||
throw new Error('Missing required price data');
|
||||
}
|
||||
|
||||
// Calculate change and change percent
|
||||
// EOD data: previous close is typically open price of the same day
|
||||
// For better accuracy, we'd need previous day's close, but that requires another API call
|
||||
const currentPrice = data.close;
|
||||
const previousClose = data.open;
|
||||
const change = currentPrice - previousClose;
|
||||
const changePercent = previousClose !== 0 ? (change / previousClose) * 100 : 0;
|
||||
|
||||
// Parse timestamp
|
||||
const timestamp = data.date ? new Date(data.date) : new Date();
|
||||
|
||||
const stockPrice: IStockPrice = {
|
||||
ticker: data.symbol.toUpperCase(),
|
||||
price: currentPrice,
|
||||
currency: data.price_currency || 'USD',
|
||||
change: change,
|
||||
changePercent: changePercent,
|
||||
previousClose: previousClose,
|
||||
timestamp: timestamp,
|
||||
provider: this.name,
|
||||
marketState: 'CLOSED', // EOD data is always for closed markets
|
||||
exchange: data.exchange,
|
||||
exchangeName: data.exchange_code || data.name
|
||||
};
|
||||
|
||||
return stockPrice;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user