Skip to content

Hyunwoo2267/statarb

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

statarb — relative-sentiment statistical arbitrage

A research system and methodology for measuring, without self-deception, whether a relative-sentiment signal has edge after costs — and refusing to trade if it doesn't. Not investment advice.

  • DESIGN.md — authoritative specification (single source of truth)
  • FINDINGS.md — full research report (what was tested, what was found)

Languages / 언어: English · 한국어


English

Overview

The goal was never a "profit bot." It was three things (per DESIGN.md):

  1. Measure, without self-deception, whether relative sentiment has edge after costs.
  2. If it does, harvest it under risk control.
  3. If it doesn't (or it decays), never risk ruin — accept the result cleanly.

The pre-registered hypothesis: within a homogeneous cluster, names whose community/news sentiment is relatively overheated underperform their neglected peers. The real question: does a sentiment feature add incremental alpha over a pure spread (z-score) signal, net of costs?

This repository is the answer — and the answer is an honest, well-validated negative result, which is exactly the output the system was designed to produce.

Research outcome (TL;DR)

Six layers + a real-data pipeline were built test-first (180 tests), then three hypotheses were tested on real data. All three are null after costs, multiple-testing correction, and robustness checks:

Hypothesis How it was tested Result
Spread-only pairs baseline 223 pairs, Kalman dynamic hedge + cointegration filter + monthly + vol-scale, walk-forward + sealed holdout dead — ~0.2 annualized Sharpe, not significant
News sentiment as a spread increment FNSPID news (24y) + FinBERT, 11 cointegrated pairs, baseline vs sentiment non-robust false positive — lexicon +0.52 @ p=0.13 collapsed to FinBERT +0.03 @ p=0.51
Cross-sectional "overhyped underperforms" (core hypothesis) 16 clusters, contrarian long-short on FinBERT sentiment null — pooled Sharpe +0.00, bootstrap p=0.50

Along the way the anti-self-deception discipline caught four would-be false positives before any capital was risked: a multiple-testing "best pair" trap, a non-robust lexicon signal, a recurring short-window holdout illusion, and a 4.6× Sharpe-annualization bug (monthly returns annualized with √252 instead of √12), which only the scale-free bootstrap p-value exposed.

What's built (DESIGN roadmap §10, steps 1–6)

Layer Modules DoD tests
1 data (PIT, survivorship-free, corp-actions, ingest) data/store.py, universe.py, corporate_actions.py, ingest_prices.py, sources.py, yfinance_source.py test_pit_store, test_no_lookahead, test_universe, test_corporate_actions, test_ingest, test_yfinance_source
2 cost model + event engine backtest/costs.py, engine.py test_costs, test_engine
3 spread signal + validation signals/spread.py, kalman.py, backtest/validation.py, strategy/pairs_strategy.py, kalman_strategy.py test_spread, test_cointegration, test_kalman, test_validation, test_strategy, test_kalman_strategy
4 sentiment + PnL + reports signals/sentiment.py, scorer.py, data/ingest_sentiment.py, sentiment_sources.py, backtest/portfolio.py, report.py, cross_sectional.py test_sentiment, test_scorer, test_ingest_sentiment, test_csv_sentiment_source, test_sentiment_integration, test_portfolio, test_report, test_cross_sectional
5 risk: sizing, limits, stress risk/sizing.py, limits.py, stress.py test_sizing, test_safety_limits, test_stress
6 execution safety + OMS execution/safety.py, oms.py test_execution_safety, test_oms

Every module was written test-first (TDD): the Definition-of-Done test was authored, confirmed red, then the implementation made it green.

Non-negotiable invariants (enforced in code)

  1. No look-ahead / PIT / survivorship-free. Every record carries event_time (when it refers to) and as_of (when it became knowable); the store answers queries by as_of, so a decision at time t can never see data knowable only after t. This is proven by a mutation test: a deliberately leaky store fails the invariant. Delisted/merged names stay in the universe.
  2. Baseline before sentiment. Sentiment is kept only if it adds robust, statistically-significant incremental alpha over the spread-only baseline, on the same data/costs/validation. (It did not — see FINDINGS.)
  3. Short-squeeze stress. Sizing is calibrated to survive a +400% squeeze (config/risk.yaml stress_calibration, computed by scripts/calibrate_risk.py); 2008/2020 vol scenarios cost <8%.

Methodology — the anti-self-deception machinery

  • PIT two-timestamp model makes look-ahead structurally impossible; the store is the single gatekeeper.
  • Full cost model (commission, volatility-scaled slippage, market impact, time-varying borrow, FX, taxes, short dividends). Every reported number is net of costs.
  • Multiple-testing defenses: time-ordered walk-forward, a once-evaluated sealed holdout, the Deflated Sharpe Ratio, and White's Reality Check (circular block bootstrap).
  • Bootstrap p as the primary significance metric: a Sharpe ratio is fragile to scale, annualization, and non-normality, so the primary test is a block-bootstrap p-value that a book's mean return exceeds zero. This caught the 4.6× Sharpe bug that a naïve t-stat would have hidden.
  • Robustness via scorer swap: a promising signal from a crude lexicon scorer was re-tested with a proper FinBERT scorer — and did not survive.

Setup

python -m venv .venv
.venv/Scripts/python.exe -m pip install -e ".[dev]"
# also used: scipy statsmodels yfinance ; FinBERT path: torch (CPU) transformers

Run

.venv/Scripts/python.exe -m pytest                  # 180-test DoD suite

# synthetic demos
.venv/Scripts/python.exe scripts/run_backtest.py            # baseline vs sentiment (synthetic)
.venv/Scripts/python.exe scripts/run_sentiment_increment.py # sentiment stack e2e (mock + CSV)
.venv/Scripts/python.exe scripts/calibrate_risk.py          # short-squeeze survival sizing

# real data (network)
.venv/Scripts/python.exe scripts/run_pair_search.py         # spread baseline (FINDINGS §5.1)
# after downloading FNSPID news (5.5GB):
.venv/Scripts/python.exe scripts/prepare_fnspid.py          # extract headlines
.venv/Scripts/python.exe scripts/score_fnspid_finbert.py    # FinBERT scoring (cached)
.venv/Scripts/python.exe scripts/run_fnspid_increment.py        # sentiment increment (§5.2)
.venv/Scripts/python.exe scripts/run_fnspid_cross_sectional.py  # core hypothesis (§5.3)

Plugging in your own sentiment data

Any historical posts CSV plugs in with no code changes (provider-agnostic SentimentSource contract):

from statarb.data.sentiment_sources import CsvSentimentSource
src = CsvSentimentSource("posts.csv", symbol_col="ticker", text_col="body",
                         time_col="created_utc", time_is_epoch=True,
                         score_col="finbert")  # score_col optional (cached scores)

Not built (intentional)

  • Broad, long-horizon retail/social sentiment data — the only qualitatively untested variant (the original GME-style hypothesis), needing data not freely available (see FINDINGS §8).
  • ingest_borrow.py; broker adapters execution/broker_ibkr.py, broker_kis.py; monitoring/ wiring (only needed once a real edge justifies live trading).

Conclusion

On accessible (free, news-based) data there is no robust, significant sentiment edge. The disciplined decision is to accept the negative result and not trade. This is not a failure of the system — it is the system achieving its purpose: measuring honestly and refusing self-deception. The platform remains a validated research foundation, ready to resume the moment better data or a better hypothesis is available.

Config lives in config/*.yaml; data (data/) and secrets (.env) are gitignored.


한국어

개요

이 프로젝트의 목표는 "수익 봇"이 아니라 세 가지였다 (DESIGN.md 기준):

  1. 상대감성 신호에 비용 차감 후 엣지가 있는지를 자기기만 없이 측정한다.
  2. 엣지가 있다면 리스크를 통제하며 수확한다.
  3. 엣지가 없거나 사라져도 파산하지 않는다 — 결과를 깨끗이 받아들인다.

사전 등록된 핵심 가설: 동종 클러스터 내에서 커뮤니티/뉴스 감성이 상대적으로 과열된 종목은 소외된 동종 대비 언더퍼폼한다. 진짜 검증 질문은 "감성 피처가 순수 스프레드(z-score) 신호 대비 비용 차감 후 증분 알파를 주는가" 이다.

이 저장소는 그 질문에 대한 답이며, 답은 정직하고 충분히 검증된 음성 결과(no edge) 다 — 그것이 바로 이 시스템이 내도록 설계된 산출물이다.

연구 결과 (요약)

여섯 레이어와 실데이터 파이프라인을 테스트 우선(180개) 으로 구축한 뒤, 세 가설을 실데이터로 검정했다. 세 가지 모두 비용·다중검정·강건성 검정 후 null:

가설 검정 방법 결과
순수 스프레드 페어 베이스라인 223페어, Kalman 동적 헤지 + 공적분 필터 + 월간 + vol-scale, walk-forward + 봉인 홀드아웃 죽음 — 연 Sharpe ~0.2, 비유의
감성 = 스프레드 증분 FNSPID 뉴스(24년) + FinBERT, 11개 공적분 페어, 베이스라인 vs 감성 견고하지 않은 거짓양성 — Lexicon +0.52@p0.13 → FinBERT +0.03@p0.51
횡단면 "과열 종목 언더퍼폼" (핵심 가설) 16개 클러스터, FinBERT 감성으로 과열 숏/소외 롱 null — 풀링 Sharpe +0.00, 부트스트랩 p=0.50

그 과정에서 자기기만 방지 장치가 거짓양성 네 건을 자본 투입 전에 잡았다: 다중검정 "최고 페어" 함정, 견고하지 않은 Lexicon 신호, 반복되는 짧은-홀드아웃 환상, 그리고 Sharpe 4.6배 과대평가 버그(월간 수익률을 √12가 아니라 √252로 연율화) — 마지막은 스케일 무관 부트스트랩 p값만이 적발했다.

구축한 것 (DESIGN 로드맵 §10, 1~6단계)

레이어 모듈 DoD 테스트
1 데이터 (PIT·생존편향제거·코퍼레이트액션·인입) data/store.py, universe.py, corporate_actions.py, ingest_prices.py, sources.py, yfinance_source.py test_pit_store, test_no_lookahead, test_universe, test_corporate_actions, test_ingest, test_yfinance_source
2 비용 모델 + 이벤트 엔진 backtest/costs.py, engine.py test_costs, test_engine
3 스프레드 시그널 + 검증 signals/spread.py, kalman.py, backtest/validation.py, strategy/pairs_strategy.py, kalman_strategy.py test_spread, test_cointegration, test_kalman, test_validation, test_strategy, test_kalman_strategy
4 감성 + PnL + 리포트 signals/sentiment.py, scorer.py, data/ingest_sentiment.py, sentiment_sources.py, backtest/portfolio.py, report.py, cross_sectional.py test_sentiment, test_scorer, test_ingest_sentiment, test_csv_sentiment_source, test_sentiment_integration, test_portfolio, test_report, test_cross_sectional
5 리스크: 사이징·한도·스트레스 risk/sizing.py, limits.py, stress.py test_sizing, test_safety_limits, test_stress
6 실행 안전장치 + OMS execution/safety.py, oms.py test_execution_safety, test_oms

모든 모듈은 테스트 우선(TDD) 으로 작성했다: DoD 테스트를 먼저 쓰고, 빨강을 확인한 뒤, 구현으로 초록을 만들었다.

비타협 원칙 (코드로 강제됨)

  1. look-ahead 차단 / 시점고정(PIT) / 생존편향 제거. 모든 레코드가 event_time(관측이 가리키는 시점)과 as_of(알 수 있게 된 시점)을 보유한다. 저장소는 as_of로 질의하므로, 시각 t의 결정은 t 이후에야 알 수 있는 데이터를 절대 볼 수 없다. 이는 뮤테이션 테스트로 증명한다 — 일부러 누수되게 만든 저장소는 불변식 검사에서 실패한다. 상폐/합병 종목도 유니버스에 남긴다.
  2. 베이스라인 먼저, 그다음 감성. 같은 데이터·비용·검증 구간에서 순수 스프레드 대비 견고하고 통계적으로 유의한 증분 알파를 줄 때만 감성을 채택한다. (주지 못했다 — FINDINGS 참조.)
  3. 숏스퀴즈 스트레스. 사이징은 +400% 스퀴즈에서 계좌가 생존하도록 보정한다 (config/risk.yamlstress_calibration, scripts/calibrate_risk.py로 산출). 2008/2020 변동성 시나리오 손실은 8% 미만.

방법론 — 자기기만 방지 장치

  • PIT 2-타임스탬프 모델이 look-ahead를 구조적으로 불가능하게 한다. 저장소가 유일한 관문이다.
  • 비용 모델 전체 (수수료, 변동성연동 슬리피지, 시장충격, 시변동 borrow, 환전, 세금, 숏 배당). 보고된 모든 수치는 비용 차감 후(NET) 다.
  • 다중검정 보정: 시간순 walk-forward, 단 한 번 평가하는 봉인 홀드아웃, Deflated Sharpe Ratio, White Reality Check(순환 블록 부트스트랩).
  • 1차 유의성 지표 = 부트스트랩 p: Sharpe는 스케일·연율화·정규성 가정에 취약하므로, book 수익률 평균이 0을 넘는지에 대한 블록 부트스트랩 p값을 1차 지표로 쓴다. 이것이 단순 t-stat라면 숨겼을 Sharpe 4.6배 버그를 적발했다.
  • 스코어러 교체 강건성 검정: 약한 Lexicon 스코어러에서 나온 유망한 신호를 제대로 된 FinBERT 스코어러로 재검증했고, 살아남지 못했다.

설치

python -m venv .venv
.venv/Scripts/python.exe -m pip install -e ".[dev]"
# 추가 사용: scipy statsmodels yfinance ; FinBERT 경로: torch (CPU) transformers

실행

.venv/Scripts/python.exe -m pytest                  # 180개 DoD 테스트

# 합성 데모
.venv/Scripts/python.exe scripts/run_backtest.py            # 베이스라인 vs 감성 (합성)
.venv/Scripts/python.exe scripts/run_sentiment_increment.py # 감성 스택 e2e (mock + CSV)
.venv/Scripts/python.exe scripts/calibrate_risk.py          # 숏스퀴즈 생존 사이징

# 실데이터 (네트워크 필요)
.venv/Scripts/python.exe scripts/run_pair_search.py         # 스프레드 베이스라인 (FINDINGS §5.1)
# FNSPID 뉴스(5.5GB) 다운로드 후:
.venv/Scripts/python.exe scripts/prepare_fnspid.py          # 헤드라인 추출
.venv/Scripts/python.exe scripts/score_fnspid_finbert.py    # FinBERT 점수화 (캐시)
.venv/Scripts/python.exe scripts/run_fnspid_increment.py        # 감성 증분 (§5.2)
.venv/Scripts/python.exe scripts/run_fnspid_cross_sectional.py  # 핵심 가설 (§5.3)

직접 감성 데이터 연결하기

어떤 과거 포스트 CSV든 코드 수정 없이 꽂힌다 (제공사 독립적 SentimentSource 계약):

from statarb.data.sentiment_sources import CsvSentimentSource
src = CsvSentimentSource("posts.csv", symbol_col="ticker", text_col="body",
                         time_col="created_utc", time_is_epoch=True,
                         score_col="finbert")  # score_col은 선택 (캐시된 점수)

미구현 (의도적)

  • 넓고 긴 retail/소셜 감성 데이터 — 유일하게 질적으로 미검정인 변형(원래 GME류 가설). 무료로 구하기 어려운 데이터가 필요하다 (FINDINGS §8 참조).
  • ingest_borrow.py; 브로커 어댑터 execution/broker_ibkr.py, broker_kis.py; monitoring/ 배선 (진짜 엣지가 실거래를 정당화할 때만 필요).

결론

접근 가능한(무료·뉴스 기반) 데이터로는 견고하고 유의한 감성 엣지가 없다. 규율 있는 결정은 음성 결과를 받아들이고 실거래하지 않는 것이다. 이는 시스템의 실패가 아니라 설계 목적의 달성 이다 — 정직하게 측정하고 자기기만을 거부했다. 플랫폼은 검증된 연구 토대로 남아, 더 나은 데이터나 가설이 생기면 즉시 재개할 수 있다.

설정은 config/*.yaml에, 데이터(data/)와 비밀키(.env)는 gitignore된다.

About

Rigorous, test-first stat-arb research platform for measuring relative-sentiment edge after costs (PIT · multiple-testing · bootstrap). Result: honest null.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages