본문으로 건너뛰기
Life Saver Wiki

[소프트웨어 테스트 + 금융공학] 프로퍼티 기반 테스트로 검증하는 알고리즘 거래 전략의 불변 조건

예제 기반 단위 테스트가 놓친 알고리즘 거래 전략의 포지션 한도 위반을 Hypothesis 프로퍼티 기반 테스트로 탐지한 경험과 적용 방법을 정리합니다.

운영자
Life Saver Wiki

알고리즘 거래 전략의 포지션 계산 모듈을 단위 테스트로 검증하면서, 정상적인 예제 입력에서는 모두 통과했지만 실제 운영에서 포지션 한도를 초과하는 케이스가 발생했습니다. 원인을 분석하니 연속적인 부분 체결이 빠른 속도로 들어올 때 누적 포지션 계산에 경쟁 조건이 있었고, 이 케이스는 수작업으로 작성한 예제 기반 테스트로는 발견할 수 없었습니다. Hypothesis 라이브러리를 이용한 프로퍼티 기반 테스트를 도입한 이후 이 유형의 불변 조건 위반을 자동으로 탐지하게 됐습니다.

알고리즘 거래 전략 검증을 위한 프로퍼티 기반 테스트 코드 화면

배경

예제 기반 테스트의 한계

전통적인 단위 테스트는 개발자가 직접 입력값과 기대 출력값을 작성합니다. 이 방식은 개발자가 생각한 케이스만 커버합니다. 문제는 버그가 주로 개발자가 미처 생각하지 못한 입력 조합에서 발생한다는 점입니다.

알고리즘 거래 전략에서 예제 기반 테스트가 특히 취약한 상황은 다음과 같습니다.

  • 부분 체결(partial fill)이 연속으로 발생하는 엣지 케이스
  • 음수 포지션(공매도)과 롱 포지션이 동시에 존재하는 상태
  • 체결 수량이 0인 이벤트 또는 정수 오버플로우 경계
  • 여러 심볼에 걸친 포지션 합산 시 부동소수점 오차 누적

이러한 케이스를 모두 손으로 작성하는 것은 사실상 불가능합니다. 그리고 운영 환경은 항상 테스트가 다루지 못한 입력 공간을 찾아냅니다.

퀀트 전략의 불변 조건

퀀트 전략에서 **불변 조건(invariant)**은 어떤 입력이 주어지더라도 반드시 참이어야 하는 명제입니다. 예제 기반 테스트가 “이 입력에서 이 출력이 나오는가”를 확인한다면, 프로퍼티 기반 테스트는 “모든 가능한 입력에서 이 성질이 유지되는가”를 확인합니다.

알고리즘 거래 전략에서 대표적인 불변 조건은 다음과 같습니다.

  • 포지션 한도 불변: 어떤 체결 시퀀스가 들어오더라도 누적 포지션은 설정된 한도를 초과하지 않아야 합니다.
  • 거래량 보존: 발주된 수량의 합은 체결된 수량의 합과 미체결 잔량의 합과 일치해야 합니다.
  • 손익 단조성: 수수료와 슬리피지를 반영한 누적 손익은 동일 포지션에서 시작한 두 계산 경로가 동일한 결과를 내야 합니다.
  • 백테스트 룩어헤드 바이어스 부재: 시점 T의 신호 계산에 T+1 이후 데이터가 사용되지 않아야 합니다.

프로퍼티 기반 테스트

핵심 개념

프로퍼티 기반 테스트의 핵심 메커니즘은 두 가지입니다.

자동 입력 생성(Fuzzing): 라이브러리가 정의된 입력 타입에서 무작위 케이스를 자동 생성합니다. 개발자는 입력 공간의 형태(정수, 리스트, 커스텀 도메인 모델 등)만 정의합니다.

Shrinking: 불변 조건을 위반하는 케이스를 발견하면, 라이브러리는 해당 케이스를 가능한 한 작은 형태로 축소합니다. 버그를 재현하는 최소 입력을 자동으로 찾아줍니다. 이 기능이 없으면 복잡한 입력 시퀀스에서 실제 버그 원인을 분리하기 어렵습니다.

Hypothesis 라이브러리

Python 생태계에서 프로퍼티 기반 테스트의 표준은 Hypothesis입니다. @given 데코레이터와 strategies 모듈로 입력 공간을 정의하고, 테스트 본문에서 불변 조건을 assert합니다.

pip install hypothesis

코드 예시: 거래 전략 불변 조건 테스트

from hypothesis import given, settings, assume
from hypothesis import strategies as st
from hypothesis.stateful import RuleBasedStateMachine, rule, invariant
from dataclasses import dataclass, field
from typing import List
import pytest


@dataclass
class Fill:
    symbol: str
    quantity: int   # 양수: 매수, 음수: 매도
    price: float


@dataclass
class PositionTracker:
    position_limit: int
    positions: dict = field(default_factory=dict)
    total_pnl: float = 0.0

    def process_fill(self, fill: Fill) -> bool:
        """체결 처리. 한도 초과 시 False 반환."""
        current = self.positions.get(fill.symbol, 0)
        new_position = current + fill.quantity
        if abs(new_position) > self.position_limit:
            return False
        self.positions[fill.symbol] = new_position
        return True

    def net_position(self, symbol: str) -> int:
        return self.positions.get(symbol, 0)


# 1. 기본 프로퍼티 테스트: 포지션 한도 불변
@given(
    fills=st.lists(
        st.builds(
            Fill,
            symbol=st.sampled_from(["AAPL", "TSLA", "SPY"]),
            quantity=st.integers(min_value=-100, max_value=100).filter(lambda x: x != 0),
            price=st.floats(min_value=1.0, max_value=1000.0, allow_nan=False),
        ),
        min_size=1,
        max_size=50,
    ),
    limit=st.integers(min_value=10, max_value=200),
)
def test_position_limit_invariant(fills: List[Fill], limit: int):
    """어떤 체결 시퀀스가 들어오더라도 포지션은 한도를 초과하지 않아야 합니다."""
    tracker = PositionTracker(position_limit=limit)
    for fill in fills:
        tracker.process_fill(fill)

    for symbol, position in tracker.positions.items():
        assert abs(position) <= limit, (
            f"{symbol} 포지션 {position}이 한도 {limit}을 초과했습니다."
        )


# 2. 거래량 보존 프로퍼티
@given(
    quantities=st.lists(
        st.integers(min_value=-50, max_value=50).filter(lambda x: x != 0),
        min_size=2,
        max_size=30,
    )
)
def test_position_is_sum_of_fills(quantities: List[int]):
    """누적 포지션은 체결 수량의 합과 일치해야 합니다."""
    tracker = PositionTracker(position_limit=10_000)
    symbol = "TEST"
    for qty in quantities:
        tracker.process_fill(Fill(symbol=symbol, quantity=qty, price=100.0))

    expected = sum(quantities)
    actual = tracker.net_position(symbol)
    assert actual == expected, (
        f"누적 포지션 {actual}이 체결 합계 {expected}와 다릅니다."
    )


# 3. 상태 기반 테스트: 복잡한 시퀀스 검증
class TradingStrategyMachine(RuleBasedStateMachine):
    """Hypothesis 상태 기반 테스트로 체결 시퀀스의 불변 조건을 검증합니다."""

    LIMIT = 100

    def __init__(self):
        super().__init__()
        self.tracker = PositionTracker(position_limit=self.LIMIT)
        self.fill_count = 0

    @rule(qty=st.integers(min_value=1, max_value=60))
    def buy(self, qty: int):
        self.tracker.process_fill(Fill("AAPL", qty, 150.0))
        self.fill_count += 1

    @rule(qty=st.integers(min_value=1, max_value=60))
    def sell(self, qty: int):
        self.tracker.process_fill(Fill("AAPL", -qty, 150.0))
        self.fill_count += 1

    @invariant()
    def position_within_limit(self):
        pos = self.tracker.net_position("AAPL")
        assert abs(pos) <= self.LIMIT, (
            f"포지션 한도 위반: {pos} (한도: {self.LIMIT})"
        )


TestTradingStrategy = TradingStrategyMachine.TestCase

위 코드에서 RuleBasedStateMachine은 Hypothesis의 stateful testing 기능입니다. 매수·매도 규칙을 정의하면 Hypothesis가 임의의 순서로 이 규칙들을 조합해 상태 전환 시퀀스를 생성하고, @invariant로 선언한 조건이 모든 중간 상태에서 유지되는지 확인합니다.

금융공학 관점

백테스트 엔진의 불변 조건

백테스트 엔진에서 프로퍼티 기반 테스트가 특히 유용한 영역은 룩어헤드 바이어스(look-ahead bias) 검증입니다. 신호 계산 함수에 미래 데이터가 유입되지 않는다는 조건을 불변 조건으로 표현할 수 있습니다.

예를 들어, 이동평균 계산 함수에 timestamp 파라미터를 추가하고, 해당 함수가 timestamp 이후의 데이터를 절대 참조하지 않는다는 조건을 Hypothesis로 검증합니다. 임의의 타임스탬프 슬라이스를 생성해서 결과가 항상 해당 슬라이스 범위 내 데이터만으로 산출됨을 assert하는 방식입니다.

퀀트 전략 회귀 테스트

전략 로직을 수정할 때 회귀 테스트는 필수입니다. 예제 기반 회귀 테스트는 이전 버전과 새 버전이 동일한 출력을 내는지를 몇 개의 케이스로 확인하지만, 프로퍼티 기반 회귀 테스트는 임의의 입력 공간에서 두 버전이 동일한 불변 조건을 만족하는지를 수백 개 케이스로 확인합니다. 리팩터링 후 숨어 있는 동작 변화를 더 안정적으로 탐지합니다.

트레이드오프

항목예제 기반 테스트프로퍼티 기반 테스트
커버리지개발자가 상상한 케이스만입력 공간을 광범위하게 탐색
버그 탐지력알려진 케이스에 한정예상치 못한 엣지 케이스 탐지
유지보수로직 변경 시 케이스 수동 업데이트불변 조건만 유지하면 됨
실행 시간빠름 (고정 케이스)느림 (수백~수천 케이스 생성)
실패 메시지명확 (예제가 직관적)shrinking 후 명확해지지만 초기엔 복잡
금융 도메인 적합성단순 계산 검증에 적합시퀀스 의존 불변 조건 검증에 필수
도입 난이도낮음중간 (전략 도메인을 불변 조건으로 추상화 필요)

결론

알고리즘 거래 전략의 포지션 한도 위반은 예제 기반 테스트로 찾기 어렵습니다. 위반이 특정 체결 시퀀스의 조합에서만 발생하기 때문입니다. 프로퍼티 기반 테스트는 입력 공간을 자동으로 탐색해 이 조합을 발견하고, shrinking으로 최소 재현 케이스를 제시합니다.

퀀트 전략 검증에서 프로퍼티 기반 테스트를 도입할 때 가장 중요한 것은 전략의 불변 조건을 명확히 정의하는 작업입니다. “이 함수가 올바른 결과를 반환하는가”가 아니라 “이 시스템이 어떤 상황에서도 반드시 지켜야 하는 성질은 무엇인가”를 먼저 도출해야 합니다. 이 질문에 답하는 과정 자체가 전략의 설계를 명확히 하는 부수 효과도 가져옵니다.

예제 기반 테스트와 프로퍼티 기반 테스트는 대체 관계가 아닙니다. 명확한 예제 케이스는 그대로 유지하면서, 불변 조건 검증은 프로퍼티 기반으로 보완하는 것이 실무에서 균형 잡힌 접근입니다.