한국투자증권(KIS) API 기반 자동매매 봇
- 전략 4종 - RSI Mean Reversion(운영 중), Heikin-Ashi Flip, RSI+HA, Momentum
- 백테스트 - 웹에서 종목·기간·파라미터를 넣고 전략별 시뮬레이션, 캔들/HA 차트와 매매 시점 오버레이
- 미국 주식 전 세션 대응 - 주간거래, 프리마켓, 정규장, 애프터마켓 세션별 스케줄링
- 세션별 KIS 라우팅 - 주간거래 주문 endpoint와 PRE/REGULAR/AFTER 정규 endpoint를 구분
- 웹 대시보드 - 실시간 포지션, RSI 모니터, 차트 (포트 9095)
- 포트폴리오 관리 - 종목별 최대 비중 설정, 파이차트 시각화
- 종목 관리 - 웹에서 실시간 종목 추가/삭제/활성화 (KIS API 검증)
- 전략 설정 - 웹에서 RSI 기간, 매수/매도 레벨, 손절 등 실시간 변경
- 페이퍼 트레이딩 - 실제 시세 + 가상 주문으로 전략 검증
- DB 기반 상태 복구 - 현재 포지션, 매수/매도 stage, 체결, 성과 이력을 재시작 후 복원
- Discord 알림 - 매수/매도 체결, 손절, 시스템 오류 알림
- 분석 - 일간/주간/월간 리포트, settlement-adjusted equity curve, 드로우다운, 승률 통계
- 양도소득세 개산 - 연도별 실현손익을 체결일 환율로 원화 환산해 예상 세액과 공제 잔여 표시
- 모바일/PWA 대시보드 - 모바일 카드형 테이블, 햄버거 메뉴, 명시적 테마 토글
전략 4종을 내장한다. 시장(market)마다 전략 인스턴스를 하나씩 붙이고, 파라미터는 웹 설정 화면에서 재시작 없이 바꾼다.
| 전략 | 클래스 | 백테스트 키 | 한 줄 요약 |
|---|---|---|---|
| RSI Mean Reversion | rsi_mean_reversion.py |
rsi |
과매도에 분할 매수, 과매수에 분할 매도 |
| Heikin-Ashi Flip | heikin_ashi_flip.py |
ha |
HA 캔들 색이 바뀌면 전량 진입/청산 |
| RSI + Heikin-Ashi | rsi_heikin_ashi.py |
rsi_ha |
위 둘이 동시에 동의할 때만 매수 |
| Momentum | momentum.py |
(미등록) | 골든크로스 진입, 데드크로스 청산 |
아래 숫자는 코드 기본값이다. 실제 운영값은 DB
strategy_configs.params_json에 있고 웹에서 바뀐다. 예를 들어 현재 3개 설정 모두 매수 사다리가35/30/25이고 NASDAQ·AMEX는stop_loss: -100으로 손절을 사실상 꺼둔 상태다.
"많이 빠지면 나눠 사고, 많이 오르면 나눠 판다." 한 번에 사고파는 대신 사다리로 쪼개서, 바닥·천장을 맞히지 않아도 평단이 개선되도록 한다.
[매수] RSI가 단계별 임계값 아래로 내려갈 때마다 1단계씩
1차: RSI <= 30 → 남은 비중의 50%
2차: RSI <= 25 → 30%
3차: RSI <= 20 → 20%
매수 금액 = 총자산 x 종목 최대비중 x 단계비율
[매도] RSI가 단계별 임계값 위로 올라갈 때마다 1단계씩 (보유량 기준)
1차: RSI >= 70 → 보유량의 30%
2차: RSI >= 75 → 40%
3차: RSI >= 80 → 50%
전량이 아니라 비율이라, 계속 오르면 일부를 남긴 채 익절이 이어진다
[손익 사다리] 평단 대비 손익률로도 단계 청산
stop_loss / take_profit 스칼라 하나만 주면 1단계,
stop_loss_levels / take_profit_levels로 여러 단계도 가능
[쿨다운] cooldown_days 경과 후 같은 단계를 다시 밟을 수 있다
(RSI가 회복될 것을 요구하지 않음)
[재진입] reentry_cooldown_days — 전량 청산 후 재매수까지의 대기
단계 번호는 DB에 저장되므로 봇을 재시작해도 "3단계까지 샀다"는 사실이 유지된다. 이게 없으면 재시작 때마다 1단계부터 다시 사서 비중이 초과된다.
Heikin-Ashi는 캔들을 평균내어 잔물결을 지운 차트다. 색이 바뀌는 지점을 추세 전환으로 보고 전량 진입·청산한다.
[매수] HA 캔들 음봉 → 양봉 전환
[매도] HA 캔들 양봉 → 음봉 전환
확정된 일봉만 읽는다. 형성 중인 캔들은 장중에 색이 바뀌므로, 그걸 보고 신호를 내면 하루에도 사고팔기를 반복하게 된다. 그래서 전환은 그 캔들이 닫힌 다음 세션 첫 틱에 집행된다 — 백테스트가 가정한 타이밍과 같다.
RSI 전략을 그대로 상속하고 매수에만 조건을 하나 더 건다: 마지막 확정 HA 캔들이 양봉이어야 한다. 사다리·손절·쿨다운은 전부 부모와 동일하다.
매도는 게이트하지 않는다. "추세가 아직 괜찮아 보인다"는 지표 때문에 손절이 막히면 손실 사다리의 존재 이유가 사라지기 때문이다.
두 조건은 구조적으로 서로 반대를 본다 — RSI 30은 계속 빠졌다는 뜻이고 HA 양봉은 오르고 있다는 뜻이다. 그래서 거래가 훨씬 드물다. 2018-2026년 14개 종목 측정에서 RSI 진입 신호 1,862건 중 **6.6%(122건)**만 HA 양봉과 겹쳤다.
역추세인 위 셋과 달리 추세 추종이다. 이동평균 교차로 진입/청산한다.
[매수] 단기 MA(10)가 장기 MA(20)를 상향 돌파 + RSI > 30
[매도] 데드크로스 또는 RSI > 70
백테스트 레지스트리(scripts/backtest_registry.py)에는 등록돼 있지 않다.
- RSI는 일봉 기준이다. 장중 현재가는 오늘 일봉의 종가 자리를 갱신해 실시간 RSI를 추정하는 데 쓴다.
- RSI 계산은 Wilder 평활법.
rsi_method파라미터로 바꿀 수 있다. - 종목별
max_weight(최대 비중)가 매수 금액의 상한이다. 사다리 단계비율은 그 안에서 나뉜다.
US 전용 스케줄러는 KST 기준으로 KIS가 지원하는 미국 주식 세션을 구분한다. 휴장일과 단축장은 XNYS 캘린더로 게이트하며, 캘린더 캐시는 재시작마다 5년 앞까지 생성한다.
| 세션 | KST 서머타임 기준 | KIS 처리 |
|---|---|---|
| 주간거래 | 09:00-17:00 | /daytime-order, TTTS6036U/6037U, 지정가 |
| 프리마켓 | 17:00-22:30 | 정규 해외주식 주문 endpoint, 지정가 |
| 정규장 | 22:30-05:00 | 정규 해외주식 주문 endpoint, 지정가 |
| 애프터마켓 | 05:00-07:00 | 정규 해외주식 주문 endpoint, 지정가 |
| CLOSED | 07:00-09:00 | 주문 불가 |
시세 조회는 세션별 venue 코드를 다르게 사용한다.
- 주간거래:
BAQ/BAY/BAA - 프리마켓, 정규장, 애프터마켓:
NAS/NYS/AMS
주간거래 활동은 로그와 Discord 알림에 [DAYTIME] 태그가 붙어 정규/프리/애프터와 구분된다.
.env 파일 생성:
# Broker selection: kis or yfinance
BROKER=yfinance
# Trading mode: paper or live. yfinance supports paper only.
TRADING_MODE=paper
# KIS API (BROKER=kis일 때 필요)
KIS_APP_KEY=your_app_key
KIS_APP_SECRET=your_app_secret
KIS_ACCOUNT_NO=your_account_number
KIS_HTS_ID=your_hts_id
# MySQL
MYSQL_ROOT_PASSWORD=your_root_password
MYSQL_DATABASE=quant
MYSQL_USER=quant
MYSQL_PASSWORD=your_password
# 대시보드 로그인 (필수)
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=change_me
# Discord (선택)
DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
# 로깅
LOG_LEVEL=INFO전체 항목은 .env.example 참고.
docker-compose up -d --buildhttp://localhost:9095
docker logs quant-bot -f| 경로 | 설명 |
|---|---|
/ |
메인 대시보드 (포지션, RSI, 시그널) |
/symbols |
종목 관리 (추가/삭제/활성화/비중 설정) |
/settings |
전략 파라미터 실시간 변경 |
/portfolio |
포트폴리오 비중 차트 |
/portfolio/correlation |
보유 종목 간 상관관계 히트맵 |
/portfolio/rsi-trend |
종목별 RSI 추이 |
/trades |
거래 내역 |
/backtest |
전략 백테스트 (종목·기간·파라미터 지정, 캔들/HA 차트) |
/performance |
DB 기반 equity curve, 성과 분석, 연도별 양도소득세 |
/analytics |
리포트, 드로우다운, 연속 손익, 종목별 통계 |
/symbol/{symbol} |
종목 상세 차트 (가격 + RSI) |
/menu |
모바일 메뉴 |
/login |
로그인 |
BeRich/
├── config/
│ └── settings.yaml # 봇 설정 (워밍업, 리스크)
├── src/
│ ├── bot/ # 봇 코어 (틱 처리, 대시보드 동기화)
│ ├── broker/
│ │ ├── kis/ # 한국투자증권 API 클라이언트
│ │ └── paper.py # 페이퍼 트레이딩 브로커
│ ├── strategy/
│ │ ├── base.py # 전략 베이스 클래스
│ │ ├── engine.py # 전략 실행 엔진
│ │ └── builtin/ # 전략 4종
│ ├── execution/ # 주문 관리자
│ ├── data/ # DB 모델 및 스토리지
│ ├── analytics/ # 리포트, 드로우다운, 통계, 양도소득세
│ ├── web/ # FastAPI 대시보드
│ ├── risk/ # 리스크 관리
│ └── utils/ # 설정, 로거, 스케줄러, 알림
├── scripts/
│ ├── run_bot.py # 봇 실행 (--web로 대시보드 동시 기동)
│ ├── run_dashboard.py # 대시보드 단독 실행
│ ├── backtest_engine.py # 전략 비의존 백테스트 엔진 (자금·체결 담당)
│ └── backtest_registry.py # API 키 → 백테스트 전략 매핑
├── tests/ # 테스트
├── docker-compose.yml
├── Dockerfile
└── .env # 환경 변수 (gitignore)
현재 봇과 웹 대시보드는 같은 코드베이스에서 동작하며, 일부 실시간 화면 상태는 src.web.app의 in-memory DashboardState를 공유한다.
DB가 source of truth인 데이터:
strategy_configs/strategy_params: 전략·종목·파라미터 설정orders/fills: 주문·체결 이력current_positions: 현재 보유 포지션과 매수/매도 stageprice_rsi: tick 경로에서 기록한 가격·RSI 이력equity_snapshots: 잔고/equity curve 히스토리, settlement adjustment 포함
재시작 시 복원되는 데이터:
- 현재 보유 포지션
- RSI 전략의 buy/sell stage 상태
- 체결 기반 trade log와 performance 지표
- DB 기반 90일 equity curve
아직 메모리 의존이 남아 있는 데이터:
- 현재 잔고/현금/PnL
- 봇 상태, pause 상태, 최근 업데이트 시각
- 최근 signal/order 이벤트
- 현재 RSI snapshot과 WebSocket broadcast 상태
목표는 봇과 웹을 DB-first 구조로 분리해 별도 프로세스/K8s deployment로 독립 실행하는 것이다. 자세한 작업 목록은 TODO.md의 "Bot/Web 완전 분리" 섹션을 따른다.
핵심 원칙:
- 봇은 DB writer, 웹은 DB reader/control-command writer로 역할 분리
- 봇/실행/브로커 코드에서
src.web.appimport 제거 - 웹은 봇 객체, 전략 인스턴스, callback을 직접 참조하지 않음
- 현재 잔고는
account_state단일 row로 관리 - 잔고·성과 히스토리는 기존
equity_snapshots유지 - pause/resume/reload/settings apply는
bot_commands큐로 전달
로드맵:
account_state,bot_status,bot_events,bot_commands추가- 봇이 잔고/status/equity/signal/order 이벤트를 DB에 기록
- 웹 Dashboard/Performance/Portfolio가 메모리 대신 DB에서 읽도록 변경
- pause/reload/settings apply를 callback 대신 command queue로 변경
- 봇/실행/브로커에서
src.web.appimport 완전 제거 src/web/app.py를 route/service 단위로 분리
전략·종목은 DB(strategy_configs 테이블)에서 관리. 웹 /symbols 페이지에서 추가/수정.
- 종목 추가 시 KIS API로 유효성 검증
- 종목별 최대 포트폴리오 비중(%) 설정 가능
- 활성/비활성 전환 시 재시작 없이 즉시 반영
웹 /settings 페이지에서 실시간 변경:
- RSI 기간, 쿨다운 일수, 재진입 쿨다운
- 매수(avg down) / 매도 RSI 레벨 및 단계별 비율
- 손절·익절 사다리 (
stop_loss_levels/take_profit_levels) — 단일 값만 넣으면 1단계
KIS API 키 없이 미국/한국 종목을 paper trading으로 테스트하려면 .env에서:
BROKER=yfinance
TRADING_MODE=paper
DASHBOARD_USERNAME=admin
DASHBOARD_PASSWORD=change_me이 모드는 yfinance에서 시세/일봉 데이터를 가져오고, 주문은 로컬 paper 계좌에서 즉시 체결 처리합니다. 실제 주문은 발생하지 않습니다. Paper cash/positions/orders/fills 상태는 기본적으로 data/yfinance_paper_state.json에 저장되어 재시작 후에도 유지됩니다.
실행:
uv run python scripts/run_bot.py --web --web-port 9095주의:
- yfinance 데이터는 지연/누락될 수 있어 실거래 판단용으로 쓰면 안 됩니다.
- 한국 종목은 yfinance suffix가 필요할 수 있습니다. 예:
005930.KS,091990.KQ. - suffix 없는 6자리 KRX 코드는 기본적으로
.KS로 조회합니다. - yfinance broker는 live trading을 지원하지 않습니다.
.env에서 BROKER=yfinance, TRADING_MODE=paper로 설정하면:
- yfinance 시세/일봉 데이터 사용
- 주문은 로컬 paper 계좌에서 가상 체결 (실제 돈 사용 안 함)
- paper 상태는
data/yfinance_paper_state.json에 유지 - 워밍업 없이 즉시 시작
- 대시보드에
PAPER배지 표시
KIS 기반 paper trading을 쓰려면 BROKER=kis, TRADING_MODE=paper와 유효한 KIS API 키가 필요합니다.
uv run --python 3.13 --locked --extra dev pytest- Python 3.13 / FastAPI / SQLAlchemy (async)
- MySQL 8.0 / Docker Compose
- KIS Open API (한국투자증권)
- LightweightCharts 4.2 (차트)
- Discord Webhooks (알림)