<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jake Cho</title>
    <description>The latest articles on DEV Community by Jake Cho (@jake_cho_bd1665179f4c45d5).</description>
    <link>https://dev.to/jake_cho_bd1665179f4c45d5</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4112148%2F65ec6817-b7a1-44c3-95ce-ba2d3b0276b2.png</url>
      <title>DEV Community: Jake Cho</title>
      <link>https://dev.to/jake_cho_bd1665179f4c45d5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jake_cho_bd1665179f4c45d5"/>
    <language>en</language>
    <item>
      <title>Joining Korean company data: DART codes, LEIs and the limits of name matching</title>
      <dc:creator>Jake Cho</dc:creator>
      <pubDate>Sat, 26 Sep 2026 04:42:37 +0000</pubDate>
      <link>https://dev.to/jake_cho_bd1665179f4c45d5/joining-korean-company-data-dart-codes-leis-and-the-limits-of-name-matching-267m</link>
      <guid>https://dev.to/jake_cho_bd1665179f4c45d5/joining-korean-company-data-dart-codes-leis-and-the-limits-of-name-matching-267m</guid>
      <description>&lt;p&gt;&lt;em&gt;By the FinBridge team at GRONOX Corp.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An identifier-matching note from the Korea Rosetta build, 22 September 2026.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A Korean filing, a trading system and a counterparty database can describe the same company using different keys. Before comparing their numbers, you need to establish that they refer to the same legal entity.&lt;/p&gt;

&lt;p&gt;OpenDART distinguishes its eight-character &lt;code&gt;corp_code&lt;/code&gt; from the six-character &lt;code&gt;stock_code&lt;/code&gt;. Its company profile supplies two more fields: a business registration number and a corporate registry number. A global dataset may instead identify the entity by its LEI. Those fields are useful bridges, but they are not interchangeable. &lt;a href="https://opendart.fss.or.kr/guide/detail.do?apiGrpCd=DS001&amp;amp;apiId=2019018" rel="noopener noreferrer"&gt;OpenDART's identifier specification&lt;/a&gt; documents the first two; &lt;a href="https://www.gleif.org/en/lei-data/access-and-use-lei-data" rel="noopener noreferrer"&gt;GLEIF&lt;/a&gt; provides LEI reference data.&lt;/p&gt;

&lt;p&gt;Korea Rosetta puts these identifiers alongside one another. It is an entity crosswalk, not a price feed or a table of individual securities.&lt;/p&gt;

&lt;h2&gt;
  
  
  A match rate needs a denominator
&lt;/h2&gt;

&lt;p&gt;The 22 September snapshot contains 3,991 DART records with stock codes. Of these, 2,753 have a KOSPI, KOSDAQ or KONEX classification in the snapshot. There are 337 populated LEIs, or 8.4% of all 3,991 rows.&lt;/p&gt;

&lt;p&gt;That is the registration-number match coverage of this build. It does &lt;strong&gt;not&lt;/strong&gt; establish the LEI adoption rate of all Korean companies, or prove that every unmatched company lacks an LEI. Source coverage, identifier availability and the join rules all affect the result.&lt;/p&gt;

&lt;p&gt;The downloadable sample tells a different-looking story: 118 of its 200 rows have an LEI, or 59%. The sample was selected from large companies by reported revenue. It is suitable for inspecting columns and trying a join; it is not a representative sample for estimating coverage.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Snapshot&lt;/th&gt;
&lt;th&gt;Rows&lt;/th&gt;
&lt;th&gt;Rows with LEI&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Full member dataset&lt;/td&gt;
&lt;td&gt;3,991&lt;/td&gt;
&lt;td&gt;337&lt;/td&gt;
&lt;td&gt;8.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public sample&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;118&lt;/td&gt;
&lt;td&gt;59.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These counts were recalculated from the CSV files on 26 September. The underlying snapshot remains dated 22 September.&lt;/p&gt;

&lt;h2&gt;
  
  
  Names are evidence, not keys
&lt;/h2&gt;

&lt;p&gt;Spaces, abbreviations and spelling variations complicate English-name comparisons. Removing punctuation can recover a candidate; it can also make distinct names look identical.&lt;/p&gt;

&lt;p&gt;The build therefore separates two outputs. &lt;code&gt;lei&lt;/code&gt; contains registration-number joins. &lt;code&gt;lei_candidate&lt;/code&gt; holds unconfirmed name-based candidates. A downstream system should not fill missing LEIs from the candidate column without review.&lt;/p&gt;

&lt;p&gt;A registration-number match is still something to audit. Preserve the matching method and source date. Check the registration authority and investigate duplicate identifiers or disagreements between sources before accepting a join. The current CSV has no repeated populated LEI, but that check alone does not prove every match correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep codes as text
&lt;/h2&gt;

&lt;p&gt;An identifier is not a quantity. Converting a stock code to an integer removes leading zeroes. Keep the original strings when loading the CSV, and validate uniqueness before using a lookup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;korea_rosetta_sample.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8-sig&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;by_corp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;corp_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;by_corp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Duplicate corp_code: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;by_corp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;

&lt;span class="n"&gt;lei_by_corp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lei&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;by_corp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lei&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lei_match_method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bizr_no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jurir_no&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rows: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;by_corp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; registration-number LEI matches: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lei_by_corp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this sample, the output is &lt;code&gt;Rows: 200; registration-number LEI matches: 118&lt;/code&gt;. The lookup deliberately excludes name-based candidates. Use the DART &lt;code&gt;corp_code&lt;/code&gt; from a filing to look up its entity; if it is absent from this 200-row sample, that is a sample coverage gap, not proof that the issuer or its LEI does not exist.&lt;/p&gt;

&lt;p&gt;This preserves strings and detects duplicate entity keys. It does not turn the entity table into a security master: different share classes require a separate security-to-issuer mapping.&lt;/p&gt;

&lt;h2&gt;
  
  
  A snapshot cannot answer a historical question
&lt;/h2&gt;

&lt;p&gt;The file records a source snapshot. Its &lt;code&gt;FORMER&lt;/code&gt; label is derived from a stock code combined with the &lt;code&gt;OTHER&lt;/code&gt; market classification; it is not a verified delisting date. Do not treat it as a complete history of exchange membership.&lt;/p&gt;

&lt;p&gt;Historical research needs dated observations and evidence for changes. Renames, transfers and delistings should be retained as events rather than silently overwritten. We have started recording snapshots, but this release does not reconstruct earlier listing events or provide verified delisting dates. A company that is listed in today's snapshot should not automatically enter a universe for an earlier year.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/Jakechj/korea-rosetta" rel="noopener noreferrer"&gt;public repository&lt;/a&gt; contains the 200-company sample and the build script under CC BY 4.0. The full member dataset is offered separately through &lt;a href="https://www.gronox.kr/rosetta" rel="noopener noreferrer"&gt;Korea Rosetta&lt;/a&gt;; the sample's licence should not be assumed to apply to it. No ISIN column or filing text is included in this crosswalk.&lt;/p&gt;

</description>
      <category>python</category>
      <category>dataengineering</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Free financial data for AI agents: what it took to wire four regulators into one schema</title>
      <dc:creator>Jake Cho</dc:creator>
      <pubDate>Sun, 06 Sep 2026 10:30:34 +0000</pubDate>
      <link>https://dev.to/jake_cho_bd1665179f4c45d5/free-financial-data-for-ai-agents-what-it-took-to-wire-four-regulators-into-one-schema-1e0b</link>
      <guid>https://dev.to/jake_cho_bd1665179f4c45d5/free-financial-data-for-ai-agents-what-it-took-to-wire-four-regulators-into-one-schema-1e0b</guid>
      <description>&lt;p&gt;If you ask a language model "which free financial data APIs exist?", you get the same list every time: SEC EDGAR, FRED, a couple of vendor free tiers, and &lt;code&gt;yfinance&lt;/code&gt;. It is not wrong. It is just very American, and it stops at "here is a URL". This post is about what happens after the URL — what we learned building a small data service that pulls &lt;strong&gt;Korea, the United States, Japan and Taiwan&lt;/strong&gt; from their regulators every night, normalises the result into one schema, and hands it to Claude, ChatGPT or any HTTP client.&lt;/p&gt;

&lt;p&gt;The service is &lt;a href="https://www.gronox.kr" rel="noopener noreferrer"&gt;FinBridge&lt;/a&gt;. It has a free tier and we run it as a business, so read the last section with that in mind. Everything before it is just what we found.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The free, official sources actually exist — one per country
&lt;/h2&gt;

&lt;p&gt;The good news first. Every one of the four markets publishes its filings and financial statements for free, from the regulator or exchange itself, and lets you redistribute the data. That last part matters: most vendor free tiers are for personal use only, so you cannot build anything on them that serves other people.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Market&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Limits we live with&lt;/th&gt;
&lt;th&gt;Licence and the trap&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Korea&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://opendart.fss.or.kr/" rel="noopener noreferrer"&gt;OpenDART&lt;/a&gt; (Financial Supervisory Service)&lt;/td&gt;
&lt;td&gt;Free key&lt;/td&gt;
&lt;td&gt;20,000 calls/day&lt;/td&gt;
&lt;td&gt;Statements, filings, insider trades. Daily prices are &lt;strong&gt;not&lt;/strong&gt; here — they come from &lt;a href="https://www.data.go.kr/" rel="noopener noreferrer"&gt;data.go.kr&lt;/a&gt; (Financial Services Commission), published the next business day after 13:00 KST, so you are always one session behind.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;United States&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.sec.gov/search-filings/edgar-application-programming-interfaces" rel="noopener noreferrer"&gt;SEC EDGAR&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;None, but a &lt;code&gt;User-Agent&lt;/code&gt; with contact details is mandatory&lt;/td&gt;
&lt;td&gt;10 requests/second&lt;/td&gt;
&lt;td&gt;Public domain. The bulk XBRL "financial statement data sets" (DERA) are the only place you get &lt;em&gt;as-filed&lt;/em&gt; numbers — see §3.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Japan&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://disclosure2.edinet-fsa.go.jp/" rel="noopener noreferrer"&gt;EDINET&lt;/a&gt; (Financial Services Agency)&lt;/td&gt;
&lt;td&gt;Free key&lt;/td&gt;
&lt;td&gt;Generous&lt;/td&gt;
&lt;td&gt;PDL 1.0 — attribution &lt;strong&gt;and&lt;/strong&gt; a note that you processed the data are licence conditions. The archive is a &lt;strong&gt;10-year rolling window&lt;/strong&gt;: documents fall off the far end, so whatever you fetched is yours to keep, not theirs to re-serve. No prices at all.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Taiwan&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://openapi.twse.com.tw/" rel="noopener noreferrer"&gt;TWSE OpenAPI&lt;/a&gt; / TPEx OpenAPI&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;The exchange's own site tolerates about 0.15 requests/second before it starts serving challenge pages&lt;/td&gt;
&lt;td&gt;Open Government Data Licence v1 — attribution required. 143 endpoints on TWSE alone, and none of them is business-segment data.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Macro&lt;/td&gt;
&lt;td&gt;&lt;a href="https://fred.stlouisfed.org/docs/api/fred/" rel="noopener noreferrer"&gt;FRED&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Free key&lt;/td&gt;
&lt;td&gt;120 calls/minute&lt;/td&gt;
&lt;td&gt;Attribution in every response is a term of use. Some series (S&amp;amp;P 500, NASDAQ Composite) are third-party copyright even inside FRED — do not redistribute those.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two absences shaped the whole design. &lt;strong&gt;Japan has no redistributable daily prices&lt;/strong&gt;: EDINET publishes documents, and JPX quote data may not be re-served. &lt;strong&gt;Europe&lt;/strong&gt; (via ESEF, the single electronic reporting format) is the same — statements yes, prices no. So the honest product description is "filings and statements everywhere; prices where the licence allows", and we say exactly that on every relevant page.&lt;/p&gt;

&lt;p&gt;We keep the per-market details, including what you &lt;em&gt;cannot&lt;/em&gt; get free and why, in a set of guides: &lt;a href="https://www.gronox.kr/guides/free-financial-data/kr" rel="noopener noreferrer"&gt;Korea&lt;/a&gt; · &lt;a href="https://www.gronox.kr/guides/free-financial-data/us" rel="noopener noreferrer"&gt;US&lt;/a&gt; · &lt;a href="https://www.gronox.kr/guides/free-financial-data/jp" rel="noopener noreferrer"&gt;Japan&lt;/a&gt; · &lt;a href="https://www.gronox.kr/guides/free-financial-data/tw" rel="noopener noreferrer"&gt;Taiwan&lt;/a&gt;, with a &lt;a href="https://www.gronox.kr/guides" rel="noopener noreferrer"&gt;one-table round-up&lt;/a&gt; that also lists the vendor free tiers fairly.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. "Adjusted prices" means something different in every market
&lt;/h2&gt;

&lt;p&gt;The US teaches you that split adjustment comes from a corporate-actions feed. Korea has no such feed you may redistribute, so we tried the obvious thing — detect price jumps — and it was wrong in both directions: real 50% rallies looked like reverse splits, and a 1:2 split with a same-day move did not look like anything.&lt;/p&gt;

&lt;p&gt;What worked was &lt;strong&gt;measuring the split from the listed share count&lt;/strong&gt; (&lt;code&gt;lstgStCnt&lt;/code&gt; in the daily file) and using our own price jumps only as &lt;em&gt;candidates&lt;/em&gt;. Of the candidates, 359 turned out to be real corporate actions and 602 were genuine moves that we would have "corrected" away. The share count is the ground truth; the price is the symptom.&lt;/p&gt;

&lt;p&gt;The US had the mirror problem. Our first price source under-reported volume by a factor of 20 to 50 (a "mini" dataset that only counts some venues). Closes were right, so nothing looked broken until every liquidity screen quietly excluded Apple. Lesson: verify volume against a known name before you trust a feed, and when you fix it, &lt;strong&gt;null out the bad history rather than keep a number that is 3% of reality&lt;/strong&gt;. Our US volume simply starts on 2024-07-01, and the docs say so.&lt;/p&gt;

&lt;p&gt;ETFs are a third case: 1940-Act funds do not file the XBRL split tags that operating companies do, so ETF splits have to be inferred — and the only safe inference is an &lt;em&gt;integer&lt;/em&gt; ratio (2:1, 1:4) where the whole day, low to high, sits at the new level. Allow 4:5 in your candidate list and every 20% drawdown becomes a "split".&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Point-in-time is where free data quietly lies to you
&lt;/h2&gt;

&lt;p&gt;Every free statements source gives you the &lt;em&gt;latest&lt;/em&gt; number for a fiscal year. That is correct for a company page and wrong for research: if you backtest a value factor on restated financials, you are trading on numbers nobody had at the time.&lt;/p&gt;

&lt;p&gt;For the US, the fix is free but nobody uses it: SEC's DERA quarterly datasets carry every submission with its filing date, so you can rebuild what was knowable on any day. We loaded &lt;strong&gt;141,554 as-filed statement versions — one per submission, covering 59,780 company fiscal years for 5,092 companies (FY2007–2026)&lt;/strong&gt; — and our factor layer now reads those first. The price was a streaming ZIP reader — &lt;code&gt;num.txt&lt;/code&gt; is 600 MB per quarter — and a rule that one submission reporting three fiscal years yields three rows, each stamped with the date it became public.&lt;/p&gt;

&lt;p&gt;Korea does not have an equivalent archive, so we keep every DART correction as a new version instead of overwriting, and we tell the model when a number is the restated one. We also learned to record every &lt;em&gt;attempt&lt;/em&gt; to re-fetch a corrected filing: Korean "text corrections" do not change the receipt number, and a naive loop refetches them forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Segments, LEIs and the axis that double-counts
&lt;/h2&gt;

&lt;p&gt;Business-segment revenue was the feature people asked for most, and it comes from three different places: SEC's DERA (the &lt;code&gt;companyfacts&lt;/code&gt; API strips dimensional facts, so segments simply are not there), EDINET's XBRL CSVs for Japan, and — for Taiwan — nowhere. The TWSE and TPEx APIs have monthly revenue and industry-level income statements, not reportable segments; we went through all 368 endpoints to be sure.&lt;/p&gt;

&lt;p&gt;Traps that cost us a day each:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A fact with &lt;strong&gt;two axes&lt;/strong&gt; is a breakdown &lt;em&gt;inside&lt;/em&gt; a segment. Sum it with the one-axis facts and you double-count.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;Geographical&lt;/code&gt; axis is a country split, not a segment. Some companies, especially Japanese ones, define their reportable segments regionally anyway, so you cannot filter by axis name alone.&lt;/li&gt;
&lt;li&gt;US segment names are &lt;strong&gt;end markets&lt;/strong&gt;, not industries: AMD's "Data Center" is chips. A classifier that takes names literally will file it under real estate.&lt;/li&gt;
&lt;li&gt;In ESEF, the entity key is the &lt;strong&gt;LEI&lt;/strong&gt;, and it lives in the filing's &lt;code&gt;fxo_id&lt;/code&gt;, not in the entities table you would naturally join on. Filers also mislabel currencies — statements in EUR with EPS in DKK — so we cross-check against per-share values before trusting a unit.&lt;/li&gt;
&lt;li&gt;Half-year reports sit in the same index as annual ones. Group facts by &lt;strong&gt;period end&lt;/strong&gt;, not by year, or an August balance lands in the same row as a February income statement.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. One more that had nothing to do with finance
&lt;/h2&gt;

&lt;p&gt;For a day, Node could not reach a European filings host that &lt;code&gt;curl&lt;/code&gt; on the same machine reached fine. The machine had no global IPv6 route; Node received AAAA records and tried v6 first, the connection opened and TLS never completed. &lt;code&gt;curl&lt;/code&gt; goes through glibc and never saw the v6 address. If you ever meet "curl works, Node hangs", pin outbound to IPv4 before you debug anything else.&lt;/p&gt;

&lt;p&gt;Related: US tickers get reused. When a new listing inherits a ticker, symbol-mapped feeds prepend the previous owner's history to the new company. We now trim every US name to its first SEC filing date, and any jump detector requires the two prices to be within five sessions — gaps are gaps, not splits.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Serving it: MCP for agents, REST for everyone else
&lt;/h2&gt;

&lt;p&gt;Agents connect over the &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; — Claude, ChatGPT (developer mode), Cursor and Claude Code all speak it — so FinBridge is an MCP server first: one endpoint over statements, filings, insider trades, segments, prices, valuation with the five nearest peers attached, screeners and cost-aware backtests. Three habits made the model's answers better than the raw data:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every company carries a &lt;code&gt;page_url&lt;/code&gt;.&lt;/strong&gt; The model cites a page a person can open, with the filing behind each number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every answer carries &lt;code&gt;data_as_of&lt;/code&gt;&lt;/strong&gt; — last price session, snapshot date, latest reported period — because "the P/E is 14" without a date is not a fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attribution rides along.&lt;/strong&gt; TWSE/TPEx and EDINET make it a licence condition; we put the source line in the response, not just in a footer nobody reads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Not everything is an agent, and the lists people consult for "free financial APIs" list APIs, so the same data is also available as plain GET: &lt;code&gt;https://mcp.gronox.kr/api/v1/companies/{kr|us|jp|tw}/{symbol}&lt;/code&gt; plus &lt;code&gt;/financials&lt;/code&gt;, &lt;code&gt;/valuation&lt;/code&gt;, &lt;code&gt;/peers&lt;/code&gt; and &lt;code&gt;/prices&lt;/code&gt;, with an OpenAPI 3.1 description at &lt;code&gt;/api/v1/openapi.json&lt;/code&gt;. Same key, same quota, same depth as MCP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer smcp_..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://mcp.gronox.kr/api/v1/companies/kr/005930/financials
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. The commercial part, stated plainly
&lt;/h2&gt;

&lt;p&gt;The free plan is 200 calls a day, every tool, the last four fiscal years and 130 trading sessions, no card. Paid plans buy &lt;strong&gt;history depth&lt;/strong&gt;, not attempts — because the free plan is the same depth as the roughly 21,000 public company pages, and a free tier deeper than the public site would be unexplainable. Everything is a nightly snapshot of the primary source; nothing is real-time; Japan and Europe have no prices; and none of it is investment advice.&lt;/p&gt;

&lt;p&gt;If you only want the map of free sources, the &lt;a href="https://www.gronox.kr/guides" rel="noopener noreferrer"&gt;guides&lt;/a&gt; are free with no account and will stay that way — they are the notes we wish we had found when we started.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written from the project's engineering log, with drafting help from Claude. All figures and dates are from our own logs as of 2026-09-06.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>finance</category>
      <category>api</category>
      <category>ai</category>
      <category>data</category>
    </item>
  </channel>
</rss>
