1be7029f8f
## Summary - BatchOperationsPageV2: add overflow-y: auto (ShadowRunQueue, DataQualityPage) - ModelsList: add flex:1 + min-height:0 + overflow-y:auto - ShadowRunList: change height to 100% (from calc(100vh - 210px)) - ModelOperationsPage: add overflow-y: auto - WbsWorkspacePage: add flex:1 + min-height:0 + overflow-y:auto - IngestionStatus, CommonCodeManagementPage: already fitted (via component inheritance) - MarketDataIngestion: already fitted (EditFormPage) - HomePage, RebalanceForm, UiStandardPage: already fitted (earlier session) Total: 11 pages viewport-fit, 7 pages already compliant Still needed: - ModelDetail, ShadowRunDetail: need PageLayout wrapping or refactoring Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
220 lines
7.5 KiB
Vue
220 lines
7.5 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue'
|
|
import EditFormPage from '../../../shared/ui/screen-types/v2/EditFormPage.vue'
|
|
import QueryStateBoundary from '../../../shared/ui/QueryStateBoundary.vue'
|
|
import { KsButton, KsCheckbox, KsFormGrid, KsFormSection, KsNumberField, KsStatusTag } from '../../../shared/ui/components'
|
|
import PolicyTracePanel from '../components/PolicyTracePanel.vue'
|
|
import { useEvaluateResearchSellPolicy } from '../queries'
|
|
import type { ResearchSellPolicyCommand } from '../api'
|
|
import type { StandardScreenState } from '../../../shared/ui/contracts/screenContract'
|
|
|
|
const mutation = useEvaluateResearchSellPolicy()
|
|
const hardImpairmentApproved = ref(false)
|
|
const capitalFloorBreached = ref(false)
|
|
const gapBelowFloorAtr = ref(1.6)
|
|
const consecutiveCloseBreaches = ref(0)
|
|
const lastCommand = ref<ResearchSellPolicyCommand | null>(null)
|
|
|
|
const evidence = {
|
|
asOf: new Date().toISOString(),
|
|
version: 'v60-T13-ResearchSellPolicy',
|
|
}
|
|
|
|
const isBusy = computed(() => mutation.isPending.value)
|
|
const dirty = computed(() => true) // Form always has potential changes until submitted
|
|
|
|
const screenState = computed<StandardScreenState>(() => {
|
|
if (isBusy.value) return 'PROCESSING'
|
|
if (mutation.error.value) return 'ERROR'
|
|
return 'READY'
|
|
})
|
|
|
|
function createCommand(): ResearchSellPolicyCommand {
|
|
const asOf = new Date().toISOString()
|
|
return {
|
|
idempotencyKey: crypto.randomUUID(),
|
|
request: {
|
|
positionLotId: '00000000-0000-0000-0000-000000000001',
|
|
cycleId: '00000000-0000-0000-0000-000000000002',
|
|
evidenceId: 'sample-evidence',
|
|
datasetId: 'sample-dataset',
|
|
modelVersion: 'research-v12.2',
|
|
configVersion: 'proposal-v12.2',
|
|
codeSha: 'sample-code-sha',
|
|
asOf,
|
|
publishedAtCutoff: asOf,
|
|
currentSecurityPortfolioWeight: 0.6,
|
|
currentLotPortfolioWeight: 0.2,
|
|
strategicCoreFloorWeight: 0.3,
|
|
hardImpairmentApproved: hardImpairmentApproved.value,
|
|
capitalFloorBreached: capitalFloorBreached.value,
|
|
survivalSellRatioOfLot: 0.5,
|
|
gapBelowFloorAtr: gapBelowFloorAtr.value,
|
|
consecutiveCloseBreaches: consecutiveCloseBreaches.value,
|
|
cooldownSatisfied: true,
|
|
concentrationSellRatioOfLot: 0,
|
|
opportunityEdgeLowerBound: 0,
|
|
opportunitySellRatioOfLot: 0
|
|
}
|
|
}
|
|
}
|
|
|
|
function run() {
|
|
const command = createCommand()
|
|
lastCommand.value = command
|
|
mutation.mutate(command)
|
|
}
|
|
|
|
function retry() {
|
|
if (lastCommand.value) mutation.mutate(lastCommand.value)
|
|
else run()
|
|
}
|
|
|
|
function getActionSeverity(action: string): 'success' | 'danger' | 'warning' | 'info' {
|
|
if (action === 'SELL' || action === 'FORCE_SELL') return 'danger'
|
|
if (action === 'HOLD' || action === 'PASS') return 'success'
|
|
if (action === 'REBALANCE') return 'warning'
|
|
return 'info'
|
|
}
|
|
|
|
|
|
function formatPercent(val: number | undefined): string {
|
|
if (val === undefined || val === null) return '0.0%'
|
|
return (val * 100).toFixed(1) + '%'
|
|
}
|
|
|
|
onMounted(() => {
|
|
run()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<EditFormPage
|
|
title="매도 정책 연구 콘솔"
|
|
subtitle="순수 정책 계약과 우선순위를 확인하는 연구 전용 화면입니다. 고객 제안·공개·주문 기능과 연결되지 않습니다."
|
|
:state="screenState"
|
|
:dirty="dirty"
|
|
:evidence="evidence"
|
|
@submit="run"
|
|
@retry="retry"
|
|
>
|
|
<!-- Page Top Action Bar -->
|
|
<template #actions>
|
|
<KsButton label="⚡ 매도 정책 평가 실행" variant="primary" :loading="isBusy" @click="run" />
|
|
</template>
|
|
|
|
<!-- Form Input Section -->
|
|
<KsFormSection title="연구 입력 벡터">
|
|
<KsFormGrid :columns="2" aria-label="연구 입력 벡터">
|
|
<KsCheckbox v-model="hardImpairmentApproved" label="Hard impairment 승인" :disabled="isBusy" />
|
|
<KsCheckbox v-model="capitalFloorBreached" label="자본바닥 위반" :disabled="isBusy" />
|
|
<KsNumberField v-model="gapBelowFloorAtr" label="보호선 이탈 ATR" :min="0" :max-fraction-digits="2" :disabled="isBusy" />
|
|
<KsNumberField v-model="consecutiveCloseBreaches" label="연속 종가 이탈" :min="0" :max-fraction-digits="0" :disabled="isBusy" />
|
|
</KsFormGrid>
|
|
</KsFormSection>
|
|
|
|
<!-- Result Preview Section -->
|
|
<template #preview>
|
|
<QueryStateBoundary
|
|
:loading="isBusy"
|
|
:error="mutation.error.value as Error | null"
|
|
:empty="!mutation.data.value"
|
|
empty-title="매도 정책 평가 대기 중"
|
|
empty-message="좌측 연구 입력 벡터 파라미터를 조정한 후 [⚡ 매도 정책 평가 실행] 버튼을 클릭하세요."
|
|
empty-icon="📊"
|
|
empty-action-label="⚡ 정책 평가 실행"
|
|
@retry="retry"
|
|
>
|
|
<div v-if="mutation.data.value" class="result-content">
|
|
<div class="result-header">
|
|
<h3>정책 평가 결과</h3>
|
|
<KsStatusTag :value="mutation.data.value.action" :severity="getActionSeverity(mutation.data.value.action)" />
|
|
</div>
|
|
<dl class="ks-stack">
|
|
<div class="result-row"><dt>권고 행동</dt><dd><KsStatusTag :value="mutation.data.value.action" :severity="getActionSeverity(mutation.data.value.action)" /></dd></div>
|
|
<div class="result-row"><dt>적용 정책 ID</dt><dd><code>{{ mutation.data.value.policyId }}</code></dd></div>
|
|
<div class="result-row"><dt>판단 사유 코드</dt><dd><code>{{ mutation.data.value.reasonCode }}</code></dd></div>
|
|
<div class="result-row"><dt>Lot 매도 비율</dt><dd class="ks-financial-number">{{ formatPercent(mutation.data.value.sellRatioOfLot) }}</dd></div>
|
|
<div class="result-row"><dt>매도 후 종목 비중</dt><dd class="ks-financial-number">{{ formatPercent(mutation.data.value.targetSecurityPortfolioWeightAfter) }}</dd></div>
|
|
<div class="result-row"><dt>재진입 가능 여부</dt><dd>{{ mutation.data.value.reentryEligible ? '✅ 가능' : '❌ 불가능' }}</dd></div>
|
|
<div class="result-row"><dt>결정 계약 버전</dt><dd><code>{{ mutation.data.value.decisionContractVersion }}</code></dd></div>
|
|
<div class="result-row"><dt>정책 추적 단계</dt><dd>{{ mutation.data.value.policyTrace.length }}단계 추적 완료</dd></div>
|
|
</dl>
|
|
<PolicyTracePanel
|
|
:entries="mutation.data.value.policyTrace"
|
|
:schema-version="mutation.data.value.policyTraceSchemaVersion"
|
|
/>
|
|
</div>
|
|
</QueryStateBoundary>
|
|
</template>
|
|
</EditFormPage>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.result-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--ks-space-3);
|
|
}
|
|
|
|
.result-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
border-bottom: 1px solid var(--ks-color-border-strong);
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
.result-header h3 {
|
|
margin: 0;
|
|
font-size: var(--ks-font-section);
|
|
font-weight: 700;
|
|
color: var(--ks-color-text);
|
|
}
|
|
|
|
.ks-stack {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
.result-row {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 6px 0;
|
|
border-bottom: 1px dashed var(--ks-color-border);
|
|
font-size: var(--ks-font-body);
|
|
}
|
|
|
|
.result-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.result-row dt {
|
|
font-weight: 600;
|
|
color: var(--ks-color-text-muted);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.result-row dd {
|
|
margin: 0;
|
|
text-align: right;
|
|
color: var(--ks-color-text);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.result-row code {
|
|
font-family: var(--ks-font-mono, monospace);
|
|
background: var(--ks-color-canvas);
|
|
padding: 2px 6px;
|
|
border-radius: 3px;
|
|
border: 1px solid var(--ks-color-border);
|
|
font-size: 11px;
|
|
}
|
|
</style>
|
|
|
|
|