Skip to main content

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.0
# 초 격자 호가 리플레이(backtest.lob)를 numba 로 가속하려면 extra 로: "krx-quant-core[fast]==0.6.0"
# optuna 스윕(research.optuna_search)까지 쓰려면: "krx-quant-core[fast,opt]==0.6.0"

# PyPI 릴리스 전(또는 태그 고정 개발 중)에는 git 태그로:
pip install "krx-quant-core @ git+https://github.com/younghwan91/krx-quant-core@v0.6.0"

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 자동) + Postgres kqc_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() 은 절대 주문을 내지 않는다. 장부 vs broker.holdings() 차이를 보고만 한다 — 어느 쪽이 맞는지는 사람이 판단한다.

판정 축 (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 상수를 박지 않았다.

설계 원칙

  1. 이식은 수치 동일. 소비 레포가 실매매일에 갈아탈 수 있어야 한다. 포트마다 원본 코드와 무작위 입력으로 대조했다 — 주문 가드 16,000건 사유 문자열 동일, 킬스위치 9,000 스텝 동일, 실제 dart.db 공시 26,551건 분류 동일, 통계·횡단면 시뮬 40 시드 NaN 포함 동일.
  2. 판정하지 않는다. 통계 함수는 숫자를 리포트할 뿐 PASS/FAIL 을 돌려주지 않는다 (테스트가 강제). 합격선은 사전등록 문서에 있어야 한다.
  3. 모르는 건 모른다고 적는다. 시장가 trde_tp 는 "03"/"3" 둘 중 무엇인지 실호출로 확인된 적이 없어 TRDE_TP_MARKET_VERIFIED = False 로 둔다. 2025 이전 거래세 단계는 2차 출처만 있다고 docstring 에 적었다.
  4. 토큰은 공유 자원이다. 두 실매매 프로세스가 같은 앱키를 쓴다. 연결 해제 시 토큰을 폐기하면 다른 프로세스가 죽는다(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-it RiskGuard 킬 판정을 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.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for krx-quant-core 0.6.0
File Size Uploaded
krx_quant_core-0.6.0.tar.gz 354.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for krx-quant-core 0.6.0
File Interpreter ABI Platform
krx_quant_core-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 559.5 kB

Release files / krx_quant_core-0.6.0.tar.gz

Download URL krx_quant_core-0.6.0.tar.gz
Size 354.5 kB
Tags Source
SHA-256 checksum
How to use checksums
bb23b53beb1fe0d8bffdda308cfc98a25a7ba87c9997722bb32516335f7d3872
BLAKE2b-256 checksum
How to use checksums
e7f76a08ba724cd1d787e3f6b363009012a87fba5236ac9bb815aa2b4d7632c1
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

Release files / krx_quant_core-0.6.0-py3-none-any.whl

Download URL krx_quant_core-0.6.0-py3-none-any.whl
Size 205.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
be2d3389c83a97c031a7311ac7ccbe77dc0c113b161d433e217d5cae417ca147
BLAKE2b-256 checksum
How to use checksums
719ae9fe833de12798a32a1f927032a9c8aea556d2e72837a4aefc55c2fd46b4
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

Release history Release notifications | RSS feed

0.6.2

2 release files

0.6.1

2 release files

This release

0.6.0 This release

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page