From b843a5ff386bafc5109bd72bcfcdc2caa1de3ad2 Mon Sep 17 00:00:00 2001 From: Shotaro Nakamura Date: Fri, 19 Jun 2026 21:39:59 +0900 Subject: [PATCH] fix --- .env.example | 1 + AGENTS.md | 1 + apps/csbie-server/src/config.ts | 24 + apps/csbie-server/src/routes/mcp.ts | 34 + apps/csbie-server/src/rpc/methods.ts | 5 + apps/csbie-server/src/rpc/sbi-session.ts | 12 + apps/csbie-server/src/rpc/ws.ts | 13 +- apps/csbie-ui/AGENTS.md | 12 + apps/csbie-ui/index.html | 2 +- apps/csbie-ui/src/App.vue | 65 +- .../src/components/ApiKeyPolicyEditor.vue | 5 + apps/csbie-ui/src/components/ui/UiModal.vue | 82 +- .../src/components/ui/UiSegmented.vue | 66 +- apps/csbie-ui/src/constants/market.ts | 28 + apps/csbie-ui/src/constants/motion.ts | 45 + apps/csbie-ui/src/constants/trade.ts | 33 +- .../src/features/history/HistoryView.vue | 79 +- .../src/features/orders/OrderDialogs.vue | 251 ++--- .../src/features/portfolio/PortfolioView.vue | 102 +- .../src/features/search/SearchDialog.vue | 52 +- .../src/features/settings/SettingsView.vue | 141 ++- .../src/features/trade/RealtimePriceChart.vue | 94 +- .../csbie-ui/src/features/trade/TradeView.vue | 760 +++++++------- .../src/features/trading/trading-data.ts | 29 +- .../src/features/trading/useTradingSession.ts | 510 ++++++---- apps/csbie-ui/src/router.ts | 8 +- apps/csbie-ui/src/styles/ui.ts | 18 +- apps/csbie-ui/src/types/trading.ts | 5 +- apps/csbie-ui/src/utils/format.ts | 21 +- packages/sbi-client/src/index.ts | 11 + packages/sbi-client/src/markets.ts | 50 + packages/sbi-client/src/methods/index.ts | 951 ++++++++++++++++-- packages/sbi-client/src/methods/types.ts | 72 +- packages/sbi-client/src/methods/us-stock.ts | 810 +++++++++++++++ packages/sbi-client/src/session/index.ts | 366 +++++-- packages/sbi-client/src/types.ts | 141 ++- 36 files changed, 3891 insertions(+), 1008 deletions(-) create mode 100644 apps/csbie-ui/AGENTS.md create mode 100644 apps/csbie-ui/src/constants/market.ts create mode 100644 apps/csbie-ui/src/constants/motion.ts create mode 100644 packages/sbi-client/src/markets.ts create mode 100644 packages/sbi-client/src/methods/us-stock.ts diff --git a/.env.example b/.env.example index d4534c2..0428b10 100644 --- a/.env.example +++ b/.env.example @@ -19,5 +19,6 @@ CSBIE_SETUP_PASSWORD_HASH= SBI_AUTH_BASE_URL= SBI_MTS_BASE_URL= SBI_IZANAGI_BASE_URL= +SBI_MAIN_SITE_BASE_URL= SBI_TRADE_PASSWORD= SBI_DEVICE_ID= diff --git a/AGENTS.md b/AGENTS.md index a41aa17..bd3260e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,7 @@ - Run `bun fmt` and `bun typecheck` before finishing. - Do not hardcode real endpoint + - origin のみ。パスは許可。 ## Hints diff --git a/apps/csbie-server/src/config.ts b/apps/csbie-server/src/config.ts index ddf2582..aa40990 100644 --- a/apps/csbie-server/src/config.ts +++ b/apps/csbie-server/src/config.ts @@ -12,6 +12,18 @@ export type ServerConfig = { authBaseUrl?: string mtsBaseUrl?: string izanagiBaseUrl?: string + foreignStockBaseUrl?: string + usStockBaseUrl?: string + foreignStockRestUrl?: string + foreignStockGraphqlBffUrl?: string + foreignStockGraphqlIntUrl?: string + mainSiteBaseUrl?: string + mainSiteEtGatePath?: string + mainSiteAssetsValuationsPath?: string + mainSiteExchangeOrderInputPath?: string + mainSiteExchangeOrderPasswordPath?: string + mainSiteExchangeOrderConfirmPath?: string + mainSiteExchangeOrderCompletePath?: string } const optionalUrl = (value: string | undefined) => { @@ -38,5 +50,17 @@ export const loadConfig = (): ServerConfig => { authBaseUrl: optionalUrl(process.env.SBI_AUTH_BASE_URL), mtsBaseUrl: optionalUrl(process.env.SBI_MTS_BASE_URL), izanagiBaseUrl: optionalUrl(process.env.SBI_IZANAGI_BASE_URL), + foreignStockBaseUrl: optionalUrl(process.env.SBI_FOREIGN_STOCK_BASE_URL), + usStockBaseUrl: optionalUrl(process.env.SBI_US_STOCK_BASE_URL), + foreignStockRestUrl: optionalUrl(process.env.SBI_FOREIGN_STOCK_REST_URL), + foreignStockGraphqlBffUrl: optionalUrl(process.env.SBI_FOREIGN_STOCK_GRAPHQL_BFF_URL), + foreignStockGraphqlIntUrl: optionalUrl(process.env.SBI_FOREIGN_STOCK_GRAPHQL_INT_URL), + mainSiteBaseUrl: optionalUrl(process.env.SBI_MAIN_SITE_BASE_URL), + mainSiteEtGatePath: process.env.SBI_MAIN_SITE_ET_GATE_PATH, + mainSiteAssetsValuationsPath: process.env.SBI_MAIN_SITE_ASSETS_VALUATIONS_PATH, + mainSiteExchangeOrderInputPath: process.env.SBI_MAIN_SITE_EXCHANGE_ORDER_INPUT_PATH, + mainSiteExchangeOrderPasswordPath: process.env.SBI_MAIN_SITE_EXCHANGE_ORDER_PASSWORD_PATH, + mainSiteExchangeOrderConfirmPath: process.env.SBI_MAIN_SITE_EXCHANGE_ORDER_CONFIRM_PATH, + mainSiteExchangeOrderCompletePath: process.env.SBI_MAIN_SITE_EXCHANGE_ORDER_COMPLETE_PATH, } } diff --git a/apps/csbie-server/src/routes/mcp.ts b/apps/csbie-server/src/routes/mcp.ts index 1a1aab5..34d4c3d 100644 --- a/apps/csbie-server/src/routes/mcp.ts +++ b/apps/csbie-server/src/routes/mcp.ts @@ -75,6 +75,7 @@ const orderSubmitMethodByEstimateMethod = { 'orders.ifd.estimateCorrection': 'orders.ifd.placeCorrection', 'orders.ifd.estimateCancel': 'orders.ifd.placeCancel', 'orders.themeInvestment.estimate': 'orders.themeInvestment.place', + 'orders.exchange.estimate': 'orders.exchange.place', } as const satisfies Partial> const submitMethodForEstimateMethod = (method: RpcMethod) => @@ -567,9 +568,39 @@ const placeThemeInvestmentOrderSchema = themeInvestmentOrderSchema.extend({ .describe('Explicitly allows sending a live theme investment order'), }) +const exchangeOrderSchema = z.object({ + currencyCode: z.string().min(1).describe('Currency code, such as USD'), + side: z.enum(['buy', 'sell']).describe('Buy or sell the foreign currency'), + tradeQuantity: z.union([z.number().positive(), z.string().min(1)]).describe('Order quantity'), + specificMethod: z + .enum(['foreign', 'domestic']) + .optional() + .describe('foreign for foreign-currency quantity, domestic for yen amount'), + accountKind: z.enum(['GENERAL', 'JR_NISA']).optional().describe('SBI exchange account kind'), + sellMethod: z.enum(['SELL_PART', 'SELL_ALL']).optional().describe('Required for sell orders'), + orderAmount: z + .union([z.number().positive(), z.string().min(1)]) + .optional() + .describe('Hidden order amount posted to SBI; required for domestic orders'), + tradePassword: z.string().optional().describe('Trading password used by SBI'), +}) + +const exchangeRateSchema = exchangeOrderSchema.pick({ + currencyCode: true, + side: true, +}) + +const placeExchangeOrderSchema = exchangeOrderSchema.extend({ + allowTrading: z + .literal(true) + .optional() + .describe('Explicitly allows sending a live exchange order'), +}) + const methodParamSchemas = { 'session.profile': undefined, 'account.profile': undefined, + 'account.assets.current': undefined, 'account.power.buyingPower': undefined, 'account.power.collateralRatio': undefined, 'account.positions.cash': cashPositionOptionsSchema.optional(), @@ -629,6 +660,9 @@ const methodParamSchemas = { 'orders.themeInvestment.list': themeInvestmentPreOrderSchema, 'orders.themeInvestment.estimate': themeInvestmentOrderSchema, 'orders.themeInvestment.place': placeThemeInvestmentOrderSchema, + 'orders.exchange.rate': exchangeRateSchema, + 'orders.exchange.estimate': exchangeOrderSchema, + 'orders.exchange.place': placeExchangeOrderSchema, } satisfies Record const createMcpServer = (c: Context) => { diff --git a/apps/csbie-server/src/rpc/methods.ts b/apps/csbie-server/src/rpc/methods.ts index 50d1dab..6b1c539 100644 --- a/apps/csbie-server/src/rpc/methods.ts +++ b/apps/csbie-server/src/rpc/methods.ts @@ -3,6 +3,7 @@ import type { SbiClientMethods } from '@repo/sbi-client' export const RPC_METHODS = [ 'session.profile', 'account.profile', + 'account.assets.current', 'account.power.buyingPower', 'account.power.collateralRatio', 'account.positions.cash', @@ -62,6 +63,9 @@ export const RPC_METHODS = [ 'orders.themeInvestment.list', 'orders.themeInvestment.estimate', 'orders.themeInvestment.place', + 'orders.exchange.rate', + 'orders.exchange.estimate', + 'orders.exchange.place', ] as const export type RpcMethod = (typeof RPC_METHODS)[number] @@ -80,6 +84,7 @@ const tradingMethods = new Set([ 'orders.ifd.placeCorrection', 'orders.ifd.placeCancel', 'orders.themeInvestment.place', + 'orders.exchange.place', ]) const cashOrderMethods = new Set([ diff --git a/apps/csbie-server/src/rpc/sbi-session.ts b/apps/csbie-server/src/rpc/sbi-session.ts index 1d13d9c..f93311a 100644 --- a/apps/csbie-server/src/rpc/sbi-session.ts +++ b/apps/csbie-server/src/rpc/sbi-session.ts @@ -27,6 +27,18 @@ export const connectSbi = async ( authBaseUrl: config.authBaseUrl, mtsBaseUrl: config.mtsBaseUrl, izanagiBaseUrl: config.izanagiBaseUrl, + foreignStockBaseUrl: config.foreignStockBaseUrl, + usStockBaseUrl: config.usStockBaseUrl, + foreignStockRestUrl: config.foreignStockRestUrl, + foreignStockGraphqlBffUrl: config.foreignStockGraphqlBffUrl, + foreignStockGraphqlIntUrl: config.foreignStockGraphqlIntUrl, + mainSiteBaseUrl: config.mainSiteBaseUrl, + mainSiteEtGatePath: config.mainSiteEtGatePath, + mainSiteAssetsValuationsPath: config.mainSiteAssetsValuationsPath, + mainSiteExchangeOrderInputPath: config.mainSiteExchangeOrderInputPath, + mainSiteExchangeOrderPasswordPath: config.mainSiteExchangeOrderPasswordPath, + mainSiteExchangeOrderConfirmPath: config.mainSiteExchangeOrderConfirmPath, + mainSiteExchangeOrderCompletePath: config.mainSiteExchangeOrderCompletePath, }, clientOptions, ) diff --git a/apps/csbie-server/src/rpc/ws.ts b/apps/csbie-server/src/rpc/ws.ts index a13879b..7efdf3d 100644 --- a/apps/csbie-server/src/rpc/ws.ts +++ b/apps/csbie-server/src/rpc/ws.ts @@ -1,4 +1,4 @@ -import type { SbiClientMethods } from '@repo/sbi-client' +import type { MarketCode, SbiClientMethods } from '@repo/sbi-client' import { createBunWebSocket } from 'hono/bun' import type { WSContext } from 'hono/ws' import { randomUUID } from 'node:crypto' @@ -27,7 +27,7 @@ type RpcSocketState = { type BoardPollingParams = { issueCode: string - market?: string + market: MarketCode intervalSeconds?: number } @@ -67,8 +67,11 @@ const parseBoardPollingParams = (params: unknown): BoardPollingParams => { if (typeof value.issueCode !== 'string' || !value.issueCode) { throw new Error('issueCode is required') } - if (value.market != null && typeof value.market !== 'string') { - throw new Error('market must be a string') + if (typeof value.market !== 'string' || !value.market) { + throw new Error('market is required') + } + if (!['XTKS', 'XNAS', 'XNYS', 'ARCX'].includes(value.market)) { + throw new Error('market must be XTKS, XNAS, XNYS, or ARCX') } if ( value.intervalSeconds != null && @@ -81,7 +84,7 @@ const parseBoardPollingParams = (params: unknown): BoardPollingParams => { return { issueCode: value.issueCode, - market: typeof value.market === 'string' ? value.market : undefined, + market: value.market as MarketCode, intervalSeconds: typeof value.intervalSeconds === 'number' ? value.intervalSeconds : undefined, } } diff --git a/apps/csbie-ui/AGENTS.md b/apps/csbie-ui/AGENTS.md new file mode 100644 index 0000000..2323265 --- /dev/null +++ b/apps/csbie-ui/AGENTS.md @@ -0,0 +1,12 @@ +- 無駄な情報は入れない。プロンプトの内容に関連付けた内容も入れない。 + - 開発者向けではなく、ユーザー視点で UI を構築する + - 重複した UI の内容は削除 + - 重複するなら積極的に削除 + - 明らかにわかる情報はいれない + - 変な英訳も入れない +- Tailwind + - Tailwind のデフォルトの数値を優先 + - bg-[#xxxxxx] は控え、代わりに bg-xxx-xxx を使用する + - もし使いたい色があるのなら @color で css に定義してから使う + - rounded-[xxpx] の代わりに rounded-lg, md, ... などをつかう + - その他も同様 diff --git a/apps/csbie-ui/index.html b/apps/csbie-ui/index.html index af51719..7eea365 100644 --- a/apps/csbie-ui/index.html +++ b/apps/csbie-ui/index.html @@ -3,7 +3,7 @@ - CSBIE + cSBIe
diff --git a/apps/csbie-ui/src/App.vue b/apps/csbie-ui/src/App.vue index e34d874..cf94c1b 100644 --- a/apps/csbie-ui/src/App.vue +++ b/apps/csbie-ui/src/App.vue @@ -1,6 +1,6 @@ diff --git a/apps/csbie-ui/src/components/ui/UiSegmented.vue b/apps/csbie-ui/src/components/ui/UiSegmented.vue index 0135231..cb1d3a4 100644 --- a/apps/csbie-ui/src/components/ui/UiSegmented.vue +++ b/apps/csbie-ui/src/components/ui/UiSegmented.vue @@ -1,31 +1,61 @@ diff --git a/apps/csbie-ui/src/constants/market.ts b/apps/csbie-ui/src/constants/market.ts new file mode 100644 index 0000000..bcd7fa2 --- /dev/null +++ b/apps/csbie-ui/src/constants/market.ts @@ -0,0 +1,28 @@ +export type MarketSession = readonly [openMinutes: number, closeMinutes: number] + +const minutes = (hour: number, minute = 0) => hour * 60 + minute + +export const marketTimeZones: Record = { + ARCX: 'America/New_York', + XNAS: 'America/New_York', + XNYS: 'America/New_York', + XTKS: 'Asia/Tokyo', +} + +export const countryTimeZones: Record = { + アメリカ: 'America/New_York', + 日本: 'Asia/Tokyo', + 米国: 'America/New_York', + US: 'America/New_York', + USA: 'America/New_York', +} + +export const marketSessions: Record = { + ARCX: [[minutes(9, 30), minutes(16)]], + XNAS: [[minutes(9, 30), minutes(16)]], + XNYS: [[minutes(9, 30), minutes(16)]], + XTKS: [ + [minutes(9), minutes(11, 30)], + [minutes(12, 30), minutes(15, 30)], + ], +} diff --git a/apps/csbie-ui/src/constants/motion.ts b/apps/csbie-ui/src/constants/motion.ts new file mode 100644 index 0000000..78c5ba8 --- /dev/null +++ b/apps/csbie-ui/src/constants/motion.ts @@ -0,0 +1,45 @@ +export const motionProfiles = { + expressive: { + spatial: { + fast: { duration: 0.35, ease: [0.42, 1.67, 0.21, 0.9] } as const, + default: { duration: 0.5, ease: [0.38, 1.21, 0.22, 1] } as const, + slow: { duration: 0.65, ease: [0.39, 1.29, 0.35, 0.98] } as const, + }, + effects: { + fast: { duration: 0.15, ease: [0.31, 0.94, 0.34, 1] } as const, + default: { duration: 0.2, ease: [0.34, 0.8, 0.34, 1] } as const, + slow: { duration: 0.3, ease: [0.34, 0.88, 0.34, 1] } as const, + }, + }, + standard: { + spatial: { + fast: { duration: 0.35, ease: [0.27, 1.06, 0.18, 1] } as const, + default: { duration: 0.5, ease: [0.27, 1.06, 0.18, 1] } as const, + slow: { duration: 0.75, ease: [0.27, 1.06, 0.18, 1] } as const, + }, + effects: { + fast: { duration: 0.15, ease: [0.31, 0.94, 0.34, 1] } as const, + default: { duration: 0.2, ease: [0.34, 0.8, 0.34, 1] } as const, + slow: { duration: 0.3, ease: [0.34, 0.88, 0.34, 1] } as const, + }, + }, +} as const + +export const uiMotion = { + overlay: { + fadeDefault: motionProfiles.standard.effects.default, + }, + modal: { + sheetOpen: motionProfiles.standard.spatial.default, + sheetOpenExpressive: motionProfiles.expressive.spatial.fast, + openExpressiveSpatial: motionProfiles.expressive.spatial.fast, + }, + trade: { + tabIndicator: motionProfiles.standard.spatial.default, + segmentedIndicator: motionProfiles.expressive.spatial.fast, + tabContentFade: motionProfiles.standard.effects.default, + advancedPanel: motionProfiles.expressive.effects.default, + advancedOptions: motionProfiles.expressive.spatial.fast, + searchSheet: motionProfiles.expressive.spatial.fast, + }, +} as const diff --git a/apps/csbie-ui/src/constants/trade.ts b/apps/csbie-ui/src/constants/trade.ts index 427ffaf..03118b0 100644 --- a/apps/csbie-ui/src/constants/trade.ts +++ b/apps/csbie-ui/src/constants/trade.ts @@ -33,19 +33,36 @@ export const cashOrderMarketOptions: Array<{ value: CashOrderMarket }> = [ { label: '自動', value: 'auto' }, - { label: '東証', value: 'TKY' }, - { label: '名証', value: 'NGY' }, - { label: '福証', value: 'FKO' }, - { label: '札証', value: 'SPR' }, - { label: 'SOR', value: 'SOR' }, - { label: 'PTS', value: 'PTS' }, - { label: 'PTS(X)', value: 'PTX' }, + { label: '東証', value: 'XTKS' }, + { label: 'NASDAQ', value: 'XNAS' }, + { label: 'NYSE', value: 'XNYS' }, + { label: 'NYSE Arca', value: 'ARCX' }, ] +export const searchableMarkets: CashOrderMarket[] = ['XTKS', 'XNAS', 'XNYS', 'ARCX'] + +export const tradeRouteIdFromStockId = (id: string) => { + const separator = id.indexOf(':') + if (separator <= 0) return id + + const market = id.slice(0, separator).toUpperCase() + if (!searchableMarkets.includes(market as CashOrderMarket)) return id + return `${market}.${id.slice(separator + 1)}` +} + +export const stockIdFromTradeRouteId = (id: string) => { + const separator = id.indexOf('.') + if (separator <= 0) return id + + const market = id.slice(0, separator).toUpperCase() + if (!searchableMarkets.includes(market as CashOrderMarket)) return id + return `${market}:${id.slice(separator + 1)}` +} + export const sKabuOrderMarketOptions: Array<{ label: string value: CashOrderMarket -}> = [{ label: 'S株', value: 'STK' }] +}> = [] export const cashOrderPriceConditionOptions: Array<{ label: string diff --git a/apps/csbie-ui/src/features/history/HistoryView.vue b/apps/csbie-ui/src/features/history/HistoryView.vue index f1b3ca0..697af42 100644 --- a/apps/csbie-ui/src/features/history/HistoryView.vue +++ b/apps/csbie-ui/src/features/history/HistoryView.vue @@ -1,14 +1,19 @@ diff --git a/apps/csbie-ui/src/features/orders/OrderDialogs.vue b/apps/csbie-ui/src/features/orders/OrderDialogs.vue index 17350f9..7799728 100644 --- a/apps/csbie-ui/src/features/orders/OrderDialogs.vue +++ b/apps/csbie-ui/src/features/orders/OrderDialogs.vue @@ -1,5 +1,6 @@ diff --git a/apps/csbie-ui/src/features/portfolio/PortfolioView.vue b/apps/csbie-ui/src/features/portfolio/PortfolioView.vue index bcee69a..21704fe 100644 --- a/apps/csbie-ui/src/features/portfolio/PortfolioView.vue +++ b/apps/csbie-ui/src/features/portfolio/PortfolioView.vue @@ -1,10 +1,20 @@ diff --git a/apps/csbie-ui/src/features/search/SearchDialog.vue b/apps/csbie-ui/src/features/search/SearchDialog.vue index 0b462c9..a8392e5 100644 --- a/apps/csbie-ui/src/features/search/SearchDialog.vue +++ b/apps/csbie-ui/src/features/search/SearchDialog.vue @@ -1,24 +1,54 @@