feat: 멀티테넌트 1단계 — 사용자별 KIS 계좌·포트폴리오·수동매매 (MULTI_TENANT_ENABLED 기본 OFF)
설계: docs/multi_tenant_design.md / 검수: docs/multi_tenant_phase1_review.md (High 0, MT=False 배포 가능)
데이터·인프라
- user_kis_credentials(Fernet 암호화, PBKDF2 키 파생, key_version), stock_bans_v2 신설,
portfolio/trade_history/active_trades/trade_journals/trading_sessions 등 user_id nullable 추가
- alembic 0009~0011(idempotent, legacy owner 백필). main.py 기동 시 portfolio.user_id 스키마 검사(H1)
- Jenkinsfile: DB Migration 을 Deploy 앞으로, alembic 이력 없으면 stamp 0008 후 upgrade(N1)
- bootstrap: users 비어 있으면 BOOTSTRAP_ADMIN_* 로 관리자 시드 + .env KIS 자격증명 이관 + 백필(SAVEPOINT)
백엔드
- core/crypto.py, core/tenant_auth.py(get_current_user — 비활성 사용자 401, API 키→legacy owner,
deny_non_owner_session_when_mt), core/dt_utils.py
- services/tenant: context, kis_pool, registry(UserContextRegistry, 락 최소화·복호화 to_thread), risk_factory
- KoreaInvestmentAPI.for_user(사용자별 인스턴스, 토큰캐시 users/{id}/, 시세는 공용 위임)
- trade_executor.safe_place_order_v2(사용자별 락/세마포어, legacy ctx 는 기존 키)
- mdd_manager/loss_tracker _v2 + ban_until tz-aware/naive 경계 정규화(레거시 경로 버그 포함 수정)
- db_worker: payload user_id 옵션 필터, upsert_stock_ban_v2, legacy ActiveTrade 조회 LIMIT 1
- API: /account/credentials(등록·마스킹조회·삭제·연결테스트·자동매매), /auth/admin/tenant/*,
/portfolio/v2(조회·요약·sync·buy·sell), /trades/v2(+pending). PortfolioResponse Optional 수정
프론트
- /account 내 계좌 설정, useTenantMode(세션=v2 / API키=legacy, 미확정 시 쿼리·버튼 비활성),
계좌 미등록 인라인 안내, v2 매수/매도/거래이력 분기
테스트: tests/integration(격리 Postgres, KIS 전면 mock) 20 passed / 1 xfailed(0012 이전 제약)
2단계(스케줄러 사용자화·legacy 라우터 격리) 전까지 MULTI_TENANT_ENABLED=True 전환 금지
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
62
backend/app/services/tenant/context.py
Normal file
62
backend/app/services/tenant/context.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""멀티테넌트 사용자 실행 컨텍스트 (설계 문서 §2, §5.6).
|
||||
|
||||
`UserContext`는 사용자 1명의 매매 실행에 필요한 모든 상태(KIS 클라이언트,
|
||||
리스크 매니저, 데이트레이딩 상태 등)를 한데 묶는다. 실제 조립(레지스트리)은
|
||||
dev-team/risk-monitor-agent가 담당하는 `registry.py`에서 이루어지며, 이 파일은
|
||||
데이터 구조만 정의한다 — import 순환을 피하기 위해 mdd/loss_tracker/dt_state는
|
||||
구체 타입을 import하지 않고 `Any`로 둔다.
|
||||
|
||||
이 모듈은 DB 세션을 열지 않는다(자격증명 복호화는 호출 측이 수행해 넘겨준다).
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.services.korea_investment_api import KoreaInvestmentAPI
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserContext:
|
||||
"""사용자 1명의 매매 실행 컨텍스트.
|
||||
|
||||
Attributes:
|
||||
user_id: `users.id`. legacy(단일 계좌) 컨텍스트는 legacy owner id 또는 None.
|
||||
username: 표시/로그용.
|
||||
mode: "virtual" | "real" — `kis.base_url`과 동일해야 함.
|
||||
kis: 이 사용자의 주문·잔고 전용 `KoreaInvestmentAPI` 인스턴스
|
||||
(`KoreaInvestmentAPI.for_user(...)` 또는 legacy 전역 `korea_investment_api`).
|
||||
mdd: 사용자별 `MDDManager` 인스턴스 (risk-monitor-agent 담당, 타입은 순환 방지 위해 Any).
|
||||
loss_tracker: 사용자별 `ConsecutiveLossTracker` 인스턴스 (Any, 위와 동일 이유).
|
||||
dt_state: 사용자별 `DayTradingState` 인스턴스 (2단계에서 사용, 1단계는 None 허용).
|
||||
portfolio_cache: 사용자별 `PortfolioCache` 인스턴스 (2단계, 1단계는 None 허용).
|
||||
auto_trade_enabled: 사용자 단위 자동매매 on/off.
|
||||
is_legacy: True면 기존 전역 싱글톤(korea_investment_api/mdd_manager/loss_tracker)을
|
||||
그대로 감싼 컨텍스트 — Redis 락/키 네임스페이스가 하위 호환 경로(user_id 미포함)를 쓴다.
|
||||
"""
|
||||
|
||||
user_id: Optional[int]
|
||||
username: str
|
||||
mode: str
|
||||
kis: KoreaInvestmentAPI
|
||||
mdd: Any
|
||||
loss_tracker: Any
|
||||
dt_state: Any = None
|
||||
portfolio_cache: Any = None
|
||||
auto_trade_enabled: bool = False
|
||||
is_legacy: bool = False
|
||||
|
||||
@property
|
||||
def effective_auto_trade_enabled(self) -> bool:
|
||||
"""`auto_trade_enabled`의 실시간 버전.
|
||||
|
||||
legacy 컨텍스트는 `registry.legacy()`가 조립 시점에 `settings.AUTO_TRADE_ENABLED`를
|
||||
스냅샷해 최대 60초(TTL) 동안 고정한다(L4) — 런타임에 `.env`/설정을 토글해도
|
||||
캐시가 갱신되기 전까지 반영되지 않는 문제. legacy 컨텍스트에서는 필드 대신 이
|
||||
프로퍼티로 `settings.AUTO_TRADE_ENABLED`를 매번 다시 읽어 즉시 반영한다.
|
||||
사용자 컨텍스트(is_legacy=False)는 DB에 저장된 사용자별 토글이 필드값이므로
|
||||
그대로 반환한다. portfolio_v2 buy 가드는 `ctx.auto_trade_enabled` 대신 이
|
||||
프로퍼티 사용을 권장.
|
||||
"""
|
||||
if self.is_legacy:
|
||||
from app.core.config import settings
|
||||
return settings.AUTO_TRADE_ENABLED
|
||||
return self.auto_trade_enabled
|
||||
Reference in New Issue
Block a user