Dmitry Beresnev commited on
Commit
a98decd
·
1 Parent(s): 2671ab5

fix the requests to the SEC API

Browse files
.env.example CHANGED
@@ -9,4 +9,5 @@ WEBHOOK_SECRET=
9
  SPACE_URL=
10
  HF_TOKEN=
11
  HF_DATASET_REPO=
12
- FMP_API_TOKEN=
 
 
9
  SPACE_URL=
10
  HF_TOKEN=
11
  HF_DATASET_REPO=
12
+ FMP_API_TOKEN=
13
+ SEC_API_TOKEN=
requirements.txt CHANGED
@@ -33,3 +33,5 @@ typing-extensions>=4.5.0
33
  pytz>=2023.3
34
 
35
  TA-Lib>=0.4.19
 
 
 
33
  pytz>=2023.3
34
 
35
  TA-Lib>=0.4.19
36
+
37
+ sec-api
src/api/insiders/insider_trading_aggregator.py CHANGED
@@ -109,6 +109,67 @@ class InsiderTradingAggregator:
109
  # This was a major performance bottleneck.
110
  # await asyncio.sleep(60 / self.apis[api_name]['requests_per_minute'])
111
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  async def get_fmp_insider_trades(self, session: aiohttp.ClientSession,
113
  symbol: str, limit: int = 100, filter_days: int = 30) -> List[InsiderTrade]:
114
  """
@@ -182,6 +243,7 @@ class InsiderTradingAggregator:
182
  f"Retrieved {len(all_trades)} trades from FMP for {symbol} by iterating through {len(date_range)} days.")
183
  return all_trades
184
 
 
185
  async def get_sec_api_insider_trades(self, session: aiohttp.ClientSession,
186
  symbol: str, limit: int = 100) -> List[InsiderTrade]:
187
  """Get insider trades from SEC-API"""
@@ -215,7 +277,185 @@ class InsiderTradingAggregator:
215
  continue
216
 
217
  logger.info(f"Retrieved {len(trades)} trades from SEC-API for {symbol}")
218
- return trades
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
 
220
  async def get_eod_insider_trades(self, session: aiohttp.ClientSession,
221
  symbol: str) -> List[InsiderTrade]:
@@ -393,8 +633,8 @@ class InsiderTradingAggregator:
393
  async with aiohttp.ClientSession(timeout=timeout) as session:
394
  # Create tasks for all API calls
395
  tasks = [
396
- self.get_fmp_insider_trades(session, symbol, limit_per_api),
397
- #self.get_sec_api_insider_trades(session, symbol, limit_per_api),
398
  #self.get_eod_insider_trades(session, symbol),
399
  #self.get_tradefeeds_insider_trades(session, symbol, limit_per_api),
400
  #self.get_factored_ai_insider_trades(session, symbol)
 
109
  # This was a major performance bottleneck.
110
  # await asyncio.sleep(60 / self.apis[api_name]['requests_per_minute'])
111
 
112
+ '''
113
+ async def _make_post_request(self, session: aiohttp.ClientSession, api_name: str,
114
+ endpoint: str, payload: Dict) -> Optional[Dict]:
115
+ """Make an async rate-limited POST request to an API."""
116
+ if not self._check_rate_limit(api_name):
117
+ logger.warning(f"Daily rate limit reached for {api_name}")
118
+ return None
119
+
120
+ if not self.apis[api_name]['api_key']:
121
+ logger.warning(f"No API key set for {api_name}, skipping request.")
122
+ return None
123
+
124
+ async with self.semaphores[api_name]:
125
+ try:
126
+ url = f"{self.apis[api_name]['base_url']}/{endpoint}"
127
+
128
+ # Set Authorization header per SEC API docs (no "Bearer" prefix)
129
+ headers = {
130
+ 'Authorization': self.apis[api_name]['api_key'],
131
+ 'Content-Type': 'application/json'
132
+ }
133
+
134
+ async with session.post(url, json=payload, headers=headers) as response:
135
+ response.raise_for_status()
136
+ self.request_counts[api_name] += 1
137
+ return await response.json()
138
+
139
+ except aiohttp.ClientError as e:
140
+ logger.error(f"POST request failed for {api_name} ({url}): {e}")
141
+ return None
142
+ '''
143
+
144
+ async def _make_post_request(self, session: aiohttp.ClientSession, api_name: str,
145
+ endpoint: str, payload: Dict) -> Optional[Dict]:
146
+ """Make an async rate-limited POST request to an API."""
147
+ if not self._check_rate_limit(api_name):
148
+ logger.warning(f"Daily rate limit reached for {api_name}")
149
+ return None
150
+
151
+ if not self.apis[api_name]['api_key']:
152
+ logger.warning(f"No API key set for {api_name}, skipping request.")
153
+ return None
154
+
155
+ async with self.semaphores[api_name]:
156
+ try:
157
+ # Build the full URL
158
+ url = f"{self.apis[api_name]['base_url']}/{endpoint}"
159
+
160
+ # Add API key as query parameter (SEC API supports this method)
161
+ url_with_token = f"{url}?token={self.apis[api_name]['api_key']}"
162
+
163
+ # Make POST request with JSON payload
164
+ async with session.post(url_with_token, json=payload) as response:
165
+ response.raise_for_status()
166
+ self.request_counts[api_name] += 1
167
+ return await response.json()
168
+
169
+ except aiohttp.ClientError as e:
170
+ logger.error(f"POST request failed for {api_name} ({url}): {e}")
171
+ return None
172
+
173
  async def get_fmp_insider_trades(self, session: aiohttp.ClientSession,
174
  symbol: str, limit: int = 100, filter_days: int = 30) -> List[InsiderTrade]:
175
  """
 
243
  f"Retrieved {len(all_trades)} trades from FMP for {symbol} by iterating through {len(date_range)} days.")
244
  return all_trades
245
 
246
+ '''
247
  async def get_sec_api_insider_trades(self, session: aiohttp.ClientSession,
248
  symbol: str, limit: int = 100) -> List[InsiderTrade]:
249
  """Get insider trades from SEC-API"""
 
277
  continue
278
 
279
  logger.info(f"Retrieved {len(trades)} trades from SEC-API for {symbol}")
280
+ return trades
281
+
282
+
283
+ async def get_sec_api_insider_trades(self, session: aiohttp.ClientSession,
284
+ symbol: str, limit: int = 50,
285
+ filter_days: int = 30) -> List[InsiderTrade]:
286
+ """Get insider trades from SEC-API using a POST request with proper query structure."""
287
+ all_trades = []
288
+
289
+ # Ensure the limit does not exceed the API's maximum
290
+ if limit > 50:
291
+ limit = 50
292
+
293
+ # Date range for filtering
294
+ to_date = datetime.now()
295
+ from_date = to_date - timedelta(days=filter_days)
296
+
297
+ start_index = 0
298
+
299
+ while True:
300
+ # Construct query payload according to SEC API documentation
301
+ query_payload = {
302
+ "query": {
303
+ "query_string": {
304
+ "query": f'issuer.tradingSymbol:"{symbol}" AND periodOfReport:[{from_date.strftime("%Y-%m-%d")} TO {to_date.strftime("%Y-%m-%d")}]'
305
+ }
306
+ },
307
+ "from": str(start_index),
308
+ "size": str(limit),
309
+ "sort": [{"filedAt": {"order": "desc"}}]
310
+ }
311
+
312
+ endpoint = "insider-trading"
313
+ logger.info(f"Requesting SEC-API data for {symbol}, starting at index {start_index}...")
314
+ data = await self._make_post_request(session, 'sec_api', endpoint, query_payload)
315
+
316
+ if not data or 'transactions' not in data or not data['transactions']:
317
+ break
318
+
319
+ for filing in data['transactions']:
320
+ # Parse both non-derivative and derivative transactions
321
+ transactions_list = filing.get('nonDerivativeTable', {}).get('transactions', []) + \
322
+ filing.get('derivativeTable', {}).get('transactions', [])
323
+
324
+ for transaction in transactions_list:
325
+ try:
326
+ # Map transaction codes to buy/sell
327
+ trans_code = transaction.get('coding', {}).get('code', '')
328
+ if trans_code in ('A', 'P'): # Acquisition or Purchase
329
+ trans_type = TransactionType.BUY.value
330
+ elif trans_code in ('D', 'S'): # Disposition or Sale
331
+ trans_type = TransactionType.SELL.value
332
+ else:
333
+ continue # Skip other transaction types
334
+
335
+ shares = transaction.get('amounts', {}).get('shares')
336
+ price_per_share = transaction.get('amounts', {}).get('pricePerShare')
337
+
338
+ if shares is None or price_per_share is None:
339
+ continue
340
+
341
+ shares = int(shares)
342
+ price_per_share = float(price_per_share)
343
+
344
+ insider_trade = InsiderTrade(
345
+ symbol=filing.get('issuer', {}).get('tradingSymbol', symbol),
346
+ company_name=filing.get('issuer', {}).get('name', ''),
347
+ insider_name=filing.get('reportingOwner', {}).get('name', ''),
348
+ position=filing.get('reportingOwner', {}).get('relationship', {}).get('officerTitle',
349
+ 'Insider'),
350
+ transaction_date=transaction.get('transactionDate', {}).get('value', ''),
351
+ transaction_type=trans_type,
352
+ shares=shares,
353
+ price=price_per_share,
354
+ value=float(shares * price_per_share),
355
+ form_type=filing.get('documentType', ''),
356
+ source='SEC-API',
357
+ filing_date=filing.get('filedAt', '')
358
+ )
359
+ all_trades.append(insider_trade)
360
+
361
+ except (ValueError, TypeError, AttributeError) as e:
362
+ logger.warning(f"Error parsing SEC-API transaction for {symbol}: {e}")
363
+ continue
364
+
365
+ start_index += limit
366
+
367
+ # Stop if we got fewer results than requested (last page)
368
+ if len(data['transactions']) < limit:
369
+ break
370
+
371
+ logger.info(f"Retrieved {len(all_trades)} trades from SEC-API for {symbol}.")
372
+ return all_trades
373
+ '''
374
+
375
+ async def get_sec_api_insider_trades(self, session: aiohttp.ClientSession,
376
+ symbol: str, limit: int = 50,
377
+ filter_days: int = 30) -> List[InsiderTrade]:
378
+ """Get insider trades from SEC-API using a POST request with proper query structure."""
379
+ all_trades = []
380
+
381
+ if limit > 50:
382
+ limit = 50
383
+
384
+ to_date = datetime.now()
385
+ from_date = to_date - timedelta(days=filter_days)
386
+
387
+ start_index = 0
388
+
389
+ while True:
390
+ # Use simple query string format as shown in official Python SDK
391
+ query_payload = {
392
+ "query": f'issuer.tradingSymbol:"{symbol}" AND periodOfReport:[{from_date.strftime("%Y-%m-%d")} TO {to_date.strftime("%Y-%m-%d")}]',
393
+ "from": str(start_index),
394
+ "size": str(limit),
395
+ "sort": [{"filedAt": {"order": "desc"}}]
396
+ }
397
+
398
+ endpoint = "insider-trading"
399
+ logger.info(f"Requesting SEC-API data for {symbol}, starting at index {start_index}...")
400
+ data = await self._make_post_request(session, 'sec_api', endpoint, query_payload)
401
+
402
+ if not data or 'transactions' not in data or not data['transactions']:
403
+ break
404
+
405
+ for filing in data['transactions']:
406
+ # Check if nonDerivativeTable exists and has transactions
407
+ non_deriv_trans = filing.get('nonDerivativeTable', {}).get('transactions', [])
408
+ deriv_trans = filing.get('derivativeTable', {}).get('transactions', [])
409
+
410
+ transactions_list = non_deriv_trans + deriv_trans
411
+
412
+ for transaction in transactions_list:
413
+ try:
414
+ trans_code = transaction.get('coding', {}).get('code', '')
415
+ if trans_code in ('A', 'P'):
416
+ trans_type = TransactionType.BUY.value
417
+ elif trans_code in ('D', 'S'):
418
+ trans_type = TransactionType.SELL.value
419
+ else:
420
+ continue
421
+
422
+ shares = transaction.get('amounts', {}).get('shares')
423
+ price_per_share = transaction.get('amounts', {}).get('pricePerShare')
424
+
425
+ if shares is None or price_per_share is None:
426
+ continue
427
+
428
+ shares = float(shares)
429
+ price_per_share = float(price_per_share)
430
+
431
+ insider_trade = InsiderTrade(
432
+ symbol=filing.get('issuer', {}).get('tradingSymbol', symbol),
433
+ company_name=filing.get('issuer', {}).get('name', ''),
434
+ insider_name=filing.get('reportingOwner', {}).get('name', ''),
435
+ position=filing.get('reportingOwner', {}).get('relationship', {}).get('officerTitle',
436
+ 'Insider'),
437
+ transaction_date=transaction.get('transactionDate', ''),
438
+ transaction_type=trans_type,
439
+ shares=int(shares),
440
+ price=price_per_share,
441
+ value=shares * price_per_share,
442
+ form_type=filing.get('documentType', ''),
443
+ source='SEC-API',
444
+ filing_date=filing.get('filedAt', '')
445
+ )
446
+ all_trades.append(insider_trade)
447
+
448
+ except (ValueError, TypeError, AttributeError) as e:
449
+ logger.warning(f"Error parsing SEC-API transaction for {symbol}: {e}")
450
+ continue
451
+
452
+ start_index += limit
453
+
454
+ if len(data['transactions']) < limit:
455
+ break
456
+
457
+ logger.info(f"Retrieved {len(all_trades)} trades from SEC-API for {symbol}.")
458
+ return all_trades
459
 
460
  async def get_eod_insider_trades(self, session: aiohttp.ClientSession,
461
  symbol: str) -> List[InsiderTrade]:
 
633
  async with aiohttp.ClientSession(timeout=timeout) as session:
634
  # Create tasks for all API calls
635
  tasks = [
636
+ #self.get_fmp_insider_trades(session, symbol, limit_per_api),
637
+ self.get_sec_api_insider_trades(session, symbol, limit_per_api),
638
  #self.get_eod_insider_trades(session, symbol),
639
  #self.get_tradefeeds_insider_trades(session, symbol, limit_per_api),
640
  #self.get_factored_ai_insider_trades(session, symbol)
src/telegram_bot/config.py CHANGED
@@ -17,6 +17,7 @@ class Config:
17
  OPENROUTER_API_KEY_2 = os.getenv('OPENROUTER_API_TOKEN_2', '')
18
  GEMINI_API_KEY = os.getenv('GEMINI_API_TOKEN', '')
19
  FMP_API_KEY = os.getenv('FMP_API_TOKEN', '')
 
20
 
21
  @classmethod
22
  def validate(cls) -> bool:
 
17
  OPENROUTER_API_KEY_2 = os.getenv('OPENROUTER_API_TOKEN_2', '')
18
  GEMINI_API_KEY = os.getenv('GEMINI_API_TOKEN', '')
19
  FMP_API_KEY = os.getenv('FMP_API_TOKEN', '')
20
+ SEC_API_KEY = os.getenv('SEC_API_TOKEN', '')
21
 
22
  @classmethod
23
  def validate(cls) -> bool: