같은 80점인데 비교할 수 없다고? 다중 전략 주식 점수와 심프슨의 역설
주식 스크리너에 두 종목이 모두 80점으로 표시되어 있다고 해봅시다. 하나는 여러 기술적 신호가 겹친 결과이고, 다른 하나는 추세와 거래량 조건을 만족한 스크리너 프리셋의 결과입니다. 화면에는 같은 점수 열이 있으니 두 종목을 그대로 정렬하고 싶어집니다.
하지만 먼저 물어야 할 질문이 있습니다. 이 두 개의 80점은 과연 같은 대상을 측정한 숫자일까요?
여러 출처의 주식 신호를 모으는 시스템의 개발 기록을 정리하면서, 출처에 따라 계산에 쓸 수 있는 특성이 처음부터 다르다는 점을 다시 확인했습니다. 종합 기술 신호에만 존재하는 항목을 다른 스크리너에서 0점으로 취급하면 그 스크리너는 구조적으로 불리합니다. 그래서 출처별 평가(source-aware scoring)가 필요합니다. 그렇다고 여러 출처가 내놓은 0~100점이 자동으로 같은 통계적 의미를 갖게 되는 것은 아닙니다.
규칙 기반 80점은 5일 뒤 수익이 날 확률 80%가 아닙니다. 출처가 다른 점수를 비교하려면 먼저 미래에 무엇을 성공으로 볼지 고정하고, 실제로 결과가 확정된 관측값을 동일한 조건에서 검증해야 합니다. 데이터가 부족하다면 가짜 정밀도를 가진 통합 랭킹보다 출처별 순위를 유지하는 편이 낫습니다.
출처가 다른 신호에 같은 입력 항목을 강요하면 안 되는 이유
2026년 6월에 작성된 프로젝트 설계 노트에서는 종합 신호의 signal_quality를 resonance_score(기술적 공진 점수)와 technical_rating(기술 평가)으로 구성하고 당시 예시로 70% / 30%의 가중치를 사용했습니다.
반면 스크리너 프리셋은 technical_rating, 이동평균 평가, RSI, MFI, 52주 최고가와의 거리, 조건 충족 횟수 등을 사용할 수 있습니다. 종합 신호 전용 resonance_score는 없을 수 있습니다. 이것을 단순한 결측치로 보고 0점을 주면 스크리너라는 이유만으로 감점됩니다.
| 출처 | 내부 점수가 답하는 질문 | 피해야 할 평가 |
|---|---|---|
| technical_signal | 여러 기술 신호가 서로 뒷받침하는가 | 존재하는 공진 정보를 무시하기 |
| screener_preset | 추세·가격·거래량 조건에 얼마나 맞는가 | 종합 신호 전용 항목이 없다는 이유로 감점하기 |
| factor_strategy | 정의된 팩터 전략 조건을 충족하는가 | 다른 출처의 필드를 필수값으로 요구하기 |
이것은 과거 설계 문서에 담긴 논리이지, 오늘의 운영 코드와 가중치를 확인한 결과가 아닙니다. 출처별 입력 체계가 필요한 이유는 설명하지만 두 출처의 80점이 같은 성공 확률인지는 답하지 못합니다.
A에서 80점은 공진 규칙과의 높은 일치도이고, B에서 80점은 스크리너 조건과의 높은 일치도일 수 있습니다. 표시 눈금이 같다고 사건 확률이 같아지지 않습니다. scikit-learn의 확률 보정 문서는 예측 확률과 실제 발생 빈도의 대응을 독립 데이터로 확인하는 방법을 설명합니다. 임의의 규칙 점수를 그대로 0.80이라는 확률로 읽는 것은 다른 이야기입니다.
그러면 출처별로 80점이었던 사례의 과거 성공률을 합산하면 충분할까요? 여기에도 함정이 있습니다.
심프슨의 역설: 모두 80점인데 전체 비교는 뒤집힌다
시장 잡음을 빼고 집계 방식만 보려고 **의도적으로 만든 80건의 교육용 합성 데이터(Toy Example, 실전 백테스트가 아님)**를 사용하겠습니다. 모든 행의 raw_score는 80이며, 가상의 시장 상태는 상승 추세(trend)와 횡보(range)로 나눕니다. 성공은 미리 정해 놓은 5일 이진 사건 레이블입니다.
| 출처 | 추세 장세 | 횡보 장세 | 전체 합산 |
|---|---|---|---|
| A: technical_signal | 21 / 30 = 70% | 3 / 10 = 30% | 24 / 40 = 60% |
| B: screener_preset | 8 / 10 = 80% | 15 / 30 = 50% | 23 / 40 = 57.5% |
마지막 열만 보면 A가 더 나아 보입니다. 하지만 장세별로 나누면 추세에서는 B가 80%로 A의 70%보다 높고, 횡보에서도 B가 50%로 A의 30%보다 높습니다. A는 긍정 레이블이 쉽게 나타나는 추세 구간의 관측치가 많고, B는 횡보 구간의 관측치가 많습니다. 표본 구성이 다르기 때문에 전체 순위만 뒤집힌 것입니다.
두 출처에 공통으로 추세 50%, 횡보 50%라는 가상 비율을 적용해보겠습니다.
| 출처 | 원래 표본 혼합 비율 | 동일한 50/50 장세 비율 |
|---|---|---|
| A | 60% | 50% |
| B | 57.5% | 65% |
이번에는 B가 더 높습니다. 이것이 **심프슨의 역설(Simpson’s paradox)**에서 나타나는 대표적인 집계 문제입니다. 50/50 비율은 설명을 위한 선택일 뿐 실제 시장 비중을 측정한 값이 아닙니다. 실전 연구라면 결과를 보기 전에 비교할 모집단과 가중치를 정해야 합니다.
여기서 단순한 나눗셈보다 유용한 자동 점검은 작은 표본에서 높은 성공률을 얼마나 신뢰할 수 있는지입니다. B의 추세 그룹은 10건 중 8건, 즉 80%입니다. 독립적인 이진 관측이라는 가정 아래 계산한 **95% Wilson 신뢰구간 하한은 약 49.0%**에 불과합니다.
import math
def wilson_lower_bound(wins: int, n: int, z: float = 1.96) -> float:
if n <= 0 or not 0 <= wins <= n:
raise ValueError("invalid wins or sample size")
p = wins / n
denom = 1 + z*z/n
center = p + z*z/(2*n)
margin = z * math.sqrt(p*(1-p)/n + z*z/(4*n*n))
return (center - margin) / denom
print(f"8/10 = 80%; Wilson 下界 = {wilson_lower_bound(8, 10):.1%}")
# 8/10 = 80%; Wilson 下界 = 49.0%
함수에서 n=0은 의도적으로 오류를 발생시킵니다. 관측이 없다는 것과 성공률이 0%라고 검증됐다는 것은 다릅니다. Wilson 하한은 실제 거래에서 보장되는 보수적 승률도 아닙니다. 같은 종목이 여러 번 나오거나 5일 선행 수익 구간이 겹치면 관측의 독립성 가정이 깨지므로 상관관계를 고려해야 합니다.
80건 합성 데이터와 네 가지 assert를 포함한 전체 재현 Python 스크립트
#!/usr/bin/env python3
"""Deterministic synthetic check of group-conditional scoring; NOT a market backtest.
All labels below are intentionally constructed. There are no real stock prices,
no actual trading outcomes, and no production scoring-engine output.
"""
import json
import math
from collections import defaultdict
# Fixed illustrative counts: (source, hypothetical regime, observations, positive outcomes)
SPEC = [
("technical_signal", "trend", 30, 21),
("technical_signal", "range", 10, 3),
("screener_preset", "trend", 10, 8),
("screener_preset", "range", 30, 15),
]
def wilson_interval(positive, total, z=1.959963984540054):
if total < 1:
raise ValueError("empty group")
p = positive / total
denom = 1 + z*z/total
center = (p + z*z/(2*total)) / denom
half = z * math.sqrt(p*(1-p)/total + z*z/(4*total*total)) / denom
return [round(center-half, 4), round(center+half, 4)]
def make_group(total, positive):
return {"n": total, "positive": positive, "rate": round(positive / total, 4),
"wilson_95": wilson_interval(positive, total)}
def main():
rows = []
for source, regime, total, positive in SPEC:
rows.extend({"source": source, "regime": regime, "raw_score": 80,
"positive_event_5d": int(i < positive)} for i in range(total))
summaries = defaultdict(dict)
pooled = defaultdict(lambda: [0, 0])
for source, regime, n, yes in SPEC:
summaries[source][regime] = make_group(n, yes)
pooled[source][0] += n
pooled[source][1] += yes
result = {"scope": "CONSTRUCTED_SYNTHETIC_EXAMPLE_NOT_MARKET_DATA",
"event_definition": "binary illustrative positive_event_5d, not executed trade PnL",
"raw_score": 80, "total_rows": len(rows), "sources": {}}
for source in sorted(summaries):
trend = summaries[source]["trend"]["rate"]
range_ = summaries[source]["range"]["rate"]
n, yes = pooled[source]
result["sources"][source] = {
"by_regime": summaries[source], "pooled": make_group(n, yes),
"equal_regime_mix_rate": round((trend + range_) / 2, 4)
}
a = result["sources"]["technical_signal"]
b = result["sources"]["screener_preset"]
assert len(rows) == 80
assert a["pooled"]["rate"] == 0.6 and b["pooled"]["rate"] == 0.575
assert a["equal_regime_mix_rate"] == 0.5 and b["equal_regime_mix_rate"] == 0.65
assert all(b["by_regime"][r]["rate"] > a["by_regime"][r]["rate"] for r in ("trend", "range"))
result["test_assertions"] = {
"fixed_80_records": "PASS", "pooled_order_a_above_b": "PASS",
"within_each_regime_b_above_a": "PASS", "fixed_equal_mix_order_b_above_a": "PASS"
}
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
if __name__ == "__main__":
main()
전체 Python 표준 라이브러리 코드는 80개의 합성 레코드를 만들고, 출처·장세별 비율과 전체 비율, Wilson 구간을 출력한 뒤 네 개의 assert로 역전을 검증합니다. 확인되는 것은 통계 집계 구조의 함정이지 실제 전략의 수익 우위가 아닙니다.
실제 연구에는 동일 종목이 같은 날 여러 출처를 만족하거나 t일과 t+1일의 5일 선행 수익이 겹치는 문제도 있습니다. 관측 행 수가 늘었다고 독립적인 정보량이 같은 비율로 증가하지 않습니다.
확률을 보정하기 전에 성공 사건부터 고정하자
처음부터 로지스틱 보정과 Isotonic Regression 중 하나를 고를 필요는 없습니다. 먼저 무엇을 ‘성공’으로 기록할지 정의해야 합니다.
‘5거래일 뒤 주가 상승’, ‘5일 뒤 벤치마크 수익률 초과’, ‘실제로 체결 가능하고 비용을 뺀 후 순이익 발생’은 각각 다른 사건입니다. 1일과 10일 레이블을 한 가지 확률로 섞을 수도 없습니다.
좁은 연구 기준의 예로, 특정 시장과 종가 기준 의사결정 시점을 고정한 후 5거래일이 지난 시점의 수익률이 명시한 비용 가정 아래 0보다 큰지를 이진 레이블로 삼을 수 있습니다. 결과가 확정되기 전에는 미결로 남겨야 합니다. 실제 체결가가 없다면 가격 기반 proxy일 뿐 실현손익이라고 부르면 안 됩니다.
의사결정 당시의 값을 다음과 같이 변경 불가능한 스냅샷으로 남기는 것이 유용합니다.
decision_id / symbol / trade_date / as_of_time
source_key / raw_score / model_version
market / session / regime
horizon_days / label_status / realized_outcome
source_key는 점수 산출 규칙을, model_version은 규칙과 가중치의 버전을 식별합니다. as_of_time은 각 입력이 의사결정 시점에 이용 가능했는지 확인하는 경계입니다. 몇 달 뒤 수정된 데이터와 최신 코드로 과거 점수를 다시 계산하면 당시 실제로 보였던 신호가 아니라 다른 대상을 검증하게 됩니다.
동일 종목·동일 날짜에 두 출처가 모두 반응한 경우, 출처 귀속을 연구하기 위해 두 행을 남길 수 있습니다. 하지만 통계 신뢰구간이나 포트폴리오 손익에서는 서로 독립적인 두 거래처럼 취급할 수 없습니다. 종목과 날짜의 중복 및 그룹 간 상관을 처리해야 합니다.
pending outcome도 중요합니다. 5거래일이 아직 지나지 않은 신호는 실패도 아니고 수익률 0도 아닙니다. 두 출처의 레이블 확정 비율이 다른데 completed 기록만 모으면 성숙도 선택 편향이 생길 수 있습니다. 종목 유니버스, 상장폐지, 재무 정보 발표 지연, 수정주가 정의, 유동성 조건 역시 비교 대상 자체를 바꿉니다.
점수 보정 모델에 앞서 이런 데이터 계약부터 정리해야 합니다.
순위 예측, 확률 보정, 기대손익은 다른 검증 문제다
먼저 출처·시장·장세·기간별로 raw_score를 구간화하고, 각 구간의 표본 수, 레이블 확정률, 이벤트 비율, 수익률 분포와 불확실성을 함께 봅니다. 관측이 적다면 출처별 순위를 유지하거나 shrinkage가 들어간 계층적 추정을 고려하는 편이, source × regime × session별로 복잡한 보정기를 독립적으로 학습하는 것보다 안전할 수 있습니다.
충분한 확정 데이터가 있을 때는 단순한 Sigmoid 보정과 더 유연한 비모수 방식을 비교할 수 있습니다. 중요한 것은 보정에 사용한 관측을 같은 보정기의 미래 성능 증거로 다시 쓰지 않는 것입니다. scikit-learn 확률 보정 가이드의 reliability diagram은 예측 확률 구간별 평균 예측과 실제 긍정 레이블 비율을 비교합니다. Brier Score 역시 보정성뿐 아니라 구분 능력과 사건 자체의 불확실성에도 영향을 받으므로 점수 개선이 바로 실전 수익 개선을 뜻하지 않습니다.
시계열에서는 미래 정보 누출도 막아야 합니다. t일 신호에 대한 5일 결과는 t+5일이 되어야 확정됩니다. 학습 데이터의 신호 날짜가 검증 구간 이전이라고 해도, 그 레이블에 검증 구간 중의 가격이 포함되면 미래를 미리 본 셈입니다. TimeSeriesSplit은 시간 순서 분리와 gap을 지원하지만 gap=5를 기계적으로 넣는 것이 항상 올바른 purge/embargo는 아닙니다. 레이블 성숙 시점, 거래일 캘린더, 종목별 중첩을 함께 다뤄야 합니다.
| 연구 질문 | 확인할 증거 |
|---|---|
| 높은 점수가 미래 성과가 좋은 후보를 위에 놓는가? | 시간 외 순위 지표, Top/Rest의 선행 수익률 분포 |
| 예측 60%인 사건이 실제로 약 60% 발생하는가? | 시간 외 reliability 구간, 표본 수, 출처별 안정성, 확률 오차 |
랭킹이 잘되는 점수가 확률로도 보정돼 있다는 보장은 없습니다. 반대로 기준 발생률만 맞히는 확률 모델은 종목을 구분하는 힘이 거의 없을 수 있습니다. Google의 prediction bias 설명은 평균 예측과 평균 실제값의 차이를 간단히 점검할 때 도움이 되지만, 모든 구간과 출처의 보정 수준을 보장하지는 않습니다.
마지막으로 확률은 수익이 아닙니다. 5일 뒤 수익률이 양수일 확률이 정확하더라도 이익과 손실의 크기, 체결 슬리피지, 낙폭, 포지션 규모, 실제 기대수익은 별도입니다. 높은 적중률과 나쁜 손익비는 동시에 존재할 수 있습니다.
진짜 비교 증거가 쌓이기 전까지는 출처별 원점수를 유지하자
저라면 지금 당장 모든 출처에 하나의 통합 점수를 부여하지 않겠습니다. 공진 신호는 공진 정보를, 스크리너는 자신의 조건을 평가하는 것이 합리적입니다. 모자란 것은 두 출처의 점수를 서로 비교할 수 있다는 증거입니다.
다음 단계의 읽기 전용 분석에서는 실제로 결과가 확정된 일반 운영 배치만 골라 수동 테스트를 제외하고, source_key와 model_version, 시장, session, horizon을 고정할 것입니다. 점수 구간과 장세에 따라 사건 비율, 수익률 중앙값, 큰 손실, 종목 중복, 자료 범위 및 확정도를 본 다음, 시간 외 구간에서 확률 매핑을 평가하겠습니다.
이 질문은 앞서 작성한 Bucket Spread와 Rank IC 팩터 검증 및 Regime·Shadow·가중치 변경 검증과 이어집니다. 첫 번째가 팩터의 순위 정보량을, 세 번째가 가중치를 운영에 적용할 준비를 묻는다면 이번 글은 중간 단계입니다. 서로 다른 전략이 말하는 ‘강한 신호’를 같은 축에서 비교해도 되는가?
같은 정의로 결산된 시간 외 outcome을 확인하기 전에는 원점수와 출처를 함께 보여주는 것이, 근거 없이 정밀한 ‘통합 승률’을 계산해 표시하는 것보다 낫습니다.