포지션 리컨실러
position-reconciler는 5분 주기로 각 활성 사용자에 대해 브로커의 실제
포지션과 DB의 positions 테이블을 비교하고, 불일치를 audit / 알림 /
emergency_stop 단계로 처리합니다. 단일 replica 설계 — 한 사이클이 충분히
싸므로 다중화로 얻는 이득이 없고 broker API 호출만 두 배가 됩니다.
동작 흐름
flowchart LR
POLL[POLL_INTERVAL<br/>5분] --> USERS[활성 사용자 목록]
USERS --> CONCUR[MAX_CONCURRENT<br/>5명 병렬]
CONCUR --> CALL[broker.get_positions]
CALL --> DIFF{불일치?}
DIFF -->|< TOLERANCE_PCT| INFO[severity: info<br/>로그만]
DIFF -->|>= TOLERANCE_PCT| WARN[severity: warning<br/>Telegram]
DIFF -->|>= CRITICAL_PCT| KILL[severity: critical<br/>emergency_stop]
INFO --> AUDIT[broker_order_events<br/>1행 audit]
WARN --> AUDIT
KILL --> AUDIT
환경변수
| 변수 | 기본값 | 설명 |
|---|---|---|
RECONCILER_POLL_INTERVAL_SECONDS | 300 | 사이클 주기. 짧을수록 빠른 탐지 / broker rate limit 위험 |
RECONCILER_TOLERANCE_PCT | 0.01 | 1% 미만 차이는 info (실시간 시세 noise) |
RECONCILER_CRITICAL_PCT | 0.05 | 5% 이상 차이 → emergency_stop 자동 발동 |
RECONCILER_MAX_CONCURRENT_USERS | 5 | 한 사이클당 동시 검사 사용자 수 |
시작 / 정지 / 재시작
# 시작
docker compose up -d position-reconciler
# 정지 (graceful 30s — rolling_restart.sh와 동일 정책)
docker compose stop -t 30 position-reconciler
# 재시작
docker compose restart position-reconciler
# 로그
docker compose logs -f --tail=100 position-reconciler
audit 행 형식
매 사이클은 사용자별로 broker_order_events에 정확히 1행을 기록합니다 —
"리컨실러가 X시 Y분에 사용자 N의 자산군 Z를 검사했다"는 fact만 기록.
SELECT received_at, user_id, asset_class,
event_type, -- 'reconcile_pass'
payload->>'severity', -- 'info' / 'warning' / 'critical'
payload->>'broker_qty',
payload->>'db_qty',
payload->>'diff_pct'
FROM broker_order_events
WHERE event_type = 'reconcile_pass'
ORDER BY received_at DESC LIMIT 20;
severity 임계값 튜닝
기본 임계는 보수적이며 운영 데이터에 맞춰 조정해야 합니다.
| 시나리오 | 권장 |
|---|---|
| 거래량이 적은 인스턴스 | 기본값 유지 (0.01 / 0.05) |
| 빠른 변동성 (1m bar 적극 활용) | TOLERANCE_PCT=0.02로 완화 — false positive 감소 |
| 다수 미체결 주문 | 위와 같음. pending 주문이 broker 측에서 부분 체결되면 0.5% 진동 정상 |
| 고빈도 KIS 주문 | POLL_INTERVAL=180 (3분)로 단축 — KIS는 fill 통보가 1~2분 지연 |
| 디버깅 (모든 차이를 보고 싶음) | TOLERANCE_PCT=0.0로 임시 → 모든 차이가 warning 이상으로 |
변경 후:
# .env 갱신 → reconciler만 재시작
sed -i 's/^RECONCILER_TOLERANCE_PCT=.*/RECONCILER_TOLERANCE_PCT=0.02/' .env
docker compose restart position-reconciler
emergency_stop 발동 시
severity=critical 행이 나오면 reconciler는 사용자 × 자산군 단위로
emergency_stop을 호출합니다. 이는 비상 정지
의 0단계와 같은 메커니즘입니다.
1. broker.get_positions(user) vs SELECT * FROM positions WHERE user_id=...
2. Σ|broker_qty - db_qty| / Σ|db_qty| > CRITICAL_PCT
3. broker_order_events에 severity=critical 행
4. BotManager.get_manager(user.id, asset_class).emergency_stop()
5. Telegram 알림: "RECONCILE CRITICAL user=N asset=Z diff=7.3%"
6. bot_states.emergency_stop_until = now + 24h
운영자는 24h 안에 사후 분석 후 해제할 수 있습니다 — 단, 잘못된 DB 상태가 원인이라면 먼저 데이터 보정 후 해제해야 다음 사이클에서 다시 critical로 잡히지 않습니다.
# 사용자별 마지막 reconcile audit 확인
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT received_at, payload
FROM broker_order_events
WHERE user_id = <USER_ID>
AND event_type = 'reconcile_pass'
ORDER BY received_at DESC LIMIT 5;
"
테스트 시 임계 비활성
테스트넷 / 페이퍼 환경에서는 상향:
# .env (test 전용)
RECONCILER_CRITICAL_PCT=1.0 # 100% 미만은 critical 안 됨
RECONCILER_POLL_INTERVAL_SECONDS=60 # 빠른 피드백
단일 replica 원칙
# docker-compose.yml (의도적으로 deploy.replicas 미지정)
position-reconciler:
command: python -m src.execution.reconcile_main
docker compose up --scale position-reconciler=2를 실행하면 두 인스턴스가
같은 사용자를 동시에 검사하게 되어 broker rate limit이 두 배가 됩니다.
스케일 업이 정말 필요한 경우는 사용자 수가 수백 명 이상일 때만이며,
그 시점에는 사용자 ID 해시로 partition을 두는 PR이 필요합니다.
검증
# 1. 컨테이너 동작
docker compose ps position-reconciler
# Up 상태
# 2. 한 사이클이 정상적으로 audit를 남기는지
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT MAX(received_at), COUNT(*)
FROM broker_order_events
WHERE event_type = 'reconcile_pass'
AND received_at > NOW() - INTERVAL '15 minutes';
"
# 활성 사용자 수만큼의 row가 마지막 15분 안에 있어야 함
# 3. severity 분포
docker compose exec timescaledb psql -U quant -d quantai -c "
SELECT payload->>'severity' AS severity, COUNT(*)
FROM broker_order_events
WHERE event_type = 'reconcile_pass'
AND received_at > NOW() - INTERVAL '1 hour'
GROUP BY 1;
"
트러블슈팅
| 증상 | 원인 / 조치 |
|---|---|
| reconciler가 부팅 즉시 종료 | DB 컬럼 부재 → alembic upgrade head (특히 09_positions_table) |
| 모든 사이클이 warning | 임계가 너무 빡빡 — TOLERANCE_PCT 상향 |
| broker rate limit (429 다발) | MAX_CONCURRENT_USERS 하향 또는 POLL_INTERVAL 상향 |
| critical로 잡혔지만 broker 쪽이 정확함 | DB가 stale. 마지막 fill 이벤트 확인. 필요 시 positions를 broker 값으로 보정 후 emergency_stop 해제 |
| 새 사용자가 audit에 안 나타남 | users.is_active / user_settings.live_enabled 확인. reconciler는 활성 사용자만 검사 |
| 사용자 1명만 누락 | 그 사용자의 거래소 키가 무효 (Fernet 복호화 실패) → 브로커 401 |
관련 페이지
- 환경변수 카탈로그
- 비상 정지
- 브로커 401
- Grafana — Broker Health
- 코드:
src/execution/reconcile_main.py