파이썬 자동매매 프로젝트 구조 잡기 — 모듈 분리와 설정 관리
하나의 파일에서 시작해서 얻은 교훈
처음 자동매매 코드를 짤 때는 다들 비슷한 경로를 거친다. 시세를 가져오고, 조건을 확인하고, 주문을 넣는 로직을 파일 하나에 순서대로 이어 쓴다. 짧은 프로토타입 단계에서는 이게 오히려 빠르다. 그런데 리스크 관리 규칙이 추가되고, 모의투자 환경과 실전 환경을 구분해야 하고, 알림 로직까지 들어가기 시작하면 파일 하나짜리 구조는 금방 한계에 부딪힌다. 어디서 주문이 나가는지, 어디서 리스크 판단이 이뤄지는지가 코드 안에 뒤섞여 있으면 무언가 하나를 고칠 때마다 다른 부분이 같이 깨지지 않았는지 매번 전체를 다시 읽어야 한다. 이 글은 그 문제를 겪은 뒤 정리한, 아주 기초적인 수준의 프로젝트 구조와 설정 관리 방식을 다룬다. 특정 회사의 실제 코드가 아니라 누구나 참고할 수 있는 일반적인 패턴이다.
레이어를 나누는 기준
프로젝트를 다섯 개의 레이어로 나눴다. 기준은 하나다 — "이 코드가 하는 일이 바뀌는 이유"가 서로 달라야 분리한다. 예를 들어 시세를 가져오는 코드와 매매 조건을 판단하는 코드는 서로 다른 이유로 바뀐다. 시세 수집 코드는 API 스펙이 바뀌거나 데이터 소스가 늘어날 때 바뀌고, 전략 코드는 매매 아이디어가 바뀔 때 바뀐다. 이 둘이 한 파일에 섞여 있으면, 전략만 바꾸려다가 시세 수집 로직까지 건드리게 되는 실수가 생긴다.
project/
├── config/
│ ├── settings.py # 설정 로딩, 환경변수 매핑
│ └── .env.example # 실제 값 없는 템플릿 (실제 .env는 gitignore)
├── data/
│ └── market_data.py # 시세 수집·정제·저장
├── strategy/
│ └── ma_cross.py # 매매 신호 생성 로직 (전략별로 파일 분리)
├── risk/
│ └── risk_manager.py # 포지션 한도, 손실 한도, 승인 절차
├── execution/
│ └── order_client.py # 실제 주문 요청·응답 처리
└── main.py # 각 레이어를 조합해 실행 흐름을 구성
레이어별 역할과 경계
data(데이터). 시세를 가져오고, 결측치나 이상치를 정제하고, 필요하면 저장한다. 이 레이어는 "지금 시장 상황이 어떤가"를 답할 뿐, 무엇을 사고팔지는 절대 판단하지 않는다.
strategy(전략). 정제된 데이터를 입력으로 받아 매매 신호(사라/팔아라/유지해라)를 만든다. 여기서 중요한 원칙은 전략 코드가 주문을 직접 내보내지 않는다는 것이다. 전략은 "이런 신호가 나왔다"는 결과만 반환하고, 그 신호를 실제로 실행할지 말지는 리스크 레이어를 거친 뒤 실행 레이어가 결정한다.
risk(리스크). 전략이 낸 신호를 받아 포지션 한도, 손실 한도, 승인 절차를 확인한다. 한도를 넘는 신호는 여기서 걸러진다. 이전 글(008번 참고)에서 다룬 것처럼, 이 레이어는 전략 로직과 분리되어 있어야 전략에 버그가 생겨도 리스크 판단까지 같이 무력화되지 않는다.
execution(실행). 리스크 레이어를 통과한 신호만 실제 주문 요청으로 바꿔 증권사 API에 보낸다. 이 레이어가 모의투자 환경과 실전 환경의 base URL을 구분하는 지점이기도 하다.
config(설정). 나머지 모든 레이어가 필요로 하는 값(API 키, 한도 값, 모의투자/실전 여부)을 한 곳에서 읽어온다. 다른 레이어는 절대 환경변수를 직접 읽지 않고, 이 config 레이어를 거쳐서만 값을 받는다.
설정 관리 — 코드와 값을 분리하기
이 구조에서 가장 신경 쓴 지점이 config 레이어다. 원칙은 단순하다. 키·한도값·환경 구분 같은 "값"은 코드 안에 있으면 안 된다. 006번 글에서 다룬 인증 키 관리와 같은 맥락이다.
# config/settings.py
import os
from dataclasses import dataclass
@dataclass(frozen=True)
class Settings:
broker_app_key: str
broker_app_secret: str
base_url: str
is_paper_trading: bool
max_position_loss_ratio: float
max_daily_loss_ratio: float
def load_settings() -> Settings:
"""환경변수에서 설정을 읽는다. 값이 없으면 여기서 바로 실패한다 —
조용히 기본값으로 넘어가면 실전 환경에 잘못된 한도가 적용될 수 있다."""
return Settings(
broker_app_key=os.environ["BROKER_APP_KEY"],
broker_app_secret=os.environ["BROKER_APP_SECRET"],
base_url=os.environ["BROKER_BASE_URL"],
is_paper_trading=os.environ.get("IS_PAPER_TRADING", "true") == "true",
max_position_loss_ratio=float(os.environ["MAX_POSITION_LOSS_RATIO"]),
max_daily_loss_ratio=float(os.environ["MAX_DAILY_LOSS_RATIO"]),
)
여기서 의도적으로 넣은 선택이 하나 있다. 필수 값(API 키, 한도값)은 os.environ[...]으로 읽어서, 값이 없으면 프로그램이 그 자리에서 바로 죽도록 만들었다. os.environ.get(...)으로 기본값을 주면 편해 보이지만, 예를 들어 한도값 환경변수를 설정하지 않았는데 기본값 때문에 시스템이 "한도 없음"으로 조용히 시작해버리는 상황이 훨씬 위험하다. 설정 누락은 시끄럽게 실패하는 게 맞다.
main.py — 레이어를 조합만 하는 곳
from config.settings import load_settings
from data.market_data import MarketDataClient
from strategy.ma_cross import MovingAverageCrossStrategy
from risk.risk_manager import RiskManager
from execution.order_client import OrderClient
def run():
settings = load_settings()
data_client = MarketDataClient(settings)
strategy = MovingAverageCrossStrategy()
risk_manager = RiskManager(settings)
order_client = OrderClient(settings)
market_data = data_client.fetch_latest()
signal = strategy.generate_signal(market_data)
approved_signal = risk_manager.check(signal) # 한도 초과 시 None 반환
if approved_signal is not None:
order_client.submit(approved_signal)
if __name__ == "__main__":
run()
main.py는 의도적으로 로직을 거의 담지 않는다. 각 레이어를 순서대로 불러서 연결하는 역할만 한다. 이렇게 해두면 어떤 레이어를 테스트하고 싶을 때 나머지 레이어를 흉내 낸 가짜 객체로 바꿔치기하기 쉽고, 전체 흐름을 한눈에 읽을 수 있다는 장점도 있다.
처음부터 이렇게 나누지 않아도 되는 이유
여기서 한 가지는 분명히 해두고 싶다. 프로토타입 단계에서부터 이 구조를 강제할 필요는 없다. 아이디어를 빠르게 검증하는 단계에서는 파일 하나짜리 스크립트가 오히려 낫다. 문제는 그 프로토타입을 백테스팅·모의투자를 거쳐 실전에 가깝게 발전시키는 시점부터다. 이 시점에 레이어를 나누지 않으면, 나중에 나눌 때 이미 얽혀버린 로직을 풀어내는 비용이 처음부터 나눴을 때보다 훨씬 커진다. 우리가 겪은 시행착오도 정확히 이 지점이었다 — 프로토타입이 "그럭저럭 동작"하자 구조를 정리할 타이밍을 놓치고 계속 키우다가, 나중에 리스크 관리 로직을 분리하는 데 예상보다 훨씬 많은 시간을 썼다.
오늘의 정리
- 코드가 바뀌는 "이유"가 다르면 레이어를 분리한다 — 데이터·전략·리스크·실행·설정 다섯 레이어로 나눴다.
- 전략 레이어는 신호만 만들고 주문을 직접 내보내지 않는다. 리스크 레이어를 반드시 거치게 만드는 게 핵심이다.
- 키·한도값 같은 설정은 코드가 아니라 환경변수로 관리하고, 필수 값이 없으면 기본값으로 넘어가지 않고 바로 실패하게 만든다.
main.py는 로직 없이 레이어를 조합만 하는 자리로 남겨둔다.- 프로토타입 단계부터 구조를 강제할 필요는 없지만, 실전에 가까워지기 전에는 반드시 정리해야 나중 비용이 줄어든다.
다음 글에서는 다시 이야기로 돌아와, 지금까지 이 시리즈를 쓰며 되짚어본 회고를 정리한다.
이 글의 코드 구조는 개념 설명을 위한 예시이며, 특정 시스템의 실제 파일 구성이나 설정값을 담고 있지 않습니다. 이 글은 투자 조언이 아니며 특정 종목이나 매매기법을 추천하지 않습니다. 수익을 보장하지 않으며, 투자 손실에 대한 책임은 투자자 본인에게 있습니다.