English · 한국어
US equity factor research that charges for its own search — walk-forward only, Deflated Sharpe and PBO at the gate, and the rejections ship too.
Log scale — equal slopes mean equal returns. The flat stretches in 2008, 2012 and 2022 are the 200-day overlay holding cash; those three are where the drawdown parts company with SPY. (Chart labels are Korean; the alt text carries the reading.)
Three things that are unusual about this repo
make install # uv sync --extra dev — requires uv (https://docs.astral.sh/uv/)
make test # pytest + coverage — sanity-check the install
No API key needed to explore the results that are already checked into the repo:
# Reproduce the strategy-search cost (DSR/PBO) — reads only results/oos/, no vendor data
uv run python scripts/strategy_search_cost.py
# Original VAA strategy — free data (yfinance), interactive menu
make run
Re-running the factor engine or TAA from scratch (ingesting fresh data,
optimizing new configs) needs a Sharadar subscription — see
Usage below. Everything reported in this README was measured on data through
2026-08-14, and the outputs in results/ are what ships, so the numbers can
be checked without vendor data.
Three subsystems answer three separate questions, each measured against the same overfitting gate (Deflated Sharpe + PBO).
Factor engine (factor/) |
TAA allocation (taa/) |
Original VAA (strategies/) |
|
|---|---|---|---|
| Question | Which stocks to buy | Which asset class to rotate into | (same — first attempt) |
| Scope | US single stocks (20,931 tickers, 1997–2026) | 18 ETFs | 7–11 ETFs |
| Data | Sharadar direct (point-in-time, delisted included) | Sharadar funds bulk (closeadj) |
yfinance daily closes |
| Entry point | opt-factor · opt-factor-tui |
scripts/run_taa.py |
make run · run.py |
| Outcome | 1 adopted (large-cap) | 0 adopted — all 9 failed the PBO gate | Kept as the record of why it failed |
None of the three imports another, with one exception: taa/ →
factor.research.overfitting (DSR and PBO gate).
docs/factor-library.md.configs/ — adopted,
rejected and retired alike, operating parameters included.PITStore, DuckDB) with first-print-wins fundamentals and
delisted names retained.Operating candidate · walk-forward validation window · 2002-12 – 2026-08 (23.6y)
Large-cap, 5 factors + 200-day moving-average timing overlay (configs/strategy_lean_timed.json)
| Metric | Slippage 15bps | Slippage 50bps | SPY (same window) |
|---|---|---|---|
| CAGR | 16.34% | 14.91% | 11.66% |
| Max drawdown | −24.3% | −24.3% | −55.2% |
| Volatility | 15.7% | 15.7% | 18.6% |
| Sharpe | 0.727 | 0.648 | 0.418 |
| Calmar | 0.67 | 0.61 | 0.21 |
| Deflated Sharpe (72 parameter trials) | 0.996 ✓ | 0.988 ✓ | — |
| Deflated Sharpe (35 strategy trials) | 0.982 ✓ | 0.957 ✓ | — |
| PBO (CSCV over 35 configs · monthly · S=16) | 0.303 ✓ | — | — |
This strategy is measured with the guards switched on — $5 minimum price, $1M minimum dollar volume, and slippage. The universe is the historical S&P 500, so there is no capacity limit. Raising slippage to 50bps leaves drawdown and volatility unchanged and costs 1.4pp of return.
The last two rows charge for the search outside the walk-forward — “35 strategies
were tried and one was picked”. The reasoning behind the gate’s settings (monthly,
S=16) is below. Reproduce with uv run python scripts/strategy_search_cost.py; it
reads only results/oos/ and needs no vendor data.
Quant backtests fail in a small number of well-known ways. Each one is blocked structurally here, under one design principle: never fail silently.
| Common failure | How it is prevented |
|---|---|
| Survivorship bias — only today’s survivors are in the sample | Delisted names retained (Enron, old American Airlines, Ambac verified present) |
| Look-ahead — using numbers before they were public | Expressions cannot touch raw tables; everything passes through PanelContext, which enforces datekey alignment |
| Restatement contamination — using revised figures | First print wins — only the number the market originally saw is stored |
| Silent truncation — partial data reported as success | Pagination raises TruncatedDataError when the expected range isn’t reached |
| Overfitting — run hundreds of variants, report the best | Deflated Sharpe Ratio + PBO charge for the number of trials |
| In-sample performance reporting | Official performance is walk-forward only; single backtests are labelled reference-only |
Same strategy, same window; the only difference is slippage, minimum price and minimum dollar volume. The blue line was this README’s headline until 2026-08-16.
A micro-cap, 8-factor strategy at CAGR 23.78% stood here until the three guards the design document calls mandatory were switched on. They collapse it — Sharpe 1.047 → −0.224 — because 98% of the universe disappears with them on. The headline moved to the large-cap strategy, which had always been measured with its guards on and so had never been compared on equal terms.
Over twenty candidates were rejected at the Deflated Sharpe gate, and the PBO number
was published wrong twice before it was measured properly. The whole trail is
written down: docs/journal/.
Monthly rotation across 18 ETFs. Nine pre-registered configurations, all nine rejected: PBO = 0.770. The gate was not relaxed.
That does not mean nothing works. All six BAA variants (Keller 2022) beat 60/40 on Calmar without exception, 0.535–0.812 against 0.354 — but PBO across just those six is 0.861, so which one is best cannot be determined from this data. Both sentences have to stand together.
The nine configurations, the eight defects caught along the way, and why VAA measured
6.07% here against the papers’ 16–17% are in
docs/taa/01-results.md (Korean) and
docs/journal/.
flowchart LR
subgraph Factor engine
SH[(Sharadar<br/>sf1 · sep · daily · actions)] -->|"opt-factor ingest"| STORE[(PITStore<br/>us.duckdb)]
STORE --> CTX["PanelContext<br/>(datekey-aligned, no look-ahead)"]
CTX --> DSL["Factor DSL<br/>158 factors, factor/library"]
DSL --> UNIV["Universe filters<br/>price · dollar-volume · sector"]
UNIV --> PIPE["FactorPipeline.run<br/>factor/pipeline.py"]
PIPE --> BT["Backtest engine<br/>factor/backtest"]
BT --> WF["walk-forward optimize<br/>opt-factor optimize"]
WF --> DSRF["DSR + PBO gate<br/>factor/research/overfitting.py"]
DSRF -->|"passes"| HOLD["opt-factor holdings<br/>trade plan"]
end
subgraph TAA allocation
FUNDS[(Sharadar funds bulk<br/>closeadj, 18 ETFs)] -->|"taa/data.py"| SIG["Signals<br/>13612w · sma13"]
SIG --> STRAT["StrategySpec registry<br/>9 pre-registered configs"]
STRAT --> TBT["Monthly backtest<br/>taa/backtest.py"]
TBT --> EVAL["evaluate_all + verdict<br/>taa/evaluate.py"]
EVAL --> DSRF
end
subgraph Original VAA
YF[(yfinance<br/>daily closes)] --> VAA["VAAStrategy<br/>strategies/vaa.py"]
VAA --> RUNPY["run.py / make run<br/>backtest + report"]
end
Reproducing the ingest/optimize commands below needs a Sharadar subscription — the only retail-priced source with point-in-time fundamentals and delisted coverage. The adapter sits behind a neutral
Providerprotocol, so swapping sources means rewriting one file.
# Ingest data (Sharadar subscription required)
export SHARADAR_API_KEY=...
opt-factor ingest --store us.duckdb --provider sharadar \
--tables sf1,sep,daily,actions,sp500,tickers
# Screen factor predictive power — decile spread, IC, turnover
uv run python scripts/factor_lab.py --store us.duckdb --factors GP_A,PER,SIZE
# Official performance (walk-forward + Deflated Sharpe)
opt-factor optimize --store us.duckdb --config configs/strategy_lean_timed.json \
--space configs/space.json --objective calmar
# What to buy today (pass current holdings to get a trade plan)
opt-factor holdings --store us.duckdb \
--config configs/strategy_lean_timed.json --current my_holdings.csv
# TAA: 9 configurations · PBO · verdict table
uv run python scripts/run_taa.py
make lint # ruff check + format --check
make typecheck # mypy src/
Dependencies are managed with uv (uv.lock). Do not use pip install. Code lives
in src/opt_portfolio/: factor/, taa/, strategies/, with analysis/ and
core/ shared.
Design documents are Korean, in docs/factor-system/ — specs,
the data contract (store schema · PIT rules · vendor measurements), the
walk-forward mathematics, and the experiment log with the full rejection list. In
English: docs/journal/ and
docs/factor-library.md.
quant-airflow ingest DAG
is switched off), so the store ~/data/us_micro.duckdb is frozen at 2026-09-10
prices and no longer rebuilds — re-running ingest against the live API will not
succeed. Taxes are not modelled.strategies/ is in-sample on a different data source — for comparison use taa/.⚠️ All backtests are historical and do not guarantee future returns.
MIT
Part of an open-source stack spanning Korean equities, US equities and crypto. Each repository stands on its own.
| Market | Project | What it is |
|---|---|---|
| 🇰🇷 Korean equities | kiwoom-client | Kiwoom Securities REST API client — full domestic-equity endpoint coverage, real-time WebSocket, sync + async (pip install kiwoom-client) |
| 🇰🇷 Korean equities | krx-fundamentals-client | Korean corporate fundamentals Python client library — financial statements, valuation, dividends, screening (DART + KRX + Naver) |
| 🇰🇷 Korean equities | krx-news-client | Korean market news & disclosures as one schema — Python client library (DART + Toss Securities) |
| 🇰🇷 Korean equities | krx-quant-core | Shared Python core for Korean-equity quant systems — KRX tick, price-limit and session rules, date-aware transaction-tax cost model, Kiwoom order guards, DART material-disclosure risk classification, fill simulation, Deflated Sharpe and purged CV stats |
| 🇰🇷 Korean equities | fin-checkup | Telegram alerts for risk disclosures + a DART/SEC financial health checkup — reports measurements and facts, never a recommendation |
| 🇰🇷 Korean equities | quant-airflow | Airflow pipeline collecting Korean market data into TimescaleDB — delisted names included, so downstream backtests aren’t survivorship-biased |
| 🇰🇷 Korean equities | swing-it | KOSPI/KOSDAQ alpha validation framework — judged on the per-trade distribution, with random null controls, purged CV and Deflated Sharpe enforced as CI guardrails |
| 🇺🇸 US equities | macro-sector-agent | Top-down US sector-cycle research — asks which industry has been forgotten, not what to buy; the machine excludes, never picks |
| 🇺🇸 US equities | automated-stock-trading-systems | Backtester for Bensdorp’s seven non-correlated trading systems (educational reimplementation) |
| ₿ Crypto | binance-quant-engine | Crypto futures backtest & execution engine — zero lookahead, backtest↔live parity |
| ₿ Crypto | crypto-quant-insights | Chart-backed research notes from validating a live futures system — crash stress tests, Kelly leverage, live/backtest execution gaps |
Younghwan Chae (채영환) · GitHub @younghwan91 · LinkedIn
The full open-source quant stack is listed on the profile.