# mgmt - 컨테이너 관리 ## 개요 **mgmt** 컴포넌트는 가상 자산 컨테이너 생성 및 운영 관리를 담당합니다. ### 책임 - 컨테이너 생명주기 관리 (생성, 수정, 삭제, 활성화/비활성화) - 가상 자산 격리 및 밸런스 관리 - 컨테이너 간 자산 이동 - 실제 계좌 vs 컨테이너 총합 일치성 검증 (Reconciliation) ### 핵심 개념 **컨테이너(Container)**는 하나의 실제 계좌 내에서 가상으로 분리된 자산 공간입니다. ```mermaid graph TB subgraph "실제 계좌 (한국투자증권)" Account[계좌 총 자산: 1억원] subgraph "컨테이너 A" A_Cash[현금: 500만원] A_Stock[삼성전자: 2500만원] A_Total[총 3000만원] end subgraph "컨테이너 B" B_Cash[현금: 1000만원] B_ETF[KODEX 200: 4000만원] B_Total[총 5000만원] end subgraph "컨테이너 C" C_Cash[현금: 2000만원] C_Total[총 2000만원] end end Account --> A_Total Account --> B_Total Account --> C_Total style Account fill:#4CAF50,color:#fff style A_Total fill:#2196F3,color:#fff style B_Total fill:#2196F3,color:#fff style C_Total fill:#2196F3,color:#fff ``` ### 의존성 ```mermaid graph LR Mgmt[mgmt] --> Balance[balance] Mgmt --> Risk[risk] Mgmt --> DB[(Database)] Scheduler[scheduler] --> Mgmt Analytics[analytics] --> Mgmt Monitor[monitor] --> Mgmt style Mgmt fill:#4CAF50,color:#fff ``` ## 주요 기능 ### 1. 컨테이너 생명주기 관리 #### 1.1 컨테이너 생성 ```typescript /** * 새로운 컨테이너를 생성합니다 */ async function createContainer(params: { accountId: string // 소속 계좌 ID name: string // 컨테이너 이름 description?: string // 설명 initialAmount: number // 초기 할당 금액 cashReserve: number // 최소 현금 보유량 constraints: Constraints // 제약 조건 }): Promise { // 1. 계좌 잔고 확인 const balance = await balance.getCurrentBalance(params.accountId) // 2. 할당 가능 금액 검증 const allocatedSum = await getTotalAllocated(params.accountId) if (allocatedSum + params.initialAmount > balance.totalEquity) { throw new Error('할당 가능 금액을 초과합니다') } // 3. 컨테이너 생성 const container = await db.containers.create({ ...params, status: 'ACTIVE', allocation: { initialAmount: params.initialAmount, currentEquity: params.initialAmount, cashReserve: params.cashReserve }, createdAt: new Date() }) // 4. 초기 가상 잔고 생성 await createVirtualBalance(container.id, { cash: params.initialAmount, positions: [] }) // 5. 감사 로그 await audit.logEvent({ eventType: 'CONFIG_CHANGE', action: 'CREATE', entity: 'Container', entityId: container.id, after: container }) return container } ``` #### 1.2 컨테이너 상태 관리 ```mermaid stateDiagram-v2 [*] --> Created: createContainer() Created --> Active: activate() Created --> [*]: delete() Active --> Running: scheduler trigger Active --> Paused: pause() Active --> [*]: delete() Running --> Active: execution complete Running --> Error: execution failed Running --> Paused: emergency stop Paused --> Active: resume() Paused --> [*]: delete() Error --> Active: resolve & restart Error --> Paused: manual intervention Error --> [*]: delete() ``` ```typescript /** * 컨테이너를 일시 정지합니다 */ async function pauseContainer(containerId: string): Promise { const container = await getContainer(containerId) if (container.status === 'ARCHIVED') { throw new Error('삭제된 컨테이너는 일시 정지할 수 없습니다') } await updateContainerStatus(containerId, 'PAUSED') // 실행 중인 스케줄 일시 정지 await scheduler.pauseSchedulesByContainer(containerId) await audit.logEvent({ eventType: 'CONFIG_CHANGE', action: 'UPDATE', entity: 'Container', entityId: containerId, before: { status: container.status }, after: { status: 'PAUSED' } }) } ``` ### 2. 가상 자산 격리 #### 2.1 가상 잔고 관리 ```typescript /** * 컨테이너의 가상 잔고를 조회합니다 */ async function getContainerBalance(containerId: string): Promise { const virtualBalance = await db.virtualBalances.findOne({ containerId }) // 실시간 시세 반영 const updatedPositions = await Promise.all( virtualBalance.positions.map(async (position) => { const quote = await balance.getCurrentPrice(position.symbol) return { ...position, currentPrice: quote.price, marketValue: position.quantity * quote.price, unrealizedPnL: (quote.price - position.averagePrice) * position.quantity } }) ) const totalValue = virtualBalance.cash + updatedPositions.reduce((sum, p) => sum + p.marketValue, 0) return { containerId, cash: virtualBalance.cash, positions: updatedPositions, totalValue, availableCash: virtualBalance.cash, allocatedValue: virtualBalance.allocatedValue, timestamp: new Date() } } ``` #### 2.2 포지션 업데이트 ```typescript /** * 주문 체결 후 컨테이너 포지션을 업데이트합니다 */ async function updatePosition( containerId: string, update: { symbol: string quantity: number // +: 매수, -: 매도 price: number commission: number } ): Promise { const virtualBalance = await db.virtualBalances.findOne({ containerId }) if (update.quantity > 0) { // 매수 const totalCost = update.quantity * update.price + update.commission if (virtualBalance.cash < totalCost) { throw new Error('가상 잔고가 부족합니다') } virtualBalance.cash -= totalCost // 기존 포지션 업데이트 또는 신규 추가 const existingPosition = virtualBalance.positions.find(p => p.symbol === update.symbol) if (existingPosition) { // 평균 매입가 재계산 const totalQuantity = existingPosition.quantity + update.quantity const totalCost = existingPosition.averagePrice * existingPosition.quantity + update.price * update.quantity existingPosition.averagePrice = totalCost / totalQuantity existingPosition.quantity = totalQuantity } else { virtualBalance.positions.push({ symbol: update.symbol, quantity: update.quantity, averagePrice: update.price, currentPrice: update.price, marketValue: update.quantity * update.price, unrealizedPnL: 0, unrealizedPnLPct: 0 }) } } else { // 매도 const position = virtualBalance.positions.find(p => p.symbol === update.symbol) if (!position || position.quantity < Math.abs(update.quantity)) { throw new Error('매도할 수량이 부족합니다') } const sellAmount = Math.abs(update.quantity) * update.price - update.commission virtualBalance.cash += sellAmount position.quantity += update.quantity // update.quantity는 음수 // 실현 손익 계산 const realizedPnL = (update.price - position.averagePrice) * Math.abs(update.quantity) - update.commission // 포지션 완전 청산 시 제거 if (position.quantity === 0) { virtualBalance.positions = virtualBalance.positions.filter(p => p.symbol !== update.symbol) } } await db.virtualBalances.update({ containerId }, virtualBalance) } ``` ### 3. 컨테이너 간 자산 이동 ```typescript /** * 컨테이너 간 현금을 이동합니다 */ async function transferCash(params: { fromContainerId: string toContainerId: string amount: number reason?: string }): Promise { // 1. 출발 컨테이너 잔고 확인 const fromBalance = await getContainerBalance(params.fromContainerId) if (fromBalance.availableCash < params.amount) { throw new Error('이동 가능한 현금이 부족합니다') } // 2. 두 컨테이너가 같은 계좌 소속인지 확인 const fromContainer = await getContainer(params.fromContainerId) const toContainer = await getContainer(params.toContainerId) if (fromContainer.accountId !== toContainer.accountId) { throw new Error('같은 계좌 내 컨테이너 간에만 이동 가능합니다') } // 3. 트랜잭션으로 원자적 실행 await db.transaction(async (tx) => { // 출발 컨테이너에서 차감 await tx.virtualBalances.update( { containerId: params.fromContainerId }, { $inc: { cash: -params.amount } } ) // 도착 컨테이너에 추가 await tx.virtualBalances.update( { containerId: params.toContainerId }, { $inc: { cash: params.amount } } ) // 이동 기록 생성 const transfer = await tx.transfers.create({ fromContainerId: params.fromContainerId, toContainerId: params.toContainerId, amount: params.amount, reason: params.reason, createdAt: new Date() }) return transfer }) // 4. 감사 로그 await audit.logEvent({ eventType: 'CONFIG_CHANGE', action: 'TRANSFER', entity: 'Transfer', entityId: transfer.id, metadata: params }) return transfer } ``` ### 4. 밸런스 조정 (Reconciliation) ```typescript /** * 실제 계좌 잔고와 컨테이너 총합이 일치하는지 검증합니다 */ async function reconcileAssets(accountId: string): Promise { // 1. 실제 계좌 잔고 조회 const actualBalance = await balance.getCurrentBalance(accountId) // 2. 모든 컨테이너 잔고 합산 const containers = await getContainersByAccount(accountId) const virtualBalances = await Promise.all( containers.map(c => getContainerBalance(c.id)) ) const virtualTotal = { cash: virtualBalances.reduce((sum, vb) => sum + vb.cash, 0), positions: aggregatePositions(virtualBalances.map(vb => vb.positions)) } // 3. 차이 검증 const cashDiscrepancy = actualBalance.cash.krw - virtualTotal.cash const positionDiscrepancies = findPositionDiscrepancies(actualBalance.positions, virtualTotal.positions) const isReconciled = Math.abs(cashDiscrepancy) < 100 && positionDiscrepancies.length === 0 const report: ReconciliationReport = { accountId, isReconciled, actual: { cash: actualBalance.cash.krw, positions: actualBalance.positions }, virtual: { cash: virtualTotal.cash, positions: virtualTotal.positions }, discrepancies: { cash: cashDiscrepancy, positions: positionDiscrepancies }, timestamp: new Date() } // 4. 불일치 시 알림 if (!isReconciled) { await monitor.sendAlert({ type: 'SYSTEM', severity: 'ERROR', message: `밸런스 불일치 감지: 계좌 ${accountId}`, metadata: report }) } return report } function aggregatePositions(positionGroups: Position[][]): Position[] { const aggregated = new Map() for (const positions of positionGroups) { for (const position of positions) { if (aggregated.has(position.symbol)) { const existing = aggregated.get(position.symbol)! existing.quantity += position.quantity existing.marketValue += position.marketValue } else { aggregated.set(position.symbol, { ...position }) } } } return Array.from(aggregated.values()) } ``` ## 데이터 모델 주요 데이터 모델: - [Container](../../docs/03-data-models.md#21-container-컨테이너) - [VirtualBalance](../../docs/03-data-models.md#22-virtualbalance-가상-잔고) ## API 명세 ### POST /api/containers 컨테이너 생성 **요청**: ```json { "accountId": "account-123", "name": "성장주 전략", "initialAmount": 10000000, "cashReserve": 1000000, "constraints": { "maxSinglePositionPct": 20, "maxDrawdown": 15, "allowedAssetClasses": ["STOCK"] } } ``` ### GET /api/containers/:containerId 컨테이너 조회 **응답**: ```json { "id": "container-456", "accountId": "account-123", "name": "성장주 전략", "strategyId": "strategy-momentum", "status": "ACTIVE", "allocation": { "initialAmount": 10000000, "currentEquity": 10500000, "cashReserve": 1000000 } } ``` ### GET /api/containers/:containerId/balance 가상 잔고 조회 **응답**: ```json { "containerId": "container-456", "cash": 3000000, "positions": [ { "symbol": "005930", "quantity": 100, "averagePrice": 70000, "currentPrice": 75000, "marketValue": 7500000, "unrealizedPnL": 500000 } ], "totalValue": 10500000, "availableCash": 3000000 } ``` ## 구현 고려사항 ### 1. 원자성 보장 컨테이너 간 자산 이동은 반드시 트랜잭션으로 처리하여 데이터 무결성을 보장해야 합니다. ### 2. 정기적인 Reconciliation 매일 장 마감 후 자동으로 Reconciliation 실행: ```typescript scheduler.scheduleDaily('MARKET_CLOSE', async () => { const accounts = await balance.getAllAccounts() for (const account of accounts) { const report = await mgmt.reconcileAssets(account.id) if (!report.isReconciled) { await monitor.sendAlert({ type: 'SYSTEM', severity: 'CRITICAL', message: `밸런스 불일치: ${account.accountName}` }) } } }) ``` ### 3. 컨테이너 삭제 시 주의사항 컨테이너 삭제 시 보유 포지션이 있다면 경고: ```typescript async function deleteContainer(containerId: string): Promise { const balance = await getContainerBalance(containerId) if (balance.positions.length > 0) { throw new Error('포지션이 남아있는 컨테이너는 삭제할 수 없습니다. 먼저 모든 포지션을 청산하세요.') } // ARCHIVED 상태로 변경 (소프트 삭제) await updateContainerStatus(containerId, 'ARCHIVED') } ``` ## 관련 문서 - [시스템 개요](../../docs/01-overview.md) - [전체 아키텍처](../../docs/02-architecture.md) - [공통 데이터 모델](../../docs/03-data-models.md) ### 관련 컴포넌트 - [balance - 계좌 관리](./balance.md) - [strategy - 전략 관리](./strategy.md) - [risk - 리스크 관리](./risk.md)