실시간 주문 스트림에서 시세조종 패턴(스푸핑·레이어링·워시트레이딩) 을 탐지하는 시스템입니다. 합성 데이터에 조작 패턴을 직접 주입해 정답(ground truth)을 확보한 상태에서 탐지 모델을 정밀도·재현율로 엄밀하게 평가하는 것이 핵심입니다.
⚠️ 이 프로젝트는 100% 합성(synthetic) 데이터만 사용합니다. 실제 종목·거래소·투자자·실거래 데이터와 무관합니다. 코스콤이 KRX를 위해 운영하는 시장감시시스템(CAMS)을 "그대로 재현"한 것이 아니라, 시세조종 탐지의 핵심 원리와 ML 평가 방법론을 직접 구현해 본 교육·포트폴리오 목적의 프로젝트입니다.
🔗 라이브 데모: https://market-surveillance-m32jbrixpqvrfxy2wf6e2j.streamlit.app
대부분의 학부 ML 프로젝트는 "정확도 95%"로 끝나지만 그 숫자가 신뢰할 만한지 검증할 방법이 없습니다. 이 프로젝트는 조작 패턴을 합성 데이터에 직접 주입하므로 어떤 주문이 조작인지 정답을 알고 있고, 따라서 정밀도·재현율·혼동행렬이 진짜 의미를 갖습니다. 이 프로젝트의 차별점은 화려한 모델이 아니라 "평가 방법론의 엄밀함" 입니다.
| 패턴 | 정의 | 기계적 신호(피처) |
|---|---|---|
| 스푸핑(Spoofing) | 대량 주문으로 호가를 유인한 뒤 체결 직전 취소 | 비정상적으로 높은 취소율 + 큰 주문량 |
| 워시트레이딩(Wash Trading) | 자기매매로 허위 거래량 생성 | 동일 계좌 간 자기체결(self-trade) 반복 |
| 레이어링(Layering) | 여러 가격 레벨에 주문을 층층이 쌓아 호가창 왜곡 | 다중 레벨 동시 주문 밀도 (Phase 2) |
핵심 모듈 (src/surveillance/):
generator/— 데이터 모델(events.py), 경량 매칭엔진(orderbook.py), 정상 흐름(normal_flow.py), 패턴 주입(injectors.py), 오케스트레이션(dataset.py)features/— (계좌, 시간윈도우) 단위 행동 피처detection/— 룰 기반 탐지(rules.py) + 평가(evaluate.py)
$ python scripts/run_phase1.py
생성 이벤트: 40,548 (NEW=17,626, TRADE=14,341, CANCEL=8,581) | 조작 주문 라벨: 136
========================================================
탐지 평가 리포트 (단위: 계좌 × 시간윈도우)
========================================================
표본 수: 736 | 실제 조작: 16 (2.17%) ← 불균형
혼동행렬 (조작 탐지 = positive)
예측:조작 예측:정상
실제:조작 TP=16 FN=0
실제:정상 FP=0 TN=720
정밀도(Precision): 1.000 (탐지한 것 중 진짜 조작 비율 → 오탐 억제)
재현율(Recall): 1.000 (진짜 조작 중 탐지 비율 → 미탐 억제)
F1: 1.000
Accuracy: 1.000 ← 불균형이라 단독 지표로 부적절
========================================================
Phase 1(MVP)의 룰 기반 탐지는 깨끗하게 주입된 패턴을 완벽히 분리합니다. 핵심 산출물은 이 숫자 자체가 아니라, 정답을 알고 있으므로 이 숫자를 신뢰할 수 있다는 점입니다.
깨끗한 패턴은 룰로도 완벽하지만, 조작이 정상거래를 섞고 강도를 낮추면(위장 모드) 고정 임계값 룰은 무너집니다. 같은 데이터를 시간 분할(과거 학습 → 미래 탐지)로 평가한 결과:
$ python scripts/run_phase2.py
방법 정밀도 재현율 F1 (TP/FP/FN)
------------------------------------------------------------
룰 기반 1.000 0.286 0.444 (6/0/15) ← 위장에 약화된 조작 15건 미탐
ML 1.000 1.000 1.000 (21/0/0) ← 다중 피처 조합으로 잡아냄
이 격차(룰 재현율 0.29 vs ML 1.00)가 "왜 룰이 아니라 ML인가"의 직접 증거입니다. Phase 3에서는 정답을 알고 있다는 강점으로 "어떤 강도의 조작을 놓치고, 얼마나 늦게 잡고, 무엇을 오탐하는가"까지 분해합니다(§7).
조작 패턴을 합성 데이터에 직접 주입하고 각 조작 주문에 order_id 기준 라벨을 부착합니다. 평가
시 이 라벨을 (계좌, 시간윈도우) 단위로 집계해 탐지 결과와 정합 비교합니다. 정답을 알기 때문에
혼동행렬·정밀도·재현율이 추정이 아니라 사실입니다.
ground truth 라벨을 OrderEvent에 넣지 않고 별도의 GroundTruthLabel 테이블로 물리적으로
분리했습니다. 피처 엔지니어링은 라벨이 없는 이벤트 스트림만 입력으로 받으므로, 라벨이 피처로
새어 들어가는 누설이 타입 수준에서 불가능합니다. (정답이 입력에 섞이면 평가가 무의미해집니다.)
조작은 전체의 극소수입니다(데모에서 2.17%). "전부 정상"이라고만 예측해도 accuracy는 97%를 넘지만 조작은 하나도 못 잡습니다(recall 0). 그래서:
- 정밀도(Precision) = 탐지한 것 중 진짜 조작 비율 → 오탐(false positive) 억제가 중요한 이유
- 재현율(Recall) = 진짜 조작 중 탐지한 비율 → 미탐(false negative) 억제가 중요한 이유
확률적 근사 대신 가격-시간 우선순위 호가창을 실제로 유지합니다. 덕분에 취소율·자기체결· 주문량 z-score 같은 피처가 실제 시장 미시구조에서 도출됩니다.
실제 거래소처럼 정상 참여자의 자기체결을 매칭엔진이 차단합니다(STP). 따라서 정상 흐름에는
자기체결이 0건이고, 자기체결은 오직 워시트레이딩에서만 발생합니다 — self_trade_count가 깨끗한
탐지 신호가 됩니다. 워시트레이딩은 의도적으로 STP를 끄고 자기체결을 만듭니다(이것이 곧 악용 방식).
단일 RNG 시드로 전체 데이터셋이 완전히 재현됩니다. 평가의 엄밀성은 재현 가능한 데이터 위에서만 의미를 갖습니다.
ML 분류기는 (계좌, 윈도우) 표본을 윈도우 시간 순서로 분할해 앞 구간으로 학습하고 뒤 구간으로
평가합니다. 무작위 분할 대신 시간 분할을 쓰는 이유는 "미래 정보로 과거를 맞히는" 누설을 막고,
실제 운영(과거로 학습 → 미래 탐지)을 모사하기 위해서입니다. 모델 입력 피처에서 account_id·
window를 제외해 식별자 암기도 차단합니다.
룰 기반은 해석 가능하고 각 임계값이 어떤 조작 메커니즘을 겨냥하는지 명확합니다(스푸핑=초대량+취소, 레이어링=다수 레벨+낮은 체결, 워시=반복 자기체결). 이를 기준선으로 두고 ML(RandomForest)과 동일 test 구간에서 정밀도·재현율을 비교하며, ROC·PR 커브로 임계값 트레이드오프를 시각화합니다.
이 프로젝트의 차별점은 스트리밍 인프라가 아니라 탐지 모델의 평가 엄밀함입니다. Kafka를 추가하면 프로듀서·컨슈머 운영 복잡도는 올라가지만, ground truth 주입·정밀도/재현율 평가라는 핵심 메시지에는 아무것도 더해지지 않습니다. 실제 프로덕션이라면 Kafka 같은 메시지 큐를 붙이겠지만, 포트폴리오 범위에서는 인메모리 스트림으로 탐지 로직을 완전히 검증할 수 있다고 판단해 의도적으로 스코프 밖에 뒀습니다. 이벤트 스트림 인터페이스는 Kafka 컨슈머로 교체하기 쉬운 구조로 설계돼 있습니다.
- Python 3.12, numpy, pandas
- scikit-learn (룰 기반 → ML 분류기)
- Streamlit + Plotly (대시보드)
- pytest
pip install -r requirements.txt
pytest # 전체 테스트 (34개)
python scripts/run_phase1.py # Phase 1: 인메모리 룰 기반 탐지/평가
python scripts/run_phase2.py # Phase 2: 3종 패턴 + ML vs 룰 + ROC/PR (artifacts/ 저장)
python scripts/run_phase3.py # Phase 3: 패턴×강도 분해 + 탐지 지연 + 오탐 분석
streamlit run src/surveillance/app.py # 대시보드: 주문흐름 + 탐지 알림 + 평가 지표- Phase 1 (완료) — 합성 생성기 + 스푸핑·워시 주입(ground truth), (계좌×윈도우) 피처, 룰 기반 탐지, 정밀도/재현율/혼동행렬 평가, 인메모리 스트림
- Phase 2 (완료) — ✅ 레이어링 추가(3종), ✅ 정교한 피처(다중레벨 밀도·주문체결비율), ✅ ML 분류기 + 룰 대비(시간분할), ✅ ROC/PR·임계값 트레이드오프 시각화, ✅ Streamlit 대시보드
- Phase 3 (완료) — ✅ 패턴별 탐지 정확도 분해, ✅ 강도(약/강)별 탐지율, ✅ 탐지 지연 측정, ✅ 오탐 케이스 분석
정답을 알고 있으므로 "어디서 놓치고/늦고/헛짚는가"를 정량화할 수 있다(scripts/run_phase3.py,
에피소드 75개 기준):
-
강도별 탐지율: 고정 임계값은 강한 조작은 잘 잡지만 약한 조작은 놓친다 — 임계값이 곧 정밀도·재현율의 경계선임을 직접 보여준다.
패턴 약한 조작(≤중앙값) 강한 조작(>중앙값) 레이어링 0.231 1.000 스푸핑 0.500 0.909 워시트레이딩 0.357 1.000 -
오탐 비용: 약한 조작까지 잡으려 임계값을 낮추면 오탐이 730건 폭증한다. 오탐의 정체는 활발한 정상 계좌(체결 ≈39건인데 취소·다레벨 주문도 많은 계좌)였다 → 재현율을 올리면 정밀도가 떨어지는 트레이드오프를 ground truth로 확인.
-
탐지 지연: 패턴 발생부터 최초 탐지까지 중앙값 252ms(윈도우 단위 집계의 구조적 지연).
조작 계좌가 정상 거래를 섞고(camouflage) 강도를 약화시키면, 고정 임계값 룰의 재현율은 ~0.3까지
급락합니다(놓친 조작 多). 반면 시간 분할로 학습한 ML은 다중 피처 조합으로 약화된 신호까지
잡아내 재현율을 유지합니다. 이 격차가 "왜 룰이 아니라(또는 룰에 더해) ML인가"에 대한 직접적인
근거입니다. python scripts/run_phase2.py가 동일 test 구간에서 두 방법을 비교합니다.
정직한 기록: 본 합성 데이터에서는 조작이 구조적 흔적(일방향·전량취소 주문 등)을 남겨 ML의 분별력이 매우 높습니다(AUC≈1.0). 즉 이 데이터셋에서 어려운 부분은 분류 가능성이 아니라 고정 룰의 취약성에 있습니다. 실제 시장 수준의 모호한 케이스(완전 위장)는 Phase 3에서 더 다룹니다.
이 프로젝트에서 가장 신경 쓴 건 모델 성능이 아니라 "이 숫자를 믿을 수 있는가" 였습니다. 그 원칙이 흔들렸던 순간들과, 거기서 내린 판단을 기록합니다.
초기 워시트레이딩 정밀도가 0.062까지 떨어졌습니다. 원인을 추적해 보니 정상 공격 주문이
호가창을 깊게 쓸면서(sweep) 같은 계좌의 잔량과 우연히 자기체결을 만들고 있었습니다 — 워시의
핵심 신호인 self_trade_count가 정상 흐름에서도 오염된 것입니다. 단순히 임계값을 올려 덮는 대신,
실제 거래소처럼 self-trade prevention(STP)을 매칭엔진에 설계해 넣었습니다. 정상 참여자의
자기체결을 구조적으로 차단하니 자기체결은 오직 워시에서만 발생하는 깨끗한 신호가 됐습니다.
데이터를 만들다 만난 버그가, 실제 시장 미시구조를 이해하고 구현하는 계기가 됐습니다.
레이어링은 "여러 가격 레벨에 주문을 쌓는다"는 정의라 distinct_price_levels로 잡으면 될 줄
알았는데, 정밀도가 0.205에 그쳤습니다. 분석해 보니 활발한 정상 계좌가 오히려 레벨 수가 더
많았습니다(레이어링 7 vs 정상 12–27). 직관이 틀린 것입니다. 결국 "레이어링 주문은 거의 체결되지
않는다"는 성질에서 order_to_trade_ratio를 찾아내 분리에 성공했습니다(레이어링 OTR↑↑ vs 정상 ~0.6).
"그럴듯한 피처"와 "실제로 분리되는 피처"는 다르며, 그 차이는 데이터를 봐야만 알 수 있다는 교훈.
ML의 ROC AUC가 ≈1.0으로 나왔습니다. "너무 쉬워 보인다"는 이유로 라벨에 노이즈를 섞어 숫자를 어렵게 만들 수도 있었지만, 그건 이 프로젝트의 핵심 원칙(평가 엄밀성)을 스스로 훼손하는 일이라 하지 않았습니다. 대신 "이 데이터에서 어려운 건 분류 가능성이 아니라 고정 룰의 취약성" 이라고 정직하게 프레이밍하고, 위장 모드에서 룰 재현율이 ~0.3으로 무너지는 격차를 ML의 근거로 삼았습니다. 보기 좋은 숫자보다 정직한 서사가 평가 방법론 프로젝트의 본질에 맞습니다.
Phase 3에서 "강한 조작은 잡고 약한 조작은 놓친다"를 보이려 했는데, 위장 모드가 켜져 있으니 강도와 무관하게 신호가 희석돼 강한 조작이 오히려 덜 잡히는 역전이 생겼습니다. 위장(신호 희석)과 강도(임계값 교차)라는 두 변수가 섞인 탓입니다. Phase 3는 위장을 끄고 강도만 변화시켜 두 효과를 분리했고, 그제서야 강도별 재현율(강함 ≈0.9–1.0 vs 약함 ≈0.2–0.5)이 고정 임계값의 한계를 깨끗이 보여줬습니다.
- 절대 카운트 피처가 비율 피처보다 위장에 강건하다. 자기체결 수(절대 카운트) 기반 워시 탐지는 정상거래를 섞어도 버텼지만, 취소율(비율) 기반 스푸핑/레이어링 탐지는 거래량 희석에 취약했습니다.
- 단일 정확도(accuracy)가 아니라 정밀도·재현율·혼동행렬·ROC/PR로 봐야 하는 이유를, 불균형 데이터(조작 ≈2–3%)와 Phase 3의 트레이드오프 실증으로 직접 보였습니다.
- 면접 대비: ground truth 확보 방법, 라벨 누설 구조적 차단(별도 테이블), accuracy를 피한 이유, 패턴별 피처 설계 근거(스푸핑=주문량 z-score, 레이어링=다중 레벨+낮은 체결, 워시=자기체결), 룰 vs ML 선택, lookahead 방지(시간 분할), 그리고 위 네 가지 디버깅 서사까지 설명 가능합니다.
