페이퍼 → 라이브
quant-ai의 라이브 모드는 다단계 게이트로 보호됩니다. 본 페이지는 운영자가 사용자의 라이브 승급을 검토 / 승인 / 사후 모니터링하는 절차를 다룹니다. 사용자 시점의 절차는 튜토리얼 — 라이브 모드로 졸업 참조.
게이트 4종
flowchart LR
A[페이퍼 검증<br/>90d / 50 trades] --> B[HMAC confirm token<br/>12h 유효]
B --> C[reCAPTCHA v3<br/>봇 차단]
C --> D[ALLOW_LIVE=1<br/>VM 호스트 환경변수]
D --> E[FEATURE_EQUITY_LIVE=true<br/>compose env]
E --> F[LIVE_DAILY_TRADE_LIMIT<br/>일일 상한]
| 게이트 | 위치 | 책임 | 우회 가능? |
|---|---|---|---|
| 페이퍼 검증 | OperationsService.is_live_ready() | 사용자가 충분히 검증했는지 | 운영자가 RISK_PAPER_MIN_* 임시 하향 |
| HMAC confirm | OperationsService.issue_confirm_token | 이메일 소유 검증 | 불가 (DB에 sha256 hash만) |
| reCAPTCHA | API 미들웨어 | 봇 차단 | RECAPTCHA_SECRET_KEY 빈값이면 스킵 (개발용) |
ALLOW_LIVE | VM 호스트 env | prod 이중 잠금 | 호스트 root 접근 필요 |
LIVE_DAILY_TRADE_LIMIT | API 라우터 | 폭주 방지 | 환경변수 변경 |
사용자 승급 흐름 (운영자 시점)
1. 사전 요건 확인
| 변수 | 운영자 점검 |
|---|---|
RISK_PAPER_MIN_DAYS | 기본 90. 임시 하향 시 사후 사용자별 모니터링 의무 |
RISK_PAPER_MIN_TRADES | 기본 50 |
RECAPTCHA_SECRET_KEY | 운영에서 반드시 설정. 빈값이면 검증 스킵 |
LIVE_CONFIRM_SECRET | 빈값이면 AUTH_SECRET_KEY 폴백. placeholder 금지 (changeme 등) |
SMTP_* | 비어 있으면 confirm 토큰을 로그에만 남김 (개발용). 운영에서는 SMTP 필수 |
OperationsService._resolve_secret()이 placeholder 값을 거부하므로, 잘못
설정된 운영 환경은 부팅 시점에 즉시 실패합니다.
2. 사용자 승급 요청 처리
사용자가 UI에서 "라이브 승급" 버튼을 누르면:
1. POST /api/operations/live/readiness
→ server/services/operations.py::is_live_ready()
→ PaperStats(days_active, paper_trades_count) 계산
→ 두 임계 모두 통과 시 ok=True
2. POST /api/operations/live/confirm/issue
→ issue_confirm_token() — HMAC 토큰 12h 유효
→ DB에 sha256 hash만 저장, 평문은 SMTP로 1회 전송
3. 사용자가 이메일에서 토큰을 가져옴 / reCAPTCHA 통과
4. POST /api/operations/live/enable {confirm_token}
→ consume_confirm_token() — HMAC 검증 + DB row burn
→ set_live_enabled() — UserSettingsRow.live_enabled[asset_class]=true
운영자 개입 포인트:
- 임계 미달인데 승급 요청 → 페이퍼 검증 페이지로 안내. 임시 하향이 필요한 경우 사후 추적 의무.
- confirm 이메일 미수신 → SMTP 로그 확인. 토큰 평문은 DB에 없으므로
재발급 (
issue_confirm_token이 기존 활성 토큰을 자동 비활성화). - 사용자가 봇이 의심됨 → reCAPTCHA 점수 확인 후 수동 차단.
3. 첫 라이브 주문
OperationsService.mark_first_live_order()이 첫 호출 시점을 기록합니다.
# 첫 라이브 주문 시 broker layer가 첫 호출에 한해 True를 받음 →
# broker_order_events에 first_live_order=true 마커
existing = settings.live_first_order_at.get(asset_class.value)
if not existing:
settings.live_first_order_at[asset_class.value] = now.isoformat()
return True
return False
운영자는 Grafana — Multi-Asset Overview
에서 broker_order_events 첫 라이브 마커가 뜨면 사용자별 모니터링을
시작합니다 — 첫 라이브 24h는 가장 위험한 윈도우입니다.
4. 일일 상한
# server/services/operations.py
def increment_live_daily_count(user_id, asset_class) -> int:
# UTC 자정에 자동 리셋
bucket = settings.live_daily_count[asset_class.value]
if bucket["date"] != today:
bucket = {"date": today, "count": 0}
bucket["count"] += 1
return bucket["count"]
기본 LIVE_DAILY_TRADE_LIMIT=10. 도달 시 API가 429를 반환하고 다음
UTC 자정까지 사용자별 라이브 주문 차단.
운영자가 일시 상향이 필요한 경우:
# .env
LIVE_DAILY_TRADE_LIMIT=20
docker compose restart api
상향 후 사용자에게 알림. 영구 변경은 디자인 리뷰 필요.
절차: 신규 사용자 라이브 승급 검토
# 1. 사용자 페이퍼 통계 (운영자 admin API 또는 직접 DB)
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT user_id, asset_class, mode, COUNT(*) AS n,
MIN(executed_at) AS oldest
FROM equity_trades
WHERE user_id = <USER_ID> AND mode='paper'
GROUP BY user_id, asset_class, mode;
"
# 2. UserSettings 현재 상태
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT user_id, live_enabled, live_first_order_at, live_daily_count
FROM user_settings WHERE user_id = <USER_ID>;
"
# 3. 미사용 confirm token 정리 (필요 시)
docker compose exec timescaledb psql -U quant -d quantai -c "
UPDATE live_confirm_tokens SET is_active = false
WHERE user_id = <USER_ID> AND consumed_at IS NULL;
"
검증
-
is_live_ready가ok=True -
live_confirm_tokens.is_active = true인 1건이 있고 만료 12h 내 - 사용자가 confirm 이메일 평문을
/enable에 제출 시consume_confirm_token()이 True -
user_settings.live_enabled[asset_class]= true - 첫 라이브 주문 후
broker_order_events에 first_live_order 마커 - Daily Loss Monitor 에서 해당 사용자가 표에 등장
회수 (라이브 비활성)
특정 사용자의 라이브를 즉시 비활성화하려면:
docker compose exec timescaledb psql -U quant -d quantai -c "
UPDATE user_settings
SET live_enabled = jsonb_set(live_enabled, '{us_equity}', 'false'::jsonb)
WHERE user_id = <USER_ID>;
"
이는 사용자 단위 emergency_stop과는 다릅니다 — emergency_stop은 24h 잠금이며 비상 정지에서 다룹니다.
트러블슈팅
| 증상 | 원인 / 조치 |
|---|---|
is_live_ready 가 항상 false | equity_trades.mode='paper' 가 한 건도 없음. 페이퍼 모드 활성 여부 확인 |
| Confirm 이메일이 평문 토큰을 안 보여줌 | SMTP 미설정 → API 로그에 confirm_token_plaintext=... 검색 (개발 환경 한정) |
consume_confirm_token 이 False만 반환 | (a) 토큰 만료 12h 경과 (b) 다른 사용자가 발급 (c) 이미 consumed. live_confirm_tokens 직접 조회 |
| 라이브 활성화는 됐는데 주문이 503 | FEATURE_EQUITY_LIVE 또는 ALLOW_LIVE가 OFF (prod 이중 잠금). VM env 확인 |
LIVE_DAILY_TRADE_LIMIT 도달 | UTC 자정에 자동 리셋. 즉시 필요 시 상향 + api 재시작 |
관련 페이지
- 피처 플래그
- 환경변수 카탈로그
- 비상 정지
- 포지션 리컨실러
- 튜토리얼 — 라이브 모드로 졸업
- 코드:
server/services/operations.py