#!/usr/bin/env python3 """Bootstrap the project-local HK IPO historical archive.""" from __future__ import annotations import csv import hashlib import sqlite3 from pathlib import Path ARCHIVE_AS_OF = "2026-06-15T06:15:00Z" DB_PATH = Path("data/hk_ipo.sqlite") SCHEMA_PATH = Path("schema/hk_ipo.schema.sql") SNAPSHOT_DIR = Path("data/snapshots") STALE_GAP_IDS = [ "06106_full_prospectus_classification_2026_06_15", ] IPO_MASTER = [ { "ticker": "06658", "company_name_en": "Liuliumei Co., Ltd.", "company_name_zh": "溜溜梅股份有限公司", "stock_short_name": "LIULIUMEI", "status": "listed", "listing_date": "2026-06-15", "application_start_date": "2026-06-05", "application_end_date": "2026-06-10", "allotment_results_expected_date": "2026-06-12", "industry_label": "Snack food / preserved fruit", "data_as_of": ARCHIVE_AS_OF, "notes": "Seeded from HKEXnews prospectus and allotment results.", }, { "ticker": "06675", "company_name_en": "SENASIC Electronics Technology Co., Ltd.", "company_name_zh": "琻捷電子科技(江蘇)股份有限公司", "stock_short_name": None, "status": "pending_listing", "listing_date": "2026-06-17", "application_start_date": "2026-06-09", "application_end_date": "2026-06-12", "allotment_results_expected_date": "2026-06-16", "industry_label": "Automotive wireless sensing SoC / semiconductors", "data_as_of": ARCHIVE_AS_OF, "notes": "Seeded from HKEXnews prospectus and global offering announcement; allotment results not yet archived.", }, { "ticker": "06106", "company_name_en": "Shanghai Seer Intelligent Technology Co., Ltd.", "company_name_zh": "上海仙工智能科技股份有限公司", "stock_short_name": None, "status": "open_for_subscription", "listing_date": "2026-06-24", "application_start_date": "2026-06-15", "application_end_date": "2026-06-18", "allotment_results_expected_date": "2026-06-23", "industry_label": "Industrial intelligent robots / robot controllers", "data_as_of": ARCHIVE_AS_OF, "notes": "Seeded from HKEXnews global offering announcement; full prospectus source classification needs follow-up.", }, ] OFFERING_TERMS = [ { "ticker": "06658", "source_id": "06658_prospectus_2026_06_05", "prospectus_date": "2026-06-05", "offer_price_hkd": 43.58, "board_lot": 100, "min_subscription_amount_hkd": 4401.96, "global_offer_shares": 11464100, "hk_offer_shares_initial": 1146500, "international_offer_shares_initial": 10317600, "public_offer_pct_initial": 0.10, "over_allotment_offer_shares": None, "offer_size_adjustment_offer_shares": None, "market_cap_hkd_m": 3434.59, "gross_proceeds_hkd_m": 499.6, "net_proceeds_hkd_m": 440.1, "issued_shares_upon_listing": 78811208, "data_as_of": ARCHIVE_AS_OF, }, { "ticker": "06675", "source_id": "06675_global_offering_announcement_2026_06_09", "prospectus_date": "2026-06-09", "offer_price_hkd": 18.36, "board_lot": 200, "min_subscription_amount_hkd": 3709.04, "global_offer_shares": 53407000, "hk_offer_shares_initial": 5340800, "international_offer_shares_initial": 48066200, "public_offer_pct_initial": 0.10, "over_allotment_offer_shares": 8011000, "offer_size_adjustment_offer_shares": None, "market_cap_hkd_m": 6959.2, "gross_proceeds_hkd_m": None, "net_proceeds_hkd_m": 906.7, "issued_shares_upon_listing": 379041820, "data_as_of": ARCHIVE_AS_OF, }, { "ticker": "06106", "source_id": "06106_prospectus_candidate_2026_06_15", "prospectus_date": "2026-06-15", "offer_price_hkd": 101.60, "board_lot": 50, "min_subscription_amount_hkd": 5131.24, "global_offer_shares": 10497300, "hk_offer_shares_initial": 524900, "international_offer_shares_initial": 9972400, "public_offer_pct_initial": 0.05, "over_allotment_offer_shares": 1574550, "offer_size_adjustment_offer_shares": 1574550, "market_cap_hkd_m": 11226.52568, "gross_proceeds_hkd_m": 1066.52568, "net_proceeds_hkd_m": 995.4, "issued_shares_upon_listing": 110497300, "data_as_of": ARCHIVE_AS_OF, }, ] IPO_DEMAND = [ { "demand_id": "06658_allotment_2026_06_12", "ticker": "06658", "source_id": "06658_allotment_results_2026_06_12", "stage_date": "2026-06-12", "valid_applications": 180507, "successful_applications": 11465, "public_oversubscription_times": 6586.73, "international_placees": 64, "international_oversubscription_times": 2.64, "final_hk_offer_shares": 1146500, "final_international_offer_shares": 10317600, "data_as_of": ARCHIVE_AS_OF, "notes": "Claw-back shown as N/A in the HKEXnews allotment results.", }, ] SOURCES = [ { "source_id": "06658_prospectus_2026_06_05", "ticker": "06658", "source_type": "prospectus", "title": "Liuliumei Co., Ltd. Prospectus", "local_path": "data/raw/06658/prospectus_2026-06-05.pdf", "url": "https://www1.hkexnews.hk/listedco/listconews/sehk/2026/0605/2026060500023.pdf", "source_date": "2026-06-05", "notes": "HKEXnews prospectus.", }, { "source_id": "06658_allotment_results_2026_06_12", "ticker": "06658", "source_type": "allotment_results", "title": "Liuliumei Co., Ltd. Announcement of Allotment Results", "local_path": "data/raw/06658/allotment_results_2026-06-12.pdf", "url": "https://www1.hkexnews.hk/listedco/listconews/sehk/2026/0612/2026061202100.pdf", "source_date": "2026-06-12", "notes": "HKEXnews allotment results.", }, { "source_id": "06675_prospectus_2026_06_09", "ticker": "06675", "source_type": "prospectus", "title": "SENASIC Electronics Technology Co., Ltd. Prospectus", "local_path": "data/raw/06675/prospectus_2026-06-09.pdf", "url": "https://www.hkexnews.hk/listedco/listconews/sehk/2026/0609/2026060900029.pdf", "source_date": "2026-06-09", "notes": "HKEXnews prospectus.", }, { "source_id": "06675_global_offering_announcement_2026_06_09", "ticker": "06675", "source_type": "global_offering_announcement", "title": "SENASIC Electronics Technology Co., Ltd. Global Offering Announcement", "local_path": "data/raw/06675/global_offering_announcement_2026-06-09.pdf", "url": "https://www.hkexnews.hk/listedco/listconews/sehk/2026/0609/2026060900009.pdf", "source_date": "2026-06-09", "notes": "HKEXnews global offering announcement.", }, { "source_id": "06106_prospectus_notice_2026_06_15", "ticker": "06106", "source_type": "prospectus_notice", "title": "Shanghai Seer Intelligent Technology Co., Ltd. Prospectus Notice", "local_path": "data/raw/06106/prospectus_notice_2026-06-15.pdf", "url": "https://www1.hkexnews.hk/listedco/listconews/sehk/2026/0615/2026061500011.pdf", "source_date": "2026-06-15", "notes": "HKEXnews announcement containing global offering terms and timetable.", }, { "source_id": "06106_prospectus_candidate_2026_06_15", "ticker": "06106", "source_type": "prospectus", "title": "Shanghai Seer Intelligent Technology Co., Ltd. Prospectus", "local_path": "data/raw/06106/prospectus_candidate_2026-06-15.pdf", "url": "https://www1.hkexnews.hk/listedco/listconews/sehk/2026/0615/2026061500013.pdf", "source_date": "2026-06-15", "notes": "HKEXnews prospectus; verified by text extraction as a 424-page GLOBAL OFFERING document.", }, ] DATA_GAPS = [ { "gap_id": "06675_allotment_results_pending_2026_06_15", "ticker": "06675", "stage": "T1_allotment", "field_name": "ipo_demand", "reason": "Allotment results were expected on 2026-06-16 and were not available in this seed archive.", "expected_resolution_date": "2026-06-16", "created_at": ARCHIVE_AS_OF, "notes": "Update after the HKEXnews allotment results announcement is published.", }, { "gap_id": "06106_allotment_results_pending_2026_06_15", "ticker": "06106", "stage": "T1_allotment", "field_name": "ipo_demand", "reason": "Allotment results were expected on 2026-06-23 and were not available in this seed archive.", "expected_resolution_date": "2026-06-23", "created_at": ARCHIVE_AS_OF, "notes": "Update after the HKEXnews allotment results announcement is published.", }, ] def repo_root() -> Path: return Path.cwd() def hash_file(relative_path: str) -> str: path = repo_root() / relative_path digest = hashlib.sha256() with path.open("rb") as handle: for chunk in iter(lambda: handle.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def ensure_relative_path(relative_path: str) -> None: path = Path(relative_path) if path.is_absolute() or relative_path.startswith("./") or "\\" in relative_path: raise ValueError(f"Path must be repo-relative POSIX style: {relative_path}") if not (repo_root() / relative_path).exists(): raise FileNotFoundError(relative_path) def upsert_rows(conn: sqlite3.Connection, table: str, rows: list[dict[str, object]]) -> None: if not rows: return columns = list(rows[0]) placeholders = ", ".join("?" for _ in columns) assignments = ", ".join(f"{column}=excluded.{column}" for column in columns) sql = ( f"INSERT INTO {table} ({', '.join(columns)}) VALUES ({placeholders}) " f"ON CONFLICT DO UPDATE SET {assignments}" ) conn.executemany(sql, ([row[column] for column in columns] for row in rows)) def export_snapshot(conn: sqlite3.Connection, table: str) -> None: SNAPSHOT_DIR.mkdir(parents=True, exist_ok=True) cursor = conn.execute(f"SELECT * FROM {table} ORDER BY 1") columns = [description[0] for description in cursor.description] with (SNAPSHOT_DIR / f"{table}.csv").open("w", newline="", encoding="utf-8") as handle: writer = csv.writer(handle, lineterminator="\n") writer.writerow(columns) writer.writerows(cursor.fetchall()) def main() -> None: DB_PATH.parent.mkdir(parents=True, exist_ok=True) with sqlite3.connect(DB_PATH) as conn: conn.executescript(SCHEMA_PATH.read_text(encoding="utf-8")) conn.executemany("DELETE FROM data_gaps WHERE gap_id = ?", [(gap_id,) for gap_id in STALE_GAP_IDS]) upsert_rows(conn, "ipo_master", IPO_MASTER) upsert_rows(conn, "offering_terms", OFFERING_TERMS) upsert_rows(conn, "ipo_demand", IPO_DEMAND) source_rows = [] for source in SOURCES: ensure_relative_path(source["local_path"]) source_rows.append( { **source, "path_base": "repo_root", "file_sha256": hash_file(source["local_path"]), "archived_at": ARCHIVE_AS_OF, } ) upsert_rows(conn, "source_refs", source_rows) upsert_rows(conn, "data_gaps", DATA_GAPS) for table in [ "ipo_master", "offering_terms", "ipo_demand", "price_performance", "new_listing_report_entries", "source_refs", "data_gaps", "sync_runs", "ticker_sync_state", "sync_tasks", ]: export_snapshot(conn, table) if __name__ == "__main__": main()