From 207f9c2164de6fc0b9fb9cbacc27882fecc5e74c Mon Sep 17 00:00:00 2001
From: Shotaro Nakamura
Date: Sun, 21 Jun 2026 14:09:05 +0900
Subject: [PATCH] feat: introduce better mcp
---
apps/csbie-server/src/routes/mcp.ts | 308 +++++---
apps/csbie-server/src/rpc/methods.ts | 2 +
apps/csbie-ui/src/App.vue | 12 +
.../src/components/ApiKeyPolicyEditor.vue | 2 +
apps/csbie-ui/src/constants/nav.ts | 4 +-
apps/csbie-ui/src/constants/trade.ts | 13 +-
.../src/features/history/HistoryView.vue | 405 ++++++++++-
.../src/features/portfolio/PortfolioView.vue | 175 ++++-
.../src/features/trading/trading-data.ts | 151 +++-
.../src/features/trading/useTradingSession.ts | 165 ++++-
apps/csbie-ui/src/styles/ui.ts | 23 +-
apps/csbie-ui/src/types/trading.ts | 61 +-
packages/sbi-client/src/index.ts | 2 +
packages/sbi-client/src/markets.ts | 3 +
packages/sbi-client/src/methods/index.ts | 48 +-
packages/sbi-client/src/methods/types.ts | 26 +
packages/sbi-client/src/methods/us-stock.ts | 670 +++++++++++++++++-
packages/sbi-client/src/types.ts | 28 +-
scripts/check-env.ts | 18 +-
19 files changed, 1862 insertions(+), 254 deletions(-)
diff --git a/apps/csbie-server/src/routes/mcp.ts b/apps/csbie-server/src/routes/mcp.ts
index 34d4c3d..b3e898b 100644
--- a/apps/csbie-server/src/routes/mcp.ts
+++ b/apps/csbie-server/src/routes/mcp.ts
@@ -7,13 +7,7 @@ import { eq } from 'drizzle-orm'
import * as z from 'zod/v4'
import type { AppBindings, AuthContext } from '../context'
import { sbiPasskeys } from '../db/schema'
-import {
- invokeSbiMethod,
- isCashOrderMethod,
- isTradingMethod,
- RPC_METHODS,
- type RpcMethod,
-} from '../rpc/methods'
+import { invokeSbiMethod, isCashOrderMethod, isTradingMethod, type RpcMethod } from '../rpc/methods'
import { connectSbi } from '../rpc/sbi-session'
import type { StoredSbiPasskeySecret } from './admin'
import {
@@ -33,8 +27,6 @@ const requireAuthenticated = (auth: AuthContext) => {
if (!auth.authenticated) throw new Error('unauthorized')
}
-const toolNameForMethod = (method: RpcMethod) => `csbie_sbi_${method.replaceAll('.', '_')}`
-
const ORDER_SUBMIT_TICKET_TTL_MS = 10 * 60 * 1000
type OrderSubmitTicket = {
@@ -81,10 +73,6 @@ const orderSubmitMethodByEstimateMethod = {
const submitMethodForEstimateMethod = (method: RpcMethod) =>
orderSubmitMethodByEstimateMethod[method as keyof typeof orderSubmitMethodByEstimateMethod]
-const isDirectOrderSubmitMethod = (method: RpcMethod) => isTradingMethod(method)
-
-const mcpExposedRpcMethods = RPC_METHODS.filter((method) => !isDirectOrderSubmitMethod(method))
-
const orderSubmitParams = (value: unknown, confirmationId?: string) => {
if (!value || typeof value !== 'object' || Array.isArray(value)) return { allowTrading: true }
return {
@@ -179,6 +167,17 @@ const orderInquiryOptionsSchema = z.object({
status: z.enum(['open', 'executed', 'cancelled', 'expired', 'rejected', 'unknown']).optional(),
})
+const orderDetailSchema = z.object({
+ orderNumber: z.string().min(1).optional().describe('Order number shown in order inquiry'),
+ orderId: orderIdSchema.optional().describe('Order id shown in order inquiry'),
+ issueCode: issueCodeSchema.optional(),
+ market: marketCodeSchema.describe('US stock market code for order detail'),
+})
+
+const tradeRecordInquiryOptionsSchema = orderInquiryOptionsSchema.extend({
+ accountType: accountTypeSchema.optional(),
+})
+
const boardOptionsSchema = issueOptionsSchema.extend({
accountType: accountTypeSchema.optional(),
side: z
@@ -337,6 +336,8 @@ const placeOrderCorrectionSchema = orderCorrectionSchema.extend({
const orderCancelSchema = z.object({
orderNumber: z.string().min(1).describe('Order number shown in order inquiry'),
orderId: orderIdSchema.optional().describe('Original order id shown in order inquiry'),
+ issueCode: issueCodeSchema.optional().describe('Original issue code shown in order inquiry'),
+ market: marketCodeSchema.optional().describe('Original market code shown in order inquiry'),
tradeId: z.string().min(1).optional().describe('Original trade id code'),
cancelType: z.string().min(1).optional().describe('Additional cancel flag'),
})
@@ -628,8 +629,10 @@ const methodParamSchemas = {
'market.ranking.sbi': undefined,
'news.list': undefined,
'watchlist.list': undefined,
+ 'orders.inquiry.detail': orderDetailSchema,
'orders.inquiry.executionsToday': orderInquiryOptionsSchema.optional(),
'orders.inquiry.open': orderInquiryOptionsSchema.optional(),
+ 'orders.inquiry.tradeRecords': tradeRecordInquiryOptionsSchema,
'orders.cash.preOrder': cashOrderPreOrderSchema,
'orders.cash.estimate': cashOrderSchema,
'orders.cash.place': placeCashOrderSchema,
@@ -665,6 +668,84 @@ const methodParamSchemas = {
'orders.exchange.place': placeExchangeOrderSchema,
} satisfies Record
+const getActionToMethod = {
+ sessionProfile: 'session.profile',
+ accountProfile: 'account.profile',
+ currentAssets: 'account.assets.current',
+ buyingPower: 'account.power.buyingPower',
+ collateralRatio: 'account.power.collateralRatio',
+ cashPositions: 'account.positions.cash',
+ cashPositionDetail: 'account.positions.cashDetail',
+ cashPositionForIssue: 'account.positions.cashForIssue',
+ marginPositions: 'account.positions.margin',
+ marginPositionDetail: 'account.positions.marginDetail',
+ marginPositionForIssue: 'account.positions.marginForIssue',
+ marginSummaryForIssue: 'account.positions.marginSummaryForIssue',
+ marginDetailsForIssue: 'account.positions.marginDetailsForIssue',
+ closeableMarginPositions: 'account.positions.closeableMargin',
+ deliverableMarginPositions: 'account.positions.deliverableMargin',
+ unrealizedProfitLoss: 'account.profitLoss.unrealized',
+ issueSearch: 'market.issue.search',
+ issueSuggest: 'market.issue.suggest',
+ allowedPrices: 'market.issue.allowedPrices',
+ board: 'market.issue.board',
+ chart: 'market.issue.chart',
+ openOrdersForIssue: 'market.issue.openOrders',
+ tradingInfo: 'market.issue.tradingInfo',
+ majorIndexes: 'market.index.major',
+ marketOverview: 'market.overview',
+ marketRanking: 'market.ranking.market',
+ sectorRanking: 'market.ranking.sector',
+ sbiRanking: 'market.ranking.sbi',
+ news: 'news.list',
+ watchlist: 'watchlist.list',
+ orderDetail: 'orders.inquiry.detail',
+ executionsToday: 'orders.inquiry.executionsToday',
+ openOrders: 'orders.inquiry.open',
+ tradeRecords: 'orders.inquiry.tradeRecords',
+ cashOrderPreflight: 'orders.cash.preOrder',
+ marginOpenPreflight: 'orders.margin.preOrderOpen',
+ marginClosePreflight: 'orders.margin.preOrderClose',
+ actualDeliveryPreflight: 'orders.margin.preOrderActualDelivery',
+ themeInvestmentList: 'orders.themeInvestment.list',
+ exchangeRate: 'orders.exchange.rate',
+} as const satisfies Record
+
+const changeActionToEstimateMethod = {
+ cashOrder: 'orders.cash.estimate',
+ cashOrderCorrection: 'orders.cash.estimateCorrection',
+ cashOrderCorrectionConfirm: 'orders.cash.estimateCorrectionConfirm',
+ cashOrderCancel: 'orders.cash.estimateCancel',
+ marginOpenOrder: 'orders.margin.estimateOpen',
+ marginCloseOrder: 'orders.margin.estimateClose',
+ marginCloseSummaryOrder: 'orders.margin.estimateSummary',
+ marginCloseSummaryConfirm: 'orders.margin.estimateCloseSummary',
+ actualDeliveryOrder: 'orders.margin.estimateActualDelivery',
+ ifdOrder: 'orders.ifd.estimate',
+ ifdOrderCorrection: 'orders.ifd.estimateCorrection',
+ ifdOrderCancel: 'orders.ifd.estimateCancel',
+ themeInvestmentOrder: 'orders.themeInvestment.estimate',
+ exchangeOrder: 'orders.exchange.estimate',
+} as const satisfies Record
+
+const getActions = Object.keys(getActionToMethod) as Array
+const changeActions = Object.keys(changeActionToEstimateMethod) as Array<
+ keyof typeof changeActionToEstimateMethod
+>
+const getActionSchema = z.enum(['capabilities', 'passkeys', ...getActions])
+const changeActionSchema = z.enum(changeActions)
+
+const parseMethodParams = (method: RpcMethod, input: unknown) => {
+ const schema = methodParamSchemas[method]
+ if (!schema) {
+ if (input !== undefined && input !== null) {
+ throw new Error(`${method} does not accept input`)
+ }
+ return undefined
+ }
+ return schema.parse(input)
+}
+
const createMcpServer = (c: Context) => {
const db = c.get('db')
const config = c.get('config')
@@ -675,59 +756,33 @@ const createMcpServer = (c: Context) => {
version: '0.1.0',
})
- server.registerTool(
- 'csbie_sbi_methods',
- {
- title: 'List SBI RPC Methods',
- description: 'List SBI client methods exposed through CSBIE.',
- inputSchema: {},
- },
- async () => {
- requireAuthenticated(auth)
- return textResult({
- methods: mcpExposedRpcMethods,
- submitTool: 'csbie_sbi_submit_order',
+ const listPasskeys = async () => {
+ const rows = await db
+ .select({
+ id: sbiPasskeys.id,
+ label: sbiPasskeys.label,
+ keyringAccount: sbiPasskeys.keyringAccount,
+ createdAt: sbiPasskeys.createdAt,
+ updatedAt: sbiPasskeys.updatedAt,
})
- },
- )
+ .from(sbiPasskeys)
+ .orderBy(sbiPasskeys.createdAt)
+ return Promise.all(
+ rows.map(async ({ keyringAccount, ...row }) => {
+ const secret = await readSecret(keyringAccount)
+ const hasDeviceId = Boolean(effectiveSbiDeviceId(secret))
+ const hasTradePassword = Boolean(effectiveSbiTradePassword(secret))
+ return {
+ ...row,
+ hasTradePassword,
+ hasDeviceId,
+ cashOrderReady: hasTradePassword && hasDeviceId,
+ }
+ }),
+ )
+ }
- server.registerTool(
- 'csbie_sbi_passkeys',
- {
- title: 'List SBI Passkeys',
- description: 'List saved SBI passkey profiles. Secret material is never returned.',
- inputSchema: {},
- },
- async () => {
- requireAuthenticated(auth)
- const rows = await db
- .select({
- id: sbiPasskeys.id,
- label: sbiPasskeys.label,
- keyringAccount: sbiPasskeys.keyringAccount,
- createdAt: sbiPasskeys.createdAt,
- updatedAt: sbiPasskeys.updatedAt,
- })
- .from(sbiPasskeys)
- .orderBy(sbiPasskeys.createdAt)
- const passkeys = await Promise.all(
- rows.map(async ({ keyringAccount, ...row }) => {
- const secret = await readSecret(keyringAccount)
- const hasDeviceId = Boolean(effectiveSbiDeviceId(secret))
- const hasTradePassword = Boolean(effectiveSbiTradePassword(secret))
- return {
- ...row,
- hasTradePassword,
- hasDeviceId,
- cashOrderReady: hasTradePassword && hasDeviceId,
- }
- }),
- )
- return textResult({ passkeys })
- },
- )
-
- const callSbiMethod = async (method: RpcMethod, passkeyId: string, params: unknown) => {
+ const invokeCheckedSbiMethod = async (method: RpcMethod, passkeyId: string, params: unknown) => {
requireAuthenticated(auth)
if (auth.type === 'apiKey') {
@@ -767,41 +822,105 @@ const createMcpServer = (c: Context) => {
}
const client = await connectSbi(db, config, passkeyId)
- const result = await invokeSbiMethod(client, method, params)
- const submitMethod = submitMethodForEstimateMethod(method)
- if (!submitMethod) return textResult(result)
+ return invokeSbiMethod(client, method, params)
+ }
+
+ const createChangeRequest = async (
+ action: keyof typeof changeActionToEstimateMethod,
+ passkeyId: string,
+ input: unknown,
+ ) => {
+ const estimateMethod = changeActionToEstimateMethod[action]
+ const submitMethod = submitMethodForEstimateMethod(estimateMethod)
+ if (!submitMethod) throw new Error(`${action} cannot create a confirmable request`)
+
+ const params = parseMethodParams(estimateMethod, input)
+ const preview = await invokeCheckedSbiMethod(estimateMethod, passkeyId, params)
cleanupExpiredOrderSubmitTickets()
const uuid = randomUUID()
const expiresAt = new Date(Date.now() + ORDER_SUBMIT_TICKET_TTL_MS)
- const confirmationId = confirmationIdFromPreview(result)
+ const confirmationId = confirmationIdFromPreview(preview)
orderSubmitTickets.set(uuid, {
passkeyId,
- estimateMethod: method,
+ estimateMethod,
submitMethod,
params,
confirmationId,
authKey: authKey(auth),
expiresAt,
})
- return textResult({
- preview: result,
- submit: {
- uuid,
- tool: 'csbie_sbi_submit_order',
- expiresAt: expiresAt.toISOString(),
- },
- })
+
+ return {
+ uuid,
+ expiresAt: expiresAt.toISOString(),
+ preview,
+ confirmTool: 'confirm-request',
+ }
}
server.registerTool(
- 'csbie_sbi_submit_order',
+ 'csbie-get',
{
- title: 'Submit Estimated SBI Order',
+ title: 'Get SBI Data',
description:
- 'Submit the same SBI order as a previous MCP estimate result by UUID. The UUID expires shortly and is bound to the same authenticated caller.',
+ 'Read SBI data through a small abstract action API. This tool never places, corrects, cancels, or otherwise changes real orders.',
inputSchema: {
- uuid: z.string().uuid().describe('UUID returned by an order estimate tool'),
+ action: getActionSchema.describe('Read action to perform'),
+ passkeyId: z
+ .string()
+ .optional()
+ .describe('Saved SBI passkey id. Required for SBI-backed read actions.'),
+ input: z.unknown().optional().describe('Action input object'),
+ },
+ },
+ async ({ action, passkeyId, input }) => {
+ requireAuthenticated(auth)
+
+ if (action === 'capabilities') {
+ return textResult({
+ getActions,
+ changeActions,
+ changeTool: 'csbie-request-change',
+ confirmTool: 'confirm-request',
+ })
+ }
+
+ if (action === 'passkeys') return textResult({ passkeys: await listPasskeys() })
+
+ const method = getActionToMethod[action]
+ if (!method) throw new Error(`unsupported get action: ${String(action)}`)
+ if (!passkeyId) throw new Error(`${action} requires passkeyId`)
+
+ const params = parseMethodParams(method, input)
+ return textResult(await invokeCheckedSbiMethod(method, passkeyId, params))
+ },
+ )
+
+ server.registerTool(
+ 'csbie-request-change',
+ {
+ title: 'Create SBI Change Request',
+ description:
+ 'Prepare a real SBI change by running the corresponding estimate/preview and returning a UUID. This tool never submits the change; pass the UUID to confirm-request.',
+ inputSchema: {
+ action: changeActionSchema.describe('Change action to prepare'),
+ passkeyId: z.string().describe('Saved SBI passkey id'),
+ input: z.unknown().describe('Change action input object'),
+ },
+ },
+ async ({ action, passkeyId, input }) =>
+ textResult(await createChangeRequest(action, passkeyId, input)),
+ )
+
+ server.registerTool(
+ 'confirm-request',
+ {
+ title: 'Confirm SBI Change Request',
+ description:
+ 'Submit a previously prepared SBI change request by UUID. The UUID expires shortly, is single-use, and is bound to the same authenticated caller.',
+ inputSchema: {
+ uuid: z.string().uuid().describe('UUID returned by csbie-request-change'),
},
},
async ({ uuid }) => {
@@ -809,40 +928,21 @@ const createMcpServer = (c: Context) => {
cleanupExpiredOrderSubmitTickets()
const ticket = orderSubmitTickets.get(uuid)
- if (!ticket) throw new Error('order submit uuid not found or expired')
+ if (!ticket) throw new Error('request uuid not found or expired')
if (ticket.authKey !== authKey(auth)) {
- throw new Error('order submit uuid was created by a different authenticated caller')
+ throw new Error('request uuid was created by a different authenticated caller')
}
orderSubmitTickets.delete(uuid)
- return callSbiMethod(
+ const result = await invokeCheckedSbiMethod(
ticket.submitMethod,
ticket.passkeyId,
orderSubmitParams(ticket.params, ticket.confirmationId),
)
+ return textResult(result)
},
)
- for (const method of mcpExposedRpcMethods) {
- const paramsSchema = methodParamSchemas[method]
- server.registerTool(
- toolNameForMethod(method),
- {
- title: `Call ${method}`,
- description: `Connect with one saved SBI passkey and call ${method}. API key method permissions and trading limits are enforced.`,
- inputSchema: {
- passkeyId: z.string().describe('Saved SBI passkey id from csbie_sbi_passkeys'),
- ...(paramsSchema
- ? {
- params: paramsSchema.describe(`${method} params`),
- }
- : {}),
- },
- },
- async ({ passkeyId, params }) => callSbiMethod(method, passkeyId, params),
- )
- }
-
return server
}
diff --git a/apps/csbie-server/src/rpc/methods.ts b/apps/csbie-server/src/rpc/methods.ts
index 6b1c539..1333822 100644
--- a/apps/csbie-server/src/rpc/methods.ts
+++ b/apps/csbie-server/src/rpc/methods.ts
@@ -31,8 +31,10 @@ export const RPC_METHODS = [
'market.ranking.sbi',
'news.list',
'watchlist.list',
+ 'orders.inquiry.detail',
'orders.inquiry.executionsToday',
'orders.inquiry.open',
+ 'orders.inquiry.tradeRecords',
'orders.cash.preOrder',
'orders.cash.estimate',
'orders.cash.place',
diff --git a/apps/csbie-ui/src/App.vue b/apps/csbie-ui/src/App.vue
index cf94c1b..28f3140 100644
--- a/apps/csbie-ui/src/App.vue
+++ b/apps/csbie-ui/src/App.vue
@@ -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"
/>
diff --git a/apps/csbie-ui/src/components/ApiKeyPolicyEditor.vue b/apps/csbie-ui/src/components/ApiKeyPolicyEditor.vue
index de643f4..2df4a1b 100644
--- a/apps/csbie-ui/src/components/ApiKeyPolicyEditor.vue
+++ b/apps/csbie-ui/src/components/ApiKeyPolicyEditor.vue
@@ -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',
diff --git a/apps/csbie-ui/src/constants/nav.ts b/apps/csbie-ui/src/constants/nav.ts
index f92982c..29ac2e1 100644
--- a/apps/csbie-ui/src/constants/nav.ts
+++ b/apps/csbie-ui/src/constants/nav.ts
@@ -8,14 +8,14 @@ export type PageMeta = {
}
export const pageMeta: Record = {
- 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 },
]
diff --git a/apps/csbie-ui/src/constants/trade.ts b/apps/csbie-ui/src/constants/trade.ts
index 03118b0..3a725f9 100644
--- a/apps/csbie-ui/src/constants/trade.ts
+++ b/apps/csbie-ui/src/constants/trade.ts
@@ -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(':')
diff --git a/apps/csbie-ui/src/features/history/HistoryView.vue b/apps/csbie-ui/src/features/history/HistoryView.vue
index 697af42..d9120e2 100644
--- a/apps/csbie-ui/src/features/history/HistoryView.vue
+++ b/apps/csbie-ui/src/features/history/HistoryView.vue
@@ -1,21 +1,40 @@
取引履歴
-
+
+
+
+
+ {{ orderActionError }}
{{ order.stock }}
- {{ order.date }} / 通常単元
+ {{ order.code }} / {{ order.market }} / {{ order.date }}
{{ order.side === 'buy' ? '購入' : '売却' }}
@@ -71,17 +211,43 @@ const confirmCancel = () => {
{{ order.status }}
-
+
+
+
+
+
@@ -98,6 +264,195 @@ const confirmCancel = () => {
+
+
+
+
+
- 注文番号
+ - {{ orderDetail.orderNumber ?? '-' }}
+
+
+
- 枝番
+ - {{ orderDetail.orderSubNo ?? '-' }}
+
+
+
- 銘柄
+ - {{ orderDetail.code }} / {{ orderDetail.market }}
+
+
+
- 売買
+ - {{ orderDetail.side === 'buy' ? '購入' : '売却' }}
+
+
+
- 数量
+ - {{ orderQuantityText(orderDetail) }}
+
+
+
- 未約定数量
+ - {{ orderDetail.unexecutedQuantity ?? '-' }}
+
+
+
- 価格
+ - {{ priceText(orderDetail.price, orderDetail.market) }}
+
+
+
- 状態
+ - {{ orderDetail.statusText ?? orderDetail.status }}
+
+
+
- 有効期限
+ - {{ orderDetail.expiresAt ?? '-' }}
+
+
+
- 取消/訂正
+ -
+ {{ orderDetail.cancelable === false ? '取消不可' : '取消可' }} /
+ {{ orderDetail.correctable === false ? '訂正不可' : '訂正可' }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ record.stock }}
+ {{ record.code }} / {{ record.market }} / {{ record.type }}
+
+ {{ record.tradeDate ?? '-' }} 約定 / {{ record.valueDate ?? '-' }} 受渡
+
+
+
+ {{ tradeAmountText(record) }}
+
+ {{ record.quantity ?? '-' }}株 @ {{ priceText(record.price, record.market) }}
+
+
+
+
+ 該当する約定履歴はありません
+
+
+
+
+
+
+
+
+
- 注文番号
+ - {{ correctionCandidate.orderNumber }}
+
+
+
- 現在数量
+ - {{ orderQuantityText(correctionCandidate) }}
+
+
+
- 現在価格
+ - {{ priceText(correctionCandidate.price, correctionCandidate.market) }}
+
+
+
+
+
+
+
+
+
+
- 見積数量
+ - {{ correctionPreview.quantity }}株
+
+
+
- 見積金額
+ -
+ {{
+ correctionPreview.price
+ ? priceText(correctionPreview.price.value, correctionCandidate.market)
+ : '-'
+ }}
+
+
+
+
+
+
+ 戻る
+
+
+
+
+ 見積
+
+
+
+
+ 訂正
+
+
+
+
+
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
}>()
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(null)
+const positionDetail = ref(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 = () => {
{{ currency(totalAssetValue) }}
+
+ 通算評価損益:
+
+ {{ signedCurrency(totalProfitLoss) }} · {{ signedPercent(totalProfitLossRate) }}
+
+
+
余力:
{{ currency(buyingPower) }}
@@ -120,17 +165,6 @@ const confirmCancel = () => {
-
- 通算評価損益
-
-
- {{ signedCurrency(totalProfitLoss) }}
- · {{ signedPercent(totalProfitLossRate) }}
-
-
-
-
-
保有銘柄
@@ -151,19 +185,18 @@ const confirmCancel = () => {
数量
評価額
評価損益
+ 操作
-
SBIに接続すると保有銘柄を表示します
+ {{ positionDetailError }}
@@ -234,8 +282,85 @@ const confirmCancel = () => {
+
+
+
+
指数
+
+
+
+ {{ index.name }}
+ {{ indexValueText(index) }}
+ {{ indexChangeText(index) }}
+
+
+
+
+
+ SBIに接続すると指数を表示します
+
+
+
+
+
+
- 銘柄
+ - {{ positionDetail.code }} / {{ positionDetail.market }}
+
+
+
- 預り区分
+ - {{ positionDetail.type ?? positionDetail.accountType ?? '-' }}
+
+
+
- 数量
+ - {{ positionDetail.quantity }}
+
+
+
- 現在値
+ -
+ {{
+ positionDetail.currentPrice == null
+ ? '-'
+ : currencyForMarket(positionDetail.currentPrice, positionDetail.market)
+ }}
+
+
+
+
- 平均取得単価
+ - {{ currencyForMarket(positionDetail.avgPrice, positionDetail.market) }}
+
+
+
- 評価額
+ - {{ currencyForMarket(positionDetail.marketValue, positionDetail.market) }}
+
+
+
- 評価損益
+ -
+ {{ signedCurrencyForMarket(positionDetail.profitLoss, positionDetail.market) }} /
+ {{ signedPercent(positionDetail.profitLossRate) }}
+
+
+
+
+
+
+
+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): 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,
-): Stock => {
+const accountTypeLabels: Record = {
+ 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,
}
}
diff --git a/apps/csbie-ui/src/features/trading/useTradingSession.ts b/apps/csbie-ui/src/features/trading/useTradingSession.ts
index eaf3c82..6668a29 100644
--- a/apps/csbie-ui/src/features/trading/useTradingSession.ts
+++ b/apps/csbie-ui/src/features/trading/useTradingSession.ts
@@ -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) => {
const holdingsMarketValue = ref(0)
const totalProfitLoss = ref(0)
const totalProfitLossRate = ref(0)
+ const marketIndexes = ref([])
const orders = ref([])
const cancelingOrderKey = ref('')
const orderHistoryLoaded = ref(false)
@@ -522,7 +531,8 @@ export const useTradingSession = (selectedPasskeyId: Ref) => {
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) => {
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) => {
const loadOrderHistoryFromSdk = async () => {
orderHistoryLoaded.value = false
orderHistoryNotice.value = ''
- const [openOrdersResult, executionsTodayResult] = await Promise.allSettled([
+ const orderResults = await Promise.allSettled([
rpcCallOptional('orders.inquiry.open'),
rpcCallOptional('orders.inquiry.executionsToday'),
- rpcCallOptional('orders.inquiry.open', { market: 'XNAS' }),
- rpcCallOptional('orders.inquiry.executionsToday', { market: 'XNAS' }),
+ ...usOrderMarkets.flatMap((market) => [
+ rpcCallOptional('orders.inquiry.open', { market }),
+ rpcCallOptional('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) => {
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) => {
const loadTradingData = async () => {
dataLoading.value = true
try {
- const [assetsResult, ...positionResults] = await Promise.allSettled([
+ const [assetsResult, indexResult, ...positionResults] = await Promise.allSettled([
rpcCallOptional('account.assets.current', undefined, 20_000),
+ rpcCallOptional('market.index.major', undefined, 15_000),
rpcCallOptional('account.positions.cash', undefined, 15_000),
- rpcCallOptional('account.positions.cash', { market: 'XNAS' }, 15_000),
+ ...usOrderMarkets.map((market) =>
+ rpcCallOptional('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) => {
.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) => {
}
}
- 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) => {
: searchableMarkets.filter((market) => market !== 'auto')
const results = await Promise.allSettled(
marketsToSearch.map((market) =>
- rpcCall('market.issue.search', { query, market, limit: 12 }),
+ rpcCall('market.issue.suggest', { query, market, limit: 12 }),
),
)
const issues = fulfilledValues(results)
@@ -1121,6 +1160,7 @@ export const useTradingSession = (selectedPasskeyId: Ref) => {
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) => {
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) => {
}
}
+ const loadOrderDetail = async (order: OrderRow): Promise => {
+ if (!isUsMarket(order.market)) {
+ throw new Error('注文詳細は米国株のみ対応しています')
+ }
+ if (!order.orderNumber && !order.id && !order.orderSubNo) {
+ throw new Error('注文番号を取得できないため詳細を取得できません')
+ }
+ const detail = await rpcCall('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 => {
+ const result = await rpcCall('orders.inquiry.tradeRecords', { limit: 50 })
+ return asArray(result.records)
+ .map(tradeRecordFromApi)
+ .filter((record): record is TradeRecordRow => Boolean(record))
+ }
+
+ const loadPositionDetail = async (position: Position): Promise => {
+ if (!isUsMarket(position.market)) {
+ throw new Error('保有詳細は米国株のみ対応しています')
+ }
+ const detail = await rpcCall('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 => {
+ if (!isUsMarket(order.market)) throw new Error('注文訂正は米国株のみ対応しています')
+ const preview = await rpcCall(
+ '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) => {
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) => {
holdingsMarketValue,
totalProfitLoss,
totalProfitLossRate,
+ marketIndexes,
orders,
cancelingOrderKey,
orderHistoryLoaded,
@@ -1379,6 +1501,11 @@ export const useTradingSession = (selectedPasskeyId: Ref) => {
askPlaceOrder,
placeCashOrder,
cancelOrder,
+ loadOrderDetail,
+ loadTradeRecords,
+ loadPositionDetail,
+ estimateOrderCorrection,
+ placeOrderCorrection,
downloadCsv,
openTradeForStock,
openTradeForPosition,
diff --git a/apps/csbie-ui/src/styles/ui.ts b/apps/csbie-ui/src/styles/ui.ts
index 5c46a30..d25b016 100644
--- a/apps/csbie-ui/src/styles/ui.ts
+++ b/apps/csbie-ui/src/styles/ui.ts
@@ -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]',
diff --git a/apps/csbie-ui/src/types/trading.ts b/apps/csbie-ui/src/types/trading.ts
index 1fe4daf..721d6ee 100644
--- a/apps/csbie-ui/src/types/trading.ts
+++ b/apps/csbie-ui/src/types/trading.ts
@@ -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
}
diff --git a/packages/sbi-client/src/index.ts b/packages/sbi-client/src/index.ts
index 42503d6..19aba7b 100644
--- a/packages/sbi-client/src/index.ts
+++ b/packages/sbi-client/src/index.ts
@@ -37,6 +37,8 @@ export type {
OrderCorrectionPreOrder,
OrderCorrectionPreOrderDetail,
OrderPreview,
+ TradeRecord,
+ TradeRecordList,
StockOrderPreOrder,
StockOrderPreOrderMarginTradeType,
StockOrderPreOrderPaymentLimit,
diff --git a/packages/sbi-client/src/markets.ts b/packages/sbi-client/src/markets.ts
index 54c43a3..d63d234 100644
--- a/packages/sbi-client/src/markets.ts
+++ b/packages/sbi-client/src/markets.ts
@@ -4,6 +4,9 @@ export type MarketRegion = 'domestic' | 'us'
const DOMESTIC_TO_MTS = {
XTKS: 'TKY',
+ XNGO: 'NGY',
+ XFKA: 'FKO',
+ XSAP: 'SPR',
} as const satisfies Record
const MTS_TO_DOMESTIC = Object.fromEntries(
diff --git a/packages/sbi-client/src/methods/index.ts b/packages/sbi-client/src/methods/index.ts
index f380ae9..d88a177 100644
--- a/packages/sbi-client/src/methods/index.ts
+++ b/packages/sbi-client/src/methods/index.ts
@@ -297,19 +297,21 @@ export const createMethodsFromSession = (session: SbiSession): SbiClientMethods
positions: {
cash: async (options) =>
options?.market && isUsMarket(options.market)
- ? usStock.positions()
+ ? usStock.positions(options)
: parseCashPositions(
await callMts(session, 'F2631', listAccountTrin(session, options)),
options,
),
cashDetail: async (options) =>
- parseCashPositions(
- await callMts(session, 'F2632', listAccountTrin(session, options)),
- options,
- ),
+ options?.market && isUsMarket(options.market)
+ ? usStock.positionsDetail(options)
+ : parseCashPositions(
+ await callMts(session, 'F2632', listAccountTrin(session, options)),
+ options,
+ ),
cashForIssue: async (options) => {
return isUsMarket(options.market)
- ? usStock.positions()
+ ? usStock.positions(options)
: filterCashPositions(
parseCashPositions(
await callMts(session, 'F2602', issuePositionTrin(session, options)),
@@ -487,6 +489,18 @@ export const createMethodsFromSession = (session: SbiSession): SbiClientMethods
),
options,
),
+ detail: async (options) => {
+ if (isUsMarket(options.market)) return usStock.orderDetail(options)
+ throw new Error(
+ 'orders.inquiry.detail is currently implemented only for US stock markets',
+ )
+ },
+ tradeRecords: async (options) => {
+ if (!options.market || isUsMarket(options.market)) return usStock.tradeRecords(options)
+ throw new Error(
+ 'orders.inquiry.tradeRecords is currently implemented only for US stock markets',
+ )
+ },
},
cash: {
preOrder: async (options) =>
@@ -528,22 +542,28 @@ export const createMethodsFromSession = (session: SbiSession): SbiClientMethods
)
},
estimateCorrection: async (options) =>
- parseOrderCorrectionPreOrder(
- await callMts(session, 'F2301', orderCorrectionPreOrderTrin(session, options)),
- cashCorrectionPreviewInput,
- ),
+ options.market && isUsMarket(options.market)
+ ? usStock.estimateCorrection(options)
+ : parseOrderCorrectionPreOrder(
+ await callMts(session, 'F2301', orderCorrectionPreOrderTrin(session, options)),
+ cashCorrectionPreviewInput,
+ ),
estimateCorrectionConfirm: async (options) =>
- parseOrderPreview(
- await callMts(session, 'F2302', orderCorrectionSubmitTrin(session, options)),
- cashCorrectionPreviewInput,
- ),
+ options.market && isUsMarket(options.market)
+ ? usStock.estimateCorrectionConfirm(options)
+ : parseOrderPreview(
+ await callMts(session, 'F2302', orderCorrectionSubmitTrin(session, options)),
+ cashCorrectionPreviewInput,
+ ),
placeCorrection: async (options) => {
+ if (options.market && isUsMarket(options.market)) return usStock.placeCorrection(options)
assertTradingAllowed(options, 'orders.cash.placeCorrection')
return parseOrderReceipt(
await callMts(session, 'F2303', orderCorrectionSubmitTrin(session, options)),
)
},
estimateCancel: async (options) => {
+ if (options.market && isUsMarket(options.market)) return usStock.estimateCancel(options)
assertOrderCancelOptions(options)
return parseOrderCorrectionPreOrder(
await callMts(session, 'F2311', orderCancelPreOrderTrin(session, options)),
diff --git a/packages/sbi-client/src/methods/types.ts b/packages/sbi-client/src/methods/types.ts
index 0216873..284315b 100644
--- a/packages/sbi-client/src/methods/types.ts
+++ b/packages/sbi-client/src/methods/types.ts
@@ -23,6 +23,7 @@ import type {
MarketCode,
MarketIndex,
NewsList,
+ Order,
OrderId,
OrderKind,
OrderList,
@@ -36,6 +37,7 @@ import type {
Ranking,
ThemeId,
ThemeInvestmentList,
+ TradeRecordList,
TradeSide,
Watchlist,
} from '../types'
@@ -116,6 +118,22 @@ export type OrderInquiryOptions = PagingOptions &
status?: OrderStatus
}
+export type OrderDetailOptions = {
+ /** Order number shown in order inquiry. */
+ orderNumber?: string
+ /** Order ID from order inquiry. For US stocks this is often `orderSubNo`. */
+ orderId?: OrderId
+ /** Issue code used to fetch the related security and quote details. */
+ issueCode?: IssueCode
+ /** Market code used to fetch the related security and quote details. */
+ market: MarketCode
+}
+
+export type TradeRecordInquiryOptions = OrderInquiryOptions & {
+ /** Filters trade records by account type. */
+ accountType?: AccountType
+}
+
export type BoardOptions = IssueOptions & {
/** Account type used when requesting board-order information. */
accountType?: AccountType
@@ -384,6 +402,10 @@ export type OrderCancelOptions = {
orderNumber: string
/** Original order ID shown in order inquiry. */
orderId?: OrderId
+ /** Original issue code shown in order inquiry. */
+ issueCode?: IssueCode
+ /** Original market code shown in order inquiry. */
+ market?: MarketCode
/** Original trade ID code. Defaults to cash stock when omitted. */
tradeId?: string
/** Additional cancel flag used by the mobile MTS route. */
@@ -667,6 +689,10 @@ export interface SbiClientMethodOrderInquiry {
executionsToday(options?: OrderInquiryOptions): Promise
/** Fetches open or recently active orders. */
open(options?: OrderInquiryOptions): Promise
+ /** Fetches a detailed order record. Currently implemented for US stock orders. */
+ detail(options: OrderDetailOptions): Promise
+ /** Fetches trade records. Currently implemented for US stock trades. */
+ tradeRecords(options: TradeRecordInquiryOptions): Promise
}
export interface SbiClientMethodCashOrder {
diff --git a/packages/sbi-client/src/methods/us-stock.ts b/packages/sbi-client/src/methods/us-stock.ts
index 8f62f2a..7edf5fd 100644
--- a/packages/sbi-client/src/methods/us-stock.ts
+++ b/packages/sbi-client/src/methods/us-stock.ts
@@ -16,23 +16,32 @@ import type {
SbiSession,
SignedTextValue,
StockOrderPreOrder,
+ TradeRecord,
+ TradeRecordList,
TradeSide,
} from '../types'
import type {
BoardOptions,
CashOrderOptions,
CashOrderPreOrderOptions,
+ CashPositionOptions,
IssueChartOptions,
IssueOptions,
IssueSearchOptions,
+ OrderCancelOptions,
+ OrderCorrectionOptions,
+ OrderDetailOptions,
OrderInquiryOptions,
PlaceCashOrderOptions,
PlaceOrderCancelOptions,
+ PlaceOrderCorrectionOptions,
+ TradeRecordInquiryOptions,
} from './types'
import { requireUsMarket } from '../markets'
const COUNTRY_US = 'US'
const DEFAULT_CHART_COUNT = 120
+const DEFAULT_US_INQUIRY_LOOKBACK_DAYS = 90
const US_CHART_INTERVALS: Record> = {
minute: { 1: '1', 5: '2', 10: '3', 15: '4' },
day: { 1: '7' },
@@ -127,7 +136,10 @@ export const createUsStockAdapter = (session: SbiSession) => ({
quote,
}
},
- positions: async (): Promise => fetchUsCashPositions(session),
+ positions: async (options?: CashPositionOptions): Promise =>
+ fetchUsCashPositions(session, options),
+ positionsDetail: async (options?: CashPositionOptions): Promise =>
+ fetchUsCashPositionDetail(session, options),
unrealized: async () => {
const positions = await fetchUsCashPositions(session)
return {
@@ -139,6 +151,10 @@ export const createUsStockAdapter = (session: SbiSession) => ({
},
orders: async (options?: OrderInquiryOptions): Promise =>
fetchUsOrders(session, options),
+ orderDetail: async (options: OrderDetailOptions): Promise =>
+ fetchUsOrderDetail(session, options),
+ tradeRecords: async (options: TradeRecordInquiryOptions): Promise =>
+ fetchUsTradeRecords(session, options),
preOrder: async (options: CashOrderPreOrderOptions): Promise => {
requireUsMarket(options.market, 'orders.cash.preOrder')
const data = await callUsGraphql(session, 'GetOrderCreatingInitialData', ORDER_INITIAL_DATA, {
@@ -204,11 +220,108 @@ export const createUsStockAdapter = (session: SbiSession) => ({
message: stringAt(data, ['createForeignStockOrder', 'message']),
}
},
+ estimateCorrection: async (options: OrderCorrectionOptions): Promise => {
+ requireUsOrderCorrectionOptions(options, 'orders.cash.estimateCorrection')
+ const order = await resolveUsOrderForAction(session, options)
+ const input = usOrderCorrectionInput(options, order)
+ const market = requireUsOrderMarket(order, 'orders.cash.estimateCorrection')
+ const data = await callUsGraphql(
+ session,
+ 'GetOrderUpdatingInitialData',
+ ORDER_UPDATE_INITIAL_DATA,
+ {
+ countryCode: COUNTRY_US,
+ securitiesCode: order.issue.code,
+ baseDate: undefined,
+ orderSubNo: input.orderSubNo,
+ rics: [usRic(order.issue.code, market)],
+ },
+ )
+ const init = objectAt(data, ['getForeignStockUpdatedOrderInitialization'])
+ const initOrder = objectAt(init, ['order'])
+ return {
+ issue: order.issue,
+ side: order.side,
+ quantity: numberAt(initOrder, ['orderQuantity']) ?? order.quantity ?? options.quantity,
+ price: usd(stringAt(initOrder, ['orderPrice']) ?? String(options.price ?? '')),
+ estimatedAmount: usd(stringAt(initOrder, ['frnNetAmount'])),
+ commission: usd(stringAt(initOrder, ['frnCommissionAmount'])),
+ tax: usd(stringAt(initOrder, ['frnCommissionCtax'])),
+ warnings: [],
+ confirmationId: input.orderSubNo,
+ correction: usOrderCorrectionPreOrderFromOrder(order, init),
+ }
+ },
+ estimateCorrectionConfirm: async (options: OrderCorrectionOptions): Promise => {
+ requireUsOrderCorrectionOptions(options, 'orders.cash.estimateCorrectionConfirm')
+ const order = await resolveUsOrderForAction(session, options)
+ const input = usOrderCorrectionInput(options, order)
+ const data = await callUsGraphql(
+ session,
+ 'ConfirmOrderUpdating',
+ CONFIRM_ORDER_UPDATE,
+ { input: { order: input } },
+ { tradePassword: requireUsTradePassword(session, 'orders.cash.estimateCorrectionConfirm') },
+ )
+ return orderUpdatePreviewFromConfirmation(data, order, options)
+ },
+ placeCorrection: async (options: PlaceOrderCorrectionOptions): Promise => {
+ requireUsOrderCorrectionOptions(options, 'orders.cash.placeCorrection')
+ if (options.allowTrading !== true) {
+ throw new Error('orders.cash.placeCorrection requires allowTrading: true')
+ }
+ const order = await resolveUsOrderForAction(session, options)
+ const input = usOrderCorrectionInput(options, order)
+ const data = await callUsGraphql(
+ session,
+ 'SubmitOrderUpdating',
+ SUBMIT_ORDER_UPDATE,
+ { input: { order: input } },
+ { tradePassword: requireUsTradePassword(session, 'orders.cash.placeCorrection') },
+ )
+ const updatedOrder = objectAt(data, ['updateForeignStockOrder', 'order'])
+ return orderReceiptFromUsOrder(updatedOrder, 'updateForeignStockOrder')
+ },
+ estimateCancel: async (options: OrderCancelOptions): Promise => {
+ const order = await resolveUsOrderForAction(session, options)
+ const orderSubNo = requireUsOrderSubNo(order, options, 'orders.cash.estimateCancel')
+ await callUsGraphql(session, 'ConfirmCancelOrderInitialization', CONFIRM_CANCEL_ORDER, {
+ input: { orderSubNo },
+ })
+ return {
+ issue: order.issue,
+ side: order.side,
+ quantity: order.unexecutedQuantity ?? order.quantity ?? undefined,
+ price: order.price,
+ estimatedAmount: usd(stringAt(order, ['frnNetAmount'])),
+ commission: usd(''),
+ tax: usd(''),
+ warnings: [],
+ confirmationId: orderSubNo,
+ correction: usOrderCorrectionPreOrderFromOrder(order),
+ }
+ },
placeCancel: async (options: PlaceOrderCancelOptions): Promise => {
if (options.allowTrading !== true) {
throw new Error('orders.cash.placeCancel requires allowTrading: true')
}
- throw new Error('orders.cash.placeCancel is not implemented for US stock markets')
+ const order = await resolveUsOrderForAction(session, options)
+ const orderSubNo = requireUsOrderSubNo(order, options, 'orders.cash.placeCancel')
+ await callUsGraphql(session, 'ConfirmCancelOrderInitialization', CONFIRM_CANCEL_ORDER, {
+ input: { orderSubNo },
+ })
+ const data = await callUsGraphql(
+ session,
+ 'SubmitOrderCancelling',
+ SUBMIT_ORDER_CANCEL,
+ { input: { orderSubNo } },
+ {
+ tradePassword:
+ options.tradePassword ?? requireUsTradePassword(session, 'orders.cash.placeCancel'),
+ },
+ )
+ const deletedOrder = objectAt(data, ['deleteForeignStockOrder', 'order'])
+ return orderReceiptFromUsOrder(deletedOrder, 'deleteForeignStockOrder')
},
})
@@ -224,39 +337,75 @@ const fetchStockDetail = async (session: SbiSession, options: IssueOptions, meth
return { stock, marketPrice }
}
-const fetchUsCashPositions = async (session: SbiSession): Promise => {
+const fetchUsCashPositions = async (
+ session: SbiSession,
+ options?: CashPositionOptions,
+): Promise => {
const data = await callUsGraphql(session, 'GetSecuritiesBalanceList', SECURITIES_BALANCES, {
input: { countryCode: COUNTRY_US, page: { pageNum: 1, pageSize: 999 } },
})
const balances = arrayAt(data, ['listSecuritiesBalances', 'securitiesBalances'])
- const positions = balances.map((balance): CashPosition => {
- const securities = objectAt(balance, ['securities'])
- const evaluation = objectAt(balance, ['evaluationProfitLoss'])
- const market = usMarketFromGraphql(objectAt(balance, ['market']))
- return {
- issue: {
- code: stringAt(securities, ['securitiesCode']) ?? '',
- market,
- name:
- stringAt(securities, ['securitiesName']) ?? stringAt(securities, ['securitiesShortName']),
- },
- accountType: mapUsSpecificAccount(stringAt(balance, ['specificAccountCode'])),
- depositType: mapUsSpecificAccount(stringAt(balance, ['specificAccountCode'])),
- quantity: numberAt(balance, ['securitiesQuantity']),
- currentPrice: usd(stringAt(balance, ['stockPrice', 'last'])),
- averagePrice: usd(stringAt(balance, ['frnAcquisitionPrice'])),
- purchasePrice: usd(stringAt(balance, ['frnAcquisitionPrice'])),
- marketValue: usd(stringAt(evaluation, ['frnEvaluationAmount'])),
- valuationPrice: usd(stringAt(evaluation, ['frnEvaluationAmount'])),
- profitLoss: signed(stringAt(evaluation, ['frnEvaluationProfitLoss'])),
- profitLossRate: percent(stringAt(evaluation, ['frnEvaluationProfitLossPercent'])),
- }
- })
+ const positions = balances
+ .map((balance) => cashPositionFromUsBalance(balance))
+ .filter((position) => {
+ if (options?.issueCode && position.issue.code !== options.issueCode) return false
+ if (options?.market && position.issue.market !== options.market) return false
+ if (options?.accountType && position.accountType !== options.accountType) return false
+ return true
+ })
+ const limited = limitList(positions, options)
return {
- positions,
+ positions: limited,
totalCount: positions.length,
- totalMarketValue: sumAmounts(positions.map((position) => position.marketValue)),
- totalProfitLoss: sumSigned(positions.map((position) => position.profitLoss)),
+ totalMarketValue: sumAmounts(limited.map((position) => position.marketValue)),
+ totalProfitLoss: sumSigned(limited.map((position) => position.profitLoss)),
+ }
+}
+
+const fetchUsCashPositionDetail = async (
+ session: SbiSession,
+ options?: CashPositionOptions,
+): Promise => {
+ if (options?.market) requireUsMarket(options.market, 'account.positions.cashDetail')
+ if (!options?.issueCode) {
+ throw new Error('account.positions.cashDetail requires issueCode for US stock positions')
+ }
+
+ const baseList = await fetchUsCashPositions(session, options)
+ const base = baseList.positions[0]
+ if (!base) {
+ throw new Error('account.positions.cashDetail could not find the requested US stock position')
+ }
+
+ const data = await callUsGraphql(
+ session,
+ 'GetSecuritiesBalanceDetail',
+ SECURITIES_BALANCE_DETAIL,
+ {
+ inputSecuritiesBalance: {
+ productCode: 'FOREIGN_STOCK',
+ countryCode: COUNTRY_US,
+ currencyCode: 'USD',
+ specificAccountCode: usSpecificAccountCode(base.accountType),
+ securitiesCode: base.issue.code,
+ },
+ inputExchangeRate: { currencyPair: 'USDJPY' },
+ inputStockSecurities: {
+ countryCode: COUNTRY_US,
+ securitiesCode: base.issue.code,
+ },
+ },
+ )
+ const balance = objectAt(data, ['getSecuritiesBalance'])
+ if (!balance) {
+ throw new Error('account.positions.cashDetail returned no US stock balance detail')
+ }
+ const position = cashPositionFromUsBalance(balance, base.issue.market)
+ return {
+ positions: [position],
+ totalCount: 1,
+ totalMarketValue: position.marketValue,
+ totalProfitLoss: position.profitLoss,
}
}
@@ -264,10 +413,21 @@ const fetchUsOrders = async (
session: SbiSession,
options?: OrderInquiryOptions,
): Promise => {
+ const dateRange = usInquiryDateRange(options)
const data = await callUsGraphql(session, 'GetOrderList', ORDER_LIST, {
- input: { countryCode: COUNTRY_US, page: { pageNum: 1, pageSize: options?.limit ?? 999 } },
+ input: {
+ countryCode: COUNTRY_US,
+ securitiesCode: options?.issueCode,
+ orderDateFrom: dateRange.from,
+ orderDateTo: dateRange.to,
+ orderDateType: 'ORDER_INPUT_DATE',
+ page: { pageNum: options?.index ?? 1, pageSize: options?.limit ?? 999 },
+ },
})
- const orders = arrayAt(data, ['listForeignStockOrders', 'orderDecodes']).map(orderFromGraphql)
+ const orders = arrayAt(data, ['listForeignStockOrders', 'orderDecodes']).map((order) =>
+ orderFromGraphql(order),
+ )
+ const list = objectAt(data, ['listForeignStockOrders'])
return {
orders: orders.filter((order) => {
if (options?.issueCode && order.issue.code !== options.issueCode) return false
@@ -275,6 +435,65 @@ const fetchUsOrders = async (
if (options?.status && order.status !== options.status) return false
return true
}),
+ hasMore: booleanAt(list, ['page', 'hasNextPage']),
+ }
+}
+
+const fetchUsOrderDetail = async (
+ session: SbiSession,
+ options: OrderDetailOptions,
+): Promise => {
+ requireUsMarket(options.market, 'orders.inquiry.detail')
+ const base = await resolveUsOrderForAction(session, options)
+ const orderNo = base.orderNumber ?? options.orderNumber
+ if (!orderNo) throw new Error('orders.inquiry.detail requires orderNumber for US stock orders')
+ const data = await callUsGraphql(session, 'GetOrderDetail', ORDER_DETAIL, {
+ input: { orderNo },
+ inputStockSecurities: {
+ countryCode: COUNTRY_US,
+ securitiesCode: base.issue.code,
+ },
+ inputStockPrice: {
+ countryCode: COUNTRY_US,
+ rics: [usRic(base.issue.code, requireUsOrderMarket(base, 'orders.inquiry.detail'))],
+ },
+ })
+ const detail = objectAt(data, ['getForeignStockOrderDetail', 'orderDetail'])
+ const order = objectAt(detail, ['order'])
+ if (!order) throw new Error('orders.inquiry.detail returned no US stock order detail')
+ return orderFromGraphql(order, {
+ issue: base.issue,
+ cancelable: booleanAt(detail, ['cancelable']),
+ correctable: booleanAt(detail, ['correctable']),
+ })
+}
+
+const fetchUsTradeRecords = async (
+ session: SbiSession,
+ options: TradeRecordInquiryOptions,
+): Promise => {
+ if (options.market) requireUsMarket(options.market, 'orders.inquiry.tradeRecords')
+ const dateRange = usInquiryDateRange(options)
+ const data = await callUsGraphql(session, 'GetTradeRecordList', TRADE_RECORD_LIST, {
+ input: {
+ productCode: 'FOREIGN_STOCK',
+ countryCode: COUNTRY_US,
+ securitiesCode: options.issueCode,
+ specificAccountCode: usSpecificAccountCode(options.accountType),
+ tradeHistoryType: 'TRADE_RECORD',
+ searchDateType: 'TRADE_DATE_BASE',
+ searchDateFrom: dateRange.from,
+ searchDateTo: dateRange.to,
+ page: { pageNum: options.index ?? 1, pageSize: options.limit ?? 999 },
+ },
+ })
+ const list = objectAt(data, ['listTradeRecords'])
+ const records = arrayAt(list, ['tradeRecords']).map((record) =>
+ tradeRecordFromGraphql(record, options.market),
+ )
+ return {
+ records,
+ hasMore: booleanAt(list, ['page', 'hasNextPage']),
}
}
@@ -450,16 +669,23 @@ const orderPreviewFromConfirmation = (
}
}
-const orderFromGraphql = (value: unknown): Order => {
- const market = usMarketFromGraphql(objectAt(value, ['market']))
+const orderFromGraphql = (
+ value: unknown,
+ extra: { issue?: Order['issue']; cancelable?: boolean; correctable?: boolean } = {},
+): Order => {
+ const market = extra.issue?.market ?? usMarketFromGraphql(objectAt(value, ['market']))
const securities = objectAt(value, ['securities'])
+ const orderNo = stringAt(value, ['orderNo'])
+ const orderSubNo = stringAt(value, ['orderSubNo'])
return {
- id: stringAt(value, ['orderNo']) ?? stringAt(value, ['orderSubNo']) ?? '',
+ id: orderSubNo ?? orderNo ?? '',
issue: {
- code: stringAt(securities, ['securitiesCode']) ?? '',
+ code: extra.issue?.code ?? stringAt(securities, ['securitiesCode']) ?? '',
market,
name:
- stringAt(securities, ['securitiesName']) ?? stringAt(securities, ['securitiesShortName']),
+ extra.issue?.name ??
+ stringAt(securities, ['securitiesName']) ??
+ stringAt(securities, ['securitiesShortName']),
},
side: stringAt(value, ['buySellCode']) === 'SELL' ? 'sell' : 'buy',
status: mapOrderStatus(stringAt(value, ['orderStatus'])),
@@ -470,7 +696,204 @@ const orderFromGraphql = (value: unknown): Order => {
price: usd(stringAt(value, ['orderPrice'])),
executedPrice: usd(stringAt(value, ['executionAveragePrice'])),
orderedAt: stringAt(value, ['orderInputDatetime']),
- orderNumber: stringAt(value, ['orderNo']),
+ expiresAt: stringAt(value, ['expiredDatetime']) ?? stringAt(value, ['orderTerm']),
+ orderNumber: orderNo,
+ orderSubNo,
+ accountType: mapUsSpecificAccount(stringAt(value, ['specificAccountCode'])),
+ depositType: mapUsSpecificAccount(stringAt(value, ['specificAccountCode'])),
+ cancelable: extra.cancelable,
+ correctable: extra.correctable,
+ }
+}
+
+const cashPositionFromUsBalance = (balance: unknown, fallbackMarket?: MarketCode): CashPosition => {
+ const securities = objectAt(balance, ['securities'])
+ const evaluation = objectAt(balance, ['evaluationProfitLoss'])
+ const market = usMarketFromGraphql(objectAt(balance, ['market']), fallbackMarket)
+ return {
+ issue: {
+ code: stringAt(securities, ['securitiesCode']) ?? '',
+ market,
+ name:
+ stringAt(securities, ['securitiesName']) ?? stringAt(securities, ['securitiesShortName']),
+ },
+ accountType: mapUsSpecificAccount(stringAt(balance, ['specificAccountCode'])),
+ depositType: mapUsSpecificAccount(stringAt(balance, ['specificAccountCode'])),
+ quantity: numberAt(balance, ['securitiesQuantity']),
+ currentPrice: usd(stringAt(balance, ['stockPrice', 'last'])),
+ averagePrice: usd(stringAt(balance, ['frnAcquisitionPrice'])),
+ purchasePrice: usd(stringAt(balance, ['frnAcquisitionPrice'])),
+ marketValue: usd(stringAt(evaluation, ['frnEvaluationAmount'])),
+ valuationPrice: usd(stringAt(evaluation, ['frnEvaluationAmount'])),
+ profitLoss: signed(stringAt(evaluation, ['frnEvaluationProfitLoss'])),
+ profitLossRate: percent(stringAt(evaluation, ['frnEvaluationProfitLossPercent'])),
+ }
+}
+
+const resolveUsOrderForAction = async (
+ session: SbiSession,
+ options: {
+ orderNumber?: string
+ orderId?: string
+ issueCode?: string
+ market?: MarketCode
+ },
+): Promise => {
+ if (options.market) requireUsMarket(options.market, 'US stock order action')
+ const orders = await fetchUsOrders(session, {
+ issueCode: options.issueCode,
+ market: options.market,
+ limit: 999,
+ })
+ const order = orders.orders.find((candidate) => {
+ const ids = [candidate.id, candidate.orderNumber, candidate.orderSubNo].filter(Boolean)
+ return (
+ (options.orderId != null && ids.includes(options.orderId)) ||
+ (options.orderNumber != null && ids.includes(options.orderNumber))
+ )
+ })
+ if (!order) {
+ throw new Error('US stock order action could not find the requested order in order inquiry')
+ }
+ return order
+}
+
+const requireUsOrderSubNo = (
+ order: Order,
+ options: { orderId?: string; orderNumber?: string },
+ methodName: string,
+) => {
+ const orderSubNo = order.orderSubNo ?? order.id ?? options.orderId ?? options.orderNumber
+ if (!orderSubNo) throw new Error(`${methodName} requires US stock orderSubNo`)
+ return orderSubNo
+}
+
+const requireUsOrderMarket = (order: Order, methodName: string) => {
+ if (!order.issue.market) throw new Error(`${methodName} requires US stock order market`)
+ requireUsMarket(order.issue.market, methodName)
+ return order.issue.market
+}
+
+const requireUsOrderCorrectionOptions = (options: OrderCorrectionOptions, methodName: string) => {
+ if (options.market) requireUsMarket(options.market, methodName)
+ if (options.orderMethod && options.orderMethod !== 'normal') {
+ throw new Error(`${methodName} does not support stop/OCO/IFD correction for US stocks`)
+ }
+ if (options.secondaryPriceCondition || options.secondaryPrice || options.ifdPriceCondition) {
+ throw new Error(`${methodName} does not support OCO/IFD correction for US stocks`)
+ }
+ if (
+ options.triggerZone ||
+ options.triggerPrice ||
+ options.ifdOrderMethod ||
+ options.ifdTriggerZone
+ ) {
+ throw new Error(`${methodName} does not support stop correction for US stocks`)
+ }
+ if (!Number.isFinite(options.quantity) || options.quantity == null || options.quantity <= 0) {
+ throw new Error(`${methodName} requires quantity for US stock correction`)
+ }
+ const priceCondition = options.priceCondition ?? 'limit'
+ if (priceCondition !== 'market' && priceCondition !== 'limit') {
+ throw new Error(`${methodName} supports only market or limit correction for US stocks`)
+ }
+ if (priceCondition === 'limit' && options.price == null) {
+ throw new Error(`${methodName} requires price for limit US stock correction`)
+ }
+ if (priceCondition === 'market' && options.price != null) {
+ throw new Error(`${methodName} cannot specify price for market US stock correction`)
+ }
+}
+
+const usOrderCorrectionInput = (options: OrderCorrectionOptions, order: Order) => {
+ const priceCondition = options.priceCondition ?? 'limit'
+ const orderSubNo = requireUsOrderSubNo(order, options, 'orders.cash.correction')
+ return {
+ orderSubNo,
+ countryCode: COUNTRY_US,
+ orderQuantity: String(options.quantity),
+ orderPriceKindCode: priceCondition === 'market' ? 'MARKET' : 'LIMIT',
+ orderPrice: priceCondition === 'market' ? undefined : String(options.price),
+ stopPrice: undefined,
+ }
+}
+
+const usOrderCorrectionPreOrderFromOrder = (order: Order, init?: Record) => ({
+ issue: order.issue,
+ details: [],
+ orderNumber: order.orderNumber,
+ orderId: order.orderSubNo ?? order.id,
+ status: order.status,
+ statusText: order.statusText,
+ quantity: order.unexecutedQuantity ?? order.quantity,
+ price: order.price?.value,
+ priceAmount: order.price,
+ priceSteps: arrayAt(init, ['tickSizes']).map((tick) => ({
+ from: usd(stringAt(tick, ['tickSize']) ?? stringAt(tick, ['basePriceFrom'])),
+ to: usd(stringAt(tick, ['basePriceTo'])),
+ })),
+ marketName: order.issue.market,
+})
+
+const orderUpdatePreviewFromConfirmation = (
+ data: Record,
+ order: Order,
+ options: OrderCorrectionOptions,
+): OrderPreview => {
+ const confirmation = objectAt(data, ['confirmForeignStockUpdatedOrder'])
+ const confirmedOrder = objectAt(confirmation, ['order'])
+ return {
+ issue: order.issue,
+ side: order.side,
+ quantity: options.quantity,
+ price: usd(stringAt(confirmedOrder, ['orderPrice']) ?? String(options.price ?? '')),
+ estimatedAmount: usd(stringAt(confirmedOrder, ['frnNetAmount'])),
+ commission: usd(stringAt(confirmedOrder, ['frnCommissionAmount'])),
+ tax: usd(stringAt(confirmedOrder, ['frnCommissionCtax'])),
+ warnings: stringArrayAt(confirmation, ['warningStatuses']),
+ confirmationId: stringAt(confirmedOrder, ['orderSubNo']) ?? order.orderSubNo,
+ correction: usOrderCorrectionPreOrderFromOrder(order),
+ }
+}
+
+const orderReceiptFromUsOrder = (order: unknown, sourceName: string): OrderReceipt => {
+ if (!order) throw new Error(`${sourceName} returned no US stock order`)
+ return {
+ accepted: true,
+ orderId: stringAt(order, ['orderSubNo']) ?? stringAt(order, ['orderNo']),
+ acceptedAt: stringAt(order, ['orderInputDatetime']),
+ message: stringAt(order, ['orderStatus']) ?? sourceName,
+ }
+}
+
+const tradeRecordFromGraphql = (
+ value: unknown,
+ fallbackMarket: MarketCode = 'XNAS',
+): TradeRecord => {
+ const securities = objectAt(value, ['securities'])
+ const code = stringAt(securities, ['securitiesCode']) ?? ''
+ const tradeDate = stringAt(value, ['tradeDate'])
+ const typeCode = stringAt(value, ['tradeRecordTypeCode'])
+ return {
+ id: [code, tradeDate, typeCode, stringAt(value, ['valueDate'])].filter(Boolean).join(':'),
+ issue: {
+ code,
+ market: fallbackMarket,
+ name:
+ stringAt(securities, ['securitiesName']) ?? stringAt(securities, ['securitiesShortName']),
+ },
+ tradeRecordTypeCode: typeCode,
+ tradeCurrencyCode: stringAt(value, ['tradeCurrencyCode']),
+ listedSecuritiesStatus: stringAt(value, ['listedSecuritiesStatus']),
+ orderPriceKindCode: stringAt(value, ['orderPriceKindCode']),
+ accountType: mapUsSpecificAccount(stringAt(value, ['specificAccountCode'])),
+ settlementCurrencyCode: stringAt(value, ['settlementCurrencyCode']),
+ amount: usd(stringAt(value, ['amount'])),
+ quantity: numberAt(value, ['quantity']),
+ price: usd(stringAt(value, ['price'])),
+ tradeDate,
+ valueDate: stringAt(value, ['valueDate']),
+ marginCloseLimitType: stringAt(value, ['marginCloseLimitType']),
}
}
@@ -549,6 +972,35 @@ const normalizeUsOrderDate = (value: string) => {
return normalized
}
+const usInquiryDateRange = (options?: { from?: string; to?: string }) => {
+ const to = normalizeUsOptionalDate(options?.to) ?? formatUsDate(new Date())
+ const from =
+ normalizeUsOptionalDate(options?.from) ??
+ formatUsDate(addDays(parseUsDate(to), -DEFAULT_US_INQUIRY_LOOKBACK_DAYS))
+ return { from, to }
+}
+
+const parseUsDate = (value: string) => {
+ const date = new Date(`${value}T00:00:00.000Z`)
+ if (Number.isNaN(date.getTime())) {
+ throw new Error('US stock inquiry date must be yyyy-MM-dd or yyyyMMdd')
+ }
+ return date
+}
+
+const addDays = (date: Date, days: number) => {
+ const next = new Date(date)
+ next.setUTCDate(next.getUTCDate() + days)
+ return next
+}
+
+const formatUsDate = (date: Date) => {
+ const year = date.getUTCFullYear()
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0')
+ const day = String(date.getUTCDate()).padStart(2, '0')
+ return `${year}-${month}-${day}`
+}
+
const usGraphqlMarketCode = (market: MarketCode) => {
switch (market) {
case 'XNAS':
@@ -705,6 +1157,20 @@ const stringArrayAt = (source: unknown, path: string[]) =>
const numberAt = (source: unknown, path: string[]) => parseNumber(stringAt(source, path))
+const booleanAt = (source: unknown, path: string[]) => {
+ const value = valueAt(source, path)
+ return typeof value === 'boolean' ? value : undefined
+}
+
+const limitList = (items: T[], options?: { index?: number; limit?: number }) => {
+ const start = Math.max((options?.index ?? 1) - 1, 0)
+ const end = options?.limit ? start + options.limit : undefined
+ return items.slice(start, end)
+}
+
+const normalizeUsOptionalDate = (value: string | undefined) =>
+ value ? normalizeUsOrderDate(value) : undefined
+
const valueAt = (source: unknown, path: string[]): unknown =>
path.reduce((current, key) => {
if (!current || typeof current !== 'object') return undefined
@@ -801,10 +1267,138 @@ query GetOrderList($input: Input_fstock_order_ListForeignStockOrdersRequest) {
listForeignStockOrders(input: $input) {
orderDecodes {
orderNo orderSubNo buySellCode orderStatus orderQuantity unexecutedQuantity executionQuantity
- orderPrice executionAveragePrice orderInputDatetime
+ orderPrice executionAveragePrice orderInputDatetime orderTerm specificAccountCode
securities { securitiesCode securitiesName securitiesShortName }
market { marketCode marketName marketShortName }
}
page { hasNextPage pageNum pageSize }
}
}`
+
+const ORDER_UPDATE_INITIAL_DATA = `
+query GetOrderUpdatingInitialData($countryCode: common_enums_CountryEnum_Country, $securitiesCode: String, $baseDate: String, $orderSubNo: String, $rics: [String]) {
+ getForeignStockUpdatedOrderInitialization(input: { orderSubNo: $orderSubNo }) {
+ priceRangeLimitMax priceRangeLimitMin priceRangeNoLimit
+ tickSizes { basePriceFrom basePriceTo tickSize }
+ orderPriceKindCodes buyPossibleAmount
+ order {
+ orderNo orderSubNo buySellCode orderStatus orderQuantity unexecutedQuantity executionQuantity
+ orderPrice executionAveragePrice orderInputDatetime orderTerm frnNetAmount frnCommissionAmount frnCommissionCtax
+ securities { securitiesCode securitiesName securitiesShortName }
+ market { marketCode marketName marketShortName }
+ }
+ }
+ getForeignStockSecurities(input: { countryCode: $countryCode securitiesCode: $securitiesCode }) {
+ securities { countryCode securitiesCode securitiesName securitiesShortName ric }
+ market { marketCode marketName marketShortName timeZone }
+ }
+ checkJrNisaRestrictedReleaseBefore(input: { baseDate: $baseDate }) { restrictedReleaseBefore }
+ listMarketPrices(input: { countryCode: $countryCode rics: $rics }) {
+ marketPrices { ask askSize bid bidSize price { last lastDatetime change changePercent } }
+ }
+ checkJrNisaOpen { opened }
+}`
+
+const CONFIRM_ORDER_UPDATE = `
+query ConfirmOrderUpdating($input: Input_fstock_order_ConfirmForeignStockUpdatedOrderRequest) {
+ confirmForeignStockUpdatedOrder(input: $input) {
+ buyPossibleAmount nisaBuyLimitAmount warningStatuses
+ order {
+ orderNo orderSubNo buySellCode orderStatus orderQuantity unexecutedQuantity executionQuantity
+ orderPrice executionAveragePrice orderInputDatetime orderTerm frnNetAmount frnCommissionAmount frnCommissionCtax
+ securities { securitiesCode securitiesName securitiesShortName }
+ market { marketCode marketName marketShortName }
+ }
+ }
+ checkJrNisaOpen { opened }
+}`
+
+const SUBMIT_ORDER_UPDATE = `
+mutation SubmitOrderUpdating($input: Input_fstock_order_UpdateForeignStockOrderRequest) {
+ updateForeignStockOrder(input: $input) {
+ warningStatuses
+ order {
+ orderNo orderSubNo buySellCode orderStatus orderQuantity unexecutedQuantity executionQuantity
+ orderPrice executionAveragePrice orderInputDatetime orderTerm
+ securities { securitiesCode securitiesName securitiesShortName }
+ market { marketCode marketName marketShortName }
+ }
+ }
+}`
+
+const CONFIRM_CANCEL_ORDER = `
+query ConfirmCancelOrderInitialization($input: Input_fstock_order_GetForeignStockDeletedOrderInitializationRequest) {
+ getForeignStockDeletedOrderInitialization(input: $input) { __typename }
+}`
+
+const SUBMIT_ORDER_CANCEL = `
+mutation SubmitOrderCancelling($input: Input_fstock_order_DeleteForeignStockOrderRequest) {
+ deleteForeignStockOrder(input: $input) {
+ order {
+ orderNo orderSubNo buySellCode orderStatus orderQuantity unexecutedQuantity executionQuantity
+ orderPrice executionAveragePrice orderInputDatetime orderTerm
+ securities { securitiesCode securitiesName securitiesShortName }
+ market { marketCode marketName marketShortName }
+ }
+ }
+}`
+
+const ORDER_DETAIL = `
+query GetOrderDetail($input: Input_fstock_order_GetForeignStockOrderDetailRequest, $inputStockSecurities: Input_fstock_securities_GetForeignStockSecuritiesRequest, $inputStockPrice: Input_information_marketprice_ListMarketPricesRequest) {
+ getForeignStockOrderDetail(input: $input) {
+ orderDetail {
+ cancelable correctable
+ order {
+ orderNo orderSubNo buySellCode specificAccountCode orderQuantity unexecutedQuantity
+ orderPriceKindCode stopPrice trailingStopAmount noLimitPrice orderLimitCode orderTerm
+ settlementMethodCode settlementCurrencyCode orderPrice executionAveragePrice orderInputDatetime
+ executionDatetime orderStatus tradeCurrencyCode expiredDatetime executionQuantity frnTradeDate
+ tradeDate valueDate frnCommissionAmount commissionAmount frnCommissionCtax commissionCtax
+ frnLocalCharge localCharge frnLocalNetAmount localNetAmount frnGrossAmount grossAmount
+ frnNetAmount netAmount executionNetAmount exchangeRate executionStatus workingStatus stockTradeType
+ market { marketCode marketName marketShortName timeZone }
+ }
+ }
+ }
+ getForeignStockSecurities(input: $inputStockSecurities) {
+ securities { countryCode securitiesCode securitiesName securitiesShortName ric }
+ market { marketCode marketName marketShortName timeZone }
+ }
+ listMarketPrices(input: $inputStockPrice) {
+ marketPrices { ask askSize bid bidSize price { last lastDatetime change changePercent } }
+ }
+ checkJrNisaOpen { opened }
+}`
+
+const TRADE_RECORD_LIST = `
+query GetTradeRecordList($input: Input_account_ListTradeRecordsRequest) {
+ listTradeRecords(input: $input) {
+ tradeRecords {
+ securities { countryCode securitiesCode securitiesName securitiesShortName ric }
+ tradeRecordTypeCode tradeCurrencyCode listedSecuritiesStatus orderPriceKindCode
+ specificAccountCode settlementCurrencyCode amount quantity price tradeDate valueDate marginCloseLimitType
+ }
+ page { hasNextPage }
+ }
+ checkJrNisaOpen { opened }
+}`
+
+const SECURITIES_BALANCE_DETAIL = `
+query GetSecuritiesBalanceDetail($inputSecuritiesBalance: Input_account_balance_GetSecuritiesBalanceRequest, $inputExchangeRate: Input_exchange_master_GetExchangeRateRequest, $inputStockSecurities: Input_fstock_securities_GetForeignStockSecuritiesRequest) {
+ getSecuritiesBalance(input: $inputSecuritiesBalance) {
+ securities { countryCode securitiesCode securitiesName securitiesShortName ric }
+ listedSecuritiesStatus stockPrice { last lastDatetime tickArrow change changePercent open high low prevClose volume }
+ evaluationProfitLoss {
+ frnEvaluationAmount frnEvaluationProfitLoss evaluationAmount evaluationProfitLoss evaluationProfitLossPercent frnEvaluationProfitLossPercent
+ }
+ specificAccountCode securitiesQuantity sellFixedOrderQuantity frnAcquisitionPrice acquisitionPrice
+ frnAcquisitionAmount acquisitionAmount countryCode currencyCode attentionSecurities
+ market { marketCode marketName marketShortName timeZone }
+ }
+ getExchangeRate(input: $inputExchangeRate) { rateDatetime exchangeRate }
+ getForeignStockSecurities(input: $inputStockSecurities) {
+ securities { countryCode securitiesCode securitiesName securitiesShortName ric }
+ market { marketCode marketName marketShortName timeZone }
+ }
+ checkJrNisaOpen { opened }
+}`
diff --git a/packages/sbi-client/src/types.ts b/packages/sbi-client/src/types.ts
index da420f8..b23a09d 100644
--- a/packages/sbi-client/src/types.ts
+++ b/packages/sbi-client/src/types.ts
@@ -180,7 +180,7 @@ export type SbiTradeAuthenticationOptions = {
}
export type IssueCode = string
-export type DomesticMarketCode = 'XTKS'
+export type DomesticMarketCode = 'XTKS' | 'XNGO' | 'XFKA' | 'XSAP'
export type SKabuMarketCode = 'STK'
export type UsStockMarketCode = 'XNAS' | 'XNYS' | 'ARCX'
export type MarketCode = DomesticMarketCode | SKabuMarketCode | UsStockMarketCode
@@ -647,9 +647,12 @@ export type Order = {
orderedAt?: string
expiresAt?: string
orderNumber?: string
+ orderSubNo?: string
tradeId?: string
exchangeCode?: string
accountInformation?: string
+ cancelable?: boolean
+ correctable?: boolean
}
export type OrderList = {
@@ -658,6 +661,29 @@ export type OrderList = {
error?: SbiMethodError
}
+export type TradeRecord = {
+ id: string
+ issue: IssueRef
+ tradeRecordTypeCode?: string
+ tradeCurrencyCode?: string
+ listedSecuritiesStatus?: string
+ orderPriceKindCode?: string
+ accountType?: AccountType
+ settlementCurrencyCode?: string
+ amount?: CurrencyAmount
+ quantity?: number | null
+ price?: CurrencyAmount
+ tradeDate?: string
+ valueDate?: string
+ marginCloseLimitType?: string
+}
+
+export type TradeRecordList = {
+ records: TradeRecord[]
+ hasMore?: boolean
+ error?: SbiMethodError
+}
+
export type OrderCorrectionPreOrderDetail = {
exchangeName?: string
marketLoanKbn?: string
diff --git a/scripts/check-env.ts b/scripts/check-env.ts
index 271b222..98e1700 100644
--- a/scripts/check-env.ts
+++ b/scripts/check-env.ts
@@ -8,7 +8,13 @@ const runtimeDefaults = {
CSBIE_RP_ID: '127.0.0.1',
}
-const optionalUrls = ['SBI_AUTH_BASE_URL', 'SBI_MTS_BASE_URL', 'CSBIE_ORIGIN', 'CSBIE_CORS_ORIGIN']
+const optionalUrls = [
+ 'SBI_AUTH_BASE_URL',
+ 'SBI_MTS_BASE_URL',
+ 'SBI_IZANAGI_BASE_URL',
+ 'CSBIE_ORIGIN',
+ 'CSBIE_CORS_ORIGIN',
+]
const present = (key: string) => Boolean(process.env[key]?.trim())
@@ -44,8 +50,14 @@ if (process.env.CSBIE_KEYRING_BACKEND === 'sqlite' && !present('CSBIE_KEYRING_SE
messages.push('Set CSBIE_KEYRING_SECRET when CSBIE_KEYRING_BACKEND=sqlite')
}
-if (!present('SBI_AUTH_BASE_URL') || !present('SBI_MTS_BASE_URL')) {
- messages.push('Set SBI_AUTH_BASE_URL and SBI_MTS_BASE_URL before connecting an SBI session')
+if (
+ !present('SBI_AUTH_BASE_URL') ||
+ !present('SBI_MTS_BASE_URL') ||
+ !present('SBI_IZANAGI_BASE_URL')
+) {
+ messages.push(
+ 'Set SBI_AUTH_BASE_URL, SBI_MTS_BASE_URL, and SBI_IZANAGI_BASE_URL before connecting an SBI session with domestic issue search',
+ )
}
if (messages.length === 0) {