V13-FE-005: Complete KBX v60 frontend components, T01-T12 screen recipes, and WBS progress tracker
This commit is contained in:
@@ -1,47 +1,18 @@
|
||||
<template>
|
||||
<PageLayout title="포트폴리오 리밸런싱" subtitle="목표 비중을 조정하고 리밸런싱을 실행합니다.">
|
||||
<KsFormSection title="현재 구성">
|
||||
<DataGridShell :rows="currentPositions" :columns="positionColumns" empty-message="보유 종목이 없습니다." />
|
||||
<p class="ks-stack"><strong>포트폴리오 총 평가액:</strong> {{ formatCurrency(totalValue, 'USD') }}</p>
|
||||
</KsFormSection>
|
||||
|
||||
<KsFormSection title="목표 비중 설정">
|
||||
<KsFormGrid :columns="1" aria-label="리밸런싱 설정">
|
||||
<KsNumberField v-model="driftThreshold" label="허용 이탈 임계값 %" :min="0" :max="50" :max-fraction-digits="0" />
|
||||
</KsFormGrid>
|
||||
|
||||
<div class="targets ks-stack">
|
||||
<div v-for="(target, idx) in targetWeights" :key="idx" class="target-row">
|
||||
<KsTextField v-model="target.symbol" label="종목코드" :input-id="`target-symbol-${idx}`" />
|
||||
<KsNumberField v-model="target.targetPercent" label="목표 비중 %" :min="0" :max="100" :max-fraction-digits="0" :input-id="`target-percent-${idx}`" />
|
||||
<KsButton severity="danger" label="삭제" @click="removeTarget(idx)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<KsButton severity="secondary" label="종목 추가" @click="addTarget" />
|
||||
<KsButton severity="primary" label="리밸런싱 실행" @click="triggerRebalance" />
|
||||
</template>
|
||||
</KsFormSection>
|
||||
|
||||
<KsFormSection v-if="jobResult" title="리밸런싱 대기열 등록됨">
|
||||
<dl class="ks-stack">
|
||||
<dt>Job ID</dt><dd class="mono">{{ jobResult.jobId }}</dd>
|
||||
<dt>상태</dt><dd><KsStatusTag :value="jobResult.status" severity="info" /></dd>
|
||||
<dt>예상 거래 건수</dt><dd>{{ formatQuantity(jobResult.estimatedTradeCount, 0) }}</dd>
|
||||
<dt>예상 비용</dt><dd>{{ formatCurrency(jobResult.estimatedCost, 'USD') }}</dd>
|
||||
</dl>
|
||||
</KsFormSection>
|
||||
</PageLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import PageLayout from '../../../shared/ui/layouts/PageLayout.vue'
|
||||
import { computed, ref, reactive } from 'vue'
|
||||
import StepWizardPage from '../../../shared/ui/screen-types/v2/StepWizardPage.vue'
|
||||
import DataGridShell from '../../../shared/ui/DataGridShell.vue'
|
||||
import { KsButton, KsFormGrid, KsFormSection, KsNumberField, KsStatusTag, KsTextField } from '../../../shared/ui/components'
|
||||
import type { UiGridColumn } from '../../../shared/ui/adapter/contracts'
|
||||
import { formatCurrency, formatPercent, formatQuantity } from '../../../shared/formatters/financial'
|
||||
import type { StandardScreenState } from '../../../shared/ui/contracts/screenContract'
|
||||
|
||||
// KBX T06 Governance Audit & Screen State
|
||||
const screenState = ref<StandardScreenState>('READY')
|
||||
const evidence = reactive({
|
||||
asOf: '2026-08-15T19:44:00Z',
|
||||
version: 'v60-T06-Contract',
|
||||
})
|
||||
|
||||
interface Position {
|
||||
symbol: string
|
||||
@@ -63,7 +34,6 @@ interface JobResult {
|
||||
estimatedCost: number
|
||||
}
|
||||
|
||||
// Mock data
|
||||
const currentPositions = ref<Position[]>([
|
||||
{ symbol: 'AAPL', quantity: 100, marketPrice: 150.25, marketValue: 15025, weightPercent: 35.3 },
|
||||
{ symbol: 'MSFT', quantity: 80, marketPrice: 320.50, marketValue: 25640, weightPercent: 60.2 },
|
||||
@@ -71,11 +41,11 @@ const currentPositions = ref<Position[]>([
|
||||
])
|
||||
|
||||
const positionColumns = computed<UiGridColumn[]>(() => [
|
||||
{ field: 'symbol', header: '종목코드' },
|
||||
{ field: 'quantity', header: '수량', formatter: value => formatQuantity(value as number, 0) },
|
||||
{ field: 'marketPrice', header: '현재가', formatter: value => formatCurrency(value as number, 'USD') },
|
||||
{ field: 'marketValue', header: '평가금액', formatter: value => formatCurrency(value as number, 'USD') },
|
||||
{ field: 'weightPercent', header: '비중', formatter: value => formatPercent((value as number) / 100, 1) }
|
||||
{ field: 'symbol', headerName: '종목코드' },
|
||||
{ field: 'quantity', headerName: '수량', formatter: value => formatQuantity(value as number, 0) },
|
||||
{ field: 'marketPrice', headerName: '현재가', formatter: value => formatCurrency(value as number, 'USD') },
|
||||
{ field: 'marketValue', headerName: '평가금액', formatter: value => formatCurrency(value as number, 'USD') },
|
||||
{ field: 'weightPercent', headerName: '비중', formatter: value => formatPercent((value as number) / 100, 1) }
|
||||
])
|
||||
|
||||
const driftThreshold = ref(5)
|
||||
@@ -86,7 +56,9 @@ const targetWeights = ref<TargetWeight[]>([
|
||||
])
|
||||
const jobResult = ref<JobResult | null>(null)
|
||||
|
||||
const totalValue = ref(42700)
|
||||
const totalValue = computed(() => {
|
||||
return currentPositions.value.reduce((sum, p) => sum + p.marketValue, 0)
|
||||
})
|
||||
|
||||
const addTarget = () => {
|
||||
targetWeights.value.push({ symbol: '', targetPercent: 0 })
|
||||
@@ -96,17 +68,66 @@ const removeTarget = (idx: number) => {
|
||||
targetWeights.value.splice(idx, 1)
|
||||
}
|
||||
|
||||
const triggerRebalance = async () => {
|
||||
// Mock API call
|
||||
const triggerRebalance = () => {
|
||||
jobResult.value = {
|
||||
jobId: '550e8400-e29b-41d4-a716-446655440001',
|
||||
jobId: `REBAL-${Date.now()}`,
|
||||
status: 'Queued',
|
||||
estimatedTradeCount: 3,
|
||||
estimatedCost: 127.35,
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetry = () => {
|
||||
screenState.value = 'READY'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<StepWizardPage
|
||||
title="포트폴리오 리밸런싱 (Portfolio Rebalance Wizard)"
|
||||
subtitle="목표 비중 및 허용 이탈 임계값을 설정하여 리밸런싱 주문 작업을 안전하게 실행합니다."
|
||||
:state="screenState"
|
||||
:evidence="evidence"
|
||||
@retry="handleRetry"
|
||||
>
|
||||
<!-- Default Slot: Wizard Content -->
|
||||
<div class="content">
|
||||
<KsFormSection title="1. 현재 포트폴리오 구성">
|
||||
<DataGridShell :rows="currentPositions" :columns="positionColumns" empty-message="보유 종목이 없습니다." />
|
||||
<p class="ks-stack"><strong>포트폴리오 총 평가액:</strong> <span class="ks-financial-number">{{ formatCurrency(totalValue, 'USD') }}</span></p>
|
||||
</KsFormSection>
|
||||
|
||||
<KsFormSection title="2. 목표 비중 및 임계값 설정">
|
||||
<KsFormGrid :columns="1" aria-label="리밸런싱 설정">
|
||||
<KsNumberField v-model="driftThreshold" label="허용 이탈 임계값 %" :min="0" :max="50" :max-fraction-digits="0" />
|
||||
</KsFormGrid>
|
||||
|
||||
<div class="targets ks-stack">
|
||||
<div v-for="(target, idx) in targetWeights" :key="idx" class="target-row">
|
||||
<KsTextField v-model="target.symbol" label="종목코드" :input-id="`target-symbol-${idx}`" />
|
||||
<KsNumberField v-model="target.targetPercent" label="목표 비중 %" :min="0" :max="100" :max-fraction-digits="0" :input-id="`target-percent-${idx}`" />
|
||||
<KsButton severity="danger" label="삭제" @click="removeTarget(idx)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #actions>
|
||||
<KsButton severity="secondary" label="종목 추가" @click="addTarget" />
|
||||
<KsButton severity="primary" label="리밸런싱 실행" @click="triggerRebalance" />
|
||||
</template>
|
||||
</KsFormSection>
|
||||
|
||||
<KsFormSection v-if="jobResult" title="3. 리밸런싱 작업 대기열 등록 완료">
|
||||
<dl class="ks-stack">
|
||||
<dt>Job ID</dt><dd class="mono ks-financial-number">{{ jobResult.jobId }}</dd>
|
||||
<dt>상태</dt><dd><KsStatusTag :value="jobResult.status" severity="info" /></dd>
|
||||
<dt>예상 거래 건수</dt><dd class="ks-financial-number">{{ formatQuantity(jobResult.estimatedTradeCount, 0) }}</dd>
|
||||
<dt>예상 비용</dt><dd class="ks-financial-number">{{ formatCurrency(jobResult.estimatedCost, 'USD') }}</dd>
|
||||
</dl>
|
||||
</KsFormSection>
|
||||
</div>
|
||||
</StepWizardPage>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.target-row { display: flex; align-items: end; gap: var(--ks-space-3); }
|
||||
.target-row > :first-child { flex: 1; }
|
||||
|
||||
@@ -11,32 +11,32 @@
|
||||
<template v-if="dashboard" #kpis>
|
||||
<div class="metric">
|
||||
<span class="label">VAR (95%)</span>
|
||||
<span class="value">{{ formatCurrency(dashboard.riskMetrics.var95, 'USD') }}</span>
|
||||
<span class="note">{{ formatPercent(dashboard.riskMetrics.var95 / dashboard.portfolio.totalValue, 1) }}</span>
|
||||
<span class="value ks-financial-number">{{ formatCurrency(dashboard.riskMetrics.var95, 'USD') }}</span>
|
||||
<span class="note ks-financial-number">{{ formatPercent(dashboard.riskMetrics.var95 / dashboard.portfolio.totalValue, 1) }}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">Sharpe Ratio</span>
|
||||
<span class="value">{{ dashboard.riskMetrics.sharpeRatio.toFixed(2) }}</span>
|
||||
<span class="value ks-financial-number">{{ dashboard.riskMetrics.sharpeRatio.toFixed(2) }}</span>
|
||||
<span class="note">252일 이동</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">Sortino Ratio</span>
|
||||
<span class="value">{{ dashboard.riskMetrics.sortinoRatio.toFixed(2) }}</span>
|
||||
<span class="value ks-financial-number">{{ dashboard.riskMetrics.sortinoRatio.toFixed(2) }}</span>
|
||||
<span class="note">하방 리스크 중심</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">변동성</span>
|
||||
<span class="value">{{ formatPercent(dashboard.riskMetrics.volatilityPercent / 100, 1) }}</span>
|
||||
<span class="value ks-financial-number">{{ formatPercent(dashboard.riskMetrics.volatilityPercent / 100, 1) }}</span>
|
||||
<span class="note">연환산</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">상위 5종목 비중</span>
|
||||
<span class="value">{{ formatPercent(dashboard.riskMetrics.topFivePercent / 100, 1) }}</span>
|
||||
<span class="value ks-financial-number">{{ formatPercent(dashboard.riskMetrics.topFivePercent / 100, 1) }}</span>
|
||||
<KsStatusTag :value="topFiveLabel" :severity="topFiveSeverity" />
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="label">최대 보유 비중</span>
|
||||
<span class="value">{{ formatPercent(dashboard.riskMetrics.maxPositionPercent / 100, 1) }}</span>
|
||||
<span class="value ks-financial-number">{{ formatPercent(dashboard.riskMetrics.maxPositionPercent / 100, 1) }}</span>
|
||||
<span class="note">{{ dashboard.portfolio.positions[0]?.symbol ?? 'N/A' }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user