EDINET Daily Filings Monitor — Spec & Starter Code
A Mac Mini–friendly puller for Japan FSA EDINET disclosures. Default focus: activist 5%-rule filings, tender offers, and material events — the highest-signal flow for active equity trading.
This page is the brief you can hand to Claude Code. Two viable paths:
- Path A (recommended): EDINET DB wrapper — works today, no FSA SMS verification, pre-parsed XBRL
- Path B: FSA direct API — only if you can sign up; SMS gateway has been intermittently down
Why Path A wins
The FSA's official EDINET API requires SMS verification at signup, and their SMS gateway has been throwing "Messages are currently unavailable" for foreign phone numbers — server-side outage, no user-side fix.
EDINET DB is a third-party wrapper (independent project, not FSA-affiliated) that:
- Proxies the official FSA API
- Pre-parses XBRL for 5%-rule filings → returns
holding_ratio,holding_ratio_change,purpose_category,est_holding_value,est_acquisition_priceas structured fields - Normalizes filer aliases via
parent_group_id(Dalton LLC + Dalton Inc →dalton; Reno + シティインデックスイレブンス → Murakami group) - Unifies EDINET filings + TDnet earnings/buybacks/TOB into one
/eventsfeed, refreshed every 15 min - Free tier: 100 calls/day. Pro: 1,000/day. Auth via
X-API-Keyheader. Email-only signup.
This skips the entire XBRL parsing roadmap that was item 4.1 on the original extension queue. They've already done it.
Path A: EDINET DB wrapper
Endpoints that matter
Base URL: https://edinetdb.jp/v1 (note .jp not .com for the API)
Auth: X-API-Key: {your-key} header
| Endpoint | Use case |
|---|---|
/events?since=YYYY-MM-DD&until=YYYY-MM-DD&event_type=... |
Daily monitor — unified EDINET + TDnet feed |
/shareholders/activists?category=activist |
Snapshot: ~918 current activist positions across ~417 filers |
/shareholders/history?filer=Oasis |
Track one activist's full position history with Δratio per filing |
/shareholders/history?issuer=... |
All 5%+ filings on one company over time |
/companies/{edinet_code}/shareholders |
Latest 5%+ holders for a company |
/shareholders/search?q=BlackRock |
Reverse lookup — what does this filer hold? |
Event types in /events: large_holding_report, activist_filing, tender_offer, mbo, merger, buyback, earnings_summary, earnings_revision — 25 total. Coverage: 2025-04-28 onward.
Coverage caveats: Large-holding reports go back to March 2021 (EDINET API v2 limit). ~71,000 filings / ~4,330 companies indexed.
Python script (Path A)
#!/usr/bin/env python3
"""
EDINET DB — Daily Activist & Material-Event Monitor
====================================================
Uses the EDINET DB wrapper (https://edinetdb.com), which proxies the FSA
official API and adds: filer normalization, purpose-category tagging,
estimated holding values, and a unified events feed (EDINET + TDnet).
Setup
-----
1. Get a free API key: https://edinetdb.com/developers (email-only)
2. export EDINETDB_API_KEY='your-key'
3. python edinet_db_monitor.py --help
Common usage
------------
# Yesterday's high-severity events (activist filings, TOB, MBO, buybacks)
python edinet_db_monitor.py --days 1
# Last 7 days, large-holding reports only
python edinet_db_monitor.py --days 7 --event-types large_holding_report
# Watchlist (4-digit JP tickers)
python edinet_db_monitor.py --days 7 --watchlist 6857,6920,7203
# Snapshot: all current activist positions
python edinet_db_monitor.py --activists
# Specific filer's history (full activist track record)
python edinet_db_monitor.py --filer "Oasis Management"
# JSON for downstream
python edinet_db_monitor.py --days 1 --json
Standard library + requests only. Python 3.10+.
"""
from __future__ import annotations
import argparse
import json
import os
import sys
from dataclasses import dataclass, asdict, field
from datetime import date, timedelta
from typing import Any
import requests
API_BASE = "https://edinetdb.jp/v1"
TIMEOUT = 30
# Event types most relevant for active trading (subset of EDINET DB's 25)
HIGH_SIGNAL_EVENTS = [
"large_holding_report", # 5%-rule filings (initial + change)
"activist_filing", # purpose = engagement / important suggestion
"tender_offer",
"mbo",
"merger",
"buyback",
"earnings_revision", # upward/downward guidance revisions
]
# -------------------------- API client --------------------------
class EdinetDBClient:
def __init__(self, api_key: str | None):
self.api_key = api_key
self.session = requests.Session()
if api_key:
self.session.headers.update({"X-API-Key": api_key})
def _get(self, path: str, params: dict | None = None) -> dict:
url = f"{API_BASE}{path}"
resp = self.session.get(url, params=params or {}, timeout=TIMEOUT)
if resp.status_code == 401:
sys.exit("ERROR: missing/invalid EDINETDB_API_KEY (X-API-Key header).")
if resp.status_code == 429:
sys.exit("ERROR: daily rate limit hit. Free tier = 100/day. Wait or upgrade.")
resp.raise_for_status()
return resp.json()
@staticmethod
def _unwrap(payload: Any) -> list[dict]:
"""EDINET DB wraps lists under varying keys: data / events / results / etc."""
if isinstance(payload, list):
return payload
if isinstance(payload, dict):
for key in ("data", "events", "results", "activists", "history",
"shareholders", "holders", "items"):
v = payload.get(key)
if isinstance(v, list):
return v
return []
def events(
self,
since: date,
until: date,
event_types: list[str] | None = None,
sec_code: str | None = None,
severity: list[str] | None = None,
limit: int = 1000,
) -> list[dict]:
params: dict[str, Any] = {
"since": since.isoformat(),
"until": until.isoformat(),
"limit": min(limit, 1000),
}
if event_types:
params["event_type"] = ",".join(event_types)
if sec_code:
params["sec_code"] = sec_code
if severity:
params["severity"] = ",".join(severity)
return self._unwrap(self._get("/events", params))
def activists(self, category: str = "activist", limit: int = 500) -> list[dict]:
return self._unwrap(self._get("/shareholders/activists",
{"category": category, "limit": limit}))
def shareholder_history(self, filer: str | None = None, issuer: str | None = None) -> list[dict]:
params: dict[str, Any] = {}
if filer:
params["filer"] = filer
if issuer:
params["issuer"] = issuer
if not params:
sys.exit("ERROR: --filer or --issuer required for history pull.")
return self._unwrap(self._get("/shareholders/history", params))
def company_shareholders(self, edinet_code: str) -> list[dict]:
return self._unwrap(self._get(f"/companies/{edinet_code}/shareholders", {}))
# -------------------------- Output --------------------------
def fmt_pct(v: Any) -> str:
try:
return f"{float(v):+.2f}%"
except (TypeError, ValueError):
return "—"
def fmt_yen(v: Any) -> str:
try:
v = float(v)
except (TypeError, ValueError):
return "—"
if abs(v) >= 1e12:
return f"¥{v/1e12:.2f}T"
if abs(v) >= 1e9:
return f"¥{v/1e9:.2f}B"
if abs(v) >= 1e6:
return f"¥{v/1e6:.1f}M"
return f"¥{v:.0f}"
def render_events_md(events: list[dict]) -> str:
if not events:
return "_No matching events._\n"
by_type: dict[str, list[dict]] = {}
for e in events:
by_type.setdefault(e.get("event_type", "other"), []).append(e)
lines = [f"# EDINET DB — {len(events)} events\n"]
for et, items in sorted(by_type.items()):
lines.append(f"\n## {et} ({len(items)})\n")
for e in items:
ts = e.get("event_time") or e.get("disclosure_time") or e.get("submitted_at", "")
sec = e.get("sec_code") or "—"
name = e.get("filer_name") or e.get("company_name") or e.get("issuer_name") or ""
title = e.get("title") or e.get("description") or e.get("event_subtype") or ""
sev = e.get("severity") or ""
sev_tag = f"[{sev}]" if sev else ""
line = f"- `{ts}` [{sec}] **{name}** {sev_tag} — {title}"
if "holding_ratio" in e or "holding_ratio_change" in e:
line += f" ratio={fmt_pct(e.get('holding_ratio'))} Δ={fmt_pct(e.get('holding_ratio_change'))}"
if "purpose_category" in e:
line += f" purpose={e['purpose_category']}"
if "est_holding_value" in e:
line += f" value={fmt_yen(e['est_holding_value'])}"
lines.append(line)
return "\n".join(lines) + "\n"
def render_activists_md(positions: list[dict]) -> str:
if not positions:
return "_No activist positions._\n"
by_group: dict[str, list[dict]] = {}
for p in positions:
g = p.get("parent_group_id") or p.get("filer_name") or "—"
by_group.setdefault(g, []).append(p)
lines = [f"# Active activist positions — {len(positions)} positions across {len(by_group)} groups\n"]
for group, items in sorted(by_group.items(), key=lambda x: -len(x[1])):
total_value = sum(float(p.get("est_holding_value") or 0) for p in items)
lines.append(f"\n## {group} — {len(items)} positions, total {fmt_yen(total_value)}\n")
for p in sorted(items, key=lambda x: -float(x.get("est_holding_value") or 0)):
sec = p.get("sec_code") or "—"
issuer = p.get("issuer_name") or ""
ratio = fmt_pct(p.get("holding_ratio"))
value = fmt_yen(p.get("est_holding_value"))
acq = fmt_yen(p.get("est_acquisition_price"))
last = p.get("latest_filing_date") or ""
lines.append(f"- [{sec}] **{issuer}** — {ratio} value={value} cost~{acq} last={last}")
return "\n".join(lines) + "\n"
def render_history_md(history: list[dict]) -> str:
if not history:
return "_No history._\n"
lines = [f"# Shareholding history — {len(history)} filings\n"]
for h in sorted(history, key=lambda x: x.get("filing_date", ""), reverse=True):
date_s = h.get("filing_date", "")
filer = h.get("filer_name", "")
issuer = h.get("issuer_name", "")
sec = h.get("sec_code", "—")
ratio = fmt_pct(h.get("holding_ratio"))
delta = fmt_pct(h.get("holding_ratio_change"))
purpose = h.get("purpose_category", "")
value = fmt_yen(h.get("est_holding_value"))
lines.append(
f"- `{date_s}` [{sec}] **{filer}** → {issuer}: ratio={ratio} Δ={delta} value={value} purpose={purpose}"
)
return "\n".join(lines) + "\n"
# -------------------------- CLI --------------------------
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="EDINET DB activist & material-event monitor")
p.add_argument("--days", type=int, default=1,
help="Days back to scan for events (default 1)")
p.add_argument("--end-date", type=str, default=None,
help="End date YYYY-MM-DD (default = today)")
p.add_argument("--event-types", type=str, default=None,
help=f"Comma-separated. Default: {','.join(HIGH_SIGNAL_EVENTS)}")
p.add_argument("--severity", type=str, default=None,
help="Comma-separated: critical,high,medium,low")
p.add_argument("--watchlist", type=str, default=None,
help="Comma-separated 4-digit sec codes")
p.add_argument("--activists", action="store_true",
help="Snapshot mode: show all current activist positions")
p.add_argument("--category", type=str, default="activist",
help="Activist filter category (with --activists)")
p.add_argument("--filer", type=str, default=None,
help="History mode: pull all filings by this filer name")
p.add_argument("--issuer", type=str, default=None,
help="History mode: pull all 5%%+ filings on this issuer")
p.add_argument("--company", type=str, default=None,
help="EDINET code (e.g. E02144) — show all 5%%+ holders for this company")
p.add_argument("--json", action="store_true", help="Emit JSON instead of Markdown")
return p.parse_args()
def main() -> int:
args = parse_args()
api_key = os.environ.get("EDINETDB_API_KEY", "").strip()
if not api_key:
print("WARNING: EDINETDB_API_KEY not set. Anonymous access is 100/day.", file=sys.stderr)
client = EdinetDBClient(api_key or None)
if args.activists:
positions = client.activists(category=args.category, limit=500)
out = json.dumps(positions, ensure_ascii=False, indent=2) if args.json else render_activists_md(positions)
print(out)
return 0
if args.filer or args.issuer:
history = client.shareholder_history(filer=args.filer, issuer=args.issuer)
out = json.dumps(history, ensure_ascii=False, indent=2) if args.json else render_history_md(history)
print(out)
return 0
if args.company:
holders = client.company_shareholders(args.company)
out = json.dumps(holders, ensure_ascii=False, indent=2) if args.json else render_history_md(holders)
print(out)
return 0
# Default: events feed
end = date.fromisoformat(args.end_date) if args.end_date else date.today()
since = end - timedelta(days=args.days)
event_types = args.event_types.split(",") if args.event_types else HIGH_SIGNAL_EVENTS
severity = args.severity.split(",") if args.severity else None
if args.watchlist:
watchlist = {w.strip() for w in args.watchlist.split(",") if w.strip()}
all_events: list[dict] = []
for sec in watchlist:
evs = client.events(since=since, until=end, event_types=event_types, sec_code=sec, severity=severity)
all_events.extend(evs)
events = all_events
else:
events = client.events(since=since, until=end, event_types=event_types, severity=severity)
events.sort(key=lambda e: e.get("event_time") or e.get("submitted_at") or "", reverse=True)
out = json.dumps(events, ensure_ascii=False, indent=2) if args.json else render_events_md(events)
print(out)
print(f"\n# {len(events)} events range={since}..{end} types={event_types}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
Quick sanity check
The public search endpoint (no auth needed) confirms connectivity:
curl -s "https://edinetdb.jp/v1/search?q=Toyota" | jq '.data[:2]'
Should return Toyota Motor (E02144) and Toyota Boshoku (E00540).
Telegram hook (matches OpenClaw setup)
Drop in after the final print(out) line in main():
def telegram_alert(text: str) -> None:
import os, requests
token = os.environ.get("TELEGRAM_BOT_TOKEN")
chat_id = os.environ.get("TELEGRAM_CHAT_ID")
if not (token and chat_id):
return
for chunk in [text[i:i+4000] for i in range(0, len(text), 4000)]:
requests.post(
f"https://api.telegram.org/bot{token}/sendMessage",
json={"chat_id": chat_id, "text": chunk, "parse_mode": "Markdown",
"disable_web_page_preview": True},
timeout=10,
)
# After the final print:
if events and not args.json:
telegram_alert(out)
Cron on Mac Mini
# Every weekday at 19:00 JST after most filings settle
0 19 * * 1-5 cd ~/edinet && EDINETDB_API_KEY=xxxxx /usr/bin/python3 edinet_db_monitor.py --days 1 >> logs/$(date +\%Y\%m).md 2>&1
Bonus: their MCP server
EDINET DB ships an MCP server. If you'd rather skip the script entirely for interactive use, connect it directly to Claude Code or Claude Desktop — see https://edinetdb.com/docs/mcp-guide. The Python script is still useful for cron-driven Telegram alerts, but the MCP path is faster for ad hoc digging.
Path B: FSA direct API (only if you can sign up)
Use this only if you successfully get past SMS verification, OR if EDINET DB's coverage gap (March 2021 onwards) is a problem and you need 2015–2020 data.
Sign-up gotchas
- Popup/redirect blocker. Add
https://api.edinet-fsa.go.jpto allowed pop-ups in Chrome. - Phone format. Country code
+66 Thailand, number without leading 0.[PHONE]→81234-5678. - Email confirmation expires in ~30 min. Don't walk away mid-signup.
- SMS gateway outages. Error message "Messages are currently unavailable" — server-side. Try again later or fall back to Path A.
API model
Base URL: https://api.edinet-fsa.go.jp/api/v2
Auth: Subscription-Key=KEY (query param)
Rate limit: 3-second floor between calls.
GET /documents.json?date=YYYY-MM-DD&type=2&Subscription-Key={KEY}
GET /documents/{docID}?type={1|2|4|5}&Subscription-Key={KEY}
Document types worth filtering:
| Code | Meaning |
|---|---|
| 340 | 大量保有報告書 (initial 5%+) |
| 350 | 変更報告書 (change in 5%+ holding) |
| 360 | Amended large shareholding/change |
| 240 | TOB |
| 290 | TOB Opinion Statement |
| 180 | 臨時報告書 (extraordinary report) |
| 220 | Treasury buyback status |
| 120 | 有価証券報告書 (annual report) |
Python script (Path B)
#!/usr/bin/env python3
"""
FSA direct EDINET monitor — fallback path.
Requires API key from https://api.edinet-fsa.go.jp/api/auth/index.aspx?mode=1
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from dataclasses import dataclass, asdict
from datetime import date, datetime, timedelta
from pathlib import Path
from typing import Iterable
import requests
API_BASE = "https://api.edinet-fsa.go.jp/api/v2"
RATE_LIMIT_SLEEP = 3.0
MAX_RETRIES = 5
RETRY_BACKOFF = 10.0
DOC_TYPES = {
"120": "Annual Securities Report",
"130": "Amended Annual Securities Report",
"140": "Quarterly Report",
"150": "Amended Quarterly Report",
"160": "Semi-Annual Report",
"170": "Amended Semi-Annual Report",
"180": "Extraordinary Report",
"190": "Amended Extraordinary Report",
"220": "Treasury Stock Buyback Status",
"230": "Amended Treasury Stock Buyback Status",
"235": "Internal Control Report",
"240": "Tender Offer Statement (TOB)",
"250": "Amended Tender Offer Statement",
"260": "Tender Offer Withdrawal",
"270": "Tender Offer Report",
"290": "TOB Opinion Statement",
"340": "Large Shareholding Report (initial)",
"350": "Change Report (5%+ holding change)",
"360": "Amended Large Shareholding / Change",
}
DEFAULT_TYPES = {"340", "350", "360", "240", "250", "270", "290", "180"}
class EdinetClient:
def __init__(self, api_key: str, sleep: float = RATE_LIMIT_SLEEP):
if not api_key:
sys.exit("ERROR: EDINET_API_KEY is not set.")
self.api_key = api_key
self.sleep = sleep
self.session = requests.Session()
self._last_call = 0.0
def _throttle(self) -> None:
elapsed = time.time() - self._last_call
if elapsed < self.sleep:
time.sleep(self.sleep - elapsed)
self._last_call = time.time()
def _get(self, url: str, params: dict, *, stream: bool = False):
params = {**params, "Subscription-Key": self.api_key}
for attempt in range(1, MAX_RETRIES + 1):
self._throttle()
try:
resp = self.session.get(url, params=params, stream=stream, timeout=30)
except requests.RequestException as e:
if attempt == MAX_RETRIES:
raise
print(f" ! request error ({e}); retry {attempt}/{MAX_RETRIES}", file=sys.stderr)
time.sleep(RETRY_BACKOFF)
continue
if resp.status_code == 403 and attempt < MAX_RETRIES:
print(f" ! 403 from EDINET; retry {attempt}/{MAX_RETRIES}", file=sys.stderr)
time.sleep(RETRY_BACKOFF)
continue
resp.raise_for_status()
return resp
raise RuntimeError("EDINET API: exhausted retries")
def list_documents(self, on_date: date) -> list[dict]:
url = f"{API_BASE}/documents.json"
resp = self._get(url, {"date": on_date.isoformat(), "type": 2})
return resp.json().get("results", []) or []
def fetch_document(self, doc_id: str, doc_type: int = 2) -> bytes:
url = f"{API_BASE}/documents/{doc_id}"
resp = self._get(url, {"type": doc_type}, stream=True)
return resp.content
@dataclass
class Filing:
doc_id: str
submitted_at: str
doc_type_code: str
doc_type_label: str
description: str
filer: str
issuer_edinet_code: str | None
sec_code: str | None
period_start: str | None
period_end: str | None
has_pdf: bool
has_english: bool
@property
def ticker4(self) -> str | None:
return self.sec_code[:4] if self.sec_code and len(self.sec_code) >= 4 else None
@property
def edinet_url(self) -> str:
return f"https://disclosure2.edinet-fsa.go.jp/WEEK0030.aspx?docId={self.doc_id}"
def parse_filing(d: dict) -> Filing:
code = d.get("docTypeCode") or ""
return Filing(
doc_id=d.get("docID") or "",
submitted_at=d.get("submitDateTime") or "",
doc_type_code=code,
doc_type_label=DOC_TYPES.get(code, "Other"),
description=d.get("docDescription") or "",
filer=d.get("filerName") or "",
issuer_edinet_code=d.get("issuerEdinetCode"),
sec_code=d.get("secCode"),
period_start=d.get("periodStart"),
period_end=d.get("periodEnd"),
has_pdf=d.get("pdfFlag") == "1",
has_english=d.get("englishDocFlag") == "1",
)
def filter_filings(filings, types, watchlist, filer_substring):
out = []
for f in filings:
if types and f.doc_type_code not in types:
continue
if watchlist:
if not (f.sec_code in watchlist or (f.ticker4 and f.ticker4 in watchlist)):
continue
if filer_substring and filer_substring.lower() not in f.filer.lower():
continue
out.append(f)
return out
def render_markdown(filings):
if not filings:
return "_No matching filings._\n"
by_type = {}
for f in filings:
by_type.setdefault(f.doc_type_label, []).append(f)
lines = [f"# EDINET filings — {len(filings)} hits\n"]
for label, items in sorted(by_type.items()):
lines.append(f"\n## {label} ({len(items)})\n")
for f in items:
tk = f"[{f.ticker4}]" if f.ticker4 else "[—]"
lines.append(f"- `{f.submitted_at}` {tk} **{f.filer}** — {f.description}")
lines.append(f" - {f.edinet_url}")
return "\n".join(lines) + "\n"
def main() -> int:
p = argparse.ArgumentParser()
p.add_argument("--days", type=int, default=1)
p.add_argument("--end-date", type=str, default=None)
p.add_argument("--types", type=str, default=None)
p.add_argument("--watchlist", type=str, default=None)
p.add_argument("--filer", type=str, default=None)
p.add_argument("--json", action="store_true")
args = p.parse_args()
api_key = os.environ.get("EDINET_API_KEY", "").strip()
client = EdinetClient(api_key)
end = date.fromisoformat(args.end_date) if args.end_date else date.today()
days = [end - timedelta(days=i) for i in range(args.days)]
types = set(args.types.split(",")) if args.types else DEFAULT_TYPES
watchlist = set(args.watchlist.split(",")) if args.watchlist else None
all_filings = []
for d in days:
if d.weekday() >= 5:
continue
print(f" · scanning {d.isoformat()}…", file=sys.stderr)
try:
raw = client.list_documents(d)
except requests.HTTPError as e:
print(f" ! {e}", file=sys.stderr)
continue
all_filings.extend(parse_filing(r) for r in raw if r.get("docID"))
filtered = filter_filings(all_filings, types, watchlist, args.filer)
filtered.sort(key=lambda x: x.submitted_at, reverse=True)
out = json.dumps([asdict(f) for f in filtered], ensure_ascii=False, indent=2) if args.json else render_markdown(filtered)
print(out)
return 0
if __name__ == "__main__":
sys.exit(main())
Next-step extensions (priority order)
For Claude Code to work through. Builds on Path A.
1. SQLite persistence + dedup
Cron will re-fetch overlapping days. Add a SQLite store keyed on a stable event ID:
- Schema:
events(event_id PK, event_time, event_type, sec_code, filer_group, raw_json, alerted_at) - On each run: insert-or-ignore, only push to Telegram for rows where
alerted_at IS NULL - Lets you backfill historical activist data once and stop re-pulling
2. Watchlist driven by live portfolio
Pull tickers from wherever you keep your live book and pipe into --watchlist. Then alerts only fire for names you actually trade.
3. Activist signal backtest
Walk-forward eval: "buy on filer X entry, hold N days, exit on Y." Sharpe × hit-rate on Murakami / Oasis / Effissimo / Dalton entries 2021–2026. Same evaluation framework as the autoresearch system. Probably the single most valuable downstream use.
EDINET DB's parent_group_id already groups the alias variants, and est_acquisition_price gives an entry price proxy — both pre-built. The work is just: cross-reference filing dates against price data (J-Quants free tier for JP daily bars), compute forward returns, aggregate by filer group.
4. Cross-reference EDINET filings with same-day TDnet announcements
The /events endpoint already merges these. But for activist plays specifically, you want: did a 5%+ filing on company X arrive within N days of company X disclosing a buyback / earnings revision / governance change? That co-incidence is the highest-confidence read on intent.
5. Filer outreach pattern detection
Some activists file series (Murakami group hits ~20 names per year). Detect: "filer X just initiated 3 new 5% positions in the last 5 trading days" → potential start of a campaign cycle worth front-running.
6. FSA direct fallback for pre-2021 history
If FSA SMS ever clears, run the Path B script as a one-off backfill 2015–2021, dump to your SQLite. EDINET DB picks up from there.
Things to know / gotchas
- EDINET DB coverage: large-holding reports March 2021+. For older history, you need Path B.
- secCode is 5 digits in raw EDINET responses (e.g.
72030); first 4 = tradeable ticker. EDINET DB uses both interchangeably. - Special reporting (特例報告). Institutional investors have a relaxed monthly reporting regime — their 5% filings can lag actual position changes by weeks. Treat institutional
350s as backward-looking; treat individual / fund340s as real-time. EDINET DB'spurpose_categorydistinguishesinstitutionalfromactivist. - Quarterly reports (140) abolished post-2024 — replaced by half-year reports + voluntary TDnet earnings disclosures.
- Rate limits: EDINET DB free tier 100/day, Pro 1,000/day. Cron once daily uses ~1-3 calls per run, very comfortable on free.
- MCP alternative: EDINET DB has an MCP server. If you want to skip Python entirely for ad-hoc work, connect it to Claude Code/Desktop and just ask in natural language.
No redactions applied.
Published via wiis. Originals are encrypted at rest; this page reflects the redacted version. report abuse.
Discussion
No comments yet.