A research-grade pipeline for detecting institutional + foreign accumulation in Korean equities (KIS / Korea Investment & Securities API) and back-testing the "follow smart-money dual net-buying" hypothesis without look-ahead bias.
This repository is a methodology & engineering showcase. The signal concept (following days when both foreign and institutional investors net-buy the same name) is public knowledge. The tuned parameters that produced edge in my own research — scoring weights, take-profit / stop-loss levels, streak / window thresholds — live in a private config and are not included here. See Reproducibility.
Most retail "smart-money follower" strategies are back-tested wrong: they peek at the close to generate a signal and then assume they could have bought at that same close. That single look-ahead leak is enough to turn a losing rule into a "winner" on paper.
This framework was built to answer one question honestly:
If you systematically buy names where foreign and institutional investors are both accumulating, and you can only act on information available before the trade — does it actually pay?
The short answer from my data so far: inconclusive — and the framework is designed to say so loudly rather than hide it. (See Honest limitations.)
| Module | Role |
|---|---|
src/collect.py |
Daily ingestion: foreign/institutional flow ranks, dual net-buy detection, minute bars, execution-strength snapshots, fundamental features → SQLite |
src/signal.py |
Accumulation detector: streak, N-day cumulative net-buy, acceleration, density → transparent additive score |
src/backtest.py |
Event-driven back-test, 3 entry timings × TP/SL/time exit, look-ahead-free by construction |
src/query.py |
Read-side helpers over the SQLite store |
config.example.yaml |
All tunable parameters, externalized (real values withheld) |
These are deliberate choices to keep the back-test from lying to me:
-
No look-ahead. Signals are computed on confirmed end-of-day data; entry is the next session's open. The signal at index
tcan only readpanel[0..t](signal.py,backtest.py:signal_*). -
Pessimistic fills. When a bar's high hits the target and its low hits the stop on the same day, the back-test assumes the stop filled first. Gaps are filled at the open. Ambiguity always resolves against the strategy.
-
Survivorship bias is named, not hidden. The candidate universe is "today's net-buy leaders," which is itself a forward-looking-ish pool. This is documented at every output, not swept under the rug.
-
Small-sample honesty. The KIS investor endpoint returns ~30 trading days. That is not enough for a statistical conclusion, so the tool prints sample size and refuses to call a result significant. The intended use is to accumulate data daily via
collect.pyand re-validate on months of history. -
Transparent scoring. The accumulation score is a plain additive function (streak + dual-cumulative + acceleration + density), not a black box — so every rank is explainable. The weights are the tunable, withheld part.
KIS REST API ──┐
├─► collect.py ──► SQLite (quant.db)
│ • investor_flow_rank • minute_bar
│ • dual_net_buy • strength_snapshot
│ • stock_feature
│
signal.py ◄┘ build universe → fetch 30d investor history → score → rank
│
backtest.py ◄─ build OHLC×investor panel → entry signal → simulate exit → stats
Single-file modules, no framework lock-in, SQLite for the store. The point is clarity and auditability, not infrastructure.
The code is intentionally not runnable out of the box as a signal generator:
api.py(KIS auth, token, signed headers) is excluded — you must supply your own KIS credentials and a thin client matching the interface insrc/api_stub.py.config.example.yamlships with neutral placeholder parameters. The weights and thresholds that worked in my research are not published.
So you can read, audit, and understand the methodology — but you cannot run it to lift a live signal. That is by design.
- ~30-day lookback per name → tiny sample, directional reference only, not a result.
- Candidate pool = today's net-buy leaders → survivorship / selection bias.
- No transaction-cost / slippage model beyond pessimistic fills and open-gap execution.
- Korea-only, single data vendor (KIS).
These are listed first, not buried — because for a research tool, knowing why you can't trust a number yet is the whole job.
Python 3.11+ · SQLite · KIS Open API (REST) · requests. No heavyweight dependencies.
Lee Geonjun (이건준) — Statistics, Korea University · quant research / algorithmic trading (KR & US markets), WorldQuant IQC. 📧 mark461182@gmail.com