비상 정지
quant-ai는 다섯 단계 비상 정지 절차를 갖습니다 — 가장 빠른 광역 차단부터 세밀한 사용자 단위 잠금까지. 상황별로 가장 작은 단위에서 큰 단위로 넘어가는 것이 원칙입니다.
비상 정지 단계
| 단계 | 영향 범위 | 트리거 | 해제 |
|---|---|---|---|
| 0 | 자동: 사용자 × 자산군 | 일일 손실 -5% | 24h 자동 잠금, 운영자 재활성화 |
| 1 | 사용자 1인 라이브 | user_settings.live_enabled[ac]=false | 즉시 |
| 2 | 자산군 전체 라이브 | FEATURE_EQUITY_LIVE=false (compose env) | 즉시 |
| 3 | 모든 라이브 | ALLOW_LIVE=0 (VM 호스트 env) | 즉시 |
| 4 | 모든 거래 (페이퍼 포함) | API 컨테이너 정지 | 수동 재기동 |
0. 자동 kill switch
트리거
-- 사용자 × 자산군 별 일일 PnL%
WITH per_user AS (
SELECT user_id,
asset_class,
COALESCE(SUM(realized_pnl), 0) + COALESCE(SUM(unrealized_pnl), 0) AS daily_pnl,
GREATEST(SUM(ABS(entry_price * size)), 1) AS notional
FROM positions
WHERE COALESCE(closed_at, opened_at) >= date_trunc('day', NOW())
GROUP BY user_id, asset_class
)
SELECT user_id, asset_class, daily_pnl / notional * 100.0 AS pct
FROM per_user
WHERE daily_pnl / notional * 100.0 <= -5.0;
해당 사용자 × 자산군의 BotManager 인스턴스가 emergency_stop()을 호출
받고:
- 모든 미체결 주문 취소
- (옵션) 보유 포지션 시장가 청산
bot_states테이블에emergency_stop_until = now + 24h기록- Telegram 알림 발송 (사용자 + ops 양쪽)
임박 알림 (-4%)
Daily Loss Imminent Grafana 룰이 -4% 도달 시 critical 알림을 Telegram +
이메일로 보냅니다. 이 시점에 운영자가 다음을 결정할 수 있습니다.
- 사용자에게 자발적 정지 권유
- 단계 1로 라이브 비활성화
해제
24h 자동 만료 또는 운영자 수동:
# 사용자 × 자산군 잠금 해제
docker compose exec timescaledb psql -U quant -d quantai -c "
UPDATE bot_states
SET emergency_stop_until = NULL
WHERE user_id = <USER_ID> AND asset_class = '<us_equity|kr_equity|crypto>';
"
# (옵션) UserSettings도 재활성화 — 자동 만료 후라면 자동으로 다시 가능
docker compose exec timescaledb psql -U quant -d quantai -c "
UPDATE user_settings
SET live_enabled = jsonb_set(live_enabled, '{us_equity}', 'true'::jsonb)
WHERE user_id = <USER_ID>;
"
24h가 가짜 윈도우는 아니다
24h 잠금은 사용자에게 손실 회복 충동(revenge trading)을 막기 위한 의도적 쿨다운입니다. 명백한 시스템 버그 (잘못된 PnL 계산 등)가 원인이 아닌 한, 운영자가 24h 안에 해제하지 않습니다.
1. 사용자 1인 라이브 차단
특정 사용자의 의심 행동 / 격리 사고:
# 즉시 비활성
docker compose exec timescaledb psql -U quant -d quantai -c "
UPDATE user_settings
SET live_enabled = jsonb_build_object(
'us_equity', false,
'kr_equity', false,
'crypto', false)
WHERE user_id = <USER_ID>;
"
# 진행 중 봇 매니저도 강제 정지 (admin API)
curl -X POST -H "Authorization: Bearer <ADMIN_JWT>" \
http://localhost:8000/api/admin/users/<USER_ID>/bot/stop
2. 자산군 전체 라이브 차단
특정 브로커 5xx 폭주 등 자산군 단위 인시던트:
# .env에서 해당 브로커만 OFF
sed -i 's/^FEATURE_ALPACA_BROKER=.*/FEATURE_ALPACA_BROKER=false/' .env
# 또는
sed -i 's/^FEATURE_KIS_BROKER=.*/FEATURE_KIS_BROKER=false/' .env
docker compose restart api analysis-worker
3. 모든 라이브 차단 (가장 빠름)
VM 호스트 env만 끄면 됩니다 — .env 안의 FEATURE_EQUITY_LIVE는 그대로
두어도 안전합니다 (이중 잠금).
ssh azureuser@<vm-ip> '
cd ~/quantai
sed -i "s/^ALLOW_LIVE=.*/ALLOW_LIVE=0/" .env
sudo -E docker compose -f docker-compose.prod.yml restart api analysis-worker position-reconciler
'
# 검증
ssh azureuser@<vm-ip> 'curl -s http://localhost:8000/health/flags | jq .FEATURE_EQUITY_LIVE'
# false
해제는 ALLOW_LIVE=1로 되돌리고 같은 컨테이너 재시작.
4. 모든 거래 차단 (최후)
API 컨테이너 자체를 정지합니다. 페이퍼 / 라이브 / 분석 / 사용자 로그인까지 전부 503이 됩니다.
docker compose stop api
# 진행 중 거래소 주문이 있다면 사용자 안내 후
# Grafana — Broker Health 에서 미체결 주문 청산 여부 확인
재기동:
docker compose start api
# /health 200 확인 후 정상 동작
curl -fsS http://localhost:8000/health
인시던트 워크플로우
flowchart TD
A[알림 수신<br/>Telegram] --> B{영향 범위?}
B -->|사용자 1인| C[Step 1: live_enabled=false]
B -->|자산군 전체| D[Step 2: FEATURE_*_BROKER=false]
B -->|모든 라이브| E[Step 3: ALLOW_LIVE=0]
B -->|시스템 광역| F[Step 4: API 정지]
C --> G[Grafana 진단]
D --> G
E --> G
F --> G
G --> H[패치 / 재개]
사후
비상 정지 후에는 항상:
- 인시던트 티켓 (severity / scope / actions)
- 영향 사용자에게 Telegram + 이메일 사후 안내
- 5 Whys 포스트모템 (24h 내)
- 재발 방지 PR (테스트 / 알림 룰 / 임계 조정)
검증
# 현재 라이브 활성 여부
curl -s http://localhost:8000/health/flags | jq .
# 사용자별 emergency_stop 잠금 상태
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT user_id, asset_class, emergency_stop_until
FROM bot_states
WHERE emergency_stop_until > NOW();
"
# 미체결 주문 / 보유 포지션 잔량
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT user_id, asset_class, COUNT(*) AS open_positions
FROM positions WHERE closed_at IS NULL
GROUP BY user_id, asset_class;
"
트러블슈팅
| 증상 | 원인 / 조치 |
|---|---|
ALLOW_LIVE=0인데 라이브 주문이 들어감 | API 재시작 누락. docker compose restart api |
| Step 1 후에도 봇이 주문 시도 | BotManager가 메모리 캐시에 가지고 있음. admin API로 명시적 stop 호출 |
| 잠금 해제 후에도 첫 주문이 거부 | 24h 윈도우가 아직 진행 중. bot_states.emergency_stop_until 확인 |
| Telegram 알림이 폭주 | Grafana repeat_interval을 늘리거나 임시로 룰 mute |
| reconciler가 정지된 봇에서도 액션 | 정상. 리컨실러는 broker vs DB 정합성을 위해 bot 상태와 무관하게 동작 → 포지션 리컨실러 |