feat: 프로젝트 개요 및 컴포넌트별 명세, 로드맵 등 문서 추가
This commit is contained in:
759
components/phase1/balance.md
Normal file
759
components/phase1/balance.md
Normal file
@@ -0,0 +1,759 @@
|
||||
# balance - 계좌 관리
|
||||
|
||||
## 개요
|
||||
|
||||
**balance** 컴포넌트는 증권사 API 통합 및 계좌 자산 관리를 담당합니다.
|
||||
|
||||
### 책임
|
||||
|
||||
- 증권사 API 인증 및 세션 관리
|
||||
- 다중 계좌 등록/수정/삭제
|
||||
- 계좌 잔고 및 포지션 조회
|
||||
- 시장 시세 조회 (현재가, 호가, 과거 데이터)
|
||||
- 주문 처리 (제출, 취소, 상태 조회)
|
||||
|
||||
### 의존성
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Balance[balance] --> Broker1[한국투자증권 API]
|
||||
Balance --> Broker2[삼성증권 API]
|
||||
Balance --> Broker3[키움증권 API]
|
||||
|
||||
Mgmt[mgmt] --> Balance
|
||||
Scheduler[scheduler] --> Balance
|
||||
Monitor[monitor] --> Balance
|
||||
|
||||
Balance --> DB[(Database)]
|
||||
Balance --> Cache[(Redis Cache)]
|
||||
|
||||
style Balance fill:#00BCD4,color:#fff
|
||||
```
|
||||
|
||||
## 주요 기능
|
||||
|
||||
### 1. 계좌 연동 관리
|
||||
|
||||
#### 1.1 계좌 등록
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 새로운 증권사 계좌를 등록합니다
|
||||
*/
|
||||
async function registerAccount(params: {
|
||||
brokerId: string // 증권사 식별자 (예: 'korea_investment')
|
||||
accountNumber: string // 계좌 번호
|
||||
accountName: string // 계좌 별칭
|
||||
accountType: AccountType // 계좌 유형
|
||||
credentials: { // 인증 정보
|
||||
apiKey: string
|
||||
apiSecret: string
|
||||
accountPassword?: string
|
||||
}
|
||||
}): Promise<Account>
|
||||
```
|
||||
|
||||
**처리 흐름**:
|
||||
1. 증권사 식별자 검증
|
||||
2. API 키 암호화 (AES-256)
|
||||
3. 테스트 연결 수행 (인증 정보 유효성 검증)
|
||||
4. 계좌 정보 데이터베이스 저장
|
||||
5. 초기 잔고 조회
|
||||
|
||||
#### 1.2 API 키 암호화 저장
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* API 키를 AES-256으로 암호화하여 저장
|
||||
*/
|
||||
function encryptCredentials(credentials: {
|
||||
apiKey: string
|
||||
apiSecret: string
|
||||
accountPassword?: string
|
||||
}): EncryptedCredentials {
|
||||
const iv = crypto.randomBytes(16)
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', ENCRYPTION_KEY, iv)
|
||||
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(JSON.stringify(credentials), 'utf8'),
|
||||
cipher.final()
|
||||
])
|
||||
|
||||
return {
|
||||
encryptedData: encrypted.toString('base64'),
|
||||
iv: iv.toString('base64'),
|
||||
algorithm: 'aes-256-gcm'
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 자산 조회
|
||||
|
||||
#### 2.1 현재 잔고 조회
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 계좌의 현재 잔고를 조회합니다
|
||||
*/
|
||||
async function getCurrentBalance(accountId: string): Promise<Balance> {
|
||||
const account = await getAccount(accountId)
|
||||
const adapter = getBrokerAdapter(account.brokerId)
|
||||
|
||||
// 증권사 API 호출
|
||||
const balance = await adapter.getBalance(account)
|
||||
|
||||
// 캐시 업데이트
|
||||
await cache.set(`balance:${accountId}`, balance, 60) // 60초 TTL
|
||||
|
||||
return balance
|
||||
}
|
||||
```
|
||||
|
||||
**반환 데이터**:
|
||||
```typescript
|
||||
interface Balance {
|
||||
accountId: string
|
||||
cash: {
|
||||
krw: number // 원화 잔고
|
||||
usd: number // 달러 잔고 (해외 주식용)
|
||||
}
|
||||
positions: Position[] // 보유 종목
|
||||
totalEquity: number // 총 자산 평가액
|
||||
buyingPower: number // 매수 가능 금액
|
||||
withdrawableCash: number // 출금 가능 금액
|
||||
timestamp: Date // 조회 시점
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 포트폴리오 스냅샷
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 자산 클래스별 비중 및 종목별 손익률을 포함한 포트폴리오 스냅샷
|
||||
*/
|
||||
async function getPortfolioSnapshot(accountId: string): Promise<Portfolio> {
|
||||
const balance = await getCurrentBalance(accountId)
|
||||
|
||||
// 자산 클래스별 비중 계산
|
||||
const assetAllocation = calculateAssetAllocation(balance.positions)
|
||||
|
||||
// 종목별 손익률 계산
|
||||
const positionsWithPnL = balance.positions.map(position => ({
|
||||
...position,
|
||||
unrealizedPnLPct: (position.unrealizedPnL / (position.averagePrice * position.quantity)) * 100
|
||||
}))
|
||||
|
||||
return {
|
||||
...balance,
|
||||
assetAllocation,
|
||||
positions: positionsWithPnL
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 시세 조회
|
||||
|
||||
#### 3.1 현재가 조회
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 종목의 현재가 및 호가 정보를 조회합니다
|
||||
*/
|
||||
async function getCurrentPrice(symbol: string): Promise<Quote> {
|
||||
// 캐시 우선 조회 (실시간 시세)
|
||||
const cached = await cache.get(`quote:${symbol}`)
|
||||
if (cached && Date.now() - cached.timestamp < 5000) { // 5초 이내 캐시
|
||||
return cached
|
||||
}
|
||||
|
||||
// 증권사 API 호출
|
||||
const adapter = getBrokerAdapter('default')
|
||||
const quote = await adapter.getQuote(symbol)
|
||||
|
||||
// 캐시 업데이트
|
||||
await cache.set(`quote:${symbol}`, quote, 10) // 10초 TTL
|
||||
|
||||
return quote
|
||||
}
|
||||
```
|
||||
|
||||
**반환 데이터**:
|
||||
```typescript
|
||||
interface Quote {
|
||||
symbol: string // 종목 코드
|
||||
price: number // 현재가
|
||||
change: number // 전일 대비 변동
|
||||
changePct: number // 변동률 (%)
|
||||
volume: number // 거래량
|
||||
bidPrice: number // 매수 호가 1
|
||||
askPrice: number // 매도 호가 1
|
||||
bidSize: number // 매수 호가 수량
|
||||
askSize: number // 매도 호가 수량
|
||||
timestamp: Date // 시세 시간
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 과거 가격 조회
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 과거 가격 데이터를 조회합니다 (백테스트용)
|
||||
*/
|
||||
async function getHistoricalPrices(params: {
|
||||
symbol: string
|
||||
from: Date
|
||||
to: Date
|
||||
interval: Interval // '1d', '1w', '1M' 등
|
||||
}): Promise<PriceBar[]> {
|
||||
const adapter = getBrokerAdapter('default')
|
||||
const prices = await adapter.getHistoricalPrices(params)
|
||||
|
||||
// 배당 및 액면분할 조정
|
||||
const adjustedPrices = await adjustForCorporateActions(prices, params.symbol)
|
||||
|
||||
return adjustedPrices
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 주문 처리
|
||||
|
||||
#### 4.1 주문 제출
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 주문을 제출합니다
|
||||
*/
|
||||
async function placeOrder(order: Order): Promise<OrderResult> {
|
||||
// 1. 주문 검증
|
||||
await validateOrder(order)
|
||||
|
||||
// 2. 증권사 어댑터 선택
|
||||
const account = await getAccount(order.accountId)
|
||||
const adapter = getBrokerAdapter(account.brokerId)
|
||||
|
||||
// 3. 주문 제출
|
||||
try {
|
||||
const result = await adapter.submitOrder(order)
|
||||
|
||||
// 4. 주문 상태 저장
|
||||
await saveOrderStatus(result)
|
||||
|
||||
// 5. 감사 로그 기록
|
||||
await audit.logEvent({
|
||||
eventType: 'ORDER',
|
||||
action: 'CREATE',
|
||||
entity: 'Order',
|
||||
entityId: result.orderId!,
|
||||
after: result
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
} catch (error) {
|
||||
// 에러 로깅
|
||||
await logger.error('Order submission failed', { order, error })
|
||||
|
||||
// 알림 발송
|
||||
await monitor.sendAlert({
|
||||
type: 'EXECUTION',
|
||||
severity: 'ERROR',
|
||||
message: `주문 제출 실패: ${order.symbol} ${order.side} ${order.quantity}`,
|
||||
metadata: { order, error }
|
||||
})
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**주문 검증**:
|
||||
```typescript
|
||||
async function validateOrder(order: Order): Promise<void> {
|
||||
// 계좌 존재 여부 확인
|
||||
const account = await getAccount(order.accountId)
|
||||
if (!account.isActive) {
|
||||
throw new Error('계좌가 비활성 상태입니다')
|
||||
}
|
||||
|
||||
// 주문 수량 검증
|
||||
if (order.quantity <= 0) {
|
||||
throw new Error('주문 수량은 0보다 커야 합니다')
|
||||
}
|
||||
|
||||
// 호가 단위 검증 (지정가 주문인 경우)
|
||||
if (order.orderType === 'LIMIT' && order.price) {
|
||||
validatePriceTick(order.price, order.symbol)
|
||||
}
|
||||
|
||||
// 잔고 확인 (매수 주문인 경우)
|
||||
if (order.side === 'BUY') {
|
||||
const balance = await getCurrentBalance(order.accountId)
|
||||
const requiredCash = order.quantity * (order.price || await getCurrentPrice(order.symbol).price)
|
||||
|
||||
if (balance.buyingPower < requiredCash) {
|
||||
throw new Error('매수 가능 금액이 부족합니다')
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 주문 취소
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 미체결 주문을 취소합니다
|
||||
*/
|
||||
async function cancelOrder(orderId: string): Promise<boolean> {
|
||||
// 1. 주문 정보 조회
|
||||
const order = await getOrder(orderId)
|
||||
|
||||
if (!['PENDING', 'SUBMITTED', 'PARTIALLY_FILLED'].includes(order.status)) {
|
||||
throw new Error(`취소 불가능한 주문 상태: ${order.status}`)
|
||||
}
|
||||
|
||||
// 2. 증권사 어댑터로 취소 요청
|
||||
const account = await getAccount(order.accountId)
|
||||
const adapter = getBrokerAdapter(account.brokerId)
|
||||
|
||||
const success = await adapter.cancelOrder(orderId)
|
||||
|
||||
if (success) {
|
||||
// 3. 주문 상태 업데이트
|
||||
await updateOrderStatus(orderId, 'CANCELLED')
|
||||
|
||||
// 4. 감사 로그 기록
|
||||
await audit.logEvent({
|
||||
eventType: 'ORDER',
|
||||
action: 'CANCEL',
|
||||
entity: 'Order',
|
||||
entityId: orderId
|
||||
})
|
||||
}
|
||||
|
||||
return success
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.3 주문 상태 조회
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 주문의 현재 상태를 조회합니다
|
||||
*/
|
||||
async function getOrderStatus(orderId: string): Promise<OrderStatus> {
|
||||
const order = await getOrder(orderId)
|
||||
|
||||
// 최종 상태가 아닌 경우 증권사 API에서 최신 상태 조회
|
||||
if (!['FILLED', 'CANCELLED', 'REJECTED', 'EXPIRED'].includes(order.status)) {
|
||||
const account = await getAccount(order.accountId)
|
||||
const adapter = getBrokerAdapter(account.brokerId)
|
||||
|
||||
const latestStatus = await adapter.getOrderStatus(orderId)
|
||||
|
||||
// 상태가 변경된 경우 업데이트
|
||||
if (latestStatus.status !== order.status) {
|
||||
await updateOrderStatus(orderId, latestStatus.status, latestStatus)
|
||||
}
|
||||
|
||||
return latestStatus
|
||||
}
|
||||
|
||||
return {
|
||||
orderId: order.orderId!,
|
||||
status: order.status,
|
||||
filledQuantity: order.filledQuantity,
|
||||
averageFillPrice: order.averageFillPrice
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 증권사 추상화 인터페이스
|
||||
|
||||
### BrokerAdapter 인터페이스
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 모든 증권사 어댑터가 구현해야 하는 인터페이스
|
||||
*/
|
||||
interface BrokerAdapter {
|
||||
/**
|
||||
* 증권사 API에 연결하고 세션을 생성합니다
|
||||
*/
|
||||
connect(credentials: Credentials): Promise<Session>
|
||||
|
||||
/**
|
||||
* 계좌 잔고를 조회합니다
|
||||
*/
|
||||
getBalance(account: Account): Promise<Balance>
|
||||
|
||||
/**
|
||||
* 종목의 현재 시세를 조회합니다
|
||||
*/
|
||||
getQuote(symbol: string): Promise<Quote>
|
||||
|
||||
/**
|
||||
* 과거 가격 데이터를 조회합니다
|
||||
*/
|
||||
getHistoricalPrices(params: HistoricalPricesParams): Promise<PriceBar[]>
|
||||
|
||||
/**
|
||||
* 주문을 제출합니다
|
||||
*/
|
||||
submitOrder(order: Order): Promise<OrderResult>
|
||||
|
||||
/**
|
||||
* 주문을 취소합니다
|
||||
*/
|
||||
cancelOrder(orderId: string): Promise<boolean>
|
||||
|
||||
/**
|
||||
* 주문 상태를 조회합니다
|
||||
*/
|
||||
getOrderStatus(orderId: string): Promise<OrderStatus>
|
||||
|
||||
/**
|
||||
* 주문 이력을 조회합니다
|
||||
*/
|
||||
getOrderHistory(account: Account, from: Date, to: Date): Promise<Order[]>
|
||||
|
||||
/**
|
||||
* 세션을 종료합니다
|
||||
*/
|
||||
disconnect(): Promise<void>
|
||||
}
|
||||
```
|
||||
|
||||
### 구현 예시: 한국투자증권 Adapter
|
||||
|
||||
```typescript
|
||||
class KoreaInvestmentAdapter implements BrokerAdapter {
|
||||
private accessToken?: string
|
||||
private tokenExpiry?: Date
|
||||
|
||||
async connect(credentials: Credentials): Promise<Session> {
|
||||
// OAuth 토큰 발급
|
||||
const response = await axios.post('https://openapi.koreainvestment.com/oauth2/token', {
|
||||
grant_type: 'client_credentials',
|
||||
appkey: credentials.apiKey,
|
||||
appsecret: credentials.apiSecret
|
||||
})
|
||||
|
||||
this.accessToken = response.data.access_token
|
||||
this.tokenExpiry = new Date(Date.now() + response.data.expires_in * 1000)
|
||||
|
||||
return {
|
||||
sessionId: response.data.access_token,
|
||||
expiresAt: this.tokenExpiry
|
||||
}
|
||||
}
|
||||
|
||||
async getBalance(account: Account): Promise<Balance> {
|
||||
await this.ensureConnected()
|
||||
|
||||
const response = await axios.get(
|
||||
'https://openapi.koreainvestment.com/uapi/domestic-stock/v1/trading/inquire-balance',
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${this.accessToken}`,
|
||||
appkey: account.credentials.apiKey,
|
||||
appsecret: account.credentials.apiSecret,
|
||||
tr_id: 'TTTC8434R'
|
||||
},
|
||||
params: {
|
||||
CANO: account.accountNumber.slice(0, 8),
|
||||
ACNT_PRDT_CD: account.accountNumber.slice(8, 10)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// API 응답을 공통 Balance 형식으로 변환
|
||||
return this.transformBalance(response.data)
|
||||
}
|
||||
|
||||
async submitOrder(order: Order): Promise<OrderResult> {
|
||||
await this.ensureConnected()
|
||||
|
||||
const response = await axios.post(
|
||||
'https://openapi.koreainvestment.com/uapi/domestic-stock/v1/trading/order-cash',
|
||||
{
|
||||
CANO: order.accountId.slice(0, 8),
|
||||
ACNT_PRDT_CD: order.accountId.slice(8, 10),
|
||||
PDNO: order.symbol,
|
||||
ORD_DVSN: order.orderType === 'MARKET' ? '01' : '00',
|
||||
ORD_QTY: order.quantity.toString(),
|
||||
ORD_UNPR: order.price?.toString() || '0'
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
authorization: `Bearer ${this.accessToken}`,
|
||||
tr_id: order.side === 'BUY' ? 'TTTC0802U' : 'TTTC0801U'
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
...order,
|
||||
orderId: response.data.ODNO,
|
||||
status: 'SUBMITTED',
|
||||
submittedAt: new Date()
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureConnected(): Promise<void> {
|
||||
if (!this.accessToken || !this.tokenExpiry || this.tokenExpiry < new Date()) {
|
||||
throw new Error('세션이 만료되었습니다. 다시 연결하세요.')
|
||||
}
|
||||
}
|
||||
|
||||
private transformBalance(apiResponse: any): Balance {
|
||||
// 증권사 API 응답을 공통 형식으로 변환
|
||||
// ...
|
||||
}
|
||||
|
||||
// ... 기타 메서드 구현
|
||||
}
|
||||
```
|
||||
|
||||
## 데이터 모델
|
||||
|
||||
주요 데이터 모델은 [공통 데이터 모델](../../docs/03-data-models.md)을 참조하세요.
|
||||
|
||||
- [Account](../../docs/03-data-models.md#11-account-계좌)
|
||||
- [Balance](../../docs/03-data-models.md#12-balance-잔고)
|
||||
- [Position](../../docs/03-data-models.md#13-position-포지션)
|
||||
- [Order](../../docs/03-data-models.md#41-order-주문)
|
||||
- [Quote](../../docs/03-data-models.md#52-quote-현재가)
|
||||
|
||||
## API 명세
|
||||
|
||||
### REST API
|
||||
|
||||
#### GET /api/accounts
|
||||
계좌 목록 조회
|
||||
|
||||
**응답**:
|
||||
```json
|
||||
{
|
||||
"accounts": [
|
||||
{
|
||||
"id": "account-123",
|
||||
"brokerId": "korea_investment",
|
||||
"accountNumber": "1234567890",
|
||||
"accountName": "메인 계좌",
|
||||
"accountType": "STOCK",
|
||||
"isActive": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/accounts
|
||||
계좌 등록
|
||||
|
||||
**요청**:
|
||||
```json
|
||||
{
|
||||
"brokerId": "korea_investment",
|
||||
"accountNumber": "1234567890",
|
||||
"accountName": "메인 계좌",
|
||||
"accountType": "STOCK",
|
||||
"credentials": {
|
||||
"apiKey": "...",
|
||||
"apiSecret": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/accounts/:accountId/balance
|
||||
잔고 조회
|
||||
|
||||
**응답**:
|
||||
```json
|
||||
{
|
||||
"accountId": "account-123",
|
||||
"cash": {
|
||||
"krw": 5000000,
|
||||
"usd": 0
|
||||
},
|
||||
"positions": [
|
||||
{
|
||||
"symbol": "005930",
|
||||
"symbolName": "삼성전자",
|
||||
"quantity": 100,
|
||||
"averagePrice": 70000,
|
||||
"currentPrice": 75000,
|
||||
"marketValue": 7500000,
|
||||
"unrealizedPnL": 500000,
|
||||
"unrealizedPnLPct": 7.14
|
||||
}
|
||||
],
|
||||
"totalEquity": 12500000,
|
||||
"buyingPower": 4800000,
|
||||
"timestamp": "2024-01-15T09:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/orders
|
||||
주문 제출
|
||||
|
||||
**요청**:
|
||||
```json
|
||||
{
|
||||
"accountId": "account-123",
|
||||
"symbol": "005930",
|
||||
"side": "BUY",
|
||||
"orderType": "LIMIT",
|
||||
"quantity": 10,
|
||||
"price": 74000
|
||||
}
|
||||
```
|
||||
|
||||
## 구현 고려사항
|
||||
|
||||
### 1. 에러 처리 및 재시도
|
||||
|
||||
```typescript
|
||||
// 지수 백오프 재시도 로직
|
||||
async function retryWithBackoff<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries: number = 3
|
||||
): Promise<T> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await fn()
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) throw error
|
||||
|
||||
// 재시도 가능한 에러인지 확인
|
||||
if (!isRetryableError(error)) throw error
|
||||
|
||||
// 지수 백오프
|
||||
const delay = Math.pow(2, i) * 1000
|
||||
await sleep(delay)
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Should not reach here')
|
||||
}
|
||||
|
||||
function isRetryableError(error: any): boolean {
|
||||
// 네트워크 에러, 타임아웃, 5xx 에러 등
|
||||
return (
|
||||
error.code === 'ECONNRESET' ||
|
||||
error.code === 'ETIMEDOUT' ||
|
||||
(error.response?.status >= 500 && error.response?.status < 600)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Rate Limiting
|
||||
|
||||
증권사 API는 요청 제한이 있으므로 Rate Limiter 구현 필요:
|
||||
|
||||
```typescript
|
||||
class RateLimiter {
|
||||
private queue: Array<() => Promise<any>> = []
|
||||
private running: number = 0
|
||||
private maxConcurrent: number
|
||||
private minInterval: number
|
||||
|
||||
constructor(maxConcurrent: number = 5, minInterval: number = 100) {
|
||||
this.maxConcurrent = maxConcurrent
|
||||
this.minInterval = minInterval
|
||||
}
|
||||
|
||||
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.queue.push(async () => {
|
||||
try {
|
||||
const result = await fn()
|
||||
resolve(result)
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
|
||||
this.processQueue()
|
||||
})
|
||||
}
|
||||
|
||||
private async processQueue() {
|
||||
if (this.running >= this.maxConcurrent || this.queue.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const fn = this.queue.shift()!
|
||||
this.running++
|
||||
|
||||
try {
|
||||
await fn()
|
||||
} finally {
|
||||
this.running--
|
||||
setTimeout(() => this.processQueue(), this.minInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 캐싱 전략
|
||||
|
||||
- **실시간 시세**: Redis에 5-10초 TTL로 캐싱
|
||||
- **계좌 잔고**: 1분 TTL
|
||||
- **주문 상태**: 캐싱 안함 (항상 최신 데이터)
|
||||
|
||||
### 4. 보안
|
||||
|
||||
- API 키는 절대 로그에 기록하지 않음
|
||||
- 평문 API 키는 메모리에만 존재 (복호화 후 즉시 사용)
|
||||
- HTTPS 필수
|
||||
- API 키 로테이션 주기적 수행
|
||||
|
||||
## 테스트
|
||||
|
||||
### 단위 테스트 예시
|
||||
|
||||
```typescript
|
||||
describe('balance - placeOrder', () => {
|
||||
it('should place a valid buy order', async () => {
|
||||
const order = {
|
||||
accountId: 'test-account',
|
||||
symbol: 'TEST',
|
||||
side: 'BUY',
|
||||
orderType: 'LIMIT',
|
||||
quantity: 10,
|
||||
price: 1000
|
||||
}
|
||||
|
||||
const result = await balance.placeOrder(order)
|
||||
|
||||
expect(result.orderId).toBeDefined()
|
||||
expect(result.status).toBe('SUBMITTED')
|
||||
})
|
||||
|
||||
it('should reject order with insufficient balance', async () => {
|
||||
const order = {
|
||||
accountId: 'test-account',
|
||||
symbol: 'TEST',
|
||||
side: 'BUY',
|
||||
orderType: 'LIMIT',
|
||||
quantity: 1000000,
|
||||
price: 100000
|
||||
}
|
||||
|
||||
await expect(balance.placeOrder(order)).rejects.toThrow('매수 가능 금액이 부족합니다')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [시스템 개요](../../docs/01-overview.md)
|
||||
- [전체 아키텍처](../../docs/02-architecture.md)
|
||||
- [공통 데이터 모델](../../docs/03-data-models.md)
|
||||
- [주요 워크플로우](../../docs/04-workflows.md)
|
||||
- [구현 로드맵](../../docs/05-roadmap.md)
|
||||
|
||||
### 관련 컴포넌트
|
||||
- [mgmt - 컨테이너 관리](./mgmt.md)
|
||||
- [data - 데이터 관리](./data.md)
|
||||
- [scheduler - 실행 스케줄러](./scheduler.md)
|
||||
135
components/phase1/data.md
Normal file
135
components/phase1/data.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# data - 데이터 관리
|
||||
|
||||
## 개요
|
||||
|
||||
**data** 컴포넌트는 시계열 데이터 수집, 정제, 저장, 제공을 담당합니다.
|
||||
|
||||
### 책임
|
||||
|
||||
- 외부 데이터 소스 연동 및 수집 스케줄링
|
||||
- 실시간 시세 스트리밍 및 캐시 업데이트
|
||||
- 데이터 품질 검증/보정
|
||||
- 백테스트 및 전략 실행을 위한 데이터 제공
|
||||
|
||||
### 의존성
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Data[data] --> Providers[Data Providers]
|
||||
Data --> DB[(Time-series DB)]
|
||||
Data --> Cache[(Redis Cache)]
|
||||
|
||||
Strategy[strategy] --> Data
|
||||
Analytics[analytics] --> Data
|
||||
Monitor[monitor] --> Data
|
||||
|
||||
style Data fill:#9C27B0,color:#fff
|
||||
```
|
||||
|
||||
## 주요 기능
|
||||
|
||||
### 1. 데이터 수집
|
||||
|
||||
```typescript
|
||||
collectMarketData(symbols: string[], from: Date, to: Date): void
|
||||
updateRealTimeData(symbols: string[]): void
|
||||
```
|
||||
|
||||
- 외부 제공자(Yahoo Finance, Alpha Vantage 등)에서 시세를 수집합니다.
|
||||
- 증분 업데이트로 중복 수집을 방지합니다.
|
||||
|
||||
### 2. 데이터 제공
|
||||
|
||||
```typescript
|
||||
getPriceHistory(symbol: string, from: Date, to: Date, interval: Interval): PriceBar[]
|
||||
getLatestPrice(symbol: string): Price
|
||||
getMultipleSymbols(symbols: string[], date: Date): Record<string, Price>
|
||||
```
|
||||
|
||||
- 실시간 데이터는 캐시 우선으로 제공합니다.
|
||||
|
||||
### 3. 데이터 품질 관리
|
||||
|
||||
```typescript
|
||||
validateData(symbol: string, from: Date, to: Date): ValidationReport
|
||||
adjustForCorporateActions(symbol: string): void
|
||||
fillMissingData(symbol: string, method: 'FORWARD_FILL' | 'INTERPOLATE'): void
|
||||
```
|
||||
|
||||
- 결측치/이상치 탐지 및 보정 정책을 적용합니다.
|
||||
|
||||
### 4. 데이터 소스 관리
|
||||
|
||||
```typescript
|
||||
addDataSource(source: DataSourceConfig): DataSource
|
||||
syncDataSource(sourceId: string): SyncResult
|
||||
```
|
||||
|
||||
## 데이터 모델
|
||||
|
||||
```typescript
|
||||
interface PriceBar {
|
||||
symbol: string
|
||||
timestamp: Date
|
||||
open: number
|
||||
high: number
|
||||
low: number
|
||||
close: number
|
||||
volume: number
|
||||
adjustedClose?: number
|
||||
}
|
||||
|
||||
interface DataSource {
|
||||
id: string
|
||||
name: string
|
||||
provider: 'BROKER' | 'YAHOO' | 'ALPHA_VANTAGE' | 'CUSTOM'
|
||||
config: {
|
||||
apiKey?: string
|
||||
endpoint?: string
|
||||
rateLimit?: number
|
||||
}
|
||||
coverage: {
|
||||
symbols: string[]
|
||||
intervals: string[]
|
||||
delay: number
|
||||
}
|
||||
priority: number
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
interface ValidationReport {
|
||||
symbol: string
|
||||
period: { from: Date, to: Date }
|
||||
issues: {
|
||||
type: 'MISSING_DATA' | 'OUTLIER' | 'ZERO_VOLUME' | 'PRICE_GAP'
|
||||
date: Date
|
||||
description: string
|
||||
severity: 'LOW' | 'MEDIUM' | 'HIGH'
|
||||
}[]
|
||||
completeness: number
|
||||
quality: 'EXCELLENT' | 'GOOD' | 'FAIR' | 'POOR'
|
||||
}
|
||||
```
|
||||
|
||||
## API 명세
|
||||
|
||||
### GET /api/data/prices
|
||||
가격 데이터 조회
|
||||
|
||||
### POST /api/data/sources
|
||||
데이터 소스 추가
|
||||
|
||||
## 구현 고려사항
|
||||
|
||||
- 데이터 소스별 레이트 리밋을 준수합니다.
|
||||
- 기업 이벤트(배당, 액면분할) 반영 규칙을 문서화합니다.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [공통 데이터 모델](../../docs/03-data-models.md)
|
||||
- [주요 워크플로우](../../docs/04-workflows.md)
|
||||
|
||||
### 관련 컴포넌트
|
||||
- [strategy - 전략 관리](./strategy.md)
|
||||
- [analytics - 성과 분석](../phase2/analytics.md)
|
||||
- [monitor - 모니터링](../phase2/monitor.md)
|
||||
550
components/phase1/mgmt.md
Normal file
550
components/phase1/mgmt.md
Normal file
@@ -0,0 +1,550 @@
|
||||
# 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<Container> {
|
||||
// 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<void> {
|
||||
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<VirtualBalance> {
|
||||
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<void> {
|
||||
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<Transfer> {
|
||||
// 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<ReconciliationReport> {
|
||||
// 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<string, Position>()
|
||||
|
||||
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<void> {
|
||||
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)
|
||||
169
components/phase1/risk.md
Normal file
169
components/phase1/risk.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# risk - 리스크 관리
|
||||
|
||||
## 개요
|
||||
|
||||
**risk** 컴포넌트는 사전 주문 검증과 포지션 리스크 모니터링을 담당합니다.
|
||||
|
||||
### 책임
|
||||
|
||||
- 주문 전 리스크 체크 (잔고, 포지션 한도, 레버리지)
|
||||
- 포지션 리스크 지표 계산 (VaR, 베타, 집중도)
|
||||
- 손절/익절 조건 관리 및 트리거 평가
|
||||
- 리스크 한도 설정 및 위반 감지
|
||||
|
||||
### 의존성
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Risk[risk] --> Mgmt[mgmt]
|
||||
Risk --> Balance[balance]
|
||||
Risk --> Data[data]
|
||||
Risk --> DB[(Database)]
|
||||
|
||||
Scheduler[scheduler] --> Risk
|
||||
Monitor[monitor] --> Risk
|
||||
|
||||
style Risk fill:#E53935,color:#fff
|
||||
```
|
||||
|
||||
## 주요 기능
|
||||
|
||||
### 1. 사전 주문 검증
|
||||
|
||||
```typescript
|
||||
validateOrder(order: Order, container: Container): RiskCheckResult
|
||||
validateOrderBatch(orders: Order[], container: Container): RiskCheckResult[]
|
||||
```
|
||||
|
||||
- 잔고 충분성, 포지션 사이즈, 집중도, 레버리지를 검증합니다.
|
||||
|
||||
### 2. 포지션 리스크 모니터링
|
||||
|
||||
```typescript
|
||||
calculatePositionRisk(containerId: string): PositionRisk
|
||||
checkRiskLimits(containerId: string): LimitViolation[]
|
||||
```
|
||||
|
||||
### 3. 손절/익절 관리
|
||||
|
||||
```typescript
|
||||
setStopLoss(containerId: string, symbol: string, config: StopLossConfig): void
|
||||
setTakeProfit(containerId: string, symbol: string, level: number): void
|
||||
checkStopConditions(containerId: string): StopTrigger[]
|
||||
```
|
||||
|
||||
### 4. 리스크 한도 설정
|
||||
|
||||
```typescript
|
||||
setRiskLimits(containerId: string, limits: RiskLimits): void
|
||||
getRiskProfile(containerId: string): RiskProfile
|
||||
```
|
||||
|
||||
### 5. 포트폴리오 리스크 분석
|
||||
|
||||
```typescript
|
||||
calculatePortfolioVaR(containerId: string, confidence: number, horizon: number): VaRResult
|
||||
calculateCorrelationMatrix(containerId: string): CorrelationMatrix
|
||||
stressTest(containerId: string, scenarios: Scenario[]): StressTestResult[]
|
||||
```
|
||||
|
||||
## 데이터 모델
|
||||
|
||||
```typescript
|
||||
interface RiskLimits {
|
||||
containerId: string
|
||||
position: {
|
||||
maxSinglePositionPct: number
|
||||
maxSectorPct: number
|
||||
maxTotalLeverage: number
|
||||
}
|
||||
loss: {
|
||||
maxDailyLossPct: number
|
||||
maxDrawdownPct: number
|
||||
stopLossEnabled: boolean
|
||||
}
|
||||
exposure: {
|
||||
maxLongExposure: number
|
||||
maxShortExposure: number
|
||||
maxGrossExposure: number
|
||||
}
|
||||
}
|
||||
|
||||
interface RiskCheckResult {
|
||||
passed: boolean
|
||||
violations: {
|
||||
rule: string
|
||||
severity: 'BLOCKING' | 'WARNING'
|
||||
message: string
|
||||
currentValue: number
|
||||
limitValue: number
|
||||
}[]
|
||||
recommendations?: string[]
|
||||
}
|
||||
|
||||
interface PositionRisk {
|
||||
containerId: string
|
||||
positions: {
|
||||
symbol: string
|
||||
marketValue: number
|
||||
weightPct: number
|
||||
VaR95: number
|
||||
beta: number
|
||||
sector: string
|
||||
}[]
|
||||
portfolio: {
|
||||
totalValue: number
|
||||
totalVaR95: number
|
||||
beta: number
|
||||
correlationRisk: number
|
||||
}
|
||||
limits: {
|
||||
type: string
|
||||
current: number
|
||||
limit: number
|
||||
utilizationPct: number
|
||||
}[]
|
||||
calculatedAt: Date
|
||||
}
|
||||
|
||||
interface StopLossConfig {
|
||||
type: 'FIXED' | 'TRAILING' | 'TIME_BASED'
|
||||
fixedPrice?: number
|
||||
fixedPct?: number
|
||||
trailingPct?: number
|
||||
holdingPeriodDays?: number
|
||||
autoExecute: boolean
|
||||
}
|
||||
|
||||
interface VaRResult {
|
||||
confidence: number
|
||||
horizon: number
|
||||
VaR: number
|
||||
VaRPct: number
|
||||
method: 'HISTORICAL' | 'PARAMETRIC' | 'MONTE_CARLO'
|
||||
calculatedAt: Date
|
||||
}
|
||||
```
|
||||
|
||||
## API 명세
|
||||
|
||||
### POST /api/risk/check
|
||||
주문 리스크 체크
|
||||
|
||||
### GET /api/risk/containers/:containerId
|
||||
리스크 프로필 조회
|
||||
|
||||
## 구현 고려사항
|
||||
|
||||
- 리스크 위반은 monitor 알림과 연동합니다.
|
||||
- 한도 변경은 감사 로그로 추적합니다.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [공통 데이터 모델](../../docs/03-data-models.md)
|
||||
- [주요 워크플로우](../../docs/04-workflows.md)
|
||||
|
||||
### 관련 컴포넌트
|
||||
- [mgmt - 컨테이너 관리](./mgmt.md)
|
||||
- [balance - 계좌 관리](./balance.md)
|
||||
- [strategy - 전략 관리](./strategy.md)
|
||||
149
components/phase1/scheduler.md
Normal file
149
components/phase1/scheduler.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# scheduler - 실행 스케줄러
|
||||
|
||||
## 개요
|
||||
|
||||
**scheduler** 컴포넌트는 전략 실행 자동화, 리밸런싱, 승인 워크플로우를 관리합니다.
|
||||
|
||||
### 책임
|
||||
|
||||
- 컨테이너별 실행 일정 관리
|
||||
- 실행 트리거 및 리밸런싱 스케줄링
|
||||
- 승인 요청 및 실행 흐름 제어
|
||||
- 실행 이력 저장 및 알림
|
||||
|
||||
### 의존성
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Scheduler[scheduler] --> Mgmt[mgmt]
|
||||
Scheduler --> Strategy[strategy]
|
||||
Scheduler --> Risk[risk]
|
||||
Scheduler --> Balance[balance]
|
||||
Scheduler --> Monitor[monitor]
|
||||
Scheduler --> DB[(Database)]
|
||||
|
||||
style Scheduler fill:#FF9800,color:#fff
|
||||
```
|
||||
|
||||
## 주요 기능
|
||||
|
||||
### 1. 스케줄 관리
|
||||
|
||||
```typescript
|
||||
createSchedule(containerId: string, schedule: ScheduleConfig): Schedule
|
||||
updateSchedule(scheduleId: string, config: Partial<ScheduleConfig>): Schedule
|
||||
pauseSchedule(scheduleId: string): boolean
|
||||
resumeSchedule(scheduleId: string): boolean
|
||||
```
|
||||
|
||||
- Cron/interval/이벤트 기반 트리거를 지원합니다.
|
||||
- 시장 시간 및 휴일을 고려해 다음 실행 시점을 계산합니다.
|
||||
|
||||
### 2. 실행 트리거
|
||||
|
||||
```typescript
|
||||
executeStrategy(containerId: string, mode: 'AUTO' | 'MANUAL'): Execution
|
||||
scheduleRebalancing(containerId: string): void
|
||||
```
|
||||
|
||||
- 신호 생성 → 리스크 체크 → 주문 생성 → 승인 처리 순서로 실행합니다.
|
||||
|
||||
### 3. 승인 워크플로우
|
||||
|
||||
```typescript
|
||||
requestApproval(execution: Execution): ApprovalRequest
|
||||
approveExecution(requestId: string, approved: boolean): boolean
|
||||
autoExecuteWithNotification(execution: Execution): ExecutionResult
|
||||
```
|
||||
|
||||
- 승인 모드에서는 예상 주문 내역과 비용을 사용자에게 제공합니다.
|
||||
|
||||
### 4. 실행 이력 관리
|
||||
|
||||
```typescript
|
||||
getExecutionHistory(containerId: string, from: Date): Execution[]
|
||||
getExecutionDetail(executionId: string): ExecutionDetail
|
||||
```
|
||||
|
||||
## 데이터 모델
|
||||
|
||||
```typescript
|
||||
interface Schedule {
|
||||
id: string
|
||||
containerId: string
|
||||
trigger: {
|
||||
type: 'CRON' | 'INTERVAL' | 'EVENT'
|
||||
expression?: string
|
||||
intervalMinutes?: number
|
||||
event?: 'MARKET_OPEN' | 'MARKET_CLOSE'
|
||||
}
|
||||
executionMode: 'AUTO' | 'APPROVAL_REQUIRED'
|
||||
constraints: {
|
||||
marketHoursOnly: boolean
|
||||
skipHolidays: boolean
|
||||
minIntervalHours?: number
|
||||
}
|
||||
isActive: boolean
|
||||
nextRun?: Date
|
||||
lastRun?: Date
|
||||
}
|
||||
|
||||
interface Execution {
|
||||
id: string
|
||||
containerId: string
|
||||
strategyId: string
|
||||
status: 'PENDING' | 'APPROVED' | 'REJECTED' | 'RUNNING' | 'COMPLETED' | 'FAILED'
|
||||
signals: Signal[]
|
||||
plannedOrders: Order[]
|
||||
executedOrders: Order[]
|
||||
estimatedCost: {
|
||||
commission: number
|
||||
totalValue: number
|
||||
}
|
||||
approvalRequest?: ApprovalRequest
|
||||
startedAt?: Date
|
||||
completedAt?: Date
|
||||
error?: string
|
||||
}
|
||||
|
||||
interface ApprovalRequest {
|
||||
id: string
|
||||
executionId: string
|
||||
summary: {
|
||||
numOrders: number
|
||||
buyValue: number
|
||||
sellValue: number
|
||||
estimatedCommission: number
|
||||
}
|
||||
orders: Order[]
|
||||
requestedAt: Date
|
||||
expiresAt: Date
|
||||
approvedAt?: Date
|
||||
approvedBy?: string
|
||||
decision?: 'APPROVED' | 'REJECTED'
|
||||
}
|
||||
```
|
||||
|
||||
## API 명세
|
||||
|
||||
### POST /api/schedules
|
||||
스케줄 생성
|
||||
|
||||
### POST /api/executions/:executionId/approve
|
||||
승인/거부 처리
|
||||
|
||||
## 구현 고려사항
|
||||
|
||||
- 승인 만료 처리 및 재요청 정책을 정의해야 합니다.
|
||||
- 실행 실패 시 재시도/중단 기준을 명확히 둡니다.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [주요 워크플로우](../../docs/04-workflows.md)
|
||||
- [공통 데이터 모델](../../docs/03-data-models.md)
|
||||
|
||||
### 관련 컴포넌트
|
||||
- [mgmt - 컨테이너 관리](./mgmt.md)
|
||||
- [strategy - 전략 관리](./strategy.md)
|
||||
- [risk - 리스크 관리](./risk.md)
|
||||
- [balance - 계좌 관리](./balance.md)
|
||||
177
components/phase1/strategy.md
Normal file
177
components/phase1/strategy.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# strategy - 전략 관리
|
||||
|
||||
## 개요
|
||||
|
||||
**strategy** 컴포넌트는 전략 등록, 버전 관리, 신호 생성, 백테스트를 담당합니다.
|
||||
|
||||
### 책임
|
||||
|
||||
- 전략 코드/메타데이터 등록 및 버전 관리
|
||||
- 시장 데이터 기반 매매 신호 생성
|
||||
- 신호를 주문으로 변환
|
||||
- 백테스트 실행 및 성과 지표 계산
|
||||
- 파라미터 관리 및 검증
|
||||
|
||||
### 의존성
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Strategy[strategy] --> Data[data]
|
||||
Strategy --> Risk[risk]
|
||||
Strategy --> DB[(Database)]
|
||||
|
||||
Scheduler[scheduler] --> Strategy
|
||||
Analytics[analytics] --> Strategy
|
||||
|
||||
style Strategy fill:#2196F3,color:#fff
|
||||
```
|
||||
|
||||
## 주요 기능
|
||||
|
||||
### 1. 전략 등록 및 버전 관리
|
||||
|
||||
```typescript
|
||||
registerStrategy(strategy: StrategyDefinition): Strategy
|
||||
updateStrategy(id: string, version: StrategyVersion): Strategy
|
||||
getStrategy(id: string, version?: string): Strategy
|
||||
```
|
||||
|
||||
- 전략 코드와 파라미터 메타데이터를 저장합니다.
|
||||
- 새 버전은 불변으로 보존하고 변경 이력을 추적합니다.
|
||||
|
||||
### 2. 전략 실행 엔진
|
||||
|
||||
```typescript
|
||||
calculateSignals(strategyId: string, marketData: MarketData): Signal[]
|
||||
generateOrders(containerId: string, signals: Signal[]): Order[]
|
||||
```
|
||||
|
||||
- 전략 신호를 생성하고 현재 포지션과 목표 포지션 차이로 주문을 계산합니다.
|
||||
|
||||
### 3. 백테스트
|
||||
|
||||
```typescript
|
||||
runBacktest(config: BacktestConfig): BacktestResult
|
||||
calculateMetrics(backtestResult: BacktestResult): PerformanceMetrics
|
||||
```
|
||||
|
||||
- 거래 비용과 슬리피지를 반영해 성과 지표를 산출합니다.
|
||||
|
||||
### 4. 파라미터 관리
|
||||
|
||||
```typescript
|
||||
getParameters(strategyId: string): Parameter[]
|
||||
setParameters(strategyId: string, params: Record<string, any>): void
|
||||
```
|
||||
|
||||
- 파라미터 타입/범위를 검증하고 실행 컨텍스트에 반영합니다.
|
||||
|
||||
### 전략 인터페이스
|
||||
|
||||
```typescript
|
||||
interface StrategyInterface {
|
||||
initialize(context: StrategyContext): void
|
||||
generateSignals(data: MarketData): Signal[]
|
||||
|
||||
onMarketOpen?(): void
|
||||
onMarketClose?(): void
|
||||
onOrderFilled?(order: Order): void
|
||||
}
|
||||
```
|
||||
|
||||
## 데이터 모델
|
||||
|
||||
```typescript
|
||||
interface Strategy {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: 'ASSET_ALLOCATION' | 'MOMENTUM' | 'VALUE' | 'ARBITRAGE' | 'CUSTOM'
|
||||
versions: StrategyVersion[]
|
||||
currentVersion: string
|
||||
parameters: Parameter[]
|
||||
requiredData: string[]
|
||||
createdBy: string
|
||||
createdAt: Date
|
||||
isPublic: boolean
|
||||
}
|
||||
|
||||
interface StrategyVersion {
|
||||
version: string
|
||||
code: string
|
||||
changelog?: string
|
||||
createdAt: Date
|
||||
backtestResults?: BacktestResult[]
|
||||
}
|
||||
|
||||
interface Signal {
|
||||
symbol: string
|
||||
action: 'BUY' | 'SELL' | 'HOLD'
|
||||
targetWeight?: number
|
||||
targetQuantity?: number
|
||||
reason?: string
|
||||
confidence?: number
|
||||
generatedAt: Date
|
||||
}
|
||||
|
||||
interface BacktestConfig {
|
||||
strategyId: string
|
||||
strategyVersion: string
|
||||
startDate: Date
|
||||
endDate: Date
|
||||
initialCapital: number
|
||||
universe: string[]
|
||||
benchmark?: string
|
||||
costs: {
|
||||
commission: number
|
||||
slippage: number
|
||||
}
|
||||
}
|
||||
|
||||
interface BacktestResult {
|
||||
strategyId: string
|
||||
config: BacktestConfig
|
||||
equity: {
|
||||
date: Date
|
||||
value: number
|
||||
cash: number
|
||||
positions: Record<string, number>
|
||||
}[]
|
||||
trades: {
|
||||
date: Date
|
||||
symbol: string
|
||||
action: 'BUY' | 'SELL'
|
||||
quantity: number
|
||||
price: number
|
||||
commission: number
|
||||
}[]
|
||||
metrics: PerformanceMetrics
|
||||
runAt: Date
|
||||
}
|
||||
```
|
||||
|
||||
## API 명세
|
||||
|
||||
### POST /api/strategies
|
||||
전략 등록
|
||||
|
||||
### GET /api/strategies/:strategyId
|
||||
전략 조회 (version 파라미터 지원)
|
||||
|
||||
### POST /api/strategies/:strategyId/backtests
|
||||
백테스트 실행
|
||||
|
||||
## 구현 고려사항
|
||||
|
||||
- 전략 코드 저장 시 보안 검토 및 샌드박스 실행 환경을 고려합니다.
|
||||
- 파라미터 변경은 감사 로그에 기록합니다.
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [시스템 개요](../../docs/01-overview.md)
|
||||
- [공통 데이터 모델](../../docs/03-data-models.md)
|
||||
|
||||
### 관련 컴포넌트
|
||||
- [data - 데이터 관리](./data.md)
|
||||
- [risk - 리스크 관리](./risk.md)
|
||||
- [scheduler - 실행 스케줄러](./scheduler.md)
|
||||
Reference in New Issue
Block a user