feat: introduce better mcp

This commit is contained in:
Shotaro Nakamura
2026-06-21 14:09:05 +09:00
parent 17c5a1156e
commit 207f9c2164
19 changed files with 1862 additions and 254 deletions
+12
View File
@@ -105,6 +105,7 @@ const {
holdingsMarketValue,
totalProfitLoss,
totalProfitLossRate,
marketIndexes,
orders,
cancelingOrderKey,
orderHistoryLoaded,
@@ -146,6 +147,11 @@ const {
askPlaceOrder,
placeCashOrder,
cancelOrder,
loadOrderDetail,
loadTradeRecords,
loadPositionDetail,
estimateOrderCorrection,
placeOrderCorrection,
downloadCsv,
openTradeForStock,
openTradeForPosition,
@@ -232,6 +238,7 @@ onMounted(async () => {
:holdings-market-value="holdingsMarketValue"
:total-profit-loss="totalProfitLoss"
:total-profit-loss-rate="totalProfitLossRate"
:market-indexes="marketIndexes"
:stock-asset-ratio="stockAssetRatio"
:cash-asset-ratio="cashAssetRatio"
:positions="positions"
@@ -241,6 +248,7 @@ onMounted(async () => {
:connected="connected"
:order-history-loaded="orderHistoryLoaded"
:order-history-notice="orderHistoryNotice"
:load-position-detail="loadPositionDetail"
@connect="connect"
@open-position="
(code) =>
@@ -304,6 +312,10 @@ onMounted(async () => {
:canceling-order-key="cancelingOrderKey"
:order-history-loaded="orderHistoryLoaded"
:order-history-notice="orderHistoryNotice"
:load-order-detail="loadOrderDetail"
:load-trade-records="loadTradeRecords"
:estimate-order-correction="estimateOrderCorrection"
:place-order-correction="placeOrderCorrection"
@refresh="loadTradingData"
@cancel="cancelOrder"
/>
@@ -39,8 +39,10 @@ const rpcMethods = [
'market.ranking.sbi',
'news.list',
'watchlist.list',
'orders.inquiry.detail',
'orders.inquiry.executionsToday',
'orders.inquiry.open',
'orders.inquiry.tradeRecords',
'orders.cash.estimate',
'orders.cash.place',
'orders.cash.estimateCorrection',
+2 -2
View File
@@ -8,14 +8,14 @@ export type PageMeta = {
}
export const pageMeta: Record<RouteName, PageMeta> = {
portfolio: { title: 'ポートフォリオ', icon: WalletCards },
portfolio: { title: 'ホーム', icon: WalletCards },
trade: { title: '取引', icon: ArrowLeftRight },
history: { title: '取引履歴', icon: History },
settings: { title: '設定', icon: Settings },
}
export const sidebarItems: Array<{ name: RouteName; label: string; icon: Component }> = [
{ name: 'portfolio', label: '資産一覧', icon: pageMeta.portfolio.icon },
{ name: 'portfolio', label: 'ホーム', icon: pageMeta.portfolio.icon },
{ name: 'trade', label: '取引', icon: pageMeta.trade.icon },
{ name: 'settings', label: '設定', icon: pageMeta.settings.icon },
]
+12 -1
View File
@@ -34,12 +34,23 @@ export const cashOrderMarketOptions: Array<{
}> = [
{ label: '自動', value: 'auto' },
{ label: '東証', value: 'XTKS' },
{ label: '名証', value: 'XNGO' },
{ label: '福証', value: 'XFKA' },
{ label: '札証', value: 'XSAP' },
{ label: 'NASDAQ', value: 'XNAS' },
{ label: 'NYSE', value: 'XNYS' },
{ label: 'NYSE Arca', value: 'ARCX' },
]
export const searchableMarkets: CashOrderMarket[] = ['XTKS', 'XNAS', 'XNYS', 'ARCX']
export const searchableMarkets: CashOrderMarket[] = [
'XTKS',
'XNGO',
'XFKA',
'XSAP',
'XNAS',
'XNYS',
'ARCX',
]
export const tradeRouteIdFromStockId = (id: string) => {
const separator = id.indexOf(':')
@@ -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"
@@ -1,28 +1,30 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { AnimatePresence } from 'motion-v'
import { ArrowLeft, Ban, Plug } from 'lucide-vue-next'
import { ArrowLeft, Ban, FileText, Plug } 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, Position } from '../../types/trading'
import type { MarketIndex, OrderRow, Position } from '../../types/trading'
import {
currency,
currencyForMarket,
number as formatNumber,
signedCurrency,
signedCurrencyForMarket,
signedPercent,
} from '../../utils/format'
import { orderAmountText, orderHistoryKey, orderQuantityText } from '../trading/trading-data'
defineProps<{
const props = defineProps<{
showPortfolioSpinner: boolean
totalAssetValue: number
buyingPower: number
holdingsMarketValue: number
totalProfitLoss: number
totalProfitLossRate: number
marketIndexes: MarketIndex[]
stockAssetRatio: number
cashAssetRatio: number
positions: Position[]
@@ -32,6 +34,7 @@ defineProps<{
connected: boolean
orderHistoryLoaded: boolean
orderHistoryNotice: string
loadPositionDetail: (position: Position) => Promise<Position>
}>()
const emit = defineEmits<{
@@ -41,12 +44,47 @@ const emit = defineEmits<{
}>()
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 isCanceling = (order: OrderRow, cancelingOrderKey: string) =>
orderHistoryKey(order) === cancelingOrderKey
const cancelCandidate = ref<OrderRow | null>(null)
const positionDetail = ref<Position | null>(null)
const positionDetailLoadingKey = ref('')
const positionDetailError = ref('')
const cancelTitle = computed(() => cancelCandidate.value?.stock ?? '')
const indexValueText = (index: MarketIndex) =>
index.valueText || (index.value == null ? '-' : formatNumber(index.value))
const indexChangeText = (index: MarketIndex) => {
const change = index.changeText || (index.change == null ? '' : formatNumber(index.change))
const rate =
index.changeRateText || (index.changeRate == null ? '' : signedPercent(index.changeRate))
return [change, rate].filter(Boolean).join(' / ') || '-'
}
const indexTone = (index: MarketIndex) => {
if (index.sign === 'positive') return ui.positive
if (index.sign === 'negative') return ui.negative
return ui.muted
}
const positionKey = (position: Position) =>
[position.market, position.code, position.accountType].filter(Boolean).join(':')
const canLoadPositionDetail = (position: Position) => isUsMarket(position.market)
const showPositionDetail = async (position: Position) => {
if (!canLoadPositionDetail(position) || positionDetailLoadingKey.value) return
const key = positionKey(position)
positionDetailLoadingKey.value = key
positionDetailError.value = ''
try {
positionDetail.value = await props.loadPositionDetail(position)
} catch (cause) {
positionDetailError.value =
cause instanceof Error ? cause.message : '保有詳細の取得に失敗しました'
} finally {
positionDetailLoadingKey.value = ''
}
}
const askCancel = (order: OrderRow) => {
if (!canCancel(order)) return
@@ -69,6 +107,13 @@ const confirmCancel = () => {
<template v-if="!showPortfolioSpinner">{{ currency(totalAssetValue) }}</template>
<Spinner v-else size="lg" />
</strong>
<small :class="[ui.assetOverviewSubtext, totalProfitLoss >= 0 ? ui.positive : ui.negative]">
通算評価損益:
<template v-if="!showPortfolioSpinner">
{{ signedCurrency(totalProfitLoss) }} · {{ signedPercent(totalProfitLossRate) }}
</template>
<Spinner v-else size="sm" />
</small>
<small :class="ui.assetOverviewSubtext">
余力:
<template v-if="!showPortfolioSpinner">{{ currency(buyingPower) }}</template>
@@ -120,17 +165,6 @@ const confirmCancel = () => {
</div>
</article>
<article :class="ui.metricPanel">
<span :class="ui.metricLabel">通算評価損益</span>
<strong :class="[ui.metricValue, totalProfitLoss >= 0 ? ui.positive : ui.negative]">
<template v-if="!showPortfolioSpinner">
{{ signedCurrency(totalProfitLoss) }}
<small> · {{ signedPercent(totalProfitLossRate) }}</small>
</template>
<Spinner v-else size="lg" />
</strong>
</article>
<article :class="ui.holdingsPanel">
<div :class="ui.panelHead">
<h2>保有銘柄</h2>
@@ -151,19 +185,18 @@ const confirmCancel = () => {
<span>数量</span>
<span>評価額</span>
<span>評価損益</span>
<span>操作</span>
</div>
<div v-if="positions.length" :class="ui.holdingsRows">
<button
v-for="position in positions"
:key="position.code"
:class="ui.holdingRow"
type="button"
@click="emit('openPosition', position.code)"
>
<span class="grid gap-1">
<div v-for="position in positions" :key="positionKey(position)" :class="ui.holdingRow">
<button
class="grid gap-1 text-left text-[#e3e3e9]"
type="button"
@click="emit('openPosition', position.code)"
>
<strong>{{ position.name }}</strong>
<small>{{ position.code }}</small>
</span>
</button>
<b :class="ui.typePill">{{
position.type ?? (position.quantity >= 100 ? '単元' : 'S株')
}}</b>
@@ -178,12 +211,27 @@ const confirmCancel = () => {
<strong>{{ signedCurrencyForMarket(position.profitLoss, position.market) }}</strong>
<small>{{ signedPercent(position.profitLossRate) }}</small>
</span>
</button>
<span :class="ui.rowActions">
<button
v-if="canLoadPositionDetail(position)"
:class="ui.ghostButton"
class="min-h-8 px-3 text-xs"
type="button"
:disabled="Boolean(positionDetailLoadingKey)"
@click="showPositionDetail(position)"
>
<Spinner v-if="positionDetailLoadingKey === positionKey(position)" size="sm" />
<FileText v-else class="h-3.5 w-3.5" aria-hidden="true" />
詳細
</button>
</span>
</div>
</div>
<div v-else-if="dataLoading" :class="[ui.muted, ui.emptyState]">
<Spinner />
</div>
<p v-else :class="[ui.muted, ui.emptyState]">SBIに接続すると保有銘柄を表示します</p>
<p v-if="positionDetailError" :class="ui.dialogNote">{{ positionDetailError }}</p>
</div>
</article>
@@ -234,8 +282,85 @@ const confirmCancel = () => {
</p>
</div>
</article>
<article :class="ui.marketIndexPanel">
<div :class="ui.panelHead">
<h2>指数</h2>
</div>
<div v-if="marketIndexes.length" :class="ui.marketIndexGrid">
<div
v-for="index in marketIndexes"
:key="index.code ?? index.name"
:class="ui.marketIndexCard"
>
<span :class="ui.metricLabel">{{ index.name }}</span>
<strong :class="ui.marketIndexValue">{{ indexValueText(index) }}</strong>
<small :class="indexTone(index)">{{ indexChangeText(index) }}</small>
</div>
</div>
<div v-else-if="dataLoading" :class="[ui.muted, ui.emptyState]">
<Spinner />
</div>
<p v-else :class="[ui.muted, ui.emptyState]">SBIに接続すると指数を表示します</p>
</article>
</section>
<AnimatePresence>
<UiModal
v-if="positionDetail"
key="position-detail-dialog"
eyebrow="保有詳細"
:title="positionDetail.name"
@close="positionDetail = null"
>
<dl :class="ui.confirmList">
<div :class="ui.confirmRow">
<dt>銘柄</dt>
<dd>{{ positionDetail.code }} / {{ positionDetail.market }}</dd>
</div>
<div :class="ui.confirmRow">
<dt>預り区分</dt>
<dd>{{ positionDetail.type ?? positionDetail.accountType ?? '-' }}</dd>
</div>
<div :class="ui.confirmRow">
<dt>数量</dt>
<dd>{{ positionDetail.quantity }}</dd>
</div>
<div :class="ui.confirmRow">
<dt>現在値</dt>
<dd>
{{
positionDetail.currentPrice == null
? '-'
: currencyForMarket(positionDetail.currentPrice, positionDetail.market)
}}
</dd>
</div>
<div :class="ui.confirmRow">
<dt>平均取得単価</dt>
<dd>{{ currencyForMarket(positionDetail.avgPrice, positionDetail.market) }}</dd>
</div>
<div :class="ui.confirmRow">
<dt>評価額</dt>
<dd>{{ currencyForMarket(positionDetail.marketValue, positionDetail.market) }}</dd>
</div>
<div :class="ui.confirmRow">
<dt>評価損益</dt>
<dd :class="positionDetail.profitLoss >= 0 ? ui.positive : ui.negative">
{{ signedCurrencyForMarket(positionDetail.profitLoss, positionDetail.market) }} /
{{ signedPercent(positionDetail.profitLossRate) }}
</dd>
</div>
</dl>
<div :class="ui.actions">
<UiButton variant="ghost" @click="positionDetail = null">
<ArrowLeft class="h-4 w-4" aria-hidden="true" />
閉じる
</UiButton>
</div>
</UiModal>
</AnimatePresence>
<AnimatePresence>
<UiModal
v-if="cancelCandidate"
@@ -1,19 +1,29 @@
import type {
ChartNotice,
MarketIndex,
OrderDetail,
OrderPreview,
OrderRow,
Position,
RealtimePricePoint,
Stock,
TradeRecordRow,
} from '../../types/trading'
import { currency } from '../../utils/format'
export type RecordLike = Record<string, unknown>
type IssueLike = {
code: string
market: string
name: string
searchText?: string
}
export const emptyStock: Stock = {
code: '',
name: '未選択',
symbol: '',
searchText: '',
country: '日本',
market: '',
sector: '',
@@ -75,13 +85,27 @@ const countryFromMarket = (market: string) =>
export const issueFrom = (value: unknown) => {
const issue = asRecord(value)
const code = textValue(issue.code, textValue(issue.issueCode))
const market = textValue(issue.market, textValue(issue.marketCode, ''))
const name = textValue(
issue.name,
textValue(issue.issueName, textValue(issue.stockName, textValue(issue.displayName))),
)
const searchText = [
code,
market,
name,
textValue(issue.extract),
textValue(issue.extractWord),
textValue(issue.hitString),
]
.filter(Boolean)
.join(' ')
return {
code: textValue(issue.code, textValue(issue.issueCode)),
market: textValue(issue.market, textValue(issue.marketCode, '')),
name: textValue(
issue.name,
textValue(issue.issueName, textValue(issue.stockName, textValue(issue.displayName))),
),
code,
market,
name,
searchText,
}
}
@@ -112,10 +136,11 @@ const boxFromHistory = (history: number[]) => {
return { min: at(0), q1: at(0.25), median: at(0.5), q3: at(0.75), max: at(1) }
}
export const stockFromIssue = (issue: ReturnType<typeof issueFrom>): Stock => ({
export const stockFromIssue = (issue: IssueLike): Stock => ({
code: issue.code,
name: issue.name || issue.code,
symbol: issue.code ? issueSymbol(issue.code, issue.market) : '',
searchText: issue.searchText,
country: countryFromMarket(issue.market),
market: issue.market,
sector: '',
@@ -146,10 +171,16 @@ export const stockFromPosition = (position: Position): Stock => {
}
}
export const stockFromBoard = (
value: unknown,
fallbackIssue?: ReturnType<typeof issueFrom>,
): Stock => {
const accountTypeLabels: Record<string, string> = {
specific: '特定',
general: '一般',
growthInvestment: 'NISA成長投資枠',
nisa: 'NISA',
}
const accountTypeLabel = (value: string) => accountTypeLabels[value] ?? value
export const stockFromBoard = (value: unknown, fallbackIssue?: IssueLike): Stock => {
const board = asRecord(value)
const quoteRecord = asRecord(board.quote)
const boardIssue = issueFrom(board.issue)
@@ -158,6 +189,7 @@ export const stockFromBoard = (
code: quoteIssue.code || boardIssue.code || fallbackIssue?.code || '',
market: quoteIssue.market || boardIssue.market || fallbackIssue?.market || '',
name: quoteIssue.name || boardIssue.name || fallbackIssue?.name || '',
searchText: quoteIssue.searchText || boardIssue.searchText || fallbackIssue?.searchText || '',
}
const quote = quoteFrom(quoteRecord)
const codeAsNumber = Number(issue.code)
@@ -169,6 +201,7 @@ export const stockFromBoard = (
code: issue.code,
name: issue.name,
symbol: issue.code ? issueSymbol(issue.code, issue.market) : '',
searchText: issue.searchText,
country: countryFromMarket(issue.market),
market: issue.market,
sector: '',
@@ -186,6 +219,53 @@ export const stockFromBoard = (
}
}
const signedValueFrom = (value: unknown) => {
const record = asRecord(value)
const parsedValue = nullableNumberValue(record.value)
const text = textValue(record.text, parsedValue == null ? '' : String(parsedValue))
const rawSign = textValue(record.sign)
const sign: MarketIndex['sign'] =
rawSign === 'positive' || rawSign === 'negative' || rawSign === 'zero'
? rawSign
: parsedValue == null || parsedValue === 0
? 'zero'
: parsedValue > 0
? 'positive'
: 'negative'
return { value: parsedValue, text, sign }
}
const percentValueFrom = (value: unknown) => {
const record = asRecord(value)
const parsedValue = nullableNumberValue(record.value)
return {
value: parsedValue,
text: textValue(record.text, parsedValue == null ? '' : `${parsedValue}%`),
}
}
export const marketIndexFromApi = (value: unknown): MarketIndex | null => {
const index = asRecord(value)
const name = textValue(index.name)
if (!name) return null
const parsedValue = nullableNumberValue(index.value)
const change = signedValueFrom(index.change)
const changeRate = percentValueFrom(index.changeRate)
return {
code: textValue(index.code) || undefined,
name,
value: parsedValue,
valueText: textValue(index.valueText, parsedValue == null ? '' : String(parsedValue)),
change: change.value,
changeText: change.text,
changeRate: changeRate.value,
changeRateText: changeRate.text,
sign: change.sign,
timestamp: textValue(index.timestamp) || undefined,
}
}
const zonedPartsFormatter = (timeZone: string) =>
new Intl.DateTimeFormat('en-US', {
timeZone,
@@ -357,16 +437,20 @@ export const positionFromApi = (value: unknown): Position | null => {
const costBasis = avgPrice * quantity
const profitLossRate =
numberValue(item.profitLossRate) || (costBasis ? (profitLoss / costBasis) * 100 : 0)
const accountType = textValue(item.accountType, textValue(item.depositType))
return {
code: issue.code,
name: issue.name,
market: issue.market,
quantity,
avgPrice,
currentPrice: nullableNumberValue(item.currentPrice),
marketValue: numberValue(item.marketValue ?? item.valuationPrice),
profitLoss,
profitLossRate,
type: textValue(item.depositTypeText) || undefined,
type:
textValue(item.depositTypeText) || (accountType ? accountTypeLabel(accountType) : undefined),
accountType: accountType || undefined,
}
}
@@ -396,16 +480,59 @@ export const orderFromApi = (value: unknown): OrderRow | null => {
: (nullableNumberValue(item.price) ?? nullableNumberValue(item.executedPrice))
return {
id: textValue(item.id, textValue(item.orderNumber, `${issue.code}-${item.orderedAt ?? ''}`)),
code: issue.code,
date: textValue(item.orderedAt, textValue(item.expiresAt)),
stock: issue.name || issue.code,
market: issue.market,
side,
kind,
quantity,
unexecutedQuantity,
executedQuantity,
price,
status,
orderNumber: textValue(item.orderNumber),
orderSubNo: textValue(item.orderSubNo),
tradeId: textValue(item.tradeId),
accountType: textValue(item.accountType, textValue(item.depositType)),
cancelable: typeof item.cancelable === 'boolean' ? item.cancelable : undefined,
correctable: typeof item.correctable === 'boolean' ? item.correctable : undefined,
}
}
export const orderDetailFromApi = (value: unknown): OrderDetail | null => {
const row = orderFromApi(value)
if (!row) return null
const item = asRecord(value)
return {
...row,
expiresAt: textValue(item.expiresAt) || undefined,
statusText: textValue(item.statusText) || undefined,
depositType: textValue(item.depositType) || undefined,
accountInformation: textValue(item.accountInformation) || undefined,
}
}
export const tradeRecordFromApi = (value: unknown): TradeRecordRow | null => {
const item = asRecord(value)
const issue = issueFrom(item.issue)
if (!issue.code) return null
const tradeDate = textValue(item.tradeDate)
const valueDate = textValue(item.valueDate)
const type = textValue(item.tradeRecordTypeCode, textValue(item.type, '取引'))
return {
id: textValue(item.id, [issue.code, tradeDate, valueDate, type].filter(Boolean).join(':')),
code: issue.code,
stock: issue.name || issue.code,
market: issue.market,
type,
quantity: nullableNumberValue(item.quantity),
price: nullableNumberValue(item.price),
amount: nullableNumberValue(item.amount),
tradeDate: tradeDate || undefined,
valueDate: valueDate || undefined,
accountType: textValue(item.accountType) || undefined,
settlementCurrencyCode: textValue(item.settlementCurrencyCode) || undefined,
}
}
@@ -17,6 +17,8 @@ import type {
CashOrderTerm,
CashOrderTriggerZone,
JsonRpcResponse,
MarketIndex,
OrderDetail,
OrderKind,
OrderPreview,
OrderRow,
@@ -24,6 +26,7 @@ import type {
RealtimePricePoint,
RpcMessage,
Stock,
TradeRecordRow,
TradeSide,
} from '../../types/trading'
import {
@@ -34,7 +37,9 @@ import {
isOrderPreview,
issueFrom,
marketDateKey,
marketIndexFromApi,
numberValue,
orderDetailFromApi,
orderFromApi,
orderHistoryKey,
orderHistoryResultNotice,
@@ -44,6 +49,7 @@ import {
stockFromBoard,
stockFromIssue,
stockFromPosition,
tradeRecordFromApi,
textValue,
type RecordLike,
} from './trading-data'
@@ -157,6 +163,8 @@ const displayApkOrderTermDate = (value: string) => {
}
const usMarkets = new Set(['XNAS', 'XNYS', 'ARCX'])
const isUsMarket = (market: string) => usMarkets.has(market)
const usOrderMarkets = ['XNAS', 'XNYS', 'ARCX'] as const
const parseApkExchangeMarkets = (value: string) => {
const markets: CashOrderMarket[] = []
@@ -212,6 +220,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
const holdingsMarketValue = ref(0)
const totalProfitLoss = ref(0)
const totalProfitLossRate = ref(0)
const marketIndexes = ref<MarketIndex[]>([])
const orders = ref<OrderRow[]>([])
const cancelingOrderKey = ref('')
const orderHistoryLoaded = ref(false)
@@ -522,7 +531,8 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
const matchesQuery =
stock.name.toLowerCase().includes(query) ||
stock.code.includes(query) ||
stock.symbol.toLowerCase().includes(query)
stock.symbol.toLowerCase().includes(query) ||
stock.searchText?.toLowerCase().includes(query)
return matchesQuery && matchesFilters(stock)
})
: viewedStocks.value.filter(matchesFilters)
@@ -753,6 +763,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
price: stock.price || current?.price || 0,
change: stock.change || current?.change || 0,
changeAmount: stock.changeAmount || current?.changeAmount || 0,
searchText: stock.searchText || current?.searchText || '',
history: stock.price ? stock.history : (current?.history ?? stock.history),
box: stock.price ? stock.box : (current?.box ?? stock.box),
})
@@ -771,16 +782,19 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
const loadOrderHistoryFromSdk = async () => {
orderHistoryLoaded.value = false
orderHistoryNotice.value = ''
const [openOrdersResult, executionsTodayResult] = await Promise.allSettled([
const orderResults = await Promise.allSettled([
rpcCallOptional<RecordLike>('orders.inquiry.open'),
rpcCallOptional<RecordLike>('orders.inquiry.executionsToday'),
rpcCallOptional<RecordLike>('orders.inquiry.open', { market: 'XNAS' }),
rpcCallOptional<RecordLike>('orders.inquiry.executionsToday', { market: 'XNAS' }),
...usOrderMarkets.flatMap((market) => [
rpcCallOptional<RecordLike>('orders.inquiry.open', { market }),
rpcCallOptional<RecordLike>('orders.inquiry.executionsToday', { market }),
]),
])
const [openOrdersResult, executionsTodayResult] = orderResults
if (openOrdersResult.status === 'rejected' && executionsTodayResult.status === 'rejected') {
throw openOrdersResult.reason
}
const nextOrders = fulfilledValues([openOrdersResult, executionsTodayResult])
const nextOrders = fulfilledValues(orderResults)
.flatMap((orderList) => asArray(orderList.orders))
.map(orderFromApi)
.filter((order): order is OrderRow => Boolean(order))
@@ -790,9 +804,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
orders.value = [...deduped.values()]
orderHistoryLoaded.value = true
if (!orders.value.length) {
const notices = fulfilledValues([openOrdersResult, executionsTodayResult])
.map(orderHistoryResultNotice)
.filter(Boolean)
const notices = fulfilledValues(orderResults).map(orderHistoryResultNotice).filter(Boolean)
orderHistoryNotice.value = [...new Set(notices)].join(' / ')
}
}
@@ -822,11 +834,25 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
const loadTradingData = async () => {
dataLoading.value = true
try {
const [assetsResult, ...positionResults] = await Promise.allSettled([
const [assetsResult, indexResult, ...positionResults] = await Promise.allSettled([
rpcCallOptional<RecordLike>('account.assets.current', undefined, 20_000),
rpcCallOptional<unknown[]>('market.index.major', undefined, 15_000),
rpcCallOptional<RecordLike>('account.positions.cash', undefined, 15_000),
rpcCallOptional<RecordLike>('account.positions.cash', { market: 'XNAS' }, 15_000),
...usOrderMarkets.map((market) =>
rpcCallOptional<RecordLike>('account.positions.cash', { market }, 15_000),
),
])
if (indexResult.status === 'fulfilled') {
marketIndexes.value = asArray(indexResult.value)
.map(marketIndexFromApi)
.filter((index): index is MarketIndex => Boolean(index))
} else {
marketIndexes.value = []
reportDataError(
errorMessage(indexResult.reason, '指数の取得に失敗しました'),
indexResult.reason,
)
}
const cashPositionLists = fulfilledValues(positionResults)
const cashPositions = cashPositionLists[0] ?? {}
const nextPositions = cashPositionLists
@@ -835,13 +861,26 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
.filter((position): position is Position => Boolean(position))
positions.value = nextPositions
mergeStocks(nextPositions.map(stockFromPosition))
const nextHoldingsMarketValue = numberValue(
cashPositions.totalMarketValue,
nextPositions.reduce((sum, position) => sum + position.marketValue, 0),
const summedHoldingsMarketValue = nextPositions.reduce(
(sum, position) => sum + position.marketValue,
0,
)
const summedProfitLoss = nextPositions.reduce((sum, position) => sum + position.profitLoss, 0)
const summedCostBasis = nextPositions.reduce(
(sum, position) => sum + (position.marketValue - position.profitLoss),
0,
)
const nextHoldingsMarketValue =
nextPositions.length > 0
? summedHoldingsMarketValue
: numberValue(cashPositions.totalMarketValue)
holdingsMarketValue.value = nextHoldingsMarketValue
totalProfitLoss.value = numberValue(cashPositions.totalProfitLoss)
totalProfitLossRate.value = numberValue(cashPositions.totalProfitLossRate)
totalProfitLoss.value =
nextPositions.length > 0 ? summedProfitLoss : numberValue(cashPositions.totalProfitLoss)
totalProfitLossRate.value =
nextPositions.length > 0 && summedCostBasis
? (summedProfitLoss / summedCostBasis) * 100
: numberValue(cashPositions.totalProfitLossRate)
const hasAccountAssets = assetsResult.status === 'fulfilled'
if (hasAccountAssets) {
@@ -1011,7 +1050,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
}
}
const searchIssues = async (query: string) => {
const suggestIssues = async (query: string) => {
if (!connected.value || query.trim().length < 2) return
const marketsToSearch =
marketFilter.value !== 'all'
@@ -1019,7 +1058,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
: searchableMarkets.filter((market) => market !== 'auto')
const results = await Promise.allSettled(
marketsToSearch.map((market) =>
rpcCall<RecordLike>('market.issue.search', { query, market, limit: 12 }),
rpcCall<RecordLike>('market.issue.suggest', { query, market, limit: 12 }),
),
)
const issues = fulfilledValues(results)
@@ -1121,6 +1160,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
orders.value = [
{
id: textValue(receipt.orderId, `ord-${Date.now()}`),
code: selectedStock.value.code,
date: textValue(receipt.acceptedAt, new Date().toLocaleString('ja-JP')),
stock: selectedStock.value.name,
market: selectedStock.value.market,
@@ -1147,7 +1187,9 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
try {
const params = {
orderNumber: order.orderNumber,
orderId: order.id,
orderId: order.orderSubNo || order.id,
issueCode: order.code,
market: order.market,
tradeId: order.tradeId || undefined,
}
await rpcCall('orders.cash.placeCancel', {
@@ -1163,6 +1205,85 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
}
}
const loadOrderDetail = async (order: OrderRow): Promise<OrderDetail> => {
if (!isUsMarket(order.market)) {
throw new Error('注文詳細は米国株のみ対応しています')
}
if (!order.orderNumber && !order.id && !order.orderSubNo) {
throw new Error('注文番号を取得できないため詳細を取得できません')
}
const detail = await rpcCall<RecordLike>('orders.inquiry.detail', {
orderNumber: order.orderNumber,
orderId: order.orderSubNo || order.id,
issueCode: order.code,
market: order.market,
})
const parsed = orderDetailFromApi(detail)
if (!parsed) throw new Error('注文詳細を読み取れませんでした')
return parsed
}
const loadTradeRecords = async (): Promise<TradeRecordRow[]> => {
const result = await rpcCall<RecordLike>('orders.inquiry.tradeRecords', { limit: 50 })
return asArray(result.records)
.map(tradeRecordFromApi)
.filter((record): record is TradeRecordRow => Boolean(record))
}
const loadPositionDetail = async (position: Position): Promise<Position> => {
if (!isUsMarket(position.market)) {
throw new Error('保有詳細は米国株のみ対応しています')
}
const detail = await rpcCall<RecordLike>('account.positions.cashDetail', {
issueCode: position.code,
market: position.market,
accountType: position.accountType,
limit: 1,
})
const parsed = asArray(detail.positions).map(positionFromApi)[0]
if (!parsed) throw new Error('保有詳細を読み取れませんでした')
return parsed
}
const orderCorrectionParams = (
order: OrderRow,
draft: { quantity: number; priceCondition: 'market' | 'limit'; price?: number },
) => ({
orderNumber: order.orderNumber,
orderId: order.orderSubNo || order.id,
issueCode: order.code,
market: order.market,
quantity: draft.quantity,
priceCondition: draft.priceCondition,
price: draft.priceCondition === 'limit' ? draft.price : undefined,
orderMethod: 'normal',
})
const estimateOrderCorrection = async (
order: OrderRow,
draft: { quantity: number; priceCondition: 'market' | 'limit'; price?: number },
): Promise<OrderPreview> => {
if (!isUsMarket(order.market)) throw new Error('注文訂正は米国株のみ対応しています')
const preview = await rpcCall<unknown>(
'orders.cash.estimateCorrection',
orderCorrectionParams(order, draft),
)
if (!isOrderPreview(preview)) throw new Error('注文訂正の見積を読み取れませんでした')
return preview
}
const placeOrderCorrection = async (
order: OrderRow,
draft: { quantity: number; priceCondition: 'market' | 'limit'; price?: number },
) => {
if (!isUsMarket(order.market)) throw new Error('注文訂正は米国株のみ対応しています')
await rpcCall('orders.cash.placeCorrection', {
...orderCorrectionParams(order, draft),
allowTrading: true,
})
await loadTradingData()
}
const downloadCsv = () => {
const header = ['code', 'name', 'symbol', 'market', 'price'].join(',')
const rows = selectedStock.value.history.map((price) =>
@@ -1284,7 +1405,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
searchTimer = setTimeout(async () => {
const requestId = ++searchRequestId
try {
await searchIssues(query)
await suggestIssues(query)
} catch (cause) {
reportDataError(errorMessage(cause, '銘柄検索に失敗しました'), cause)
} finally {
@@ -1337,6 +1458,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
holdingsMarketValue,
totalProfitLoss,
totalProfitLossRate,
marketIndexes,
orders,
cancelingOrderKey,
orderHistoryLoaded,
@@ -1379,6 +1501,11 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
askPlaceOrder,
placeCashOrder,
cancelOrder,
loadOrderDetail,
loadTradeRecords,
loadPositionDetail,
estimateOrderCorrection,
placeOrderCorrection,
downloadCsv,
openTradeForStock,
openTradeForPosition,
+14 -9
View File
@@ -24,12 +24,12 @@ export const ui = {
panelHead: 'flex flex-wrap items-center justify-between gap-3',
eyebrow: 'mb-1 text-xs font-black uppercase text-[#9aa0a9]',
dashboardGrid:
'grid min-h-0 flex-1 grid-cols-1 items-stretch gap-5 lg:grid-cols-[minmax(0,1.25fr)_minmax(20rem,0.75fr)] lg:grid-rows-[auto_minmax(0,1fr)] lg:gap-6 lg:overflow-hidden',
'grid min-h-0 flex-1 grid-cols-1 items-stretch gap-5 lg:grid-cols-[minmax(0,1.25fr)_minmax(20rem,0.75fr)] lg:grid-rows-[auto_minmax(20rem,auto)_auto] lg:gap-6',
metricPanel:
'grid min-h-36 content-center gap-3 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 shadow-lg shadow-black/15 sm:min-h-40 sm:p-7',
assetOverviewPanel:
'grid min-h-40 grid-cols-1 items-center gap-x-7 gap-y-5 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 shadow-lg shadow-black/15 sm:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] sm:p-7',
assetOverviewHead: 'grid gap-2',
'grid min-h-48 grid-cols-1 items-center gap-x-7 gap-y-5 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 pb-8 shadow-lg shadow-black/15 sm:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)] sm:p-7 sm:pb-10 lg:col-span-2',
assetOverviewHead: 'grid gap-3',
assetOverviewSubtext: 'text-sm font-semibold text-[#c3c7cf]',
assetBreakdownPanel: 'grid gap-3',
assetBreakdownTitle: 'text-xs font-extrabold text-[#9aa0a9]',
@@ -52,19 +52,24 @@ export const ui = {
assetBreakdownAmount: 'text-sm font-black text-[#e3e3e9]',
assetBreakdownRatio: 'text-xs font-semibold text-[#8f949d]',
holdingsPanel:
'flex min-h-0 flex-col gap-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 shadow-lg shadow-black/15 sm:p-7',
'flex min-h-80 flex-col gap-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 shadow-lg shadow-black/15 sm:p-7',
holdingsBody: 'flex min-h-0 flex-1 flex-col overflow-visible lg:overflow-hidden',
holdingsRows: 'grid min-h-0 flex-1 content-start overflow-y-auto',
holdingsHead:
'hidden grid-cols-[1.7fr_0.8fr_0.7fr_1fr_1fr] items-center gap-4 border-b border-[#33383f] py-3 text-xs font-extrabold text-[#8f949d] md:grid',
'hidden grid-cols-[1.6fr_0.7fr_0.6fr_0.9fr_0.9fr_auto] items-center gap-4 border-b border-[#33383f] py-3 text-xs font-extrabold text-[#8f949d] md:grid',
holdingRow:
'grid min-h-16 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-2xl bg-[#111418] px-3 py-3 text-left text-[#e3e3e9] transition hover:bg-[#242930] md:grid-cols-[1.7fr_0.8fr_0.7fr_1fr_1fr] md:gap-4 md:bg-transparent',
'grid min-h-16 grid-cols-[minmax(0,1fr)_auto] items-center gap-3 rounded-2xl bg-[#111418] px-3 py-3 text-left text-[#e3e3e9] transition hover:bg-[#242930] md:grid-cols-[1.6fr_0.7fr_0.6fr_0.9fr_0.9fr_auto] md:gap-4 md:bg-transparent',
typePill: 'w-fit rounded-full bg-[#263141] px-3 py-1 text-xs text-[#d3e3fd]',
muted: 'text-[#8f949d]',
portfolioHistory:
'flex min-h-0 flex-col gap-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 shadow-lg shadow-black/15 sm:p-7',
'flex min-h-80 flex-col gap-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 shadow-lg shadow-black/15 sm:p-7',
historyList: 'flex min-h-0 flex-1 flex-col overflow-visible lg:overflow-hidden',
historyRows: 'grid min-h-0 flex-1 content-start gap-5 overflow-y-auto lg:gap-7',
marketIndexPanel:
'grid gap-4 rounded-[28px] border border-[#30343a] bg-[#1b1f24] p-5 shadow-lg shadow-black/15 sm:p-7 lg:col-span-2',
marketIndexGrid: 'grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4',
marketIndexCard: 'grid min-h-28 content-center gap-2 rounded-2xl bg-[#111418] p-4',
marketIndexValue: 'break-words text-xl font-black text-[#e3e3e9]',
emptyState: 'flex flex-1 items-center justify-center py-8 text-center',
miniOrder: 'grid grid-cols-[minmax(0,1fr)_auto] gap-3',
tradeLayout:
@@ -153,13 +158,13 @@ export const ui = {
'inline-flex min-h-10 items-center justify-center gap-2 rounded-full border border-[#4a5058] bg-transparent px-5 font-extrabold text-[#d3e3fd] transition hover:bg-[#263141]',
list: 'grid gap-2',
orderRow:
'grid grid-cols-2 items-center gap-3 rounded-[20px] bg-[#111418] p-4 md:grid-cols-[minmax(180px,1fr)_auto_auto_auto_auto_auto]',
'grid grid-cols-2 items-center gap-3 rounded-[20px] bg-[#111418] p-4 md:grid-cols-[minmax(180px,1fr)_auto_auto_auto_auto_minmax(12rem,auto)]',
statusBadge: 'w-fit rounded-full bg-[#263141] px-3 py-1 text-xs font-black text-[#d3e3fd]',
pendingBadge: 'bg-[#4a3720] text-[#ffddb3]',
apiLayout: 'grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_minmax(20rem,0.9fr)] lg:gap-7',
settingsLayout: 'grid gap-7',
row: 'grid grid-cols-1 items-center gap-3 rounded-[20px] bg-[#111418] p-4 sm:grid-cols-[minmax(0,1fr)_auto]',
rowActions: 'flex gap-2',
rowActions: 'flex flex-wrap items-center justify-end gap-2',
keyRow: 'grid gap-3 rounded-[24px] bg-[#111418] p-4',
profileRow:
'grid grid-cols-[auto_minmax(0,1fr)] items-center gap-3 rounded-[20px] bg-[#111418] p-4 sm:grid-cols-[auto_minmax(0,1fr)_auto]',
+60 -1
View File
@@ -1,7 +1,16 @@
export type TradeSide = 'buy' | 'sell'
export type OrderKind = 'standard' | 's'
export type CashOrderAccountType = 'specific' | 'general' | 'growthInvestment' | 'nisa'
export type CashOrderMarket = 'auto' | 'XTKS' | 'XNAS' | 'XNYS' | 'ARCX' | 'STK'
export type CashOrderMarket =
| 'auto'
| 'XTKS'
| 'XNGO'
| 'XFKA'
| 'XSAP'
| 'XNAS'
| 'XNYS'
| 'ARCX'
| 'STK'
export type CashOrderPriceCondition =
| 'limit'
| 'limitAtOpen'
@@ -31,6 +40,11 @@ export type OrderPreview = {
}
side: string
quantity: number
price?: {
value: number | null
text: string
currency?: string
}
warnings: string[]
confirmationId?: string
message?: string
@@ -61,10 +75,24 @@ export type ChartNotice = {
detail?: string
}
export type MarketIndex = {
code?: string
name: string
value: number | null
valueText: string
change: number | null
changeText: string
changeRate: number | null
changeRateText: string
sign: 'positive' | 'negative' | 'zero'
timestamp?: string
}
export type Stock = {
code: string
name: string
symbol: string
searchText?: string
country: string
market: string
sector: string
@@ -89,16 +117,45 @@ export type Stock = {
export type OrderRow = {
id: string
code: string
date: string
stock: string
market: string
side: TradeSide
kind: OrderKind
quantity: number | null
unexecutedQuantity?: number | null
executedQuantity?: number | null
price: number | null
status: '注文中' | '約定済' | '取消済'
orderNumber?: string
orderSubNo?: string
tradeId?: string
accountType?: string
cancelable?: boolean
correctable?: boolean
}
export type OrderDetail = OrderRow & {
expiresAt?: string
statusText?: string
depositType?: string
accountInformation?: string
}
export type TradeRecordRow = {
id: string
code: string
stock: string
market: string
type: string
quantity: number | null
price: number | null
amount: number | null
tradeDate?: string
valueDate?: string
accountType?: string
settlementCurrencyCode?: string
}
export type Position = {
@@ -107,8 +164,10 @@ export type Position = {
market: string
quantity: number
avgPrice: number
currentPrice?: number | null
marketValue: number
profitLoss: number
profitLossRate: number
type?: string
accountType?: CashOrderAccountType | string
}