@fin.cx/opendata 3.0.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/stocks/classes.baseproviderservice.d.ts +129 -0
- package/dist_ts/stocks/classes.baseproviderservice.js +204 -0
- package/dist_ts/stocks/classes.fundamentalsservice.d.ts +104 -0
- package/dist_ts/stocks/classes.fundamentalsservice.js +298 -0
- package/dist_ts/stocks/classes.stockdataservice.d.ts +121 -0
- package/dist_ts/stocks/classes.stockdataservice.js +495 -0
- package/dist_ts/stocks/index.d.ts +6 -0
- package/dist_ts/stocks/index.js +9 -2
- package/dist_ts/stocks/interfaces/fundamentals.d.ts +82 -0
- package/dist_ts/stocks/interfaces/fundamentals.js +6 -0
- package/dist_ts/stocks/interfaces/stockdata.d.ts +57 -0
- package/dist_ts/stocks/interfaces/stockdata.js +2 -0
- package/dist_ts/stocks/providers/provider.secedgar.d.ts +102 -0
- package/dist_ts/stocks/providers/provider.secedgar.js +343 -0
- package/package.json +1 -1
- package/readme.md +453 -403
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/stocks/classes.baseproviderservice.ts +296 -0
- package/ts/stocks/classes.fundamentalsservice.ts +404 -0
- package/ts/stocks/classes.stockdataservice.ts +647 -0
- package/ts/stocks/index.ts +10 -2
- package/ts/stocks/interfaces/fundamentals.ts +102 -0
- package/ts/stocks/interfaces/stockdata.ts +65 -0
- package/ts/stocks/providers/provider.secedgar.ts +429 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interfaces for stock fundamental data (financials from SEC filings)
|
|
3
|
+
* Separate from stock price data (OHLCV) to maintain clean architecture
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Request types for fundamental data
|
|
7
|
+
export interface IFundamentalsCurrentRequest {
|
|
8
|
+
type: 'fundamentals-current';
|
|
9
|
+
ticker: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IFundamentalsBatchRequest {
|
|
13
|
+
type: 'fundamentals-batch';
|
|
14
|
+
tickers: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type IFundamentalsRequest =
|
|
18
|
+
| IFundamentalsCurrentRequest
|
|
19
|
+
| IFundamentalsBatchRequest;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Stock fundamental data from SEC filings (10-K, 10-Q)
|
|
23
|
+
* Contains financial metrics like EPS, Revenue, Assets, etc.
|
|
24
|
+
*/
|
|
25
|
+
export interface IStockFundamentals {
|
|
26
|
+
ticker: string;
|
|
27
|
+
cik: string;
|
|
28
|
+
companyName: string;
|
|
29
|
+
provider: string;
|
|
30
|
+
timestamp: Date;
|
|
31
|
+
fetchedAt: Date;
|
|
32
|
+
|
|
33
|
+
// Per-share metrics
|
|
34
|
+
earningsPerShareBasic?: number;
|
|
35
|
+
earningsPerShareDiluted?: number;
|
|
36
|
+
sharesOutstanding?: number;
|
|
37
|
+
weightedAverageSharesOutstanding?: number;
|
|
38
|
+
|
|
39
|
+
// Income statement (annual USD)
|
|
40
|
+
revenue?: number;
|
|
41
|
+
netIncome?: number;
|
|
42
|
+
operatingIncome?: number;
|
|
43
|
+
grossProfit?: number;
|
|
44
|
+
costOfRevenue?: number;
|
|
45
|
+
|
|
46
|
+
// Balance sheet (annual USD)
|
|
47
|
+
assets?: number;
|
|
48
|
+
liabilities?: number;
|
|
49
|
+
stockholdersEquity?: number;
|
|
50
|
+
cash?: number;
|
|
51
|
+
propertyPlantEquipment?: number;
|
|
52
|
+
|
|
53
|
+
// Calculated metrics (requires current price)
|
|
54
|
+
marketCap?: number; // price × sharesOutstanding
|
|
55
|
+
priceToEarnings?: number; // price / EPS
|
|
56
|
+
priceToBook?: number; // marketCap / stockholdersEquity
|
|
57
|
+
|
|
58
|
+
// Metadata
|
|
59
|
+
fiscalYear?: string;
|
|
60
|
+
fiscalQuarter?: string;
|
|
61
|
+
filingDate?: Date;
|
|
62
|
+
form?: '10-K' | '10-Q' | string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Provider interface for fetching fundamental data
|
|
67
|
+
* Parallel to IStockProvider but for fundamentals instead of prices
|
|
68
|
+
*/
|
|
69
|
+
export interface IFundamentalsProvider {
|
|
70
|
+
name: string;
|
|
71
|
+
priority: number;
|
|
72
|
+
fetchData(request: IFundamentalsRequest): Promise<IStockFundamentals | IStockFundamentals[]>;
|
|
73
|
+
isAvailable(): Promise<boolean>;
|
|
74
|
+
readonly requiresAuth: boolean;
|
|
75
|
+
readonly rateLimit?: {
|
|
76
|
+
requestsPerMinute: number;
|
|
77
|
+
requestsPerDay?: number;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Configuration for fundamentals providers
|
|
83
|
+
*/
|
|
84
|
+
export interface IFundamentalsProviderConfig {
|
|
85
|
+
enabled: boolean;
|
|
86
|
+
priority?: number;
|
|
87
|
+
timeout?: number;
|
|
88
|
+
retryAttempts?: number;
|
|
89
|
+
retryDelay?: number;
|
|
90
|
+
cacheTTL?: number; // Custom cache TTL for this provider
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Registry for managing fundamental data providers
|
|
95
|
+
*/
|
|
96
|
+
export interface IFundamentalsProviderRegistry {
|
|
97
|
+
register(provider: IFundamentalsProvider, config?: IFundamentalsProviderConfig): void;
|
|
98
|
+
unregister(providerName: string): void;
|
|
99
|
+
getProvider(name: string): IFundamentalsProvider | undefined;
|
|
100
|
+
getAllProviders(): IFundamentalsProvider[];
|
|
101
|
+
getEnabledProviders(): IFundamentalsProvider[];
|
|
102
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { IStockPrice } from './stockprice.js';
|
|
2
|
+
import type { IStockFundamentals } from './fundamentals.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Combined stock data with price and fundamentals
|
|
6
|
+
* All calculated metrics (market cap, P/E, P/B) are automatically included
|
|
7
|
+
*/
|
|
8
|
+
export interface IStockData {
|
|
9
|
+
/** Stock ticker symbol */
|
|
10
|
+
ticker: string;
|
|
11
|
+
|
|
12
|
+
/** Price information */
|
|
13
|
+
price: IStockPrice;
|
|
14
|
+
|
|
15
|
+
/** Fundamental data (optional - may not be available for all stocks) */
|
|
16
|
+
fundamentals?: IStockFundamentals;
|
|
17
|
+
|
|
18
|
+
/** When this combined data was fetched */
|
|
19
|
+
fetchedAt: Date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Configuration for StockDataService
|
|
24
|
+
*/
|
|
25
|
+
export interface IStockDataServiceConfig {
|
|
26
|
+
/** Cache configuration */
|
|
27
|
+
cache?: {
|
|
28
|
+
/** TTL for price data (default: 24 hours) */
|
|
29
|
+
priceTTL?: number;
|
|
30
|
+
/** TTL for fundamentals data (default: 90 days) */
|
|
31
|
+
fundamentalsTTL?: number;
|
|
32
|
+
/** Max cache entries (default: 10000) */
|
|
33
|
+
maxEntries?: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Provider timeouts */
|
|
37
|
+
timeout?: {
|
|
38
|
+
/** Timeout for price providers (default: 10000ms) */
|
|
39
|
+
price?: number;
|
|
40
|
+
/** Timeout for fundamentals providers (default: 30000ms) */
|
|
41
|
+
fundamentals?: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Request type for getting complete stock data
|
|
47
|
+
*/
|
|
48
|
+
export interface ICompleteStockDataRequest {
|
|
49
|
+
ticker: string;
|
|
50
|
+
/** Whether to include fundamentals (default: true) */
|
|
51
|
+
includeFundamentals?: boolean;
|
|
52
|
+
/** Whether to enrich fundamentals with price calculations (default: true) */
|
|
53
|
+
enrichFundamentals?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Batch request for multiple stocks
|
|
58
|
+
*/
|
|
59
|
+
export interface ICompleteStockDataBatchRequest {
|
|
60
|
+
tickers: string[];
|
|
61
|
+
/** Whether to include fundamentals (default: true) */
|
|
62
|
+
includeFundamentals?: boolean;
|
|
63
|
+
/** Whether to enrich fundamentals with price calculations (default: true) */
|
|
64
|
+
enrichFundamentals?: boolean;
|
|
65
|
+
}
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type {
|
|
3
|
+
IFundamentalsProvider,
|
|
4
|
+
IStockFundamentals,
|
|
5
|
+
IFundamentalsRequest,
|
|
6
|
+
IFundamentalsCurrentRequest,
|
|
7
|
+
IFundamentalsBatchRequest
|
|
8
|
+
} from '../interfaces/fundamentals.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration for SEC EDGAR provider
|
|
12
|
+
*/
|
|
13
|
+
export interface ISecEdgarConfig {
|
|
14
|
+
userAgent: string; // Required: Format "Company Name Email" (e.g., "fin.cx info@fin.cx")
|
|
15
|
+
cikCacheTTL?: number; // Default: 30 days (CIK codes rarely change)
|
|
16
|
+
fundamentalsCacheTTL?: number; // Default: 90 days (quarterly filings)
|
|
17
|
+
timeout?: number; // Request timeout in ms
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Rate limiter for SEC EDGAR API
|
|
22
|
+
* SEC requires: 10 requests per second maximum
|
|
23
|
+
*/
|
|
24
|
+
class RateLimiter {
|
|
25
|
+
private requestTimes: number[] = [];
|
|
26
|
+
private readonly maxRequestsPerSecond = 10;
|
|
27
|
+
|
|
28
|
+
public async waitForSlot(): Promise<void> {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const oneSecondAgo = now - 1000;
|
|
31
|
+
|
|
32
|
+
// Remove requests older than 1 second
|
|
33
|
+
this.requestTimes = this.requestTimes.filter(time => time > oneSecondAgo);
|
|
34
|
+
|
|
35
|
+
// If we've hit the limit, wait
|
|
36
|
+
if (this.requestTimes.length >= this.maxRequestsPerSecond) {
|
|
37
|
+
const oldestRequest = this.requestTimes[0];
|
|
38
|
+
const waitTime = 1000 - (now - oldestRequest) + 10; // +10ms buffer
|
|
39
|
+
await plugins.smartdelay.delayFor(waitTime);
|
|
40
|
+
return this.waitForSlot(); // Recursively check again
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Record this request
|
|
44
|
+
this.requestTimes.push(now);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* SEC EDGAR Fundamental Data Provider
|
|
50
|
+
*
|
|
51
|
+
* Features:
|
|
52
|
+
* - Free public access (no API key required)
|
|
53
|
+
* - All US public companies
|
|
54
|
+
* - Financial data from 10-K/10-Q filings
|
|
55
|
+
* - US GAAP standardized metrics
|
|
56
|
+
* - Historical data back to ~2009
|
|
57
|
+
* - < 1 minute filing delay
|
|
58
|
+
*
|
|
59
|
+
* Documentation: https://www.sec.gov/edgar/sec-api-documentation
|
|
60
|
+
*
|
|
61
|
+
* Rate Limits:
|
|
62
|
+
* - 10 requests per second (enforced by SEC)
|
|
63
|
+
* - Requires User-Agent header in format: "Company Name Email"
|
|
64
|
+
*
|
|
65
|
+
* Data Sources:
|
|
66
|
+
* - Company Facts API: /api/xbrl/companyfacts/CIK##########.json
|
|
67
|
+
* - Ticker Lookup: /files/company_tickers.json
|
|
68
|
+
*/
|
|
69
|
+
export class SecEdgarProvider implements IFundamentalsProvider {
|
|
70
|
+
public name = 'SEC EDGAR';
|
|
71
|
+
public priority = 100; // High priority - free, authoritative, comprehensive
|
|
72
|
+
public readonly requiresAuth = false; // No API key needed!
|
|
73
|
+
public readonly rateLimit = {
|
|
74
|
+
requestsPerMinute: 600, // 10 requests/second = 600/minute
|
|
75
|
+
requestsPerDay: undefined // No daily limit
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
private logger = console;
|
|
79
|
+
private baseUrl = 'https://data.sec.gov/api/xbrl';
|
|
80
|
+
private tickersUrl = 'https://www.sec.gov/files/company_tickers.json';
|
|
81
|
+
private userAgent: string;
|
|
82
|
+
private config: Required<ISecEdgarConfig>;
|
|
83
|
+
|
|
84
|
+
// Caching
|
|
85
|
+
private cikCache = new Map<string, { cik: string; timestamp: Date }>();
|
|
86
|
+
private tickerListCache: { data: any; timestamp: Date } | null = null;
|
|
87
|
+
|
|
88
|
+
// Rate limiting
|
|
89
|
+
private rateLimiter = new RateLimiter();
|
|
90
|
+
|
|
91
|
+
constructor(config: ISecEdgarConfig) {
|
|
92
|
+
// Validate User-Agent
|
|
93
|
+
if (!config.userAgent) {
|
|
94
|
+
throw new Error('User-Agent is required for SEC EDGAR provider (format: "Company Name Email")');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Validate User-Agent format (must contain space and @ symbol)
|
|
98
|
+
if (!config.userAgent.includes(' ') || !config.userAgent.includes('@')) {
|
|
99
|
+
throw new Error(
|
|
100
|
+
'Invalid User-Agent format. Required: "Company Name Email" (e.g., "fin.cx info@fin.cx")'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this.userAgent = config.userAgent;
|
|
105
|
+
this.config = {
|
|
106
|
+
userAgent: config.userAgent,
|
|
107
|
+
cikCacheTTL: config.cikCacheTTL || 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
108
|
+
fundamentalsCacheTTL: config.fundamentalsCacheTTL || 90 * 24 * 60 * 60 * 1000, // 90 days
|
|
109
|
+
timeout: config.timeout || 30000
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Unified data fetching method
|
|
115
|
+
*/
|
|
116
|
+
public async fetchData(
|
|
117
|
+
request: IFundamentalsRequest
|
|
118
|
+
): Promise<IStockFundamentals | IStockFundamentals[]> {
|
|
119
|
+
switch (request.type) {
|
|
120
|
+
case 'fundamentals-current':
|
|
121
|
+
return this.fetchFundamentals(request);
|
|
122
|
+
case 'fundamentals-batch':
|
|
123
|
+
return this.fetchBatchFundamentals(request);
|
|
124
|
+
default:
|
|
125
|
+
throw new Error(`Unsupported request type: ${(request as any).type}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Fetch fundamental data for a single ticker
|
|
131
|
+
*/
|
|
132
|
+
private async fetchFundamentals(request: IFundamentalsCurrentRequest): Promise<IStockFundamentals> {
|
|
133
|
+
try {
|
|
134
|
+
// 1. Get CIK for ticker (with caching)
|
|
135
|
+
const cik = await this.getCIK(request.ticker);
|
|
136
|
+
|
|
137
|
+
// 2. Fetch company facts from SEC (with rate limiting)
|
|
138
|
+
const companyFacts = await this.fetchCompanyFacts(cik);
|
|
139
|
+
|
|
140
|
+
// 3. Parse facts into structured fundamental data
|
|
141
|
+
return this.parseCompanyFacts(request.ticker, cik, companyFacts);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
this.logger.error(`Failed to fetch fundamentals for ${request.ticker}:`, error);
|
|
144
|
+
throw new Error(`SEC EDGAR: Failed to fetch fundamentals for ${request.ticker}: ${error.message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Fetch fundamental data for multiple tickers
|
|
150
|
+
*/
|
|
151
|
+
private async fetchBatchFundamentals(
|
|
152
|
+
request: IFundamentalsBatchRequest
|
|
153
|
+
): Promise<IStockFundamentals[]> {
|
|
154
|
+
const results: IStockFundamentals[] = [];
|
|
155
|
+
const errors: string[] = [];
|
|
156
|
+
|
|
157
|
+
for (const ticker of request.tickers) {
|
|
158
|
+
try {
|
|
159
|
+
const fundamentals = await this.fetchFundamentals({
|
|
160
|
+
type: 'fundamentals-current',
|
|
161
|
+
ticker
|
|
162
|
+
});
|
|
163
|
+
results.push(fundamentals);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
this.logger.warn(`Failed to fetch fundamentals for ${ticker}:`, error);
|
|
166
|
+
errors.push(`${ticker}: ${error.message}`);
|
|
167
|
+
// Continue with other tickers
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (results.length === 0) {
|
|
172
|
+
throw new Error(`Failed to fetch fundamentals for all tickers. Errors: ${errors.join(', ')}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get CIK (Central Index Key) for a ticker symbol
|
|
180
|
+
* Uses SEC's public ticker-to-CIK mapping file
|
|
181
|
+
*/
|
|
182
|
+
private async getCIK(ticker: string): Promise<string> {
|
|
183
|
+
const tickerUpper = ticker.toUpperCase();
|
|
184
|
+
|
|
185
|
+
// Check cache first
|
|
186
|
+
const cached = this.cikCache.get(tickerUpper);
|
|
187
|
+
if (cached) {
|
|
188
|
+
const age = Date.now() - cached.timestamp.getTime();
|
|
189
|
+
if (age < this.config.cikCacheTTL) {
|
|
190
|
+
return cached.cik;
|
|
191
|
+
}
|
|
192
|
+
// Cache expired, remove it
|
|
193
|
+
this.cikCache.delete(tickerUpper);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Fetch ticker list (with caching at list level)
|
|
197
|
+
const tickers = await this.fetchTickerList();
|
|
198
|
+
|
|
199
|
+
// Find ticker in list (case-insensitive)
|
|
200
|
+
const entry = Object.values(tickers).find((t: any) => t.ticker === tickerUpper);
|
|
201
|
+
|
|
202
|
+
if (!entry) {
|
|
203
|
+
throw new Error(`CIK not found for ticker ${ticker}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const cik = String((entry as any).cik_str);
|
|
207
|
+
|
|
208
|
+
// Cache the result
|
|
209
|
+
this.cikCache.set(tickerUpper, {
|
|
210
|
+
cik,
|
|
211
|
+
timestamp: new Date()
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return cik;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Fetch the SEC ticker-to-CIK mapping list
|
|
219
|
+
* Cached for 24 hours (list updates daily)
|
|
220
|
+
*/
|
|
221
|
+
private async fetchTickerList(): Promise<any> {
|
|
222
|
+
// Check cache
|
|
223
|
+
if (this.tickerListCache) {
|
|
224
|
+
const age = Date.now() - this.tickerListCache.timestamp.getTime();
|
|
225
|
+
if (age < 24 * 60 * 60 * 1000) { // 24 hours
|
|
226
|
+
return this.tickerListCache.data;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Wait for rate limit slot
|
|
231
|
+
await this.rateLimiter.waitForSlot();
|
|
232
|
+
|
|
233
|
+
// Fetch from SEC
|
|
234
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
235
|
+
.url(this.tickersUrl)
|
|
236
|
+
.headers({
|
|
237
|
+
'User-Agent': this.userAgent,
|
|
238
|
+
'Accept': 'application/json'
|
|
239
|
+
})
|
|
240
|
+
.timeout(this.config.timeout)
|
|
241
|
+
.get();
|
|
242
|
+
|
|
243
|
+
const data = await response.json();
|
|
244
|
+
|
|
245
|
+
// Cache the list
|
|
246
|
+
this.tickerListCache = {
|
|
247
|
+
data,
|
|
248
|
+
timestamp: new Date()
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return data;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Fetch company facts from SEC EDGAR
|
|
256
|
+
*/
|
|
257
|
+
private async fetchCompanyFacts(cik: string): Promise<any> {
|
|
258
|
+
// Pad CIK to 10 digits
|
|
259
|
+
const paddedCIK = cik.padStart(10, '0');
|
|
260
|
+
const url = `${this.baseUrl}/companyfacts/CIK${paddedCIK}.json`;
|
|
261
|
+
|
|
262
|
+
// Wait for rate limit slot
|
|
263
|
+
await this.rateLimiter.waitForSlot();
|
|
264
|
+
|
|
265
|
+
// Fetch from SEC
|
|
266
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
267
|
+
.url(url)
|
|
268
|
+
.headers({
|
|
269
|
+
'User-Agent': this.userAgent,
|
|
270
|
+
'Accept': 'application/json',
|
|
271
|
+
'Accept-Encoding': 'gzip, deflate',
|
|
272
|
+
'Host': 'data.sec.gov'
|
|
273
|
+
})
|
|
274
|
+
.timeout(this.config.timeout)
|
|
275
|
+
.get();
|
|
276
|
+
|
|
277
|
+
const data = await response.json();
|
|
278
|
+
|
|
279
|
+
// Validate response
|
|
280
|
+
if (!data || !data.facts) {
|
|
281
|
+
throw new Error('Invalid response from SEC EDGAR API');
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return data;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Parse SEC company facts into structured fundamental data
|
|
289
|
+
*/
|
|
290
|
+
private parseCompanyFacts(ticker: string, cik: string, data: any): IStockFundamentals {
|
|
291
|
+
const usGaap = data.facts?.['us-gaap'];
|
|
292
|
+
|
|
293
|
+
if (!usGaap) {
|
|
294
|
+
throw new Error('No US GAAP data available');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Extract latest values for key metrics
|
|
298
|
+
const fundamentals: IStockFundamentals = {
|
|
299
|
+
ticker: ticker.toUpperCase(),
|
|
300
|
+
cik: cik,
|
|
301
|
+
companyName: data.entityName,
|
|
302
|
+
provider: this.name,
|
|
303
|
+
timestamp: new Date(),
|
|
304
|
+
fetchedAt: new Date(),
|
|
305
|
+
|
|
306
|
+
// Per-share metrics
|
|
307
|
+
earningsPerShareBasic: this.getLatestValue(usGaap, 'EarningsPerShareBasic'),
|
|
308
|
+
earningsPerShareDiluted: this.getLatestValue(usGaap, 'EarningsPerShareDiluted'),
|
|
309
|
+
sharesOutstanding: this.getLatestValue(usGaap, 'CommonStockSharesOutstanding'),
|
|
310
|
+
weightedAverageSharesOutstanding: this.getLatestValue(
|
|
311
|
+
usGaap,
|
|
312
|
+
'WeightedAverageNumberOfSharesOutstandingBasic'
|
|
313
|
+
),
|
|
314
|
+
|
|
315
|
+
// Income statement
|
|
316
|
+
revenue: this.getLatestValue(usGaap, 'Revenues') ||
|
|
317
|
+
this.getLatestValue(usGaap, 'RevenueFromContractWithCustomerExcludingAssessedTax'),
|
|
318
|
+
netIncome: this.getLatestValue(usGaap, 'NetIncomeLoss'),
|
|
319
|
+
operatingIncome: this.getLatestValue(usGaap, 'OperatingIncomeLoss'),
|
|
320
|
+
grossProfit: this.getLatestValue(usGaap, 'GrossProfit'),
|
|
321
|
+
costOfRevenue: this.getLatestValue(usGaap, 'CostOfRevenue'),
|
|
322
|
+
|
|
323
|
+
// Balance sheet
|
|
324
|
+
assets: this.getLatestValue(usGaap, 'Assets'),
|
|
325
|
+
liabilities: this.getLatestValue(usGaap, 'Liabilities'),
|
|
326
|
+
stockholdersEquity: this.getLatestValue(usGaap, 'StockholdersEquity'),
|
|
327
|
+
cash: this.getLatestValue(usGaap, 'CashAndCashEquivalentsAtCarryingValue'),
|
|
328
|
+
propertyPlantEquipment: this.getLatestValue(usGaap, 'PropertyPlantAndEquipmentNet'),
|
|
329
|
+
|
|
330
|
+
// Metadata (from latest available data point)
|
|
331
|
+
fiscalYear: this.getLatestMetadata(usGaap, 'EarningsPerShareDiluted')?.fy,
|
|
332
|
+
fiscalQuarter: this.getLatestMetadata(usGaap, 'EarningsPerShareDiluted')?.fp,
|
|
333
|
+
filingDate: this.getLatestMetadata(usGaap, 'EarningsPerShareDiluted')?.filed
|
|
334
|
+
? new Date(this.getLatestMetadata(usGaap, 'EarningsPerShareDiluted')!.filed)
|
|
335
|
+
: undefined,
|
|
336
|
+
form: this.getLatestMetadata(usGaap, 'EarningsPerShareDiluted')?.form
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
return fundamentals;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Get the latest value for a US GAAP metric
|
|
344
|
+
*/
|
|
345
|
+
private getLatestValue(usGaap: any, metricName: string): number | undefined {
|
|
346
|
+
const metric = usGaap[metricName];
|
|
347
|
+
if (!metric?.units) {
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Get the first unit type (USD, shares, etc.)
|
|
352
|
+
const unitType = Object.keys(metric.units)[0];
|
|
353
|
+
const values = metric.units[unitType];
|
|
354
|
+
|
|
355
|
+
if (!values || !Array.isArray(values) || values.length === 0) {
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Get the latest value (last in array)
|
|
360
|
+
const latest = values[values.length - 1];
|
|
361
|
+
return latest?.val;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get metadata from the latest data point
|
|
366
|
+
*/
|
|
367
|
+
private getLatestMetadata(usGaap: any, metricName: string): any | undefined {
|
|
368
|
+
const metric = usGaap[metricName];
|
|
369
|
+
if (!metric?.units) {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const unitType = Object.keys(metric.units)[0];
|
|
374
|
+
const values = metric.units[unitType];
|
|
375
|
+
|
|
376
|
+
if (!values || !Array.isArray(values) || values.length === 0) {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return values[values.length - 1];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Check if SEC EDGAR API is available
|
|
385
|
+
*/
|
|
386
|
+
public async isAvailable(): Promise<boolean> {
|
|
387
|
+
try {
|
|
388
|
+
// Test with Apple's well-known CIK
|
|
389
|
+
const url = `${this.baseUrl}/companyfacts/CIK0000320193.json`;
|
|
390
|
+
|
|
391
|
+
const response = await plugins.smartrequest.SmartRequest.create()
|
|
392
|
+
.url(url)
|
|
393
|
+
.headers({
|
|
394
|
+
'User-Agent': this.userAgent,
|
|
395
|
+
'Accept': 'application/json'
|
|
396
|
+
})
|
|
397
|
+
.timeout(5000)
|
|
398
|
+
.get();
|
|
399
|
+
|
|
400
|
+
const data = await response.json();
|
|
401
|
+
return data && data.facts !== undefined;
|
|
402
|
+
} catch (error) {
|
|
403
|
+
this.logger.warn('SEC EDGAR provider is not available:', error);
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get cache statistics
|
|
410
|
+
*/
|
|
411
|
+
public getCacheStats(): {
|
|
412
|
+
cikCacheSize: number;
|
|
413
|
+
hasTickerList: boolean;
|
|
414
|
+
} {
|
|
415
|
+
return {
|
|
416
|
+
cikCacheSize: this.cikCache.size,
|
|
417
|
+
hasTickerList: this.tickerListCache !== null
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Clear all caches
|
|
423
|
+
*/
|
|
424
|
+
public clearCache(): void {
|
|
425
|
+
this.cikCache.clear();
|
|
426
|
+
this.tickerListCache = null;
|
|
427
|
+
this.logger.log('SEC EDGAR cache cleared');
|
|
428
|
+
}
|
|
429
|
+
}
|