Files
system-specs/docs/04-workflows.md

18 KiB

QuantBench 주요 워크플로우

이 문서는 시스템의 핵심 워크플로우를 상세히 설명합니다.

1. 전략 실행 워크플로우

1.1 전체 흐름

sequenceDiagram
    participant S as Scheduler
    participant M as Mgmt
    participant ST as Strategy
    participant D as Data
    participant R as Risk
    participant B as Balance
    participant A as Analytics
    participant MO as Monitor

    Note over S: 스케줄 트리거 발생

    S->>M: 1. 컨테이너 정보 조회
    M-->>S: 컨테이너 설정 & 현재 포지션

    S->>ST: 2. 전략 실행 요청
    ST->>D: 2.1 시장 데이터 조회
    D-->>ST: 최신 시세 데이터
    ST->>ST: 2.2 신호 생성
    ST-->>S: 매매 신호 반환

    S->>R: 3. 리스크 체크
    R->>R: 3.1 포지션 사이즈 검증
    R->>R: 3.2 VaR 계산
    R->>R: 3.3 한도 확인

    alt 리스크 체크 실패
        R-->>S: 검증 실패
        S->>MO: 알림 발송
        MO-->>S: 알림 완료
        Note over S: 실행 중단
    else 리스크 체크 통과
        R-->>S: 검증 통과

        alt 승인 필요 모드
            S->>S: 승인 요청 생성
            S->>MO: 사용자에게 알림
            Note over S: 사용자 승인 대기
            S->>S: 승인 완료
        end

        S->>B: 4. 주문 제출
        B->>B: 4.1 증권사 API 호출
        B-->>S: 주문 접수 완료

        Note over B: 체결 대기...

        B->>B: 4.2 체결 확인
        B->>M: 5. 포지션 업데이트
        M->>M: 5.1 컨테이너 밸런스 갱신

        M->>A: 6. 성과 계산 트리거
        A->>A: 6.1 수익률 계산
        A->>A: 6.2 리스크 지표 갱신

        A->>MO: 7. 메트릭 업데이트
        MO->>MO: 7.1 대시보드 갱신
        MO->>MO: 7.2 이상 탐지

        alt 이상 탐지 시
            MO->>MO: 알림 발송
        end

        S->>S: 8. 실행 완료 기록
    end

1.2 상세 단계

1단계: 스케줄 트리거

// 스케줄러가 설정된 시간에 실행을 트리거
scheduler.onTrigger(schedule => {
  const container = mgmt.getContainer(schedule.containerId)
  const execution = scheduler.createExecution(container, schedule)
  scheduler.executeStrategy(execution)
})

2단계: 신호 생성

// 전략이 시장 데이터를 기반으로 신호 생성
const marketData = await data.getMarketData(container.strategyUniverse)
const signals = await strategy.generateSignals(marketData, container.parameters)

// 생성된 신호 예시
// [
//   { symbol: 'AAPL', action: 'BUY', targetWeight: 0.3 },
//   { symbol: 'GOOGL', action: 'SELL', targetWeight: 0 }
// ]

3단계: 리스크 검증

// 신호를 주문으로 변환
const plannedOrders = strategy.signalsToOrders(signals, container)

// 리스크 검증
const riskResult = await risk.validateOrders(plannedOrders, container)

if (!riskResult.passed) {
  // 리스크 위반 시 실행 중단
  await monitor.sendAlert({
    type: 'RISK',
    severity: 'ERROR',
    message: `리스크 검증 실패: ${riskResult.violations}`,
    containerId: container.id
  })
  return
}

4단계: 주문 실행

// 승인 필요 모드인 경우
if (schedule.executionMode === 'APPROVAL_REQUIRED') {
  const approvalRequest = await scheduler.requestApproval(execution)
  await monitor.sendNotification(approvalRequest)

  // 승인 대기
  const approved = await scheduler.waitForApproval(approvalRequest)
  if (!approved) return
}

// 주문 제출
const executedOrders = []
for (const order of plannedOrders) {
  const result = await balance.placeOrder(order)
  executedOrders.push(result)
}

5단계: 포지션 업데이트

// 체결 완료 후 컨테이너 밸런스 업데이트
for (const order of executedOrders) {
  if (order.status === 'FILLED') {
    await mgmt.updatePosition(container.id, {
      symbol: order.symbol,
      quantity: order.side === 'BUY' ? order.filledQuantity : -order.filledQuantity,
      price: order.averageFillPrice
    })
  }
}

// 밸런스 조정 (reconciliation)
await mgmt.reconcileContainer(container.id)

2. 리스크 관리 워크플로우

2.1 사전 주문 검증

graph TB
    Start([주문 요청]) --> GetPosition[현재 포지션 조회]
    GetPosition --> CalcNewPosition[신규 포지션 계산]

    CalcNewPosition --> CheckBalance{잔고 충분?}
    CheckBalance -->|No| Reject1[주문 거부:<br/>잔고 부족]
    CheckBalance -->|Yes| CheckSize

    CheckSize{포지션 사이즈<br/>한도 내?}
    CheckSize -->|No| Reject2[주문 거부:<br/>사이즈 초과]
    CheckSize -->|Yes| CheckConcentration

    CheckConcentration{집중도<br/>한도 내?}
    CheckConcentration -->|No| Reject3[주문 거부:<br/>집중도 초과]
    CheckConcentration -->|Yes| CalcVaR

    CalcVaR[VaR 계산]
    CalcVaR --> CheckVaR{VaR<br/>한도 내?}
    CheckVaR -->|No| Warning1[경고 발생<br/>계속 진행]
    CheckVaR -->|Yes| CheckDrawdown

    Warning1 --> CheckDrawdown

    CheckDrawdown{최대 낙폭<br/>한도 내?}
    CheckDrawdown -->|No| Reject4[주문 거부:<br/>MDD 초과]
    CheckDrawdown -->|Yes| CheckLeverage

    CheckLeverage{레버리지<br/>한도 내?}
    CheckLeverage -->|No| Reject5[주문 거부:<br/>레버리지 초과]
    CheckLeverage -->|Yes| Approve

    Approve([검증 통과:<br/>주문 실행])

    Reject1 --> Log[리스크 로그 기록]
    Reject2 --> Log
    Reject3 --> Log
    Reject4 --> Log
    Reject5 --> Log

    Approve --> ExecuteOrder[주문 제출]
    ExecuteOrder --> Monitor[실시간 모니터링]

    Monitor --> CheckStop{손절/익절<br/>조건 충족?}
    CheckStop -->|Yes| AutoClose[자동 청산]
    CheckStop -->|No| Monitor

    style Start fill:#4CAF50,color:#fff
    style Approve fill:#4CAF50,color:#fff
    style Reject1 fill:#F44336,color:#fff
    style Reject2 fill:#F44336,color:#fff
    style Reject3 fill:#F44336,color:#fff
    style Reject4 fill:#F44336,color:#fff
    style Reject5 fill:#F44336,color:#fff
    style Warning1 fill:#FF9800,color:#fff

2.2 실시간 리스크 모니터링

// 실시간 포지션 모니터링
monitor.watchContainer(container.id, async (positions, marketData) => {
  // 손절 조건 체크
  for (const position of positions) {
    const stopLoss = risk.getStopLoss(container.id, position.symbol)
    if (stopLoss && position.unrealizedPnLPct <= -stopLoss.pct) {
      // 손절 트리거
      await scheduler.executeEmergencyClose(container.id, position.symbol)
      await monitor.sendAlert({
        type: 'RISK',
        severity: 'WARNING',
        message: `손절 실행: ${position.symbol} ${position.unrealizedPnLPct}%`
      })
    }
  }

  // MDD 체크
  const performance = await analytics.getCurrentPerformance(container.id)
  const limits = await risk.getRiskLimits(container.id)

  if (performance.currentDrawdown >= limits.loss.maxDrawdownPct) {
    // 컨테이너 일시 정지
    await mgmt.pauseContainer(container.id)
    await monitor.sendAlert({
      type: 'RISK',
      severity: 'CRITICAL',
      message: `최대 낙폭 초과: ${performance.currentDrawdown}%`
    })
  }
})

3. 승인 워크플로우

3.1 승인 요청 및 처리

graph TB
    Trigger[스케줄 트리거] --> GenerateSignals[신호 생성]
    GenerateSignals --> RiskCheck[리스크 체크]

    RiskCheck --> CheckMode{실행 모드}

    CheckMode -->|AUTO| AutoExecute[자동 실행]
    CheckMode -->|APPROVAL| CreateRequest

    CreateRequest[승인 요청 생성]
    CreateRequest --> SendNotification[알림 발송]
    SendNotification --> WaitApproval[승인 대기]

    WaitApproval --> CheckTimeout{타임아웃?}
    CheckTimeout -->|Yes| Expired[요청 만료]
    CheckTimeout -->|No| WaitApproval

    WaitApproval --> UserDecision{사용자 결정}

    UserDecision -->|승인| Execute[주문 실행]
    UserDecision -->|거부| Rejected[실행 거부]

    AutoExecute --> Execute

    Execute --> PlaceOrders[주문 제출]
    PlaceOrders --> Success{성공?}

    Success -->|Yes| NotifySuccess[성공 알림]
    Success -->|No| NotifyFail[실패 알림]

    Rejected --> LogRejection[거부 로그]
    Expired --> LogExpired[만료 로그]

    NotifySuccess --> End([완료])
    NotifyFail --> End
    LogRejection --> End
    LogExpired --> End

    style Execute fill:#4CAF50,color:#fff
    style Rejected fill:#F44336,color:#fff
    style Expired fill:#FF9800,color:#fff
    style AutoExecute fill:#2196F3,color:#fff

3.2 승인 요청 생성

async function createApprovalRequest(execution: Execution): Promise<ApprovalRequest> {
  // 주문 요약 생성
  const summary = {
    numOrders: execution.plannedOrders.length,
    buyValue: execution.plannedOrders
      .filter(o => o.side === 'BUY')
      .reduce((sum, o) => sum + o.quantity * o.price!, 0),
    sellValue: execution.plannedOrders
      .filter(o => o.side === 'SELL')
      .reduce((sum, o) => sum + o.quantity * o.price!, 0),
    estimatedCommission: execution.estimatedCost.commission
  }

  // 승인 요청 생성
  const request = await scheduler.createApprovalRequest({
    executionId: execution.id,
    summary,
    orders: execution.plannedOrders,
    expiresAt: new Date(Date.now() + 30 * 60 * 1000) // 30분 후 만료
  })

  // 알림 발송
  await monitor.sendNotification({
    type: 'EXECUTION',
    severity: 'INFO',
    title: '주문 승인 요청',
    message: `
      ${summary.numOrders}건의 주문이 대기 중입니다.
      매수: ${summary.buyValue.toLocaleString()}      매도: ${summary.sellValue.toLocaleString()}      예상 수수료: ${summary.estimatedCommission.toLocaleString()}    `,
    channels: ['EMAIL', 'PUSH']
  })

  return request
}

4. 컨테이너 생명주기

4.1 상태 전이도

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()

    state Running {
        [*] --> GenerateSignals
        GenerateSignals --> RiskCheck
        RiskCheck --> PlaceOrders: pass
        RiskCheck --> [*]: fail
        PlaceOrders --> AwaitFill
        AwaitFill --> UpdatePosition
        UpdatePosition --> [*]
    }

4.2 컨테이너 생성 및 활성화

// 1. 컨테이너 생성
const container = await mgmt.createContainer({
  accountId: 'account-123',
  name: '성장주 전략',
  allocation: {
    initialAmount: 10000000,  // 1천만원
    cashReserve: 1000000      // 최소 100만원 현금 보유
  },
  constraints: {
    maxSinglePositionPct: 20,  // 단일 종목 최대 20%
    maxDrawdown: 15,           // 최대 낙폭 15%
    allowedAssetClasses: ['STOCK']
  }
})

// 2. 전략 할당
await mgmt.assignStrategy(container.id, 'strategy-momentum-v1')

// 3. 리스크 한도 설정
await risk.setRiskLimits(container.id, {
  position: {
    maxSinglePositionPct: 20,
    maxSectorPct: 40,
    maxTotalLeverage: 1.0
  },
  loss: {
    maxDailyLossPct: 5,
    maxDrawdownPct: 15,
    stopLossEnabled: true
  }
})

// 4. 스케줄 설정
await scheduler.createSchedule({
  containerId: container.id,
  trigger: {
    type: 'CRON',
    expression: '0 9 * * 1-5'  // 평일 오전 9시
  },
  executionMode: 'APPROVAL_REQUIRED',
  constraints: {
    marketHoursOnly: true,
    skipHolidays: true
  }
})

// 5. 컨테이너 활성화
await mgmt.activateContainer(container.id)

5. 백테스트 워크플로우

5.1 백테스트 실행

sequenceDiagram
    participant U as 사용자
    participant ST as Strategy
    participant D as Data
    participant BE as Backtest Engine
    participant A as Analytics

    U->>ST: 백테스트 요청
    ST->>D: 과거 데이터 조회
    D-->>ST: 시계열 데이터

    ST->>BE: 백테스트 시작

    loop 각 날짜마다
        BE->>ST: 신호 생성
        ST-->>BE: 매매 신호
        BE->>BE: 가상 주문 체결
        BE->>BE: 포트폴리오 업데이트
    end

    BE->>A: 성과 계산
    A-->>BE: 성과 지표

    BE-->>U: 백테스트 결과

5.2 백테스트 구현

async function runBacktest(config: BacktestConfig): Promise<BacktestResult> {
  // 1. 데이터 로드
  const marketData = await data.getHistoricalPrices(
    config.universe,
    config.startDate,
    config.endDate
  )

  // 2. 초기 상태 설정
  let portfolio = {
    cash: config.initialCapital,
    positions: {} as Record<string, number>,
    equity: [{ date: config.startDate, value: config.initialCapital }]
  }

  const trades: Trade[] = []

  // 3. 시뮬레이션 실행
  for (const date of marketData.dates) {
    // 신호 생성
    const signals = await strategy.generateSignals(
      marketData.getDataAsOf(date)
    )

    // 주문 생성 및 체결
    for (const signal of signals) {
      if (signal.action === 'HOLD') continue

      const price = marketData.getPrice(signal.symbol, date)
      const quantity = calculateQuantity(signal, portfolio, price)

      // 슬리피지 및 수수료 적용
      const executionPrice = applySlippage(price, signal.action, config.costs.slippage)
      const commission = executionPrice * quantity * config.costs.commission

      // 포트폴리오 업데이트
      if (signal.action === 'BUY') {
        portfolio.cash -= executionPrice * quantity + commission
        portfolio.positions[signal.symbol] = (portfolio.positions[signal.symbol] || 0) + quantity
      } else {
        portfolio.cash += executionPrice * quantity - commission
        portfolio.positions[signal.symbol] -= quantity
      }

      // 거래 기록
      trades.push({
        date,
        symbol: signal.symbol,
        action: signal.action,
        quantity,
        price: executionPrice,
        commission
      })
    }

    // 일일 자산 가치 계산
    const positionsValue = Object.entries(portfolio.positions)
      .reduce((sum, [symbol, qty]) => {
        return sum + qty * marketData.getPrice(symbol, date)
      }, 0)

    portfolio.equity.push({
      date,
      value: portfolio.cash + positionsValue,
      cash: portfolio.cash,
      positions: { ...portfolio.positions }
    })
  }

  // 4. 성과 지표 계산
  const metrics = await analytics.calculateMetrics({
    equity: portfolio.equity,
    trades,
    benchmark: config.benchmark
  })

  return {
    id: generateId(),
    strategyId: config.strategyId,
    config,
    equity: portfolio.equity,
    trades,
    metrics,
    runAt: new Date(),
    duration: Date.now() - startTime
  }
}

6. 데이터 수집 워크플로우

6.1 데이터 수집 파이프라인

graph LR
    subgraph "외부 소스"
        Broker[증권사 API]
        Yahoo[Yahoo Finance]
        Alpha[Alpha Vantage]
    end

    subgraph "데이터 수집"
        Collector[Data Collector]
        RealTime[실시간 스트림]
        Historical[과거 데이터 수집]
    end

    subgraph "데이터 처리"
        Validator[데이터 검증]
        Adjuster[조정 처리]
        QualityCheck[품질 체크]
    end

    subgraph "저장소"
        Cache[(Redis<br/>실시간)]
        TimeSeries[(TimescaleDB<br/>과거)]
        Metadata[(Metadata<br/>종목 정보)]
    end

    Broker --> RealTime
    Yahoo --> Historical
    Alpha --> Historical

    RealTime --> Collector
    Historical --> Collector

    Collector --> Validator
    Validator --> QualityCheck
    QualityCheck -->|통과| Adjuster
    QualityCheck -->|실패| Alert[품질 알림]

    Adjuster --> Cache
    Adjuster --> TimeSeries
    Adjuster --> Metadata

    style Cache fill:#FF6B6B,color:#fff
    style TimeSeries fill:#4ECDC4,color:#fff
    style Metadata fill:#45B7D1,color:#fff

6.2 데이터 품질 관리

// 데이터 수집 및 검증
async function collectAndValidateData(symbols: string[], date: Date) {
  for (const symbol of symbols) {
    // 1. 데이터 수집
    const rawData = await dataProvider.fetchPriceData(symbol, date)

    // 2. 데이터 검증
    const validationResult = await data.validateData(rawData)

    if (!validationResult.isValid) {
      // 품질 문제 감지
      await monitor.sendAlert({
        type: 'SYSTEM',
        severity: 'WARNING',
        message: `데이터 품질 문제: ${symbol} - ${validationResult.issues.join(', ')}`
      })

      // 결측치 보정 시도
      if (validationResult.canFill) {
        rawData = await data.fillMissingData(rawData, 'FORWARD_FILL')
      } else {
        continue  // 보정 불가능하면 skip
      }
    }

    // 3. 기업 행동 조정 (배당, 액면분할 등)
    const adjustedData = await data.adjustForCorporateActions(rawData, symbol)

    // 4. 저장
    await data.storePriceData(adjustedData)
  }
}

7. 성과 분석 워크플로우

7.1 일일 성과 계산

// 매일 장 마감 후 실행
scheduler.scheduleDaily('MARKET_CLOSE', async () => {
  const containers = await mgmt.getActiveContainers()

  for (const container of containers) {
    // 1. 현재 포지션 평가
    const positions = await mgmt.getPositions(container.id)
    const marketData = await data.getLatestPrices(positions.map(p => p.symbol))

    // 2. 일일 수익률 계산
    const dailyReturn = await analytics.calculateDailyReturn(
      container.id,
      positions,
      marketData
    )

    // 3. 누적 성과 업데이트
    await analytics.updatePerformance(container.id, {
      date: new Date(),
      return: dailyReturn,
      equity: positions.reduce((sum, p) => sum + p.marketValue, 0)
    })

    // 4. 리스크 지표 재계산
    const riskMetrics = await analytics.calculateRiskMetrics(container.id)

    // 5. 이상 탐지
    if (dailyReturn < -5) {  // 일일 손실 5% 초과
      await monitor.sendAlert({
        type: 'PERFORMANCE',
        severity: 'WARNING',
        message: `일일 손실 초과: ${container.name} ${dailyReturn.toFixed(2)}%`
      })
    }
  }
})

8. 관련 문서