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
|
# 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)
|
## 2025-09-24 - 1.6.1 - fix(stocks)
|
||||||
Fix Yahoo provider request handling and bump dependency versions
|
Fix Yahoo provider request handling and bump dependency versions
|
||||||
|
|
||||||
|
@@ -17,7 +17,7 @@
|
|||||||
"@git.zone/tsbuild": "^2.6.8",
|
"@git.zone/tsbuild": "^2.6.8",
|
||||||
"@git.zone/tsbundle": "^2.5.1",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^1.3.3",
|
||||||
"@git.zone/tstest": "^2.3.8",
|
"@git.zone/tstest": "^2.4.2",
|
||||||
"@types/node": "^22.14.0"
|
"@types/node": "^22.14.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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
|
# @fin.cx/opendata
|
||||||
|
|
||||||
🚀 **Real-time financial data and German business intelligence in one powerful TypeScript library**
|
🚀 **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.
|
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
|
```bash
|
||||||
npm install @fin.cx/opendata
|
npm install @fin.cx/opendata
|
||||||
# or
|
# or
|
||||||
yarn add @fin.cx/opendata
|
pnpm add @fin.cx/opendata
|
||||||
```
|
```
|
||||||
|
|
||||||
## Quick Start
|
## 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
|
```typescript
|
||||||
import { StockPriceService, YahooFinanceProvider } from '@fin.cx/opendata';
|
import { StockPriceService, MarketstackProvider } from '@fin.cx/opendata';
|
||||||
|
|
||||||
// Initialize the service
|
// Initialize the service with caching
|
||||||
const stockService = new StockPriceService();
|
const stockService = new StockPriceService({
|
||||||
stockService.register(new YahooFinanceProvider());
|
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
|
// Get single stock price
|
||||||
const apple = await stockService.getPrice({ ticker: 'AAPL' });
|
const apple = await stockService.getPrice({ ticker: 'AAPL' });
|
||||||
console.log(`Apple: $${apple.price} (${apple.changePercent.toFixed(2)}%)`);
|
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({
|
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!
|
// 125,000+ tickers across 72+ exchanges worldwide
|
||||||
const marketData = await stockService.getPrices({
|
const internationalStocks = await stockService.getPrices({
|
||||||
tickers: ['^GSPC', '^DJI', 'BTC-USD', 'EURUSD=X', 'GC=F']
|
tickers: ['AAPL', 'VOD.LON', 'SAP.DEX', 'TM', 'BABA']
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -73,11 +82,12 @@ await openData.buildInitialDb();
|
|||||||
|
|
||||||
### 🎯 Stock Market Module
|
### 🎯 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
|
- **Batch operations** - fetch 100+ symbols in one request
|
||||||
- **Smart caching** - configurable TTL, automatic invalidation
|
- **Smart caching** - configurable TTL, automatic invalidation
|
||||||
- **Provider system** - easily extensible for new data sources
|
- **Extensible provider system** - easily add new data sources
|
||||||
- **Automatic retries** and fallback mechanisms
|
- **Retry logic** - configurable retry attempts and delays
|
||||||
- **Type-safe** - full TypeScript support with detailed interfaces
|
- **Type-safe** - full TypeScript support with detailed interfaces
|
||||||
|
|
||||||
### 🇩🇪 German Business Intelligence
|
### 🇩🇪 German Business Intelligence
|
||||||
@@ -92,25 +102,20 @@ await openData.buildInitialDb();
|
|||||||
|
|
||||||
### Market Dashboard
|
### Market Dashboard
|
||||||
|
|
||||||
Create a real-time market overview:
|
Create an EOD market overview:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const indicators = [
|
const indicators = [
|
||||||
// Indices
|
// Indices
|
||||||
{ ticker: '^GSPC', name: 'S&P 500' },
|
{ ticker: '^GSPC', name: 'S&P 500' },
|
||||||
{ ticker: '^IXIC', name: 'NASDAQ' },
|
{ ticker: '^DJI', name: 'DOW Jones' },
|
||||||
|
|
||||||
// Tech Giants
|
// Tech Giants
|
||||||
{ ticker: 'AAPL', name: 'Apple' },
|
{ ticker: 'AAPL', name: 'Apple' },
|
||||||
{ ticker: 'MSFT', name: 'Microsoft' },
|
{ ticker: 'MSFT', name: 'Microsoft' },
|
||||||
|
{ ticker: 'GOOGL', name: 'Alphabet' },
|
||||||
// Crypto
|
{ ticker: 'AMZN', name: 'Amazon' },
|
||||||
{ ticker: 'BTC-USD', name: 'Bitcoin' },
|
{ ticker: 'TSLA', name: 'Tesla' }
|
||||||
{ ticker: 'ETH-USD', name: 'Ethereum' },
|
|
||||||
|
|
||||||
// Commodities
|
|
||||||
{ ticker: 'GC=F', name: 'Gold' },
|
|
||||||
{ ticker: 'CL=F', name: 'Oil' }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const prices = await stockService.getPrices({
|
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
|
### Handelsregister Integration
|
||||||
|
|
||||||
Automate German company data retrieval:
|
Automate German company data retrieval:
|
||||||
@@ -196,8 +220,8 @@ const stockService = new StockPriceService({
|
|||||||
maxEntries: 1000 // Max cached symbols
|
maxEntries: 1000 // Max cached symbols
|
||||||
});
|
});
|
||||||
|
|
||||||
// Provider configuration
|
// Marketstack - EOD data, requires API key
|
||||||
stockService.register(new YahooFinanceProvider(), {
|
stockService.register(new MarketstackProvider('YOUR_API_KEY'), {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
priority: 100,
|
priority: 100,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
@@ -208,7 +232,7 @@ stockService.register(new YahooFinanceProvider(), {
|
|||||||
|
|
||||||
### MongoDB Setup
|
### MongoDB Setup
|
||||||
|
|
||||||
Set environment variables:
|
Set environment variables for German business data:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
MONGODB_URL=mongodb://localhost:27017
|
MONGODB_URL=mongodb://localhost:27017
|
||||||
@@ -217,6 +241,14 @@ MONGODB_USER=myuser
|
|||||||
MONGODB_PASS=mypass
|
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
|
## API Reference
|
||||||
|
|
||||||
### Stock Types
|
### Stock Types
|
||||||
@@ -240,10 +272,21 @@ interface IStockPrice {
|
|||||||
### Key Methods
|
### Key Methods
|
||||||
|
|
||||||
**StockPriceService**
|
**StockPriceService**
|
||||||
- `getPrice(request)` - Single stock price
|
- `getPrice(request)` - Single stock price with automatic provider selection
|
||||||
- `getPrices(request)` - Batch prices
|
- `getPrices(request)` - Batch prices (100+ symbols in one request)
|
||||||
- `register(provider)` - Add data provider
|
- `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
|
- `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**
|
**OpenData**
|
||||||
- `start()` - Initialize MongoDB connection
|
- `start()` - Initialize MongoDB connection
|
||||||
@@ -251,31 +294,48 @@ interface IStockPrice {
|
|||||||
- `CBusinessRecord` - Business record class
|
- `CBusinessRecord` - Business record class
|
||||||
- `handelsregister` - Registry automation
|
- `handelsregister` - Registry automation
|
||||||
|
|
||||||
## Performance
|
## Provider Architecture
|
||||||
|
|
||||||
- **Batch fetching**: Get 100+ prices in <500ms
|
The library uses a flexible provider system that makes it easy to add new data sources:
|
||||||
- **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:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
class MyCustomProvider implements IStockProvider {
|
class MyCustomProvider implements IStockProvider {
|
||||||
name = 'My Provider';
|
name = 'My Provider';
|
||||||
|
priority = 50;
|
||||||
|
requiresAuth = true;
|
||||||
|
|
||||||
async fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
|
async fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
|
||||||
// Your implementation
|
// 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());
|
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
|
## Testing
|
||||||
|
|
||||||
Run the comprehensive test suite:
|
Run the comprehensive test suite:
|
||||||
@@ -284,15 +344,17 @@ Run the comprehensive test suite:
|
|||||||
npm test
|
npm test
|
||||||
```
|
```
|
||||||
|
|
||||||
View live market data:
|
Test stock provider:
|
||||||
|
|
||||||
```bash
|
```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
|
## 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 = {
|
export const commitinfo = {
|
||||||
name: '@fin.cx/opendata',
|
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.'
|
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 providers
|
||||||
export * from './providers/provider.yahoo.js';
|
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