자동매매 여러 전략 동시 운영 — 자본 배분과 신호 충돌 방지 설계
전략 하나로는 만족이 안 되는 시점이 온다
전략 하나를 만들고 백테스팅과 실전을 거치다 보면 자연스럽게 다음 질문이 따라온다. "이 전략은 특정 국면에서만 잘 먹히는데, 다른 국면을 커버할 전략을 하나 더 붙이면 어떨까." 추세추종 전략 하나만 돌리다가 횡보장에서 계속 손실을 보면, 평균회귀 전략을 하나 더 추가하고 싶어지는 식이다.
문제는 여기서부터다. 전략이 하나일 때는 "이 계좌의 자금을 이 전략이 다 쓴다"는 전제가 자동으로 성립했다. 전략이 둘 이상이 되는 순간 그 전제가 깨진다. 각 전략이 얼마씩 쓸 수 있는지, 두 전략이 같은 종목을 동시에 건드리면 무슨 일이 벌어지는지를 새로 설계해야 한다. 이 글은 그 설계, 즉 자본 배분과 신호 충돌 방지를 다룬다.
구체적인 배분 비율이나 실제 운영 중인 전략 조합은 밝히지 않는다. 여러 전략을 같은 계좌에서 굴릴 때 반드시 세워야 하는 구조를 개념과 의사코드 수준에서 정리한다.
전략별 자본을 어떻게 나누는가 — 고정 배분과 성과 기반 배분
여러 전략을 한 계좌에서 돌린다는 건, 전체 자본을 전략 단위로 쪼개서 각 전략에게 "너는 이 범위 안에서만 매매해라"라고 선을 긋는다는 뜻이다. 이 선이 없으면 전략 A가 오늘 낸 신호와 전략 B가 오늘 낸 신호가 같은 잔고를 두고 경쟁하게 된다.
가장 단순한 방법은 고정 배분이다. 전략마다 전체 자본의 일정 비율을 미리 정해두고, 그 비율만큼의 하위 계좌가 있는 것처럼 다룬다.
python 코드 보기
def allocate_fixed(total_equity: float, strategy_weights: dict) -> dict:
"""전략별 고정 비율로 자본을 나눈다. strategy_weights 합은 1.0 이하."""
return {
strategy_id: total_equity * weight
for strategy_id, weight in strategy_weights.items()
}고정 배분은 단순하고 예측 가능하지만, 성과가 나쁜 전략에도 계속 같은 비율의 자본이 묶인다는 단점이 있다. 이를 보완하는 게 성과 기반 배분이다. 일정 주기마다 최근 성과를 반영해 배분 비율 자체를 조정하는 방식이다.
python 코드 보기
def allocate_by_performance(total_equity: float, strategy_scores: dict, min_weight: float) -> dict:
"""최근 성과 점수(score)가 높을수록 더 많은 자본을 배정하되, 최소 배분(min_weight)은 보장한다."""
total_score = sum(max(score, 0) for score in strategy_scores.values())
allocations = {}
for strategy_id, score in strategy_scores.items():
raw_weight = (max(score, 0) / total_score) if total_score > 0 else 0
weight = max(raw_weight, min_weight)
allocations[strategy_id] = total_equity * weight
return allocations여기서 중요한 건 min_weight로 최소 배분을 보장한다는 점이다. 성과가 일시적으로 나쁘다고 자본을 0으로 만들어버리면, 그 전략이 다시 좋은 국면을 만나도 자본이 없어 기회를 못 잡는다. 성과 기반 배분은 비중을 조절하는 것이지, 특정 전략을 완전히 퇴출시키는 로직이 아니다. 포트폴리오 리밸런싱 로직 설계에서 다룬 "목표비중과 허용오차" 개념을 전략 단위로 옮겨온 것과 비슷하다고 보면 된다 — 종목 대신 전략이 리밸런싱 대상이 되는 셈이다.
같은 종목에 신호가 겹칠 때 — 충돌의 두 가지 형태
전략별 자본을 나눴어도 충돌은 남는다. 자본은 전략마다 분리했지만, 매매 대상 종목까지 분리되는 건 아니기 때문이다. 전략 A와 전략 B가 둘 다 같은 종목을 다룬다면 충돌이 생길 수 있다.
충돌은 크게 두 형태로 나타난다.
첫째, 중복 매수다. 전략 A와 전략 B가 같은 종목에 대해 동시에 매수 신호를 낸다. 각자의 배분 자본 안에서는 정상적인 주문이지만, 합쳐놓고 보면 그 종목 하나에 예상보다 훨씬 많은 자금이 쏠린다.
둘째, 상반된 신호다. 전략 A는 매수를, 전략 B는 매도를 같은 종목에 대해 동시에 낸다. 둘 다 그대로 실행하면 사실상 제로섬으로 수수료만 나가는 매매가 되거나, 실행 순서에 따라 원치 않는 포지션이 만들어진다.
이 두 형태를 막으려면 두 가지 장치가 필요하다. 하나는 우선순위, 하나는 락(lock)이다.
python 코드 보기
def resolve_signal_conflict(symbol: str, pending_signals: list[dict], strategy_priority: dict) -> dict | None:
"""같은 종목에 여러 전략의 신호가 몰렸을 때 하나만 통과시킨다.
pending_signals: [{"strategy_id": ..., "action": "BUY"/"SELL", ...}, ...]
"""
if not pending_signals:
return None
actions = {s["action"] for s in pending_signals}
if len(actions) > 1:
# 상반된 신호 — 이번 틱은 아무것도 실행하지 않고 다음 판단으로 넘긴다
return None
# 방향이 같으면(중복 매수 등) 우선순위가 가장 높은 전략의 신호만 통과
pending_signals.sort(key=lambda s: strategy_priority.get(s["strategy_id"], 0), reverse=True)
return pending_signals[0]strategy_priority는 어떤 전략의 신호를 더 신뢰할지를 미리 정해둔 값이다(예: 검증 기간이 더 긴 전략, 리스크가 더 낮은 전략을 우선시). 상반된 신호가 들어오면 이 예시처럼 "아무것도 안 한다"를 기본값으로 두는 게 안전하다. 둘 중 하나를 임의로 골라 실행하는 것보다, 판단을 보류하고 사람이나 상위 로직이 다시 볼 기회를 남기는 편이 사고를 줄인다.
우선순위만으로는 부족한 지점도 있다. 여러 전략이 동시에 같은 종목의 주문을 넣으려는 순간, 판단 로직이 끝나기도 전에 주문이 먼저 나가버릴 수 있다. 이걸 막는 게 종목 단위 락이다. 한 종목에 대한 주문 판단이 진행 중이면, 그 종목에 대한 다른 전략의 판단은 대기시키거나 이번 틱에서는 건너뛴다.
python 코드 보기
symbol_locks = set()
def try_place_order(symbol: str, strategy_id: str, order_fn):
"""종목 단위 락 — 이미 처리 중인 종목이면 주문을 건너뛴다."""
if symbol in symbol_locks:
return False # 다른 전략이 이 종목을 처리 중
symbol_locks.add(symbol)
try:
order_fn()
return True
finally:
symbol_locks.remove(symbol)락은 "동시에 두 주문이 같은 종목에 대해 경쟁하는 상황" 자체를 차단하는 장치이고, 우선순위는 "그 경쟁에서 누가 이길지"를 정하는 규칙이다. 둘은 역할이 다르므로 하나만 두면 안 된다.
전략별 리스크 한도는 전체 포트폴리오 한도에 종속되어야 한다
자동매매 리스크 관리 규칙 설계에서 손절·포지션 한도·서킷브레이커를 다뤘다. 전략이 하나일 때는 이 한도들이 곧 계좌 전체의 한도였다. 전략이 여러 개가 되면 이야기가 달라진다. 전략마다 각자의 포지션 한도를 잘 지키고 있어도, 전략들을 합친 전체 노출은 한도를 넘을 수 있다.
예를 들어 전략 A와 전략 B가 각각 "한 종목에 전체 자본의 10%까지"라는 한도를 지킨다고 해도, 둘 다 같은 종목에 10%씩 배정하면 그 종목의 실제 비중은 20%가 된다. 전략 단위로만 한도를 체크하면 이런 경우를 못 잡는다.
그래서 리스크 한도는 두 층으로 나눠야 한다. 전략별 한도는 그 전략에게 배분된 자본 안에서의 한도이고, 그 위에 전체 포트폴리오 기준으로 종목별·전체 노출을 다시 한번 체크하는 상위 계층이 있어야 한다.
python 코드 보기
def can_open_position_portfolio_level(symbol: str, order_value: float, strategy_id: str,
strategy_allocations: dict, all_positions_by_symbol: dict,
total_equity: float, max_symbol_pct_of_total: float) -> bool:
"""전략별 한도를 통과한 주문이라도, 전체 계좌 기준 종목 집중도를 다시 체크한다."""
existing_symbol_value = all_positions_by_symbol.get(symbol, 0)
projected_total = (existing_symbol_value + order_value) / total_equity
if projected_total > max_symbol_pct_of_total:
return False
return True이 함수는 전략별 포지션 한도 체크(008에서 다룬 can_open_position과 같은 성격의 함수)를 통과한 주문에 대해 한 번 더 실행되는 상위 게이트다. 순서가 중요하다 — 전략별 체크가 먼저, 전체 포트폴리오 체크가 그다음이다. 전략은 자기 자본 안에서 자유롭게 판단하되, 최종 실행 직전에는 반드시 전체 계좌 관점의 문지기를 거치게 만든다. 이 구조가 없으면 개별 전략은 모두 "규칙대로 하고 있다"고 믿는 채로 전체 계좌만 위험해지는 상황이 생긴다.
자주 하는 실수
- 전략별 자본을 나누지 않고 공유 잔고를 그냥 같이 쓴다. 배분 로직 없이 여러 전략이 같은 계좌 잔고를 조회해서 각자 주문 가능 금액을 계산하면, 두 전략이 동시에 신호를 낸 순간 실제 가용 자금보다 많은 주문이 나갈 수 있다. 전략을 추가하기 전에 반드시 자본부터 나눠야 한다.
- 상관관계가 높은 전략을 묶어놓고 "분산"이라고 착각한다. 추세추종 전략 두 개를 이름만 다르게 운영하면, 둘 다 같은 시장 국면에서 같은 방향으로 손실을 낸다. 진짜 분산은 서로 다른 국면에서 강한 전략을 묶는 것이지, 전략 개수를 늘리는 것 자체가 아니다.
- 신호 충돌 처리 순서를 안 정해서 나중에 실행된 전략이 이긴다. 우선순위 없이 코드 실행 순서(등록 순서, 반복문 순서)에 따라 결과가 갈리면, 같은 상황에서도 그날그날 다른 결과가 나올 수 있다. 우선순위는 코드가 아니라 설계 단계에서 명시적으로 정해야 한다.
- 전략별 리스크 한도만 두고 전체 포트폴리오 한도를 안 둔다. 앞서 다룬 것처럼 개별 전략은 각자 한도를 지켜도 합산 노출은 초과할 수 있다. 전략 단위 체크와 전체 계좌 단위 체크는 항상 한 쌍으로 있어야 한다.
오늘의 정리
- 전략이 하나에서 여러 개로 늘어나는 순간, "계좌 자금을 전략 하나가 다 쓴다"는 전제가 깨지므로 자본 배분 로직을 새로 설계해야 한다.
- 자본 배분은 전략마다 고정 비율을 주는 방식과, 최근 성과에 따라 비중을 조정하는 방식이 있으며 후자도 최소 배분은 보장해야 한다.
- 같은 종목에 여러 전략의 신호가 겹치는 충돌은 우선순위(누구 신호를 따를지)와 락(동시 처리 자체를 막을지)이라는 서로 다른 역할의 두 장치로 막는다.
- 전략별 리스크 한도는 전체 포트폴리오 한도의 하위 개념이어야 한다 — 개별 전략이 규칙을 지켜도 합산하면 초과할 수 있으므로 상위 계층의 체크가 별도로 필요하다.
- 전략 개수를 늘리는 것 자체는 분산이 아니다. 자본 배분·충돌 방지·상위 리스크 한도라는 구조가 갖춰졌을 때만 "여러 전략 운영"이 의미가 있다.
본문 코드는 개념 설명을 위한 의사코드이며 실제 거래 시스템의 구현이 아닙니다. 이 글은 투자 조언이 아니고 특정 종목이나 매매기법을 추천하지 않으며, 전략을 여러 개로 늘린다고 수익이 보장되지 않습니다. 투자 손실의 책임은 투자자 본인에게 있습니다.