diff --git a/apps/csbie-server/src/routes/mcp.ts b/apps/csbie-server/src/routes/mcp.ts index 552ace6..1a1aab5 100644 --- a/apps/csbie-server/src/routes/mcp.ts +++ b/apps/csbie-server/src/routes/mcp.ts @@ -99,8 +99,22 @@ const confirmationIdFromPreview = (value: unknown) => { return typeof confirmationId === 'string' && confirmationId ? confirmationId : undefined } -const accountTypeSchema = z.enum(['general', 'specific', 'nisa', 'juniorNisa', 'unknown']) -const depositTypeSchema = z.enum(['general', 'specific', 'nisa', 'juniorNisa', 'unknown']) +const accountTypeSchema = z.enum([ + 'general', + 'specific', + 'growthInvestment', + 'nisa', + 'juniorNisa', + 'unknown', +]) +const depositTypeSchema = z.enum([ + 'general', + 'specific', + 'growthInvestment', + 'nisa', + 'juniorNisa', + 'unknown', +]) const tradeSideSchema = z.enum(['buy', 'sell']) const marketCodeSchema = z.string().min(1).describe('SBI market code') const issueCodeSchema = z.string().min(1).describe('Issue code') @@ -202,6 +216,9 @@ const cashOrderPriceConditionSchema = z.enum([ ]) const cashOrderSchema = stockOrderBaseSchema.extend({ + preOrderMarket: marketCodeSchema + .optional() + .describe('APK pre-order market for S-kabu; live S-kabu orders still send STK'), price: z.number().positive().optional().describe('Order price for price-based orders'), kind: z.enum(['market', 'limit', 'stop', 'oco', 'ifd', 'ifdo', 's', 'unknown']).optional(), priceCondition: cashOrderPriceConditionSchema @@ -219,21 +236,92 @@ const cashOrderSchema = stockOrderBaseSchema.extend({ .optional() .describe('Secondary execution condition for OCO orders'), secondaryPrice: z.number().positive().optional().describe('Secondary price for OCO orders'), + ippanMarginPaymentLimit: z + .string() + .optional() + .describe('APK ippan margin payment-limit code from board/pre-order information'), sorLastMarket: marketCodeSchema .optional() .describe('Previous market code sent with SOR orders; defaults to login profile'), }) +const cashOrderPreOrderSchema = z.object({ + issueCode: issueCodeSchema, + market: marketCodeSchema, + side: tradeSideSchema, + accountType: accountTypeSchema.optional(), + depositType: depositTypeSchema.optional(), + kind: z.enum(['s']).optional().describe('Requests APK S-kabu pre-order constraints'), + preOrderMarket: marketCodeSchema.optional().describe('APK pre-order market for S-kabu checks'), +}) + +const stockOrderMarginPositionSchema = z.object({ + openTradeDate: z + .string() + .describe('Open trade date from the margin position record, in yyyyMMdd or yyyy-MM-dd format'), + openPrice: z + .union([z.number().positive(), z.string().min(1)]) + .describe('Open price from the margin position record'), + quantity: z + .union([z.number().positive(), z.string().min(1)]) + .describe('Selected quantity from the margin position record'), + orgNewTradeDate: z.string().describe('Original new-trade date from the margin position record'), + bargainMarketCode: marketCodeSchema.describe( + 'Bargain market code from the margin position record', + ), +}) + const placeCashOrderSchema = cashOrderSchema.extend({ confirmationId: z .string() .optional() .describe('Confirmation ID returned by the confirmation step'), + omitConfirmation: z + .boolean() + .optional() + .describe('APK confirmation-screen omission flag for live submit calls'), allowTrading: z.literal(true).optional().describe('Explicitly allows sending a live order'), }) const orderCorrectionSchema = z.object({ + orderNumber: z + .string() + .min(1) + .optional() + .describe('Order number shown in order inquiry; required by the mobile pre-correction route'), orderId: orderIdSchema, + issueCode: issueCodeSchema.optional().describe('Issue code from the pre-correction response'), + market: marketCodeSchema.optional().describe('Market code from the pre-correction response'), + tradeId: z.string().min(1).optional().describe('Original trade id code'), + correctionType: z.string().min(1).optional().describe('Additional correction flag'), + status: z.string().min(1).optional().describe('Original order status code'), + rbeOrderStatus: z.string().min(1).optional().describe('Original RBE order status code'), + depositTypeText: z.string().min(1).optional().describe('Display deposit type text'), + orderMethod: z.enum(['normal', 'stop', 'oco']).optional().describe('Primary order method'), + priceCondition: cashOrderPriceConditionSchema + .optional() + .describe('Corrected primary execution condition'), + triggerZone: z.enum(['above', 'below']).optional().describe('Stop trigger direction'), + triggerPrice: z.number().positive().optional().describe('Stop trigger price'), + secondaryPriceCondition: cashOrderPriceConditionSchema + .optional() + .describe('Secondary/OCO execution condition'), + secondaryPrice: z.number().positive().optional().describe('Secondary/OCO price'), + ifdPriceCondition: cashOrderPriceConditionSchema + .optional() + .describe('IFD follow-up execution condition for IF/IFDOCO correction'), + ifdPrice: z.number().positive().optional().describe('IFD follow-up price'), + ifdOrderMethod: z + .enum(['normal', 'stop', 'oco']) + .optional() + .describe('IFD follow-up special order method'), + ifdTriggerZone: z.enum(['above', 'below']).optional().describe('IFD stop trigger direction'), + ifdTriggerPrice: z.number().positive().optional().describe('IFD stop trigger price'), + ifdSecondaryPriceCondition: cashOrderPriceConditionSchema + .optional() + .describe('IFD secondary/OCO execution condition'), + ifdSecondaryPrice: z.number().positive().optional().describe('IFD secondary/OCO price'), + correctionControlFlag: z.enum(['1', '2']).optional().describe('Mobile correction control flag'), quantity: z.number().positive().optional().describe('Corrected order quantity'), price: z.number().positive().optional().describe('Corrected order price'), }) @@ -260,13 +348,40 @@ const placeOrderCancelSchema = orderCancelSchema.extend({ .describe('Explicitly allows sending a live cancellation request'), }) -const marginOpenOrderSchema = cashOrderSchema +const marginOpenTradeTypeSchema = z.enum([ + 'standard', + 'generalBuy', + 'generalSellShort', + 'generalSellInventoryLimited', + 'generalSellInventoryUnlimited', + 'day', + 'hyper', +]) + +const marginOpenOrderSchema = cashOrderSchema.extend({ + kind: z.enum(['market', 'limit', 'stop', 'oco', 'ifd', 'ifdo', 'unknown']).optional(), + marginTradeType: marginOpenTradeTypeSchema.describe('APK margin-open trade type'), + ippanMarginPaymentLimit: z + .string() + .optional() + .describe('APK ippan margin payment-limit code from board/pre-order information'), +}) + +const marginOpenOrderPreOrderSchema = stockOrderBaseSchema.omit({ quantity: true }).extend({ + marginTradeType: marginOpenTradeTypeSchema + .optional() + .describe('APK margin-open trade type used by the mobile pre-order request'), +}) const placeMarginOpenOrderSchema = marginOpenOrderSchema.extend({ confirmationId: z .string() .optional() .describe('Confirmation ID returned by the confirmation step'), + omitConfirmation: z + .boolean() + .optional() + .describe('APK confirmation-screen omission flag for live submit calls'), allowTrading: z .literal(true) .optional() @@ -275,9 +390,31 @@ const placeMarginOpenOrderSchema = marginOpenOrderSchema.extend({ const marginCloseOrderSchema = cashOrderSchema.extend({ positionId: positionIdSchema.optional().describe('Position ID to close'), + marginCloseTradeType: z + .enum(['sixMonth', 'noLimit', 'oneDay', 'fifteenDay']) + .describe('APK margin-close trade type'), + marginPositions: z + .array(stockOrderMarginPositionSchema) + .optional() + .describe('Margin position records selected for specified close orders'), + marginClosePositionOrder: z + .enum(['profitFirst', 'lossFirst', 'newestFirst', 'oldestFirst', 'specify']) + .optional() + .describe('APK close-position ordering used by summary close orders'), +}) + +const marginCloseOrderPreOrderSchema = stockOrderBaseSchema.omit({ quantity: true }).extend({ + marginCloseTradeType: z + .enum(['sixMonth', 'noLimit', 'oneDay', 'fifteenDay']) + .optional() + .describe('APK margin-close trade type used by the mobile pre-order request'), }) const placeMarginCloseOrderSchema = marginCloseOrderSchema.extend({ + omitConfirmation: z + .boolean() + .optional() + .describe('APK confirmation-screen omission flag for live submit calls'), allowTrading: z .literal(true) .optional() @@ -293,6 +430,22 @@ const actualDeliveryOrderSchema = z.object({ price: z.number().positive().optional().describe('Order price for price-based requests'), kind: z.enum(['genbiki', 'genwatashi']), positionId: positionIdSchema.optional().describe('Position ID to deliver'), + marginPositions: z + .array(stockOrderMarginPositionSchema) + .optional() + .describe('Margin position records selected for genbiki/genwatashi delivery'), + ippanMarginPaymentLimit: z + .string() + .optional() + .describe('APK ippan margin payment-limit code from board/pre-order information'), +}) + +const actualDeliveryOrderPreOrderSchema = actualDeliveryOrderSchema.omit({ + quantity: true, + price: true, + positionId: true, + marginPositions: true, + ippanMarginPaymentLimit: true, }) const placeActualDeliveryOrderSchema = actualDeliveryOrderSchema.extend({ @@ -300,6 +453,10 @@ const placeActualDeliveryOrderSchema = actualDeliveryOrderSchema.extend({ .string() .optional() .describe('Confirmation ID returned by the confirmation step'), + omitConfirmation: z + .boolean() + .optional() + .describe('APK confirmation-screen omission flag for live submit calls'), allowTrading: z .literal(true) .optional() @@ -311,6 +468,39 @@ const ifdOrderSchema = cashOrderSchema.extend({ .enum(['cash', 'marginOpen']) .optional() .describe('Product to use for the first IFD leg'), + marginTradeType: marginOpenTradeTypeSchema + .optional() + .describe('APK margin-open trade type for the first leg when tradeType is marginOpen'), + ippanMarginPaymentLimit: z + .string() + .optional() + .describe('APK ippan margin payment-limit code for the first IFD leg'), + ifdPriceCondition: cashOrderPriceConditionSchema + .optional() + .describe('Execution condition for the IFD follow-up leg'), + ifdPrice: z.number().positive().optional().describe('Order price for the IFD follow-up leg'), + ifdOrderTerm: z.enum(['day', 'week', 'date']).optional().describe('IFD follow-up validity term'), + ifdOrderDate: z + .string() + .optional() + .describe('IFD follow-up validity date in yyyyMMdd or yyyy-MM-dd format'), + ifdOrderMethod: z + .enum(['normal', 'stop', 'oco']) + .optional() + .describe('Special order method for the IFD follow-up leg'), + ifdTriggerZone: z + .enum(['above', 'below']) + .optional() + .describe('Stop trigger direction for the IFD follow-up leg'), + ifdTriggerPrice: z + .number() + .positive() + .optional() + .describe('Stop trigger price for the IFD follow-up leg'), + ifdSecondaryPriceCondition: cashOrderPriceConditionSchema + .optional() + .describe('Secondary OCO execution condition for IFDOCO'), + ifdSecondaryPrice: z.number().positive().optional().describe('Secondary OCO price for IFDOCO'), }) const placeIfdOrderSchema = ifdOrderSchema.extend({ @@ -318,15 +508,58 @@ const placeIfdOrderSchema = ifdOrderSchema.extend({ .string() .optional() .describe('Confirmation ID returned by the confirmation step'), + omitConfirmation: z + .boolean() + .optional() + .describe('APK confirmation-screen omission flag for live submit calls'), allowTrading: z.literal(true).optional().describe('Explicitly allows sending a live IFD order'), }) const themeInvestmentOrderSchema = z.object({ themeId: z.string().min(1).describe('Theme ID for the theme investment order'), + themeSetYyyymm: z + .string() + .length(6) + .describe('Theme set year/month (`theme_set_yyyymm`) from the mobile APK handoff'), + themeCourse: z + .union([z.number().int().nonnegative(), z.string().min(1).max(2)]) + .describe('Theme course (`theme_course`) from the mobile APK handoff'), side: tradeSideSchema, + accountType: accountTypeSchema.optional().describe('Account/deposit type used for the order'), + depositType: accountTypeSchema.optional().describe('Deposit type used for the order'), + components: z + .array( + z.object({ + issueCode: issueCodeSchema, + quantity: z.union([z.number().positive(), z.string().min(1)]), + }), + ) + .min(1) + .max(10) + .describe('Component stock orders selected by the mobile theme investment flow'), amount: z.number().positive().optional().describe('Order amount for the theme investment order'), }) +const themeInvestmentPreOrderSchema = z.object({ + themeId: z.string().min(1).describe('Theme ID from the mobile theme investment handoff'), + themeName: z + .string() + .min(1) + .optional() + .describe('Theme name from the mobile theme investment handoff'), + exchangeCode: marketCodeSchema.describe('Exchange code used by the mobile pre-order call'), + components: z + .array( + z.object({ + issueCode: issueCodeSchema, + quantity: z.union([z.number().positive(), z.string().min(1)]).optional(), + }), + ) + .min(1) + .max(10) + .describe('Component stocks selected by the mobile theme investment flow'), +}) + const placeThemeInvestmentOrderSchema = themeInvestmentOrderSchema.extend({ allowTrading: z .literal(true) @@ -366,6 +599,7 @@ const methodParamSchemas = { 'watchlist.list': undefined, 'orders.inquiry.executionsToday': orderInquiryOptionsSchema.optional(), 'orders.inquiry.open': orderInquiryOptionsSchema.optional(), + 'orders.cash.preOrder': cashOrderPreOrderSchema, 'orders.cash.estimate': cashOrderSchema, 'orders.cash.place': placeCashOrderSchema, 'orders.cash.estimateCorrection': orderCorrectionSchema, @@ -373,23 +607,26 @@ const methodParamSchemas = { 'orders.cash.placeCorrection': placeOrderCorrectionSchema, 'orders.cash.estimateCancel': orderCancelSchema, 'orders.cash.placeCancel': placeOrderCancelSchema, + 'orders.margin.preOrderOpen': marginOpenOrderPreOrderSchema, 'orders.margin.estimateOpen': marginOpenOrderSchema, 'orders.margin.open': placeMarginOpenOrderSchema, + 'orders.margin.preOrderClose': marginCloseOrderPreOrderSchema, 'orders.margin.estimateClose': marginCloseOrderSchema, 'orders.margin.close': placeMarginCloseOrderSchema, 'orders.margin.estimateCloseSummary': marginCloseOrderSchema, 'orders.margin.closeSummary': placeMarginCloseOrderSchema, 'orders.margin.estimateSummary': marginCloseOrderSchema, 'orders.margin.placeSummary': placeMarginCloseOrderSchema, + 'orders.margin.preOrderActualDelivery': actualDeliveryOrderPreOrderSchema, 'orders.margin.estimateActualDelivery': actualDeliveryOrderSchema, 'orders.margin.actualDelivery': placeActualDeliveryOrderSchema, 'orders.ifd.estimate': ifdOrderSchema, 'orders.ifd.place': placeIfdOrderSchema, 'orders.ifd.estimateCorrection': orderCorrectionSchema, 'orders.ifd.placeCorrection': placeOrderCorrectionSchema, - 'orders.ifd.estimateCancel': orderCorrectionSchema, - 'orders.ifd.placeCancel': placeOrderCorrectionSchema, - 'orders.themeInvestment.list': undefined, + 'orders.ifd.estimateCancel': orderCancelSchema, + 'orders.ifd.placeCancel': placeOrderCancelSchema, + 'orders.themeInvestment.list': themeInvestmentPreOrderSchema, 'orders.themeInvestment.estimate': themeInvestmentOrderSchema, 'orders.themeInvestment.place': placeThemeInvestmentOrderSchema, } satisfies Record diff --git a/apps/csbie-server/src/rpc/methods.ts b/apps/csbie-server/src/rpc/methods.ts index 4292389..50d1dab 100644 --- a/apps/csbie-server/src/rpc/methods.ts +++ b/apps/csbie-server/src/rpc/methods.ts @@ -32,6 +32,7 @@ export const RPC_METHODS = [ 'watchlist.list', 'orders.inquiry.executionsToday', 'orders.inquiry.open', + 'orders.cash.preOrder', 'orders.cash.estimate', 'orders.cash.place', 'orders.cash.estimateCorrection', @@ -39,14 +40,17 @@ export const RPC_METHODS = [ 'orders.cash.placeCorrection', 'orders.cash.estimateCancel', 'orders.cash.placeCancel', + 'orders.margin.preOrderOpen', 'orders.margin.estimateOpen', 'orders.margin.open', + 'orders.margin.preOrderClose', 'orders.margin.estimateClose', 'orders.margin.close', 'orders.margin.estimateCloseSummary', 'orders.margin.closeSummary', 'orders.margin.estimateSummary', 'orders.margin.placeSummary', + 'orders.margin.preOrderActualDelivery', 'orders.margin.estimateActualDelivery', 'orders.margin.actualDelivery', 'orders.ifd.estimate', diff --git a/apps/csbie-ui/package.json b/apps/csbie-ui/package.json index fab453d..487e40e 100644 --- a/apps/csbie-ui/package.json +++ b/apps/csbie-ui/package.json @@ -10,6 +10,7 @@ }, "dependencies": { "@simplewebauthn/browser": "^13.1.2", + "d3": "^7.9.0", "lucide-vue-next": "^1.0.0", "motion-v": "^2.3.0", "tailwindcss": "^4.1.10", @@ -19,6 +20,7 @@ }, "devDependencies": { "@tailwindcss/vite": "^4.3.1", + "@types/d3": "^7.4.3", "@vitejs/plugin-vue": "^6.0.7", "typescript": "^5", "vue-tsc": "^3" diff --git a/apps/csbie-ui/src/App.vue b/apps/csbie-ui/src/App.vue index e877783..e34d874 100644 --- a/apps/csbie-ui/src/App.vue +++ b/apps/csbie-ui/src/App.vue @@ -25,7 +25,10 @@ const activeTab = computed(() => { return routeNames.includes(route.name as RouteName) ? (route.name as RouteName) : 'portfolio' }) const showAuthGate = computed(() => true) -const navigate = (name: RouteName) => router.push({ name }) +const navigate = (name: RouteName) => { + if (name === 'settings') return router.push('/settings/api-keys') + return router.push({ name }) +} const { status, @@ -67,6 +70,7 @@ const { quantityInput, priceInput, chartMode, + chartRange, showSearch, searchQuery, countryFilter, @@ -86,6 +90,7 @@ const { orderHistoryNotice, positions, chartPricePoints, + chartNotice, pricePolling, selectedStock, orderQuantity, @@ -93,6 +98,11 @@ const { cashOrderPrimaryRequiresPrice, cashOrderTriggerPrice, cashOrderSecondaryPrice, + cashOrderAccountTypeOptions, + cashOrderMarketOptions, + cashOrderTermOptions, + cashOrderDateOptions, + cashOrderPriceStep, estimatedAmount, showPortfolioSpinner, canRequestCashEstimate, @@ -106,7 +116,6 @@ const { totalAssetValue, stockAssetRatio, cashAssetRatio, - boxPlotStyle, hasQuote, selectStock, connect, @@ -211,17 +220,23 @@ onMounted(async () => { v-model:quantity-input="quantityInput" v-model:price-input="priceInput" v-model:chart-mode="chartMode" + v-model:chart-range="chartRange" :viewed-stocks="viewedStocks" :selected-stock="selectedStock" :selected-position="selectedPosition" :connected="connected" :order-quantity="orderQuantity" :estimated-amount="estimatedAmount" + :cash-order-account-type-options="cashOrderAccountTypeOptions" + :cash-order-market-options="cashOrderMarketOptions" + :cash-order-term-options="cashOrderTermOptions" + :cash-order-date-options="cashOrderDateOptions" + :cash-order-price-step="cashOrderPriceStep" :can-request-cash-estimate="canRequestCashEstimate" :can-place-cash-order="canPlaceCashOrder" :realtime-price-points="chartPricePoints" + :chart-notice="chartNotice" :price-polling="pricePolling" - :box-plot-style="boxPlotStyle" :has-quote="hasQuote" @open-search="showSearch = true" @select-stock="selectStock" diff --git a/apps/csbie-ui/src/components/ApiKeyPolicyEditor.vue b/apps/csbie-ui/src/components/ApiKeyPolicyEditor.vue index 6a4a176..7edfa76 100644 --- a/apps/csbie-ui/src/components/ApiKeyPolicyEditor.vue +++ b/apps/csbie-ui/src/components/ApiKeyPolicyEditor.vue @@ -103,8 +103,8 @@ const methodAllowed = (method: string) => { const ui = { root: 'grid gap-3', - limitGrid: 'grid grid-cols-3 gap-3', - limitGridCompact: 'grid-cols-5', + limitGrid: 'grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3', + limitGridCompact: 'xl:grid-cols-5', label: 'grid gap-2 text-xs font-extrabold text-[#9aa0a9]', input: 'min-h-12 w-full rounded-[16px] border border-[#4a5058] bg-[#111418] px-4 text-[#e3e3e9] outline-none transition focus:border-[#a8c7fa]', @@ -113,7 +113,7 @@ const ui = { actions: 'mt-3 flex flex-wrap gap-2', button: 'min-h-9 rounded-full bg-[#263141] px-4 text-sm font-extrabold text-[#d3e3fd] transition hover:bg-[#303b4d]', - methodGrid: 'mt-3 grid grid-cols-2 gap-2', + methodGrid: 'mt-3 grid grid-cols-1 gap-2 xl:grid-cols-2', methodToggle: 'flex min-h-10 items-center gap-2 rounded-[16px] bg-[#111418] p-3 text-xs text-[#c3c7cf]', checkbox: 'h-4 min-h-4 w-4', @@ -128,7 +128,7 @@ const ui = {