diff --git a/app/api/v1/endpoints/insider.py b/app/api/v1/endpoints/insider.py index 78f6fb7..c1a72e2 100644 --- a/app/api/v1/endpoints/insider.py +++ b/app/api/v1/endpoints/insider.py @@ -137,7 +137,7 @@ async def get_form4( as_of: date = Query(..., description="Point-in-time cutoff (filing_date ≤ as_of). Required."), start: Optional[date] = Query(None, description="Window start (filing_date ≥ start)"), end: Optional[date] = Query(None, description="Window end (filing_date ≤ end)"), - buy_only: bool = Query(False, description="Only return buy transactions (P/A, shares > 0)"), + buy_only: bool = Query(False, description="Only return open-market purchases (transaction_code=P, shares > 0). Excludes awards/grants."), csuite_only: bool = Query(False, description="Only return C-suite insider transactions"), db: AsyncSession = Depends(get_db), ): @@ -170,7 +170,7 @@ async def get_form4( async def get_form4_by_date( filing_date: date, response: Response, - buy_only: bool = Query(False, description="Only return buy transactions"), + buy_only: bool = Query(False, description="Only return open-market purchases (transaction_code=P). Excludes awards/grants."), db: AsyncSession = Depends(get_db), ): svc = InsiderTransactionService() diff --git a/app/core/config.py b/app/core/config.py index bc62343..a21524f 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -53,7 +53,7 @@ class Settings(BaseSettings): SEC_DATA_REFRESH_HOURS: int = 24 SEC_DATA_START_YEAR: int = 1994 # SEC EDGAR data available from 1994 SEC_INGEST_TIMEZONE: str = "America/New_York" - SEC_FORM4_BOOTSTRAP_QUARTERS: int = 8 # how many recent quarters to backfill + SEC_FORM4_BOOTSTRAP_QUARTERS: int = 10 # how many recent quarters to backfill (~2.5 years) # Security SECRET_KEY: str = os.getenv("SECRET_KEY", "development-secret-key-change-in-production") diff --git a/app/schemas/insider.py b/app/schemas/insider.py index 3232427..958e503 100644 --- a/app/schemas/insider.py +++ b/app/schemas/insider.py @@ -161,6 +161,11 @@ class Form4ByDateResponse(BaseModel): class Form4AggregateResponse(BaseModel): + """Aggregate Form 4 insider activity over a rolling window. + + All fields are computed over open-market purchases only (transaction_code='P', + shares > 0, non-derivative). Awards/grants (A-code) are excluded. + """ symbol: str as_of: date window_days: int diff --git a/app/services/insider_transaction_service.py b/app/services/insider_transaction_service.py index 1ac8bac..812d0c5 100644 --- a/app/services/insider_transaction_service.py +++ b/app/services/insider_transaction_service.py @@ -327,7 +327,7 @@ class InsiderTransactionService: if end: conditions.append(InsiderTransaction.filing_date <= datetime(end.year, end.month, end.day, 23, 59, 59, tzinfo=timezone.utc)) if buy_only: - conditions.append(InsiderTransaction.transaction_code.in_(["P", "A"])) + conditions.append(InsiderTransaction.transaction_code == "P") conditions.append(InsiderTransaction.shares > 0) if csuite_only: conditions.append(InsiderTransaction.is_c_suite == True) @@ -357,7 +357,7 @@ class InsiderTransactionService: func.cast(InsiderTransaction.filing_date, SADate) == filing_date, ] if buy_only: - conditions.append(InsiderTransaction.transaction_code.in_(["P", "A"])) + conditions.append(InsiderTransaction.transaction_code == "P") conditions.append(InsiderTransaction.shares > 0) result = await db.execute( @@ -384,7 +384,7 @@ class InsiderTransactionService: InsiderTransaction.ticker == ticker, InsiderTransaction.filing_date > window_start, InsiderTransaction.filing_date <= as_of_dt, - InsiderTransaction.transaction_code.in_(["P", "A"]), + InsiderTransaction.transaction_code == "P", InsiderTransaction.shares > 0, InsiderTransaction.is_derivative == False, ) @@ -553,9 +553,9 @@ class InsiderTransactionService: if price is not None and shares is not None: total_value = round(abs(shares) * price, 2) - # purchase_pct_of_holding: only for open-market buys/awards with known post-holding + # purchase_pct_of_holding: only for open-market purchases (P) with known post-holding purchase_pct = None - if code in ("P", "A") and shares is not None and shares > 0 and shares_after and shares_after > 0: + if code == "P" and shares is not None and shares > 0 and shares_after and shares_after > 0: purchase_pct = abs(shares) / shares_after return {