From 93a81fd35aa9a408920d8817951fc82960e5a363 Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Thu, 23 Apr 2026 21:40:21 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20Form=204=20buy=5Fonly=3DP-only=20+=2010?= =?UTF-8?q?=EB=B6=84=EA=B8=B0=20backfill=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - buy_only=true 필터를 P+A → P(open-market purchase) 단독으로 변경 - get_form4_pit / get_form4_by_date / get_form4_aggregate 3곳 동일 수정 - aggregate의 buy_dollar_total / cluster_size / csuite_count / avg_pct_of_holding 모두 P-only 기준으로 정확해짐 (A코드 awards 제외) - _parse_transaction_element: purchase_pct_of_holding 계산도 P-only로 정합 - SEC_FORM4_BOOTSTRAP_QUARTERS: 8 → 10 (2024Q1~) - Form4AggregateResponse docstring에 P-only 명시 Co-Authored-By: Claude Sonnet 4.6 --- app/api/v1/endpoints/insider.py | 4 ++-- app/core/config.py | 2 +- app/schemas/insider.py | 5 +++++ app/services/insider_transaction_service.py | 10 +++++----- 4 files changed, 13 insertions(+), 8 deletions(-) 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 {