krx-quant-core
한국 주식(KOSPI·KOSDAQ) 퀀트 공통 코어. 호가단위·가격제한폭·세션 규칙, 일자별 증권거래세 비용모델, 키움 주문 가드, DART 중대공시 분류, 킬스위치, 체결 시뮬레이션, Deflated Sharpe·purged CV 검증 통계를 한 패키지로 묶었다.
전략은 여기 없다. 전략을 가진 레포들이 같은 시장 규칙과 같은 비용 숫자를 쓰게 하는 것이 이 패키지의 존재 이유다.
| 소비자 | 매매 스타일 | 실행 | 이 패키지에서 쓰는 것 |
|---|---|---|---|
| scalp-it (비공개) | 스캘핑 | 자동 실주문 | 호가·상한가·주문 가드·호가 스윕·체결 규칙·호스트 가드 |
| daytrade-it (비공개) | 데이트레이딩 | 자동 실주문 | 비용모델·공시 리스크 게이트·DART DB·코드 정규화·호스트 가드 |
| swing-it | 스윙 | 리서치 + 수동 체결 | DSR·purged CV·부트스트랩·취약성 진단·횡단면 시뮬 |
왜 따로 뺐나
세 레포가 같은 규칙을 각자 들고 있었고, 실제로 어긋나 있었다.
normalize_code가 두 레포에 복붙돼 있었다. 한쪽 옛 구현은0155E0을001550(완전히 다른 회사)으로 바꾸던 버그를 안고 있었다.- 거래비용이 레포마다
0.0023·0.0034·0.0064·50bp로 흩어져 있었고, 거래세가 2023→2024→2025→2026 에 네 번 바뀌었는데 일자별로 적용하는 코드가 어디에도 없었다. - 체결 판정(
through/touch)이 같은 레포 안에서도 알 수 없는 값을 받으면 한 곳은 through, 다른 곳은 touch 로 반대로 읽었다. - 하한가 계산은 없었다. 대칭식으로 짜면 틀린다 — 실측 대조 결과는 아래.
설치
pip install krx-quant-core==0.6.2
# 초 격자 호가 리플레이(backtest.lob)를 numba 로 가속하려면 extra 로: "krx-quant-core[fast]==0.6.2"
# optuna 스윕(research.optuna_search)까지 쓰려면: "krx-quant-core[fast,opt]==0.6.2"
# PyPI 릴리스 전(또는 태그 고정 개발 중)에는 git 태그로:
pip install "krx-quant-core @ git+https://github.com/younghwan91/krx-quant-core@v0.6.2"
Python ≥ 3.11. 의존성은 kiwoom-client(호가단위 표의 정본), numpy, pandas 뿐이다.
선택 extra fast 는 numba 를 더한다 — 없으면 같은 커널을 파이썬으로 돌려 같은 숫자를 낸다.
모듈
krx_quant_core/
├── market/ 종목코드·Market, 호가단위, 상/하한가, KST 세션, 거래일 달력
├── costs/ 일자별 거래세 스케줄, KoreanCostModel(Decimal), round_trip_cost(float)
├── execution/ 주문 관리 계층 — OrderManager·InstanceLock(oms.py), PositionBook(book.py),
│ Broker 프로토콜·PaperBroker·KiwoomBroker, EngineCore(같은 전략, 실매매/리플레이),
│ 키움 REST 주문 스펙, OrderIntent/OrderResult, OrderGuard(순수 가드)
├── risk/ DART 중대공시 분류·RiskGate, DartDisclosureDB, KillSwitch
├── backtest/ 호가 스윕 VWAP·왕복비용, 지정가 체결 규칙, 트레이드 원장 지표, 횡단면 시뮬,
│ replay.py(run_replay — EngineCore+PaperBroker 로 과거 이벤트 리플레이),
│ drift.py(일중·야간 수익 분해 — 이 유니버스의 일중은 구조적으로 음수인가)
│ └── lob/ 틱·호가 → 초 격자 특징·경로, 배치=실시간 공용 커널, 에피소드 시뮬, 무작위 대조군, 지정가 대기열 모델,
│ ceiling.py(오라클 천장 — 완벽한 예지력으로도 비용을 넘는가)
├── research/ run_sweep(격자·병렬·캐시), optuna_search(TPE, extra `opt`) — 둘 다 모든
│ config 를 시행 원장에 적는다
├── stats/ Deflated/Probabilistic Sharpe, t-haircut, purged walk-forward, 부트스트랩, 취약성,
│ selection.py(후보 선택편향 — 검증 최대를 고른 값이 실력 없이 얼마나 오르나),
│ matched_null.py(층 매칭 대조 + 날짜 클러스터 부트스트랩)
└── runtime/ 호스트 가드, 실행 기록 start_run, OOS 하드 잠금, kqc CLI(simnode 원격 실행 · kqc nightly ·
kqc nightly status · kqc pins)
from datetime import date
from krx_quant_core.market import Market, limit_up_price, limit_down_price, shift_ticks
from krx_quant_core.costs import round_trip_cost, tax_rate
limit_up_price(24_250) # 31500
limit_down_price(239_000) # 167500.0
shift_ticks(49_950, 2) # Decimal('50100') — 밴드 경계를 넘어도 유효 호가
tax_rate(date(2025, 6, 2), Market.KOSPI).total # Decimal('0.0015')
round_trip_cost(date(2026, 9, 14), Market.KOSDAQ, slippage_one_way=0.0015) # 0.0053
초 격자 호가 리플레이 (backtest.lob, v0.2)
scalp-it 80·81·82번에 흩어져 있던 틱·호가 리플레이를 옮겼다. 원본 build_code·
_strength_exit_labels·random_control 사본과 합성 틱·호가로 대조해 비트 단위 동일
(골든 테스트), numba 경로와 파이썬 폴백도 동일, 배치와 실시간 증분도 동일하다.
from datetime import date
from krx_quant_core.backtest.lob import build_second_grid, simulate_exits, random_entry_control
feat, path = build_second_grid(ticks, quotes) # 09:00~15:20 초 격자, 09:05~15:10 특징 행
r = simulate_exits(feat.sec, path["bid"], path["ask"], path["strength"],
strength_drop=3.0, max_hold=300, # 82번 청산 규칙
trade_date=date(2026, 9, 8), market="KOSDAQ") # 비용 = round_trip_cost
r.net, r.hold, r.reason # 순수익·보유초·청산사유(EXIT_*)
실시간은 SecondFeatureStream().update(...) 를 초마다 부른다 — 배치와 같은 step 커널이다.
체결 가정은 원본 그대로 낙관적이다(1호가 전량 체결, 잔량·대기열 무시).
초 단위 피처 v2 (backtest.lob.features_v2)
scalp-it 93·94번 강화학습 정책의 입력 37열(scripts/rl_93/features.json) 중 종목 하나로 계산되는
35열(FEATURE_SET_V2, 정본 순서)을 83번 build_code·84번 _extra 원본과 비트 단위로 같게
(nan 포함) 낸다. 정의는 고치지 않았다 — 84번이 호가 잔량을 유효성 필터 없이 다시 읽는 것,
ofiw 분모가 pandas rolling.mean(Kahan 보정합)인 것까지 그대로다.
import numpy as np
from krx_quant_core.backtest.lob import (
FEATURE_SET_V2, SecondFeatureStreamV2, aggregate_seconds_v2, compute_features_v2,
)
# 배치(연구): 한 종목·하루
X, (ptr, price, volume, side) = aggregate_seconds_v2(ticks, quotes)
F = compute_features_v2(X, ptr, price, volume, side) # (22800초, STEP_COLUMNS_V2) float64
obs = F[:, : len(FEATURE_SET_V2)].astype(np.float32) # 학습 캐시와 같은 float32
# 실시간(운용): 종목마다 스트림 하나, 초가 닫히면 그 초 호가 행 + 그 초 체결들
stream = SecondFeatureStreamV2()
row = stream.update_array(x, price_s, volume_s, side_s) # 매번 새 float64 배열
입력은 초당 호가 행(INPUT_COLUMNS_V2)과 체결 CSR(ptr·가격·수량·방향)이다 — same_size_val10
(최근 10초 같은 수량 반복)이 체결 단위라서다. 출력 STEP_COLUMNS_V2 = 35열 + 보조 cumval·last,
자료형은 배치·실시간 모두 float64(원본 계산 자료형). 정본 중 FEATURE_SET_V2_EXCLUDED =
cum_rank·dayret_rank 는 종목 간 순위(전일 종가·대상 종목 집합 필요)라 내지 않는다 — 소비자가
채우고, 원본 식은 cross_section_ranks_v2(cumval, last, prevclose) 에 옮겨 두었다. 원본 cum_rank
대상은 "그날 틱 500건 이상" 종목이라 하루가 끝나야 정해진다는 점에 주의.
실시간 초당 입력 만들기(배치 aggregate_seconds_v2 와 같은 규칙) — 초 s 가 닫힐 때:
x = [
last_tick.best_bid, last_tick.best_ask, # 그 초 마지막 틱 값, nan 이어도 그대로
last_tick.strength, # (그 초 틱이 없으면 셋 다 nan)
q.bidqty1, q.askqty1, # q = 그 초 마지막 "유효" 스냅샷(bid1>0 且 ask1>0), 없으면 nan
q.bidqty1 + q.bidqty2 + q.bidqty3, # ×q.bid1 (nan 잔량은 건너뛴 합) — bid_depth3
..., # ask_depth3 도 같은 식
r.bidqty1, r.askqty1, # r = 그 초 마지막 스냅샷(유효 여부 무관), nan 이어도 그대로
]
# 유효 스냅샷이 있으면 bid/ask 는 그 스냅샷의 bid1/ask1 로 덮는다.
row = stream.update_array(x, prices, volumes, sides) # 그 초 체결들, (ts, seq) 순
스트림 메모리는 인스턴스당 수십 KB 로 고정이다(링버퍼 301초 + 최근 10초 체결). 커널 상태가
09:00 부터의 누적이라 장중 재시작은 그날 초 격자를 처음부터 다시 넣어 복구한다:
SecondFeatureStreamV2.from_history(X, ptr, price, volume, side).
백테스트-운용 동일 커널. v2 는 한 초를 전진하는 step_v2 하나를 배치 루프와 실시간 스트림이
같이 부른다. 골든 테스트(합성 3 시드: 빈 호가창 초·체결 없는 초·폭주 체결·순위 동점)가 원본 대 배치,
배치 대 실시간, numba 대 파이썬 폴백을 모두 np.array_equal(equal_nan=True) 와 자료형까지 대조한다.
simnode 실측(워밍업 뒤, 부하 따라 흔들림): 스트림 한 초 갱신 634µs·배치 14µs/초(numba),
폴백은 각각 약 180µs·60µs.
지정가 대기열 모델 (backtest.lob.queue, v0.3)
touch/through 는 대기열 위치를 모를 때의 두 극단이다. 10단계 호가 스냅샷과 가격별 체결량으로
내 앞 잔량을 추적한다(hftbacktest L2 모델 방식, MIT): risk_averse(취소는 전부 내 뒤),
prob_power·prob_log(취소를 앞·뒤에 확률 배분). 도착 즉시 반대 호가 스윕, 부분 체결, 정수 초 지연.
from krx_quant_core.backtest.lob import build_book_grid, build_level_trades, simulate_limit_orders
book, trades = build_book_grid(quotes), build_level_trades(ticks)
r = simulate_limit_orders(+1, book.bid_px[secs, 0], 10, secs, book, trades,
max_wait=60, latency=1, queue_model="risk_averse")
r.filled_qty, r.avg_price, r.status # STATUS_FILLED / PARTIAL / CANCELED / NOT_PLACED
실데이터 점검(2026-09-10, 체결 많은 20종목, 30초마다 매수1호가 합류 10주, 최대 60초 대기):
| 모델 | 체결률 | 체결 60초 뒤 마크아웃 |
|---|---|---|
| touch | 87.3% | +3.2bp |
| prob_log | 71.0% | −4.2bp |
| risk_averse | 70.4% | −4.6bp |
| through | 61.4% | −9.4bp |
touch 가정은 체결률만 부풀리는 게 아니라 역선택을 지운다(마크아웃 부호가 뒤집힌다). 데이터가 1초 절삭이라 초 미만 순서·지연은 모델링하지 않는다 — 가정 전체는 모듈 docstring.
백테스트 실행 기반 (runtime, v0.4)
백테스트는 simnode 에서만 돈다. 숫자마다 어디서·어떤 코드로·어떤 데이터로·몇 번째 시도로 나왔는지 기계가 남긴다.
from krx_quant_core.runtime import DataSpec, start_run
with start_run("scalp84-flow", config, repo_root=ROOT,
data=DataSpec("2026-08-24", "2026-09-07", "train"), seed=84) as run:
...
run.log_result({"mean_bp": -3.1, "n": 812, "dsr_trials": run.n_trials})
- 게이트: simnode 아님 · 추적 파일 미커밋 · 잠긴 OOS 구간 →
RunRefused. - 기록:
research/runs/<label>/RUNS.jsonl(정본, git) +TRIALS.jsonl(DSR 의 N 자동) + Postgreskqc_runs색인([db]extra, 실패해도 실행 계속). - OOS:
kqc oos define→kqc prereg lock→start_run(..., final=True)는 label 당 한 번.
kqc run scalp-it -- uv run python scripts/x.py # trader 에서: 푸시된 sha 를 simnode worktree 에서 실행
kqc runs ls scalp84-flow --repo-root ~/git/scalp-it
공용 엔진 (v0.5)
daytrade-it·scalp-it 감사 결과(2026-09-16) 둘 다 코어를 25~40%만 쓰고 있었다 — 가드·킬스위치는
있는데 체결·주문관리는 각자 복제, 페이퍼 모드는 데몬이 안 씀, 재시작 대사가 없었다. v0.5 는
그 위에 얹는 세 겹이다: 주문 관리 계층(execution), 같은 전략이 실매매/리플레이를 도는 엔진
(execution.engine + backtest.replay), simnode 스윕·야간 실행(research + runtime.nightly).
| 배워온 곳 | 원리 | 적용 |
|---|---|---|
| NautilusTrader | 전략 코드는 백테스트·실매매에서 같다 — 다른 건 브로커/데이터 어댑터뿐. 시작 시 실계좌 대사 | EngineCore+Strategy 프로토콜, OrderManager.reconcile() |
| QuantConnect Lean | 브로커 모델·체결 모델·수수료 모델을 분리 | Broker 프로토콜 / PaperBroker(fill_basis=...) / 기존 costs |
| hftbacktest (MIT) | L2 호가 대기열 위치 모델 | PaperBroker 는 초 단위 스냅샷엔 backtest.fills, 격자 연구엔 backtest.lob.queue |
| vectorbt / Optuna | 대량 파라미터 스윕·병렬·조기 가지치기 | research.run_sweep(프로세스 풀·캐시), research.optuna_search(extra opt) |
| MLflow | 실행마다 코드·데이터·파라미터·지표를 기록 | 기존 runtime.start_run + TRIALS.jsonl 재사용 — 스윕의 모든 config 가 DSR 의 N 에 들어간다 |
1. 같은 전략, 실매매와 리플레이
Strategy(on_start/on_event/on_end)는 StrategyContext.oms(OrderManager)만 보고 어디서
체결되는지 모른다. 브로커만 바뀐다 — 웹소켓 이벤트는 KiwoomBroker 위 EngineCore 로, 과거
이벤트는 PaperBroker 위로 흘린다(backtest.replay.run_replay 가 그 배선을 대신 해 준다).
from krx_quant_core.execution import (
EngineCore, KiwoomBroker, OrderGuard, OrderGuardConfig, OrderManager, PositionBook, Quote,
)
class MyStrategy:
def on_event(self, ev, ctx) -> None:
if isinstance(ev, Quote) and ctx.oms.book.position(ev.code) is None:
ctx.oms.buy(ev.code, 1, int(ev.ask), ref_price=ev.ask)
# 리플레이 — run_replay 는 backtest 최상위가 아니라 backtest.replay 에서 임포트한다
# (execution.paper ↔ backtest.replay 상호 의존이라 backtest/__init__ 이 이걸 다시 내보내면 순환 임포트가 난다).
from krx_quant_core.backtest.replay import merge_events, run_replay
events = merge_events(bars=daily_bars_df) # ts, code, open/high/low/close, volume
result = run_replay(MyStrategy(), events)
result.fills, result.trades, result.book.realized_krw # trades = 청산 원장(매도 fill 당 한 행)
# 실매매 — 같은 전략, KiwoomBroker 위
broker = KiwoomBroker(api, dry_run=True) # dry_run=False 는 실주문
# fills_verified=False(기본): ka10076 체결 조회가 미검증이라 poll_fills/prime 은 [] (경고 1회).
# 모의계좌 실호출로 필드를 확인한 뒤에만 KiwoomBroker(api, dry_run=False, fills_verified=True).
guard = OrderGuard(OrderGuardConfig(max_qty=10, price_band_pct=0.05))
oms = OrderManager(broker, guard=guard, book=PositionBook(journal=Path("data/positions/2026-09-17.jsonl")),
order_log=Path("logs/orders.jsonl"))
engine = EngineCore(MyStrategy(), oms)
engine.feed(Quote(now_kst(), "005930", 70_000, 70_100)) # 웹소켓 시세 이벤트마다 호출
# 체결 동기화는 타이머로 — fills_verified=True 면 feed 마다 조회 REST 가 나가 한도를 넘는다.
# loop.call_later / 스레드 타이머 등으로 2초마다: oms.sync()
이름 주의:
execution.Trade는 시세 체결 틱 이벤트,backtest.Trade는 청산 원장 한 행이다. 한 파일에서 둘 다 쓰면from krx_quant_core.execution import Trade as TradeTick처럼 별칭을 쓸 것.
2. simnode 파라미터 스윕
from krx_quant_core.research import grid, run_sweep
from krx_quant_core.runtime import DataSpec
def objective(cfg: dict) -> dict:
... # 모듈 최상위 함수 — ProcessPoolExecutor 로 자식 프로세스에 피클된다
return {"sharpe": ..., "mean_bp": ...}
configs = grid(threshold=[0.5, 0.6, 0.7], hold_sec=[60, 120])
res = run_sweep(objective, configs, label="scalp84-sweep", repo_root=ROOT,
data=DataSpec("2026-08-24", "2026-09-07", "train"))
res.frame, res.n_trials, res.best("sharpe") # 모든 config 가 TRIALS.jsonl 에 적힌다
3. kqc nightly — simnode 야간 실행
레포에 research/nightly.toml 을 두면 kqc nightly 가 순서대로(job 하나가 실패해도 다음은 돈다)
nice -n 10 으로 실행하고, 로그와 요약(name, rc, secs, timed_out)을 ~/.kqc/nightly/<날짜>/ 에 남긴다.
# research/nightly.toml
[[job]]
name = "pair-sweep"
cmd = ["uv", "run", "python", "scripts/pair_sweep.py", "--days", "20"]
timeout_min = 60
weekdays_only = true
kqc nightly ~/git/scalp-it # 크론 한 줄(장 마감 후, simnode 전용)
kqc nightly ~/git/scalp-it --only pair-sweep --dry-run
kqc nightly status --days 7 # 최근 레포×job 결과표. 최근 실패가 있으면 exit 1 (어느 호스트든)
kqc pins # 소비 레포가 핀한 코어 버전 표. 뒤처진 레포가 있으면 exit 1
cron 의 PATH 는 /usr/bin:/bin 뿐이라 uv 처럼 ~/.local/bin 에 있는 명령을 못 찾는다 — daytrade-it
의 job 이 2026-09-23 부터 매일 rc=127 로 조용히 죽어 있었다(v0.6.0 에서 발견). 그래서 cmd[0] 을
~/.local/bin·~/.cargo/bin·/usr/local/bin 을 보탠 PATH 에서 찾고 자식에게도 넘기며, 못 찾으면 찾아본
경로를 로그에 적는다. 실패는 stderr 의 kqc nightly: <repo> FAILED … 한 줄과 kqc nightly status 로 보인다.
안전 규칙 — 지키는 것과 아직 못 미더운 것
- 매수는 킬·가드가 막지만 매도(청산)는 막지 않는다.
OrderManager.sell이 막는 건 1주 미만· 가용 보유(보유 − 걸린 매도 잔량)보다 많이 파는 것·0 이하 지정가, 셋뿐이다. 킬이 걸린 날일수록 들고 있는 포지션은 빠져나가야 한다 — 청산까지 막으면 실포지션이 감시 없이 남는다. - 주문 제출은 재시도하지 않는다. 타임아웃·예외·
return_code없는 응답은OrderStatus.UNKNOWN("unknown") — 나갔는지 모른다, 재시도 금지. 미체결·잔고로 확인한다. 매수 UNKNOWN 은 일일 횟수 한도에 센다(나간 주문을 안 세면 상한을 넘긴다). 브로커가 rc≠0 으로 거부한 것만rejected. 조회(미체결·잔고·체결)만 예외 시 1회 재시도한다. poll_fills(ka10076체결 조회)는 미검증이다. 필드명이 kiwoom-client 에 예시가 없어 브리프 추정값을 쓴다. 그래서KiwoomBroker(fills_verified=False)가 기본이고, 이때poll_fills·prime은 조회 없이[]다 — 모의계좌로 실호출 확인 전에는 켜지 말 것.OrderManager.sync는 조회 예외를poll_fills_failed로그로 남기고[]를 돌려준다(엔진이 매 이벤트 터지지 않게).holdings()(kt00018)·open_orders()(ka10075)도 전부 검증된 건 아니다. 특히ka10075의 매수/매도 판별 필드(io_tp_nm·sell_tp_nm·sell_tp)는 실호출 미확인이다. 판별 못 한 행은 버리고(skipped_rows), 행이 있는데 전부 버려지면 경고 로그를 남긴다 — 조용히 빈 목록이면 매도 잔량 예약이 0 이 되어 중복 청산이 나간다.- 공유 계좌: 내 주문의 체결만 반영한다. scalp-it·daytrade-it 이 같은 계좌를 쓰므로 체결 조회에
남의 체결이 섞인다.
OrderManager는 자기가 낸 주문번호(own_orders,normalize_ord_no로 앞자리 0 정규화)의 체결만 장부·킬스위치에 넣고, 나머지는foreign_fills와foreign_fill로그로 뺀다 (own_orders_only=False는 계좌를 혼자 쓸 때만). 주문번호 없이 UNKNOWN 이 된 주문의 체결도 여기로 빠진다 — 사람이 확인한다. - 재시작하면 메모리 상태는 사라진다(후속 과제).
own_orders·분할 청산 누적 손익·킬스위치 상태는 복원되지 않는다. 재시작 전 주문의 체결은foreign_fills로 빠지니reconcile()로 대사할 것. InstanceLock은 같은 계좌를 도는 데몬이 두 번 뜨는 사고(2026-09-15 이중 매수 원인)를flock(LOCK_EX|LOCK_NB)로 막는다. 이미 잡혀 있으면AlreadyRunning— 데몬은 이걸 정상 종료로 다뤄야 한다(재시작 루프가 계속 두 번째 인스턴스를 죽이면 안 된다).OrderManager.reconcile()은 절대 주문을 내지 않는다. 장부 vsbroker.holdings()차이를 보고만 한다 — 어느 쪽이 맞는지는 사람이 판단한다.- 소비 레포 규약 스위치(v0.6.1). scalp-it
KiwoomOrderClient가 코어 매니저로 못 넘어오던 차이 네 가지를 옵션으로 열었다 — 기본값은 전부 위 원칙 그대로다.preorder_holdings_check(매수 직전 잔고조회, 실패 시 fail-closed 거부),guard_sells(청산도 가드·횟수 적용 — 양방향 차단 종목용),count_rejected(가드 통과 시점에 계수),loss_eps(본전을 승으로 보는 1e-9). 각 데몬의 현행 규약을 그대로 옮긴 뒤 바꿀지는 따로 정한다.
판정 축 (v0.6) — "우위가 있는가"를 세 레포가 같은 식으로 잰다
2026-10-04 기준 실주문이 도는 레포가 없다. scalp-it 101번은 현물 일중 스캘핑을 측정으로 닫았다 — 왕복 실비용 41.5bp 를 넘는 우위가 어떤 지평·유니버스·방향에도 없었다. 그 측정(95 천장·98 선택편향· 101 드리프트·매치드 널)이 전부 scalp-it 스크립트에만 있어서 코어로 올렸다. 허위 우위로 실주문을 켜는 사고를 세 레포가 같은 함수로 막는다. 전부 순수 함수고 판정하지 않는다 — 문턱은 각 레포의 사전등록이 쓴다.
from datetime import date
import numpy as np
from krx_quant_core.costs import round_trip_cost
from krx_quant_core.backtest import drift_summary, intraday_overnight, rank_buckets
from krx_quant_core.backtest.lob import ceiling_table
from krx_quant_core.stats import matched_alpha, matched_control, selection_bias_report
# 1. 오라클 천장 — 매도 시점을 사후에 완벽히 골라도 비용을 넘는가 (scalp-it 95)
cost = round_trip_cost(date(2026, 10, 2), "KOSDAQ", slippage_one_way=0.0) # 세금+수수료, 스프레드는 호가가 낸다
t = ceiling_table([(path["bid"], path["ask"]) for path in paths], cost=cost, horizons=(10, 60, 300, 1800))
t[t.kind == "taker"][["horizon", "median_bp", "frac_pos"]] # 10초 테이커 중앙이 음수면 그 자리는 끝
# 2. 선택편향 — 검증 일평균 최대로 고른 체크포인트는 실력 없이 몇 bp 오르나 (scalp-it 98)
rep = selection_bias_report(val_log_rows) # val_n·val_daymean_bp·val_mean_bp 열, pick() 과 같은 필터·동점 처리
rep.premium_vs_random, rep.expected_max_curve # rl92: 후보 24개·sd 58bp → +166bp 는 탐색만으로 나오는 값
# 3. 일중·야간 분해 — 전일 거래대금 상위의 일중은 구조적으로 음수인가 (scalp-it 101 §1)
d = intraday_overnight(daily) # open/close/prev_close → intraday_bp·overnight_bp
d["bucket"] = rank_buckets(d, "turnover_prev", n=4) # 전일 정보로만 버킷 (당일 값이면 look-ahead)
drift_summary(d, "intraday_bp", by="bucket") # mean_bp·neg_month_share(121개월 중 120개월)·t_hac
# 4. 매치드 널 — 신호 뒤 수익이 같은 층(날짜×분×변동성×거래대금)의 무작위보다 큰가 (scalp-it 101 A)
ctl = matched_control(trades, pool, strata=["date", "minute", "vol_q", "turnover_q"], n_per=5)
a = matched_alpha(trades, pool, ctl, value="net_bp", cluster="date") # 날짜 클러스터 부트스트랩
a.alpha, a.ci_low, a.ci_high, a.by_stratum # 사전등록: CI 하한 > 0 이어야 통과
95·98 은 원본 스크립트와 비트 단위로 같다(골든 테스트가 원본 복사본을 대조). through_fill_second 는
numba 커널이고 폴백도 같은 숫자다. cost 는 필수 인자 — 원본의 COST = 0.0023 상수를 박지 않았다.
설계 원칙
- 이식은 수치 동일. 소비 레포가 실매매일에 갈아탈 수 있어야 한다. 포트마다 원본
코드와 무작위 입력으로 대조했다 — 주문 가드 16,000건 사유 문자열 동일, 킬스위치
9,000 스텝 동일, 실제
dart.db공시 26,551건 분류 동일, 통계·횡단면 시뮬 40 시드 NaN 포함 동일. - 판정하지 않는다. 통계 함수는 숫자를 리포트할 뿐
PASS/FAIL을 돌려주지 않는다 (테스트가 강제). 합격선은 사전등록 문서에 있어야 한다. - 모르는 건 모른다고 적는다. 시장가
trde_tp는"03"/"3"둘 중 무엇인지 실호출로 확인된 적이 없어TRDE_TP_MARKET_VERIFIED = False로 둔다. 2025 이전 거래세 단계는 2차 출처만 있다고 docstring 에 적었다. - 토큰은 공유 자원이다. 두 실매매 프로세스가 같은 앱키를 쓴다. 연결 해제 시 토큰을
폐기하면 다른 프로세스가 죽는다(
kiwoom_spec.TOKEN_SHARING_WARNING).
가격제한폭 — 실측 대조
daily_bars(2023-02 이후)에서 고가·저가가 기준가 ±29~31% 인 행에 후보 규칙을 대조했다.
| 규칙 | 규칙끼리 갈리는 행에서 적중 |
|---|---|
| 상한가 = ⌊기준가×1.3 을 그 가격대 틱⌋ | 1,211 |
| 상한가 = 기준가 + ⌊기준가×0.3 을 기준가 틱⌋ | 165 |
| 하한가 = ⌈기준가×0.7 을 그 가격대 틱⌉ (대칭식) | 25 |
| 하한가 = 기준가 − ⌊기준가×0.3 을 기준가 틱⌋ | 105 |
즉 상한가와 하한가는 대칭이 아니다.
거래세 스케줄 (매도 시)
| 시행일 | KOSPI 거래세 + 농특세 | KOSDAQ |
|---|---|---|
| 2026-01-01 | 0.05% + 0.15% = 0.20% | 0.20% |
| 2025-01-01 | 0.00% + 0.15% = 0.15% | 0.15% |
| 2024-01-01 | 0.03% + 0.15% = 0.18% | 0.18% |
| 2023-01-01 | 0.05% + 0.15% = 0.20% | 0.20% |
| 2021-01-01 | 0.08% + 0.15% = 0.23% | 0.23% |
| 2019-06-03 | 0.10% + 0.15% = 0.25% | 0.25% |
2026 개정은 시행령 개정 보도로 확인했고, 그 이전 단계는 2차 출처다. 법령 원문으로 확인되면 이 표를 갱신한다.
개발
uv sync --extra dev
uv sync --extra dev --extra fast # numba 경로까지
uv run pytest -q # 656 tests (extra fast·opt 포함)
uv run ruff check src tests
태그 v* 를 푸시하면 publish.yml 이 PyPI 로 올린다(trusted publishing 설정 후).
로드맵
- v0.6 로 판정 축(
backtest.lob.ceiling·stats.selection·backtest.drift·stats.matched_null)과kqc nightly status·kqc pins가 들어갔다. 다음 순서: scalp-it 100번(분봉 275일 탐색)이 3·4 를 쓰게 → 엔진 A 마무리(두 데몬의 킬·장부를OrderManager로, 실주문이 멈춘 지금이 교체 비용이 가장 싸다) → 101번 B-2(주식선물 기초자산 드리프트)가 양수일 때만 kiwoom-client 선물 모듈·파생 비용 스케줄. - v0.5 로 주문 관리 계층(
execution.oms)·KiwoomBroker/PaperBroker·EngineCore+backtest.replay·research.run_sweep/optuna_search·kqc nightly가 들어갔다(위 "공용 엔진" 참고). 남은 것:poll_fills(ka10076) 모의계좌 실호출 확인, scalp-itRiskGuard킬 판정을KillSwitch로 위임(대조 테스트는 이미 있음), daytrade-it/scalp-it 실주문 경로를 코어 엔진으로 갈아타는 건 수치 동일성 증명이 끝난 부분부터 단계적으로. - ETF 호가단위(kiwoom-client 정본 추가 대기). 대기열 모델(
backtest.lob.queue)은 실주문 체결로 계속 보정할 것. OrderManager재시작 복원(own_orders·분할 청산 누적·킬 상태),execution.Trade/backtest.Trade이름 충돌 정리.
라이선스
Apache-2.0
Metadata
Release files for krx-quant-core 0.6.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| krx_quant_core-0.6.2.tar.gz | 357.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| krx_quant_core-0.6.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 564.2 kB
Release files / krx_quant_core-0.6.2.tar.gz
| Download URL | krx_quant_core-0.6.2.tar.gz |
|---|---|
| Size | 357.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
6a8b53aefbed8c2154f3015bc5d14c32b840218759a2fe32ce4a572b916f732d
|
|
BLAKE2b-256 checksum How to use checksums |
d3bb69af4a2e3c7fdc0e73c2615cfdacf91eb516e9fbbfa4cb8ec2e9ef41442a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency logRelease files / krx_quant_core-0.6.2-py3-none-any.whl
| Download URL | krx_quant_core-0.6.2-py3-none-any.whl |
|---|---|
| Size | 206.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c6350a757443acf47f5ce40c0fd0b1e5b18ff3db9bf8dd4ad73bc7dadb1524bc
|
|
BLAKE2b-256 checksum How to use checksums |
a834563cf9b380e814660534793ff141791fbdd8f72fadd8e3f6d03b5a7a0463
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency log