퀀트 트레이딩(Quantitative Trading)은 수학·통계·컴퓨터 알고리즘을 활용하여 금융시장에서 자동으로 매매 의사결정을 수행하는 방법론이다. 감정이나 직관이 아닌 데이터 기반 모델이 핵심이다.
1. 개념 정의
퀀트 트레이딩은 다음과 같은 구조를 가진다.
- 시장 데이터를 수집 (가격, 거래량, 뉴스 등)
- 수학적 모델 또는 통계 기법으로 패턴 분석
- 매수/매도 신호 생성
- 자동 또는 반자동으로 주문 실행
즉, “조건 → 계산 → 실행”의 파이프라인 구조이다.
2. 어떻게 수행하는가 (프로세스)
퀀트 트레이딩은 일반적으로 다음 단계로 구성된다.
2.1 데이터 수집
- 주가 (OHLCV)
- 재무 데이터
- 뉴스 / 감성 데이터
- 거시경제 지표
2.2 전략 설계
대표적인 전략 유형:
- 평균회귀 (Mean Reversion)
- 추세추종 (Trend Following)
- 차익거래 (Arbitrage)
- 팩터 투자 (Factor Investing)
2.3 모델링
사용되는 기술:
- 통계 모델 (회귀, ARIMA 등)
- 머신러닝 (Random Forest, XGBoost)
- 딥러닝 (LSTM, Transformer)
2.4 백테스팅
- 과거 데이터로 전략 검증
- 수익률, Sharpe Ratio, MDD 분석
2.5 실행 (Execution)
- API를 통한 자동 주문
- 슬리피지, 수수료 고려
3. 기초 모델 (핵심 예시)
3.1 이동평균 기반 전략 (가장 기본)
- 단기 이동평균 > 장기 이동평균 → 매수
- 단기 이동평균 < 장기 이동평균 → 매도
이는 추세추종 전략의 기본 형태이다.
3.2 평균 회귀 모델
- 가격이 평균보다 많이 올라가면 → 하락 기대 → 매도
- 가격이 평균보다 많이 내려가면 → 상승 기대 → 매수
대표적으로 Z-score 기반 전략 사용
3.3 팩터 모델 (기관에서 많이 사용)
- Value (저평가)
- Momentum (상승 추세)
- Quality (재무 건전성)
이들을 조합하여 포트폴리오 구성
4. 실제 구현 가능 여부 (핵심 질문)
결론부터 말하면 개인도 충분히 구현 가능하다.
4.1 필요한 구성 요소
- 프로그래밍 언어: Python
- 라이브러리: pandas, numpy, sklearn
- 거래 API: 증권사 또는 거래소 제공 API
4.2 실제 계좌 연결 (가능)
다음과 같은 방식으로 연결한다.
국내 주식
- 키움증권 Open API
- 한국투자증권 API
해외 주식
- Interactive Brokers
- Alpaca
암호화폐
- Binance
- Upbit
API를 통해 다음이 가능하다:
- 실시간 시세 조회
- 주문 (매수/매도)
- 잔고 확인
4.3 기본 실행 구조 (간단 예시)
if short\_ma > long\_ma:
buy()
else:
sell()
이 로직이 자동으로 반복 실행되도록 시스템 구성
5. 현실적인 한계
퀀트 트레이딩은 “가능”하지만 “쉽지 않다”.
5.1 주요 문제
- 데이터 품질 문제
- 과최적화 (Overfitting)
- 시장 변화 대응 어려움
- 슬리피지 및 거래 비용
5.2 개인 vs 기관
기관(헤지펀드)은 다음을 보유:
- 초고속 서버
- 대규모 데이터
- 복잡한 모델
개인은 상대적으로 불리하지만,
단순 전략 + 장기 운영으로도 충분히 성과 가능
6. 정리
퀀트 트레이딩은 다음과 같이 요약된다.
- 데이터 기반 자동 매매 시스템
- 전략 → 모델 → 백테스트 → 실행 구조
- 개인도 API를 통해 실제 계좌 연결 가능
- 단, 실전에서는 리스크 관리가 핵심
7. 세부내용
실제 운용 전에는 반드시 백테스트 → 워크포워드 검증 → 페이퍼 트레이딩 → 소액 실계좌 검증 순서로 진행해야 한다.
실계좌 연동 자체는 가능하다. 예를 들어 Alpaca는 종이거래와 실거래의 API 스펙이 동일하며, 페이퍼 계정은 별도 API 키를 사용하고 일반적으로 APCA_API_BASE_URL=https://paper-api.alpaca.markets 형태로 설정한다. 주문은 /v2/orders로 제출할 수 있고, 오픈 주문 상태 업데이트는 스트리밍 인터페이스 사용이 권장된다. Python SDK alpaca-py도 공식 제공된다.
이 답변은 두 부분으로 구성된다. 첫째, Python + API 자동매매 시스템 설계. 둘째, 실제로 구현 가능한 전략 3종과 코드 구조이다.
1. 자동매매 시스템의 전체 구조
자동매매 시스템은 보통 아래 6개 계층으로 나뉜다.
- 데이터 계층
- 과거 가격 데이터
- 실시간 시세 데이터
- 종목 메타데이터
- 거래 가능 시간, 거래 정지 여부, 호가 단위 등
- 전략 계층
- 신호 생성
- 포지션 크기 계산
- 진입/청산 조건 정의
- 리스크 계층
- 최대 포지션 수
- 종목당 최대 비중
- 최대 손실 제한
- 일일 손실 한도
- 변동성 기반 수량 조절
- 실행 계층
- 시장가/지정가 주문
- 주문 재시도
- 체결 확인
- 미체결 취소
- 슬리피지 대응
- 포트폴리오 계층
- 현금, 평가금액, 노출도
- 종목별 손익
- 누적 수익률
- 회전율
- 운영 계층
- 로그
- 장애 대응
- 알림
- 환경변수 기반 키 관리
- 스케줄링
실무적으로는 “전략”보다 실행 안정성과 리스크 제어가 더 중요하다.
2. 가장 현실적인 개발 순서
가장 안전한 순서는 다음과 같다.
2.1 1단계: 백테스터 먼저 만든다
실계좌를 붙이기 전에 다음이 가능해야 한다.
- 과거 데이터 읽기
- 전략 신호 생성
- 체결 가정
- 수수료 반영
- 성과 지표 계산
2.2 2단계: 브로커 어댑터를 분리한다
전략 코드와 주문 코드를 분리해야 한다.
- strategy.py: 언제 사고팔지 결정
- broker.py: 실제 주문 보내기
- risk.py: 주문 가능 여부 심사
- runner.py: 전체 실행
2.3 3단계: 페이퍼 트레이딩에 연결한다
실계좌 전에 반드시 모의 계좌에 연결한다. Alpaca는 페이퍼 거래를 별도로 제공하고, 페이퍼와 라이브의 API 스펙이 동일하다.
2.4 4단계: 소액 실계좌 전환
실계좌 전환 시에는 전략보다 아래 항목을 먼저 본다.
- 주문이 중복으로 나가지 않는가
- 장 시작 직후/종료 직전 오류가 없는가
- 예외 발생 시 포지션 상태를 복구하는가
- 네트워크 끊김 후 재연결이 가능한가
3. 추천 프로젝트 구조
quant\_trading/
├─ config.py
├─ broker/
│ ├─ base.py
│ └─ alpaca\_broker.py
├─ data/
│ ├─ loader.py
│ └─ indicators.py
├─ strategies/
│ ├─ base.py
│ ├─ sma\_cross.py
│ ├─ mean\_reversion.py
│ └─ breakout.py
├─ risk/
│ └─ position\_sizing.py
├─ backtest/
│ ├─ engine.py
│ └─ metrics.py
├─ live/
│ └─ runner.py
└─ main.py
이 구조의 핵심은 전략을 갈아껴도 브로커와 리스크 로직은 재사용되게 만드는 것이다.
4. 먼저 알아야 할 핵심 원칙
4.1 전략보다 리스크가 먼저다
초보자가 가장 많이 하는 실수는 다음과 같다.
- 진입 규칙만 만들고 청산 규칙은 약하게 둠
- 손절 규칙이 없음
- 포지션 크기를 고정하지 않음
- 종목을 동시에 너무 많이 보유함
4.2 “좋아 보이는 백테스트”는 대부분 과최적화일 수 있다
예를 들어 이동평균 17일, 43일처럼 애매한 값이 지나치게 잘 맞으면 의심해야 한다.
4.3 거래 횟수가 많을수록 비용 영향이 커진다
수익률 계산에는 반드시 아래를 넣어야 한다.
- 수수료
- 슬리피지
- 세금 또는 기타 비용
- 미체결 가능성
5. Python + API 자동매매 코드 설계
아래는 가장 실전적인 최소 구조이다.
설명 편의를 위해 브로커는 Alpaca 기준으로 예시를 들지만, 구조 자체는 다른 증권사 API에도 그대로 적용 가능하다.
Alpaca 공식 문서 기준으로 Python SDK는 alpaca-py를 설치해 사용할 수 있고, 주문 제출은 Trading API를 통해 가능하다. 페이퍼와 라이브는 API 키와 엔드포인트만 달리 써서 같은 흐름으로 운영할 수 있다.
6. 환경 설정
6.1 설치
pip install pandas numpy scipy requests python-dotenv alpaca-py yfinance
yfinance는 백테스트용 데이터 예시이고, 실제 운영 데이터는 브로커/거래소 API 데이터로 대체하는 것이 더 적절하다.
6.2 .env 파일
APCA\_API\_KEY=YOUR\_KEY
APCA\_API\_SECRET=YOUR\_SECRET
APCA\_PAPER=true
APCA\_API\_BASE\_URL=[https://paper-api.alpaca.markets](https://paper-api.alpaca.markets)
페이퍼 트레이딩은 별도 키를 쓰며, 실거래 전환 시 키와 엔드포인트를 라이브용으로 바꾸면 된다. 공식 문서도 페이퍼와 라이브의 API 스펙은 동일하다고 설명한다.
7. 공통 설정 파일
\# config.py
from dataclasses import dataclass
import os
from dotenv import load\_dotenv
load\_dotenv()
@dataclass
class Settings:
api\_key: str = os.getenv("APCA\_API\_KEY", "")
api\_secret: str = os.getenv("APCA\_API\_SECRET", "")
paper: bool = os.getenv("APCA\_PAPER", "true").lower() == "true"
base\_url: str = os.getenv("APCA\_API\_BASE\_URL", "[https://paper-api.alpaca.markets](https://paper-api.alpaca.markets)")
max\_position\_pct: float = 0.10 # 종목당 최대 10%
max\_total\_exposure: float = 0.80 # 총 노출 80%
risk\_per\_trade: float = 0.01 # 계좌당 1% 리스크
fee\_bps: float = 5 # 왕복 비용 가정 예시
slippage\_bps: float = 5 # 슬리피지 가정 예시
8. 지표 계산 유틸리티
\# data/indicators.py
import pandas as pd
import numpy as np
def sma(series: pd.Series, window: int) -> pd.Series:
return series.rolling(window).mean()
def ema(series: pd.Series, span: int) -> pd.Series:
return series.ewm(span\=span, adjust\=False).mean()
def zscore(series: pd.Series, window: int) -> pd.Series:
mean \= series.rolling(window).mean()
std \= series.rolling(window).std(ddof\=0)
return (series \- mean) / std
def atr(df: pd.DataFrame, window: int \= 14) -> pd.Series:
high \= df\["High"\]
low \= df\["Low"\]
close \= df\["Close"\]
prev\_close \= close.shift(1)
tr \= pd.concat(\[
high \- low,
(high \- prev\_close).abs(),
(low \- prev\_close).abs()
\], axis\=1).max(axis\=1)
return tr.rolling(window).mean()
def rolling\_high(series: pd.Series, window: int) -> pd.Series:
return series.rolling(window).max()
def rolling\_low(series: pd.Series, window: int) -> pd.Series:
return series.rolling(window).min()9. 전략 베이스 클래스
\# strategies/base.py
from abc import ABC, abstractmethod
import pandas as pd
class StrategyBase(ABC):
name = "base"
@abstractmethod
def generate\_signals(self, df: pd.DataFrame) -> pd.DataFrame:
"""
반환 컬럼 예시:
\- signal: 1(롱 진입), 0(청산/관망)
\- stop\_price: 손절 기준
"""
raise NotImplementedError
10. 전략 1: 이동평균 크로스오버 전략
10.1 개념
가장 기본적인 추세추종 전략이다.
- 단기 이동평균이 장기 이동평균을 상향 돌파하면 매수
- 단기 이동평균이 장기 이동평균을 하향 이탈하면 청산
장점
- 구조가 단순하다
- 강한 추세장에서 비교적 잘 작동한다
- 초보자가 자동매매 구조를 배우기에 적합하다
단점
- 횡보장에서 잦은 손절이 발생한다
- 너무 짧은 주기를 쓰면 노이즈에 취약하다
10.2 전략 코드
\# strategies/sma\_cross.py
import pandas as pd
from strategies.base import StrategyBase
from data.indicators import sma, atr
class SmaCrossStrategy(StrategyBase):
name = "sma\_cross"
def \_\_init\_\_(self, short\_window: int = 20, long\_window: int = 50, atr\_mult: float = 2.0):
self.short\_window = short\_window
self.long\_window = long\_window
self.atr\_mult = atr\_mult
def generate\_signals(self, df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
out\["sma\_short"\] = sma(out\["Close"\], self.short\_window)
out\["sma\_long"\] = sma(out\["Close"\], self.long\_window)
out\["atr"\] = atr(out, 14)
out\["signal"\] = 0
out.loc\[out\["sma\_short"\] > out\["sma\_long"\], "signal"\] = 1
\# ATR 기반 손절선
out\["stop\_price"\] = out\["Close"\] - self.atr\_mult \* out\["atr"\]
return out
10.3 언제 유리한가
- 지수 ETF
- 추세가 길게 이어지는 종목군
- 일봉/4시간봉처럼 너무 미세하지 않은 주기
11. 전략 2: 평균회귀 Z-score 전략
11.1 개념
가격이 단기 평균에서 과도하게 이탈하면 다시 평균으로 되돌아올 가능성을 이용하는 전략이다.
- Z-score가 너무 낮으면 과매도 구간으로 보고 매수
- Z-score가 평균 근처로 복귀하면 청산
장점
- 횡보장에서 상대적으로 유리할 수 있다
- 추세 전략과 상관성이 다소 달라 분산 효과가 있다
단점
- 강한 하락 추세에서 “싸 보인다고 계속 사는” 문제가 생긴다
- 따라서 추세 필터를 함께 넣는 것이 좋다
11.2 전략 코드
\# strategies/mean\_reversion.py
import pandas as pd
from strategies.base import StrategyBase
from data.indicators import zscore, sma, atr
class MeanReversionStrategy(StrategyBase):
name = "mean\_reversion"
def \_\_init\_\_(
self,
lookback: int = 20,
entry\_z: float = -2.0,
exit\_z: float = -0.3,
trend\_filter\_window: int = 100,
atr\_mult: float = 2.0
):
self.lookback = lookback
self.entry\_z = entry\_z
self.exit\_z = exit\_z
self.trend\_filter\_window = trend\_filter\_window
self.atr\_mult = atr\_mult
def generate\_signals(self, df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
out\["z"\] = zscore(out\["Close"\], self.lookback)
out\["trend\_ma"\] = sma(out\["Close"\], self.trend\_filter\_window)
out\["atr"\] = atr(out, 14)
out\["signal"\] = 0
in\_position = False
signals = \[\]
for i in range(len(out)):
row = out.iloc\[i\]
if pd.isna(row\["z"\]) or pd.isna(row\["trend\_ma"\]):
signals.append(0)
continue
\# 장기 하락 추세 종목은 회피
trend\_ok = row\["Close"\] > row\["trend\_ma"\]
if not in\_position and trend\_ok and row\["z"\] <= self.entry\_z:
in\_position = True
signals.append(1)
elif in\_position and row\["z"\] >= self.exit\_z:
in\_position = False
signals.append(0)
else:
signals.append(1 if in\_position else 0)
out\["signal"\] = signals
out\["stop\_price"\] = out\["Close"\] - self.atr\_mult \* out\["atr"\]
return out
11.3 언제 유리한가
- 박스권 또는 변동성 회귀 구간
- 개별 종목보다 ETF, 대형주, 유동성 있는 자산에 더 안정적일 수 있음
12. 전략 3: 돌파(Breakout) 전략
12.1 개념
일정 기간의 최고가를 돌파하면 추세가 시작될 수 있다는 가정이다.
- N일 최고가 돌파 시 진입
- N일 최저가 이탈 또는 ATR 손절 시 청산
이는 전통적인 추세추종의 대표 모델이다.
장점
- 큰 추세를 타면 수익이 크게 날 수 있다
- 룰이 명확해 자동화에 적합하다
단점
- 가짜 돌파가 많다
- 횡보장에서 손실이 반복될 수 있다
12.2 전략 코드
\# strategies/breakout.py
import pandas as pd
from strategies.base import StrategyBase
from data.indicators import rolling\_high, rolling\_low, atr
class BreakoutStrategy(StrategyBase):
name = "breakout"
def \_\_init\_\_(self, entry\_window: int = 20, exit\_window: int = 10, atr\_mult: float = 2.5):
self.entry\_window = entry\_window
self.exit\_window = exit\_window
self.atr\_mult = atr\_mult
def generate\_signals(self, df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
out\["entry\_high"\] = rolling\_high(out\["High"\].shift(1), self.entry\_window)
out\["exit\_low"\] = rolling\_low(out\["Low"\].shift(1), self.exit\_window)
out\["atr"\] = atr(out, 14)
in\_position = False
signals = \[\]
for i in range(len(out)):
row = out.iloc\[i\]
if pd.isna(row\["entry\_high"\]) or pd.isna(row\["exit\_low"\]):
signals.append(0)
continue
if not in\_position and row\["Close"\] > row\["entry\_high"\]:
in\_position = True
signals.append(1)
elif in\_position and row\["Close"\] < row\["exit\_low"\]:
in\_position = False
signals.append(0)
else:
signals.append(1 if in\_position else 0)
out\["signal"\] = signals
out\["stop\_price"\] = out\["Close"\] - self.atr\_mult \* out\["atr"\]
return out
13. 포지션 사이징
전략의 성패를 크게 좌우하는 부분이다.
가장 실전적인 방식은 ATR 기반 리스크 사이징이다.
원리
- 계좌가 1,000만 원
- 거래당 감수 손실 1% = 10만 원
- 진입가와 손절가 차이가 2,000원
- 수량 = 10만 / 2,000 = 50주
\# risk/position\_sizing.py
import math
def calculate\_position\_size(account\_equity: float, risk\_per\_trade: float, entry\_price: float, stop\_price: float) -> int:
risk\_amount = account\_equity \* risk\_per\_trade
per\_share\_risk = max(entry\_price - stop\_price, 0.01)
qty = math.floor(risk\_amount / per\_share\_risk)
return max(qty, 0)
14. 백테스트 엔진
아래 코드는 단일 종목 기준의 간단한 백테스트 엔진이다.
\# backtest/engine.py
import pandas as pd
import numpy as np
class BacktestEngine:
def \_\_init\_\_(self, fee\_bps: float = 5.0, slippage\_bps: float = 5.0):
self.fee\_bps = fee\_bps / 10000
self.slippage\_bps = slippage\_bps / 10000
def run(self, df: pd.DataFrame, initial\_cash: float = 100000.0) -> pd.DataFrame:
data = df.copy().dropna().reset\_index(drop=False)
cash = initial\_cash
shares = 0
equity\_curve = \[\]
entry\_price = None
prev\_signal = 0
for i in range(len(data)):
row = data.iloc\[i\]
close = row\["Close"\]
signal = int(row\["signal"\])
\# 진입
if prev\_signal == 0 and signal == 1 and shares == 0:
buy\_price = close \* (1 + self.slippage\_bps)
qty = int(cash // buy\_price)
if qty > 0:
cost = qty \* buy\_price
fee = cost \* self.fee\_bps
cash -= (cost + fee)
shares += qty
entry\_price = buy\_price
\# 청산
elif prev\_signal == 1 and signal == 0 and shares > 0:
sell\_price = close \* (1 - self.slippage\_bps)
proceeds = shares \* sell\_price
fee = proceeds \* self.fee\_bps
cash += (proceeds - fee)
shares = 0
entry\_price = None
equity = cash + shares \* close
equity\_curve.append(equity)
prev\_signal = signal
data\["equity"\] = equity\_curve
data\["returns"\] = data\["equity"\].pct\_change().fillna(0.0)
return data
15. 성과 지표 계산
\# backtest/metrics.py
import numpy as np
import pandas as pd
def performance\_summary(bt: pd.DataFrame, periods\_per\_year: int = 252) -> dict:
rets = bt\["returns"\].dropna()
equity = bt\["equity"\]
total\_return = equity.iloc\[-1\] / equity.iloc\[0\] - 1
cagr = (equity.iloc\[-1\] / equity.iloc\[0\]) \*\* (periods\_per\_year / max(len(bt), 1)) - 1
vol = rets.std(ddof=0) \* np.sqrt(periods\_per\_year)
sharpe = (rets.mean() / rets.std(ddof=0) \* np.sqrt(periods\_per\_year)) if rets.std(ddof=0) > 0 else np.nan
rolling\_max = equity.cummax()
drawdown = equity / rolling\_max - 1
mdd = drawdown.min()
win\_rate = float((rets > 0).sum() / max((rets != 0).sum(), 1))
return {
"total\_return": total\_return,
"cagr": cagr,
"volatility": vol,
"sharpe": sharpe,
"max\_drawdown": mdd,
"win\_rate": win\_rate,
}
16. 데이터 로더 예시
\# data/loader.py
import yfinance as yf
import pandas as pd
def load\_ohlcv(symbol: str, start: str, end: str) -> pd.DataFrame:
df = yf.download(symbol, start=start, end=end, auto\_adjust=False, progress=False)
df = df.rename(columns=str.title)
df = df\[\["Open", "High", "Low", "Close", "Volume"\]\].dropna()
return df
17. 전략 테스트 실행 예시
\# main.py
from data.loader import load\_ohlcv
from strategies.sma\_cross import SmaCrossStrategy
from strategies.mean\_reversion import MeanReversionStrategy
from strategies.breakout import BreakoutStrategy
from backtest.engine import BacktestEngine
from backtest.metrics import performance\_summary
def run\_one(symbol, strategy, start="2018-01-01", end="2025-01-01"):
df = load\_ohlcv(symbol, start, end)
sig = strategy.generate\_signals(df)
bt = BacktestEngine(fee\_bps=5, slippage\_bps=5).run(sig)
summary = performance\_summary(bt)
return summary, bt
if \_\_name\_\_ == "\_\_main\_\_":
symbol = "SPY"
strategies = \[
SmaCrossStrategy(20, 50),
MeanReversionStrategy(20, -2.0, -0.3, 100),
BreakoutStrategy(20, 10),
\]
for s in strategies:
summary, \_ = run\_one(symbol, s)
print(f"\\n\[{s.name}\]")
for k, v in summary.items():
print(k, round(v, 4))
---
## 18\. 실계좌 연결용 브로커 클래스 설계
Alpaca 공식 예제 기준으로 Python SDK에서 TradingClient(..., paper=True) 형태로 종이거래 클라이언트를 만들고, submit\_order를 통해 주문을 전송할 수 있다. 주문 생성은 공식적으로 /v2/orders 엔드포인트를 사용한다.
\# broker/alpaca\_broker.py
from alpaca.trading.client import TradingClient
from alpaca.trading.requests import MarketOrderRequest, GetOrdersRequest
from alpaca.trading.enums import OrderSide, TimeInForce, QueryOrderStatus
class AlpacaBroker:
def \_\_init\_\_(self, api\_key: str, api\_secret: str, paper: bool = True):
self.client = TradingClient(api\_key, api\_secret, paper=paper)
def get\_account(self):
return self.client.get\_account()
def list\_open\_orders(self):
req = GetOrdersRequest(status=QueryOrderStatus.OPEN)
return self.client.get\_orders(filter=req)
def buy\_market(self, symbol: str, qty: float, client\_order\_id: str | None = None):
order = MarketOrderRequest(
symbol=symbol,
qty=qty,
side=OrderSide.BUY,
time\_in\_force=TimeInForce.DAY,
client\_order\_id=client\_order\_id,
)
return self.client.submit\_order(order\_data=order)
def sell\_market(self, symbol: str, qty: float, client\_order\_id: str | None = None):
order = MarketOrderRequest(
symbol=symbol,
qty=qty,
side=OrderSide.SELL,
time\_in\_force=TimeInForce.DAY,
client\_order\_id=client\_order\_id,
)
return self.client.submit\_order(order\_data=order)
def close\_position(self, symbol: str):
return self.client.close\_position(symbol)19. 실시간 실행 러너
주의할 점은 실시간 자동매매는 “코드가 한 번 돌아가는 것”이 아니라, 반복 실행 중에도 상태가 깨지지 않는 것이 핵심이라는 점이다.
\# live/runner.py
import time
from datetime import datetime
from data.loader import load\_ohlcv
from strategies.sma\_cross import SmaCrossStrategy
from broker.alpaca\_broker import AlpacaBroker
from config import Settings
class LiveRunner:
def \_\_init\_\_(self, symbol: str):
self.settings = Settings()
self.symbol = symbol
self.strategy = SmaCrossStrategy(20, 50)
self.broker = AlpacaBroker(
self.settings.api\_key,
self.settings.api\_secret,
self.settings.paper
)
def get\_latest\_signal(self):
df = load\_ohlcv(self.symbol, "2024-01-01", "2030-01-01")
sig = self.strategy.generate\_signals(df)
latest = sig.iloc\[-1\]
prev = sig.iloc\[-2\]
return int(prev\["signal"\]), int(latest\["signal"\]), float(latest\["Close"\])
def has\_position(self):
account\_positions = self.broker.client.get\_all\_positions()
symbols = {p.symbol for p in account\_positions}
return self.symbol in symbols
def run\_once(self):
prev\_signal, signal, price = self.get\_latest\_signal()
in\_position = self.has\_position()
print(datetime.now(), self.symbol, "prev=", prev\_signal, "now=", signal, "price=", price, "in\_pos=", in\_position)
\# 0 -> 1 전환 시 매수
if not in\_position and prev\_signal == 0 and signal == 1:
account = self.broker.get\_account()
buying\_power = float(account.buying\_power)
budget = buying\_power \* 0.10
qty = int(budget // price)
if qty > 0:
order\_id = f"{self.symbol}-entry-{int(time.time())}"
self.broker.buy\_market(self.symbol, qty, client\_order\_id=order\_id)
print("BUY submitted", qty)
\# 1 -> 0 전환 시 청산
elif in\_position and prev\_signal == 1 and signal == 0:
self.broker.close\_position(self.symbol)
print("CLOSE submitted")
if \_\_name\_\_ == "\_\_main\_\_":
runner = LiveRunner("SPY")
while True:
try:
runner.run\_once()
except Exception as e:
print("ERROR:", e)
\# 예시: 5분마다 확인
time.sleep(300)20. 위 구조를 실제로 운영할 때 반드시 추가해야 하는 것
위 코드는 구조 설명용 최소 예제다. 실전 배포 전에는 아래 기능이 반드시 필요하다.
20.1 중복 주문 방지
- 이미 주문이 나간 상태인지 확인
- client_order_id 규칙화
- 동일 신호 재실행 시 재주문 금지
Alpaca는 client_order_id를 통해 주문 추적을 구조화할 수 있다고 공식 문서에서 설명한다. 전략별 주문 구분에도 유용하다.
20.2 시장 시간 체크
- 장중인지 확인
- 휴장일 확인
- 프리마켓/애프터마켓 사용 여부 결정
20.3 체결 확인
- 주문 제출 후 fill 여부 재검증
- 부분 체결 대응
- 미체결 취소 규칙
20.4 브래킷 주문 또는 손절 자동화
Alpaca는 브래킷 주문도 지원한다. 즉, 진입과 동시에 이익실현/손절을 체인으로 걸 수 있다.
20.5 로그와 알림
- 주문 제출 로그
- 포지션 로그
- 예외 로그
- 텔레그램/이메일/Slack 알림
21. 전략 3개를 어떻게 비교해야 하는가
단순히 최종 수익률만 보면 안 된다. 아래를 같이 봐야 한다.
21.1 총수익률
마지막 자산이 얼마나 증가했는가
21.2 CAGR
연환산 성장률
21.3 MDD
최대 낙폭
실무에서는 수익률보다 MDD를 더 중요하게 보는 경우가 많다.
21.4 Sharpe Ratio
변동성 대비 수익
21.5 거래 횟수
거래가 너무 많으면 비용과 과최적화 위험이 커진다.
21.6 승률
승률 자체보다 손익비와 함께 봐야 한다.
22. 세 전략의 성격 비교
22.1 이동평균 크로스
- 가장 설명하기 쉽고 구현이 간단함
- 강한 상승/하락 추세에서 유리
- 횡보장에서 약함
22.2 평균회귀
- 박스권 장세에서 유리할 수 있음
- 급락 추세에서 위험
- 추세 필터가 중요
22.3 돌파
- 큰 추세를 잡으면 매우 강함
- 가짜 돌파가 많음
- 손절 규칙이 매우 중요
실무적으로는 이 세 가지를 각각 단독 운용하기보다,
시장 상태에 따라 가중치를 달리하거나 병렬 운용하는 방식이 더 낫다.
23. 가장 현실적인 개선 방향
23.1 단일 종목보다 멀티 종목
예: SPY, QQQ, IWM, TLT, GLD 같은 ETF 풀에서 신호가 가장 좋은 자산만 선택
23.2 정적 수량보다 변동성 조절
ATR이 큰 종목은 적게, 작은 종목은 많이
23.3 단일 규칙보다 필터 추가
예:
- 200일선 위에서만 롱
- 거래량 증가 종목만
- 변동성 과열 구간 제외
23.4 워크포워드 테스트
2018
2021로 튜닝하고 2022
2025로 검증하는 식으로 구간을 분리
24. 초보자가 가장 많이 망가뜨리는 부분
24.1 너무 많은 파라미터 튜닝
- 5개 이상의 파라미터를 동시에 최적화하면 거의 항상 과최적화 위험이 커진다
24.2 수수료를 0으로 둠
- 백테스트가 실제보다 훨씬 좋아진다
24.3 종가 체결을 너무 쉽게 가정
- 실제로는 슬리피지가 발생한다
24.4 생존편향
- 현재 살아남은 종목만 테스트하면 과거 현실과 다르다
24.5 미래 데이터 누수
- 오늘 종가를 알고 오늘 종가에 바로 체결한 것으로 계산하면 오류다
25. “실제 계좌에 연결해서 실현 가능한가”에 대한 정확한 답
가능하다. 다만 가능함과 바로 실전 투입 가능함은 다르다.
실계좌 자동매매가 성립하려면 최소한 아래 조건을 만족해야 한다.
- 브로커 API 키 발급
- 주문/포지션/계좌조회 API 확인
- 시장 시간 및 주문 가능 자산 확인
- 예외 처리 및 재시도 로직 구현
- 페이퍼 계좌에서 충분한 기간 검증
- 실계좌 소액 운용 후 점진 확대
Alpaca는 공식적으로 종이거래 환경을 제공하고, 종이거래와 실거래의 API 스펙이 동일하다고 안내한다. /v2/orders를 통해 주문을 생성할 수 있으며, 계좌 포트폴리오 이력도 API로 조회할 수 있다.
26. 추천 실전 로드맵
가장 권장되는 로드맵은 아래와 같다.
1주차
- Python, pandas 숙달
- OHLCV 데이터 구조 이해
- 지표 계산 구현
2주차
- SMA / Mean Reversion / Breakout 전략 구현
- 백테스트 엔진 작성
- 성과지표 출력
3주차
- 멀티 종목 테스트
- 포지션 사이징 추가
- 슬리피지/수수료 반영 강화
4주차
- Alpaca 페이퍼 계좌 연결
- 주문 제출/포지션 조회/청산 코드 구현
- 로그/알림 추가
5주차 이후
- 실시간 러너 안정화
- 워크포워드 검증
- 소액 실거래 전환
27. 어떤 전략부터 시작해야 하는가
처음에는 아래 순서를 권장한다.
- 이동평균 크로스
- 구현이 가장 쉽다
- 전체 파이프라인 학습에 적합하다
- 돌파 전략
- 추세 전략의 실전 감각을 익히기 좋다
- 평균회귀
- 필터와 리스크 관리의 중요성을 배우기 좋다
즉, “수익이 가장 좋아 보이는 것”보다
구조를 가장 잘 배울 수 있는 것부터 시작하는 것이 낫다.
28. 최종 정리
정리하면 다음과 같다.
- 자동매매는 단순한 매수·매도 코드가 아니라 데이터, 전략, 리스크, 실행, 운영의 전체 시스템이다.
- 전략 3개로는 이동평균 크로스, 평균회귀, 돌파 전략이 가장 기초적이면서도 실제 연구·운용 가치가 있다.
- 실계좌 연동은 충분히 가능하며, 공식 API를 제공하는 브로커를 사용하면 Python으로 구현 가능하다.
- 다만 실전에서 가장 중요한 것은 “좋은 진입 규칙”이 아니라 손실 제한, 주문 안정성, 과최적화 방지다.
- 첫 실전은 반드시 페이퍼 계좌에서 시작해야 한다. Alpaca는 종이거래와 실거래 API 스펙이 동일하고, Python SDK와 주문 API를 공식 제공한다.
'궁금증' 카테고리의 다른 글
| 26년 인턴준비 SQL (0) | 2026.07.24 |
|---|---|
| 26년 여름 인턴 준비 리스트 (0) | 2026.07.24 |
| 26/07/24 질문 모음 (1) | 2026.07.24 |