토스증권 보유 주식 대시보드를 실시간으로 연동해서, 익절·손절 조건까지 원클릭으로 설정할 수 있게 만든 Phase 7-7 개발기다.
📚 토스 자동매매 개발기 시리즈 전체 목차 (현재 Phase 7-7 / 전체 25편)
- #0 프로젝트 소개 및 기반 설계
- #0-1 통화 단위 버그 수정기
- #3 설정 분리 & 재시도 로직 통합
- #4 설정·재시도 로직 후속 개선
- #5 pytest 단위 테스트 도입
- #6 로그 로테이션 적용기
- #7 전략 클래스 설계
- #8 멀티 종목 모니터링 구현
- #9 데이터 영속성 설계
- #10 DB 모듈 구현
- #11 DB 통합 적용기
- #12 전략 플러그인 인터페이스
- #13 백테스트 엔진 구축
- #14 캔들 API 페이지네이션 처리
- #15 읽기 전용 대시보드 구축
- #16 텔레그램 제어 로직 (1)
- #17 텔레그램 제어 로직 (2)
- #18 스케줄러·워치독 구현
- #19 주문 한도 가드 적용
- #20 손실 한도 자동 일시정지
- #21 이중 채널 알림 시스템
- #22 보안 진단 기능 추가
- #23 종목별 익절·손절 사이클
- #24 FastAPI 대시보드 & 전략 설정 DB화
- #25 토스증권 보유 주식 대시보드 & 원클릭 조건 설정 (현재 글)
⬅ 이전 글: Phase 7-6 — FastAPI 대시보드 & 전략 설정 DB화
현재 위치: Phase 7-7 / 전체 25편
💡 이번 Phase 7-7 요약
- 목적: 토스증권 보유 주식 대시보드를 실시간 연동하고, 그 옆에서 바로 익절/손절 조건을 설정할 수 있게 만들었다.
- 핵심 포인트: 종목코드만 나오던 표를
get_display_name()유틸리티로 “삼성전자 (005930)” 형태로 통일해서 가독성을 확 끌어올렸다. - 검증:
python-multipart연동 포함 pytest 130개 100% 통과, DRY_RUN 모드에서 가상 매수→익절/손절 가상 매도 전체 사이클 검증 완료.
토스증권 보유 주식 대시보드가 없으니 정작 내가 뭘 들고 있는지 확인하려면 매번 토스 앱을 켜야 했다. 봇은 잘 돌아가는데 “지금 계좌 상태가 어떤지”는 여전히 딴 데서 봐야 하는 게 이상했다. 종목코드 몇 자리만 보고 삼성전자인지 SK하이닉스인지 헷갈리는 것도 은근 스트레스였고. 그래서 이번 Phase에서는 보유 주식 시각화랑, 그 자리에서 바로 조건을 걸 수 있는 원클릭 폼을 붙이는 걸 목표로 잡았다.
1. 토스증권 API 보유 주식 시각화 & 원클릭 조건 설정 (dashboard.py)
toss_portfolio.fetch_holdings()로 계좌 보유 종목의 수량, 평단가, 현재가, 평가손익, 수익률을 가져와서 대시보드 상단 표에 뿌렸다. 여기까지는 단순 조회였는데, 문제는 “이 종목 익절 3%로 걸어야지” 싶을 때마다 다시 전략 설정 화면으로 넘어가야 했다는 거다. 클릭 수가 늘어날수록 귀찮아서 결국 안 쓰게 되더라. 그래서 보유 주식 표 각 행에 inline Form을 바로 붙였다.
<form action="/api/strategies" method="post">
<input type="hidden" name="symbol" value="{symbol}">
<input type="number" name="target_price" placeholder="매수 목표가">
<input type="number" name="take_profit_pct" placeholder="익절 %" step="0.1">
<input type="number" name="stop_loss_pct" placeholder="손절 %" step="0.1">
<button type="submit">즉시 저장</button>
</form>
FastAPI에서 이 폼 데이터를 받으려면 python-multipart 패키지가 필요하다. 처음엔 이걸 빼먹고 테스트했더니 서버가 RuntimeError: Form data requires "python-multipart" to be installed. 를 그대로 뱉어서 원인 찾는 데 좀 걸렸다. 에러 메시지만 보면 딱 패키지 이름까지 알려주는데도 처음엔 “왜 폼이 안 먹지” 하면서 프론트 코드부터 의심했던 게 함정이었다. pip install python-multipart 한 줄로 해결됐지만, requirements.txt에 안 박혀 있으면 배포 환경에서 또 똑같이 터질 걸 알기에 바로 추가해뒀다. 폼 처리 방식이 헷갈리면 FastAPI 공식 Form 데이터 처리 가이드를 참고하면 된다.
라우트는 두 개로 나눴다. 저장은 POST /api/strategies, 삭제는 POST /api/strategies/delete. 저장 로직에서 기존 조건이 있으면 UPDATE, 없으면 INSERT 하도록 strategy_configs 테이블에 UPSERT 패턴을 넣었다. SQLite에서는 ON CONFLICT ... DO UPDATE 구문으로 이걸 한 줄에 처리할 수 있는데, 자세한 문법은 SQLite 공식 UPSERT 문서에 잘 정리돼 있다.
INSERT INTO strategy_configs (symbol, target_price, take_profit_pct, stop_loss_pct)
VALUES (?, ?, ?, ?)
ON CONFLICT(symbol) DO UPDATE SET
target_price = excluded.target_price,
take_profit_pct = excluded.take_profit_pct,
stop_loss_pct = excluded.stop_loss_pct;
2. 종목명 캐싱과 주식 포트폴리오 표시 개선 (db.py)
지금까지는 표마다 종목코드 6자리만 덩그러니 있었다. 매번 “005930이 뭐였지” 하면서 검색하는 게 은근 시간 잡아먹었다. 그래서 SQLite에 stock_names 캐시 테이블을 새로 만들어서, API에서 종목명을 받아올 때마다 save_stock_name(code, name)으로 저장해뒀다.
def get_display_name(code: str) -> str:
"""005930 -> '삼성전자 (005930)' 형태로 변환. 캐시에 없으면 코드만 반환."""
name = db.get_stock_name(code)
return f"{name} ({code})" if name else code
이 함수 하나를 보유 주식 목록, 거래 조건 목록, 최근 거래 이력, 최근 알림 이력 — 네 군데 표에 전부 적용했더니 주식 포트폴리오 시각화가 눈에 띄게 좋아졌다. 종목코드만 보고 “이게 뭐였지” 하며 검색하던 일이 없어진 게 생각보다 컸다. 캐시가 비어있는 신규 종목은 일단 코드만 표시되다가, 다음 API 호출 때 자동으로 이름이 채워지는 방식이라 별도 초기화 작업 없이도 자연스럽게 채워진다.
3. 조건 동기화 & 익절·손절 자동화 사이클 (auto_trader.py)
웹에서 조건을 바꿨는데 봇을 재시작해야 반영되면 의미가 없다. 그래서 감시 루프가 매 주기마다 DB에서 최신 조건을 다시 읽어오도록 했다. 웹 폼에서 목표가를 수정하면 다음 감시 주기(기본 60초)에 바로 반영된다.
DRY_RUN = True 상태에서 시나리오를 돌려봤다. 가상 종목 삼성전자(005930)를 목표가 70,000원에 걸어두고, 시뮬레이션 시세가 그 아래로 내려가면 가상 매수 → 평단가 대비 +5% 도달 시 익절 가상 매도, -3% 도달 시 손절 가상 매도가 발동되도록 짰다. 매도가 발동되면 trades 테이블에 SELL 레코드가 남고, 알림도 같이 나간다.
가상 계좌 기준: 삼성전자(005930) 10주, 평단가 70,000원 → 현재가 73,500원 도달 시 익절 매도 발동 (실제 계좌 데이터 아님, 전부 마스킹 처리)
이 사이클을 며칠 동안 DRY_RUN으로 돌려보면서, 목표가를 웹에서 바꿨을 때 실제로 다음 주기에 반영되는지도 로그로 확인했다. 재시작 없이 조건이 바뀌는 걸 눈으로 확인하니 그제서야 “이제 진짜 쓸만하네”라는 생각이 들었다.
4. 알림 이중화 & 보안 진단 통합 (notifier.py / security_check.py)
WARNING 이상 등급 이벤트는 텔레그램과 이메일(SMTP)로 동시에 나간다. 한쪽 채널이 죽어도 다른 쪽으로 알림이 살아있게 하려는 목적이다. security_check.py는 .env 파일이 Git에 안 들어갔는지, 5중 안전 브레이크가 정상 작동 중인지, OAuth2 토큰과 계좌 연결 상태가 살아있는지를 계좌 잔고나 실제 값을 건드리지 않고(비파괴) 점검해서 리포트로 뽑아준다.
5. 검증: pytest 130개
이번 스프린트에서 새로 추가한 폼 라우트, 캐싱 로직, 가상 매수-매도 사이클까지 포함해서 pytest 스위트를 돌렸다. python-multipart 의존성 이슈를 해결한 뒤로는 폼 관련 테스트도 전부 초록불이었다.
$ pytest -v
...
130 passed in 8.42s
토스증권 보유 주식 대시보드와 원클릭 익절/손절 설정 기능을 자동매매 봇에 추가했다. 종목코드 대신 “종목명 (코드)” 형태로 표시하는 캐싱 유틸리티를 만들었고, DRY_RUN 모드에서 매수→익절/손절 매도 전체 사이클을 검증했다. python-multipart 연동 포함 pytest 130개가 모두 통과했다.
FAQ
Q1. python-multipart는 왜 필요한가요?
FastAPI가 HTML <form> 데이터를 파싱하려면 이 패키지가 필요합니다. 없으면 RuntimeError: Form data requires "python-multipart" to be installed. 오류가 발생합니다.
Q2. DRY_RUN 모드에서 실제 매매가 발생하나요?
아니요. DRY_RUN=True 상태에서는 실제 주문이 나가지 않고 로그와 DB 기록만 남는 가상 매매 시뮬레이션입니다.
Q3. 종목명 캐시는 얼마나 자주 갱신되나요?
API 조회 시점마다 save_stock_name()이 호출돼 최신 종목명으로 덮어씁니다. 별도 배치 갱신은 없습니다.
Q4. UPSERT 패턴을 쓰는 이유는 뭔가요?
같은 종목에 조건을 두 번 저장할 때 매번 존재 여부를 먼저 조회(SELECT)하고 분기하는 대신, ON CONFLICT 구문으로 INSERT/UPDATE를 한 번에 처리해서 쿼리 왕복 횟수를 줄일 수 있기 때문입니다.
Q5. 이 시리즈 코드는 공개되어 있나요?
현재는 학습/기록 목적의 비공개 개인 프로젝트로, 핵심 로직 위주로 블로그에 정리하고 있습니다.