@fin.cx/opendata 2.1.0 → 3.1.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.
Files changed (30) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/stocks/classes.fundamentalsservice.d.ts +104 -0
  3. package/dist_ts/stocks/classes.fundamentalsservice.js +298 -0
  4. package/dist_ts/stocks/classes.stockservice.d.ts +16 -7
  5. package/dist_ts/stocks/classes.stockservice.js +17 -98
  6. package/dist_ts/stocks/index.d.ts +3 -0
  7. package/dist_ts/stocks/index.js +5 -2
  8. package/dist_ts/stocks/interfaces/fundamentals.d.ts +82 -0
  9. package/dist_ts/stocks/interfaces/fundamentals.js +6 -0
  10. package/dist_ts/stocks/interfaces/provider.d.ts +2 -3
  11. package/dist_ts/stocks/interfaces/stockprice.d.ts +2 -10
  12. package/dist_ts/stocks/interfaces/stockprice.js +2 -2
  13. package/dist_ts/stocks/providers/provider.marketstack.d.ts +6 -11
  14. package/dist_ts/stocks/providers/provider.marketstack.js +32 -24
  15. package/dist_ts/stocks/providers/provider.secedgar.d.ts +102 -0
  16. package/dist_ts/stocks/providers/provider.secedgar.js +343 -0
  17. package/dist_ts/stocks/providers/provider.yahoo.d.ts +13 -3
  18. package/dist_ts/stocks/providers/provider.yahoo.js +27 -4
  19. package/package.json +1 -1
  20. package/readme.md +383 -388
  21. package/ts/00_commitinfo_data.ts +1 -1
  22. package/ts/stocks/classes.fundamentalsservice.ts +404 -0
  23. package/ts/stocks/classes.stockservice.ts +28 -140
  24. package/ts/stocks/index.ts +5 -2
  25. package/ts/stocks/interfaces/fundamentals.ts +102 -0
  26. package/ts/stocks/interfaces/provider.ts +5 -6
  27. package/ts/stocks/interfaces/stockprice.ts +4 -19
  28. package/ts/stocks/providers/provider.marketstack.ts +39 -30
  29. package/ts/stocks/providers/provider.secedgar.ts +429 -0
  30. package/ts/stocks/providers/provider.yahoo.ts +33 -4
@@ -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
+ }
@@ -1,16 +1,15 @@
1
- import type { IStockPrice, IStockQuoteRequest, IStockBatchQuoteRequest } from './stockprice.js';
1
+ import type { IStockPrice, IStockDataRequest } from './stockprice.js';
2
2
 
3
3
  export interface IStockProvider {
4
4
  name: string;
5
5
  priority: number;
6
-
7
- fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice>;
8
- fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]>;
6
+
7
+ fetchData(request: IStockDataRequest): Promise<IStockPrice | IStockPrice[]>;
9
8
  isAvailable(): Promise<boolean>;
10
-
9
+
11
10
  supportsMarket?(market: string): boolean;
12
11
  supportsTicker?(ticker: string): boolean;
13
-
12
+
14
13
  readonly requiresAuth: boolean;
15
14
  readonly rateLimit?: {
16
15
  requestsPerMinute: number;
@@ -1,5 +1,3 @@
1
- import * as plugins from '../../plugins.js';
2
-
3
1
  // Enhanced stock price interface with additional OHLCV data
4
2
  export interface IStockPrice {
5
3
  ticker: string;
@@ -22,11 +20,11 @@ export interface IStockPrice {
22
20
  adjusted?: boolean; // If price is split/dividend adjusted
23
21
  dataType: 'eod' | 'intraday' | 'live'; // What kind of data this is
24
22
  fetchedAt: Date; // When we fetched (vs data timestamp)
23
+
24
+ // Company identification
25
+ companyName?: string; // Company name (e.g., "Apple Inc.")
26
+ companyFullName?: string; // Full company name with exchange (e.g., "Apple Inc. (NASDAQ:AAPL)")
25
27
  }
26
- type CheckStockPrice = plugins.tsclass.typeFest.IsEqual<
27
- IStockPrice,
28
- plugins.tsclass.finance.IStockPrice
29
- >;
30
28
 
31
29
  export interface IStockPriceError {
32
30
  ticker: string;
@@ -94,16 +92,3 @@ export type IStockDataRequest =
94
92
  | IStockHistoricalRequest
95
93
  | IStockIntradayRequest
96
94
  | IStockBatchCurrentRequest;
97
-
98
- // Legacy interfaces (for backward compatibility during migration)
99
- /** @deprecated Use IStockDataRequest with type: 'current' instead */
100
- export interface IStockQuoteRequest {
101
- ticker: string;
102
- includeExtendedHours?: boolean;
103
- }
104
-
105
- /** @deprecated Use IStockDataRequest with type: 'batch' instead */
106
- export interface IStockBatchQuoteRequest {
107
- tickers: string[];
108
- includeExtendedHours?: boolean;
109
- }
@@ -2,15 +2,11 @@ import * as plugins from '../../plugins.js';
2
2
  import type { IStockProvider, IProviderConfig } from '../interfaces/provider.js';
3
3
  import type {
4
4
  IStockPrice,
5
- IStockQuoteRequest,
6
- IStockBatchQuoteRequest,
7
5
  IStockDataRequest,
8
6
  IStockCurrentRequest,
9
7
  IStockHistoricalRequest,
10
8
  IStockIntradayRequest,
11
- IStockBatchCurrentRequest,
12
- IPaginatedResponse,
13
- TSortOrder
9
+ IStockBatchCurrentRequest
14
10
  } from '../interfaces/stockprice.js';
15
11
 
16
12
  /**
@@ -239,30 +235,6 @@ export class MarketstackProvider implements IStockProvider {
239
235
  }
240
236
  }
241
237
 
242
- /**
243
- * Legacy: Fetch latest EOD price for a single ticker
244
- * @deprecated Use fetchData with IStockDataRequest instead
245
- */
246
- public async fetchPrice(request: IStockQuoteRequest): Promise<IStockPrice> {
247
- // Map legacy request to new format
248
- return this.fetchCurrentPrice({
249
- type: 'current',
250
- ticker: request.ticker
251
- });
252
- }
253
-
254
- /**
255
- * Legacy: Fetch latest EOD prices for multiple tickers
256
- * @deprecated Use fetchData with IStockDataRequest instead
257
- */
258
- public async fetchPrices(request: IStockBatchQuoteRequest): Promise<IStockPrice[]> {
259
- // Map legacy request to new format
260
- return this.fetchBatchCurrentPrices({
261
- type: 'batch',
262
- tickers: request.tickers
263
- });
264
- }
265
-
266
238
  /**
267
239
  * Check if the Marketstack API is available and accessible
268
240
  */
@@ -349,12 +321,49 @@ export class MarketstackProvider implements IStockProvider {
349
321
  low: data.low,
350
322
  adjusted: data.adj_close !== undefined, // If adj_close exists, price is adjusted
351
323
  dataType: dataType,
352
- fetchedAt: fetchedAt
324
+ fetchedAt: fetchedAt,
325
+
326
+ // Company identification
327
+ companyName: data.company_name || data.name || undefined,
328
+ companyFullName: this.buildCompanyFullName(data)
353
329
  };
354
330
 
355
331
  return stockPrice;
356
332
  }
357
333
 
334
+ /**
335
+ * Build full company name with exchange and ticker information
336
+ * Example: "Apple Inc (NASDAQ:AAPL)"
337
+ */
338
+ private buildCompanyFullName(data: any): string | undefined {
339
+ // Check if API already provides full name
340
+ if (data.full_name || data.long_name) {
341
+ return data.full_name || data.long_name;
342
+ }
343
+
344
+ // Build from available data
345
+ const companyName = data.company_name || data.name;
346
+ const exchangeCode = data.exchange_code; // e.g., "NASDAQ"
347
+ const symbol = data.symbol; // e.g., "AAPL"
348
+
349
+ if (!companyName) {
350
+ return undefined;
351
+ }
352
+
353
+ // If we have exchange and symbol, build full name: "Apple Inc (NASDAQ:AAPL)"
354
+ if (exchangeCode && symbol) {
355
+ return `${companyName} (${exchangeCode}:${symbol})`;
356
+ }
357
+
358
+ // If we only have symbol: "Apple Inc (AAPL)"
359
+ if (symbol) {
360
+ return `${companyName} (${symbol})`;
361
+ }
362
+
363
+ // Otherwise just return company name
364
+ return companyName;
365
+ }
366
+
358
367
  /**
359
368
  * Format date to YYYY-MM-DD for API requests
360
369
  */
@@ -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
+ }