use crate::{
Client, Result,
endpoints::{
EndpointSpec, QueryEncoder, QueryParameters,
metadata::{EndpointBounds, EndpointMetadata, GeographicAvailability},
},
responses::insider_trading::{
BeneficialOwnershipAcquisition, InsiderReportingName, InsiderTrade, InsiderTradeStatistics,
InsiderTransactionType,
},
types::{Cik, Date, Limit, Page, SearchTerm, Ticker, TransactionTypeCode},
};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct LatestInsiderTradesQuery {
date: Option<Date>,
page: Option<Page>,
limit: Option<Limit>,
}
impl LatestInsiderTradesQuery {
pub const fn new() -> Self {
Self {
date: None,
page: None,
limit: None,
}
}
pub const fn with_date(mut self, date: Date) -> Self {
self.date = Some(date);
self
}
pub const fn with_page(mut self, page: Page) -> Self {
self.page = Some(page);
self
}
pub const fn with_limit(mut self, limit: Limit) -> Self {
self.limit = Some(limit);
self
}
pub const fn date(&self) -> Option<Date> {
self.date
}
pub const fn page(&self) -> Option<Page> {
self.page
}
pub const fn limit(&self) -> Option<Limit> {
self.limit
}
}
impl QueryParameters for LatestInsiderTradesQuery {
fn encode(&self, encoder: &mut QueryEncoder<'_>) {
encoder.optional("date", self.date);
encoder.optional("page", self.page);
encoder.optional("limit", self.limit);
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct InsiderTradesSearchQuery {
symbol: Option<Ticker>,
page: Option<Page>,
limit: Option<Limit>,
reporting_cik: Option<Cik>,
company_cik: Option<Cik>,
transaction_type: Option<TransactionTypeCode>,
}
impl InsiderTradesSearchQuery {
pub const fn new() -> Self {
Self {
symbol: None,
page: None,
limit: None,
reporting_cik: None,
company_cik: None,
transaction_type: None,
}
}
pub fn with_symbol(mut self, symbol: Ticker) -> Self {
self.symbol = Some(symbol);
self
}
pub const fn with_page(mut self, page: Page) -> Self {
self.page = Some(page);
self
}
pub const fn with_limit(mut self, limit: Limit) -> Self {
self.limit = Some(limit);
self
}
pub fn with_reporting_cik(mut self, reporting_cik: Cik) -> Self {
self.reporting_cik = Some(reporting_cik);
self
}
pub fn with_company_cik(mut self, company_cik: Cik) -> Self {
self.company_cik = Some(company_cik);
self
}
pub fn with_transaction_type(mut self, transaction_type: TransactionTypeCode) -> Self {
self.transaction_type = Some(transaction_type);
self
}
pub const fn symbol(&self) -> Option<&Ticker> {
self.symbol.as_ref()
}
pub const fn page(&self) -> Option<Page> {
self.page
}
pub const fn limit(&self) -> Option<Limit> {
self.limit
}
pub const fn reporting_cik(&self) -> Option<&Cik> {
self.reporting_cik.as_ref()
}
pub const fn company_cik(&self) -> Option<&Cik> {
self.company_cik.as_ref()
}
pub const fn transaction_type(&self) -> Option<&TransactionTypeCode> {
self.transaction_type.as_ref()
}
}
impl QueryParameters for InsiderTradesSearchQuery {
fn encode(&self, encoder: &mut QueryEncoder<'_>) {
encoder.optional("symbol", self.symbol.as_ref());
encoder.optional("page", self.page);
encoder.optional("limit", self.limit);
encoder.optional("reportingCik", self.reporting_cik.as_ref());
encoder.optional("companyCik", self.company_cik.as_ref());
encoder.optional("transactionType", self.transaction_type.as_ref());
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InsiderReportingNameSearchQuery {
name: SearchTerm,
}
impl InsiderReportingNameSearchQuery {
pub const fn new(name: SearchTerm) -> Self {
Self { name }
}
pub const fn name(&self) -> &SearchTerm {
&self.name
}
}
impl From<SearchTerm> for InsiderReportingNameSearchQuery {
fn from(name: SearchTerm) -> Self {
Self::new(name)
}
}
impl From<&SearchTerm> for InsiderReportingNameSearchQuery {
fn from(name: &SearchTerm) -> Self {
Self::new(name.clone())
}
}
impl QueryParameters for InsiderReportingNameSearchQuery {
fn encode(&self, encoder: &mut QueryEncoder<'_>) {
encoder.required("name", &self.name);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InsiderTradeStatisticsQuery {
symbol: Ticker,
}
impl InsiderTradeStatisticsQuery {
pub const fn new(symbol: Ticker) -> Self {
Self { symbol }
}
pub const fn symbol(&self) -> &Ticker {
&self.symbol
}
}
impl From<Ticker> for InsiderTradeStatisticsQuery {
fn from(symbol: Ticker) -> Self {
Self::new(symbol)
}
}
impl From<&Ticker> for InsiderTradeStatisticsQuery {
fn from(symbol: &Ticker) -> Self {
Self::new(symbol.clone())
}
}
impl QueryParameters for InsiderTradeStatisticsQuery {
fn encode(&self, encoder: &mut QueryEncoder<'_>) {
encoder.required("symbol", &self.symbol);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BeneficialOwnershipAcquisitionsQuery {
symbol: Ticker,
limit: Option<Limit>,
}
impl BeneficialOwnershipAcquisitionsQuery {
pub const fn new(symbol: Ticker) -> Self {
Self {
symbol,
limit: None,
}
}
pub const fn with_limit(mut self, limit: Limit) -> Self {
self.limit = Some(limit);
self
}
pub const fn symbol(&self) -> &Ticker {
&self.symbol
}
pub const fn limit(&self) -> Option<Limit> {
self.limit
}
}
impl From<Ticker> for BeneficialOwnershipAcquisitionsQuery {
fn from(symbol: Ticker) -> Self {
Self::new(symbol)
}
}
impl From<&Ticker> for BeneficialOwnershipAcquisitionsQuery {
fn from(symbol: &Ticker) -> Self {
Self::new(symbol.clone())
}
}
impl QueryParameters for BeneficialOwnershipAcquisitionsQuery {
fn encode(&self, encoder: &mut QueryEncoder<'_>) {
encoder.required("symbol", &self.symbol);
encoder.optional("limit", self.limit);
}
}
const PAGINATED_US_ONLY: EndpointMetadata = EndpointMetadata::new()
.with_geography(GeographicAvailability::UsOnly)
.with_bounds(
EndpointBounds::new()
.with_response_rows(1_000)
.with_page(100),
);
const US_ONLY: EndpointMetadata =
EndpointMetadata::new().with_geography(GeographicAvailability::UsOnly);
pub fn latest_insider_trades(
query: LatestInsiderTradesQuery,
) -> EndpointSpec<LatestInsiderTradesQuery, Vec<InsiderTrade>> {
EndpointSpec::get("insider-trading/latest", "insider-trading/latest", query)
.with_metadata(PAGINATED_US_ONLY)
}
pub fn search_insider_trades(
query: InsiderTradesSearchQuery,
) -> EndpointSpec<InsiderTradesSearchQuery, Vec<InsiderTrade>> {
EndpointSpec::get("insider-trading/search", "insider-trading/search", query)
.with_metadata(PAGINATED_US_ONLY)
}
pub fn search_insider_reporting_names(
query: InsiderReportingNameSearchQuery,
) -> EndpointSpec<InsiderReportingNameSearchQuery, Vec<InsiderReportingName>> {
EndpointSpec::get(
"insider-trading/reporting-name",
"insider-trading/reporting-name",
query,
)
.with_metadata(US_ONLY)
}
pub fn insider_transaction_types() -> EndpointSpec<(), Vec<InsiderTransactionType>> {
EndpointSpec::get(
"insider-trading-transaction-type",
"insider-trading-transaction-type",
(),
)
.with_metadata(US_ONLY)
}
pub fn insider_trade_statistics(
query: InsiderTradeStatisticsQuery,
) -> EndpointSpec<InsiderTradeStatisticsQuery, Vec<InsiderTradeStatistics>> {
EndpointSpec::get(
"insider-trading/statistics",
"insider-trading/statistics",
query,
)
.with_metadata(US_ONLY)
}
pub fn beneficial_ownership_acquisitions(
query: BeneficialOwnershipAcquisitionsQuery,
) -> EndpointSpec<BeneficialOwnershipAcquisitionsQuery, Vec<BeneficialOwnershipAcquisition>> {
EndpointSpec::get(
"acquisition-of-beneficial-ownership",
"acquisition-of-beneficial-ownership",
query,
)
.with_metadata(US_ONLY)
}
impl Client {
pub async fn latest_insider_trades(
&self,
query: LatestInsiderTradesQuery,
) -> Result<Vec<InsiderTrade>> {
self.execute(&latest_insider_trades(query)).await
}
pub async fn search_insider_trades(
&self,
query: InsiderTradesSearchQuery,
) -> Result<Vec<InsiderTrade>> {
self.execute(&search_insider_trades(query)).await
}
pub async fn search_insider_reporting_names(
&self,
query: impl Into<InsiderReportingNameSearchQuery>,
) -> Result<Vec<InsiderReportingName>> {
self.execute(&search_insider_reporting_names(query.into()))
.await
}
pub async fn insider_transaction_types(&self) -> Result<Vec<InsiderTransactionType>> {
self.execute(&insider_transaction_types()).await
}
pub async fn insider_trade_statistics(
&self,
query: impl Into<InsiderTradeStatisticsQuery>,
) -> Result<Vec<InsiderTradeStatistics>> {
self.execute(&insider_trade_statistics(query.into())).await
}
pub async fn beneficial_ownership_acquisitions(
&self,
query: impl Into<BeneficialOwnershipAcquisitionsQuery>,
) -> Result<Vec<BeneficialOwnershipAcquisition>> {
self.execute(&beneficial_ownership_acquisitions(query.into()))
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
fn encoded(query: &impl QueryParameters) -> Vec<(String, String)> {
let mut pairs = Vec::new();
let mut visitor = |name: &str, value: &str| {
pairs.push((name.to_owned(), value.to_owned()));
};
query.encode(&mut QueryEncoder::new(&mut visitor));
pairs
}
#[test]
fn latest_query_omits_all_filters_and_preserves_full_domains() {
assert!(encoded(&LatestInsiderTradesQuery::new()).is_empty());
let query = LatestInsiderTradesQuery::new()
.with_date(Date::parse("2026-01-27").unwrap())
.with_page(Page(0))
.with_limit(Limit(u32::MAX));
assert_eq!(
encoded(&query),
[
("date".into(), "2026-01-27".into()),
("page".into(), "0".into()),
("limit".into(), u32::MAX.to_string()),
]
);
}
#[test]
fn search_query_emits_exact_keys_in_documented_order() {
assert!(encoded(&InsiderTradesSearchQuery::new()).is_empty());
let query = InsiderTradesSearchQuery::new()
.with_symbol(Ticker::new("AAPL").unwrap())
.with_page(Page(u32::MAX))
.with_limit(Limit(0))
.with_reporting_cik(Cik::new("0001496686").unwrap())
.with_company_cik(Cik::new("0000320193").unwrap())
.with_transaction_type(TransactionTypeCode::new("S-Sale / future").unwrap());
assert_eq!(
encoded(&query),
[
("symbol".into(), "AAPL".into()),
("page".into(), u32::MAX.to_string()),
("limit".into(), "0".into()),
("reportingCik".into(), "0001496686".into()),
("companyCik".into(), "0000320193".into()),
("transactionType".into(), "S-Sale / future".into()),
]
);
}
#[test]
fn required_queries_and_unit_taxonomy_query_have_exact_shapes() {
let name =
InsiderReportingNameSearchQuery::new(SearchTerm::new("Zuckerberg, Mark").unwrap());
assert_eq!(encoded(&name), [("name".into(), "Zuckerberg, Mark".into())]);
let statistics = InsiderTradeStatisticsQuery::new(Ticker::new("AAPL").unwrap());
assert_eq!(encoded(&statistics), [("symbol".into(), "AAPL".into())]);
let ownership =
BeneficialOwnershipAcquisitionsQuery::new(Ticker::new("000001.SZ").unwrap())
.with_limit(Limit(u32::MAX));
assert_eq!(
encoded(&ownership),
[
("symbol".into(), "000001.SZ".into()),
("limit".into(), u32::MAX.to_string()),
]
);
assert!(encoded(&()).is_empty());
}
}