feat: introduce better mcp
This commit is contained in:
+204
-104
@@ -7,13 +7,7 @@ import { eq } from 'drizzle-orm'
|
|||||||
import * as z from 'zod/v4'
|
import * as z from 'zod/v4'
|
||||||
import type { AppBindings, AuthContext } from '../context'
|
import type { AppBindings, AuthContext } from '../context'
|
||||||
import { sbiPasskeys } from '../db/schema'
|
import { sbiPasskeys } from '../db/schema'
|
||||||
import {
|
import { invokeSbiMethod, isCashOrderMethod, isTradingMethod, type RpcMethod } from '../rpc/methods'
|
||||||
invokeSbiMethod,
|
|
||||||
isCashOrderMethod,
|
|
||||||
isTradingMethod,
|
|
||||||
RPC_METHODS,
|
|
||||||
type RpcMethod,
|
|
||||||
} from '../rpc/methods'
|
|
||||||
import { connectSbi } from '../rpc/sbi-session'
|
import { connectSbi } from '../rpc/sbi-session'
|
||||||
import type { StoredSbiPasskeySecret } from './admin'
|
import type { StoredSbiPasskeySecret } from './admin'
|
||||||
import {
|
import {
|
||||||
@@ -33,8 +27,6 @@ const requireAuthenticated = (auth: AuthContext) => {
|
|||||||
if (!auth.authenticated) throw new Error('unauthorized')
|
if (!auth.authenticated) throw new Error('unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolNameForMethod = (method: RpcMethod) => `csbie_sbi_${method.replaceAll('.', '_')}`
|
|
||||||
|
|
||||||
const ORDER_SUBMIT_TICKET_TTL_MS = 10 * 60 * 1000
|
const ORDER_SUBMIT_TICKET_TTL_MS = 10 * 60 * 1000
|
||||||
|
|
||||||
type OrderSubmitTicket = {
|
type OrderSubmitTicket = {
|
||||||
@@ -81,10 +73,6 @@ const orderSubmitMethodByEstimateMethod = {
|
|||||||
const submitMethodForEstimateMethod = (method: RpcMethod) =>
|
const submitMethodForEstimateMethod = (method: RpcMethod) =>
|
||||||
orderSubmitMethodByEstimateMethod[method as keyof typeof orderSubmitMethodByEstimateMethod]
|
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) => {
|
const orderSubmitParams = (value: unknown, confirmationId?: string) => {
|
||||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return { allowTrading: true }
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return { allowTrading: true }
|
||||||
return {
|
return {
|
||||||
@@ -179,6 +167,17 @@ const orderInquiryOptionsSchema = z.object({
|
|||||||
status: z.enum(['open', 'executed', 'cancelled', 'expired', 'rejected', 'unknown']).optional(),
|
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({
|
const boardOptionsSchema = issueOptionsSchema.extend({
|
||||||
accountType: accountTypeSchema.optional(),
|
accountType: accountTypeSchema.optional(),
|
||||||
side: z
|
side: z
|
||||||
@@ -337,6 +336,8 @@ const placeOrderCorrectionSchema = orderCorrectionSchema.extend({
|
|||||||
const orderCancelSchema = z.object({
|
const orderCancelSchema = z.object({
|
||||||
orderNumber: z.string().min(1).describe('Order number shown in order inquiry'),
|
orderNumber: z.string().min(1).describe('Order number shown in order inquiry'),
|
||||||
orderId: orderIdSchema.optional().describe('Original order id 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'),
|
tradeId: z.string().min(1).optional().describe('Original trade id code'),
|
||||||
cancelType: z.string().min(1).optional().describe('Additional cancel flag'),
|
cancelType: z.string().min(1).optional().describe('Additional cancel flag'),
|
||||||
})
|
})
|
||||||
@@ -628,8 +629,10 @@ const methodParamSchemas = {
|
|||||||
'market.ranking.sbi': undefined,
|
'market.ranking.sbi': undefined,
|
||||||
'news.list': undefined,
|
'news.list': undefined,
|
||||||
'watchlist.list': undefined,
|
'watchlist.list': undefined,
|
||||||
|
'orders.inquiry.detail': orderDetailSchema,
|
||||||
'orders.inquiry.executionsToday': orderInquiryOptionsSchema.optional(),
|
'orders.inquiry.executionsToday': orderInquiryOptionsSchema.optional(),
|
||||||
'orders.inquiry.open': orderInquiryOptionsSchema.optional(),
|
'orders.inquiry.open': orderInquiryOptionsSchema.optional(),
|
||||||
|
'orders.inquiry.tradeRecords': tradeRecordInquiryOptionsSchema,
|
||||||
'orders.cash.preOrder': cashOrderPreOrderSchema,
|
'orders.cash.preOrder': cashOrderPreOrderSchema,
|
||||||
'orders.cash.estimate': cashOrderSchema,
|
'orders.cash.estimate': cashOrderSchema,
|
||||||
'orders.cash.place': placeCashOrderSchema,
|
'orders.cash.place': placeCashOrderSchema,
|
||||||
@@ -665,6 +668,84 @@ const methodParamSchemas = {
|
|||||||
'orders.exchange.place': placeExchangeOrderSchema,
|
'orders.exchange.place': placeExchangeOrderSchema,
|
||||||
} satisfies Record<RpcMethod, z.ZodType | undefined>
|
} satisfies Record<RpcMethod, z.ZodType | undefined>
|
||||||
|
|
||||||
|
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<string, RpcMethod>
|
||||||
|
|
||||||
|
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<string, keyof typeof orderSubmitMethodByEstimateMethod>
|
||||||
|
|
||||||
|
const getActions = Object.keys(getActionToMethod) as Array<keyof typeof getActionToMethod>
|
||||||
|
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<AppBindings>) => {
|
const createMcpServer = (c: Context<AppBindings>) => {
|
||||||
const db = c.get('db')
|
const db = c.get('db')
|
||||||
const config = c.get('config')
|
const config = c.get('config')
|
||||||
@@ -675,59 +756,33 @@ const createMcpServer = (c: Context<AppBindings>) => {
|
|||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
})
|
})
|
||||||
|
|
||||||
server.registerTool(
|
const listPasskeys = async () => {
|
||||||
'csbie_sbi_methods',
|
const rows = await db
|
||||||
{
|
.select({
|
||||||
title: 'List SBI RPC Methods',
|
id: sbiPasskeys.id,
|
||||||
description: 'List SBI client methods exposed through CSBIE.',
|
label: sbiPasskeys.label,
|
||||||
inputSchema: {},
|
keyringAccount: sbiPasskeys.keyringAccount,
|
||||||
},
|
createdAt: sbiPasskeys.createdAt,
|
||||||
async () => {
|
updatedAt: sbiPasskeys.updatedAt,
|
||||||
requireAuthenticated(auth)
|
|
||||||
return textResult({
|
|
||||||
methods: mcpExposedRpcMethods,
|
|
||||||
submitTool: 'csbie_sbi_submit_order',
|
|
||||||
})
|
})
|
||||||
},
|
.from(sbiPasskeys)
|
||||||
)
|
.orderBy(sbiPasskeys.createdAt)
|
||||||
|
return Promise.all(
|
||||||
|
rows.map(async ({ keyringAccount, ...row }) => {
|
||||||
|
const secret = await readSecret<StoredSbiPasskeySecret>(keyringAccount)
|
||||||
|
const hasDeviceId = Boolean(effectiveSbiDeviceId(secret))
|
||||||
|
const hasTradePassword = Boolean(effectiveSbiTradePassword(secret))
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
hasTradePassword,
|
||||||
|
hasDeviceId,
|
||||||
|
cashOrderReady: hasTradePassword && hasDeviceId,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
server.registerTool(
|
const invokeCheckedSbiMethod = async (method: RpcMethod, passkeyId: string, params: unknown) => {
|
||||||
'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<StoredSbiPasskeySecret>(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) => {
|
|
||||||
requireAuthenticated(auth)
|
requireAuthenticated(auth)
|
||||||
|
|
||||||
if (auth.type === 'apiKey') {
|
if (auth.type === 'apiKey') {
|
||||||
@@ -767,41 +822,105 @@ const createMcpServer = (c: Context<AppBindings>) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const client = await connectSbi(db, config, passkeyId)
|
const client = await connectSbi(db, config, passkeyId)
|
||||||
const result = await invokeSbiMethod(client, method, params)
|
return invokeSbiMethod(client, method, params)
|
||||||
const submitMethod = submitMethodForEstimateMethod(method)
|
}
|
||||||
if (!submitMethod) return textResult(result)
|
|
||||||
|
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()
|
cleanupExpiredOrderSubmitTickets()
|
||||||
const uuid = randomUUID()
|
const uuid = randomUUID()
|
||||||
const expiresAt = new Date(Date.now() + ORDER_SUBMIT_TICKET_TTL_MS)
|
const expiresAt = new Date(Date.now() + ORDER_SUBMIT_TICKET_TTL_MS)
|
||||||
const confirmationId = confirmationIdFromPreview(result)
|
const confirmationId = confirmationIdFromPreview(preview)
|
||||||
orderSubmitTickets.set(uuid, {
|
orderSubmitTickets.set(uuid, {
|
||||||
passkeyId,
|
passkeyId,
|
||||||
estimateMethod: method,
|
estimateMethod,
|
||||||
submitMethod,
|
submitMethod,
|
||||||
params,
|
params,
|
||||||
confirmationId,
|
confirmationId,
|
||||||
authKey: authKey(auth),
|
authKey: authKey(auth),
|
||||||
expiresAt,
|
expiresAt,
|
||||||
})
|
})
|
||||||
return textResult({
|
|
||||||
preview: result,
|
return {
|
||||||
submit: {
|
uuid,
|
||||||
uuid,
|
expiresAt: expiresAt.toISOString(),
|
||||||
tool: 'csbie_sbi_submit_order',
|
preview,
|
||||||
expiresAt: expiresAt.toISOString(),
|
confirmTool: 'confirm-request',
|
||||||
},
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.registerTool(
|
server.registerTool(
|
||||||
'csbie_sbi_submit_order',
|
'csbie-get',
|
||||||
{
|
{
|
||||||
title: 'Submit Estimated SBI Order',
|
title: 'Get SBI Data',
|
||||||
description:
|
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: {
|
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 }) => {
|
async ({ uuid }) => {
|
||||||
@@ -809,40 +928,21 @@ const createMcpServer = (c: Context<AppBindings>) => {
|
|||||||
cleanupExpiredOrderSubmitTickets()
|
cleanupExpiredOrderSubmitTickets()
|
||||||
|
|
||||||
const ticket = orderSubmitTickets.get(uuid)
|
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)) {
|
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)
|
orderSubmitTickets.delete(uuid)
|
||||||
return callSbiMethod(
|
const result = await invokeCheckedSbiMethod(
|
||||||
ticket.submitMethod,
|
ticket.submitMethod,
|
||||||
ticket.passkeyId,
|
ticket.passkeyId,
|
||||||
orderSubmitParams(ticket.params, ticket.confirmationId),
|
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
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ export const RPC_METHODS = [
|
|||||||
'market.ranking.sbi',
|
'market.ranking.sbi',
|
||||||
'news.list',
|
'news.list',
|
||||||
'watchlist.list',
|
'watchlist.list',
|
||||||
|
'orders.inquiry.detail',
|
||||||
'orders.inquiry.executionsToday',
|
'orders.inquiry.executionsToday',
|
||||||
'orders.inquiry.open',
|
'orders.inquiry.open',
|
||||||
|
'orders.inquiry.tradeRecords',
|
||||||
'orders.cash.preOrder',
|
'orders.cash.preOrder',
|
||||||
'orders.cash.estimate',
|
'orders.cash.estimate',
|
||||||
'orders.cash.place',
|
'orders.cash.place',
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ const {
|
|||||||
holdingsMarketValue,
|
holdingsMarketValue,
|
||||||
totalProfitLoss,
|
totalProfitLoss,
|
||||||
totalProfitLossRate,
|
totalProfitLossRate,
|
||||||
|
marketIndexes,
|
||||||
orders,
|
orders,
|
||||||
cancelingOrderKey,
|
cancelingOrderKey,
|
||||||
orderHistoryLoaded,
|
orderHistoryLoaded,
|
||||||
@@ -146,6 +147,11 @@ const {
|
|||||||
askPlaceOrder,
|
askPlaceOrder,
|
||||||
placeCashOrder,
|
placeCashOrder,
|
||||||
cancelOrder,
|
cancelOrder,
|
||||||
|
loadOrderDetail,
|
||||||
|
loadTradeRecords,
|
||||||
|
loadPositionDetail,
|
||||||
|
estimateOrderCorrection,
|
||||||
|
placeOrderCorrection,
|
||||||
downloadCsv,
|
downloadCsv,
|
||||||
openTradeForStock,
|
openTradeForStock,
|
||||||
openTradeForPosition,
|
openTradeForPosition,
|
||||||
@@ -232,6 +238,7 @@ onMounted(async () => {
|
|||||||
:holdings-market-value="holdingsMarketValue"
|
:holdings-market-value="holdingsMarketValue"
|
||||||
:total-profit-loss="totalProfitLoss"
|
:total-profit-loss="totalProfitLoss"
|
||||||
:total-profit-loss-rate="totalProfitLossRate"
|
:total-profit-loss-rate="totalProfitLossRate"
|
||||||
|
:market-indexes="marketIndexes"
|
||||||
:stock-asset-ratio="stockAssetRatio"
|
:stock-asset-ratio="stockAssetRatio"
|
||||||
:cash-asset-ratio="cashAssetRatio"
|
:cash-asset-ratio="cashAssetRatio"
|
||||||
:positions="positions"
|
:positions="positions"
|
||||||
@@ -241,6 +248,7 @@ onMounted(async () => {
|
|||||||
:connected="connected"
|
:connected="connected"
|
||||||
:order-history-loaded="orderHistoryLoaded"
|
:order-history-loaded="orderHistoryLoaded"
|
||||||
:order-history-notice="orderHistoryNotice"
|
:order-history-notice="orderHistoryNotice"
|
||||||
|
:load-position-detail="loadPositionDetail"
|
||||||
@connect="connect"
|
@connect="connect"
|
||||||
@open-position="
|
@open-position="
|
||||||
(code) =>
|
(code) =>
|
||||||
@@ -304,6 +312,10 @@ onMounted(async () => {
|
|||||||
:canceling-order-key="cancelingOrderKey"
|
:canceling-order-key="cancelingOrderKey"
|
||||||
:order-history-loaded="orderHistoryLoaded"
|
:order-history-loaded="orderHistoryLoaded"
|
||||||
:order-history-notice="orderHistoryNotice"
|
:order-history-notice="orderHistoryNotice"
|
||||||
|
:load-order-detail="loadOrderDetail"
|
||||||
|
:load-trade-records="loadTradeRecords"
|
||||||
|
:estimate-order-correction="estimateOrderCorrection"
|
||||||
|
:place-order-correction="placeOrderCorrection"
|
||||||
@refresh="loadTradingData"
|
@refresh="loadTradingData"
|
||||||
@cancel="cancelOrder"
|
@cancel="cancelOrder"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -39,8 +39,10 @@ const rpcMethods = [
|
|||||||
'market.ranking.sbi',
|
'market.ranking.sbi',
|
||||||
'news.list',
|
'news.list',
|
||||||
'watchlist.list',
|
'watchlist.list',
|
||||||
|
'orders.inquiry.detail',
|
||||||
'orders.inquiry.executionsToday',
|
'orders.inquiry.executionsToday',
|
||||||
'orders.inquiry.open',
|
'orders.inquiry.open',
|
||||||
|
'orders.inquiry.tradeRecords',
|
||||||
'orders.cash.estimate',
|
'orders.cash.estimate',
|
||||||
'orders.cash.place',
|
'orders.cash.place',
|
||||||
'orders.cash.estimateCorrection',
|
'orders.cash.estimateCorrection',
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ export type PageMeta = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const pageMeta: Record<RouteName, PageMeta> = {
|
export const pageMeta: Record<RouteName, PageMeta> = {
|
||||||
portfolio: { title: 'ポートフォリオ', icon: WalletCards },
|
portfolio: { title: 'ホーム', icon: WalletCards },
|
||||||
trade: { title: '取引', icon: ArrowLeftRight },
|
trade: { title: '取引', icon: ArrowLeftRight },
|
||||||
history: { title: '取引履歴', icon: History },
|
history: { title: '取引履歴', icon: History },
|
||||||
settings: { title: '設定', icon: Settings },
|
settings: { title: '設定', icon: Settings },
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sidebarItems: Array<{ name: RouteName; label: string; icon: Component }> = [
|
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: 'trade', label: '取引', icon: pageMeta.trade.icon },
|
||||||
{ name: 'settings', label: '設定', icon: pageMeta.settings.icon },
|
{ name: 'settings', label: '設定', icon: pageMeta.settings.icon },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -34,12 +34,23 @@ export const cashOrderMarketOptions: Array<{
|
|||||||
}> = [
|
}> = [
|
||||||
{ label: '自動', value: 'auto' },
|
{ label: '自動', value: 'auto' },
|
||||||
{ label: '東証', value: 'XTKS' },
|
{ label: '東証', value: 'XTKS' },
|
||||||
|
{ label: '名証', value: 'XNGO' },
|
||||||
|
{ label: '福証', value: 'XFKA' },
|
||||||
|
{ label: '札証', value: 'XSAP' },
|
||||||
{ label: 'NASDAQ', value: 'XNAS' },
|
{ label: 'NASDAQ', value: 'XNAS' },
|
||||||
{ label: 'NYSE', value: 'XNYS' },
|
{ label: 'NYSE', value: 'XNYS' },
|
||||||
{ label: 'NYSE Arca', value: 'ARCX' },
|
{ 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) => {
|
export const tradeRouteIdFromStockId = (id: string) => {
|
||||||
const separator = id.indexOf(':')
|
const separator = id.indexOf(':')
|
||||||
|
|||||||
@@ -1,21 +1,40 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { AnimatePresence } from 'motion-v'
|
import { AnimatePresence } from 'motion-v'
|
||||||
import { ArrowLeft, Ban, RefreshCw } from 'lucide-vue-next'
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
Ban,
|
||||||
|
FileText,
|
||||||
|
Pencil,
|
||||||
|
ReceiptText,
|
||||||
|
RefreshCw,
|
||||||
|
ShieldCheck,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
import Spinner from '../../components/ui/Spinner.vue'
|
import Spinner from '../../components/ui/Spinner.vue'
|
||||||
import UiButton from '../../components/ui/UiButton.vue'
|
import UiButton from '../../components/ui/UiButton.vue'
|
||||||
import UiModal from '../../components/ui/UiModal.vue'
|
import UiModal from '../../components/ui/UiModal.vue'
|
||||||
import { ui } from '../../styles/ui'
|
import { ui } from '../../styles/ui'
|
||||||
import type { OrderRow } from '../../types/trading'
|
import type { OrderDetail, OrderPreview, OrderRow, TradeRecordRow } from '../../types/trading'
|
||||||
|
import { currencyForMarket } from '../../utils/format'
|
||||||
import { orderAmountText, orderHistoryKey, orderQuantityText } from '../trading/trading-data'
|
import { orderAmountText, orderHistoryKey, orderQuantityText } from '../trading/trading-data'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
orders: OrderRow[]
|
orders: OrderRow[]
|
||||||
connected: boolean
|
connected: boolean
|
||||||
dataLoading: boolean
|
dataLoading: boolean
|
||||||
cancelingOrderKey: string
|
cancelingOrderKey: string
|
||||||
orderHistoryLoaded: boolean
|
orderHistoryLoaded: boolean
|
||||||
orderHistoryNotice: string
|
orderHistoryNotice: string
|
||||||
|
loadOrderDetail: (order: OrderRow) => Promise<OrderDetail>
|
||||||
|
loadTradeRecords: () => Promise<TradeRecordRow[]>
|
||||||
|
estimateOrderCorrection: (
|
||||||
|
order: OrderRow,
|
||||||
|
draft: { quantity: number; priceCondition: 'market' | 'limit'; price?: number },
|
||||||
|
) => Promise<OrderPreview>
|
||||||
|
placeOrderCorrection: (
|
||||||
|
order: OrderRow,
|
||||||
|
draft: { quantity: number; priceCondition: 'market' | 'limit'; price?: number },
|
||||||
|
) => Promise<void>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -25,11 +44,51 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const cancelCandidate = ref<OrderRow | null>(null)
|
const cancelCandidate = ref<OrderRow | null>(null)
|
||||||
const usMarkets = new Set(['XNAS', 'XNYS', 'ARCX'])
|
const usMarkets = new Set(['XNAS', 'XNYS', 'ARCX'])
|
||||||
|
const isUsMarket = (market: string) => usMarkets.has(market)
|
||||||
const canCancel = (order: OrderRow) =>
|
const canCancel = (order: OrderRow) =>
|
||||||
order.status === '注文中' && Boolean(order.orderNumber) && !usMarkets.has(order.market)
|
order.status === '注文中' && Boolean(order.orderNumber) && order.cancelable !== false
|
||||||
|
const canShowUsDetail = (order: OrderRow) => isUsMarket(order.market)
|
||||||
|
const canCorrect = (order: OrderRow) =>
|
||||||
|
order.status === '注文中' &&
|
||||||
|
Boolean(order.orderNumber) &&
|
||||||
|
isUsMarket(order.market) &&
|
||||||
|
order.correctable !== false
|
||||||
const isCanceling = (order: OrderRow, cancelingOrderKey: string) =>
|
const isCanceling = (order: OrderRow, cancelingOrderKey: string) =>
|
||||||
orderHistoryKey(order) === cancelingOrderKey
|
orderHistoryKey(order) === cancelingOrderKey
|
||||||
const cancelTitle = computed(() => cancelCandidate.value?.stock ?? '')
|
const cancelTitle = computed(() => cancelCandidate.value?.stock ?? '')
|
||||||
|
const orderDetail = ref<OrderDetail | null>(null)
|
||||||
|
const orderDetailLoadingKey = ref('')
|
||||||
|
const orderActionError = ref('')
|
||||||
|
const tradeRecords = ref<TradeRecordRow[]>([])
|
||||||
|
const showTradeRecords = ref(false)
|
||||||
|
const tradeRecordsLoading = ref(false)
|
||||||
|
const correctionCandidate = ref<OrderRow | null>(null)
|
||||||
|
const correctionQuantityInput = ref('')
|
||||||
|
const correctionPriceCondition = ref<'market' | 'limit'>('limit')
|
||||||
|
const correctionPriceInput = ref('')
|
||||||
|
const correctionPreview = ref<OrderPreview | null>(null)
|
||||||
|
const correctionLoading = ref(false)
|
||||||
|
const correctionSubmitting = ref(false)
|
||||||
|
|
||||||
|
const orderActionKey = (order: OrderRow) => orderHistoryKey(order)
|
||||||
|
const numericInput = (value: string) => {
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isFinite(parsed) ? parsed : null
|
||||||
|
}
|
||||||
|
const correctionDraft = () => {
|
||||||
|
const quantity = numericInput(correctionQuantityInput.value)
|
||||||
|
if (!quantity || quantity <= 0) throw new Error('数量を入力してください')
|
||||||
|
if (correctionPriceCondition.value === 'market') {
|
||||||
|
return { quantity, priceCondition: 'market' as const }
|
||||||
|
}
|
||||||
|
const price = numericInput(correctionPriceInput.value)
|
||||||
|
if (!price || price <= 0) throw new Error('指値を入力してください')
|
||||||
|
return { quantity, priceCondition: 'limit' as const, price }
|
||||||
|
}
|
||||||
|
const priceText = (value: number | null | undefined, market: string) =>
|
||||||
|
value == null ? '-' : currencyForMarket(value, market)
|
||||||
|
const tradeAmountText = (record: TradeRecordRow) =>
|
||||||
|
record.amount == null ? '-' : currencyForMarket(record.amount, record.market)
|
||||||
|
|
||||||
const askCancel = (order: OrderRow) => {
|
const askCancel = (order: OrderRow) => {
|
||||||
if (!canCancel(order)) return
|
if (!canCancel(order)) return
|
||||||
@@ -41,27 +100,108 @@ const confirmCancel = () => {
|
|||||||
emit('cancel', cancelCandidate.value)
|
emit('cancel', cancelCandidate.value)
|
||||||
cancelCandidate.value = null
|
cancelCandidate.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showOrderDetail = async (order: OrderRow) => {
|
||||||
|
if (!canShowUsDetail(order) || orderDetailLoadingKey.value) return
|
||||||
|
orderActionError.value = ''
|
||||||
|
orderDetailLoadingKey.value = orderActionKey(order)
|
||||||
|
try {
|
||||||
|
orderDetail.value = await props.loadOrderDetail(order)
|
||||||
|
} catch (cause) {
|
||||||
|
orderActionError.value = cause instanceof Error ? cause.message : '注文詳細の取得に失敗しました'
|
||||||
|
} finally {
|
||||||
|
orderDetailLoadingKey.value = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTradeRecords = async () => {
|
||||||
|
if (tradeRecordsLoading.value) return
|
||||||
|
orderActionError.value = ''
|
||||||
|
tradeRecordsLoading.value = true
|
||||||
|
try {
|
||||||
|
tradeRecords.value = await props.loadTradeRecords()
|
||||||
|
showTradeRecords.value = true
|
||||||
|
} catch (cause) {
|
||||||
|
orderActionError.value = cause instanceof Error ? cause.message : '約定履歴の取得に失敗しました'
|
||||||
|
} finally {
|
||||||
|
tradeRecordsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const askCorrection = (order: OrderRow) => {
|
||||||
|
if (!canCorrect(order)) return
|
||||||
|
correctionCandidate.value = order
|
||||||
|
correctionQuantityInput.value = String(order.unexecutedQuantity ?? order.quantity ?? '')
|
||||||
|
correctionPriceCondition.value = order.price == null ? 'market' : 'limit'
|
||||||
|
correctionPriceInput.value = order.price == null ? '' : String(order.price)
|
||||||
|
correctionPreview.value = null
|
||||||
|
orderActionError.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const estimateCorrection = async () => {
|
||||||
|
if (!correctionCandidate.value || correctionLoading.value) return
|
||||||
|
orderActionError.value = ''
|
||||||
|
correctionLoading.value = true
|
||||||
|
try {
|
||||||
|
correctionPreview.value = await props.estimateOrderCorrection(
|
||||||
|
correctionCandidate.value,
|
||||||
|
correctionDraft(),
|
||||||
|
)
|
||||||
|
} catch (cause) {
|
||||||
|
orderActionError.value = cause instanceof Error ? cause.message : '注文訂正の見積に失敗しました'
|
||||||
|
} finally {
|
||||||
|
correctionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmCorrection = async () => {
|
||||||
|
if (!correctionCandidate.value || correctionSubmitting.value) return
|
||||||
|
orderActionError.value = ''
|
||||||
|
correctionSubmitting.value = true
|
||||||
|
try {
|
||||||
|
await props.placeOrderCorrection(correctionCandidate.value, correctionDraft())
|
||||||
|
correctionCandidate.value = null
|
||||||
|
correctionPreview.value = null
|
||||||
|
} catch (cause) {
|
||||||
|
orderActionError.value = cause instanceof Error ? cause.message : '注文訂正に失敗しました'
|
||||||
|
} finally {
|
||||||
|
correctionSubmitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section :class="ui.panel">
|
<section :class="ui.panel">
|
||||||
<div :class="ui.panelHead">
|
<div :class="ui.panelHead">
|
||||||
<h2>取引履歴</h2>
|
<h2>取引履歴</h2>
|
||||||
<button
|
<span :class="ui.rowActions">
|
||||||
:class="ui.ghostButton"
|
<button
|
||||||
type="button"
|
:class="ui.ghostButton"
|
||||||
:disabled="!connected || dataLoading"
|
type="button"
|
||||||
@click="emit('refresh')"
|
:disabled="!connected || tradeRecordsLoading"
|
||||||
>
|
@click="loadTradeRecords"
|
||||||
<RefreshCw class="h-4 w-4" aria-hidden="true" />
|
>
|
||||||
履歴を更新
|
<Spinner v-if="tradeRecordsLoading" size="sm" />
|
||||||
</button>
|
<ReceiptText v-else class="h-4 w-4" aria-hidden="true" />
|
||||||
|
約定履歴
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
:class="ui.ghostButton"
|
||||||
|
type="button"
|
||||||
|
:disabled="!connected || dataLoading"
|
||||||
|
@click="emit('refresh')"
|
||||||
|
>
|
||||||
|
<RefreshCw class="h-4 w-4" aria-hidden="true" />
|
||||||
|
更新
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="orderActionError" :class="ui.dialogNote">{{ orderActionError }}</p>
|
||||||
<div :class="ui.list">
|
<div :class="ui.list">
|
||||||
<div v-for="order in orders" :key="order.id" :class="ui.orderRow">
|
<div v-for="order in orders" :key="order.id" :class="ui.orderRow">
|
||||||
<span>
|
<span>
|
||||||
<strong>{{ order.stock }}</strong>
|
<strong>{{ order.stock }}</strong>
|
||||||
<small>{{ order.date }} / 通常単元</small>
|
<small>{{ order.code }} / {{ order.market }} / {{ order.date }}</small>
|
||||||
</span>
|
</span>
|
||||||
<span :class="order.side === 'buy' ? ui.positive : ui.negative">
|
<span :class="order.side === 'buy' ? ui.positive : ui.negative">
|
||||||
{{ order.side === 'buy' ? '購入' : '売却' }}
|
{{ order.side === 'buy' ? '購入' : '売却' }}
|
||||||
@@ -71,17 +211,43 @@ const confirmCancel = () => {
|
|||||||
<span :class="[ui.statusBadge, order.status === '注文中' && ui.pendingBadge]">
|
<span :class="[ui.statusBadge, order.status === '注文中' && ui.pendingBadge]">
|
||||||
{{ order.status }}
|
{{ order.status }}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<span :class="ui.rowActions">
|
||||||
v-if="canCancel(order)"
|
<button
|
||||||
:class="ui.ghostButton"
|
v-if="canShowUsDetail(order)"
|
||||||
type="button"
|
:class="ui.ghostButton"
|
||||||
:disabled="Boolean(cancelingOrderKey)"
|
class="min-h-8 px-3 text-xs"
|
||||||
@click="askCancel(order)"
|
type="button"
|
||||||
>
|
:disabled="Boolean(orderDetailLoadingKey)"
|
||||||
<Spinner v-if="isCanceling(order, cancelingOrderKey)" size="sm" />
|
@click="showOrderDetail(order)"
|
||||||
<Ban v-else class="h-4 w-4" aria-hidden="true" />
|
>
|
||||||
{{ isCanceling(order, cancelingOrderKey) ? '取消中' : '取消' }}
|
<Spinner v-if="orderDetailLoadingKey === orderActionKey(order)" size="sm" />
|
||||||
</button>
|
<FileText v-else class="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
|
詳細
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="canCorrect(order)"
|
||||||
|
:class="ui.ghostButton"
|
||||||
|
class="min-h-8 px-3 text-xs"
|
||||||
|
type="button"
|
||||||
|
:disabled="Boolean(cancelingOrderKey)"
|
||||||
|
@click="askCorrection(order)"
|
||||||
|
>
|
||||||
|
<Pencil class="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
|
訂正
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="canCancel(order)"
|
||||||
|
:class="ui.ghostButton"
|
||||||
|
class="min-h-8 px-3 text-xs"
|
||||||
|
type="button"
|
||||||
|
:disabled="Boolean(cancelingOrderKey)"
|
||||||
|
@click="askCancel(order)"
|
||||||
|
>
|
||||||
|
<Spinner v-if="isCanceling(order, cancelingOrderKey)" size="sm" />
|
||||||
|
<Ban v-else class="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
|
{{ isCanceling(order, cancelingOrderKey) ? '取消中' : '取消' }}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="dataLoading && !orders.length" :class="[ui.muted, 'grid py-8 place-items-center']">
|
<div v-if="dataLoading && !orders.length" :class="[ui.muted, 'grid py-8 place-items-center']">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
@@ -98,6 +264,195 @@ const confirmCancel = () => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
<UiModal
|
||||||
|
v-if="orderDetail"
|
||||||
|
key="order-detail-dialog"
|
||||||
|
eyebrow="注文詳細"
|
||||||
|
:title="orderDetail.stock"
|
||||||
|
@close="orderDetail = null"
|
||||||
|
>
|
||||||
|
<dl :class="ui.confirmList">
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>注文番号</dt>
|
||||||
|
<dd>{{ orderDetail.orderNumber ?? '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>枝番</dt>
|
||||||
|
<dd>{{ orderDetail.orderSubNo ?? '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>銘柄</dt>
|
||||||
|
<dd>{{ orderDetail.code }} / {{ orderDetail.market }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>売買</dt>
|
||||||
|
<dd>{{ orderDetail.side === 'buy' ? '購入' : '売却' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>数量</dt>
|
||||||
|
<dd>{{ orderQuantityText(orderDetail) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>未約定数量</dt>
|
||||||
|
<dd>{{ orderDetail.unexecutedQuantity ?? '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>価格</dt>
|
||||||
|
<dd>{{ priceText(orderDetail.price, orderDetail.market) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>状態</dt>
|
||||||
|
<dd>{{ orderDetail.statusText ?? orderDetail.status }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>有効期限</dt>
|
||||||
|
<dd>{{ orderDetail.expiresAt ?? '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>取消/訂正</dt>
|
||||||
|
<dd>
|
||||||
|
{{ orderDetail.cancelable === false ? '取消不可' : '取消可' }} /
|
||||||
|
{{ orderDetail.correctable === false ? '訂正不可' : '訂正可' }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<div :class="ui.actions">
|
||||||
|
<UiButton variant="ghost" @click="orderDetail = null">
|
||||||
|
<ArrowLeft class="h-4 w-4" aria-hidden="true" />
|
||||||
|
閉じる
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</UiModal>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
<UiModal
|
||||||
|
v-if="showTradeRecords"
|
||||||
|
key="trade-records-dialog"
|
||||||
|
eyebrow="約定履歴"
|
||||||
|
title="米国株"
|
||||||
|
@close="showTradeRecords = false"
|
||||||
|
>
|
||||||
|
<div v-if="tradeRecords.length" :class="ui.confirmList">
|
||||||
|
<div v-for="record in tradeRecords" :key="record.id" :class="ui.row">
|
||||||
|
<span class="grid gap-1">
|
||||||
|
<strong>{{ record.stock }}</strong>
|
||||||
|
<small>{{ record.code }} / {{ record.market }} / {{ record.type }}</small>
|
||||||
|
<small :class="ui.muted">
|
||||||
|
{{ record.tradeDate ?? '-' }} 約定 / {{ record.valueDate ?? '-' }} 受渡
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
<span class="grid justify-items-end gap-1 text-right">
|
||||||
|
<strong>{{ tradeAmountText(record) }}</strong>
|
||||||
|
<small :class="ui.muted">
|
||||||
|
{{ record.quantity ?? '-' }}株 @ {{ priceText(record.price, record.market) }}
|
||||||
|
</small>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else :class="ui.dialogNote">該当する約定履歴はありません</p>
|
||||||
|
<div :class="ui.actions">
|
||||||
|
<UiButton variant="ghost" @click="showTradeRecords = false">
|
||||||
|
<ArrowLeft class="h-4 w-4" aria-hidden="true" />
|
||||||
|
閉じる
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</UiModal>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
<UiModal
|
||||||
|
v-if="correctionCandidate"
|
||||||
|
key="correction-dialog"
|
||||||
|
eyebrow="注文訂正"
|
||||||
|
:title="correctionCandidate.stock"
|
||||||
|
@close="correctionCandidate = null"
|
||||||
|
>
|
||||||
|
<dl :class="ui.confirmList">
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>注文番号</dt>
|
||||||
|
<dd>{{ correctionCandidate.orderNumber }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>現在数量</dt>
|
||||||
|
<dd>{{ orderQuantityText(correctionCandidate) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>現在価格</dt>
|
||||||
|
<dd>{{ priceText(correctionCandidate.price, correctionCandidate.market) }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<div class="grid gap-3">
|
||||||
|
<label :class="ui.label">
|
||||||
|
数量
|
||||||
|
<input
|
||||||
|
v-model="correctionQuantityInput"
|
||||||
|
:class="ui.input"
|
||||||
|
inputmode="decimal"
|
||||||
|
@input="correctionPreview = null"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label :class="ui.label">
|
||||||
|
執行条件
|
||||||
|
<select
|
||||||
|
v-model="correctionPriceCondition"
|
||||||
|
:class="ui.input"
|
||||||
|
@change="correctionPreview = null"
|
||||||
|
>
|
||||||
|
<option value="limit">指値</option>
|
||||||
|
<option value="market">成行</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label v-if="correctionPriceCondition === 'limit'" :class="ui.label">
|
||||||
|
指値
|
||||||
|
<input
|
||||||
|
v-model="correctionPriceInput"
|
||||||
|
:class="ui.input"
|
||||||
|
inputmode="decimal"
|
||||||
|
@input="correctionPreview = null"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<dl v-if="correctionPreview" :class="ui.confirmList">
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>見積数量</dt>
|
||||||
|
<dd>{{ correctionPreview.quantity }}株</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>見積金額</dt>
|
||||||
|
<dd>
|
||||||
|
{{
|
||||||
|
correctionPreview.price
|
||||||
|
? priceText(correctionPreview.price.value, correctionCandidate.market)
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<div :class="ui.actions">
|
||||||
|
<UiButton variant="ghost" @click="correctionCandidate = null">
|
||||||
|
<ArrowLeft class="h-4 w-4" aria-hidden="true" />
|
||||||
|
戻る
|
||||||
|
</UiButton>
|
||||||
|
<UiButton :disabled="correctionLoading" @click="estimateCorrection">
|
||||||
|
<Spinner v-if="correctionLoading" size="sm" />
|
||||||
|
<ShieldCheck v-else class="h-4 w-4" aria-hidden="true" />
|
||||||
|
見積
|
||||||
|
</UiButton>
|
||||||
|
<UiButton
|
||||||
|
variant="danger"
|
||||||
|
:disabled="!correctionPreview || correctionSubmitting"
|
||||||
|
@click="confirmCorrection"
|
||||||
|
>
|
||||||
|
<Spinner v-if="correctionSubmitting" size="sm" />
|
||||||
|
<Pencil v-else class="h-4 w-4" aria-hidden="true" />
|
||||||
|
訂正
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</UiModal>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<UiModal
|
<UiModal
|
||||||
v-if="cancelCandidate"
|
v-if="cancelCandidate"
|
||||||
|
|||||||
@@ -1,28 +1,30 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { AnimatePresence } from 'motion-v'
|
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 Spinner from '../../components/ui/Spinner.vue'
|
||||||
import UiButton from '../../components/ui/UiButton.vue'
|
import UiButton from '../../components/ui/UiButton.vue'
|
||||||
import UiModal from '../../components/ui/UiModal.vue'
|
import UiModal from '../../components/ui/UiModal.vue'
|
||||||
import { ui } from '../../styles/ui'
|
import { ui } from '../../styles/ui'
|
||||||
import type { OrderRow, Position } from '../../types/trading'
|
import type { MarketIndex, OrderRow, Position } from '../../types/trading'
|
||||||
import {
|
import {
|
||||||
currency,
|
currency,
|
||||||
currencyForMarket,
|
currencyForMarket,
|
||||||
|
number as formatNumber,
|
||||||
signedCurrency,
|
signedCurrency,
|
||||||
signedCurrencyForMarket,
|
signedCurrencyForMarket,
|
||||||
signedPercent,
|
signedPercent,
|
||||||
} from '../../utils/format'
|
} from '../../utils/format'
|
||||||
import { orderAmountText, orderHistoryKey, orderQuantityText } from '../trading/trading-data'
|
import { orderAmountText, orderHistoryKey, orderQuantityText } from '../trading/trading-data'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
showPortfolioSpinner: boolean
|
showPortfolioSpinner: boolean
|
||||||
totalAssetValue: number
|
totalAssetValue: number
|
||||||
buyingPower: number
|
buyingPower: number
|
||||||
holdingsMarketValue: number
|
holdingsMarketValue: number
|
||||||
totalProfitLoss: number
|
totalProfitLoss: number
|
||||||
totalProfitLossRate: number
|
totalProfitLossRate: number
|
||||||
|
marketIndexes: MarketIndex[]
|
||||||
stockAssetRatio: number
|
stockAssetRatio: number
|
||||||
cashAssetRatio: number
|
cashAssetRatio: number
|
||||||
positions: Position[]
|
positions: Position[]
|
||||||
@@ -32,6 +34,7 @@ defineProps<{
|
|||||||
connected: boolean
|
connected: boolean
|
||||||
orderHistoryLoaded: boolean
|
orderHistoryLoaded: boolean
|
||||||
orderHistoryNotice: string
|
orderHistoryNotice: string
|
||||||
|
loadPositionDetail: (position: Position) => Promise<Position>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -41,12 +44,47 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const usMarkets = new Set(['XNAS', 'XNYS', 'ARCX'])
|
const usMarkets = new Set(['XNAS', 'XNYS', 'ARCX'])
|
||||||
|
const isUsMarket = (market: string) => usMarkets.has(market)
|
||||||
const canCancel = (order: OrderRow) =>
|
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) =>
|
const isCanceling = (order: OrderRow, cancelingOrderKey: string) =>
|
||||||
orderHistoryKey(order) === cancelingOrderKey
|
orderHistoryKey(order) === cancelingOrderKey
|
||||||
const cancelCandidate = ref<OrderRow | null>(null)
|
const cancelCandidate = ref<OrderRow | null>(null)
|
||||||
|
const positionDetail = ref<Position | null>(null)
|
||||||
|
const positionDetailLoadingKey = ref('')
|
||||||
|
const positionDetailError = ref('')
|
||||||
const cancelTitle = computed(() => cancelCandidate.value?.stock ?? '')
|
const 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) => {
|
const askCancel = (order: OrderRow) => {
|
||||||
if (!canCancel(order)) return
|
if (!canCancel(order)) return
|
||||||
@@ -69,6 +107,13 @@ const confirmCancel = () => {
|
|||||||
<template v-if="!showPortfolioSpinner">{{ currency(totalAssetValue) }}</template>
|
<template v-if="!showPortfolioSpinner">{{ currency(totalAssetValue) }}</template>
|
||||||
<Spinner v-else size="lg" />
|
<Spinner v-else size="lg" />
|
||||||
</strong>
|
</strong>
|
||||||
|
<small :class="[ui.assetOverviewSubtext, totalProfitLoss >= 0 ? ui.positive : ui.negative]">
|
||||||
|
通算評価損益:
|
||||||
|
<template v-if="!showPortfolioSpinner">
|
||||||
|
{{ signedCurrency(totalProfitLoss) }} · {{ signedPercent(totalProfitLossRate) }}
|
||||||
|
</template>
|
||||||
|
<Spinner v-else size="sm" />
|
||||||
|
</small>
|
||||||
<small :class="ui.assetOverviewSubtext">
|
<small :class="ui.assetOverviewSubtext">
|
||||||
余力:
|
余力:
|
||||||
<template v-if="!showPortfolioSpinner">{{ currency(buyingPower) }}</template>
|
<template v-if="!showPortfolioSpinner">{{ currency(buyingPower) }}</template>
|
||||||
@@ -120,17 +165,6 @@ const confirmCancel = () => {
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<article :class="ui.metricPanel">
|
|
||||||
<span :class="ui.metricLabel">通算評価損益</span>
|
|
||||||
<strong :class="[ui.metricValue, totalProfitLoss >= 0 ? ui.positive : ui.negative]">
|
|
||||||
<template v-if="!showPortfolioSpinner">
|
|
||||||
{{ signedCurrency(totalProfitLoss) }}
|
|
||||||
<small> · {{ signedPercent(totalProfitLossRate) }}</small>
|
|
||||||
</template>
|
|
||||||
<Spinner v-else size="lg" />
|
|
||||||
</strong>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article :class="ui.holdingsPanel">
|
<article :class="ui.holdingsPanel">
|
||||||
<div :class="ui.panelHead">
|
<div :class="ui.panelHead">
|
||||||
<h2>保有銘柄</h2>
|
<h2>保有銘柄</h2>
|
||||||
@@ -151,19 +185,18 @@ const confirmCancel = () => {
|
|||||||
<span>数量</span>
|
<span>数量</span>
|
||||||
<span>評価額</span>
|
<span>評価額</span>
|
||||||
<span>評価損益</span>
|
<span>評価損益</span>
|
||||||
|
<span>操作</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="positions.length" :class="ui.holdingsRows">
|
<div v-if="positions.length" :class="ui.holdingsRows">
|
||||||
<button
|
<div v-for="position in positions" :key="positionKey(position)" :class="ui.holdingRow">
|
||||||
v-for="position in positions"
|
<button
|
||||||
:key="position.code"
|
class="grid gap-1 text-left text-[#e3e3e9]"
|
||||||
:class="ui.holdingRow"
|
type="button"
|
||||||
type="button"
|
@click="emit('openPosition', position.code)"
|
||||||
@click="emit('openPosition', position.code)"
|
>
|
||||||
>
|
|
||||||
<span class="grid gap-1">
|
|
||||||
<strong>{{ position.name }}</strong>
|
<strong>{{ position.name }}</strong>
|
||||||
<small>{{ position.code }}</small>
|
<small>{{ position.code }}</small>
|
||||||
</span>
|
</button>
|
||||||
<b :class="ui.typePill">{{
|
<b :class="ui.typePill">{{
|
||||||
position.type ?? (position.quantity >= 100 ? '単元' : 'S株')
|
position.type ?? (position.quantity >= 100 ? '単元' : 'S株')
|
||||||
}}</b>
|
}}</b>
|
||||||
@@ -178,12 +211,27 @@ const confirmCancel = () => {
|
|||||||
<strong>{{ signedCurrencyForMarket(position.profitLoss, position.market) }}</strong>
|
<strong>{{ signedCurrencyForMarket(position.profitLoss, position.market) }}</strong>
|
||||||
<small>{{ signedPercent(position.profitLossRate) }}</small>
|
<small>{{ signedPercent(position.profitLossRate) }}</small>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
<span :class="ui.rowActions">
|
||||||
|
<button
|
||||||
|
v-if="canLoadPositionDetail(position)"
|
||||||
|
:class="ui.ghostButton"
|
||||||
|
class="min-h-8 px-3 text-xs"
|
||||||
|
type="button"
|
||||||
|
:disabled="Boolean(positionDetailLoadingKey)"
|
||||||
|
@click="showPositionDetail(position)"
|
||||||
|
>
|
||||||
|
<Spinner v-if="positionDetailLoadingKey === positionKey(position)" size="sm" />
|
||||||
|
<FileText v-else class="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
|
詳細
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="dataLoading" :class="[ui.muted, ui.emptyState]">
|
<div v-else-if="dataLoading" :class="[ui.muted, ui.emptyState]">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
<p v-else :class="[ui.muted, ui.emptyState]">SBIに接続すると保有銘柄を表示します</p>
|
<p v-else :class="[ui.muted, ui.emptyState]">SBIに接続すると保有銘柄を表示します</p>
|
||||||
|
<p v-if="positionDetailError" :class="ui.dialogNote">{{ positionDetailError }}</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
@@ -234,8 +282,85 @@ const confirmCancel = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article :class="ui.marketIndexPanel">
|
||||||
|
<div :class="ui.panelHead">
|
||||||
|
<h2>指数</h2>
|
||||||
|
</div>
|
||||||
|
<div v-if="marketIndexes.length" :class="ui.marketIndexGrid">
|
||||||
|
<div
|
||||||
|
v-for="index in marketIndexes"
|
||||||
|
:key="index.code ?? index.name"
|
||||||
|
:class="ui.marketIndexCard"
|
||||||
|
>
|
||||||
|
<span :class="ui.metricLabel">{{ index.name }}</span>
|
||||||
|
<strong :class="ui.marketIndexValue">{{ indexValueText(index) }}</strong>
|
||||||
|
<small :class="indexTone(index)">{{ indexChangeText(index) }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="dataLoading" :class="[ui.muted, ui.emptyState]">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
<p v-else :class="[ui.muted, ui.emptyState]">SBIに接続すると指数を表示します</p>
|
||||||
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
<UiModal
|
||||||
|
v-if="positionDetail"
|
||||||
|
key="position-detail-dialog"
|
||||||
|
eyebrow="保有詳細"
|
||||||
|
:title="positionDetail.name"
|
||||||
|
@close="positionDetail = null"
|
||||||
|
>
|
||||||
|
<dl :class="ui.confirmList">
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>銘柄</dt>
|
||||||
|
<dd>{{ positionDetail.code }} / {{ positionDetail.market }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>預り区分</dt>
|
||||||
|
<dd>{{ positionDetail.type ?? positionDetail.accountType ?? '-' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>数量</dt>
|
||||||
|
<dd>{{ positionDetail.quantity }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>現在値</dt>
|
||||||
|
<dd>
|
||||||
|
{{
|
||||||
|
positionDetail.currentPrice == null
|
||||||
|
? '-'
|
||||||
|
: currencyForMarket(positionDetail.currentPrice, positionDetail.market)
|
||||||
|
}}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>平均取得単価</dt>
|
||||||
|
<dd>{{ currencyForMarket(positionDetail.avgPrice, positionDetail.market) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>評価額</dt>
|
||||||
|
<dd>{{ currencyForMarket(positionDetail.marketValue, positionDetail.market) }}</dd>
|
||||||
|
</div>
|
||||||
|
<div :class="ui.confirmRow">
|
||||||
|
<dt>評価損益</dt>
|
||||||
|
<dd :class="positionDetail.profitLoss >= 0 ? ui.positive : ui.negative">
|
||||||
|
{{ signedCurrencyForMarket(positionDetail.profitLoss, positionDetail.market) }} /
|
||||||
|
{{ signedPercent(positionDetail.profitLossRate) }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<div :class="ui.actions">
|
||||||
|
<UiButton variant="ghost" @click="positionDetail = null">
|
||||||
|
<ArrowLeft class="h-4 w-4" aria-hidden="true" />
|
||||||
|
閉じる
|
||||||
|
</UiButton>
|
||||||
|
</div>
|
||||||
|
</UiModal>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
<UiModal
|
<UiModal
|
||||||
v-if="cancelCandidate"
|
v-if="cancelCandidate"
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
import type {
|
import type {
|
||||||
ChartNotice,
|
ChartNotice,
|
||||||
|
MarketIndex,
|
||||||
|
OrderDetail,
|
||||||
OrderPreview,
|
OrderPreview,
|
||||||
OrderRow,
|
OrderRow,
|
||||||
Position,
|
Position,
|
||||||
RealtimePricePoint,
|
RealtimePricePoint,
|
||||||
Stock,
|
Stock,
|
||||||
|
TradeRecordRow,
|
||||||
} from '../../types/trading'
|
} from '../../types/trading'
|
||||||
import { currency } from '../../utils/format'
|
import { currency } from '../../utils/format'
|
||||||
|
|
||||||
export type RecordLike = Record<string, unknown>
|
export type RecordLike = Record<string, unknown>
|
||||||
|
type IssueLike = {
|
||||||
|
code: string
|
||||||
|
market: string
|
||||||
|
name: string
|
||||||
|
searchText?: string
|
||||||
|
}
|
||||||
|
|
||||||
export const emptyStock: Stock = {
|
export const emptyStock: Stock = {
|
||||||
code: '',
|
code: '',
|
||||||
name: '未選択',
|
name: '未選択',
|
||||||
symbol: '',
|
symbol: '',
|
||||||
|
searchText: '',
|
||||||
country: '日本',
|
country: '日本',
|
||||||
market: '',
|
market: '',
|
||||||
sector: '',
|
sector: '',
|
||||||
@@ -75,13 +85,27 @@ const countryFromMarket = (market: string) =>
|
|||||||
|
|
||||||
export const issueFrom = (value: unknown) => {
|
export const issueFrom = (value: unknown) => {
|
||||||
const issue = asRecord(value)
|
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 {
|
return {
|
||||||
code: textValue(issue.code, textValue(issue.issueCode)),
|
code,
|
||||||
market: textValue(issue.market, textValue(issue.marketCode, '')),
|
market,
|
||||||
name: textValue(
|
name,
|
||||||
issue.name,
|
searchText,
|
||||||
textValue(issue.issueName, textValue(issue.stockName, textValue(issue.displayName))),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) }
|
return { min: at(0), q1: at(0.25), median: at(0.5), q3: at(0.75), max: at(1) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const stockFromIssue = (issue: ReturnType<typeof issueFrom>): Stock => ({
|
export const stockFromIssue = (issue: IssueLike): Stock => ({
|
||||||
code: issue.code,
|
code: issue.code,
|
||||||
name: issue.name || issue.code,
|
name: issue.name || issue.code,
|
||||||
symbol: issue.code ? issueSymbol(issue.code, issue.market) : '',
|
symbol: issue.code ? issueSymbol(issue.code, issue.market) : '',
|
||||||
|
searchText: issue.searchText,
|
||||||
country: countryFromMarket(issue.market),
|
country: countryFromMarket(issue.market),
|
||||||
market: issue.market,
|
market: issue.market,
|
||||||
sector: '',
|
sector: '',
|
||||||
@@ -146,10 +171,16 @@ export const stockFromPosition = (position: Position): Stock => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const stockFromBoard = (
|
const accountTypeLabels: Record<string, string> = {
|
||||||
value: unknown,
|
specific: '特定',
|
||||||
fallbackIssue?: ReturnType<typeof issueFrom>,
|
general: '一般',
|
||||||
): Stock => {
|
growthInvestment: 'NISA成長投資枠',
|
||||||
|
nisa: 'NISA',
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountTypeLabel = (value: string) => accountTypeLabels[value] ?? value
|
||||||
|
|
||||||
|
export const stockFromBoard = (value: unknown, fallbackIssue?: IssueLike): Stock => {
|
||||||
const board = asRecord(value)
|
const board = asRecord(value)
|
||||||
const quoteRecord = asRecord(board.quote)
|
const quoteRecord = asRecord(board.quote)
|
||||||
const boardIssue = issueFrom(board.issue)
|
const boardIssue = issueFrom(board.issue)
|
||||||
@@ -158,6 +189,7 @@ export const stockFromBoard = (
|
|||||||
code: quoteIssue.code || boardIssue.code || fallbackIssue?.code || '',
|
code: quoteIssue.code || boardIssue.code || fallbackIssue?.code || '',
|
||||||
market: quoteIssue.market || boardIssue.market || fallbackIssue?.market || '',
|
market: quoteIssue.market || boardIssue.market || fallbackIssue?.market || '',
|
||||||
name: quoteIssue.name || boardIssue.name || fallbackIssue?.name || '',
|
name: quoteIssue.name || boardIssue.name || fallbackIssue?.name || '',
|
||||||
|
searchText: quoteIssue.searchText || boardIssue.searchText || fallbackIssue?.searchText || '',
|
||||||
}
|
}
|
||||||
const quote = quoteFrom(quoteRecord)
|
const quote = quoteFrom(quoteRecord)
|
||||||
const codeAsNumber = Number(issue.code)
|
const codeAsNumber = Number(issue.code)
|
||||||
@@ -169,6 +201,7 @@ export const stockFromBoard = (
|
|||||||
code: issue.code,
|
code: issue.code,
|
||||||
name: issue.name,
|
name: issue.name,
|
||||||
symbol: issue.code ? issueSymbol(issue.code, issue.market) : '',
|
symbol: issue.code ? issueSymbol(issue.code, issue.market) : '',
|
||||||
|
searchText: issue.searchText,
|
||||||
country: countryFromMarket(issue.market),
|
country: countryFromMarket(issue.market),
|
||||||
market: issue.market,
|
market: issue.market,
|
||||||
sector: '',
|
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) =>
|
const zonedPartsFormatter = (timeZone: string) =>
|
||||||
new Intl.DateTimeFormat('en-US', {
|
new Intl.DateTimeFormat('en-US', {
|
||||||
timeZone,
|
timeZone,
|
||||||
@@ -357,16 +437,20 @@ export const positionFromApi = (value: unknown): Position | null => {
|
|||||||
const costBasis = avgPrice * quantity
|
const costBasis = avgPrice * quantity
|
||||||
const profitLossRate =
|
const profitLossRate =
|
||||||
numberValue(item.profitLossRate) || (costBasis ? (profitLoss / costBasis) * 100 : 0)
|
numberValue(item.profitLossRate) || (costBasis ? (profitLoss / costBasis) * 100 : 0)
|
||||||
|
const accountType = textValue(item.accountType, textValue(item.depositType))
|
||||||
return {
|
return {
|
||||||
code: issue.code,
|
code: issue.code,
|
||||||
name: issue.name,
|
name: issue.name,
|
||||||
market: issue.market,
|
market: issue.market,
|
||||||
quantity,
|
quantity,
|
||||||
avgPrice,
|
avgPrice,
|
||||||
|
currentPrice: nullableNumberValue(item.currentPrice),
|
||||||
marketValue: numberValue(item.marketValue ?? item.valuationPrice),
|
marketValue: numberValue(item.marketValue ?? item.valuationPrice),
|
||||||
profitLoss,
|
profitLoss,
|
||||||
profitLossRate,
|
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))
|
: (nullableNumberValue(item.price) ?? nullableNumberValue(item.executedPrice))
|
||||||
return {
|
return {
|
||||||
id: textValue(item.id, textValue(item.orderNumber, `${issue.code}-${item.orderedAt ?? ''}`)),
|
id: textValue(item.id, textValue(item.orderNumber, `${issue.code}-${item.orderedAt ?? ''}`)),
|
||||||
|
code: issue.code,
|
||||||
date: textValue(item.orderedAt, textValue(item.expiresAt)),
|
date: textValue(item.orderedAt, textValue(item.expiresAt)),
|
||||||
stock: issue.name || issue.code,
|
stock: issue.name || issue.code,
|
||||||
market: issue.market,
|
market: issue.market,
|
||||||
side,
|
side,
|
||||||
kind,
|
kind,
|
||||||
quantity,
|
quantity,
|
||||||
|
unexecutedQuantity,
|
||||||
|
executedQuantity,
|
||||||
price,
|
price,
|
||||||
status,
|
status,
|
||||||
orderNumber: textValue(item.orderNumber),
|
orderNumber: textValue(item.orderNumber),
|
||||||
|
orderSubNo: textValue(item.orderSubNo),
|
||||||
tradeId: textValue(item.tradeId),
|
tradeId: textValue(item.tradeId),
|
||||||
|
accountType: textValue(item.accountType, textValue(item.depositType)),
|
||||||
|
cancelable: typeof item.cancelable === 'boolean' ? item.cancelable : undefined,
|
||||||
|
correctable: typeof item.correctable === 'boolean' ? item.correctable : undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const orderDetailFromApi = (value: unknown): OrderDetail | null => {
|
||||||
|
const row = orderFromApi(value)
|
||||||
|
if (!row) return null
|
||||||
|
const item = asRecord(value)
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
expiresAt: textValue(item.expiresAt) || undefined,
|
||||||
|
statusText: textValue(item.statusText) || undefined,
|
||||||
|
depositType: textValue(item.depositType) || undefined,
|
||||||
|
accountInformation: textValue(item.accountInformation) || undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tradeRecordFromApi = (value: unknown): TradeRecordRow | null => {
|
||||||
|
const item = asRecord(value)
|
||||||
|
const issue = issueFrom(item.issue)
|
||||||
|
if (!issue.code) return null
|
||||||
|
const tradeDate = textValue(item.tradeDate)
|
||||||
|
const valueDate = textValue(item.valueDate)
|
||||||
|
const type = textValue(item.tradeRecordTypeCode, textValue(item.type, '取引'))
|
||||||
|
return {
|
||||||
|
id: textValue(item.id, [issue.code, tradeDate, valueDate, type].filter(Boolean).join(':')),
|
||||||
|
code: issue.code,
|
||||||
|
stock: issue.name || issue.code,
|
||||||
|
market: issue.market,
|
||||||
|
type,
|
||||||
|
quantity: nullableNumberValue(item.quantity),
|
||||||
|
price: nullableNumberValue(item.price),
|
||||||
|
amount: nullableNumberValue(item.amount),
|
||||||
|
tradeDate: tradeDate || undefined,
|
||||||
|
valueDate: valueDate || undefined,
|
||||||
|
accountType: textValue(item.accountType) || undefined,
|
||||||
|
settlementCurrencyCode: textValue(item.settlementCurrencyCode) || undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import type {
|
|||||||
CashOrderTerm,
|
CashOrderTerm,
|
||||||
CashOrderTriggerZone,
|
CashOrderTriggerZone,
|
||||||
JsonRpcResponse,
|
JsonRpcResponse,
|
||||||
|
MarketIndex,
|
||||||
|
OrderDetail,
|
||||||
OrderKind,
|
OrderKind,
|
||||||
OrderPreview,
|
OrderPreview,
|
||||||
OrderRow,
|
OrderRow,
|
||||||
@@ -24,6 +26,7 @@ import type {
|
|||||||
RealtimePricePoint,
|
RealtimePricePoint,
|
||||||
RpcMessage,
|
RpcMessage,
|
||||||
Stock,
|
Stock,
|
||||||
|
TradeRecordRow,
|
||||||
TradeSide,
|
TradeSide,
|
||||||
} from '../../types/trading'
|
} from '../../types/trading'
|
||||||
import {
|
import {
|
||||||
@@ -34,7 +37,9 @@ import {
|
|||||||
isOrderPreview,
|
isOrderPreview,
|
||||||
issueFrom,
|
issueFrom,
|
||||||
marketDateKey,
|
marketDateKey,
|
||||||
|
marketIndexFromApi,
|
||||||
numberValue,
|
numberValue,
|
||||||
|
orderDetailFromApi,
|
||||||
orderFromApi,
|
orderFromApi,
|
||||||
orderHistoryKey,
|
orderHistoryKey,
|
||||||
orderHistoryResultNotice,
|
orderHistoryResultNotice,
|
||||||
@@ -44,6 +49,7 @@ import {
|
|||||||
stockFromBoard,
|
stockFromBoard,
|
||||||
stockFromIssue,
|
stockFromIssue,
|
||||||
stockFromPosition,
|
stockFromPosition,
|
||||||
|
tradeRecordFromApi,
|
||||||
textValue,
|
textValue,
|
||||||
type RecordLike,
|
type RecordLike,
|
||||||
} from './trading-data'
|
} from './trading-data'
|
||||||
@@ -157,6 +163,8 @@ const displayApkOrderTermDate = (value: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const usMarkets = new Set(['XNAS', 'XNYS', 'ARCX'])
|
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 parseApkExchangeMarkets = (value: string) => {
|
||||||
const markets: CashOrderMarket[] = []
|
const markets: CashOrderMarket[] = []
|
||||||
@@ -212,6 +220,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
const holdingsMarketValue = ref(0)
|
const holdingsMarketValue = ref(0)
|
||||||
const totalProfitLoss = ref(0)
|
const totalProfitLoss = ref(0)
|
||||||
const totalProfitLossRate = ref(0)
|
const totalProfitLossRate = ref(0)
|
||||||
|
const marketIndexes = ref<MarketIndex[]>([])
|
||||||
const orders = ref<OrderRow[]>([])
|
const orders = ref<OrderRow[]>([])
|
||||||
const cancelingOrderKey = ref('')
|
const cancelingOrderKey = ref('')
|
||||||
const orderHistoryLoaded = ref(false)
|
const orderHistoryLoaded = ref(false)
|
||||||
@@ -522,7 +531,8 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
const matchesQuery =
|
const matchesQuery =
|
||||||
stock.name.toLowerCase().includes(query) ||
|
stock.name.toLowerCase().includes(query) ||
|
||||||
stock.code.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)
|
return matchesQuery && matchesFilters(stock)
|
||||||
})
|
})
|
||||||
: viewedStocks.value.filter(matchesFilters)
|
: viewedStocks.value.filter(matchesFilters)
|
||||||
@@ -753,6 +763,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
price: stock.price || current?.price || 0,
|
price: stock.price || current?.price || 0,
|
||||||
change: stock.change || current?.change || 0,
|
change: stock.change || current?.change || 0,
|
||||||
changeAmount: stock.changeAmount || current?.changeAmount || 0,
|
changeAmount: stock.changeAmount || current?.changeAmount || 0,
|
||||||
|
searchText: stock.searchText || current?.searchText || '',
|
||||||
history: stock.price ? stock.history : (current?.history ?? stock.history),
|
history: stock.price ? stock.history : (current?.history ?? stock.history),
|
||||||
box: stock.price ? stock.box : (current?.box ?? stock.box),
|
box: stock.price ? stock.box : (current?.box ?? stock.box),
|
||||||
})
|
})
|
||||||
@@ -771,16 +782,19 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
const loadOrderHistoryFromSdk = async () => {
|
const loadOrderHistoryFromSdk = async () => {
|
||||||
orderHistoryLoaded.value = false
|
orderHistoryLoaded.value = false
|
||||||
orderHistoryNotice.value = ''
|
orderHistoryNotice.value = ''
|
||||||
const [openOrdersResult, executionsTodayResult] = await Promise.allSettled([
|
const orderResults = await Promise.allSettled([
|
||||||
rpcCallOptional<RecordLike>('orders.inquiry.open'),
|
rpcCallOptional<RecordLike>('orders.inquiry.open'),
|
||||||
rpcCallOptional<RecordLike>('orders.inquiry.executionsToday'),
|
rpcCallOptional<RecordLike>('orders.inquiry.executionsToday'),
|
||||||
rpcCallOptional<RecordLike>('orders.inquiry.open', { market: 'XNAS' }),
|
...usOrderMarkets.flatMap((market) => [
|
||||||
rpcCallOptional<RecordLike>('orders.inquiry.executionsToday', { market: 'XNAS' }),
|
rpcCallOptional<RecordLike>('orders.inquiry.open', { market }),
|
||||||
|
rpcCallOptional<RecordLike>('orders.inquiry.executionsToday', { market }),
|
||||||
|
]),
|
||||||
])
|
])
|
||||||
|
const [openOrdersResult, executionsTodayResult] = orderResults
|
||||||
if (openOrdersResult.status === 'rejected' && executionsTodayResult.status === 'rejected') {
|
if (openOrdersResult.status === 'rejected' && executionsTodayResult.status === 'rejected') {
|
||||||
throw openOrdersResult.reason
|
throw openOrdersResult.reason
|
||||||
}
|
}
|
||||||
const nextOrders = fulfilledValues([openOrdersResult, executionsTodayResult])
|
const nextOrders = fulfilledValues(orderResults)
|
||||||
.flatMap((orderList) => asArray(orderList.orders))
|
.flatMap((orderList) => asArray(orderList.orders))
|
||||||
.map(orderFromApi)
|
.map(orderFromApi)
|
||||||
.filter((order): order is OrderRow => Boolean(order))
|
.filter((order): order is OrderRow => Boolean(order))
|
||||||
@@ -790,9 +804,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
orders.value = [...deduped.values()]
|
orders.value = [...deduped.values()]
|
||||||
orderHistoryLoaded.value = true
|
orderHistoryLoaded.value = true
|
||||||
if (!orders.value.length) {
|
if (!orders.value.length) {
|
||||||
const notices = fulfilledValues([openOrdersResult, executionsTodayResult])
|
const notices = fulfilledValues(orderResults).map(orderHistoryResultNotice).filter(Boolean)
|
||||||
.map(orderHistoryResultNotice)
|
|
||||||
.filter(Boolean)
|
|
||||||
orderHistoryNotice.value = [...new Set(notices)].join(' / ')
|
orderHistoryNotice.value = [...new Set(notices)].join(' / ')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -822,11 +834,25 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
const loadTradingData = async () => {
|
const loadTradingData = async () => {
|
||||||
dataLoading.value = true
|
dataLoading.value = true
|
||||||
try {
|
try {
|
||||||
const [assetsResult, ...positionResults] = await Promise.allSettled([
|
const [assetsResult, indexResult, ...positionResults] = await Promise.allSettled([
|
||||||
rpcCallOptional<RecordLike>('account.assets.current', undefined, 20_000),
|
rpcCallOptional<RecordLike>('account.assets.current', undefined, 20_000),
|
||||||
|
rpcCallOptional<unknown[]>('market.index.major', undefined, 15_000),
|
||||||
rpcCallOptional<RecordLike>('account.positions.cash', undefined, 15_000),
|
rpcCallOptional<RecordLike>('account.positions.cash', undefined, 15_000),
|
||||||
rpcCallOptional<RecordLike>('account.positions.cash', { market: 'XNAS' }, 15_000),
|
...usOrderMarkets.map((market) =>
|
||||||
|
rpcCallOptional<RecordLike>('account.positions.cash', { market }, 15_000),
|
||||||
|
),
|
||||||
])
|
])
|
||||||
|
if (indexResult.status === 'fulfilled') {
|
||||||
|
marketIndexes.value = asArray(indexResult.value)
|
||||||
|
.map(marketIndexFromApi)
|
||||||
|
.filter((index): index is MarketIndex => Boolean(index))
|
||||||
|
} else {
|
||||||
|
marketIndexes.value = []
|
||||||
|
reportDataError(
|
||||||
|
errorMessage(indexResult.reason, '指数の取得に失敗しました'),
|
||||||
|
indexResult.reason,
|
||||||
|
)
|
||||||
|
}
|
||||||
const cashPositionLists = fulfilledValues(positionResults)
|
const cashPositionLists = fulfilledValues(positionResults)
|
||||||
const cashPositions = cashPositionLists[0] ?? {}
|
const cashPositions = cashPositionLists[0] ?? {}
|
||||||
const nextPositions = cashPositionLists
|
const nextPositions = cashPositionLists
|
||||||
@@ -835,13 +861,26 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
.filter((position): position is Position => Boolean(position))
|
.filter((position): position is Position => Boolean(position))
|
||||||
positions.value = nextPositions
|
positions.value = nextPositions
|
||||||
mergeStocks(nextPositions.map(stockFromPosition))
|
mergeStocks(nextPositions.map(stockFromPosition))
|
||||||
const nextHoldingsMarketValue = numberValue(
|
const summedHoldingsMarketValue = nextPositions.reduce(
|
||||||
cashPositions.totalMarketValue,
|
(sum, position) => sum + position.marketValue,
|
||||||
nextPositions.reduce((sum, position) => sum + position.marketValue, 0),
|
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
|
holdingsMarketValue.value = nextHoldingsMarketValue
|
||||||
totalProfitLoss.value = numberValue(cashPositions.totalProfitLoss)
|
totalProfitLoss.value =
|
||||||
totalProfitLossRate.value = numberValue(cashPositions.totalProfitLossRate)
|
nextPositions.length > 0 ? summedProfitLoss : numberValue(cashPositions.totalProfitLoss)
|
||||||
|
totalProfitLossRate.value =
|
||||||
|
nextPositions.length > 0 && summedCostBasis
|
||||||
|
? (summedProfitLoss / summedCostBasis) * 100
|
||||||
|
: numberValue(cashPositions.totalProfitLossRate)
|
||||||
|
|
||||||
const hasAccountAssets = assetsResult.status === 'fulfilled'
|
const hasAccountAssets = assetsResult.status === 'fulfilled'
|
||||||
if (hasAccountAssets) {
|
if (hasAccountAssets) {
|
||||||
@@ -1011,7 +1050,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchIssues = async (query: string) => {
|
const suggestIssues = async (query: string) => {
|
||||||
if (!connected.value || query.trim().length < 2) return
|
if (!connected.value || query.trim().length < 2) return
|
||||||
const marketsToSearch =
|
const marketsToSearch =
|
||||||
marketFilter.value !== 'all'
|
marketFilter.value !== 'all'
|
||||||
@@ -1019,7 +1058,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
: searchableMarkets.filter((market) => market !== 'auto')
|
: searchableMarkets.filter((market) => market !== 'auto')
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
marketsToSearch.map((market) =>
|
marketsToSearch.map((market) =>
|
||||||
rpcCall<RecordLike>('market.issue.search', { query, market, limit: 12 }),
|
rpcCall<RecordLike>('market.issue.suggest', { query, market, limit: 12 }),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
const issues = fulfilledValues(results)
|
const issues = fulfilledValues(results)
|
||||||
@@ -1121,6 +1160,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
orders.value = [
|
orders.value = [
|
||||||
{
|
{
|
||||||
id: textValue(receipt.orderId, `ord-${Date.now()}`),
|
id: textValue(receipt.orderId, `ord-${Date.now()}`),
|
||||||
|
code: selectedStock.value.code,
|
||||||
date: textValue(receipt.acceptedAt, new Date().toLocaleString('ja-JP')),
|
date: textValue(receipt.acceptedAt, new Date().toLocaleString('ja-JP')),
|
||||||
stock: selectedStock.value.name,
|
stock: selectedStock.value.name,
|
||||||
market: selectedStock.value.market,
|
market: selectedStock.value.market,
|
||||||
@@ -1147,7 +1187,9 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
orderNumber: order.orderNumber,
|
orderNumber: order.orderNumber,
|
||||||
orderId: order.id,
|
orderId: order.orderSubNo || order.id,
|
||||||
|
issueCode: order.code,
|
||||||
|
market: order.market,
|
||||||
tradeId: order.tradeId || undefined,
|
tradeId: order.tradeId || undefined,
|
||||||
}
|
}
|
||||||
await rpcCall('orders.cash.placeCancel', {
|
await rpcCall('orders.cash.placeCancel', {
|
||||||
@@ -1163,6 +1205,85 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadOrderDetail = async (order: OrderRow): Promise<OrderDetail> => {
|
||||||
|
if (!isUsMarket(order.market)) {
|
||||||
|
throw new Error('注文詳細は米国株のみ対応しています')
|
||||||
|
}
|
||||||
|
if (!order.orderNumber && !order.id && !order.orderSubNo) {
|
||||||
|
throw new Error('注文番号を取得できないため詳細を取得できません')
|
||||||
|
}
|
||||||
|
const detail = await rpcCall<RecordLike>('orders.inquiry.detail', {
|
||||||
|
orderNumber: order.orderNumber,
|
||||||
|
orderId: order.orderSubNo || order.id,
|
||||||
|
issueCode: order.code,
|
||||||
|
market: order.market,
|
||||||
|
})
|
||||||
|
const parsed = orderDetailFromApi(detail)
|
||||||
|
if (!parsed) throw new Error('注文詳細を読み取れませんでした')
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadTradeRecords = async (): Promise<TradeRecordRow[]> => {
|
||||||
|
const result = await rpcCall<RecordLike>('orders.inquiry.tradeRecords', { limit: 50 })
|
||||||
|
return asArray(result.records)
|
||||||
|
.map(tradeRecordFromApi)
|
||||||
|
.filter((record): record is TradeRecordRow => Boolean(record))
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadPositionDetail = async (position: Position): Promise<Position> => {
|
||||||
|
if (!isUsMarket(position.market)) {
|
||||||
|
throw new Error('保有詳細は米国株のみ対応しています')
|
||||||
|
}
|
||||||
|
const detail = await rpcCall<RecordLike>('account.positions.cashDetail', {
|
||||||
|
issueCode: position.code,
|
||||||
|
market: position.market,
|
||||||
|
accountType: position.accountType,
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
const parsed = asArray(detail.positions).map(positionFromApi)[0]
|
||||||
|
if (!parsed) throw new Error('保有詳細を読み取れませんでした')
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderCorrectionParams = (
|
||||||
|
order: OrderRow,
|
||||||
|
draft: { quantity: number; priceCondition: 'market' | 'limit'; price?: number },
|
||||||
|
) => ({
|
||||||
|
orderNumber: order.orderNumber,
|
||||||
|
orderId: order.orderSubNo || order.id,
|
||||||
|
issueCode: order.code,
|
||||||
|
market: order.market,
|
||||||
|
quantity: draft.quantity,
|
||||||
|
priceCondition: draft.priceCondition,
|
||||||
|
price: draft.priceCondition === 'limit' ? draft.price : undefined,
|
||||||
|
orderMethod: 'normal',
|
||||||
|
})
|
||||||
|
|
||||||
|
const estimateOrderCorrection = async (
|
||||||
|
order: OrderRow,
|
||||||
|
draft: { quantity: number; priceCondition: 'market' | 'limit'; price?: number },
|
||||||
|
): Promise<OrderPreview> => {
|
||||||
|
if (!isUsMarket(order.market)) throw new Error('注文訂正は米国株のみ対応しています')
|
||||||
|
const preview = await rpcCall<unknown>(
|
||||||
|
'orders.cash.estimateCorrection',
|
||||||
|
orderCorrectionParams(order, draft),
|
||||||
|
)
|
||||||
|
if (!isOrderPreview(preview)) throw new Error('注文訂正の見積を読み取れませんでした')
|
||||||
|
return preview
|
||||||
|
}
|
||||||
|
|
||||||
|
const placeOrderCorrection = async (
|
||||||
|
order: OrderRow,
|
||||||
|
draft: { quantity: number; priceCondition: 'market' | 'limit'; price?: number },
|
||||||
|
) => {
|
||||||
|
if (!isUsMarket(order.market)) throw new Error('注文訂正は米国株のみ対応しています')
|
||||||
|
await rpcCall('orders.cash.placeCorrection', {
|
||||||
|
...orderCorrectionParams(order, draft),
|
||||||
|
allowTrading: true,
|
||||||
|
})
|
||||||
|
await loadTradingData()
|
||||||
|
}
|
||||||
|
|
||||||
const downloadCsv = () => {
|
const downloadCsv = () => {
|
||||||
const header = ['code', 'name', 'symbol', 'market', 'price'].join(',')
|
const header = ['code', 'name', 'symbol', 'market', 'price'].join(',')
|
||||||
const rows = selectedStock.value.history.map((price) =>
|
const rows = selectedStock.value.history.map((price) =>
|
||||||
@@ -1284,7 +1405,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
searchTimer = setTimeout(async () => {
|
searchTimer = setTimeout(async () => {
|
||||||
const requestId = ++searchRequestId
|
const requestId = ++searchRequestId
|
||||||
try {
|
try {
|
||||||
await searchIssues(query)
|
await suggestIssues(query)
|
||||||
} catch (cause) {
|
} catch (cause) {
|
||||||
reportDataError(errorMessage(cause, '銘柄検索に失敗しました'), cause)
|
reportDataError(errorMessage(cause, '銘柄検索に失敗しました'), cause)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1337,6 +1458,7 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
holdingsMarketValue,
|
holdingsMarketValue,
|
||||||
totalProfitLoss,
|
totalProfitLoss,
|
||||||
totalProfitLossRate,
|
totalProfitLossRate,
|
||||||
|
marketIndexes,
|
||||||
orders,
|
orders,
|
||||||
cancelingOrderKey,
|
cancelingOrderKey,
|
||||||
orderHistoryLoaded,
|
orderHistoryLoaded,
|
||||||
@@ -1379,6 +1501,11 @@ export const useTradingSession = (selectedPasskeyId: Ref<string>) => {
|
|||||||
askPlaceOrder,
|
askPlaceOrder,
|
||||||
placeCashOrder,
|
placeCashOrder,
|
||||||
cancelOrder,
|
cancelOrder,
|
||||||
|
loadOrderDetail,
|
||||||
|
loadTradeRecords,
|
||||||
|
loadPositionDetail,
|
||||||
|
estimateOrderCorrection,
|
||||||
|
placeOrderCorrection,
|
||||||
downloadCsv,
|
downloadCsv,
|
||||||
openTradeForStock,
|
openTradeForStock,
|
||||||
openTradeForPosition,
|
openTradeForPosition,
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ export const ui = {
|
|||||||
panelHead: 'flex flex-wrap items-center justify-between gap-3',
|
panelHead: 'flex flex-wrap items-center justify-between gap-3',
|
||||||
eyebrow: 'mb-1 text-xs font-black uppercase text-[#9aa0a9]',
|
eyebrow: 'mb-1 text-xs font-black uppercase text-[#9aa0a9]',
|
||||||
dashboardGrid:
|
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:
|
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',
|
'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:
|
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',
|
'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-2',
|
assetOverviewHead: 'grid gap-3',
|
||||||
assetOverviewSubtext: 'text-sm font-semibold text-[#c3c7cf]',
|
assetOverviewSubtext: 'text-sm font-semibold text-[#c3c7cf]',
|
||||||
assetBreakdownPanel: 'grid gap-3',
|
assetBreakdownPanel: 'grid gap-3',
|
||||||
assetBreakdownTitle: 'text-xs font-extrabold text-[#9aa0a9]',
|
assetBreakdownTitle: 'text-xs font-extrabold text-[#9aa0a9]',
|
||||||
@@ -52,19 +52,24 @@ export const ui = {
|
|||||||
assetBreakdownAmount: 'text-sm font-black text-[#e3e3e9]',
|
assetBreakdownAmount: 'text-sm font-black text-[#e3e3e9]',
|
||||||
assetBreakdownRatio: 'text-xs font-semibold text-[#8f949d]',
|
assetBreakdownRatio: 'text-xs font-semibold text-[#8f949d]',
|
||||||
holdingsPanel:
|
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',
|
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',
|
holdingsRows: 'grid min-h-0 flex-1 content-start overflow-y-auto',
|
||||||
holdingsHead:
|
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:
|
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]',
|
typePill: 'w-fit rounded-full bg-[#263141] px-3 py-1 text-xs text-[#d3e3fd]',
|
||||||
muted: 'text-[#8f949d]',
|
muted: 'text-[#8f949d]',
|
||||||
portfolioHistory:
|
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',
|
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',
|
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',
|
emptyState: 'flex flex-1 items-center justify-center py-8 text-center',
|
||||||
miniOrder: 'grid grid-cols-[minmax(0,1fr)_auto] gap-3',
|
miniOrder: 'grid grid-cols-[minmax(0,1fr)_auto] gap-3',
|
||||||
tradeLayout:
|
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]',
|
'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',
|
list: 'grid gap-2',
|
||||||
orderRow:
|
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]',
|
statusBadge: 'w-fit rounded-full bg-[#263141] px-3 py-1 text-xs font-black text-[#d3e3fd]',
|
||||||
pendingBadge: 'bg-[#4a3720] text-[#ffddb3]',
|
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',
|
apiLayout: 'grid grid-cols-1 gap-5 lg:grid-cols-[minmax(0,1fr)_minmax(20rem,0.9fr)] lg:gap-7',
|
||||||
settingsLayout: 'grid 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]',
|
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',
|
keyRow: 'grid gap-3 rounded-[24px] bg-[#111418] p-4',
|
||||||
profileRow:
|
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]',
|
'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]',
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
export type TradeSide = 'buy' | 'sell'
|
export type TradeSide = 'buy' | 'sell'
|
||||||
export type OrderKind = 'standard' | 's'
|
export type OrderKind = 'standard' | 's'
|
||||||
export type CashOrderAccountType = 'specific' | 'general' | 'growthInvestment' | 'nisa'
|
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 =
|
export type CashOrderPriceCondition =
|
||||||
| 'limit'
|
| 'limit'
|
||||||
| 'limitAtOpen'
|
| 'limitAtOpen'
|
||||||
@@ -31,6 +40,11 @@ export type OrderPreview = {
|
|||||||
}
|
}
|
||||||
side: string
|
side: string
|
||||||
quantity: number
|
quantity: number
|
||||||
|
price?: {
|
||||||
|
value: number | null
|
||||||
|
text: string
|
||||||
|
currency?: string
|
||||||
|
}
|
||||||
warnings: string[]
|
warnings: string[]
|
||||||
confirmationId?: string
|
confirmationId?: string
|
||||||
message?: string
|
message?: string
|
||||||
@@ -61,10 +75,24 @@ export type ChartNotice = {
|
|||||||
detail?: string
|
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 = {
|
export type Stock = {
|
||||||
code: string
|
code: string
|
||||||
name: string
|
name: string
|
||||||
symbol: string
|
symbol: string
|
||||||
|
searchText?: string
|
||||||
country: string
|
country: string
|
||||||
market: string
|
market: string
|
||||||
sector: string
|
sector: string
|
||||||
@@ -89,16 +117,45 @@ export type Stock = {
|
|||||||
|
|
||||||
export type OrderRow = {
|
export type OrderRow = {
|
||||||
id: string
|
id: string
|
||||||
|
code: string
|
||||||
date: string
|
date: string
|
||||||
stock: string
|
stock: string
|
||||||
market: string
|
market: string
|
||||||
side: TradeSide
|
side: TradeSide
|
||||||
kind: OrderKind
|
kind: OrderKind
|
||||||
quantity: number | null
|
quantity: number | null
|
||||||
|
unexecutedQuantity?: number | null
|
||||||
|
executedQuantity?: number | null
|
||||||
price: number | null
|
price: number | null
|
||||||
status: '注文中' | '約定済' | '取消済'
|
status: '注文中' | '約定済' | '取消済'
|
||||||
orderNumber?: string
|
orderNumber?: string
|
||||||
|
orderSubNo?: string
|
||||||
tradeId?: 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 = {
|
export type Position = {
|
||||||
@@ -107,8 +164,10 @@ export type Position = {
|
|||||||
market: string
|
market: string
|
||||||
quantity: number
|
quantity: number
|
||||||
avgPrice: number
|
avgPrice: number
|
||||||
|
currentPrice?: number | null
|
||||||
marketValue: number
|
marketValue: number
|
||||||
profitLoss: number
|
profitLoss: number
|
||||||
profitLossRate: number
|
profitLossRate: number
|
||||||
type?: string
|
type?: string
|
||||||
|
accountType?: CashOrderAccountType | string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export type {
|
|||||||
OrderCorrectionPreOrder,
|
OrderCorrectionPreOrder,
|
||||||
OrderCorrectionPreOrderDetail,
|
OrderCorrectionPreOrderDetail,
|
||||||
OrderPreview,
|
OrderPreview,
|
||||||
|
TradeRecord,
|
||||||
|
TradeRecordList,
|
||||||
StockOrderPreOrder,
|
StockOrderPreOrder,
|
||||||
StockOrderPreOrderMarginTradeType,
|
StockOrderPreOrderMarginTradeType,
|
||||||
StockOrderPreOrderPaymentLimit,
|
StockOrderPreOrderPaymentLimit,
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ export type MarketRegion = 'domestic' | 'us'
|
|||||||
|
|
||||||
const DOMESTIC_TO_MTS = {
|
const DOMESTIC_TO_MTS = {
|
||||||
XTKS: 'TKY',
|
XTKS: 'TKY',
|
||||||
|
XNGO: 'NGY',
|
||||||
|
XFKA: 'FKO',
|
||||||
|
XSAP: 'SPR',
|
||||||
} as const satisfies Record<string, string>
|
} as const satisfies Record<string, string>
|
||||||
|
|
||||||
const MTS_TO_DOMESTIC = Object.fromEntries(
|
const MTS_TO_DOMESTIC = Object.fromEntries(
|
||||||
|
|||||||
@@ -297,19 +297,21 @@ export const createMethodsFromSession = (session: SbiSession): SbiClientMethods
|
|||||||
positions: {
|
positions: {
|
||||||
cash: async (options) =>
|
cash: async (options) =>
|
||||||
options?.market && isUsMarket(options.market)
|
options?.market && isUsMarket(options.market)
|
||||||
? usStock.positions()
|
? usStock.positions(options)
|
||||||
: parseCashPositions(
|
: parseCashPositions(
|
||||||
await callMts(session, 'F2631', listAccountTrin(session, options)),
|
await callMts(session, 'F2631', listAccountTrin(session, options)),
|
||||||
options,
|
options,
|
||||||
),
|
),
|
||||||
cashDetail: async (options) =>
|
cashDetail: async (options) =>
|
||||||
parseCashPositions(
|
options?.market && isUsMarket(options.market)
|
||||||
await callMts(session, 'F2632', listAccountTrin(session, options)),
|
? usStock.positionsDetail(options)
|
||||||
options,
|
: parseCashPositions(
|
||||||
),
|
await callMts(session, 'F2632', listAccountTrin(session, options)),
|
||||||
|
options,
|
||||||
|
),
|
||||||
cashForIssue: async (options) => {
|
cashForIssue: async (options) => {
|
||||||
return isUsMarket(options.market)
|
return isUsMarket(options.market)
|
||||||
? usStock.positions()
|
? usStock.positions(options)
|
||||||
: filterCashPositions(
|
: filterCashPositions(
|
||||||
parseCashPositions(
|
parseCashPositions(
|
||||||
await callMts(session, 'F2602', issuePositionTrin(session, options)),
|
await callMts(session, 'F2602', issuePositionTrin(session, options)),
|
||||||
@@ -487,6 +489,18 @@ export const createMethodsFromSession = (session: SbiSession): SbiClientMethods
|
|||||||
),
|
),
|
||||||
options,
|
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: {
|
cash: {
|
||||||
preOrder: async (options) =>
|
preOrder: async (options) =>
|
||||||
@@ -528,22 +542,28 @@ export const createMethodsFromSession = (session: SbiSession): SbiClientMethods
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
estimateCorrection: async (options) =>
|
estimateCorrection: async (options) =>
|
||||||
parseOrderCorrectionPreOrder(
|
options.market && isUsMarket(options.market)
|
||||||
await callMts(session, 'F2301', orderCorrectionPreOrderTrin(session, options)),
|
? usStock.estimateCorrection(options)
|
||||||
cashCorrectionPreviewInput,
|
: parseOrderCorrectionPreOrder(
|
||||||
),
|
await callMts(session, 'F2301', orderCorrectionPreOrderTrin(session, options)),
|
||||||
|
cashCorrectionPreviewInput,
|
||||||
|
),
|
||||||
estimateCorrectionConfirm: async (options) =>
|
estimateCorrectionConfirm: async (options) =>
|
||||||
parseOrderPreview(
|
options.market && isUsMarket(options.market)
|
||||||
await callMts(session, 'F2302', orderCorrectionSubmitTrin(session, options)),
|
? usStock.estimateCorrectionConfirm(options)
|
||||||
cashCorrectionPreviewInput,
|
: parseOrderPreview(
|
||||||
),
|
await callMts(session, 'F2302', orderCorrectionSubmitTrin(session, options)),
|
||||||
|
cashCorrectionPreviewInput,
|
||||||
|
),
|
||||||
placeCorrection: async (options) => {
|
placeCorrection: async (options) => {
|
||||||
|
if (options.market && isUsMarket(options.market)) return usStock.placeCorrection(options)
|
||||||
assertTradingAllowed(options, 'orders.cash.placeCorrection')
|
assertTradingAllowed(options, 'orders.cash.placeCorrection')
|
||||||
return parseOrderReceipt(
|
return parseOrderReceipt(
|
||||||
await callMts(session, 'F2303', orderCorrectionSubmitTrin(session, options)),
|
await callMts(session, 'F2303', orderCorrectionSubmitTrin(session, options)),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
estimateCancel: async (options) => {
|
estimateCancel: async (options) => {
|
||||||
|
if (options.market && isUsMarket(options.market)) return usStock.estimateCancel(options)
|
||||||
assertOrderCancelOptions(options)
|
assertOrderCancelOptions(options)
|
||||||
return parseOrderCorrectionPreOrder(
|
return parseOrderCorrectionPreOrder(
|
||||||
await callMts(session, 'F2311', orderCancelPreOrderTrin(session, options)),
|
await callMts(session, 'F2311', orderCancelPreOrderTrin(session, options)),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import type {
|
|||||||
MarketCode,
|
MarketCode,
|
||||||
MarketIndex,
|
MarketIndex,
|
||||||
NewsList,
|
NewsList,
|
||||||
|
Order,
|
||||||
OrderId,
|
OrderId,
|
||||||
OrderKind,
|
OrderKind,
|
||||||
OrderList,
|
OrderList,
|
||||||
@@ -36,6 +37,7 @@ import type {
|
|||||||
Ranking,
|
Ranking,
|
||||||
ThemeId,
|
ThemeId,
|
||||||
ThemeInvestmentList,
|
ThemeInvestmentList,
|
||||||
|
TradeRecordList,
|
||||||
TradeSide,
|
TradeSide,
|
||||||
Watchlist,
|
Watchlist,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
@@ -116,6 +118,22 @@ export type OrderInquiryOptions = PagingOptions &
|
|||||||
status?: OrderStatus
|
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 & {
|
export type BoardOptions = IssueOptions & {
|
||||||
/** Account type used when requesting board-order information. */
|
/** Account type used when requesting board-order information. */
|
||||||
accountType?: AccountType
|
accountType?: AccountType
|
||||||
@@ -384,6 +402,10 @@ export type OrderCancelOptions = {
|
|||||||
orderNumber: string
|
orderNumber: string
|
||||||
/** Original order ID shown in order inquiry. */
|
/** Original order ID shown in order inquiry. */
|
||||||
orderId?: OrderId
|
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. */
|
/** Original trade ID code. Defaults to cash stock when omitted. */
|
||||||
tradeId?: string
|
tradeId?: string
|
||||||
/** Additional cancel flag used by the mobile MTS route. */
|
/** Additional cancel flag used by the mobile MTS route. */
|
||||||
@@ -667,6 +689,10 @@ export interface SbiClientMethodOrderInquiry {
|
|||||||
executionsToday(options?: OrderInquiryOptions): Promise<OrderList>
|
executionsToday(options?: OrderInquiryOptions): Promise<OrderList>
|
||||||
/** Fetches open or recently active orders. */
|
/** Fetches open or recently active orders. */
|
||||||
open(options?: OrderInquiryOptions): Promise<OrderList>
|
open(options?: OrderInquiryOptions): Promise<OrderList>
|
||||||
|
/** Fetches a detailed order record. Currently implemented for US stock orders. */
|
||||||
|
detail(options: OrderDetailOptions): Promise<Order>
|
||||||
|
/** Fetches trade records. Currently implemented for US stock trades. */
|
||||||
|
tradeRecords(options: TradeRecordInquiryOptions): Promise<TradeRecordList>
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SbiClientMethodCashOrder {
|
export interface SbiClientMethodCashOrder {
|
||||||
|
|||||||
@@ -16,23 +16,32 @@ import type {
|
|||||||
SbiSession,
|
SbiSession,
|
||||||
SignedTextValue,
|
SignedTextValue,
|
||||||
StockOrderPreOrder,
|
StockOrderPreOrder,
|
||||||
|
TradeRecord,
|
||||||
|
TradeRecordList,
|
||||||
TradeSide,
|
TradeSide,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import type {
|
import type {
|
||||||
BoardOptions,
|
BoardOptions,
|
||||||
CashOrderOptions,
|
CashOrderOptions,
|
||||||
CashOrderPreOrderOptions,
|
CashOrderPreOrderOptions,
|
||||||
|
CashPositionOptions,
|
||||||
IssueChartOptions,
|
IssueChartOptions,
|
||||||
IssueOptions,
|
IssueOptions,
|
||||||
IssueSearchOptions,
|
IssueSearchOptions,
|
||||||
|
OrderCancelOptions,
|
||||||
|
OrderCorrectionOptions,
|
||||||
|
OrderDetailOptions,
|
||||||
OrderInquiryOptions,
|
OrderInquiryOptions,
|
||||||
PlaceCashOrderOptions,
|
PlaceCashOrderOptions,
|
||||||
PlaceOrderCancelOptions,
|
PlaceOrderCancelOptions,
|
||||||
|
PlaceOrderCorrectionOptions,
|
||||||
|
TradeRecordInquiryOptions,
|
||||||
} from './types'
|
} from './types'
|
||||||
import { requireUsMarket } from '../markets'
|
import { requireUsMarket } from '../markets'
|
||||||
|
|
||||||
const COUNTRY_US = 'US'
|
const COUNTRY_US = 'US'
|
||||||
const DEFAULT_CHART_COUNT = 120
|
const DEFAULT_CHART_COUNT = 120
|
||||||
|
const DEFAULT_US_INQUIRY_LOOKBACK_DAYS = 90
|
||||||
const US_CHART_INTERVALS: Record<ChartPeriod, Record<number, string>> = {
|
const US_CHART_INTERVALS: Record<ChartPeriod, Record<number, string>> = {
|
||||||
minute: { 1: '1', 5: '2', 10: '3', 15: '4' },
|
minute: { 1: '1', 5: '2', 10: '3', 15: '4' },
|
||||||
day: { 1: '7' },
|
day: { 1: '7' },
|
||||||
@@ -127,7 +136,10 @@ export const createUsStockAdapter = (session: SbiSession) => ({
|
|||||||
quote,
|
quote,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
positions: async (): Promise<CashPositionList> => fetchUsCashPositions(session),
|
positions: async (options?: CashPositionOptions): Promise<CashPositionList> =>
|
||||||
|
fetchUsCashPositions(session, options),
|
||||||
|
positionsDetail: async (options?: CashPositionOptions): Promise<CashPositionList> =>
|
||||||
|
fetchUsCashPositionDetail(session, options),
|
||||||
unrealized: async () => {
|
unrealized: async () => {
|
||||||
const positions = await fetchUsCashPositions(session)
|
const positions = await fetchUsCashPositions(session)
|
||||||
return {
|
return {
|
||||||
@@ -139,6 +151,10 @@ export const createUsStockAdapter = (session: SbiSession) => ({
|
|||||||
},
|
},
|
||||||
orders: async (options?: OrderInquiryOptions): Promise<OrderList> =>
|
orders: async (options?: OrderInquiryOptions): Promise<OrderList> =>
|
||||||
fetchUsOrders(session, options),
|
fetchUsOrders(session, options),
|
||||||
|
orderDetail: async (options: OrderDetailOptions): Promise<Order> =>
|
||||||
|
fetchUsOrderDetail(session, options),
|
||||||
|
tradeRecords: async (options: TradeRecordInquiryOptions): Promise<TradeRecordList> =>
|
||||||
|
fetchUsTradeRecords(session, options),
|
||||||
preOrder: async (options: CashOrderPreOrderOptions): Promise<StockOrderPreOrder> => {
|
preOrder: async (options: CashOrderPreOrderOptions): Promise<StockOrderPreOrder> => {
|
||||||
requireUsMarket(options.market, 'orders.cash.preOrder')
|
requireUsMarket(options.market, 'orders.cash.preOrder')
|
||||||
const data = await callUsGraphql(session, 'GetOrderCreatingInitialData', ORDER_INITIAL_DATA, {
|
const data = await callUsGraphql(session, 'GetOrderCreatingInitialData', ORDER_INITIAL_DATA, {
|
||||||
@@ -204,11 +220,108 @@ export const createUsStockAdapter = (session: SbiSession) => ({
|
|||||||
message: stringAt(data, ['createForeignStockOrder', 'message']),
|
message: stringAt(data, ['createForeignStockOrder', 'message']),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
estimateCorrection: async (options: OrderCorrectionOptions): Promise<OrderPreview> => {
|
||||||
|
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<OrderPreview> => {
|
||||||
|
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<OrderReceipt> => {
|
||||||
|
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<OrderPreview> => {
|
||||||
|
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<OrderReceipt> => {
|
placeCancel: async (options: PlaceOrderCancelOptions): Promise<OrderReceipt> => {
|
||||||
if (options.allowTrading !== true) {
|
if (options.allowTrading !== true) {
|
||||||
throw new Error('orders.cash.placeCancel requires 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 }
|
return { stock, marketPrice }
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchUsCashPositions = async (session: SbiSession): Promise<CashPositionList> => {
|
const fetchUsCashPositions = async (
|
||||||
|
session: SbiSession,
|
||||||
|
options?: CashPositionOptions,
|
||||||
|
): Promise<CashPositionList> => {
|
||||||
const data = await callUsGraphql(session, 'GetSecuritiesBalanceList', SECURITIES_BALANCES, {
|
const data = await callUsGraphql(session, 'GetSecuritiesBalanceList', SECURITIES_BALANCES, {
|
||||||
input: { countryCode: COUNTRY_US, page: { pageNum: 1, pageSize: 999 } },
|
input: { countryCode: COUNTRY_US, page: { pageNum: 1, pageSize: 999 } },
|
||||||
})
|
})
|
||||||
const balances = arrayAt(data, ['listSecuritiesBalances', 'securitiesBalances'])
|
const balances = arrayAt(data, ['listSecuritiesBalances', 'securitiesBalances'])
|
||||||
const positions = balances.map((balance): CashPosition => {
|
const positions = balances
|
||||||
const securities = objectAt(balance, ['securities'])
|
.map((balance) => cashPositionFromUsBalance(balance))
|
||||||
const evaluation = objectAt(balance, ['evaluationProfitLoss'])
|
.filter((position) => {
|
||||||
const market = usMarketFromGraphql(objectAt(balance, ['market']))
|
if (options?.issueCode && position.issue.code !== options.issueCode) return false
|
||||||
return {
|
if (options?.market && position.issue.market !== options.market) return false
|
||||||
issue: {
|
if (options?.accountType && position.accountType !== options.accountType) return false
|
||||||
code: stringAt(securities, ['securitiesCode']) ?? '',
|
return true
|
||||||
market,
|
})
|
||||||
name:
|
const limited = limitList(positions, options)
|
||||||
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'])),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return {
|
return {
|
||||||
positions,
|
positions: limited,
|
||||||
totalCount: positions.length,
|
totalCount: positions.length,
|
||||||
totalMarketValue: sumAmounts(positions.map((position) => position.marketValue)),
|
totalMarketValue: sumAmounts(limited.map((position) => position.marketValue)),
|
||||||
totalProfitLoss: sumSigned(positions.map((position) => position.profitLoss)),
|
totalProfitLoss: sumSigned(limited.map((position) => position.profitLoss)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUsCashPositionDetail = async (
|
||||||
|
session: SbiSession,
|
||||||
|
options?: CashPositionOptions,
|
||||||
|
): Promise<CashPositionList> => {
|
||||||
|
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,
|
session: SbiSession,
|
||||||
options?: OrderInquiryOptions,
|
options?: OrderInquiryOptions,
|
||||||
): Promise<OrderList> => {
|
): Promise<OrderList> => {
|
||||||
|
const dateRange = usInquiryDateRange(options)
|
||||||
const data = await callUsGraphql(session, 'GetOrderList', ORDER_LIST, {
|
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 {
|
return {
|
||||||
orders: orders.filter((order) => {
|
orders: orders.filter((order) => {
|
||||||
if (options?.issueCode && order.issue.code !== options.issueCode) return false
|
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
|
if (options?.status && order.status !== options.status) return false
|
||||||
return true
|
return true
|
||||||
}),
|
}),
|
||||||
|
hasMore: booleanAt(list, ['page', 'hasNextPage']),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchUsOrderDetail = async (
|
||||||
|
session: SbiSession,
|
||||||
|
options: OrderDetailOptions,
|
||||||
|
): Promise<Order> => {
|
||||||
|
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<TradeRecordList> => {
|
||||||
|
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 orderFromGraphql = (
|
||||||
const market = usMarketFromGraphql(objectAt(value, ['market']))
|
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 securities = objectAt(value, ['securities'])
|
||||||
|
const orderNo = stringAt(value, ['orderNo'])
|
||||||
|
const orderSubNo = stringAt(value, ['orderSubNo'])
|
||||||
return {
|
return {
|
||||||
id: stringAt(value, ['orderNo']) ?? stringAt(value, ['orderSubNo']) ?? '',
|
id: orderSubNo ?? orderNo ?? '',
|
||||||
issue: {
|
issue: {
|
||||||
code: stringAt(securities, ['securitiesCode']) ?? '',
|
code: extra.issue?.code ?? stringAt(securities, ['securitiesCode']) ?? '',
|
||||||
market,
|
market,
|
||||||
name:
|
name:
|
||||||
stringAt(securities, ['securitiesName']) ?? stringAt(securities, ['securitiesShortName']),
|
extra.issue?.name ??
|
||||||
|
stringAt(securities, ['securitiesName']) ??
|
||||||
|
stringAt(securities, ['securitiesShortName']),
|
||||||
},
|
},
|
||||||
side: stringAt(value, ['buySellCode']) === 'SELL' ? 'sell' : 'buy',
|
side: stringAt(value, ['buySellCode']) === 'SELL' ? 'sell' : 'buy',
|
||||||
status: mapOrderStatus(stringAt(value, ['orderStatus'])),
|
status: mapOrderStatus(stringAt(value, ['orderStatus'])),
|
||||||
@@ -470,7 +696,204 @@ const orderFromGraphql = (value: unknown): Order => {
|
|||||||
price: usd(stringAt(value, ['orderPrice'])),
|
price: usd(stringAt(value, ['orderPrice'])),
|
||||||
executedPrice: usd(stringAt(value, ['executionAveragePrice'])),
|
executedPrice: usd(stringAt(value, ['executionAveragePrice'])),
|
||||||
orderedAt: stringAt(value, ['orderInputDatetime']),
|
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<Order> => {
|
||||||
|
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<string, unknown>) => ({
|
||||||
|
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<string, unknown>,
|
||||||
|
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
|
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) => {
|
const usGraphqlMarketCode = (market: MarketCode) => {
|
||||||
switch (market) {
|
switch (market) {
|
||||||
case 'XNAS':
|
case 'XNAS':
|
||||||
@@ -705,6 +1157,20 @@ const stringArrayAt = (source: unknown, path: string[]) =>
|
|||||||
|
|
||||||
const numberAt = (source: unknown, path: string[]) => parseNumber(stringAt(source, path))
|
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 = <T>(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 =>
|
const valueAt = (source: unknown, path: string[]): unknown =>
|
||||||
path.reduce<unknown>((current, key) => {
|
path.reduce<unknown>((current, key) => {
|
||||||
if (!current || typeof current !== 'object') return undefined
|
if (!current || typeof current !== 'object') return undefined
|
||||||
@@ -801,10 +1267,138 @@ query GetOrderList($input: Input_fstock_order_ListForeignStockOrdersRequest) {
|
|||||||
listForeignStockOrders(input: $input) {
|
listForeignStockOrders(input: $input) {
|
||||||
orderDecodes {
|
orderDecodes {
|
||||||
orderNo orderSubNo buySellCode orderStatus orderQuantity unexecutedQuantity executionQuantity
|
orderNo orderSubNo buySellCode orderStatus orderQuantity unexecutedQuantity executionQuantity
|
||||||
orderPrice executionAveragePrice orderInputDatetime
|
orderPrice executionAveragePrice orderInputDatetime orderTerm specificAccountCode
|
||||||
securities { securitiesCode securitiesName securitiesShortName }
|
securities { securitiesCode securitiesName securitiesShortName }
|
||||||
market { marketCode marketName marketShortName }
|
market { marketCode marketName marketShortName }
|
||||||
}
|
}
|
||||||
page { hasNextPage pageNum pageSize }
|
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 }
|
||||||
|
}`
|
||||||
|
|||||||
@@ -180,7 +180,7 @@ export type SbiTradeAuthenticationOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type IssueCode = string
|
export type IssueCode = string
|
||||||
export type DomesticMarketCode = 'XTKS'
|
export type DomesticMarketCode = 'XTKS' | 'XNGO' | 'XFKA' | 'XSAP'
|
||||||
export type SKabuMarketCode = 'STK'
|
export type SKabuMarketCode = 'STK'
|
||||||
export type UsStockMarketCode = 'XNAS' | 'XNYS' | 'ARCX'
|
export type UsStockMarketCode = 'XNAS' | 'XNYS' | 'ARCX'
|
||||||
export type MarketCode = DomesticMarketCode | SKabuMarketCode | UsStockMarketCode
|
export type MarketCode = DomesticMarketCode | SKabuMarketCode | UsStockMarketCode
|
||||||
@@ -647,9 +647,12 @@ export type Order = {
|
|||||||
orderedAt?: string
|
orderedAt?: string
|
||||||
expiresAt?: string
|
expiresAt?: string
|
||||||
orderNumber?: string
|
orderNumber?: string
|
||||||
|
orderSubNo?: string
|
||||||
tradeId?: string
|
tradeId?: string
|
||||||
exchangeCode?: string
|
exchangeCode?: string
|
||||||
accountInformation?: string
|
accountInformation?: string
|
||||||
|
cancelable?: boolean
|
||||||
|
correctable?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrderList = {
|
export type OrderList = {
|
||||||
@@ -658,6 +661,29 @@ export type OrderList = {
|
|||||||
error?: SbiMethodError
|
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 = {
|
export type OrderCorrectionPreOrderDetail = {
|
||||||
exchangeName?: string
|
exchangeName?: string
|
||||||
marketLoanKbn?: string
|
marketLoanKbn?: string
|
||||||
|
|||||||
+15
-3
@@ -8,7 +8,13 @@ const runtimeDefaults = {
|
|||||||
CSBIE_RP_ID: '127.0.0.1',
|
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())
|
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')
|
messages.push('Set CSBIE_KEYRING_SECRET when CSBIE_KEYRING_BACKEND=sqlite')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!present('SBI_AUTH_BASE_URL') || !present('SBI_MTS_BASE_URL')) {
|
if (
|
||||||
messages.push('Set SBI_AUTH_BASE_URL and SBI_MTS_BASE_URL before connecting an SBI session')
|
!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) {
|
if (messages.length === 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user