백엔드 없이 띄울 수 없어, Flutter 앱을 웹으로 빌드하고 모의(paper) 데이터를 주는 가짜 API에 연결해 찍었다
대시보드
비상 정지 확인
주문 내역
내 전략
흐름
주문 실행 흐름
TICK시세 수신
→SIGNAL전략 판단
→PENDING주문 먼저 기록
→BROKER증권사 호출
→FILLED체결 반영
같은 주문 ID → 건너뜀이미 처리한 client_order_id는 다시 보내지 않음
비상 정지 중 → 주문 차단실행 직전에 비상 정지를 다시 확인
증권사 오류 → 최대 3번 재시도리스크 오류는 재시도하지 않음
일일 손실 한도 도달 → 비상 정지 자동 발동발동 사유를 감사 로그 테이블에 기록
모든 주문은 한 실행기(OrderExecutionFacade)를 거친다.
문제 해결 과정
주문이 나가기 직전까지 다시 확인 — 실행 파이프라인
문제
전략이 주문을 판단한 시점과 실제로 나가는 시점 사이에 비상 정지가 걸리거나, 재시도 때문에 같은 주문이 두 번 나갈 수 있었다
해결 — 무엇을 바꿨나
주문 실행을 한 곳으로 모아 ① client_order_id로 이미 처리한 주문인지 확인 ② 실행 직전 비상 정지 재확인 ③ 증권사 호출 전에 PENDING 주문을 먼저 기록 ④ 결과 반영 ⑤ 매수는 평균 단가, 매도는 실현 손익으로 정산하는 순서로 처리했다. 증권사 오류만 최대 3번 재시도하고, 체결 확인은 새로 체결된 수량만 반영해 두 번 세지 않는다
결과
실행 시점 기준으로 비상 정지를 지키고, 같은 주문이 두 번 나가지 않는다
손실이 쌓이면 스스로 멈추기 — 일일 손실 한도
문제
자동매매는 사람이 보지 않는 동안에도 손실이 계속 쌓일 수 있다
해결 — 무엇을 바꿨나
매도 체결마다 실현 손익을 Redis에 한국 날짜별로 누적(INCRBYFLOAT)하고, 한도 아래로 떨어지면 비상 정지를 자동으로 걸어 신규 주문을 막았다. 발동 사유는 감사 로그 테이블에 남기고, 앱에서 해제 · 이력 조회를 할 수 있게 했다
결과
하루 손실이 한도에 닿는 순간부터 신규 주문이 나가지 않는다
설계 결정
실전 모드 기동 차단
기본은 모의(paper) 모드이고, 실전(live) 모드인데 비밀 키가 기본값이거나 증권사 실전 설정이 하나라도 비어 있으면 서버가 아예 켜지지 않는다.
포트 & 어댑터 구조
기능마다 ports.py(인터페이스)를 두고 DB · Redis · 증권사는 어댑터로 붙여, 리스크 가드와 주문 실행은 테스트에서 메모리 가짜 구현으로 바꿔 끼운다.
증권사 토큰 · 시세 캐시
KIS 토큰은 만료 5분 전까지만 Redis에 캐시하고 6시간마다 새로 받는다. 시세는 30초 동안 캐시하며 전략 러너에 바로 전달한다.
검증
백엔드 테스트 34개
리스크 가드 14 · 주문 실행 12 · 설정(실전 모드 차단) 4 · 전략 엔진 2 · 주문 서비스 2개.
GitHub Actions CI
백엔드는 ruff 검사 · 앱 import 확인 · pytest, Flutter 앱은 analyze · test를 돌린다.