You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
180 lines
5.7 KiB
Python
180 lines
5.7 KiB
Python
"""
|
|
Insider transaction schemas
|
|
"""
|
|
|
|
from datetime import date
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
TRANSACTION_CODE_MAP = {
|
|
"P": "Purchase",
|
|
"S": "Sale",
|
|
"A": "Award/Grant",
|
|
"M": "Exercise/Conversion",
|
|
"G": "Gift",
|
|
"D": "Disposition to Issuer",
|
|
"F": "Tax Withholding",
|
|
"C": "Conversion",
|
|
"J": "Other",
|
|
}
|
|
|
|
|
|
class InsiderTransactionEntry(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
filing_date: date
|
|
transaction_date: date
|
|
owner_name: str
|
|
owner_cik: Optional[str] = None
|
|
is_officer: bool = False
|
|
is_director: bool = False
|
|
is_ten_percent_owner: bool = False
|
|
officer_title: Optional[str] = None
|
|
security_title: Optional[str] = None
|
|
transaction_code: str
|
|
transaction_type: str # human-readable
|
|
shares: float
|
|
price_per_share: Optional[float] = None
|
|
total_value: Optional[float] = None
|
|
shares_owned_after: Optional[float] = None
|
|
is_derivative: bool = False
|
|
|
|
@classmethod
|
|
def from_orm_obj(cls, obj) -> "InsiderTransactionEntry":
|
|
return cls(
|
|
filing_date=obj.filing_date.date() if hasattr(obj.filing_date, "date") else obj.filing_date,
|
|
transaction_date=obj.transaction_date.date() if hasattr(obj.transaction_date, "date") else obj.transaction_date,
|
|
owner_name=obj.owner_name,
|
|
owner_cik=obj.owner_cik,
|
|
is_officer=obj.is_officer or False,
|
|
is_director=obj.is_director or False,
|
|
is_ten_percent_owner=obj.is_ten_percent_owner or False,
|
|
officer_title=obj.officer_title,
|
|
security_title=obj.security_title,
|
|
transaction_code=obj.transaction_code,
|
|
transaction_type=TRANSACTION_CODE_MAP.get(obj.transaction_code, obj.transaction_code),
|
|
shares=obj.shares,
|
|
price_per_share=obj.price_per_share,
|
|
total_value=obj.total_value,
|
|
shares_owned_after=obj.shares_owned_after,
|
|
is_derivative=obj.is_derivative or False,
|
|
)
|
|
|
|
|
|
class InsiderTransactionResponse(BaseModel):
|
|
symbol: str
|
|
transactions: List[InsiderTransactionEntry]
|
|
total_count: int
|
|
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class InsiderSummaryPeriod(BaseModel):
|
|
period_label: str
|
|
buy_count: int = 0
|
|
sell_count: int = 0
|
|
buy_shares: float = 0.0
|
|
sell_shares: float = 0.0
|
|
buy_value: float = 0.0
|
|
sell_value: float = 0.0
|
|
net_shares: float = 0.0
|
|
net_value: float = 0.0
|
|
unique_buyers: int = 0
|
|
unique_sellers: int = 0
|
|
|
|
|
|
class InsiderSummaryResponse(BaseModel):
|
|
symbol: str
|
|
periods: List[InsiderSummaryPeriod]
|
|
notable_transactions: List[InsiderTransactionEntry]
|
|
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
# ------------------------------------------------------------------
|
|
# New PIT-safe Form 4 schemas
|
|
# ------------------------------------------------------------------
|
|
|
|
class Form4Entry(BaseModel):
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
symbol: str
|
|
filing_date: date
|
|
transaction_date: date
|
|
owner_cik: Optional[str] = None
|
|
owner_name: str
|
|
owner_relationship: Optional[str] = None
|
|
is_officer: bool = False
|
|
is_director: bool = False
|
|
is_ten_percent_owner: bool = False
|
|
is_ceo: bool = False
|
|
is_cfo: bool = False
|
|
is_c_suite: bool = False
|
|
transaction_code: str
|
|
transaction_type: str
|
|
shares: float
|
|
price: Optional[float] = None
|
|
total_value: Optional[float] = None
|
|
shares_owned_following: Optional[float] = None
|
|
purchase_pct_of_holding: Optional[float] = None
|
|
accession_number: str
|
|
|
|
@classmethod
|
|
def from_orm_obj(cls, obj) -> "Form4Entry":
|
|
return cls(
|
|
symbol=obj.ticker,
|
|
filing_date=obj.filing_date.date() if hasattr(obj.filing_date, "date") else obj.filing_date,
|
|
transaction_date=obj.transaction_date.date() if hasattr(obj.transaction_date, "date") else obj.transaction_date,
|
|
owner_cik=obj.owner_cik,
|
|
owner_name=obj.owner_name,
|
|
owner_relationship=obj.owner_relationship,
|
|
is_officer=obj.is_officer or False,
|
|
is_director=obj.is_director or False,
|
|
is_ten_percent_owner=obj.is_ten_percent_owner or False,
|
|
is_ceo=obj.is_ceo or False,
|
|
is_cfo=obj.is_cfo or False,
|
|
is_c_suite=obj.is_c_suite or False,
|
|
transaction_code=obj.transaction_code,
|
|
transaction_type=TRANSACTION_CODE_MAP.get(obj.transaction_code, obj.transaction_code),
|
|
shares=obj.shares,
|
|
price=obj.price_per_share,
|
|
total_value=obj.total_value,
|
|
shares_owned_following=obj.shares_owned_after,
|
|
purchase_pct_of_holding=obj.purchase_pct_of_holding,
|
|
accession_number=obj.accession_number,
|
|
)
|
|
|
|
|
|
class Form4Response(BaseModel):
|
|
symbol: str
|
|
as_of: date
|
|
window: Dict[str, Any] = Field(default_factory=dict)
|
|
transactions: List[Form4Entry]
|
|
total_count: int
|
|
metadata: Dict[str, Any] = Field(default_factory=dict)
|
|
|
|
|
|
class Form4ByDateResponse(BaseModel):
|
|
filing_date: date
|
|
buy_only: bool
|
|
transactions: List[Form4Entry]
|
|
total_count: int
|
|
|
|
|
|
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
|
|
buy_count: int
|
|
buy_dollar_total: float
|
|
cluster_size: int
|
|
csuite_count: int
|
|
avg_pct_of_holding: Optional[float] = None
|
|
recency_days: int
|
|
metadata: Dict[str, Any] = Field(default_factory=dict)
|