mirror of
https://github.com/0xrsydn/idx-cli.git
synced 2026-09-21 05:54:18 +00:00
Merge pull request #21 from 0xrsydn/fix/ksei-spaced-headers
fix: handle spaced KSEI PDF headers + bump to v0.2.3
This commit is contained in:
commit
603c0e49c1
8 changed files with 80 additions and 19 deletions
|
|
@ -186,13 +186,7 @@ fn normalize_percentish(value: Option<f64>) -> Option<f64> {
|
|||
}
|
||||
|
||||
fn sanitize_current_ratio(value: Option<f64>) -> Option<f64> {
|
||||
value.and_then(|number| {
|
||||
if !number.is_finite() || number < 0.01 {
|
||||
None
|
||||
} else {
|
||||
Some(number)
|
||||
}
|
||||
})
|
||||
value.filter(|number| number.is_finite() && *number >= 0.01)
|
||||
}
|
||||
|
||||
fn round_price(value: f64) -> i64 {
|
||||
|
|
|
|||
|
|
@ -983,10 +983,10 @@ fn sort_screener_quotes(quotes: &mut [Quote], filter: &str) {
|
|||
});
|
||||
}
|
||||
"high-volume" => {
|
||||
quotes.sort_by(|a, b| b.volume.cmp(&a.volume));
|
||||
quotes.sort_by_key(|quote| std::cmp::Reverse(quote.volume));
|
||||
}
|
||||
"large-cap" => {
|
||||
quotes.sort_by(|a, b| b.market_cap.unwrap_or(0).cmp(&a.market_cap.unwrap_or(0)));
|
||||
quotes.sort_by_key(|quote| std::cmp::Reverse(quote.market_cap.unwrap_or(0)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -196,10 +196,7 @@ fn insert_ksei_holdings_rows(
|
|||
)
|
||||
.map_err(|e| IdxError::DatabaseError(e.to_string()));
|
||||
|
||||
match changed {
|
||||
Ok(n) => inserted += n,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
inserted += changed?;
|
||||
}
|
||||
|
||||
Ok(inserted)
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ pub fn format_graph_text(nodes: &[GraphNode], edges: &[GraphEdge]) -> String {
|
|||
out.push_str(&format!("nodes: {} edges: {}\n", nodes.len(), edges.len()));
|
||||
|
||||
for (ticker_id, mut rels) in ticker_to_entities {
|
||||
rels.sort_by(|a, b| b.percentage_bps.cmp(&a.percentage_bps));
|
||||
rels.sort_by_key(|edge| std::cmp::Reverse(edge.percentage_bps));
|
||||
let ticker_label = labels.get(ticker_id).map(|v| v.0).unwrap_or(ticker_id);
|
||||
out.push_str(&format!("\n{ticker_label} [TICKER]\n"));
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ const HEADER_LABELS: &[&str] = &[
|
|||
"ISSUERNAME",
|
||||
"INVESTORNAME",
|
||||
"INVESTORTYPE",
|
||||
"INVESTORCLASSIFICATION",
|
||||
"LOCALFOREIGN",
|
||||
"NATIONALITY",
|
||||
"DOMICILE",
|
||||
|
|
@ -31,14 +32,15 @@ const HOLDER_REGISTER_SCHEMA_MARKERS: &[&str] = &[
|
|||
"TEXT=\"SHARE_CODE\"",
|
||||
"TEXT=\"INVESTOR_NAME\"",
|
||||
"TEXT=\"INVESTOR_TYPE\"",
|
||||
"TEXT=\"INVESTOR_CLASSIFICATION\"",
|
||||
"TEXT=\"LOCAL_FOREIGN\"",
|
||||
"TEXT=\"TOTAL_HOLDING_SHARES\"",
|
||||
"TEXT=\"PERCENTAGE\"",
|
||||
];
|
||||
const ANNOUNCEMENT_WRAPPER_SCHEMA_MARKERS: &[&str] =
|
||||
&["TEXT=\"PENGUMUMAN\"", "PT BURSA EFEK INDONESIA (BEI)"];
|
||||
&["TEXT=\"PENGUMUMAN\"", "PT_BURSA_EFEK_INDONESIA_(BEI)"];
|
||||
const ABOVE_FIVE_SCHEMA_MARKERS: &[&str] =
|
||||
&["TEXT=\"INVS\"", "REKENING TAMPUNGAN KSEI", "CLOSED MEMBER-"];
|
||||
&["TEXT=\"INVS\"", "REKENING_TAMPUNGAN_KSEI", "CLOSED_MEMBER-"];
|
||||
const INVESTOR_TYPE_SCHEMA_MARKERS: &[&str] = &[
|
||||
"TEXT=\"STOCK_CODE\"",
|
||||
"TEXT=\"NUMBER_OF_SHARES\"",
|
||||
|
|
@ -103,7 +105,7 @@ pub fn extract_pdf_stext(path: &Path) -> Result<String, IdxError> {
|
|||
|
||||
/// Classify a PDF schema from mutool stext XML before the row parser runs.
|
||||
pub fn classify_stext_xml(xml: &str) -> OwnershipPdfSchema {
|
||||
let normalized = xml.to_ascii_uppercase();
|
||||
let normalized = normalize_stext_for_classification(xml);
|
||||
|
||||
if count_schema_markers(&normalized, HOLDER_REGISTER_SCHEMA_MARKERS) >= 5 {
|
||||
return OwnershipPdfSchema::HolderRegister;
|
||||
|
|
@ -121,6 +123,41 @@ pub fn classify_stext_xml(xml: &str) -> OwnershipPdfSchema {
|
|||
OwnershipPdfSchema::Unknown
|
||||
}
|
||||
|
||||
/// Normalize stext XML for schema classification.
|
||||
///
|
||||
/// Newer KSEI PDFs use spaces in header text (e.g. `SHARE CODE` instead of
|
||||
/// `SHARE_CODE`, `INVESTOR CLASSIFICATION` instead of `INVESTOR_TYPE`). The
|
||||
/// schema markers use the underscored form, so we replace spaces with
|
||||
/// underscores inside `text="..."` attribute values to match both layouts.
|
||||
fn normalize_stext_for_classification(xml: &str) -> String {
|
||||
let upper = xml.to_ascii_uppercase();
|
||||
let mut result = String::with_capacity(upper.len());
|
||||
let bytes = upper.as_bytes();
|
||||
let mut i = 0;
|
||||
|
||||
while i < bytes.len() {
|
||||
if bytes[i..].starts_with(b"TEXT=\"") {
|
||||
result.push_str("TEXT=\"");
|
||||
i += 6; // skip past TEXT="
|
||||
// Collect everything until the closing quote.
|
||||
while i < bytes.len() && bytes[i] != b'"' {
|
||||
let ch = bytes[i] as char;
|
||||
result.push(if ch == ' ' { '_' } else { ch });
|
||||
i += 1;
|
||||
}
|
||||
if i < bytes.len() {
|
||||
result.push('"');
|
||||
i += 1; // skip closing quote
|
||||
}
|
||||
} else {
|
||||
result.push(bytes[i] as char);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Parse a KSEI ownership PDF into raw rows.
|
||||
/// Shells out to `mutool` for XML extraction, classifies the schema,
|
||||
/// and only parses the supported holder-register layout.
|
||||
|
|
@ -684,6 +721,12 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_stext_xml_detects_spaced_holder_register_schema() {
|
||||
let xml = include_str!("../../tests/fixtures/ksei_above1_spaced_stext_excerpt.xml");
|
||||
assert_eq!(classify_stext_xml(xml), OwnershipPdfSchema::HolderRegister);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_stext_xml_detects_legacy_above5_schema() {
|
||||
let xml = include_str!("../../tests/fixtures/ksei_above5_stext_excerpt.xml");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue