English · 한국어
US equity factor engine + tactical asset allocation (TAA) validation system.
This repo answers three separate questions with three separate subsystems, each measured against the same overfitting gate (Deflated Sharpe + PBO) so the results can be trusted rather than just produced.
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 |
Start with the factor engine below — it’s the only subsystem with an adopted, live strategy. TAA and the original VAA are validation exercises kept because what failed and why is documented.
None of the three imports another, with one exception: taa/ →
factor.research.overfitting (DSR and PBO gate).
make install # uv sync --extra dev — requires uv (https://docs.astral.sh/uv/)
make test # pytest + coverage (403 tests) — 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.
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.)
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.
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
A cross-sectional US equity factor engine built so that results can be trusted, not just produced. One design principle drives everything: never fail silently.
Quant backtests fail in a small number of well-known ways. Each one is blocked structurally here.
| 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/.
Not 158 functions — a declarative expression DSL generates TTM / QoQ / YoY /
acceleration variants automatically, and only factors with a documented rationale are
included (Novy-Marx 2013, Sloan 1996, Ball et al. 2016, plus Chen & Zimmermann
replications). Categories and counts: docs/factor-library.md.
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
A strategy is fully declared by one JSON file in configs/ —
adopted, rejected and retired alike, including the operating parameters.
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/.
uv run python scripts/run_taa.py # 9 configurations · PBO · verdict table
make run # original VAA (yfinance, kept for the record)
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 pipeline — the
subscription is active and ingest does not fail. 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
If you found this useful, please ⭐ Star the repository — it improves discoverability for others looking for the same thing.
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 & disclosure Python client library (DART + Hankyung + TheBell + Toss) |
| 🇰🇷 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 | kr-quant | KOSPI/KOSDAQ alpha research — walk-forward, random null controls, purged CV and Deflated Sharpe enforced as CI guardrails |
| 🇺🇸 US equities | automated-stock-trading-systems | Backtester for Bensdorp’s seven non-correlated trading systems (educational reimplementation) |
| ₿ Crypto | quantbox-engine | Crypto futures backtest & execution engine — zero lookahead, backtest↔live parity |
Younghwan Chae (채영환) · GitHub @younghwan91 · LinkedIn
The full open-source quant stack is listed on the profile.