idx-cli/src/ownership/types.rs
Ciphercat 6671e22976 feat(ownership): complete ownership intelligence module
Sprints 0-8: Types, schema, KSEI parser, entity resolution, DB CRUD,
Bing API client, import pipeline, query commands, graph traversal,
changes diff, entity resolution CLI, FTS5 search.

- KSEI PDF parser (mutool stext + quick-xml, 99.2% accuracy)
- 7 query commands: ticker, entity, search, cross-holders, concentration, flow, releases
- Ownership graph with recursive CTE (ASCII tree + Graphviz DOT)
- Release diff/changes between KSEI snapshots
- Entity resolution CLI: unresolved, map, merge
- FTS5 trigram search on entity names
- Feature-gated under 'ownership' (default-on)
- 82 tests passing
2026-03-07 21:10:58 +00:00

451 lines
15 KiB
Rust

use chrono::NaiveDate;
use serde::{Deserialize, Serialize};
/// Which data source a holding originates from.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OwnershipSource {
/// KSEI shareholder registry source.
Ksei,
/// Bing institutional ownership source.
Bing,
}
/// Investor locality classification from KSEI.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Locality {
/// Local Indonesian investor.
Local,
/// Foreign investor.
Foreign,
}
/// Bing institutional flow signal.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FlowSignal {
/// Existing top holder.
Holder,
/// Net buyer for the period.
Buyer,
/// Net seller for the period.
Seller,
/// New position opened this period.
NewPosition,
/// Fully exited this period.
Exited,
}
/// Method used to resolve an entity alias to a canonical entity.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ResolutionMethod {
/// Exact string match after normalization.
Exact,
/// Rule-based normalization match.
Rule,
/// Fuzzy or similarity-based match.
Fuzzy,
/// Human-curated manual mapping.
Manual,
}
/// Graph node category used in ownership network output.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum GraphNodeType {
/// Canonical investor entity node.
Entity,
/// Listed issuer ticker node.
Ticker,
}
/// KSEI investor type code (for example: `CP`, `ID`, `IB`, `MF`, `SC`, `IS`, `OT`).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InvestorTypeCode(pub String);
/// Canonical resolved entity (investor or shareholder).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entity {
/// Internal entity identifier.
pub id: i64,
/// Canonical normalized display name.
pub canonical_name: String,
/// Optional coarse entity type (`fund`, `bank`, `conglomerate`, `govt`, `individual`).
pub entity_type: Option<String>,
/// Optional ISO-like country tag.
pub country: Option<String>,
}
/// A raw name variant mapped to a canonical entity.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityAlias {
/// Internal alias identifier.
pub id: i64,
/// Referenced canonical entity id.
pub entity_id: i64,
/// Raw alias text from source.
pub raw_name: String,
/// Source that produced this alias.
pub source: OwnershipSource,
/// Resolution confidence score in `0.0..=1.0`.
pub confidence: f64,
/// Matching method used for resolution.
pub method: ResolutionMethod,
}
/// Ticker (issuer) reference metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ticker {
/// Internal ticker identifier.
pub id: i64,
/// Exchange ticker code, for example `BBCA`.
pub code: String,
/// Optional long issuer name.
pub name: Option<String>,
/// Optional sector classification.
pub sector: Option<String>,
}
/// Single KSEI ownership row for one holder in one ticker and release.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KseiHolding {
/// Internal holding row identifier.
pub id: i64,
/// Referenced ticker id.
pub ticker_id: i64,
/// Referenced entity id, unresolved when `None`.
pub entity_id: Option<i64>,
/// Raw investor name exactly as imported.
pub raw_investor_name: String,
/// Optional KSEI investor type code.
pub investor_type: Option<InvestorTypeCode>,
/// Optional local/foreign classification.
pub locality: Option<Locality>,
/// Optional nationality text.
pub nationality: Option<String>,
/// Optional domicile text.
pub domicile: Option<String>,
/// Scripless holdings in shares.
pub holdings_scripless: i64,
/// Script holdings in shares.
pub holdings_scrip: i64,
/// Total shares held.
pub total_shares: i64,
/// Ownership percentage in basis points (`41.10% -> 4110`).
pub percentage_bps: i64,
/// Snapshot as-of date.
pub report_date: NaiveDate,
/// SHA-256 hash of source release for deduplication.
pub release_sha256: String,
}
/// Draft holding before entity resolution (without persisted row id and entity id).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KseiHoldingDraft {
/// Exchange ticker code, for example `BBCA`.
pub ticker_code: String,
/// Optional issuer name as provided by source row.
pub issuer_name: Option<String>,
/// Raw investor name exactly as imported.
pub raw_investor_name: String,
/// Optional KSEI investor type code.
pub investor_type: Option<InvestorTypeCode>,
/// Optional local/foreign classification.
pub locality: Option<Locality>,
/// Optional nationality text.
pub nationality: Option<String>,
/// Optional domicile text.
pub domicile: Option<String>,
/// Scripless holdings in shares.
pub holdings_scripless: i64,
/// Script holdings in shares.
pub holdings_scrip: i64,
/// Total shares held.
pub total_shares: i64,
/// Ownership percentage in basis points (`41.10% -> 4110`).
pub percentage_bps: i64,
/// Snapshot as-of date.
pub report_date: NaiveDate,
}
/// Single Bing institutional ownership row.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BingHolding {
/// Internal holding row identifier.
pub id: i64,
/// Referenced ticker id.
pub ticker_id: i64,
/// Referenced entity id, unresolved when `None`.
pub entity_id: Option<i64>,
/// Raw investor name exactly as imported.
pub raw_investor_name: String,
/// Optional Bing investor type text.
pub investor_type: Option<String>,
/// Shares currently held.
pub shares_held: Option<i64>,
/// Share delta over period (`+buy`, `-sell`).
pub shares_changed: Option<i64>,
/// Ownership percentage in basis points.
pub pct_ownership_bps: Option<i64>,
/// Position value in USD.
pub value_usd: Option<i64>,
/// Source report date.
pub report_date: NaiveDate,
/// Institutional flow signal for the row.
pub signal: FlowSignal,
/// Import timestamp (unix epoch seconds).
pub fetched_at: i64,
}
/// Raw row extracted from KSEI PDF before normalization.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KseiRawRow {
/// Date string as appears in PDF (for example `27-Feb-2026`).
pub date: String,
/// Share code string (for example `BBCA`).
pub share_code: String,
/// Issuer name as appears in source.
pub issuer_name: String,
/// Investor name as appears in source.
pub investor_name: String,
/// Investor type code string.
pub investor_type: String,
/// Local or foreign marker (`L` or `F`).
pub local_foreign: String,
/// Nationality text.
pub nationality: String,
/// Domicile text.
pub domicile: String,
/// Scripless holdings string in Indonesian locale format.
pub holdings_scripless: String,
/// Script holdings string in Indonesian locale format.
pub holdings_scrip: String,
/// Total holdings string in Indonesian locale format.
pub total_holding_shares: String,
/// Percentage string in Indonesian locale decimal format.
pub percentage: String,
}
/// Raw holder object returned by Bing ownership endpoints.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BingHolderRaw {
/// Investor name field from Bing payload.
#[serde(alias = "investorName", alias = "InvestorName")]
pub investor_name: Option<String>,
/// Investor type field from Bing payload.
#[serde(alias = "investorType", alias = "InvestorType")]
pub investor_type: Option<String>,
/// Shares held field from Bing payload.
#[serde(alias = "sharesHeld", alias = "SharesHeld")]
pub shares_held: Option<f64>,
/// Shares changed field from Bing payload.
#[serde(alias = "sharesChanged", alias = "SharesChanged")]
pub shares_changed: Option<f64>,
/// Percentage of shares outstanding field from Bing payload.
#[serde(
alias = "percentageOfSharesOutstanding",
alias = "PercentageOfSharesOutstanding"
)]
pub pct_outstanding: Option<f64>,
/// Position value field from Bing payload.
#[serde(alias = "value", alias = "Value")]
pub value: Option<f64>,
/// Report date field from Bing payload.
#[serde(alias = "reportDate", alias = "ReportDate")]
pub report_date: Option<String>,
}
/// Combined ownership view for a ticker (KSEI and Bing merged).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TickerOwnership {
/// Ticker metadata.
pub ticker: Ticker,
/// Latest KSEI as-of date.
pub ksei_as_of: Option<NaiveDate>,
/// Latest Bing reporting period label.
pub bing_as_of: Option<String>,
/// Combined holder rows for display.
pub holders: Vec<HolderRow>,
/// Concentration metrics calculated for the ticker.
pub concentration: ConcentrationMetrics,
/// Optional institutional flow breakdown.
pub flow: Option<InstitutionalFlow>,
}
/// Single row in a combined holders table.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HolderRow {
/// Ranking position in result set.
pub rank: usize,
/// Source dataset for the row.
pub source: OwnershipSource,
/// Canonical holder name or unresolved raw name.
pub name: String,
/// Optional referenced canonical entity id.
pub entity_id: Option<i64>,
/// Optional investor type label.
pub investor_type: Option<String>,
/// Optional local/foreign marker.
pub locality: Option<Locality>,
/// Held shares quantity.
pub shares: i64,
/// Ownership percentage in basis points.
pub percentage_bps: i64,
/// Optional flow signal (Bing rows only).
pub signal: Option<FlowSignal>,
}
/// Concentration metrics for ownership distribution of a ticker.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConcentrationMetrics {
/// Largest single holder percentage in basis points.
pub top1_bps: i64,
/// Sum of top 3 holder percentages in basis points.
pub top3_bps: i64,
/// Herfindahl-Hirschman index value.
pub hhi: i64,
/// Estimated free float in basis points (`10000 - known holders`).
pub free_float_bps: i64,
/// Number of KSEI holders above or equal to 1%.
pub holder_count: usize,
}
/// Institutional flow summary from Bing for one ticker and period.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstitutionalFlow {
/// Reporting period label.
pub period: String,
/// Top buyer rows.
pub top_buyers: Vec<HolderRow>,
/// Top seller rows.
pub top_sellers: Vec<HolderRow>,
/// New position rows.
pub new_positions: Vec<HolderRow>,
/// Exited position rows.
pub exited: Vec<HolderRow>,
}
/// Cross-holding summary for a single entity.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityHoldings {
/// Canonical entity metadata.
pub entity: Entity,
/// Number of distinct tickers held.
pub ticker_count: usize,
/// Per-ticker ownership rows.
pub holdings: Vec<EntityTickerRow>,
}
/// One ticker row within an entity holdings summary.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntityTickerRow {
/// Ticker metadata.
pub ticker: Ticker,
/// Source dataset for the holding.
pub source: OwnershipSource,
/// Shares held quantity.
pub shares: i64,
/// Ownership percentage in basis points.
pub percentage_bps: i64,
/// Report as-of date.
pub report_date: NaiveDate,
}
/// Cross-holder leaderboard row across multiple tickers.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrossHolderRow {
/// Canonical entity metadata.
pub entity: Entity,
/// Number of distinct tickers held.
pub ticker_count: usize,
/// Summed ownership basis points across tickers.
pub total_bps: i64,
}
/// Graph node used for ownership network visualization.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphNode {
/// Stable graph node id (`entity:42` or `ticker:BBCA`).
pub id: String,
/// Display label.
pub label: String,
/// Node category.
pub node_type: GraphNodeType,
}
/// Graph edge used for ownership network visualization.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphEdge {
/// Source node id.
pub from: String,
/// Target node id.
pub to: String,
/// Edge weight in basis points.
pub percentage_bps: i64,
/// Source dataset of the edge.
pub source: OwnershipSource,
}
/// Imported ownership release metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OwnershipRelease {
/// Internal release id.
pub id: i64,
/// Optional source URL where release was obtained.
pub source_url: Option<String>,
/// SHA-256 hash for release-level deduplication.
pub sha256: String,
/// Release as-of date.
pub as_of_date: NaiveDate,
/// Parsed row count imported.
pub row_count: usize,
/// Import timestamp (unix epoch seconds).
pub imported_at: i64,
}
/// Type of ownership change between two snapshots.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ChangeType {
/// Holder appears in `to` snapshot but not in `from`.
New,
/// Holder appears in `from` snapshot but not in `to`.
Exited,
/// Holder exists in both snapshots and percentage increased.
Increased,
/// Holder exists in both snapshots and percentage decreased.
Decreased,
}
/// One ownership change row for a ticker-holder pair.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChangeRow {
/// Exchange ticker code.
pub ticker_code: String,
/// Canonical or raw holder name.
pub entity_name: String,
/// Change classification.
pub change_type: ChangeType,
/// Old percentage in basis points (from snapshot).
pub old_bps: Option<i64>,
/// New percentage in basis points (to snapshot).
pub new_bps: Option<i64>,
/// Delta in basis points (`new - old`, missing treated as 0).
pub delta_bps: i64,
}
/// Row describing unresolved or low-confidence alias mappings.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UnresolvedRow {
/// Raw investor name captured from source data.
pub raw_name: String,
/// Source marker (`ksei` or `bing`).
pub source: String,
/// Ticker code where the unresolved alias appears.
pub ticker_code: String,
/// Current canonical entity name when available.
pub current_entity: Option<String>,
/// Resolution confidence score.
pub confidence: Option<f64>,
}