feat: introduce better mcp
This commit is contained in:
@@ -1,21 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { AnimatePresence } from 'motion-v'
|
||||
import { ArrowLeft, Ban, RefreshCw } from 'lucide-vue-next'
|
||||
import {
|
||||
ArrowLeft,
|
||||
Ban,
|
||||
FileText,
|
||||
Pencil,
|
||||
ReceiptText,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
} from 'lucide-vue-next'
|
||||
import Spinner from '../../components/ui/Spinner.vue'
|
||||
import UiButton from '../../components/ui/UiButton.vue'
|
||||
import UiModal from '../../components/ui/UiModal.vue'
|
||||
import { ui } from '../../styles/ui'
|
||||
import type { OrderRow } from '../../types/trading'
|
||||
import type { OrderDetail, OrderPreview, OrderRow, TradeRecordRow } from '../../types/trading'
|
||||
import { currencyForMarket } from '../../utils/format'
|
||||
import { orderAmountText, orderHistoryKey, orderQuantityText } from '../trading/trading-data'
|
||||
|
||||
defineProps<{
|
||||
const props = defineProps<{
|
||||
orders: OrderRow[]
|
||||
connected: boolean
|
||||
dataLoading: boolean
|
||||
cancelingOrderKey: string
|
||||
orderHistoryLoaded: boolean
|
||||
orderHistoryNotice: string
|
||||
loadOrderDetail: (order: OrderRow) => Promise<OrderDetail>
|
||||
loadTradeRecords: () => Promise<TradeRecordRow[]>
|
||||
estimateOrderCorrection: (
|
||||
order: OrderRow,
|
||||
draft: { quantity: number; priceCondition: 'market' | 'limit'; price?: number },
|
||||
) => Promise<OrderPreview>
|
||||
placeOrderCorrection: (
|
||||
order: OrderRow,
|
||||
draft: { quantity: number; priceCondition: 'market' | 'limit'; price?: number },
|
||||
) => Promise<void>
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -25,11 +44,51 @@ const emit = defineEmits<{
|
||||
|
||||
const cancelCandidate = ref<OrderRow | null>(null)
|
||||
const usMarkets = new Set(['XNAS', 'XNYS', 'ARCX'])
|
||||
const isUsMarket = (market: string) => usMarkets.has(market)
|
||||
const canCancel = (order: OrderRow) =>
|
||||
order.status === '注文中' && Boolean(order.orderNumber) && !usMarkets.has(order.market)
|
||||
order.status === '注文中' && Boolean(order.orderNumber) && order.cancelable !== false
|
||||
const canShowUsDetail = (order: OrderRow) => isUsMarket(order.market)
|
||||
const canCorrect = (order: OrderRow) =>
|
||||
order.status === '注文中' &&
|
||||
Boolean(order.orderNumber) &&
|
||||
isUsMarket(order.market) &&
|
||||
order.correctable !== false
|
||||
const isCanceling = (order: OrderRow, cancelingOrderKey: string) =>
|
||||
orderHistoryKey(order) === cancelingOrderKey
|
||||
const cancelTitle = computed(() => cancelCandidate.value?.stock ?? '')
|
||||
const orderDetail = ref<OrderDetail | null>(null)
|
||||
const orderDetailLoadingKey = ref('')
|
||||
const orderActionError = ref('')
|
||||
const tradeRecords = ref<TradeRecordRow[]>([])
|
||||
const showTradeRecords = ref(false)
|
||||
const tradeRecordsLoading = ref(false)
|
||||
const correctionCandidate = ref<OrderRow | null>(null)
|
||||
const correctionQuantityInput = ref('')
|
||||
const correctionPriceCondition = ref<'market' | 'limit'>('limit')
|
||||
const correctionPriceInput = ref('')
|
||||
const correctionPreview = ref<OrderPreview | null>(null)
|
||||
const correctionLoading = ref(false)
|
||||
const correctionSubmitting = ref(false)
|
||||
|
||||
const orderActionKey = (order: OrderRow) => orderHistoryKey(order)
|
||||
const numericInput = (value: string) => {
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
const correctionDraft = () => {
|
||||
const quantity = numericInput(correctionQuantityInput.value)
|
||||
if (!quantity || quantity <= 0) throw new Error('数量を入力してください')
|
||||
if (correctionPriceCondition.value === 'market') {
|
||||
return { quantity, priceCondition: 'market' as const }
|
||||
}
|
||||
const price = numericInput(correctionPriceInput.value)
|
||||
if (!price || price <= 0) throw new Error('指値を入力してください')
|
||||
return { quantity, priceCondition: 'limit' as const, price }
|
||||
}
|
||||
const priceText = (value: number | null | undefined, market: string) =>
|
||||
value == null ? '-' : currencyForMarket(value, market)
|
||||
const tradeAmountText = (record: TradeRecordRow) =>
|
||||
record.amount == null ? '-' : currencyForMarket(record.amount, record.market)
|
||||
|
||||
const askCancel = (order: OrderRow) => {
|
||||
if (!canCancel(order)) return
|
||||
@@ -41,27 +100,108 @@ const confirmCancel = () => {
|
||||
emit('cancel', cancelCandidate.value)
|
||||
cancelCandidate.value = null
|
||||
}
|
||||
|
||||
const showOrderDetail = async (order: OrderRow) => {
|
||||
if (!canShowUsDetail(order) || orderDetailLoadingKey.value) return
|
||||
orderActionError.value = ''
|
||||
orderDetailLoadingKey.value = orderActionKey(order)
|
||||
try {
|
||||
orderDetail.value = await props.loadOrderDetail(order)
|
||||
} catch (cause) {
|
||||
orderActionError.value = cause instanceof Error ? cause.message : '注文詳細の取得に失敗しました'
|
||||
} finally {
|
||||
orderDetailLoadingKey.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const loadTradeRecords = async () => {
|
||||
if (tradeRecordsLoading.value) return
|
||||
orderActionError.value = ''
|
||||
tradeRecordsLoading.value = true
|
||||
try {
|
||||
tradeRecords.value = await props.loadTradeRecords()
|
||||
showTradeRecords.value = true
|
||||
} catch (cause) {
|
||||
orderActionError.value = cause instanceof Error ? cause.message : '約定履歴の取得に失敗しました'
|
||||
} finally {
|
||||
tradeRecordsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const askCorrection = (order: OrderRow) => {
|
||||
if (!canCorrect(order)) return
|
||||
correctionCandidate.value = order
|
||||
correctionQuantityInput.value = String(order.unexecutedQuantity ?? order.quantity ?? '')
|
||||
correctionPriceCondition.value = order.price == null ? 'market' : 'limit'
|
||||
correctionPriceInput.value = order.price == null ? '' : String(order.price)
|
||||
correctionPreview.value = null
|
||||
orderActionError.value = ''
|
||||
}
|
||||
|
||||
const estimateCorrection = async () => {
|
||||
if (!correctionCandidate.value || correctionLoading.value) return
|
||||
orderActionError.value = ''
|
||||
correctionLoading.value = true
|
||||
try {
|
||||
correctionPreview.value = await props.estimateOrderCorrection(
|
||||
correctionCandidate.value,
|
||||
correctionDraft(),
|
||||
)
|
||||
} catch (cause) {
|
||||
orderActionError.value = cause instanceof Error ? cause.message : '注文訂正の見積に失敗しました'
|
||||
} finally {
|
||||
correctionLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const confirmCorrection = async () => {
|
||||
if (!correctionCandidate.value || correctionSubmitting.value) return
|
||||
orderActionError.value = ''
|
||||
correctionSubmitting.value = true
|
||||
try {
|
||||
await props.placeOrderCorrection(correctionCandidate.value, correctionDraft())
|
||||
correctionCandidate.value = null
|
||||
correctionPreview.value = null
|
||||
} catch (cause) {
|
||||
orderActionError.value = cause instanceof Error ? cause.message : '注文訂正に失敗しました'
|
||||
} finally {
|
||||
correctionSubmitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section :class="ui.panel">
|
||||
<div :class="ui.panelHead">
|
||||
<h2>取引履歴</h2>
|
||||
<button
|
||||
:class="ui.ghostButton"
|
||||
type="button"
|
||||
:disabled="!connected || dataLoading"
|
||||
@click="emit('refresh')"
|
||||
>
|
||||
<RefreshCw class="h-4 w-4" aria-hidden="true" />
|
||||
履歴を更新
|
||||
</button>
|
||||
<span :class="ui.rowActions">
|
||||
<button
|
||||
:class="ui.ghostButton"
|
||||
type="button"
|
||||
:disabled="!connected || tradeRecordsLoading"
|
||||
@click="loadTradeRecords"
|
||||
>
|
||||
<Spinner v-if="tradeRecordsLoading" size="sm" />
|
||||
<ReceiptText v-else class="h-4 w-4" aria-hidden="true" />
|
||||
約定履歴
|
||||
</button>
|
||||
<button
|
||||
:class="ui.ghostButton"
|
||||
type="button"
|
||||
:disabled="!connected || dataLoading"
|
||||
@click="emit('refresh')"
|
||||
>
|
||||
<RefreshCw class="h-4 w-4" aria-hidden="true" />
|
||||
更新
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="orderActionError" :class="ui.dialogNote">{{ orderActionError }}</p>
|
||||
<div :class="ui.list">
|
||||
<div v-for="order in orders" :key="order.id" :class="ui.orderRow">
|
||||
<span>
|
||||
<strong>{{ order.stock }}</strong>
|
||||
<small>{{ order.date }} / 通常単元</small>
|
||||
<small>{{ order.code }} / {{ order.market }} / {{ order.date }}</small>
|
||||
</span>
|
||||
<span :class="order.side === 'buy' ? ui.positive : ui.negative">
|
||||
{{ order.side === 'buy' ? '購入' : '売却' }}
|
||||
@@ -71,17 +211,43 @@ const confirmCancel = () => {
|
||||
<span :class="[ui.statusBadge, order.status === '注文中' && ui.pendingBadge]">
|
||||
{{ order.status }}
|
||||
</span>
|
||||
<button
|
||||
v-if="canCancel(order)"
|
||||
:class="ui.ghostButton"
|
||||
type="button"
|
||||
:disabled="Boolean(cancelingOrderKey)"
|
||||
@click="askCancel(order)"
|
||||
>
|
||||
<Spinner v-if="isCanceling(order, cancelingOrderKey)" size="sm" />
|
||||
<Ban v-else class="h-4 w-4" aria-hidden="true" />
|
||||
{{ isCanceling(order, cancelingOrderKey) ? '取消中' : '取消' }}
|
||||
</button>
|
||||
<span :class="ui.rowActions">
|
||||
<button
|
||||
v-if="canShowUsDetail(order)"
|
||||
:class="ui.ghostButton"
|
||||
class="min-h-8 px-3 text-xs"
|
||||
type="button"
|
||||
:disabled="Boolean(orderDetailLoadingKey)"
|
||||
@click="showOrderDetail(order)"
|
||||
>
|
||||
<Spinner v-if="orderDetailLoadingKey === orderActionKey(order)" size="sm" />
|
||||
<FileText v-else class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
詳細
|
||||
</button>
|
||||
<button
|
||||
v-if="canCorrect(order)"
|
||||
:class="ui.ghostButton"
|
||||
class="min-h-8 px-3 text-xs"
|
||||
type="button"
|
||||
:disabled="Boolean(cancelingOrderKey)"
|
||||
@click="askCorrection(order)"
|
||||
>
|
||||
<Pencil class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
訂正
|
||||
</button>
|
||||
<button
|
||||
v-if="canCancel(order)"
|
||||
:class="ui.ghostButton"
|
||||
class="min-h-8 px-3 text-xs"
|
||||
type="button"
|
||||
:disabled="Boolean(cancelingOrderKey)"
|
||||
@click="askCancel(order)"
|
||||
>
|
||||
<Spinner v-if="isCanceling(order, cancelingOrderKey)" size="sm" />
|
||||
<Ban v-else class="h-3.5 w-3.5" aria-hidden="true" />
|
||||
{{ isCanceling(order, cancelingOrderKey) ? '取消中' : '取消' }}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="dataLoading && !orders.length" :class="[ui.muted, 'grid py-8 place-items-center']">
|
||||
<Spinner />
|
||||
@@ -98,6 +264,195 @@ const confirmCancel = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<AnimatePresence>
|
||||
<UiModal
|
||||
v-if="orderDetail"
|
||||
key="order-detail-dialog"
|
||||
eyebrow="注文詳細"
|
||||
:title="orderDetail.stock"
|
||||
@close="orderDetail = null"
|
||||
>
|
||||
<dl :class="ui.confirmList">
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>注文番号</dt>
|
||||
<dd>{{ orderDetail.orderNumber ?? '-' }}</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>枝番</dt>
|
||||
<dd>{{ orderDetail.orderSubNo ?? '-' }}</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>銘柄</dt>
|
||||
<dd>{{ orderDetail.code }} / {{ orderDetail.market }}</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>売買</dt>
|
||||
<dd>{{ orderDetail.side === 'buy' ? '購入' : '売却' }}</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>数量</dt>
|
||||
<dd>{{ orderQuantityText(orderDetail) }}</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>未約定数量</dt>
|
||||
<dd>{{ orderDetail.unexecutedQuantity ?? '-' }}</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>価格</dt>
|
||||
<dd>{{ priceText(orderDetail.price, orderDetail.market) }}</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>状態</dt>
|
||||
<dd>{{ orderDetail.statusText ?? orderDetail.status }}</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>有効期限</dt>
|
||||
<dd>{{ orderDetail.expiresAt ?? '-' }}</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>取消/訂正</dt>
|
||||
<dd>
|
||||
{{ orderDetail.cancelable === false ? '取消不可' : '取消可' }} /
|
||||
{{ orderDetail.correctable === false ? '訂正不可' : '訂正可' }}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div :class="ui.actions">
|
||||
<UiButton variant="ghost" @click="orderDetail = null">
|
||||
<ArrowLeft class="h-4 w-4" aria-hidden="true" />
|
||||
閉じる
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiModal>
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
<UiModal
|
||||
v-if="showTradeRecords"
|
||||
key="trade-records-dialog"
|
||||
eyebrow="約定履歴"
|
||||
title="米国株"
|
||||
@close="showTradeRecords = false"
|
||||
>
|
||||
<div v-if="tradeRecords.length" :class="ui.confirmList">
|
||||
<div v-for="record in tradeRecords" :key="record.id" :class="ui.row">
|
||||
<span class="grid gap-1">
|
||||
<strong>{{ record.stock }}</strong>
|
||||
<small>{{ record.code }} / {{ record.market }} / {{ record.type }}</small>
|
||||
<small :class="ui.muted">
|
||||
{{ record.tradeDate ?? '-' }} 約定 / {{ record.valueDate ?? '-' }} 受渡
|
||||
</small>
|
||||
</span>
|
||||
<span class="grid justify-items-end gap-1 text-right">
|
||||
<strong>{{ tradeAmountText(record) }}</strong>
|
||||
<small :class="ui.muted">
|
||||
{{ record.quantity ?? '-' }}株 @ {{ priceText(record.price, record.market) }}
|
||||
</small>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else :class="ui.dialogNote">該当する約定履歴はありません</p>
|
||||
<div :class="ui.actions">
|
||||
<UiButton variant="ghost" @click="showTradeRecords = false">
|
||||
<ArrowLeft class="h-4 w-4" aria-hidden="true" />
|
||||
閉じる
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiModal>
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
<UiModal
|
||||
v-if="correctionCandidate"
|
||||
key="correction-dialog"
|
||||
eyebrow="注文訂正"
|
||||
:title="correctionCandidate.stock"
|
||||
@close="correctionCandidate = null"
|
||||
>
|
||||
<dl :class="ui.confirmList">
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>注文番号</dt>
|
||||
<dd>{{ correctionCandidate.orderNumber }}</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>現在数量</dt>
|
||||
<dd>{{ orderQuantityText(correctionCandidate) }}</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>現在価格</dt>
|
||||
<dd>{{ priceText(correctionCandidate.price, correctionCandidate.market) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div class="grid gap-3">
|
||||
<label :class="ui.label">
|
||||
数量
|
||||
<input
|
||||
v-model="correctionQuantityInput"
|
||||
:class="ui.input"
|
||||
inputmode="decimal"
|
||||
@input="correctionPreview = null"
|
||||
/>
|
||||
</label>
|
||||
<label :class="ui.label">
|
||||
執行条件
|
||||
<select
|
||||
v-model="correctionPriceCondition"
|
||||
:class="ui.input"
|
||||
@change="correctionPreview = null"
|
||||
>
|
||||
<option value="limit">指値</option>
|
||||
<option value="market">成行</option>
|
||||
</select>
|
||||
</label>
|
||||
<label v-if="correctionPriceCondition === 'limit'" :class="ui.label">
|
||||
指値
|
||||
<input
|
||||
v-model="correctionPriceInput"
|
||||
:class="ui.input"
|
||||
inputmode="decimal"
|
||||
@input="correctionPreview = null"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<dl v-if="correctionPreview" :class="ui.confirmList">
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>見積数量</dt>
|
||||
<dd>{{ correctionPreview.quantity }}株</dd>
|
||||
</div>
|
||||
<div :class="ui.confirmRow">
|
||||
<dt>見積金額</dt>
|
||||
<dd>
|
||||
{{
|
||||
correctionPreview.price
|
||||
? priceText(correctionPreview.price.value, correctionCandidate.market)
|
||||
: '-'
|
||||
}}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div :class="ui.actions">
|
||||
<UiButton variant="ghost" @click="correctionCandidate = null">
|
||||
<ArrowLeft class="h-4 w-4" aria-hidden="true" />
|
||||
戻る
|
||||
</UiButton>
|
||||
<UiButton :disabled="correctionLoading" @click="estimateCorrection">
|
||||
<Spinner v-if="correctionLoading" size="sm" />
|
||||
<ShieldCheck v-else class="h-4 w-4" aria-hidden="true" />
|
||||
見積
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="danger"
|
||||
:disabled="!correctionPreview || correctionSubmitting"
|
||||
@click="confirmCorrection"
|
||||
>
|
||||
<Spinner v-if="correctionSubmitting" size="sm" />
|
||||
<Pencil v-else class="h-4 w-4" aria-hidden="true" />
|
||||
訂正
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiModal>
|
||||
</AnimatePresence>
|
||||
|
||||
<AnimatePresence>
|
||||
<UiModal
|
||||
v-if="cancelCandidate"
|
||||
|
||||
Reference in New Issue
Block a user